From f280ef655c549f2dfea2742282dea630a073fc34 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 04:46:44 +0000 Subject: [PATCH 1/4] Initial plan From 9b36fdeae92854931f26ce71ba0cd247ecdce5b9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 14 Jul 2026 05:21:03 +0000 Subject: [PATCH 2/4] Fix TransactionManager race condition in DefaultTimeout/MaximumTimeout setters The DefaultTimeout setter was using Interlocked.Exchange to write s_defaultTimeoutTicks and then setting s_defaultTimeoutValidated = true without holding a lock. This raced with the getter's LazyInitializer.EnsureInitialized (which uses s_classSyncObject as its lock) - a concurrent getter thread could see s_defaultTimeoutValidated as false, enter EnsureInitialized, acquire the lock, and overwrite the setter's new value with the config default. Fix: wrap the writes in DefaultTimeout.set in lock(ClassSyncObject) so they are mutually exclusive with EnsureInitialized. Keep Interlocked.Exchange for 32-bit atomicity of the 64-bit field. Also fix MaximumTimeout.set which had the same issue: - Wrap writes in lock(ClassSyncObject) - Fix wrong flag-before-value ordering (was: s_cachedMaxTimeout = true then s_maximumTimeout = value; now: value before flag) Fixes dotnet/runtime#105124 Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../System/Transactions/TransactionManager.cs | 33 +++++++++++-------- 1 file changed, 20 insertions(+), 13 deletions(-) diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs index b98f51e538188a..a7c21e0c5de17b 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs @@ -320,8 +320,13 @@ public static TimeSpan DefaultTimeout etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.set_DefaultTimeout"); } - Interlocked.Exchange(ref s_defaultTimeoutTicks, ValidateTimeout(value).Ticks); - if (Interlocked.Read(ref s_defaultTimeoutTicks) != value.Ticks) + TimeSpan validatedTimeout = ValidateTimeout(value); + lock (ClassSyncObject) + { + Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedTimeout.Ticks); + s_defaultTimeoutValidated = true; + } + if (validatedTimeout != value) { if (etwLog.IsEnabled()) { @@ -329,8 +334,6 @@ public static TimeSpan DefaultTimeout } } - s_defaultTimeoutValidated = true; - if (etwLog.IsEnabled()) { etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.set_DefaultTimeout"); @@ -370,17 +373,21 @@ public static TimeSpan MaximumTimeout ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero); - s_cachedMaxTimeout = true; - s_maximumTimeout = value; - LazyInitializer.EnsureInitialized(ref s_defaultTimeoutTicks, ref s_defaultTimeoutValidated, ref s_classSyncObject, () => DefaultSettingsSection.Timeout.Ticks); - - long defaultTimeoutTicks = Interlocked.Read(ref s_defaultTimeoutTicks); - Interlocked.Exchange(ref s_defaultTimeoutTicks, ValidateTimeout(new TimeSpan(defaultTimeoutTicks)).Ticks); - if (Interlocked.Read(ref s_defaultTimeoutTicks) != defaultTimeoutTicks) + lock (ClassSyncObject) { - if (etwLog.IsEnabled()) + s_maximumTimeout = value; + s_cachedMaxTimeout = true; + LazyInitializer.EnsureInitialized(ref s_defaultTimeoutTicks, ref s_defaultTimeoutValidated, ref s_classSyncObject, () => DefaultSettingsSection.Timeout.Ticks); + + long defaultTimeoutTicks = Interlocked.Read(ref s_defaultTimeoutTicks); + long validatedDefaultTicks = ValidateTimeout(new TimeSpan(defaultTimeoutTicks)).Ticks; + if (validatedDefaultTicks != defaultTimeoutTicks) { - etwLog.ConfiguredDefaultTimeoutAdjusted(); + Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedDefaultTicks); + if (etwLog.IsEnabled()) + { + etwLog.ConfiguredDefaultTimeoutAdjusted(); + } } } From e02b92b3cee265cc6ff3f9c0f91764daf245667c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Wed, 29 Jul 2026 22:18:47 +0000 Subject: [PATCH 3/4] Restore original Interlocked.Read comparison in DefaultTimeout.set Per reviewer feedback, keep the original comparison style (Interlocked.Read(ref s_defaultTimeoutTicks) != value.Ticks) which checks whether the stored timeout differs from what was requested, rather than checking whether validation changed the value. Move the comparison inside the lock so it reads back the value just stored by this thread (not a concurrent thread's modification). Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System/Transactions/TransactionManager.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs index a7c21e0c5de17b..e1b8a6d97423cc 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs @@ -321,12 +321,14 @@ public static TimeSpan DefaultTimeout } TimeSpan validatedTimeout = ValidateTimeout(value); + bool timeoutAdjusted; lock (ClassSyncObject) { Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedTimeout.Ticks); s_defaultTimeoutValidated = true; + timeoutAdjusted = Interlocked.Read(ref s_defaultTimeoutTicks) != value.Ticks; } - if (validatedTimeout != value) + if (timeoutAdjusted) { if (etwLog.IsEnabled()) { From 57018beb30bfead6c43348070504723d8048ab1f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 3 Aug 2026 00:46:53 +0000 Subject: [PATCH 4/4] Simplify TransactionManager timeout logic per @jkotas feedback - Delete MachineSettingsSection, ConfigurationStrings, DefaultSettingsSection files - Remove those files from the .csproj - Initialize s_defaultTimeoutTicks with TimeSpan.FromMinutes(1).Ticks - Rename s_maximumTimeout to s_maximumTimeoutTicks (long), initialize with TimeSpan.FromMinutes(10).Ticks - Delete s_defaultTimeoutValidated and s_cachedMaxTimeout - Simplify DefaultTimeout getter: direct Interlocked.Read + combined ETW MethodEnter/Exit - Simplify MaximumTimeout getter: direct Interlocked.Read + combined ETW MethodEnter/Exit - DefaultTimeout setter: ValidateTimeout inside lock, Interlocked.Exchange, compare validated vs original - MaximumTimeout setter: set max with Interlocked.Exchange, revalidate default, fire event outside lock - Inline "" for DistributedTransactionManagerName Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com> --- .../src/System.Transactions.Local.csproj | 3 - .../Configuration/ConfigurationStrings.cs | 10 --- .../Configuration/DefaultSettingsSection.cs | 16 ----- .../Configuration/MachineSettingsSection.cs | 14 ---- .../System/Transactions/TransactionManager.cs | 64 +++++-------------- 5 files changed, 17 insertions(+), 90 deletions(-) delete mode 100644 src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/ConfigurationStrings.cs delete mode 100644 src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/DefaultSettingsSection.cs delete mode 100644 src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/MachineSettingsSection.cs diff --git a/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj b/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj index fb9e5552476c99..812c9f40503335 100644 --- a/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj +++ b/src/libraries/System.Transactions.Local/src/System.Transactions.Local.csproj @@ -39,9 +39,6 @@ - - - diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/ConfigurationStrings.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/ConfigurationStrings.cs deleted file mode 100644 index 0a8cb879bd9490..00000000000000 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/ConfigurationStrings.cs +++ /dev/null @@ -1,10 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Transactions.Configuration -{ - internal static class ConfigurationStrings - { - internal const string DefaultDistributedTransactionManagerName = ""; - } -} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/DefaultSettingsSection.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/DefaultSettingsSection.cs deleted file mode 100644 index 3bc168cc92c6dd..00000000000000 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/DefaultSettingsSection.cs +++ /dev/null @@ -1,16 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Transactions.Configuration -{ - internal sealed class DefaultSettingsSection // ConfigurationSection - { - private static readonly DefaultSettingsSection s_section = new DefaultSettingsSection(); - - internal static DefaultSettingsSection GetSection() => s_section; - - public static string DistributedTransactionManagerName { get; set; } = ConfigurationStrings.DefaultDistributedTransactionManagerName; - - public static TimeSpan Timeout => TimeSpan.FromMinutes(1); - } -} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/MachineSettingsSection.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/MachineSettingsSection.cs deleted file mode 100644 index 080c9343085736..00000000000000 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/Configuration/MachineSettingsSection.cs +++ /dev/null @@ -1,14 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -namespace System.Transactions.Configuration -{ - internal sealed class MachineSettingsSection // ConfigurationSection - { - private static readonly MachineSettingsSection s_section = new MachineSettingsSection(); - - internal static MachineSettingsSection GetSection() => s_section; - - public static TimeSpan MaxTimeout => TimeSpan.FromMinutes(10); - } -} diff --git a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs index e1b8a6d97423cc..fd0af63655e68e 100644 --- a/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs +++ b/src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs @@ -6,7 +6,6 @@ using System.IO; using System.Runtime.Versioning; using System.Threading; -using System.Transactions.Configuration; #if WINDOWS using System.Transactions.DtcProxyShim; #endif @@ -279,11 +278,7 @@ internal static IsolationLevel DefaultIsolationLevel } } - private static DefaultSettingsSection DefaultSettings => field ??= DefaultSettingsSection.GetSection(); - private static MachineSettingsSection MachineSettings => field ??= MachineSettingsSection.GetSection(); - - private static bool s_defaultTimeoutValidated; - private static long s_defaultTimeoutTicks; + private static long s_defaultTimeoutTicks = TimeSpan.FromMinutes(1).Ticks; public static TimeSpan DefaultTimeout { get @@ -292,22 +287,6 @@ public static TimeSpan DefaultTimeout if (etwLog.IsEnabled()) { etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultTimeout"); - } - - if (!s_defaultTimeoutValidated) - { - LazyInitializer.EnsureInitialized(ref s_defaultTimeoutTicks, ref s_defaultTimeoutValidated, ref s_classSyncObject, () => ValidateTimeout(DefaultSettingsSection.Timeout).Ticks); - if (Interlocked.Read(ref s_defaultTimeoutTicks) != DefaultSettingsSection.Timeout.Ticks) - { - if (etwLog.IsEnabled()) - { - etwLog.ConfiguredDefaultTimeoutAdjusted(); - } - } - } - - if (etwLog.IsEnabled()) - { etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultTimeout"); } return new TimeSpan(Interlocked.Read(ref s_defaultTimeoutTicks)); @@ -320,13 +299,12 @@ public static TimeSpan DefaultTimeout etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.set_DefaultTimeout"); } - TimeSpan validatedTimeout = ValidateTimeout(value); bool timeoutAdjusted; lock (ClassSyncObject) { + TimeSpan validatedTimeout = ValidateTimeout(value); Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedTimeout.Ticks); - s_defaultTimeoutValidated = true; - timeoutAdjusted = Interlocked.Read(ref s_defaultTimeoutTicks) != value.Ticks; + timeoutAdjusted = validatedTimeout != value; } if (timeoutAdjusted) { @@ -344,8 +322,7 @@ public static TimeSpan DefaultTimeout } - private static bool s_cachedMaxTimeout; - private static TimeSpan s_maximumTimeout; + private static long s_maximumTimeoutTicks = TimeSpan.FromMinutes(10).Ticks; public static TimeSpan MaximumTimeout { get @@ -354,16 +331,9 @@ public static TimeSpan MaximumTimeout if (etwLog.IsEnabled()) { etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultMaximumTimeout"); - } - - LazyInitializer.EnsureInitialized(ref s_maximumTimeout, ref s_cachedMaxTimeout, ref s_classSyncObject, () => MachineSettingsSection.MaxTimeout); - - if (etwLog.IsEnabled()) - { etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultMaximumTimeout"); } - - return s_maximumTimeout; + return new TimeSpan(Interlocked.Read(ref s_maximumTimeoutTicks)); } set { @@ -375,21 +345,21 @@ public static TimeSpan MaximumTimeout ArgumentOutOfRangeException.ThrowIfLessThan(value, TimeSpan.Zero); + bool timeoutAdjusted; lock (ClassSyncObject) { - s_maximumTimeout = value; - s_cachedMaxTimeout = true; - LazyInitializer.EnsureInitialized(ref s_defaultTimeoutTicks, ref s_defaultTimeoutValidated, ref s_classSyncObject, () => DefaultSettingsSection.Timeout.Ticks); + Interlocked.Exchange(ref s_maximumTimeoutTicks, value.Ticks); - long defaultTimeoutTicks = Interlocked.Read(ref s_defaultTimeoutTicks); - long validatedDefaultTicks = ValidateTimeout(new TimeSpan(defaultTimeoutTicks)).Ticks; - if (validatedDefaultTicks != defaultTimeoutTicks) + TimeSpan timeout = new TimeSpan(s_defaultTimeoutTicks); + TimeSpan validatedTimeout = ValidateTimeout(timeout); + Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedTimeout.Ticks); + timeoutAdjusted = validatedTimeout != timeout; + } + if (timeoutAdjusted) + { + if (etwLog.IsEnabled()) { - Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedDefaultTicks); - if (etwLog.IsEnabled()) - { - etwLog.ConfiguredDefaultTimeoutAdjusted(); - } + etwLog.ConfiguredDefaultTimeoutAdjusted(); } } @@ -624,6 +594,6 @@ internal static Transaction FindOrCreatePromotedTransaction(Guid transactionIden internal static OletxTransactionManager DistributedTransactionManager => // If the distributed transaction manager is not configured, throw an exception LazyInitializer.EnsureInitialized(ref distributedTransactionManager, ref s_classSyncObject, - () => new OletxTransactionManager(DefaultSettingsSection.DistributedTransactionManagerName)); + () => new OletxTransactionManager("")); } }