Skip to content

missing cleanup of CancellationTokenSource#4009

Open
SimonCropp wants to merge 2 commits intodotnet:mainfrom
SimonCropp:missing-cleanup-of-CancellationTokenSource-
Open

missing cleanup of CancellationTokenSource#4009
SimonCropp wants to merge 2 commits intodotnet:mainfrom
SimonCropp:missing-cleanup-of-CancellationTokenSource-

Conversation

@SimonCropp
Copy link
Contributor

No description provided.

@SimonCropp SimonCropp requested a review from a team as a code owner March 6, 2026 12:21
Copilot AI review requested due to automatic review settings March 6, 2026 12:21
@github-project-automation github-project-automation bot moved this to To triage in SqlClient Board Mar 6, 2026
Copy link
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

This PR adds missing Dispose() calls on CancellationTokenSource instances across multiple files in the SQL client library. Undisposed CTS objects can cause minor resource leaks (they hold internal kernel event handles on some platforms), so cleaning them up is a good practice.

Changes:

  • Disposes _disposalTokenSource in SqlSequentialTextReader and SqlSequentialStream during their Dispose(bool) methods
  • Disposes _cancelAsyncOnCloseTokenSource in SqlDataReader.Close() and CloseReaderFromConnection() after cancelling it
  • Disposes reconnection CTS in SqlConnection.Close() and ReconnectAsync(), with field nulling for safety
  • Disposes timeoutCts in all continuation paths (success, failure, cancellation) in SqlCommand.Reader.cs and SqlCommand.NonQuery.cs
  • Converts local CTS to a using declaration in SsrpClient.netcore.cs

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
SqlSequentialTextReader.cs Adds _disposalTokenSource.Dispose() in Dispose(bool) after SetClosed()
SqlSequentialStream.cs Adds _disposalTokenSource.Dispose() in Dispose(bool) after SetClosed()
SqlDataReader.cs Adds _cancelAsyncOnCloseTokenSource.Dispose() in Close() and CloseReaderFromConnection()
SqlConnection.cs Adds CTS disposal in Close() and ReconnectAsync() with null-field pattern
SqlCommand.Reader.cs Disposes timeoutCts in all continuation paths of reconnect setup
SqlCommand.NonQuery.cs Disposes timeoutCts in all continuation paths of reconnect setup
SsrpClient.netcore.cs Changes local CTS to a using declaration for automatic disposal

Comment on lines 883 to +884
_cancelAsyncOnCloseTokenSource.Cancel();
_cancelAsyncOnCloseTokenSource.Dispose();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Calling Dispose() on _cancelAsyncOnCloseTokenSource here makes Close() unsafe to be called multiple times. Before this change, calling Close() twice was safe because Cancel() on an already-cancelled CTS is a no-op. Now, the second call to Close() will invoke Cancel() (line 883) on an already-disposed CancellationTokenSource, which throws ObjectDisposedException.

This is a realistic scenario: Dispose(bool) calls Close(), and the ADO.NET contract allows both Close() and Dispose() to be called (e.g., reader.Close(); reader.Dispose(); or just using + explicit Close()). Also, CloseReaderFromConnection() calls Close() (line 1114) for the OpenLoggedIn case, which could overlap.

To fix, either add an early return guard (if (_isClosed) return;) before the Cancel()/Dispose() calls, or null-check the source using an interlocked swap pattern (e.g., Interlocked.Exchange(ref _cancelAsyncOnCloseTokenSource, null)?.Cancel()/?.Dispose()).

Suggested change
_cancelAsyncOnCloseTokenSource.Cancel();
_cancelAsyncOnCloseTokenSource.Dispose();
var cancelTokenSource = Interlocked.Exchange(ref _cancelAsyncOnCloseTokenSource, null);
cancelTokenSource?.Cancel();
cancelTokenSource?.Dispose();

Copilot uses AI. Check for mistakes.
Comment on lines 1124 to +1125
_cancelAsyncOnCloseTokenSource.Cancel();
_cancelAsyncOnCloseTokenSource.Dispose();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Same issue as in Close(): if Close() was previously called (which disposes _cancelAsyncOnCloseTokenSource), and then CloseReaderFromConnection() is called on the broken-connection path, calling Cancel() on the already-disposed CTS will throw ObjectDisposedException. The comment on line 1119 even notes this must be thread-safe. Consider using the same guard pattern recommended for Close() (e.g., Interlocked.Exchange or null-check after swap).

Suggested change
_cancelAsyncOnCloseTokenSource.Cancel();
_cancelAsyncOnCloseTokenSource.Dispose();
var cancelTokenSource = Interlocked.Exchange(ref _cancelAsyncOnCloseTokenSource, null);
if (cancelTokenSource != null)
{
cancelTokenSource.Cancel();
cancelTokenSource.Dispose();
}

Copilot uses AI. Check for mistakes.
{
// Set the textreader as closed
SetClosed();
_disposalTokenSource.Dispose();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Adding _disposalTokenSource.Dispose() after SetClosed() makes Dispose(bool) unsafe for double-disposal. On the second call, SetClosed() calls _disposalTokenSource.Cancel() (line 292) which will throw ObjectDisposedException because the CTS was already disposed on the first call.

Additionally, SetClosed() can be called externally from SqlDataReader.CloseActiveSequentialStreamAndTextReader(), followed by an eventual Dispose() call — the Dispose() would work, but a subsequent second Dispose() would still crash.

Consider guarding: either check if already disposed/closed before calling SetClosed(), or swap the order so Dispose() happens before Cancel() is called again (e.g., use a disposed flag, or move the Cancel() call out of SetClosed() into Dispose() with a guard).

Copilot uses AI. Check for mistakes.
{
// Set the stream as closed
SetClosed();
_disposalTokenSource.Dispose();
Copy link

Copilot AI Mar 6, 2026

Choose a reason for hiding this comment

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

Same issue as in SqlSequentialTextReader: adding _disposalTokenSource.Dispose() after SetClosed() makes Dispose(bool) unsafe for double-disposal. On the second call, SetClosed() calls _disposalTokenSource.Cancel() (line 238) on the already-disposed CTS, which throws ObjectDisposedException.

SetClosed() can also be called externally from SqlDataReader.CloseActiveSequentialStreamAndTextReader() before the user calls Dispose(), so this pattern needs a guard to prevent calling Cancel() on a disposed CTS.

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To triage

Development

Successfully merging this pull request may close these issues.

2 participants