Skip to content

Commit 9af8303

Browse files
authored
Require global opt-in for distributed transactions (#76376) (#76838)
Closes #76469 (cherry picked from commit 2070def)
1 parent 3435b39 commit 9af8303

File tree

6 files changed

+224
-32
lines changed

6 files changed

+224
-32
lines changed

src/libraries/System.Transactions.Local/ref/System.Transactions.Local.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
// Changes to this file must follow the https://aka.ms/api-review process.
55
// ------------------------------------------------------------------------------
66

7+
using System.Runtime.Versioning;
8+
79
namespace System.Transactions
810
{
911
[System.Runtime.Versioning.UnsupportedOSPlatform("browser")]
@@ -191,6 +193,7 @@ public static partial class TransactionManager
191193
[System.Diagnostics.CodeAnalysis.DisallowNullAttribute]
192194
public static System.Transactions.HostCurrentTransactionCallback? HostCurrentCallback { get { throw null; } set { } }
193195
public static System.TimeSpan MaximumTimeout { get { throw null; } set { } }
196+
public static bool ImplicitDistributedTransactions { get; [System.Runtime.Versioning.SupportedOSPlatform("windows")] [System.Diagnostics.CodeAnalysis.RequiresUnreferencedCode("Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming.")] set; }
194197
public static event System.Transactions.TransactionStartedEventHandler? DistributedTransactionStarted { add { } remove { } }
195198
public static void RecoveryComplete(System.Guid resourceManagerIdentifier) { }
196199
public static System.Transactions.Enlistment Reenlist(System.Guid resourceManagerIdentifier, byte[] recoveryInformation, System.Transactions.IEnlistmentNotification enlistmentNotification) { throw null; }

src/libraries/System.Transactions.Local/src/ILLink/ILLink.Suppressions.LibraryBuild.xml

Lines changed: 0 additions & 12 deletions
This file was deleted.

src/libraries/System.Transactions.Local/src/Resources/Strings.resx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -423,4 +423,10 @@
423423
<data name="DistributedNotSupportedOn32Bits" xml:space="preserve">
424424
<value>Distributed transactions are currently unsupported in 32-bit processes.</value>
425425
</data>
426-
</root>
426+
<data name="ImplicitDistributedTransactionsDisabled" xml:space="preserve">
427+
<value>Implicit distributed transactions have not been enabled. If you're intentionally starting a distributed transaction, set TransactionManager.ImplicitDistributedTransactions to true.</value>
428+
</data>
429+
<data name="ImplicitDistributedTransactionsCannotBeChanged" xml:space="preserve">
430+
<value>TransactionManager.ImplicitDistributedTransaction cannot be changed once set, or once System.Transactions distributed transactions have been initialized. Set this flag once at the start of your program.</value>
431+
</data>
432+
</root>

src/libraries/System.Transactions.Local/src/System/Transactions/DtcProxyShim/DtcProxyShimFactory.cs

Lines changed: 61 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,11 @@ internal sealed class DtcProxyShimFactory
2121
// at the same time.
2222
private static readonly object _proxyInitLock = new();
2323

24+
// This object will perform the actual distributed transaction connection.
25+
// It will be set only if TransactionManager.ImplicitDefaultTransactions
26+
// is set to true, allowing the relevant code to be trimmed otherwise.
27+
internal static ITransactionConnector? s_transactionConnector;
28+
2429
// Lock to protect access to listOfNotifications.
2530
private readonly object _notificationLock = new();
2631

@@ -41,6 +46,7 @@ internal DtcProxyShimFactory(EventWaitHandle notificationEventHandle)
4146

4247
// https://docs.microsoft.com/previous-versions/windows/desktop/ms678898(v=vs.85)
4348
[DllImport(Interop.Libraries.Xolehlp, CharSet = CharSet.Unicode, ExactSpelling = true, PreserveSig = false)]
49+
[RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
4450
private static extern void DtcGetTransactionManagerExW(
4551
[MarshalAs(UnmanagedType.LPWStr)] string? pszHost,
4652
[MarshalAs(UnmanagedType.LPWStr)] string? pszTmName,
@@ -49,7 +55,7 @@ private static extern void DtcGetTransactionManagerExW(
4955
object? pvConfigPararms,
5056
[MarshalAs(UnmanagedType.Interface)] out ITransactionDispenser ppvObject);
5157

52-
[RequiresUnreferencedCode("Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming.")]
58+
[RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
5359
private static void DtcGetTransactionManager(string? nodeName, out ITransactionDispenser localDispenser) =>
5460
DtcGetTransactionManagerExW(nodeName, null, Guids.IID_ITransactionDispenser_Guid, 0, null, out localDispenser);
5561

@@ -61,15 +67,27 @@ public void ConnectToProxy(
6167
out byte[] whereabouts,
6268
out ResourceManagerShim resourceManagerShim)
6369
{
64-
switch (RuntimeInformation.ProcessArchitecture)
70+
if (RuntimeInformation.ProcessArchitecture == Architecture.X86)
71+
{
72+
throw new PlatformNotSupportedException(SR.DistributedNotSupportedOn32Bits);
73+
}
74+
75+
lock (TransactionManager.s_implicitDistributedTransactionsLock)
6576
{
66-
case Architecture.X86:
67-
throw new PlatformNotSupportedException(SR.DistributedNotSupportedOn32Bits);
77+
if (s_transactionConnector is null)
78+
{
79+
// We set TransactionManager.ImplicitDistributedTransactionsInternal, so that any attempt to change it
80+
// later will cause an exception.
81+
TransactionManager.s_implicitDistributedTransactions = false;
82+
83+
throw new NotSupportedException(SR.ImplicitDistributedTransactionsDisabled);
84+
}
6885
}
6986

70-
ConnectToProxyCore(nodeName, resourceManagerIdentifier, managedIdentifier, out nodeNameMatches, out whereabouts, out resourceManagerShim);
87+
s_transactionConnector.ConnectToProxyCore(this, nodeName, resourceManagerIdentifier, managedIdentifier, out nodeNameMatches, out whereabouts, out resourceManagerShim);
7188
}
7289

90+
[RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
7391
private void ConnectToProxyCore(
7492
string? nodeName,
7593
Guid resourceManagerIdentifier,
@@ -80,9 +98,7 @@ private void ConnectToProxyCore(
8098
{
8199
lock (_proxyInitLock)
82100
{
83-
#pragma warning disable IL2026 // This warning is left in the product so developers get an ILLink warning when trimming an app using this transaction support
84101
DtcGetTransactionManager(nodeName, out ITransactionDispenser? localDispenser);
85-
#pragma warning restore IL2026
86102

87103
// Check to make sure the node name matches.
88104
if (nodeName is not null)
@@ -353,6 +369,8 @@ internal ITransactionTransmitter GetCachedTransmitter(ITransaction transaction)
353369

354370
internal void ReturnCachedTransmitter(ITransactionTransmitter transmitter)
355371
{
372+
// Note that due to race conditions, we may end up enqueuing above s_maxCachedInterfaces.
373+
// This is benign, as this is only a best-effort cache, and there are no negative consequences.
356374
if (_cachedTransmitters.Count < s_maxCachedInterfaces)
357375
{
358376
transmitter.Reset();
@@ -375,10 +393,46 @@ internal ITransactionReceiver GetCachedReceiver()
375393

376394
internal void ReturnCachedReceiver(ITransactionReceiver receiver)
377395
{
396+
// Note that due to race conditions, we may end up enqueuing above s_maxCachedInterfaces.
397+
// This is benign, as this is only a best-effort cache, and there are no negative consequences.
378398
if (_cachedReceivers.Count < s_maxCachedInterfaces)
379399
{
380400
receiver.Reset();
381401
_cachedReceivers.Enqueue(receiver);
382402
}
383403
}
404+
405+
internal interface ITransactionConnector
406+
{
407+
void ConnectToProxyCore(
408+
DtcProxyShimFactory proxyShimFactory,
409+
string? nodeName,
410+
Guid resourceManagerIdentifier,
411+
object managedIdentifier,
412+
out bool nodeNameMatches,
413+
out byte[] whereabouts,
414+
out ResourceManagerShim resourceManagerShim);
415+
}
416+
417+
[RequiresUnreferencedCode(TransactionManager.DistributedTransactionTrimmingWarning)]
418+
internal sealed class DtcTransactionConnector : ITransactionConnector
419+
{
420+
public void ConnectToProxyCore(
421+
DtcProxyShimFactory proxyShimFactory,
422+
string? nodeName,
423+
Guid resourceManagerIdentifier,
424+
object managedIdentifier,
425+
out bool nodeNameMatches,
426+
out byte[] whereabouts,
427+
out ResourceManagerShim resourceManagerShim)
428+
{
429+
proxyShimFactory.ConnectToProxyCore(
430+
nodeName,
431+
resourceManagerIdentifier,
432+
managedIdentifier,
433+
out nodeNameMatches,
434+
out whereabouts,
435+
out resourceManagerShim);
436+
}
437+
}
384438
}

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

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,12 @@
44
using System.Collections;
55
using System.Diagnostics.CodeAnalysis;
66
using System.IO;
7+
using System.Runtime.Versioning;
78
using System.Threading;
89
using System.Transactions.Configuration;
10+
#if WINDOWS
11+
using System.Transactions.DtcProxyShim;
12+
#endif
913
using System.Transactions.Oletx;
1014

1115
namespace System.Transactions
@@ -29,6 +33,10 @@ public static class TransactionManager
2933
private static TransactionTable? s_transactionTable;
3034

3135
private static TransactionStartedEventHandler? s_distributedTransactionStartedDelegate;
36+
37+
internal const string DistributedTransactionTrimmingWarning =
38+
"Distributed transactions support may not be compatible with trimming. If your program creates a distributed transaction via System.Transactions, the correctness of the application cannot be guaranteed after trimming.";
39+
3240
public static event TransactionStartedEventHandler? DistributedTransactionStarted
3341
{
3442
add
@@ -391,6 +399,60 @@ public static TimeSpan MaximumTimeout
391399
}
392400
}
393401

402+
/// <summary>
403+
/// Controls whether usage of System.Transactions APIs that require escalation to a distributed transaction will do so;
404+
/// if your application requires distributed transaction, opt into using them by setting this to <see langword="true" />.
405+
/// If set to <see langword="false" /> (the default), escalation to a distributed transaction will throw a <see cref="NotSupportedException" />.
406+
/// </summary>
407+
#if WINDOWS
408+
public static bool ImplicitDistributedTransactions
409+
{
410+
get => DtcProxyShimFactory.s_transactionConnector is not null;
411+
412+
[SupportedOSPlatform("windows")]
413+
[RequiresUnreferencedCode(DistributedTransactionTrimmingWarning)]
414+
set
415+
{
416+
lock (s_implicitDistributedTransactionsLock)
417+
{
418+
// Make sure this flag can only be set once, and that once distributed transactions have been initialized,
419+
// it's frozen.
420+
if (s_implicitDistributedTransactions is null)
421+
{
422+
s_implicitDistributedTransactions = value;
423+
424+
if (value)
425+
{
426+
DtcProxyShimFactory.s_transactionConnector ??= new DtcProxyShimFactory.DtcTransactionConnector();
427+
}
428+
}
429+
else if (value != s_implicitDistributedTransactions)
430+
{
431+
throw new InvalidOperationException(SR.ImplicitDistributedTransactionsCannotBeChanged);
432+
}
433+
}
434+
}
435+
}
436+
437+
internal static bool? s_implicitDistributedTransactions;
438+
internal static object s_implicitDistributedTransactionsLock = new();
439+
#else
440+
public static bool ImplicitDistributedTransactions
441+
{
442+
get => false;
443+
444+
[SupportedOSPlatform("windows")]
445+
[RequiresUnreferencedCode(DistributedTransactionTrimmingWarning)]
446+
set
447+
{
448+
if (value)
449+
{
450+
throw new PlatformNotSupportedException(SR.DistributedNotSupported);
451+
}
452+
}
453+
}
454+
#endif
455+
394456
// This routine writes the "header" for the recovery information, based on the
395457
// type of the calling object and its provided parameter collection. This information
396458
// we be read back by the static Reenlist method to create the necessary transaction

src/libraries/System.Transactions.Local/tests/OleTxTests.cs

Lines changed: 91 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.IO;
5+
using System.Runtime.CompilerServices;
56
using System.Runtime.InteropServices;
67
using System.Threading;
78
using Microsoft.DotNet.RemoteExecutor;
@@ -492,6 +493,80 @@ public void GetDtcTransaction()
492493
Retry(() => Assert.Equal(TransactionStatus.Committed, tx.TransactionInformation.Status));
493494
});
494495

496+
[ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
497+
public void Distributed_transactions_require_ImplicitDistributedTransactions_true()
498+
{
499+
// Temporarily skip on 32-bit where we have an issue.
500+
if (!Environment.Is64BitProcess)
501+
{
502+
return;
503+
}
504+
505+
using var _ = RemoteExecutor.Invoke(() =>
506+
{
507+
Assert.False(TransactionManager.ImplicitDistributedTransactions);
508+
509+
using var tx = new CommittableTransaction();
510+
511+
Assert.Throws<NotSupportedException>(MinimalOleTxScenario);
512+
});
513+
}
514+
515+
[ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
516+
public void ImplicitDistributedTransactions_cannot_be_changed_after_being_set()
517+
{
518+
// Temporarily skip on 32-bit where we have an issue.
519+
if (!Environment.Is64BitProcess)
520+
{
521+
return;
522+
}
523+
524+
using var _ = RemoteExecutor.Invoke(() =>
525+
{
526+
TransactionManager.ImplicitDistributedTransactions = true;
527+
528+
Assert.Throws<InvalidOperationException>(() => TransactionManager.ImplicitDistributedTransactions = false);
529+
});
530+
}
531+
532+
[ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
533+
public void ImplicitDistributedTransactions_cannot_be_changed_after_being_read_as_true()
534+
{
535+
// Temporarily skip on 32-bit where we have an issue.
536+
if (!Environment.Is64BitProcess)
537+
{
538+
return;
539+
}
540+
541+
using var _ = RemoteExecutor.Invoke(() =>
542+
{
543+
TransactionManager.ImplicitDistributedTransactions = true;
544+
545+
MinimalOleTxScenario();
546+
547+
Assert.Throws<InvalidOperationException>(() => TransactionManager.ImplicitDistributedTransactions = false);
548+
TransactionManager.ImplicitDistributedTransactions = true;
549+
});
550+
}
551+
552+
[ConditionalFact(nameof(IsRemoteExecutorSupportedAndNotNano))]
553+
public void ImplicitDistributedTransactions_cannot_be_changed_after_being_read_as_false()
554+
{
555+
// Temporarily skip on 32-bit where we have an issue.
556+
if (!Environment.Is64BitProcess)
557+
{
558+
return;
559+
}
560+
561+
using var _ = RemoteExecutor.Invoke(() =>
562+
{
563+
Assert.Throws<NotSupportedException>(MinimalOleTxScenario);
564+
565+
Assert.Throws<InvalidOperationException>(() => TransactionManager.ImplicitDistributedTransactions = true);
566+
TransactionManager.ImplicitDistributedTransactions = false;
567+
});
568+
}
569+
495570
private static void Test(Action action)
496571
{
497572
// Temporarily skip on 32-bit where we have an issue.
@@ -500,6 +575,8 @@ private static void Test(Action action)
500575
return;
501576
}
502577

578+
TransactionManager.ImplicitDistributedTransactions = true;
579+
503580
// In CI, we sometimes get XACT_E_TMNOTAVAILABLE; when it happens, it's typically on the very first
504581
// attempt to connect to MSDTC (flaky/slow on-demand startup of MSDTC), though not only.
505582
// This catches that error and retries.
@@ -549,23 +626,25 @@ private static void Retry(Action action)
549626
}
550627
}
551628

629+
static void MinimalOleTxScenario()
630+
{
631+
using var tx = new CommittableTransaction();
632+
633+
var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
634+
var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
635+
636+
tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None);
637+
tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None);
638+
639+
tx.Commit();
640+
}
641+
552642
public class OleTxFixture
553643
{
554644
// In CI, we sometimes get XACT_E_TMNOTAVAILABLE on the very first attempt to connect to MSDTC;
555645
// this is likely due to on-demand slow startup of MSDTC. Perform pre-test connecting with retry
556646
// to ensure that MSDTC is properly up when the first test runs.
557647
public OleTxFixture()
558-
=> Test(() =>
559-
{
560-
using var tx = new CommittableTransaction();
561-
562-
var enlistment1 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
563-
var enlistment2 = new TestEnlistment(Phase1Vote.Prepared, EnlistmentOutcome.Committed);
564-
565-
tx.EnlistDurable(Guid.NewGuid(), enlistment1, EnlistmentOptions.None);
566-
tx.EnlistDurable(Guid.NewGuid(), enlistment2, EnlistmentOptions.None);
567-
568-
tx.Commit();
569-
});
648+
=> Test(MinimalOleTxScenario);
570649
}
571650
}

0 commit comments

Comments
 (0)