Skip to content

Linux: debugger transport sends DB_IPCE_RESOLVE_UPDATE_METADATA_* events without their payload, crashing the debuggee #134714

Closed

Description

@pcshrosbree

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.12

Or 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 3

In 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-arm64 on Apple silicon (macOS 27.0.1) under netcoredbg 3.2.0, local 0/9 clean (3 runs each, plus 3 spaced runs on 10.0.12), while the noprof, none and memberref controls read score = 42 3/3 each per runtime. The crash reports macOS wrote show EXC_BAD_ACCESS (SIGSEGV) in Debugger::SerializeModuleMetaData on the .NET Debugger thread, 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-35 at v10.0.12). On official 10.0.12 win-arm64 under 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's SET_ENABLE_CUSTOM_NOTIFICATION_RESULT case is also missing. Its sender fills only the header, so release sends the right size by accident; a checked build asserts.
  • DB_IPCE_TEST_CRST and DB_IPCE_TEST_RWLOCK (8.0, 9.0, 10.0; sent only under UNSUPPORTED_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 at v10.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

  1. dotnet-policy-service commented on Sep 26, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @steveisok, @tommcdon, @dotnet/dotnet-diag
    See info in area-owners.md if you want to be subscribed.

  2. added
    in-prThere is an active PR which will close this issue when it is merged
    and removed
    untriagedNew issue has not been triaged by the area owner
    on Oct 1, 2026
  3. added this to the 12.0.0 milestone on Oct 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    area-Diagnostics-coreclrin-prThere is an active PR which will close this issue when it is merged

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions