Skip to content

Commit 960654b

Browse files
neildgopherbot
authored andcommittedFeb 27, 2024
net/http/httputil: avoid ReverseProxy data race on 1xx response and error
ReverseProxy uses a httptrace.ClientTrace.Got1xxResponse trace hook to capture 1xx response headers for proxying. This hook can be called asynchrnously after RoundTrip returns. (This should only happen when RoundTrip has failed for some reason.) Add synchronization so we don't attempt to modifying the ResponseWriter headers map from the hook after another goroutine has begun making use of it. Fixes #65123 Change-Id: I8b7ecb1a140f7ba7e37b9d27b8a20bca41a118b1 Reviewed-on: https://go-review.googlesource.com/c/go/+/567216 LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Jonathan Amsterdam <[email protected]> Auto-Submit: Damien Neil <[email protected]>
1 parent d3e827d commit 960654b

File tree

2 files changed

+81
-0
lines changed

2 files changed

+81
-0
lines changed
 

‎src/net/http/httputil/reverseproxy.go

+14
Original file line numberDiff line numberDiff line change
@@ -454,8 +454,19 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
454454
outreq.Header.Set("User-Agent", "")
455455
}
456456

457+
var (
458+
roundTripMutex sync.Mutex
459+
roundTripDone bool
460+
)
457461
trace := &httptrace.ClientTrace{
458462
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
463+
roundTripMutex.Lock()
464+
defer roundTripMutex.Unlock()
465+
if roundTripDone {
466+
// If RoundTrip has returned, don't try to further modify
467+
// the ResponseWriter's header map.
468+
return nil
469+
}
459470
h := rw.Header()
460471
copyHeader(h, http.Header(header))
461472
rw.WriteHeader(code)
@@ -468,6 +479,9 @@ func (p *ReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
468479
outreq = outreq.WithContext(httptrace.WithClientTrace(outreq.Context(), trace))
469480

470481
res, err := transport.RoundTrip(outreq)
482+
roundTripMutex.Lock()
483+
roundTripDone = true
484+
roundTripMutex.Unlock()
471485
if err != nil {
472486
p.getErrorHandler()(rw, outreq, err)
473487
return

‎src/net/http/httputil/reverseproxy_test.go

+67
Original file line numberDiff line numberDiff line change
@@ -1687,6 +1687,47 @@ func TestReverseProxyRewriteReplacesOut(t *testing.T) {
16871687
}
16881688
}
16891689

1690+
func Test1xxHeadersNotModifiedAfterRoundTrip(t *testing.T) {
1691+
// https://go.dev/issue/65123: We use httptrace.Got1xxResponse to capture 1xx responses
1692+
// and proxy them. httptrace handlers can execute after RoundTrip returns, in particular
1693+
// after experiencing connection errors. When this happens, we shouldn't modify the
1694+
// ResponseWriter headers after ReverseProxy.ServeHTTP returns.
1695+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
1696+
for i := 0; i < 5; i++ {
1697+
w.WriteHeader(103)
1698+
}
1699+
}))
1700+
defer backend.Close()
1701+
backendURL, err := url.Parse(backend.URL)
1702+
if err != nil {
1703+
t.Fatal(err)
1704+
}
1705+
proxyHandler := NewSingleHostReverseProxy(backendURL)
1706+
proxyHandler.ErrorLog = log.New(io.Discard, "", 0) // quiet for tests
1707+
1708+
rw := &testResponseWriter{}
1709+
func() {
1710+
// Cancel the request (and cause RoundTrip to return) immediately upon
1711+
// seeing a 1xx response.
1712+
ctx, cancel := context.WithCancel(context.Background())
1713+
defer cancel()
1714+
ctx = httptrace.WithClientTrace(ctx, &httptrace.ClientTrace{
1715+
Got1xxResponse: func(code int, header textproto.MIMEHeader) error {
1716+
cancel()
1717+
return nil
1718+
},
1719+
})
1720+
1721+
req, _ := http.NewRequestWithContext(ctx, "GET", "http://go.dev/", nil)
1722+
proxyHandler.ServeHTTP(rw, req)
1723+
}()
1724+
// Trigger data race while iterating over response headers.
1725+
// When run with -race, this causes the condition in https://go.dev/issue/65123 often
1726+
// enough to detect reliably.
1727+
for _ = range rw.Header() {
1728+
}
1729+
}
1730+
16901731
func Test1xxResponses(t *testing.T) {
16911732
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
16921733
h := w.Header()
@@ -1861,3 +1902,29 @@ func testReverseProxyQueryParameterSmuggling(t *testing.T, wantCleanQuery bool,
18611902
}
18621903
}
18631904
}
1905+
1906+
type testResponseWriter struct {
1907+
h http.Header
1908+
writeHeader func(int)
1909+
write func([]byte) (int, error)
1910+
}
1911+
1912+
func (rw *testResponseWriter) Header() http.Header {
1913+
if rw.h == nil {
1914+
rw.h = make(http.Header)
1915+
}
1916+
return rw.h
1917+
}
1918+
1919+
func (rw *testResponseWriter) WriteHeader(statusCode int) {
1920+
if rw.writeHeader != nil {
1921+
rw.writeHeader(statusCode)
1922+
}
1923+
}
1924+
1925+
func (rw *testResponseWriter) Write(p []byte) (int, error) {
1926+
if rw.write != nil {
1927+
return rw.write(p)
1928+
}
1929+
return len(p), nil
1930+
}

0 commit comments

Comments
 (0)
Please sign in to comment.