Skip to content

Commit e38fa91

Browse files
committed
net/http: fix TimeoutHandler data races; hold lock longer
The existing lock needed to be held longer. If a timeout occured while writing (but after the guarded timeout check), the writes would clobber a future connection's buffer. Also remove a harmless warning by making Write also set the flag that headers were sent (implicitly), so we don't try to write headers later (a no-op + warning) on timeout after we've started writing. Fixes #8414 Fixes #8209 LGTM=ruiu, adg R=adg, ruiu CC=golang-codereviews https://golang.org/cl/123610043
1 parent 339a24d commit e38fa91

File tree

2 files changed

+81
-5
lines changed

2 files changed

+81
-5
lines changed

src/pkg/net/http/serve_test.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515
"io"
1616
"io/ioutil"
1717
"log"
18+
"math/rand"
1819
"net"
1920
. "net/http"
2021
"net/http/httptest"
@@ -1188,6 +1189,82 @@ func TestTimeoutHandler(t *testing.T) {
11881189
}
11891190
}
11901191

1192+
// See issues 8209 and 8414.
1193+
func TestTimeoutHandlerRace(t *testing.T) {
1194+
defer afterTest(t)
1195+
1196+
delayHi := HandlerFunc(func(w ResponseWriter, r *Request) {
1197+
ms, _ := strconv.Atoi(r.URL.Path[1:])
1198+
if ms == 0 {
1199+
ms = 1
1200+
}
1201+
for i := 0; i < ms; i++ {
1202+
w.Write([]byte("hi"))
1203+
time.Sleep(time.Millisecond)
1204+
}
1205+
})
1206+
1207+
ts := httptest.NewServer(TimeoutHandler(delayHi, 20*time.Millisecond, ""))
1208+
defer ts.Close()
1209+
1210+
var wg sync.WaitGroup
1211+
gate := make(chan bool, 10)
1212+
n := 50
1213+
if testing.Short() {
1214+
n = 10
1215+
gate = make(chan bool, 3)
1216+
}
1217+
for i := 0; i < n; i++ {
1218+
gate <- true
1219+
wg.Add(1)
1220+
go func() {
1221+
defer wg.Done()
1222+
defer func() { <-gate }()
1223+
res, err := Get(fmt.Sprintf("%s/%d", ts.URL, rand.Intn(50)))
1224+
if err == nil {
1225+
io.Copy(ioutil.Discard, res.Body)
1226+
res.Body.Close()
1227+
}
1228+
}()
1229+
}
1230+
wg.Wait()
1231+
}
1232+
1233+
// See issues 8209 and 8414.
1234+
func TestTimeoutHandlerRaceHeader(t *testing.T) {
1235+
defer afterTest(t)
1236+
1237+
delay204 := HandlerFunc(func(w ResponseWriter, r *Request) {
1238+
w.WriteHeader(204)
1239+
})
1240+
1241+
ts := httptest.NewServer(TimeoutHandler(delay204, time.Nanosecond, ""))
1242+
defer ts.Close()
1243+
1244+
var wg sync.WaitGroup
1245+
gate := make(chan bool, 50)
1246+
n := 500
1247+
if testing.Short() {
1248+
n = 10
1249+
}
1250+
for i := 0; i < n; i++ {
1251+
gate <- true
1252+
wg.Add(1)
1253+
go func() {
1254+
defer wg.Done()
1255+
defer func() { <-gate }()
1256+
res, err := Get(ts.URL)
1257+
if err != nil {
1258+
t.Error(err)
1259+
return
1260+
}
1261+
defer res.Body.Close()
1262+
io.Copy(ioutil.Discard, res.Body)
1263+
}()
1264+
}
1265+
wg.Wait()
1266+
}
1267+
11911268
// Verifies we don't path.Clean() on the wrong parts in redirects.
11921269
func TestRedirectMunging(t *testing.T) {
11931270
req, _ := NewRequest("GET", "http://example.com/", nil)

src/pkg/net/http/server.go

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1916,22 +1916,21 @@ func (tw *timeoutWriter) Header() Header {
19161916

19171917
func (tw *timeoutWriter) Write(p []byte) (int, error) {
19181918
tw.mu.Lock()
1919-
timedOut := tw.timedOut
1920-
tw.mu.Unlock()
1921-
if timedOut {
1919+
defer tw.mu.Unlock()
1920+
tw.wroteHeader = true // implicitly at least
1921+
if tw.timedOut {
19221922
return 0, ErrHandlerTimeout
19231923
}
19241924
return tw.w.Write(p)
19251925
}
19261926

19271927
func (tw *timeoutWriter) WriteHeader(code int) {
19281928
tw.mu.Lock()
1929+
defer tw.mu.Unlock()
19291930
if tw.timedOut || tw.wroteHeader {
1930-
tw.mu.Unlock()
19311931
return
19321932
}
19331933
tw.wroteHeader = true
1934-
tw.mu.Unlock()
19351934
tw.w.WriteHeader(code)
19361935
}
19371936

0 commit comments

Comments
 (0)