Processor.executeHandler (protocol/h2/stream/processor.go, about line 787 on main a73afb6) reads a stream's OutboundBuffer without the stream's lock after the handler has returned:
if state == StateOpen || state == StateHalfClosedRemote {
if stream.OutboundBuffer != nil && stream.OutboundBuffer.Len() > 0 {
return
}
It runs on a shared worker pool goroutine (an async route). A response larger than the client's flow-control window leaves the rest of its DATA in OutboundBuffer, and the connection's event loop, handling the client's WINDOW_UPDATE, sends it and resets the buffer under s.mu (flushStreamOutbound, and the SETTINGS path in ProcessFrame). Every other access of the buffer takes s.mu, including the deferred check a few lines below this one. The race detector caught it in #805's TestLargeResponseIsDeliveredH2/adaptive/async-route/16777216 (16 MiB to net/http's h2c client; linux/arm64 Docker, 4 CPUs, 8 MiB memlock, -race): write by flushStreamOutbound → bytes.(*Buffer).Reset on the epoll loop, previous read by executeHandler → bytes.(*Buffer).Len on the pool goroutine. Log: evidence/lanes-20260927/WRITE/761/suites/m8-engine_epoll_engine_iouring_internal_conn-basea73afb6-head98ba46d.log (section head, "WARNING: DATA RACE"). It is timing-dependent: the loop has to reset the buffer between the handler's return and the pool goroutine's deferred, locked, check.
Fix: read it under stream.mu.RLock, as the deferred check does. Fixed in #805, whose test finds it.
Processor.executeHandler(protocol/h2/stream/processor.go, about line 787 on main a73afb6) reads a stream'sOutboundBufferwithout the stream's lock after the handler has returned:It runs on a shared worker pool goroutine (an async route). A response larger than the client's flow-control window leaves the rest of its DATA in
OutboundBuffer, and the connection's event loop, handling the client's WINDOW_UPDATE, sends it and resets the buffer unders.mu(flushStreamOutbound, and the SETTINGS path inProcessFrame). Every other access of the buffer takess.mu, including the deferred check a few lines below this one. The race detector caught it in #805'sTestLargeResponseIsDeliveredH2/adaptive/async-route/16777216(16 MiB to net/http's h2c client; linux/arm64 Docker, 4 CPUs, 8 MiB memlock,-race): write byflushStreamOutbound→bytes.(*Buffer).Reseton the epoll loop, previous read byexecuteHandler→bytes.(*Buffer).Lenon the pool goroutine. Log:evidence/lanes-20260927/WRITE/761/suites/m8-engine_epoll_engine_iouring_internal_conn-basea73afb6-head98ba46d.log(section head, "WARNING: DATA RACE"). It is timing-dependent: the loop has to reset the buffer between the handler's return and the pool goroutine's deferred, locked, check.Fix: read it under
stream.mu.RLock, as the deferred check does. Fixed in #805, whose test finds it.