From 8e4001870185921eb299c0e44cea27404a8c1580 Mon Sep 17 00:00:00 2001 From: Steven Syrek Date: Mon, 19 Jan 2026 19:08:53 +0100 Subject: [PATCH] fix(daemon): Fix double-unlock risk in debouncer on action panic (#1140) The debouncer's timer callback used a pattern that could cause a double-unlock panic if the action function panicked: d.mu.Lock() defer d.mu.Unlock() if d.seq == currentSeq { d.mu.Unlock() // Manual unlock d.action() // If this panics... d.mu.Lock() // ...this never runs } // ...but defer still tries to unlock Fix: Remove the defer and manually manage lock state. Now if the action panics, the lock is already released, preventing a double-unlock panic that would mask the original panic. Co-authored-by: Steven Syrek Co-authored-by: Claude Opus 4.5 --- cmd/bd/daemon_debouncer.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/cmd/bd/daemon_debouncer.go b/cmd/bd/daemon_debouncer.go index 9ac0808f..fae1512d 100644 --- a/cmd/bd/daemon_debouncer.go +++ b/cmd/bd/daemon_debouncer.go @@ -41,15 +41,18 @@ func (d *Debouncer) Trigger() { d.timer = time.AfterFunc(d.duration, func() { d.mu.Lock() - defer d.mu.Unlock() - // Only fire if this is still the latest trigger - if d.seq == currentSeq { - d.timer = nil - d.mu.Unlock() // Unlock before calling action to avoid holding lock during callback - d.action() - d.mu.Lock() // Re-lock for defer + if d.seq != currentSeq { + d.mu.Unlock() + return } + d.timer = nil + d.mu.Unlock() // Unlock before calling action to avoid holding lock during callback + + // Action runs without lock held. If action panics, the lock is already + // released, avoiding a double-unlock that would occur with the previous + // defer-based pattern. + d.action() }) }