JIT: (bug) Runtime-async: callee's ExecutionContext/SynchronizationContext leaks into the caller's exception filter
- 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
Assessment
This issue has not been assessed yet.