From 8e0aaea24ee1585959d16425c727159cbb47a6e7 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 10 Apr 2026 14:56:09 +0100 Subject: [PATCH 1/3] fix: prevent deadlock in donNotifier.Subscribe() on concurrent NotifyDonSet Subscribe() used a blocking channel send to deliver the cached DON value. If NotifyDonSet() concurrently filled the buffer between subscriber registration and the send, Subscribe() would deadlock. Switch to a non-blocking select, matching NotifyDonSet()'s pattern. The subscriber already has the value from NotifyDonSet if the buffer is full. Fixes: CORE-2378 --- core/capabilities/don_notifier.go | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/core/capabilities/don_notifier.go b/core/capabilities/don_notifier.go index c40bea80e01..51febb1fa27 100644 --- a/core/capabilities/don_notifier.go +++ b/core/capabilities/don_notifier.go @@ -74,8 +74,11 @@ func (n *donNotifier) Subscribe(ctx context.Context) (<-chan capabilities.DON, f n.subscribers.Store(s, struct{}{}) - if n.don.Load() != nil { - s <- *n.don.Load() + if d := n.don.Load(); d != nil { + select { + case s <- *d: + default: + } } return s, unsubscribe, nil From 6c47383ca3936e03cd9b03389e03be565fdba23d Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Fri, 10 Apr 2026 17:37:36 +0100 Subject: [PATCH 2/3] fix: prevent deadlock in donNotifier.Subscribe() on concurrent NotifyDonSet MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Subscribe() registered the subscriber channel in the map BEFORE sending the cached DON value. If NotifyDonSet fired between registration and the send, it would fill the 1-buffer, and the subsequent blocking send in Subscribe() would deadlock. Reorder: send the cached value first (safe — channel is new, buffer is empty, nobody else has a reference), then register. Eliminates the race window entirely without changing the blocking send contract. Fixes: CORE-2378 --- core/capabilities/don_notifier.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/core/capabilities/don_notifier.go b/core/capabilities/don_notifier.go index 51febb1fa27..e5ea18d7f8e 100644 --- a/core/capabilities/don_notifier.go +++ b/core/capabilities/don_notifier.go @@ -72,15 +72,12 @@ func (n *donNotifier) Subscribe(ctx context.Context) (<-chan capabilities.DON, f n.subscribers.Delete(s) } - n.subscribers.Store(s, struct{}{}) - if d := n.don.Load(); d != nil { - select { - case s <- *d: - default: - } + s <- *d } + n.subscribers.Store(s, struct{}{}) + return s, unsubscribe, nil } From 570df8372fce674d49b07ccbc53f33a9f69284f1 Mon Sep 17 00:00:00 2001 From: Michael Fletcher Date: Mon, 13 Apr 2026 13:20:48 +0100 Subject: [PATCH 3/3] fix: use non-blocking send for cached DON value in Subscribe() Register the subscriber first (preserving no-missed-notification guarantee), but use a non-blocking select when sending the cached value. If a concurrent NotifyDonSet already filled the buffer, the cached send is safely skipped since the subscriber already has a notification. Also fixes a minor double-Load race in the original code by capturing the pointer once. --- core/capabilities/don_notifier.go | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/core/capabilities/don_notifier.go b/core/capabilities/don_notifier.go index e5ea18d7f8e..407a96c8862 100644 --- a/core/capabilities/don_notifier.go +++ b/core/capabilities/don_notifier.go @@ -72,12 +72,16 @@ func (n *donNotifier) Subscribe(ctx context.Context) (<-chan capabilities.DON, f n.subscribers.Delete(s) } + n.subscribers.Store(s, struct{}{}) + if d := n.don.Load(); d != nil { - s <- *d + select { + case s <- *d: + default: + // Channel already has a value from a concurrent NotifyDonSet. + } } - n.subscribers.Store(s, struct{}{}) - return s, unsubscribe, nil }