Skip to content

Commit 2c8d2a0

Browse files
stevenhBryan C. Mills
authored and
Bryan C. Mills
committed
net/http: fix data race due to writeLoop goroutine left running
Fix a data race for clients that mutate requests after receiving a response error which is caused by the writeLoop goroutine left running, this can be seen on cancelled requests. Fixes #37669 Change-Id: Ia4743c6b8abde3a7503de362cc6a3782e19e7f60 Reviewed-on: https://go-review.googlesource.com/c/go/+/251858 Reviewed-by: Bryan C. Mills <[email protected]> Run-TryBot: Bryan C. Mills <[email protected]> TryBot-Result: Gobot Gobot <[email protected]>
1 parent 015a5a5 commit 2c8d2a0

File tree

2 files changed

+108
-1
lines changed

2 files changed

+108
-1
lines changed

src/net/http/transport.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -1967,6 +1967,15 @@ func (pc *persistConn) mapRoundTripError(req *transportRequest, startBytesWritte
19671967
return nil
19681968
}
19691969

1970+
// Wait for the writeLoop goroutine to terminate to avoid data
1971+
// races on callers who mutate the request on failure.
1972+
//
1973+
// When resc in pc.roundTrip and hence rc.ch receives a responseAndError
1974+
// with a non-nil error it implies that the persistConn is either closed
1975+
// or closing. Waiting on pc.writeLoopDone is hence safe as all callers
1976+
// close closech which in turn ensures writeLoop returns.
1977+
<-pc.writeLoopDone
1978+
19701979
// If the request was canceled, that's better than network
19711980
// failures that were likely the result of tearing down the
19721981
// connection.
@@ -1992,7 +2001,6 @@ func (pc *persistConn) mapRoundTripError(req *transportRequest, startBytesWritte
19922001
return err
19932002
}
19942003
if pc.isBroken() {
1995-
<-pc.writeLoopDone
19962004
if pc.nwrite == startBytesWritten {
19972005
return nothingWrittenError{err}
19982006
}

src/net/http/transport_test.go

+99
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import (
2525
"io"
2626
"io/ioutil"
2727
"log"
28+
mrand "math/rand"
2829
"net"
2930
. "net/http"
3031
"net/http/httptest"
@@ -6284,3 +6285,101 @@ func TestTransportRejectsSignInContentLength(t *testing.T) {
62846285
t.Fatalf("Error mismatch\nGot: %q\nWanted substring: %q", got, want)
62856286
}
62866287
}
6288+
6289+
// dumpConn is a net.Conn which writes to Writer and reads from Reader
6290+
type dumpConn struct {
6291+
io.Writer
6292+
io.Reader
6293+
}
6294+
6295+
func (c *dumpConn) Close() error { return nil }
6296+
func (c *dumpConn) LocalAddr() net.Addr { return nil }
6297+
func (c *dumpConn) RemoteAddr() net.Addr { return nil }
6298+
func (c *dumpConn) SetDeadline(t time.Time) error { return nil }
6299+
func (c *dumpConn) SetReadDeadline(t time.Time) error { return nil }
6300+
func (c *dumpConn) SetWriteDeadline(t time.Time) error { return nil }
6301+
6302+
// delegateReader is a reader that delegates to another reader,
6303+
// once it arrives on a channel.
6304+
type delegateReader struct {
6305+
c chan io.Reader
6306+
r io.Reader // nil until received from c
6307+
}
6308+
6309+
func (r *delegateReader) Read(p []byte) (int, error) {
6310+
if r.r == nil {
6311+
var ok bool
6312+
if r.r, ok = <-r.c; !ok {
6313+
return 0, errors.New("delegate closed")
6314+
}
6315+
}
6316+
return r.r.Read(p)
6317+
}
6318+
6319+
func testTransportRace(req *Request) {
6320+
save := req.Body
6321+
pr, pw := io.Pipe()
6322+
defer pr.Close()
6323+
defer pw.Close()
6324+
dr := &delegateReader{c: make(chan io.Reader)}
6325+
6326+
t := &Transport{
6327+
Dial: func(net, addr string) (net.Conn, error) {
6328+
return &dumpConn{pw, dr}, nil
6329+
},
6330+
}
6331+
defer t.CloseIdleConnections()
6332+
6333+
quitReadCh := make(chan struct{})
6334+
// Wait for the request before replying with a dummy response:
6335+
go func() {
6336+
defer close(quitReadCh)
6337+
6338+
req, err := ReadRequest(bufio.NewReader(pr))
6339+
if err == nil {
6340+
// Ensure all the body is read; otherwise
6341+
// we'll get a partial dump.
6342+
io.Copy(ioutil.Discard, req.Body)
6343+
req.Body.Close()
6344+
}
6345+
select {
6346+
case dr.c <- strings.NewReader("HTTP/1.1 204 No Content\r\nConnection: close\r\n\r\n"):
6347+
case quitReadCh <- struct{}{}:
6348+
// Ensure delegate is closed so Read doesn't block forever.
6349+
close(dr.c)
6350+
}
6351+
}()
6352+
6353+
t.RoundTrip(req)
6354+
6355+
// Ensure the reader returns before we reset req.Body to prevent
6356+
// a data race on req.Body.
6357+
pw.Close()
6358+
<-quitReadCh
6359+
6360+
req.Body = save
6361+
}
6362+
6363+
// Issue 37669
6364+
// Test that a cancellation doesn't result in a data race due to the writeLoop
6365+
// goroutine being left running, if the caller mutates the processed Request
6366+
// upon completion.
6367+
func TestErrorWriteLoopRace(t *testing.T) {
6368+
if testing.Short() {
6369+
return
6370+
}
6371+
t.Parallel()
6372+
for i := 0; i < 1000; i++ {
6373+
delay := time.Duration(mrand.Intn(5)) * time.Millisecond
6374+
ctx, cancel := context.WithTimeout(context.Background(), delay)
6375+
defer cancel()
6376+
6377+
r := bytes.NewBuffer(make([]byte, 10000))
6378+
req, err := NewRequestWithContext(ctx, MethodPost, "http://example.com", r)
6379+
if err != nil {
6380+
t.Fatal(err)
6381+
}
6382+
6383+
testTransportRace(req)
6384+
}
6385+
}

0 commit comments

Comments
 (0)