Skip to content

Add tests confirming InvalidCastException race condition in TryOpenInner (#3314)#4179

Draft
paulmedynski wants to merge 1 commit intomainfrom
dev/automation/test-issue-3314-concurrent-open-race
Draft

Add tests confirming InvalidCastException race condition in TryOpenInner (#3314)#4179
paulmedynski wants to merge 1 commit intomainfrom
dev/automation/test-issue-3314-concurrent-open-race

Conversation

@paulmedynski
Copy link
Copy Markdown
Contributor

@paulmedynski paulmedynski commented Apr 10, 2026

Summary

This` PR is currently a proof-of-concept. It should be updated to fix the actual bug before being promoted to Open.

Adds functional tests that reproduce and confirm the root cause of #3314.

When _innerConnection is in the DbConnectionClosedConnecting transitional state at the moment TryOpenInner casts it to SqlConnectionInternal, an InvalidCastException is thrown. This happens when a SqlConnection is used concurrently from multiple threads (violating its thread-safety contract).

Root Cause

In SqlConnection.TryOpenInner() (SqlConnection.cs L2228):

var tdsInnerConnection = (SqlConnectionInternal)InnerConnection;

InnerConnection is read a second time (unsynchronized) after TryOpenConnection() has already returned. Between these two reads, another thread can change _innerConnection to DbConnectionClosedConnecting — a singleton state marker that is not assignable to SqlConnectionInternal.

Tests Added

Test Validates
InnerConnection_CastToSqlConnectionInternal_ThrowsInvalidCast_WhenInConnectingState DbConnectionClosedConnectingSqlConnectionInternal cast throws InvalidCastException
InnerConnection_InConnectingState_ReportsConnectingState Connection reports Connecting state when in this transitional state
Open_WhenAlreadyConnecting_ThrowsInvalidOperation Open() on a connecting connection throws InvalidOperationException

All tests run without a SQL Server instance (functional tests using reflection).

Fixes #3314

  • Tests added
  • Public API changes documented (N/A)
  • Verified against customer repro (N/A — race condition confirmed via type analysis)
  • Ensure no breaking changes introduced (test-only change)

Add functional tests that reproduce the root cause of issue #3314:
when _innerConnection is DbConnectionClosedConnecting (a transitional
state), the cast to SqlConnectionInternal in TryOpenInner throws
InvalidCastException. This occurs when a SqlConnection is used
concurrently from multiple threads.

Tests:
- Confirm DbConnectionClosedConnecting is not assignable to SqlConnectionInternal
- Confirm the cast throws InvalidCastException
- Confirm Connecting state is reported correctly
- Confirm Open() on a connecting connection throws InvalidOperationException
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds new FunctionalTests that reproduce/confirm the InvalidCastException scenario described in #3314 by forcing SqlConnection’s internal _innerConnection into the DbConnectionClosedConnecting transitional state and validating resulting behavior.

Changes:

  • Added reflection-based tests validating that the DbConnectionClosedConnecting singleton is not assignable to SqlConnectionInternal.
  • Added a test confirming SqlConnection.State reports ConnectionState.Connecting when _innerConnection is the connecting singleton.
  • Added a test confirming Open() throws InvalidOperationException when _innerConnection is already in the connecting state.

Comment on lines +68 to +73
Exception ex = Assert.ThrowsAny<Exception>(() =>
{
// Use Convert.ChangeType or direct cast via reflection to replicate
// the CLR's cast behavior for internal types we cannot reference directly.
Convert.ChangeType(innerConnection, sqlConnectionInternalType);
});
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Convert.ChangeType(innerConnection, sqlConnectionInternalType) does not replicate the explicit CLR cast used in TryOpenInner (it uses IConvertible semantics and can throw InvalidCastException even when the runtime type is assignable). This makes the test misleading and potentially a false-positive. Consider generating an actual castclass (e.g., via Expression.Convert + compiled lambda or a small DynamicMethod) and then using Assert.Throws<InvalidCastException> directly.

Copilot uses AI. Check for mistakes.
Comment on lines +87 to +96
Type closedConnectingType = typeof(SqlConnection).Assembly
.GetType("Microsoft.Data.ProviderBase.DbConnectionClosedConnecting", throwOnError: true);
FieldInfo singletonField = closedConnectingType
.GetField("SingletonInstance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
object connectingSingleton = singletonField.GetValue(null);

var connection = new SqlConnection("Data Source=localhost");
FieldInfo innerConnectionField = typeof(SqlConnection)
.GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic);
innerConnectionField.SetValue(connection, connectingSingleton);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

This test assumes reflection lookups always succeed, but it doesn't assert singletonField, connectingSingleton, or innerConnectionField are non-null before dereferencing/using them. If internal names change, this will fail with a NullReferenceException instead of a clear assertion failure; please add explicit Assert.NotNull(...) checks (as done in the first test).

Copilot uses AI. Check for mistakes.
Comment on lines +113 to +122
Type closedConnectingType = typeof(SqlConnection).Assembly
.GetType("Microsoft.Data.ProviderBase.DbConnectionClosedConnecting", throwOnError: true);
FieldInfo singletonField = closedConnectingType
.GetField("SingletonInstance", BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public);
object connectingSingleton = singletonField.GetValue(null);

var connection = new SqlConnection("Data Source=localhost");
FieldInfo innerConnectionField = typeof(SqlConnection)
.GetField("_innerConnection", BindingFlags.Instance | BindingFlags.NonPublic);
innerConnectionField.SetValue(connection, connectingSingleton);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

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

Same as above: reflection results (singletonField, connectingSingleton, innerConnectionField) are used without null assertions. Adding Assert.NotNull(...) checks will make failures deterministic and easier to diagnose if internal member names move/are trimmed.

Copilot uses AI. Check for mistakes.
@paulmedynski paulmedynski moved this from To triage to Backlog in SqlClient Board Apr 10, 2026
@paulmedynski paulmedynski added this to the 7.1.0-preview2 milestone Apr 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Backlog

Development

Successfully merging this pull request may close these issues.

Unable to cast object of type 'Microsoft.Data.ProviderBase.DbConnectionClosedConnecting' to type 'Microsoft.Data.SqlClient.SqlInternalConnectionTds'

2 participants