Repository navigation
Description
Description
On browser-wasm with RunAOTCompilation=true, once a catch (...) when (...) filter has caught an exception, the
method reads its arguments wrong for the rest of the method, both in the catch block and after it. A string
argument reads as empty and an int argument reads as 1. this counts as an argument too, so instance fields, and an
iterator's captured arguments (which live on this), read wrong as well. Locals are kept, including ones set
before the try or inside the catch. Arguments read in the filter expression itself are right.
The same code is right in the WebAssembly interpreter (RunAOTCompilation=false) and on CoreCLR. A plain catch that
rethrows what it doesn't handle (catch (Exception e) { if (e is not X) throw; ... }) keeps everything.
In a real library this led to a crash: a filter catch when (!leaveOpen) { stream.Dispose(); throw; } disposed a
garbage stream argument, and the runtime died with an assertion in class-accessors.c:72 instead of rethrowing the
InvalidDataException.
Reproduction Steps
filterprobe.csproj:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<RuntimeIdentifier>browser-wasm</RuntimeIdentifier>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<WasmMainJSPath>main.mjs</WasmMainJSPath>
</PropertyGroup>
</Project>main.mjs:
import { dotnet } from './_framework/dotnet.js';
await dotnet.withApplicationArguments(...process.argv.slice(2)).run();Program.cs:
using System.Runtime.CompilerServices;
var p = new Probe { Flag = true };
Console.WriteLine("1 arg after filter: [" + Probe.ArgAfterFilter("name") + "]");
Console.WriteLine("2 arg after plain: [" + Probe.ArgAfterPlain("name") + "]");
Console.WriteLine("3 local set in filtered catch: " + p.LocalSetInCatch());
Console.WriteLine("4 local before try: " + Probe.LocalBeforeTry(7));
Console.WriteLine("5 int arg after filter: " + Probe.IntArgAfterFilter(42));
Console.WriteLine("6 arg in filter: " + Probe.ArgInFilter("x"));
Console.WriteLine("7 interpolated in catch: [" + Probe.Interpolated("name") + "]");
Console.WriteLine("8 rethrow outer: [" + Probe.Outer("name") + "]");
return 0;
internal sealed class Probe
{
public bool Flag { get; set; }
[MethodImpl(MethodImplOptions.NoInlining)]
static void Throw() => throw new InvalidDataException("x");
// The argument read after a filter caught.
[MethodImpl(MethodImplOptions.NoInlining)]
public static string ArgAfterFilter(string name)
{
string r;
try { Throw(); r = "ok"; }
catch (Exception e) when (e is InvalidDataException or NotSupportedException) { r = e.GetType().Name; }
return name + ": " + r;
}
// The same with a plain catch that rethrows: right.
[MethodImpl(MethodImplOptions.NoInlining)]
public static string ArgAfterPlain(string name)
{
string r;
try { Throw(); r = "ok"; }
catch (Exception e) { if (e is not InvalidDataException) throw; r = e.GetType().Name; }
return name + ": " + r;
}
// Locals set in the filtered catch: right.
[MethodImpl(MethodImplOptions.NoInlining)]
public string LocalSetInCatch()
{
List<int> a; List<int> b; bool lost = false;
try { Throw(); a = [1]; b = [2]; }
catch (InvalidDataException) when (Flag) { (a, b, lost) = ([], [], true); }
return $"{a.Count} {b.Count} {lost}";
}
// A local set before the try (y = 21) is kept; the argument x is not.
[MethodImpl(MethodImplOptions.NoInlining)]
public static int LocalBeforeTry(int x)
{
int y = x * 3;
try { Throw(); }
catch (Exception e) when (e is InvalidDataException) { }
return y + x;
}
[MethodImpl(MethodImplOptions.NoInlining)]
public static int IntArgAfterFilter(int n)
{
try { Throw(); }
catch (Exception e) when (e is InvalidDataException) { }
return n;
}
// The argument read in the filter itself: right.
[MethodImpl(MethodImplOptions.NoInlining)]
public static bool ArgInFilter(string s)
{
try { Throw(); return false; }
catch (Exception) when (s == "x") { return true; }
}
// The argument read inside the filtered catch block.
[MethodImpl(MethodImplOptions.NoInlining)]
public static string Interpolated(string name)
{
try { Throw(); return ""; }
catch (Exception e) when (e is InvalidDataException or NotSupportedException) { return $"{name}: {e.GetType().Name}"; }
}
// The exception passes an inner filter that doesn't match and is caught by an outer one.
[MethodImpl(MethodImplOptions.NoInlining)]
public static string Outer(string name)
{
try { return Inner(); }
catch (Exception e) when (e is InvalidDataException) { return name + " " + e.Message; }
}
[MethodImpl(MethodImplOptions.NoInlining)]
static string Inner()
{
try { Throw(); return ""; }
catch (Exception e) when (e is NotSupportedException) { return "wrong"; }
}
}dotnet publish -c Release -p:RunAOTCompilation=true
node bin/Release/net10.0/browser-wasm/AppBundle/main.mjs
dotnet publish -c Release -p:RunAOTCompilation=false
node bin/Release/net10.0/browser-wasm/AppBundle/main.mjs
Expected behavior
The interpreter's output (and CoreCLR's):
1 arg after filter: [name: InvalidDataException]
2 arg after plain: [name: InvalidDataException]
3 local set in filtered catch: 0 0 True
4 local before try: 28
5 int arg after filter: 42
6 arg in filter: True
7 interpolated in catch: [name: InvalidDataException]
8 rethrow outer: [name x]
Actual behavior
With RunAOTCompilation=true:
1 arg after filter: [: InvalidDataException]
2 arg after plain: [name: InvalidDataException]
3 local set in filtered catch: 0 0 True
4 local before try: 22
5 int arg after filter: 1
6 arg in filter: True
7 interpolated in catch: [: InvalidDataException]
8 rethrow outer: [ x]
Cases 1, 5, 7 and 8 read the argument after the filter caught it: name is empty and n is 1. Case 4 keeps the
local y (21) but reads the argument x as 1, giving 22 instead of 28. Cases 2 (a plain catch), 3 (locals) and 6
(the argument read in the filter itself) are right.
Regression?
Not checked against earlier versions.
Known Workarounds
Replace each catch ... when (cond) with a plain catch that rethrows when the condition fails.
Configuration
- .NET SDK 10.0.113 (runtime 10.0.13),
wasm-toolsworkload 10.0.113 - browser-wasm, run under Node.js (Emscripten 3.1.56's node); first seen the same way in a browser-side library
- Linux x64 build host
Other information
The catch-to-interpreter handoff described in #64867 (when a catch is found in AOT code, the rest of the method runs
in the interpreter) looks like the place where the arguments are lost: only values that live in the frame's argument
slots go wrong, not locals. #52394 was a different filter bug under wasm AOT (a filter evaluated after finally
blocks had run), fixed in 2021.
Activity
MihuBot commentedon Nov 1, 2026
I'm a bot. Here is a possible related and/or duplicate issue (I may be wrong):
Link dotnet/runtime#136780 from the no-exception-filters rule (#286)
dotnet-policy-service commentedon Nov 1, 2026
Tagging subscribers to 'arch-wasm': @lewing, @pavelsavara
See info in area-owners.md if you want to be subscribed.
[-][wasm][AOT] After an exception filter catches, the method's arguments read wrong (string empty, int 1)[/-][+][wasm][mono][AOT] After an exception filter catches, the method's arguments read wrong (string empty, int 1)[/+]