From d0ef2352868249facd296edfe5eefbb68abca3ef Mon Sep 17 00:00:00 2001 From: Marc Schier Date: Thu, 11 Jun 2026 09:56:06 +0200 Subject: [PATCH] RetryBudget: swap Reset() write order to avoid stale-tick race MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previous Reset(): Volatile.Write(ref m_started, 0); Interlocked.Exchange(ref m_startTicks, kNotStarted); A concurrent EnsureStarted() observer could see m_started == 0 (taking the "reset, so go start" branch), then read the stale m_startTicks value BEFORE the Interlocked.Exchange landed, and treat the budget as already-started against the pre-reset timestamp. IsExhausted / TryConsume then computed elapsed time from the old origin. Fix: reset m_startTicks first, then m_started. EnsureStarted writes m_started = 1 only after the CAS on m_startTicks succeeds, so resetting m_startTicks first preserves that invariant — no observer can see "started == 1" with a stale tick value. --- Stack/Opc.Ua.Core/Stack/Client/Channels/RetryBudget.cs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/Stack/Opc.Ua.Core/Stack/Client/Channels/RetryBudget.cs b/Stack/Opc.Ua.Core/Stack/Client/Channels/RetryBudget.cs index 6b7c652560..1cdd156530 100644 --- a/Stack/Opc.Ua.Core/Stack/Client/Channels/RetryBudget.cs +++ b/Stack/Opc.Ua.Core/Stack/Client/Channels/RetryBudget.cs @@ -121,8 +121,12 @@ namespace Opc.Ua /// public void Reset() { - Volatile.Write(ref m_started, 0); + // Order matters: clear m_startTicks BEFORE m_started so a concurrent + // EnsureStarted observer cannot see "started=1" with a stale tick value. + // EnsureStarted writes m_started=1 only after the CAS on m_startTicks + // succeeds, so resetting m_startTicks first preserves that invariant. Interlocked.Exchange(ref m_startTicks, kNotStarted); + Volatile.Write(ref m_started, 0); } private long EnsureStarted()