Skip to content

Fix TransactionManager DefaultTimeout/MaximumTimeout setter race condition - #130664

Merged
jkotas merged 4 commits into
mainfrom
copilot/fix-transactiontimeout-test-failure
Aug 3, 2026
Merged

Fix TransactionManager DefaultTimeout/MaximumTimeout setter race condition#130664
jkotas merged 4 commits into
mainfrom
copilot/fix-transactiontimeout-test-failure

Conversation

Copilot AI commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

TransactionManager.DefaultTimeout has a race condition where a concurrent read can overwrite a value set by another thread. The getter uses LazyInitializer.EnsureInitialized (which acquires s_classSyncObject lock) while the setter used Interlocked.Exchange + a plain write to the s_defaultTimeoutValidated flag — outside any lock. A concurrent getter thread could see the flag as false, enter EnsureInitialized, acquire the lock, and overwrite the setter's new value with the config default.

Changes

  • Perform all work in the DefaultTimeout and MaximumTimeout under a lock to ensure coherent state and avoid race conditions
  • Delete unnecessary ceremony for default values that was left-over from .NET Framework configuration system

Fixes #105124

…t 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 #105124

Co-authored-by: jkotas <6668460+jkotas@users.noreply.github.com>
Copilot AI requested review from Copilot and removed request for Copilot July 14, 2026 05:21
Copilot AI changed the title [WIP] Fix test failure in TransactionManagerTest for default timeout Fix TransactionManager DefaultTimeout/MaximumTimeout setter race condition Jul 14, 2026
Copilot AI requested a review from jkotas July 14, 2026 05:21
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>
Copilot AI review requested due to automatic review settings July 29, 2026 22:18

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because there is no eligible user to bill. To allow Copilot reviews on bot-authored pull requests, enable direct organization billing in your organization's Copilot settings.

Copilot AI requested a review from jkotas July 29, 2026 22:19
@jkotas

jkotas commented Aug 3, 2026

Copy link
Copy Markdown
Member

@copilot

  • Delete MachineSettingsSection, ConfigurationStrings, DefaultSettingsSection. Inline the default values in the few places that they are used instead.
  • Delete s_defaultTimeoutValidated and s_cachedMaxTimeout.
  • Initialize s_defaultTimeoutTicks with default value: static long s_defaultTimeoutTicks = TimeSpan.FromMinutes(1).Ticks;
  • Rename s_maximumTimeout to s_maximumTimeoutTicks and initialize it with default value: static long s_maximumTimeoutTicks = TimeSpan.FromMinutes(10).Ticks;
  • Change DefaultTimeout getter to just return the static value:
TransactionsEtwProvider etwLog = TransactionsEtwProvider.Log;
if (etwLog.IsEnabled())
{
     etwLog.MethodEnter(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultTimeout");
     etwLog.MethodExit(TraceSourceType.TraceSourceBase, "TransactionManager.get_DefaultTimeout");
}
return new TimeSpan(Interlocked.Read(ref s_defaultTimeoutTicks));
  • Dtto for MaximumTimeout setter
  • Change DefaultTimeout setter to take a lock around the validation and updates to ensure coherent state:
lock (ClassSyncObject)
{
    TimeSpan validatedTimeout = ValidateTimeout(value);
    Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedTimeout.Ticks);
    timeoutAdjusted = validatedTimeout != value;
}
if (timeoutAdjusted)
{
    if (etwLog.IsEnabled())
    {
        etwLog.ConfiguredDefaultTimeoutAdjusted();
    }
}
  • Change MaximumTimeout setter to take a lock around the validation and updates to ensure coherent state:
lock (ClassSyncObject)
{
    Interlocked.Exchange(ref s_maximumTimeoutTicks, value.Ticks);

    TimeSpan timeout = new TimeSpan(s_defaultTimeoutTicks);
    TimeSpan validatedTimeout = ValidateTimeout(value);
    Interlocked.Exchange(ref s_defaultTimeoutTicks, validatedTimeout.Ticks);
    timeoutAdjusted = validatedTimeout != timeout;
}
if (timeoutAdjusted)
{
    if (etwLog.IsEnabled())
    {
        etwLog.ConfiguredDefaultTimeoutAdjusted();
    }
}
  • Update PR description to reflect the changes

- 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>
Copilot AI review requested due to automatic review settings August 3, 2026 00:46

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because there is no eligible user to bill. To allow Copilot reviews on bot-authored pull requests, enable direct organization billing in your organization's Copilot settings.

Copilot AI commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

@copilot

  • Delete MachineSettingsSection, ConfigurationStrings, DefaultSettingsSection. Inline the default values in the few places that they are used instead.
    ...

Done in the latest commit. Here's a summary of all changes made:

  • Deleted MachineSettingsSection.cs, ConfigurationStrings.cs, and DefaultSettingsSection.cs; removed them from the .csproj
  • s_defaultTimeoutTicks initialized to TimeSpan.FromMinutes(1).Ticks
  • s_maximumTimeout renamed to s_maximumTimeoutTicks (long) and initialized to TimeSpan.FromMinutes(10).Ticks
  • s_defaultTimeoutValidated and s_cachedMaxTimeout deleted
  • DefaultTimeout getter simplified to just ETW + Interlocked.Read
  • MaximumTimeout getter simplified to just ETW + Interlocked.Read
  • DefaultTimeout setter: ValidateTimeout and Interlocked.Exchange inside the lock, event fired outside
  • MaximumTimeout setter: Interlocked.Exchange for the new max, then revalidate the current default timeout inside the lock, event fired outside
  • DistributedTransactionManager inlined with ""

@jkotas
jkotas marked this pull request as ready for review August 3, 2026 03:40
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @SamMonoRT, @dotnet/efteam
See info in area-owners.md if you want to be subscribed.

@AndriySvyryd
AndriySvyryd requested a review from Copilot August 3, 2026 20:53
@jkotas

jkotas commented Aug 3, 2026

Copy link
Copy Markdown
Member

@steveisok Could you please approve this? The approval from @AndriySvyryd is not good enough for some reason.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.

Suppressed comments (1)

src/libraries/System.Transactions.Local/src/System/Transactions/TransactionManager.cs:353

  • Inside MaximumTimeout setter, s_defaultTimeoutTicks is read directly (new TimeSpan(s_defaultTimeoutTicks)) even though the field is otherwise accessed via Interlocked. While this is currently under the same lock as all writers, using Interlocked.Read here keeps the access pattern consistent and avoids relying on that invariant for correctness on 32-bit / future refactors.
                    TimeSpan timeout = new TimeSpan(s_defaultTimeoutTicks);

@jkotas
jkotas merged commit 39d3f6a into main Aug 3, 2026
96 checks passed
@jkotas
jkotas deleted the copilot/fix-transactiontimeout-test-failure branch August 3, 2026 21:12
@dotnet-milestone-bot dotnet-milestone-bot Bot added this to the 11.0-rc1 milestone Aug 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test failure: System.Transactions.Tests.TransactionManagerTest.DefaultTimeout_MaxTimeout_Set_Get

5 participants