Skip to content

[release/10.0] Fix synchronous cancellation race in Channels - #135891

Open
snakefoot wants to merge 1 commit into
dotnet:release/10.0from
snakefoot:release/10.0
Open

snakefoot wants to merge 1 commit into
dotnet:release/10.0from
snakefoot:release/10.0

Conversation

@snakefoot

@snakefoot snakefoot commented Oct 15, 2026 •

Copy link
Copy Markdown
Contributor

Backport of #132230 to release/10.0

Kept original NET9_0_OR_GREATER though changing to NET would probably be prettier.

@dotnet-policy-service dotnet-policy-service Bot added the community-contribution Indicates that the PR has been added by a community member label Oct 15, 2026
@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.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

Tagging subscribers to this area: @dotnet/area-system-threading-channels
See info in area-owners.md if you want to be subscribed.

@snakefoot

Copy link
Copy Markdown
Contributor Author

@PranavSenthilnathan Ready for review

@MarcelRoozekrans

Copy link
Copy Markdown

We hit this on 10.0.13 in an audit batching forwarder, and it reproduces more easily than in #132094:

  • a single reader is enough;
  • unbounded channels are affected too;
  • with WaitToReadAsync + TryRead, the race crashes the process instead of losing the item.

Minimal repro. A single reader calls ReadAsync with a 0.5 ms CancelAfter and retries on cancellation. The writer writes one item at a time and waits for it:

using System.Threading.Channels;

var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(100) { SingleReader = true });
var received = new SemaphoreSlim(0);
using var stop = new CancellationTokenSource();

var reader = Task.Run(async () =>
{
    while (!stop.IsCancellationRequested)
    {
        using var timeout = CancellationTokenSource.CreateLinkedTokenSource(stop.Token);
        timeout.CancelAfter(TimeSpan.FromTicks(5_000)); // 0.5 ms
        try
        {
            await channel.Reader.ReadAsync(timeout.Token);
            received.Release();
        }
        catch (OperationCanceledException) { }
    }
});

for (var i = 1; i <= 100_000; i++)
{
    if (!channel.Writer.TryWrite(i)) throw new InvalidOperationException("TryWrite returned false");
    if (!await received.WaitAsync(TimeSpan.FromSeconds(5)))
    {
        Console.WriteLine($"{Environment.Version}: item {i} lost. TryWrite returned true, TryPeek = {channel.Reader.TryPeek(out _)}");
        return 1;
    }
}

stop.Cancel();
await reader;
Console.WriteLine($"{Environment.Version}: all items received");
return 0;

Results on Linux x64, 100,000 items per run:

Runtime Variant Result
8.0.31, 9.0.20 every variant below all items received, every run
10.0.12, 10.0.13 as above, bounded an item is lost within 2 to 415 items in every run; TryWrite returned true and TryPeek returns false
10.0.13 Channel.CreateUnbounded with SingleReader = true same loss
10.0.13 WaitToReadAsync(timeout.Token) + TryRead in place of ReadAsync unhandled NullReferenceException in AsyncStateMachineBox.MoveNext on the thread pool, which crashes the process
10.0.13 CancelAfter of 1 ms or more no loss

We worked around it by not cancelling channel operations: one WaitToReadAsync(CancellationToken.None) stays outstanding and is raced against a Task.Delay. Code that times out channel reads with CancelAfter is exposed until this backport ships, so a 10.0 servicing fix would be much appreciated.

@snakefoot

snakefoot commented Oct 24, 2026 •

Copy link
Copy Markdown
Contributor Author

@MarcelRoozekrans Yes would also like this merged, but seems I'm on the ignore-list by Microsoft-dev-team. Probably better to post your observations here: #132230 (Ahh sorry now see they have closed the PR for comments.. Guess new issue must be created to request back-port to NET10 like #136501 and give thumbs-up)

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-System.Threading.Channels community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants