Repository navigation
Linux: debugger transport sends DB_IPCE_RESOLVE_UPDATE_METADATA_* events without their payload, crashing the debuggee #134714
Description
Description
On Linux, if a profiler has given a method a new local signature with ICorProfilerInfo::SetILFunctionBody, a debugger that stops in that method and requests its locals can crash the debuggee with SIGSEGV. An IDE's Variables pane can make that request automatically when execution stops, for example on an exception.
The cause: DbgTransportSession::GetEventSize (src/coreclr/debug/shared/dbgtransportsession.cpp) has no case for the four DB_IPCE_RESOLVE_UPDATE_METADATA_* events (0x256, 0x15E, 0x257, 0x15F). They fall to default:, so release builds send only the event header, and checked builds assert "Unknown debugger event type". The receiver reuses its receive slots without clearing them (dbgtransportsession.cpp:1838 at v10.0.12), so the handler reads stale bytes where the MetadataUpdateRequest payload should be.
I reproduced the crash on official 8.0.31, 9.0.20 and 10.0.12 (linux-x64 and linux-arm64) and on 11.0 RC1 (linux-x64), with a minimal profiler: under netcoredbg on 8.0 to 10.0 (x64 and arm64, 3 runs each), and under VS Code on 10.0.12, 11.0 RC1 and main (x64, manual runs). A small fix, validated on 10.0.12, the 11.0 RC1 tag and main, is below; a PR follows. The same kind of omission was fixed twice before, in #97476 (DISABLE_OPTS, the Linux crash in #97461) and #110122.
Reproduction Steps
The attached repro (see its README.md) has a ~270-line C profiler (minprof.c), a net8.0 console app and a Python DAP driver for netcoredbg. The quickest route, which needs only Docker on an x64 or arm64 Linux host:
bash repro-in-docker.sh 10.0.12Or by hand: run bash build.sh, then
python3 dap_locals.py --dotnet-root <dir with one runtime> --netcoredbg <netcoredbg> \
--env DOTNET_ROLL_FORWARD=Major --modes noprof,local --runs 3In local mode the profiler reinstalls Throwing's body with one extra, unused int32 local, which adds a new StandAloneSig row. The driver stops at the first-chance exception in Throwing and requests its locals, as an IDE does. noprof is the control.
Expected behavior
The debugger shows n = 7 and score = 42, and the app runs to completion ([probe] done).
Actual behavior
The debuggee dies with SIGSEGV on the debugger RC thread (3/3 per affected runtime under netcoredbg; every manual VS Code run of an unpatched build):
Debugger::SerializeModuleMetaData(Module*, unsigned int*) debug/ee/debugger.cpp:9917 (Module::GetEmitter inlined)
Debugger::HandleIPCEvent(DebuggerIPCEvent*)
DebuggerRCThread::HandleRSEA() debug/ee/rcthread.cpp:796
DebuggerRCThread::MainLoop() debug/ee/rcthread.cpp:917
(Symbolised from a local unpatched build of the 11.0 RC1 tag, with build-id-matched symbols.) VS Code's Debug Console reports exited with code 0 for these crashes; the SIGSEGV shows only in the core dump.
With a checked runtime and debugger, the debugger process asserts instead:
ASSERT FAILED
Expression: !"Unknown debugger event type"
Location: line 2445 in src/coreclr/debug/di/../shared/dbgtransportsession.cpp
Function: GetEventSize
Regression?
No. The omission is present in every tag I checked (v6.0.36, v7.0.20, v8.0.31, v9.0.20, v10.0.12) and on main (c29faec90e46).
Known Workarounds
- A line breakpoint bound in the method before it first runs avoided the crash with netcoredbg (3/3 on 10.0.12 and 11.0 RC1). I have not traced why, and did not test this under VS Code.
- Not requesting locals in a rewritten method avoids it (the crash needs the locals request).
- A profiler can avoid it by not adding local-signature rows at JIT time.
Configuration
- Official runtimes 8.0.31, 9.0.20, 10.0.12 and 11.0.0-rc.1.26425.128; linux-x64 and linux-arm64 (Snapdragon X, WSL2).
- netcoredbg 3.2.0-1092; VS Code with C# extension 2.160.4.
- macOS uses the same transport code and reproduces the crash (my local runs, 2026-10-11): official 10.0.12 and 8.0.31
osx-arm64on Apple silicon (macOS 27.0.1) under netcoredbg 3.2.0,local0/9 clean (3 runs each, plus 3 spaced runs on 10.0.12), while thenoprof,noneandmemberrefcontrols readscore = 423/3 each per runtime. The crash reports macOS wrote showEXC_BAD_ACCESS (SIGSEGV)inDebugger::SerializeModuleMetaDataon the.NET Debuggerthread, the same crash site as on Linux. The patched build was not run on macOS. - Windows is not affected in standard builds: this transport is enabled by default only for Unix targets (
clrfeatures.cmake:31-35atv10.0.12). On official 10.0.12win-arm64under VS Code, three manual runs of the same repro showed correct locals and no crash.
Other information
Where it is reached. CordbILCode::GetLocalVarSig → CordbModule::UpdateMetaDataCacheIfNeeded → RefreshMetaData (debug/di/module.cpp). Under the default LegacyCompatPolicy, when the debugger meets a local-signature token that is not in its cached metadata, it sets m_fForceMetaDataSerialize and asks the runtime to serialise the module's metadata in-process (module.cpp:339-360, :527-551). That exchange is _1 / _1_RESULT, followed by _2 / _2_RESULT to free the buffer. The flag stays set, so later refreshes of that module take the same path. Calling ICorProfilerInfo7::ApplyMetaData after emitting, as corprof.idl requires, made no difference. (Line numbers are from v10.0.12.)
Both sides need the fix. dbgtransportsession.cpp is compiled into libmscordbi.so (debug/di/shared.cpp) and libcoreclr.so (debug/ee/shared.cpp), and GetEventSize runs in whichever process sends:
| event | sent by | sized by | payload needed |
|---|---|---|---|
_1 |
debugger (module.cpp:543-551) |
libmscordbi.so |
vmModule |
_1_RESULT |
runtime (debugger.cpp:11209-11217) |
libcoreclr.so |
pMetadataStart, nMetadataSize |
_2 |
debugger (module.cpp:429-437) |
libmscordbi.so |
pMetadataStart |
_2_RESULT |
runtime (debugger.cpp:11228-11231) |
libcoreclr.so |
none (only the header hr) |
I swapped only libmscordbi.so between unpatched and fixed local builds (x64, 10.0.12, an earlier variant of the fix that gave _2_RESULT a payload size too). An unpatched debugger with a fixed runtime still crashed. A fixed debugger with an unpatched runtime returned no locals and did not crash in 3 runs, but that pairing is not safe: the unpatched runtime's header-only _1_RESULT can leave stale bytes in the pMetadataStart position (an earlier exception event puts non-zero fields there), and the fixed debugger then sends them back in _2 for the runtime to free. So the two binaries should be updated together. Where a debugger loads the target runtime's own libmscordbi.so, updating the runtime does that; I have not confirmed which copy each debugger loads. Debuggers that ship their own DBI need their own update.
Security. I don't believe this has a security implication. The path needs a connected debugger-transport peer: _1 and _2 are sent only by the debugger side (debug/di/module.cpp:429-437, :543-551), and a profiler alone, including ApplyMetaData, cannot reach it. The transport's FIFOs are created with mode 0700 (debug/debug-pal/unix/twowaypipe.cpp:27,35) in $TMPDIR or /tmp, so only the debuggee's own user or root can open them, and a peer that has opened a session can already read and write the debuggee's memory through the transport's own MT_ReadMemory/MT_WriteMemory (dbgtransportsession.cpp:1863-1929). The receiver does not use GetEventSize either: it trusts the sender's length, checked only against an upper bound (:1773-1777), so such a peer could already send any event short. The fix changes only what the legitimate sender writes. With DOTNET_EnableDiagnostics=0 the transport is not created at all. (Line numbers at v10.0.12.) If you see it differently, I'll follow whatever process you prefer.
Results in detail (3 runs per cell unless noted; a pass means completion and n = 7, score = 42)
Linux x64, netcoredbg requesting locals at the first-chance stop in Throwing:
| runtime | no profiler | identical body reinstalled | +1 unused local | new MemberRef + code, no local |
|---|---|---|---|---|
| official 8.0.31 | pass | pass | debuggee SIGSEGV 3/3 | pass |
| official 9.0.20 | pass | pass | debuggee SIGSEGV 3/3 | pass |
| official 10.0.12 | pass | pass | debuggee SIGSEGV 3/3 | pass |
| local release 10.0.12, unpatched | pass | not run | debuggee SIGSEGV 3/3 | not run |
| local release 10.0.12 + fix | pass | pass | pass | pass |
| local checked 10.0.12, unpatched | pass, 0 asserts | pass, 0 asserts | the debugger asserts, 3/3 | pass, 0 asserts |
| local checked 10.0.12 + fix | pass, 0 asserts | pass, 0 asserts | pass, 0 asserts | pass, 0 asserts |
The unpatched and fixed local builds differ only in libcoreclr.so and libmscordbi.so. On the official runtimes, these did not crash: the new-local rewrite with the debugger never requesting locals; and a new local on a different method while locals are requested in Throwing.
linux-arm64 (release builds only): every cell with a linux-x64 counterpart had the same outcome.
.NET 11 RC1: at an exception stop on 11 RC1, netcoredbg 3.2.0 returns only $exception, even with no profiler, so I ran 11 under VS Code (manual, one run per cell). The local builds are of tag v11.0.0-rc.1.26425.128:
| runtime | no profiler | +1 unused local |
|---|---|---|
| official 10.0.12 (vsdbg control) | not run | debuggee SIGSEGV |
| official 11.0 RC1 | score = 42 |
debuggee SIGSEGV |
| local 11.0 RC1, unpatched | not run | debuggee SIGSEGV |
| local 11.0 RC1 + fix | not run | score = 42, no crash |
main (c29faec90e46), linux-x64, VS Code: a local test host with only the CoreCLR binaries swapped. Unpatched: debuggee SIGSEGV 2/2, same stack as above. With the fix: score = 42 in 3/3 release runs and 1/1 checked run (no assert); the attached console excerpts show completion.
A build with the fix minus only its _2 case aborted the debuggee with free(): invalid pointer, 6/6: the runtime freed a stale pMetadataStart.
Suggested fix
Size the three events that carry MetadataUpdateRequest, and put _2_RESULT in the existing zero-payload group:
case DB_IPCE_CATCH_HANDLER_FOUND_RESULT:
case DB_IPCE_SET_ENABLE_CUSTOM_NOTIFICATION_RESULT:
+ case DB_IPCE_RESOLVE_UPDATE_METADATA_2_RESULT:
cbAdditionalSize = 0;
break;
...
+ case DB_IPCE_RESOLVE_UPDATE_METADATA_1:
+ case DB_IPCE_RESOLVE_UPDATE_METADATA_1_RESULT:
+ case DB_IPCE_RESOLVE_UPDATE_METADATA_2:
+ cbAdditionalSize = sizeof(pEvent->MetadataUpdateRequest);
+ break;It builds cleanly and fixes the repro on main (see the details above). The attachment's patches/v2-exact/ holds the same change for each branch below; each names its branch and SHA in its first line and applies there with git apply --check. MetadataUpdateRequest has raw fields on 8, 9 and 10 and Portable<> fields on 11 and main; sizeof covers both.
| branch | tip checked |
|---|---|
main |
c29faec90e46 |
release/11.0 |
4c29572617f9 |
release/10.0 |
9596acb4a4a7 |
release/9.0-staging |
b9655fb282d2 |
release/8.0-staging |
b114ac3b2f59 |
I'd suggest it for release/11.0, ideally before GA. Patches for 10.0, 9.0 and 8.0 are attached in case you judge those worth servicing; I'll defer to you on that.
Related, for information:
- Check that every debugger IPC event type is sized by
DbgTransportSession::GetEventSize#134715 proposes a checked-build check that every declared event type is sized. - On
release/9.0-staging, Fixed missed case #110122'sSET_ENABLE_CUSTOM_NOTIFICATION_RESULTcase is also missing. Its sender fills only the header, so release sends the right size by accident; a checked build asserts. DB_IPCE_TEST_CRSTandDB_IPCE_TEST_RWLOCK(8.0, 9.0, 10.0; sent only underUNSUPPORTED_TestDataConsistency) have the same omission.- The receiver checks only an upper bound (
dbgtransportsession.cpp:1773-1777) and never clears the unwritten part of a slot (:1838; both atv10.0.12). Zeroing it on receive would leave an omitted pointer field NULL rather than stale.
repro-GetEventSize-RESOLVE_UPDATE_METADATA.zip
Note
This issue, its analysis and the attached repro were produced with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed the text before posting, and ran the VS Code sessions reported above myself.
Activity
dotnet-policy-service commentedon Sep 26, 2026
Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
See info in area-owners.md if you want to be subscribed.
Size the RESOLVE_UPDATE_METADATA events in DbgTransportSession::GetEv…
[release/11.0] Size the RESOLVE_UPDATE_METADATA events in DbgTranspor…
Check the RESOLVE_UPDATE_METADATA_1 result hr in CordbModule::Refresh…