[CoreCLR debugger] Function evaluation at user-unhandled notification can resume the app without Continue
维护者通常 1 天内回复
还没有人认领这个 Issue。
评估
调研方向
Start by tracing the supported coreclr_mobile launch configuration and the debugger handoff across the mobile debug engine, remote provider/profiler, and runtime; the report does not identify which component causes the resume. Use the managed SharedFixture reproduction and its CATCH_CONTROL and notification stops to compare actual method evaluation and target progress. Done means evaluation completes or is explicitly rejected while the target remains stopped until Continue, with a regression covering a real method invocation.
由索引模型根据 Issue 内容生成。
描述
Android framework version
net11.0-android (Preview)
Affected platform version
| Component | Tested version |
|---|---|
| Android source baseline | Clean, independently built main at eec176c804f89d846a24801e166f5b569f7a6bed |
| Build SDK | 12.0.100-alpha.1.26523.103 |
| Actual Android runtime | Microsoft.NETCore.App.Runtime.android-arm64/11.0.0-preview.7.26381.103 |
| Actual CoreLib informational version | 11.0.0-preview.7.26381.103+e2c1e00b3d0f96afb892fb261d5921565b400246 |
| App | net11.0-android37.0, ARM64 CoreCLR; PublishAot=false, Optimize=false, PublishReadyToRun=false |
| Device | Android 15 / API 35 ARM64 emulator |
| Host | macOS 26.7 / ARM64 |
| VS Code Insiders | 1.144.0-insider |
| C# extension | 11.7.18 |
| MAUI extension / paired mobile debugger resources | 11.0.114 |
| vsdbg | 18.13.11021.6, commit f27481280582c85f53886696125e0190e736c56b |
The SDK version is not the target runtime version. Runtime provenance above was read from the running app and restored runtime package. CoreLib's e2c1e00... is a dotnet/dotnet VMR commit; its source manifest maps runtime to 253bde0a42aff6d8e99e039db1e710c5e1f5436a.
Description
Tooling/debugger handoff: /cc @rmarinho, following the mobile-debugger escalation used in #10141. Please triage ownership across the supported mobile launch configuration, debug engine, remote provider/profiler, and runtime.
At a Debugger.BreakForUserUnhandledException(exception) stop, evaluating a method can resume ordinary application execution without a user Continue, leave the evaluate request pending, and eventually fail after the evaluation thread has exited. This is more serious than an expression being unavailable at an optimized/GC-unsafe stop: the debugger should either finish the evaluation and keep the original stop, or reject it while retaining the stop.
This is independent of the generated JNI boundary attributes in #13154. The problem reproduces in a separately built clean-main application with a tiny pure-managed callback/catch/notification, no NUnit, no JNI callback, and no constructor/export generated forwarder on the reproducing path. A separate real-JNI acceptance run exercises the existing JniMarshal.SafeInvokeFunc<int> path. The investigation does not establish that #13154 introduced this behavior.
Normal attach, ordinary source breakpoints, the user-unhandled exception stop itself, value inspection, and explicit Continue work. Actual managed and Java method evaluation works at the ordinary source breakpoint in the same catch on the same thread, but fails at the subsequent notification stop with the baseline configuration. This is not a claim that debugging as a whole is broken.
Requested tooling follow-up
- Determine whether the supported Android debugging launch should arrange the necessary runtime/debugger optimization settings, rather than requiring users to discover the workaround below.
- Fix the default failure path: an ineligible evaluation must not silently run application code past the stop or strand a pending evaluate. A clear failed-evaluation response with the target still stopped is acceptable when evaluation really is unavailable.
- Add a regression covering a real method invocation at this debugger-recognized notification, successful or explicitly rejected, with no ordinary target progress until user Continue. Do not rely on constants or debugger-interpreted expressions as evidence of actual runtime evaluation.
No product patch is proposed here: the captured diagnostics do not identify which native setup/resume/completion subcomponent is responsible. This report is not a request to change JNI exception policy, move notification after JNI Throw, or impose debug settings on shipping applications.
Steps to Reproduce
Minimal shared managed source
This exact file was used for both desktop and Android comparison. SHA-256: 53d8869d1c253a40bd8cfdd6a69b18a30918f092e7af4efceb86503d418c2265.
using System;
using System.Diagnostics;
using System.Runtime.CompilerServices;
namespace NotificationRepro;
public static class SharedFixture
{
public static int EvaluationCount;
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static int Return42()
{
EvaluationCount++;
return 42;
}
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static void Callback()
{
throw new InvalidOperationException("notification-func-eval");
}
[DebuggerNonUserCode]
[DebuggerDisableUserUnhandledExceptions]
[MethodImpl(MethodImplOptions.NoInlining | MethodImplOptions.NoOptimization)]
public static void Boundary(Action callback)
{
try
{
callback();
}
catch (Exception exception)
{
Console.WriteLine("CATCH_CONTROL"); // Ordinary source breakpoint.
Debugger.BreakForUserUnhandledException(exception);
Console.WriteLine("AFTER_NOTIFICATION");
}
}
}
In an Android Activity, run the fixture on a dedicated worker so worker exit is observable while the UI process remains alive:
new System.Threading.Thread(() => {
Console.WriteLine("REPRO_BEGIN");
SharedFixture.Boundary(SharedFixture.Callback);
Console.WriteLine($"REPRO_COMPLETE EvaluationCount={SharedFixture.EvaluationCount}");
}) { Name = "NotificationRepro" }.Start();
The minimal reproducing path has no Java invocation unless requested from the debugger.
App and supported debugger configuration
Use an ARM64 CoreCLR Android application, with the paired MAUI mobile debugger target/host resources, internet permission, portable symbols, Optimize=false, PublishTrimmed=false, PublishAot=false, and PublishReadyToRun=false. The last property was already present in the failing configuration.
For the controlled repro, the installed target profiler is libvsdbgremotecoreclrtarget.so; its SHA-256 is 0c03b692261861e0c1c25f015078c5e3645a45e4087b82c029b174613cce069d. The six profiler/transport runtime variables were:
CORECLR_ENABLE_PROFILING=1
CORECLR_PROFILER={9DC623E8-C88F-4FD5-AD99-77E67E1D9631}
CORECLR_PROFILER_PATH=libvsdbgremotecoreclrtarget.so
CORECLR_REMOTE_DEBUGGER_PORT=56130
CORECLR_REMOTE_DEBUGGER_ISSERVER=1
CORECLR_REMOTE_DEBUGGER_IP=127.0.0.1
Use the supported C# extension debug API/handshake, not a standalone vsdbg handshake bypass. The relevant launch configuration is:
{
"type": "coreclr_mobile",
"request": "launch",
"name": "Notification evaluation repro",
"program": "<app-output>/NotificationRepro.dll",
"stopAtEntry": false,
"justMyCode": true,
"exceptionBreakpointFilters": ["user-unhandled"],
"expressionEvaluationOptions": { "allowFastEvaluate": false },
"logging": {
"engineLogging": true,
"diagnosticsLog": {
"expressionEvaluationTracing": true,
"debugRuntimeEventTracing": true,
"debugEngineAPITracing": "error"
}
},
"coreClrMobileDebuggerOptions": {
"ip": "127.0.0.1",
"port": 56130,
"platform": "android;arm64",
"isServer": false,
"assetsPath": "<linked-managed-assets-directory>/;<paired-MAUI-resources>/VsdbgRemoteCoreclrHost/osx-arm64"
}
}
The linked-assets directory must retain the trailing /. An earlier missing slash produced an unrelated metadata file-not-found failure, including for constants; that setup problem was corrected before the reproducing/passing comparisons reported here.
Configure the user-unhandled filter before the worker executes. In the manual prebuilt-app setup, forward the chosen port and enable the CoreCLR bootstrap on the test emulator before starting the target:
adb -s <test-emulator> forward tcp:56130 tcp:56130
adb -s <test-emulator> shell setprop debug.coreclr.enabled 1
Start/configure the supported adapter, then launch the app promptly. Reset the property and remove the forward afterward. The automated comparison serialized stop handlers, recorded every DAP request/response/event, rejected stale stops, and never issued fallback Continue on inspection/evaluation failure.
Comparison
- Put a source breakpoint on
Console.WriteLine("CATCH_CONTROL")(line 35 of the exact shared file). Run with user-unhandled exceptions enabled. - Select the live
SharedFixture.Boundaryframe, not an[Exception]reconstructed frame. Evaluate sequentially:1 + 1NotificationRepro.SharedFixture.Return42()NotificationRepro.SharedFixture.EvaluationCountSystem.GC.GetTotalMemory(false)Java.Lang.JavaSystem.CurrentTimeMillis()on Android.
- At the ordinary breakpoint, all method calls complete with actual DBI
EvalCompletecallbacks. Observe the target held for at least 2 seconds, then explicitly Continue. - At the subsequent user-unhandled notification stop, repeat evaluation. The physical top frame is
System.Diagnostics.Debugger.BreakForUserUnhandledException; the selected live caller isSharedFixture.Boundaryat line 36. Constant evaluation succeeds. In the Android baseline,Return42()starts evaluation but the application runs pastAFTER_NOTIFICATIONand exits its worker, without executing the requested helper or receiving user Continue. The request later fails. - Repeat with both workaround settings below. Actual managed and Java evaluation now completes at both stops; the target remains held until each explicit Continue.
Desktop controls and source interpretation
Supported desktop coreclr comparison used the same source, C# 11.7.18 / vsdbg 18.13.11021.6, and installed .NET 10.0.13 and 11.0.0-rc.2.26504.105. These desktop runtimes are not version-matched to Android preview 7.
| Desktop runtime | Runtime ReadyToRun | suppressJITOptimizations |
Notification GC.GetTotalMemory(false) |
|---|---|---|---|
| 11 RC2 | default | false | Immediate optimized-method rejection; remains paused |
| 10.0.13 | default | false | Immediate optimized-method rejection; remains paused |
| 11 RC2 | disabled | true | Actual method evaluation succeeds; remains paused |
| 10.0.13 | disabled | true | Actual method evaluation succeeds; remains paused |
| 11 RC2 | disabled | false | Immediate optimized-method rejection; remains paused |
| 11 RC2 | default | true | Immediate GC-unsafe-point rejection; remains paused |
A successful Return42() result and counter increment alone were misleading in some desktop configurations: the debugger produced them without runtime func-eval dispatch/completion despite allowFastEvaluate=false. The noninterpretable GC call and engine/runtime completion events distinguish real invocation. Android passing runs require per-request EE start and actual DBI EvalComplete, not a text match on an error-stack method such as WaitForFuncEvalComplete.
The API itself is an intentionally empty managed NoInlining | NoOptimization method recognized by the debugger, not an intrinsic runtime custom-notification call. In the captured Android baseline, the underlying callback is DBI Breakpoint, not Exception2; the DAP reason exception must not be used to infer native exception-event state.
In version-matched runtime source jitinterface.cpp, method NoOptimization requests MIN_OPT/FRAMED separately from debugger-disabled module optimizations requesting DEBUG_CODE. The runtime environment switch also prevents use of existing ReadyToRun code. These distinctions explain why Optimize=false/NoOptimization/PublishReadyToRun=false are not equivalent to the successful debugger setup; they do not prove a specific native bug.
Did you find any workaround?
Yes, debug-only: package the target runtime variable:
<ItemGroup>
<RuntimeEnvironmentVariable Include="DOTNET_ReadyToRun" Value="0" />
</ItemGroup>
and add to the supported debugger launch:
"suppressJITOptimizations": true
Do not apply these unconditionally to shipping applications. PublishReadyToRun=false alone is not the runtime-environment setting. Both switches were individually isolated on desktop; on Android the tested joint combination is sufficient, but individual necessity was not isolated.
Validation after applying the combination:
- Clean-main, NUnit-free managed repro: 2 stops, 6 actual method completions (Return42, GC, Java at each), 2-second held-state checks, explicit-only Continue.
- Clean-main real JNI fixture: 4 stops, 12 actual method completions; Java evaluation succeeds at both JNI notification stops. Four scenarios pass: Java catches an unhandled managed callback; application
Handled=truesuppresses the stop/pending exception; Java rethrow preserves the original managed exception identity; normal success returns 42. Exactly-once application policy and Java catch/finally remain correct, with no JNI pending exception during policy or left afterward. - Original #13154 owning worktree, unchanged head
e0b8cddb0034fab75565683563f5635a8eaec734: generated constructor and primitive export each stop once on the original throw, evaluateJavaSystem.CurrentTimeMillis()successfully, remain paused for 2983 ms / 2028 ms, then Continue and preserve exception identity. Both handled-policy tests produce zero stops. Complete category 66/66 passed, 11 stops / 11 successful explicit Continues, zero evaluation/inspection/Continue failures. The live process returned"0"forDOTNET_ReadyToRunat both representative stops. - Desktop six-case comparison: 48 paired evaluations and 18 explicit Continues with none issued during a pending evaluation. Both successful configurations have actual engine func-eval dispatch/completion. Some separate desktop attempts crashed the host
vsdbg-ui; those were excluded, not counted as successful repeats. - Independent Android SDK:
make prepare CONFIGURATION=Releasepassed;make all CONFIGURATION=Releasereached the known missing extra-API local-workload output, resolved bymake leeroy CONFIGURATION=Release, which passed. Minimal/JNI app build/install passed. Harness/analyzer tests 9/9 passed, including detecting target progress without any DAP continued event or worker-exit proxy.
Relevant log output
Condensed Android baseline chronology, host UTC (complete DAP capture; same evaluation thread):
14:42:50 ordinary same-catch breakpoint, thread 4835
Return42() => 42, EvaluationCount => 1
GC.GetTotalMemory(false) => 9354024
JavaSystem.CurrentTimeMillis() => success
each method has DBI EvalComplete; held >=2s, explicit Continue seq18
14:42:53.192 underlying DBI Breakpoint callback for notification (not Exception2)
14:42:53.341 DAP evaluate seq23: SharedFixture.Return42(), physical caller frame1003
14:42:53.384 EE: Starting func-eval (intent; not proof native setup succeeded)
14:42:53.391-.399
target AFTER_NOTIFICATION / REPRO_COMPLETE EvaluationCount=1
14:42:53.399 same CordbThread ExitThread
14:42:53.402 DAP thread 4835 exited; Android UI process remains alive
NO intervening client Continue/step/pause/disconnect
NO DAP continued event and NO notification EvalComplete
14:42:58.453 engine abort intent
14:43:04.463 engine rude-abort intent
14:43:04.472 evaluate response fails: Unknown Error: 0x80131622
telemetry: System.ObjectDisposedException after evaluation-thread exit
Target logcat timestamps were calibrated against actual Java time evaluations; both progress markers fall inside the pending seq23 interval independently of the thread-exit evidence. Zero DAP continued events does not mean the target remained paused.
Frozen full traces, source/config/harness, default and passing APKs, and validation output are retained locally; they are not publicly attached to this issue. The self-contained reproducing source, settings, versions, and decisive chronology are included above. Raw trace identities for follow-up:
| Artifact | SHA-256 |
|---|---|
probe-default.jsonl |
129f6f74a1e01ccbbb70afd9712f56f9e1466a0c213d965216ab7bb6fb124e0f |
probe-joint.jsonl |
87bc6e3c1a380431fd58b140d9029798aa74f2ffd840424d8d7d2e07fd30a6a3 |
probe-jni.jsonl |
eac39a075c6b3570ddb41f4302fb4a8718684a16d8fc38e5303adaf74870b11a |
The remaining attribution gap is native func-eval setup HRESULT/actual exception-state flag, active stopped context/hijack, and completion/resume handling in the mobile stack. Existing EE start messages and desktop behavior are insufficient to choose between the proprietary engine, remote provider/profiler, or open runtime. The shipped remote host aliases libremotemscordbihost.dylib / libvsdbgremotecoreclrhost.dylib were identical (SHA-256 3d0f92127c7ce0a8444a52c92e4f9cc87ab32d1a7a4bfbe2e26e679bc4498d0c); supported tracing did not expose those native fields. Please keep this uncertainty explicit when routing the fix.
By opening this issue I agree to follow this project's Code of Conduct.
- 主要语言
- C#
- 星标
- 2.1k
- 派生
- 579
- 平均合并
- 1 天 16 小时
- 30 天内合并 PR
- 253
环境准备
- 没有 Dockerfile 或 Docker Compose 文件
- 有 Pull Request 模板
- 没有贡献指南
从这里开始
- 先读完整个 Issue,再读项目的贡献指南。
- 在 Issue 下留言说明你要接手 —— 这能避免两个人做同样的事。
- Fork 仓库,在一个分支上完成修改。
- 提交 Pull Request,并在描述里引用这个 Issue 编号。
dotnet/android 的其他 Issue
-
Area: App+Library Build
难度 2/5 1-3 小时 新手友好度 72/100
维护者通常 1 天内回复
-
Area: Mono.Android
难度 2/5 1-3 小时 新手友好度 65/100
dotnet/android#9192 · 4 条评论 · 1 个 reaction ·
维护者通常 1 天内回复
-
agentic-workflows needs-triage
难度 3/5 1-2 天 新手友好度 45/100
维护者通常 1 天内回复
-
[net11.0-android] Handler.RemoveCallbacks(Action) leaves earlier repeated posts queued, retaining managed objects and JNI references可能已有人在做 @simonrozsival 于 1 天前认领。 未关闭Area: Mono.Android needs-triage
难度 4/5 3-5 天 新手友好度 25/100
维护者通常 1 天内回复
-
agentic-workflows needs-triage
难度 4/5 1-2 天 新手友好度 25/100
维护者通常 1 天内回复
相似的 Issue
-
bug effort:S P3
难度 2/5 1-3 小时 新手友好度 85/100
nightscout/nocturne#3093 ·
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 67/100
维护者通常 3 天内回复
-
难度 1/5 1 小时以内 新手友好度 85/100
-
难度 2/5 1-3 小时 新手友好度 76/100
维护者通常 1 天内回复
-
难度 2/5 1-3 小时 新手友好度 76/100