diff --git a/src/context/context.go b/src/context/context.go index 4f150f6a1d..24bb18abd3 100644 --- a/src/context/context.go +++ b/src/context/context.go @@ -463,6 +463,8 @@ func (c *cancelCtx) Done() <-chan struct{} { func (c *cancelCtx) Err() error { // An atomic load is ~5x faster than a mutex, which can matter in tight loops. if err := c.err.Load(); err != nil { + // Ensure the done channel has been closed before returning a non-nil error. + <-c.Done() return err.(error) } return nil diff --git a/src/context/x_test.go b/src/context/x_test.go index 937cab1445..0cf19688c3 100644 --- a/src/context/x_test.go +++ b/src/context/x_test.go @@ -1177,3 +1177,23 @@ func (c *customContext) Err() error { func (c *customContext) Value(key any) any { return c.parent.Value(key) } + +// Issue #75533. +func TestContextErrDoneRace(t *testing.T) { + // 4 iterations reliably reproduced #75533. + for range 10 { + ctx, cancel := WithCancel(Background()) + donec := ctx.Done() + go cancel() + for ctx.Err() == nil { + if runtime.GOARCH == "wasm" { + runtime.Gosched() // need to explicitly yield + } + } + select { + case <-donec: + default: + t.Fatalf("ctx.Err is non-nil, but ctx.Done is not closed") + } + } +}