dotnet / dotnet/runtime

JIT: (bug) Runtime-async: callee's ExecutionContext/SynchronizationContext leaks into the caller's exception filter

Open
#133,619 3 comments 0 reactions 1 assignee Claimed by @jakobbotsch View on GitHub
area-CodeGen-coreclr runtime-async
Dominant language
C#
Stars
18.3k
Forks
5.6k
PR merge metrics
PR metrics pending

Description

## Repro

Compile with `Features=runtime-async=on` and run with `corerun`. Easiest form: drop the
body in as a `[Fact]` under `src/tests/async/` (its `Directory.Build.props` already sets
the feature).

```csharp
using System;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;

public class CtxFilter
{
static AsyncLocal s_local = new AsyncLocal();
static long? s_seenInFilter;
static SynchronizationContext s_syncCtxInFilter;

[MethodImpl(MethodImplOptions.NoInlining)]
static async Task ChangeThenThrow()
{
s_local.Value = 123;
SynchronizationContext.SetSynchronizationContext(new SynchronizationContext());
throw new Exception();
}

[MethodImpl(MethodImplOptions.NoInlining)]
static bool RecordFilter()
{
s_seenInFilter = s_local.Value;
s_syncCtxInFilter = SynchronizationContext.Current;
return true;
}

[MethodImpl(MethodImplOptions.NoInlining)]
static async Task Run()
{
s_local.Value = 43;
try
{
await ChangeThenThrow();
}
catch (Exception) when (RecordFilter())
{
Console.WriteLine($"catch body: s_local={s_local.Value}, syncctx={(SynchronizationContext.Current == null ? "null" : "non-null")}");
}

Console.WriteLine($"filter : s_local={s_seenInFilter}, syncctx={(s_syncCtxInFilter == null ? "null" : "non-null")}");
}

public static void Main() => Run().GetAwaiter().GetResult();
}
```

## Expected

```
catch body: s_local=43, syncctx=null
filter : s_local=43, syncctx=null
```

## Actual

```
catch body: s_local=43, syncctx=null
filter : s_local=123, syncctx=non-null
```

## Notes

- Reproduced on linux-x64 at `27ac7f0dea0`, on both Checked and Release Core_Root, with
and without `DOTNET_TieredCompilation=0`.
- Same source compiled **without** `runtime-async=on` (classic state machine) prints the
expected output, so this is specific to runtime-async.
- Mechanism: `Compiler::SaveAsyncContexts` (`src/coreclr/jit/async.cpp:520,640`) emits the
`AsyncHelpers.RestoreContexts` call into a handler marked `EH_HANDLER_FAULT`; the first
EH pass (`FindFirstPassHandler` in `ExceptionHandling.cs`, compiled into CoreCLR's
CoreLib) only considers `RH_EH_CLAUSE_TYPED`/`RH_EH_CLAUSE_FILTER` clauses. Switching the
handler to a `finally` would not help — that too is second-pass.
- Impact: a filter that consults an `AsyncLocal` or `SynchronizationContext.Current` to
decide whether to catch can select the wrong handler or let an exception escape.

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.