Skip to content

Fix synchronous cancellation race in Channels - #132230

Merged
steveisok merged 4 commits into
mainfrom
steveisok-investigate-bounded-channel-loss
Aug 21, 2026
Merged

Fix synchronous cancellation race in Channels#132230
steveisok merged 4 commits into
mainfrom
steveisok-investigate-bounded-channel-loss

Conversation

@steveisok

Copy link
Copy Markdown
Member

Fixes#129796
Fixes#132094

AsyncOperation used _cancellationRegistration.Token to determine whether completion needed atomic reservation. If cancellation ran synchronously inside UnsafeRegister, the registration had not yet been assigned, so the callback observed a default non-cancelable token and completed without reserving the operation.

A reader or waiter could then be reserved and completed a second time. This caused linked-list assertion failures in RendezvousChannel and could silently lose items during a BoundedChannel direct handoff.

Use an immutable _isCancelable value initialized before registration can invoke the callback. This preserves the non-cancelable fast path, pooling behavior, and registration cleanup without retaining the full CancellationToken on modern .NET.

Adds deterministic coverage for:

  • synchronous cancellation during registration
  • skipping a canceled bounded-channel reader during direct handoff
  • delayed rendezvous waiter removal after the list has been drained

The added field fits existing object padding; BlockedReadAsyncOperation<int> remains 96 bytes.

Validation

  • System.Threading.Channels Debug tests: 1,599 passed, 2 existing stress tests skipped
  • Debug and Release library builds: succeeded with no warnings
  • Release 3,000,000-item repro: 3,000,000 written and read, no missing items

Note

This pull request description was generated with the assistance of GitHub Copilot.

Use stable cancelability state initialized before cancellation registration, and add deterministic regression coverage for bounded and rendezvous channels.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

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 fixes a race in System.Threading.Channels async operations where synchronous cancellation during token registration could cause completion to proceed without an atomic reservation, enabling double-completion and corrupting waiter/reader linked lists (with downstream assertion failures and potential item loss).

Changes:

  • Make cancelability an immutable construction-time property (_isCancelable) and use it for completion reservation decisions, avoiding reliance on _cancellationRegistration.Token when registration may be a default value.
  • Add test infrastructure to construct synchronously-canceled internal channel async operations and inspect/modify their linked-list state.
  • Add deterministic regression tests for rendezvous waiter removal and bounded-channel direct handoff skipping canceled readers.
Show a summary per file
FileDescription
src/libraries/System.Threading.Channels/src/System/Threading/Channels/AsyncOperation.csIntroduces _isCancelable and switches completion reservation/assert logic to use it, addressing the synchronous-cancellation registration race.
src/libraries/System.Threading.Channels/tests/TestBase.csAdds reflection-based helpers to create/cancel internal async operations and manipulate their list links/heads for deterministic regression coverage.
src/libraries/System.Threading.Channels/tests/RendezvousChannelTests.csAdds a regression test validating delayed removal of a synchronously-canceled waiter doesn’t corrupt the waiting-reader list.
src/libraries/System.Threading.Channels/tests/BoundedChannelTests.csAdds regression tests for synchronous cancellation during registration and for skipping canceled readers during direct handoff.

Review details

  • Files reviewed: 4/4 changed files
  • Comments generated: 1
  • Review effort level: Lite

Comment threadsrc/libraries/System.Threading.Channels/tests/TestBase.cs Outdated
Comment threadsrc/libraries/System.Threading.Channels/tests/TestBase.cs Outdated
@jkotas

Copy link
Copy Markdown
Member

/azp run runtime-nativeaot-outerloop

@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 1 pipeline(s).

@tannergooding

Copy link
Copy Markdown
Member

The change LGTM, but Jan's test feedback is applicable still

@tannergoodingtannergooding added the needs-author-action An issue or pull request that requires more info or actions from the author. label Aug 18, 2026
@PranavSenthilnathan

Copy link
Copy Markdown
Member

The _isCancelable fix closes one path, but #129796’s exact assertion remains reachable independently.

Cancellation can queue removal while an operation is still linked. TryComplete then sets the live head to null under lock and clears the detached node’s Previous and Next outside the lock. Remove may observe those fields between writes because it asserts before checking the null head.

Since head == null means the operation no longer belongs to the live list, the fix is simply:

if(headisnull){return;}Debug.Assert(op.Nextisnull==op.Previousisnull);

I suggest including this fix in this PR because it closes #129796; otherwise the same Known Build Error remains possible. It does not change release behavior.

Note

GitHub Copilot helped draft this comment.

Replace reflection-based white-box coverage with a single NativeAOT-compatible UnsafeAccessor test.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 7b4977f8-095c-4594-83da-8acc9a24fe5b
CopilotAI review requested due to automatic review settings August 18, 2026 20:08
@dotnet-policy-servicedotnet-policy-serviceBot removed the needs-author-action An issue or pull request that requires more info or actions from the author. label Aug 18, 2026

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 2/2 changed files
  • Comments generated: 1
  • Review effort level: Lite

Check for a detached operation list before asserting link consistency, as completion may clear detached links concurrently.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 7b4977f8-095c-4594-83da-8acc9a24fe5b
CopilotAI review requested due to automatic review settings August 18, 2026 20:27

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

Suppressed comments (1)

src/libraries/System.Threading.Channels/tests/BoundedChannelTests.cs:541

  • The PR description claims deterministic coverage was added for additional scenarios (skipping a canceled bounded-channel reader during direct handoff, and delayed rendezvous waiter removal after the list has been drained), but the diff here only adds the synchronous-cancellation-during-registration test. Either add the missing tests (likely in BoundedChannelTests/RendezvousChannelTests) or update the PR description so it accurately reflects the changes in this PR.
#if NET
[Fact]
public async Task AsyncOperation_SynchronousCancellationDuringRegistration_ReservesCompletion()
{
using var cts = new CancellationTokenSource();
cts.Cancel();
object operation = AsyncOperationAccessors<int>.CreateBlockedReadAsyncOperation(
runContinuationsAsynchronously: true,
cts.Token,
pooled: false,
static (state, token) => Assert.True(AsyncOperationAccessors<int>.TrySetCanceled(state, token)));
var valueTask = new ValueTask<int>((IValueTaskSource<int>)operation, token: 0);
await AssertExtensions.CanceledAsync(cts.Token, async () => await valueTask);
Assert.False(AsyncOperationAccessors<int>.TryReserveCompletionIfCancelable(operation));
}
  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@steveisok

Copy link
Copy Markdown
MemberAuthor

@PranavSenthilnathan can I get another review on this?

@PranavSenthilnathan

Copy link
Copy Markdown
Member

The mono/wasm tests failures look valid: System.MissingMethodException : Could not find constructor. That test should probably be excluded from mono.

The generic UnsafeAccessorType constructor binding is unsupported on Mono and fails across desktop and browser runtimes. Keep the regression covered by CoreCLR and NativeAOT.\n\nCo-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>\nCopilot-Session: 7b4977f8-095c-4594-83da-8acc9a24fe5b
CopilotAI review requested due to automatic review settings August 21, 2026 02:58

CopilotAI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review details

  • Files reviewed: 3/3 changed files
  • Comments generated: 0 new
  • Review effort level: Lite

@steveisok
steveisok enabled auto-merge (squash) August 21, 2026 18:07
@steveisok
steveisok merged commit e4991bf into mainAug 21, 2026
79 checks passed
@steveisok
steveisok deleted the steveisok-investigate-bounded-channel-loss branch August 21, 2026 18:08
@steveisok

Copy link
Copy Markdown
MemberAuthor

/backport to release/11.0-rc1

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/11.0-rc1 (link to workflow run)

steveisok added a commit that referenced this pull request Aug 21, 2026
…2638)
Backport of #132230 to release/11.0-rc1
/cc @steveisok
## Customer Impact
- [ ] Customer reported
- [ ] Found internally
[Select one or both of the boxes. Describe how this issue impacts
customers, citing the expected and actual behaviors and scope of the
issue. If customer-reported, provide the issue number.]
## Regression
- [ ] Yes
- [ ] No
[If yes, specify when the regression was introduced. Provide the PR or
commit if known.]
## Testing
[How was the fix verified? How was the issue missed previously? What
tests were added?]
## Risk
[High/Medium/Low. Justify the indication by mentioning how risks were
measured and addressed.]
**IMPORTANT**: If this backport is for a servicing release, please
verify that:
- For .NET 8 and .NET 9: The PR target branch is `release/X.0-staging`,
not `release/X.0`.
- For .NET 10+: The PR target branch is `release/X.0` (no `-staging`
suffix).
## Package authoring no longer needed in .NET 9
**IMPORTANT**: Starting with .NET 9, you no longer need to edit a NuGet
package's csproj to enable building and bump the version.
Keep in mind that we still need package authoring in .NET 8 and older
versions.
Co-authored-by: Steve Pfister <steveisok@users.noreply.github.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

6 participants

@steveisok@jkotas@tannergooding@PranavSenthilnathan@jeffhandley