Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit c7ee148

Browse files
authored
Harden SqlConnection state transitions with CAS guards (#4267)
1 parent 297d421 commit c7ee148

4 files changed

Lines changed: 356 additions & 7 deletions

File tree

src/Microsoft.Data.SqlClient/src/Microsoft/Data/Common/AdapterUtil.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1106,6 +1106,10 @@ internal enum ConnectionError
11061106
GetConnectionReturnsNull,
11071107
ConnectionOptionsMissing,
11081108
CouldNotSwitchToClosedPreviouslyOpenedState,
1109+
InvalidSourceStateForSetInnerConnectionTo,
1110+
FailedToCommitSetInnerConnectionTo,
1111+
InvalidSourceStateForSetInnerConnectionEvent,
1112+
FailedToCommitSetInnerConnectionEvent,
11091113
}
11101114

11111115
internal static Exception InternalConnectionError(ConnectionError internalError)

src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2236,7 +2236,16 @@ private bool TryOpen(TaskCompletionSource<DbConnectionInternal> retry, SqlConnec
22362236
return result;
22372237
}
22382238

2239-
private bool TryOpenInner(TaskCompletionSource<DbConnectionInternal> retry)
2239+
/// <summary>
2240+
/// Completes the inner open/replace operation and initializes parser state for the active inner connection.
2241+
/// </summary>
2242+
/// <param name="retry">Retry continuation used by async open paths.</param>
2243+
/// <returns><see langword="true"/> when open initialization completed synchronously; otherwise <see langword="false"/>.</returns>
2244+
/// <remarks>
2245+
/// The inner connection is snapshotted after the open call so downstream parser access uses a single observed
2246+
/// instance and does not rely on a second racy read of <see cref="InnerConnection"/>.
2247+
/// </remarks>
2248+
internal bool TryOpenInner(TaskCompletionSource<DbConnectionInternal> retry)
22402249
{
22412250
// Create a single TimeoutTimer that represents the overall connection-open budget for this
22422251
// attempt. The timer is threaded through every layer (inner connection state transitions,
@@ -2408,11 +2417,32 @@ internal void RemoveWeakReference(object value)
24082417

24092418
// ClosedBusy->Closed (never opened)
24102419
// Connecting->Closed (exception during open, return to previous closed state)
2420+
/// <summary>
2421+
/// Finalizes a non-evented transition from a transitional inner state to a stable closed state.
2422+
/// </summary>
2423+
/// <param name="to">The target inner connection state object.</param>
2424+
/// <remarks>
2425+
/// This path is restricted to transition sources that intentionally serialize external callers while state is
2426+
/// being updated. A compare-and-swap prevents stale writers from overwriting a newer inner connection state.
2427+
/// </remarks>
24112428
internal void SetInnerConnectionTo(DbConnectionInternal to)
24122429
{
24132430
Debug.Assert(_innerConnection != null, "null InnerConnection");
24142431
Debug.Assert(to != null, "to null InnerConnection");
2415-
_innerConnection = to;
2432+
2433+
DbConnectionInternal originalInnerConnection = _innerConnection;
2434+
if (originalInnerConnection != DbConnectionClosedBusy.SingletonInstance &&
2435+
originalInnerConnection != DbConnectionClosedConnecting.SingletonInstance)
2436+
{
2437+
// SetInnerConnectionTo is only valid while leaving a known transitional state.
2438+
throw ADP.InternalConnectionError(ADP.ConnectionError.InvalidSourceStateForSetInnerConnectionTo);
2439+
}
2440+
2441+
if (originalInnerConnection != Interlocked.CompareExchange(ref _innerConnection, to, originalInnerConnection))
2442+
{
2443+
// Reject stale writers when another thread already advanced the state machine.
2444+
throw ADP.InternalConnectionError(ADP.ConnectionError.FailedToCommitSetInnerConnectionTo);
2445+
}
24162446
}
24172447

24182448
// Closed->Connecting: prevent set_ConnectionString during Open
@@ -2430,14 +2460,37 @@ internal bool SetInnerConnectionFrom(DbConnectionInternal to, DbConnectionIntern
24302460

24312461
// OpenBusy->Closed (previously opened)
24322462
// Connecting->Open
2463+
/// <summary>
2464+
/// Finalizes an evented state transition from a transitional inner state to a stable target state.
2465+
/// </summary>
2466+
/// <param name="to">The target inner connection state object.</param>
2467+
/// <remarks>
2468+
/// This path is intentionally restricted to the expected transition sources (<see cref="DbConnectionOpenBusy"/>
2469+
/// and <see cref="DbConnectionClosedConnecting"/>). A compare-and-swap enforces that the transition commits only
2470+
/// if no concurrent writer has already changed <see cref="_innerConnection"/>.
2471+
/// </remarks>
24332472
internal void SetInnerConnectionEvent(DbConnectionInternal to)
24342473
{
24352474
Debug.Assert(_innerConnection != null, "null InnerConnection");
24362475
Debug.Assert(to != null, "to null InnerConnection");
24372476

2438-
ConnectionState originalState = _innerConnection.State & ConnectionState.Open;
2477+
DbConnectionInternal originalInnerConnection = _innerConnection;
2478+
if (originalInnerConnection != DbConnectionOpenBusy.SingletonInstance &&
2479+
originalInnerConnection != DbConnectionClosedConnecting.SingletonInstance)
2480+
{
2481+
// Evented transitions are valid only from the two expected transitional sources.
2482+
throw ADP.InternalConnectionError(ADP.ConnectionError.InvalidSourceStateForSetInnerConnectionEvent);
2483+
}
2484+
2485+
ConnectionState originalState = originalInnerConnection.State & ConnectionState.Open;
24392486
ConnectionState currentState = to.State & ConnectionState.Open;
24402487

2488+
if (originalInnerConnection != Interlocked.CompareExchange(ref _innerConnection, to, originalInnerConnection))
2489+
{
2490+
// Ensure event and close-count side effects are tied to a committed transition only.
2491+
throw ADP.InternalConnectionError(ADP.ConnectionError.FailedToCommitSetInnerConnectionEvent);
2492+
}
2493+
24412494
if ((originalState != currentState) && (ConnectionState.Closed == currentState))
24422495
{
24432496
unchecked
@@ -2446,8 +2499,6 @@ internal void SetInnerConnectionEvent(DbConnectionInternal to)
24462499
}
24472500
}
24482501

2449-
_innerConnection = to;
2450-
24512502
if (ConnectionState.Closed == originalState && ConnectionState.Open == currentState)
24522503
{
24532504
OnStateChange(DbConnectionInternal.StateChangeOpen);
@@ -2574,9 +2625,10 @@ private void ConnectionString_Set(ConnectionPoolKey key)
25742625
flag = SetInnerConnectionFrom(DbConnectionClosedBusy.SingletonInstance, connectionInternal);
25752626
if (flag)
25762627
{
2628+
// Publish option/pool metadata before releasing the busy state back to a stable closed state.
25772629
_userConnectionOptions = connectionOptions;
25782630
_poolGroup = poolGroup;
2579-
_innerConnection = DbConnectionClosedNeverOpened.SingletonInstance;
2631+
SetInnerConnectionTo(DbConnectionClosedNeverOpened.SingletonInstance);
25802632
}
25812633
}
25822634
if (!flag)

src/Microsoft.Data.SqlClient/tests/UnitTests/Microsoft/Data/SqlClient/SqlConnectionConcurrentOpenTests.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,11 @@ private static DbConnectionInternal GetConnectingSingleton()
3636

3737
private static void ForceInnerConnection(SqlConnection connection, DbConnectionInternal innerConnectionValue)
3838
{
39-
connection.SetInnerConnectionTo(innerConnectionValue);
39+
// Use SetInnerConnectionFrom to transition from the current state via CAS.
40+
// SetInnerConnectionTo requires the connection to already be in a transitional state.
41+
DbConnectionInternal current = connection.InnerConnection;
42+
bool swapped = connection.SetInnerConnectionFrom(innerConnectionValue, current);
43+
Assert.True(swapped, $"CAS failed: expected current state {current.GetType().Name} but it changed concurrently.");
4044
}
4145

4246
private static bool InvokeTryOpenInner(SqlConnection connection, TaskCompletionSource<DbConnectionInternal> retry)

0 commit comments

Comments
 (0)