Skip to content

Commit 7b7f666

Browse files
committed
extproc: reject invalid server flow control window increments
1 parent 9f80274 commit 7b7f666

2 files changed

Lines changed: 145 additions & 26 deletions

File tree

internal/xds/httpfilter/extproc/ext_proc.go

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"errors"
2525
"fmt"
2626
"io"
27+
"math"
2728
"slices"
2829
"strings"
2930
"sync"
@@ -1513,33 +1514,13 @@ func (cs *clientStream) recvFromProcServerLoop(newStream func(context.Context, .
15131514
}
15141515

15151516
if windowUpdate := resp.GetServerWindowUpdate(); windowUpdate != nil {
1516-
deltaDownstreamToSidestream := windowUpdate.GetWindowIncrementDownstreamToSidestream()
1517-
if deltaDownstreamToSidestream != 0 {
1518-
// Add the window update to the existing window.
1519-
newQuota := cs.downstreamToSidestreamWindow.Add(deltaDownstreamToSidestream)
1520-
previousQuota := newQuota - deltaDownstreamToSidestream
1521-
// If the window turned from negative to positive, send a signal to the
1522-
// `acquireDownstreamToSidestreamWindow` function to wake it up.
1523-
if previousQuota <= 0 && newQuota > 0 {
1524-
select {
1525-
case cs.downstreamToSidestreamPositiveUpdateCh <- struct{}{}:
1526-
default:
1527-
}
1528-
}
1517+
if err := applyServerWindowUpdate(&cs.downstreamToSidestreamWindow, cs.downstreamToSidestreamPositiveUpdateCh, windowUpdate.GetWindowIncrementDownstreamToSidestream()); err != nil {
1518+
cs.failProcStream(fmt.Errorf("external processor sent an invalid downstream-to-sidestream window update: %v", err))
1519+
return
15291520
}
1530-
deltaUpstreamToSidestream := windowUpdate.GetWindowIncrementUpstreamToSidestream()
1531-
if deltaUpstreamToSidestream != 0 {
1532-
// Add the window update to the existing window.
1533-
newQuota := cs.upstreamToSidestreamWindow.Add(deltaUpstreamToSidestream)
1534-
previousQuota := newQuota - deltaUpstreamToSidestream
1535-
// If the window turned from negative to positive, send a signal to the
1536-
// `acquireUpstreamToSidestreamWindow` function to wake it up.
1537-
if previousQuota <= 0 && newQuota > 0 {
1538-
select {
1539-
case cs.upstreamToSidestreamPositiveUpdateCh <- struct{}{}:
1540-
default:
1541-
}
1542-
}
1521+
if err := applyServerWindowUpdate(&cs.upstreamToSidestreamWindow, cs.upstreamToSidestreamPositiveUpdateCh, windowUpdate.GetWindowIncrementUpstreamToSidestream()); err != nil {
1522+
cs.failProcStream(fmt.Errorf("external processor sent an invalid upstream-to-sidestream window update: %v", err))
1523+
return
15431524
}
15441525
}
15451526

@@ -2057,6 +2038,43 @@ func (cs *clientStream) waitForTrailerProcessing(recvErr error) error {
20572038
}
20582039
}
20592040

2041+
// applyServerWindowUpdate adds a flow control window increment received from
2042+
// the external processor to window, waking a blocked acquirer if the window
2043+
// becomes positive. The increment is server-controlled, so it must be
2044+
// non-negative and must not overflow the int64 window; an increment that
2045+
// violates either is a protocol error and returns one instead of wrapping the
2046+
// accounting, matching how HTTP/2 flow control rejects a window update that
2047+
// exceeds the maximum. This recv loop is the only writer that grows the
2048+
// window, so the compare-and-swap only contends with a concurrent deduction in
2049+
// acquire{Downstream,Upstream}ToSidestreamWindow.
2050+
func applyServerWindowUpdate(window *atomic.Int64, positiveUpdateCh chan struct{}, delta int64) error {
2051+
if delta == 0 {
2052+
return nil
2053+
}
2054+
if delta < 0 {
2055+
return fmt.Errorf("negative window increment %d", delta)
2056+
}
2057+
for {
2058+
previousQuota := window.Load()
2059+
if previousQuota > math.MaxInt64-delta {
2060+
return fmt.Errorf("window increment %d overflows the current window %d", delta, previousQuota)
2061+
}
2062+
newQuota := previousQuota + delta
2063+
if !window.CompareAndSwap(previousQuota, newQuota) {
2064+
continue
2065+
}
2066+
// If the window turned from non-positive to positive, wake the acquirer
2067+
// blocked in acquire{Downstream,Upstream}ToSidestreamWindow.
2068+
if previousQuota <= 0 && newQuota > 0 {
2069+
select {
2070+
case positiveUpdateCh <- struct{}{}:
2071+
default:
2072+
}
2073+
}
2074+
return nil
2075+
}
2076+
}
2077+
20602078
// acquireDownstreamToSidestreamWindow checks available flow control window for
20612079
// downstream to sidestream. If window is positive, it deducts bodySize and
20622080
// returns true immediately. If window is <= 0, it blocks until a positive
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
/*
2+
*
3+
* Copyright 2026 gRPC authors.
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*
17+
*/
18+
19+
package extproc
20+
21+
import (
22+
"math"
23+
"sync/atomic"
24+
"testing"
25+
26+
iextproc "google.golang.org/grpc/internal/xds/httpfilter/extproc/internal"
27+
)
28+
29+
// TestApplyServerWindowUpdate verifies that a flow control window increment
30+
// from the external processor is applied only when it is a non-negative value
31+
// that does not overflow the window. An out-of-range increment must be
32+
// rejected without mutating the window, so the accounting cannot wrap to a
33+
// value that permanently blocks acquireDownstreamToSidestreamWindow.
34+
func TestApplyServerWindowUpdate(t *testing.T) {
35+
tests := []struct {
36+
name string
37+
start int64
38+
delta int64
39+
wantErr bool
40+
wantWindow int64
41+
wantSignal bool
42+
}{
43+
{
44+
name: "zero increment is a no-op",
45+
start: iextproc.DefaultFlowControlWindowSize,
46+
delta: 0,
47+
wantWindow: iextproc.DefaultFlowControlWindowSize,
48+
},
49+
{
50+
name: "valid increment grows the window",
51+
start: iextproc.DefaultFlowControlWindowSize,
52+
delta: 1024,
53+
wantWindow: iextproc.DefaultFlowControlWindowSize + 1024,
54+
},
55+
{
56+
name: "increment that crosses zero signals the acquirer",
57+
start: -1024,
58+
delta: 2048,
59+
wantWindow: 1024,
60+
wantSignal: true,
61+
},
62+
{
63+
name: "overflowing increment is rejected and leaves the window unchanged",
64+
start: iextproc.DefaultFlowControlWindowSize,
65+
delta: math.MaxInt64,
66+
wantErr: true,
67+
wantWindow: iextproc.DefaultFlowControlWindowSize,
68+
},
69+
{
70+
name: "negative increment is rejected and leaves the window unchanged",
71+
start: iextproc.DefaultFlowControlWindowSize,
72+
delta: -1,
73+
wantErr: true,
74+
wantWindow: iextproc.DefaultFlowControlWindowSize,
75+
},
76+
}
77+
for _, test := range tests {
78+
t.Run(test.name, func(t *testing.T) {
79+
var window atomic.Int64
80+
window.Store(test.start)
81+
ch := make(chan struct{}, 1)
82+
83+
err := applyServerWindowUpdate(&window, ch, test.delta)
84+
if (err != nil) != test.wantErr {
85+
t.Fatalf("applyServerWindowUpdate(%d, %d) error = %v, wantErr %v", test.start, test.delta, err, test.wantErr)
86+
}
87+
if got := window.Load(); got != test.wantWindow {
88+
t.Errorf("window = %d, want %d", got, test.wantWindow)
89+
}
90+
gotSignal := false
91+
select {
92+
case <-ch:
93+
gotSignal = true
94+
default:
95+
}
96+
if gotSignal != test.wantSignal {
97+
t.Errorf("positive-update signal = %v, want %v", gotSignal, test.wantSignal)
98+
}
99+
})
100+
}
101+
}

0 commit comments

Comments
 (0)