Fix TryOpenInner InvalidCast race and add unit regression tests (#3314)#4179
Fix TryOpenInner InvalidCast race and add unit regression tests (#3314)#4179paulmedynski wants to merge 4 commits intomainfrom
Conversation
There was a problem hiding this comment.
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
DbConnectionClosedConnectingsingleton is not assignable toSqlConnectionInternal. - Added a test confirming
SqlConnection.StatereportsConnectionState.Connectingwhen_innerConnectionis the connecting singleton. - Added a test confirming
Open()throwsInvalidOperationExceptionwhen_innerConnectionis already in the connecting state.
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
3c6fdaf to
6d92d49
Compare
…t review - Rename test to TryOpenInner_WhenInnerConnectionRacesToNonSqlConnectionInternalState_* to accurately reflect that the test exercises a non-SqlConnectionInternal state (DbConnectionOpenBusy), not a connecting state. - Rename local variables connectingSingleton -> initialConnectingState and openBusyInstance -> racedNonSqlConnectionInternalState for clarity. - Use captured innerConnection.State in exception path instead of re-reading State via property, ensuring the thrown message reflects the same snapshot as the type check (avoids concurrent read under contention). Tests: all 4 concurrent open unit tests pass on net9.0, net10.0.
paulmedynski
left a comment
There was a problem hiding this comment.
Addressed in commit b077628. Test was renamed to TryOpenInner_WhenInnerConnectionRacesToNonSqlConnectionInternalState_ThrowsInvalidOperation_NotInvalidCast to accurately reflect that it exercises a non-SqlConnectionInternal state (DbConnectionOpenBusy), not a connecting state. Local variables renamed for clarity: connectingSingleton → initialConnectingState, openBusyInstance → racedNonSqlConnectionInternalState. This improves regression readability and avoids confusion about which transitional state the test validates. All 4 concurrent open tests pass on net9.0, net10.0.
paulmedynski
left a comment
There was a problem hiding this comment.
Addressed in commit b077628. Exception path now uses innerConnection.State instead of re-reading State, ensuring the thrown message reflects the same snapshot as the type check. This avoids a potential concurrent read under contention and keeps the entire sequence aligned with the captured local variable. See line 2237 in src/Microsoft.Data.SqlClient/src/Microsoft/Data/SqlClient/SqlConnection.cs.
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4179 +/- ##
==========================================
- Coverage 73.62% 64.28% -9.35%
==========================================
Files 277 272 -5
Lines 42988 65786 +22798
==========================================
+ Hits 31650 42289 +10639
- Misses 11338 23497 +12159
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
Summary
Fixes the
InvalidCastExceptionrace inSqlConnection.TryOpenInner()reported in #3314 and adds regression tests in UnitTests.The fix hardens
TryOpenInneragainst concurrent state transitions on a sharedSqlConnectioninstance:InnerConnectiononce into a local variableInvalidOperationException(ADP.ConnectionAlreadyOpen(State)) for non-SqlConnectionInternaltransitional states instead of surfacing an opaqueInvalidCastExceptionRoot Cause
TryOpenInnerpreviously performed an unsynchronized second read/cast:Between
TryOpenConnection()and this cast, another thread could transition_innerConnectiontoDbConnectionClosedConnecting, which is not assignable toSqlConnectionInternal, causingInvalidCastException.Fix
In
SqlConnection.TryOpenInner():This removes the crashy cast path and yields a controlled, meaningful exception under concurrent misuse.
Tests
Regression tests were moved from FunctionalTests to UnitTests because they do not require SQL Server or external resources.
Added in
SqlConnectionConcurrentOpenTests:InnerConnection_DbConnectionClosedConnecting_IsNotAssignableToSqlConnectionInternalInnerConnection_InConnectingState_ReportsConnectingStateOpen_WhenAlreadyConnecting_ThrowsInvalidOperationTryOpenInner_WhenInnerConnectionRacesToConnectingState_ThrowsInvalidOperation_NotInvalidCastValidation:
net8.0,net9.0, andnet10.0.Fixes #3314