This is an automated email from the ASF dual-hosted git repository. sjanc pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/mynewt-newtmgr.git
commit 281ad880e58cf10bf5a206ebad545f8a2d10a4cd Author: Szymon Janc <[email protected]> AuthorDate: Thu Oct 22 00:23:36 2026 +0200 nmxact/nmcoap: Fix race between listener timeout and Close Commit 827bef2 tried to stop the AfterTimeout callback from writing to a closed channel by checking tmoChan for nil, but the check and the send were not synchronized with Close. If the timer fired while the listener was being closed, the callback could pass the nil check and then send on the just-closed channel, crashing newtmgr with "panic: send on closed channel". The race detector also reports the unguarded access to tmoChan. Protect tmoChan and the timer with a mutex shared by the callback and Close, and make the callback's send non-blocking so it can never block while holding the lock. Assisted-by: Claude:claude-opus-5-5 --- nmxact/nmcoap/dispatcher_test.go | 11 +++++++++++ nmxact/nmcoap/listener.go | 20 +++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/nmxact/nmcoap/dispatcher_test.go b/nmxact/nmcoap/dispatcher_test.go index 374ef16..9b9819b 100644 --- a/nmxact/nmcoap/dispatcher_test.go +++ b/nmxact/nmcoap/dispatcher_test.go @@ -319,3 +319,14 @@ func TestListenerCloseBeforeTimeout(t *testing.T) { t.Errorf("timer still active after Close") } } + +// Regression test for 827bef2: the timer callback may run concurrently +// with Close. It must neither race with Close nor panic with "send on +// closed channel". The race is timing dependent, so try many times. +func TestListenerTimeoutRacesClose(t *testing.T) { + for i := 0; i < 2000; i++ { + l := NewListener(MsgCriteria{}) + l.AfterTimeout(0) + l.Close() + } +} diff --git a/nmxact/nmcoap/listener.go b/nmxact/nmcoap/listener.go index 6c43a43..4072168 100644 --- a/nmxact/nmcoap/listener.go +++ b/nmxact/nmcoap/listener.go @@ -24,6 +24,7 @@ import ( "encoding/hex" "sort" "strings" + "sync" "time" "github.com/runtimeco/go-coap" @@ -40,6 +41,9 @@ type Listener struct { ErrChan chan error tmoChan chan time.Time timer *time.Timer + + // Protects tmoChan and timer against the timer callback. + mtx sync.Mutex } func (mc *MsgCriteria) String() string { @@ -118,9 +122,20 @@ func NewListener(mc MsgCriteria) *Listener { } func (ol *Listener) AfterTimeout(tmo time.Duration) <-chan time.Time { + ol.mtx.Lock() + defer ol.mtx.Unlock() + fn := func() { + ol.mtx.Lock() + defer ol.mtx.Unlock() + + // The listener may have been closed after the timer fired. Never + // block here; a pending timeout is as good as a new one. if ol.tmoChan != nil { - ol.tmoChan <- time.Now() + select { + case ol.tmoChan <- time.Now(): + default: + } } } ol.timer = time.AfterFunc(tmo, fn) @@ -128,6 +143,9 @@ func (ol *Listener) AfterTimeout(tmo time.Duration) <-chan time.Time { } func (ol *Listener) Close() { + ol.mtx.Lock() + defer ol.mtx.Unlock() + if ol.timer != nil { ol.timer.Stop() }
