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()
        }

Reply via email to