From: Nicholas S. Husin Date: Sat, 1 Nov 2025 15:15:58 +0000 (-0400) Subject: runtime: prevent time.Timer.Reset(0) from deadlocking testing/synctest tests X-Git-Tag: go1.26rc1~408 X-Git-Url: http://www.git.cypherpunks.su/?a=commitdiff_plain;h=385dc33250;p=gostls13.git runtime: prevent time.Timer.Reset(0) from deadlocking testing/synctest tests In Go 1.23+, timer channels behave synchronously. When we have a timer channel (i.e. !async && t.isChan) we would lock the runtime.timer.sendLock mutex at the beginning of runtime.timer.modify()'s execution. Calling time.Timer.Reset(0) within a testing/synctest test, unfortunately, causes it to hang indefinitely. This is because the runtime.timer.sendLock mutex ends up being locked twice before it could be unlocked: - When calling time.Timer.Reset(), runtime.timer.modify() would lock the mutex per usual. - Due to the 0 argument, runtime.timer.modify() would also try to execute the bubbled timer immediately rather than adding them to a heap. However, in doing so, it uses runtime.timer.unlockAndRun(), which also locks the same mutex. This CL solves this issue by making sure that a locked runtime.timer.sendLock mutex is unlocked first, whenever we try to execute bubbled timer immediately in the stack. Fixes #76052 Change-Id: I66429b9bf6971400de95dcf2d5dc9670c3135492 Reviewed-on: https://go-review.googlesource.com/c/go/+/716883 Reviewed-by: Damien Neil Auto-Submit: Nicholas Husin Reviewed-by: Nicholas Husin LUCI-TryBot-Result: Go LUCI --- diff --git a/src/internal/synctest/synctest_test.go b/src/internal/synctest/synctest_test.go index 73a0a1c453..d5ac1e5c1f 100644 --- a/src/internal/synctest/synctest_test.go +++ b/src/internal/synctest/synctest_test.go @@ -5,6 +5,7 @@ package synctest_test import ( + "context" "fmt" "internal/synctest" "internal/testenv" @@ -329,6 +330,31 @@ func TestAfterFuncRunsImmediately(t *testing.T) { }) } +// TestTimerResetZeroDoNotHang verifies that using timer.Reset(0) does not +// cause the test to hang indefinitely. See https://go.dev/issue/76052. +func TestTimerResetZeroDoNotHang(t *testing.T) { + synctest.Run(func() { + timer := time.NewTimer(0) + ctx, cancel := context.WithCancel(context.Background()) + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-timer.C: + } + } + }() + + synctest.Wait() + timer.Reset(0) + synctest.Wait() + cancel() + synctest.Wait() + }) +} + func TestChannelFromOutsideBubble(t *testing.T) { choutside := make(chan struct{}) for _, test := range []struct { diff --git a/src/runtime/time.go b/src/runtime/time.go index e9d1f0b6c9..81a4c6b79f 100644 --- a/src/runtime/time.go +++ b/src/runtime/time.go @@ -636,6 +636,9 @@ func (t *timer) modify(when, period int64, f func(arg any, seq uintptr, delay in } if t.state&timerHeaped == 0 && when <= bubble.now { systemstack(func() { + if !async && t.isChan { + unlock(&t.sendLock) + } t.unlockAndRun(bubble.now, bubble) }) return pending