Skip to content

Size the RESOLVE_UPDATE_METADATA events in DbgTransportSession::GetEventSize - #134717

Merged
max-charlamb merged 1 commit into
dotnet:mainfrom
pcshrosbree:fix-getsize-resolve-update-metadata
Oct 8, 2026
Merged

max-charlamb merged 1 commit into
dotnet:mainfrom
pcshrosbree:fix-getsize-resolve-update-metadata

Conversation

@pcshrosbree

@pcshrosbree pcshrosbree commented Sep 26, 2026 •

Copy link
Copy Markdown
Contributor

Problem: on Linux, a debugger can crash the process it is debugging when it requests the locals of a method that a profiler has rewritten. If a profiler has given a method new locals with SetILFunctionBody, a debugger that requests that method's locals can make the debuggee die with SIGSEGV in Debugger::SerializeModuleMetaData. An IDE's Variables pane can make that request automatically, for example at an exception stop. In my VS Code sessions, the Debug Console reported the crashed debuggee as exited with code 0.

With checked builds, an unpatched process that sends one of the affected events asserts and aborts instead; with an unpatched checked mscordbi, that is the debugger itself. I reproduced the crash on 8.0, 9.0, 10.0, 11.0 RC1 and main, and the omission is present in the source back to at least 6.0. Standard Windows builds do not use this transport. macOS does, and the minimal repro crashes there too (official 10.0.12 and 8.0.31 osx-arm64, netcoredbg, Apple silicon; added 2026-10-11).

Cause: the Unix debugger transport sizes each event it sends with DbgTransportSession::GetEventSize, which has no case for the four DB_IPCE_RESOLVE_UPDATE_METADATA_* events. They fall to default:. Release builds therefore send all four with only the event header; for the three that carry a payload, the receiver reads stale bytes where the MetadataUpdateRequest payload should be. Checked builds assert "Unknown debugger event type" in the sending process. Under the default LegacyCompatPolicy, the debugger starts this exchange when it meets a local-signature token that is not in its cached metadata, such as a new signature that a profiler has added.

Fix: size the three events that carry MetadataUpdateRequest, and add _2_RESULT, which carries only the header hr, to the existing zero-payload group. The linked issue has the details, a minimal repro, and results on 8.0 to 11.0.

event direction fields used
_1 debugger → runtime vmModule
_1_RESULT runtime → debugger pMetadataStart, nMetadataSize
_2 debugger → runtime pMetadataStart (the buffer to free)
_2_RESULT runtime → debugger none

GetEventSize is compiled into both mscordbi and coreclr (debug/di/shared.cpp, debug/ee/shared.cpp), and each side sizes the events it sends, so both binaries need the change.

Validation

With the minimal repro from the issue (a small C profiler that adds one unused local, and a debugger requesting locals at an exception stop: netcoredbg, or VS Code where marked), 3 runs per cell unless noted:

build before after
10.0.12 release, linux-x64 and linux-arm64 debuggee SIGSEGV, 3/3 correct locals, 3/3
10.0.12 checked, linux-x64 debugger asserts in GetEventSize, 3/3 correct locals, 0 asserts, 3/3
11.0 RC1 tag, release, linux-x64 (VS Code, 1 run) debuggee SIGSEGV correct locals
main at c29faec90e46, linux-x64 (VS Code; release, and checked for "after"¹) debuggee SIGSEGV, 2/2 correct locals: release 3/3; checked 1/1, no assert

¹ The checked main build also swaps the DAC, JIT and CoreLib.

Within each release configuration, only libcoreclr.so and libmscordbi.so differ between the before and after builds. macOS shares the transport: in my local runs on 2026-10-11, the minimal repro on official 10.0.12 and 8.0.31 osx-arm64 under netcoredbg 3.2.0 (Apple silicon) crashed the debuggee in 9 of 9 local runs with clean controls; the three crash reports macOS wrote show the same SerializeModuleMetaData crash site as Linux. The patched build was not run on macOS. The controls (no profiler; an unchanged body; a new MemberRef without a new local) read the expected locals under netcoredbg on 8.0, 9.0 and 10.0. On 11.0 RC1, netcoredbg returns only $exception at the stop even with no profiler, so the 11.0 rows use VS Code.

Tests

I did not find a test in this repo that exercises the debugger transport, so this PR adds no test, as with #97476 and #110122. The repro is attached to the issue. #134715 proposes a checked-build check that every declared event type is either sized or explicitly listed as never sent. I am happy to add a test if you can point me at the right place.

Servicing

The omission is present on every supported branch. I'd suggest this for release/11.0, ideally before GA; per-branch patches for 10.0, 9.0 and 8.0 are attached to the issue in case you judge those worth servicing. If you do service it, the text below may save time:

Servicing template

Customer Impact

On Linux, when a profiler has given a method new locals with SetILFunctionBody, a debugger that requests that method's locals (an IDE's Variables pane can do so automatically, e.g. at an exception stop) can crash the debuggee. macOS uses the same transport and reproduces the crash (official 10.0.12 and 8.0.31 osx-arm64, netcoredbg, 2026-10-11); standard Windows builds do not use it.

Regression

  • Yes
  • No

Present since at least 6.0 (checked from v6.0.36).

Testing

The minimal repro in #134714: release and checked 10.0.12 builds (linux-x64), release (linux-arm64), the 11.0 RC1 tag and main (linux-x64, VS Code).

Risk

Low. Cases are added to a size switch for events that were sent header-only. Only the sent length of these three events grows, to include their payload; no other event, struct or framing changes. The debugger (libmscordbi.so) and the runtime (libcoreclr.so) should ship together: a fixed mscordbi with an unpatched coreclr can send a stale pointer back in _2 for the runtime to free (see the issue). Both are in the runtime package.

Resolves #134714

Note

This change and description were prepared with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed them before posting.

On Unix, DbgTransportSession::GetEventSize had no case for the four
DB_IPCE_RESOLVE_UPDATE_METADATA_* events, so release builds sent them
header-only and the receiver read stale bytes in place of the
MetadataUpdateRequest payload. Requesting the locals of a method that
a profiler had given new locals could then crash the debuggee.

Size _1, _1_RESULT and _2 as MetadataUpdateRequest, and add
_2_RESULT, which carries only the header hr, to the zero-payload
group.

Fix dotnet#134714
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
Successfully started running 3 pipeline(s).
13 pipeline(s) were filtered out due to trigger conditions.
There may be pipelines that require an authorized user to comment /azp run to run.

@dotnet-policy-service

Copy link
Copy Markdown
Contributor

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

@pcshrosbree

Copy link
Copy Markdown
Contributor Author

A follow-up on the "should ship together" note in the servicing template, now measured. This is local 10.0.12 linux-x64 release, the #134714 repro, netcoredbg, 3 runs.

With this PR and #134987 in libmscordbi.so and an unpatched libcoreclr.so, the session ends cleanly with no locals, but the _2 that the debugger sends carries a pointer the runtime had already freed:

  1. The unpatched runtime replies _1_RESULT header-only (96 bytes). Its hr is S_OK, so Check the RESOLVE_UPDATE_METADATA_1 result hr in CordbModule::RefreshMetaData #134987's check passes.
  2. The debugger reads pMetadataStart from its receive slot. That slot last held a DB_IPCE_FUNC_EVAL_SETUP_RESULT from the func-eval netcoredbg ran at the same stop, and that event's debuggerEvalKey is at the same union offset.
  3. The runtime had freed that DebuggerEval when it handled DB_IPCE_FUNC_EVAL_CLEANUP (Debugger::FuncEvalCleanup → DeleteInteropSafe(pDE)), 23–28 ms before _2.
  4. The _2 handler calls DeleteInteropSafe on it again.

In all three runs the value in _2 matched the debuggerEvalKey in both earlier events. I saw no crash and did not check the heap.

The reverse pairing (an unpatched mscordbi with a runtime carrying this PR) still crashes in SerializeModuleMetaData, 6/6 across a local unpatched mscordbi and Microsoft's shipped 10.0.12 one.

Zeroing the unsent tail of each received event closes both: _2 then carries NULL, and the runtime refuses the empty _1 with E_INVALIDARG. I've written that up separately as #135109, so as not to widen this PR.

Note

This comment and the measurements in it were produced with AI assistance (Anthropic Claude and OpenAI Codex), under my direction. I reviewed it before posting.

@pcshrosbree

Copy link
Copy Markdown
Contributor Author

@steveisok @tommcdon — gentle ping on this one. It's a 7-line fix for a debuggee crash that any profiler adding locals can trigger on Linux (present since 6.0, repro and per-branch patches on #134714). Build Analysis is green; the two reds are known issues #133645 and #133953. If release/11.0 is in scope the servicing template is in the description. #134987 and #135114 are independent siblings in the same area if it helps to look at them together. Happy to rebase or add a test wherever you'd like it.

@max-charlamb
max-charlamb merged commit 7c0ecc7 into dotnet:main Oct 8, 2026
121 of 123 checks passed
@pcshrosbree

Copy link
Copy Markdown
Contributor Author

Thanks for merging this. Would you consider it for release/11.0 and the servicing branches? The servicing template is in the description, and #134714 carries the per-branch patches for 10.0, 9.0 and 8.0 (patches/v2-exact/, each checked with git apply --check at its branch tip). I'll defer to the area owners on which branches, if any, are worth it; knowing either way helps us plan the profiler-side mitigation for unpatched runtimes.

Note

This comment was prepared with AI assistance (Anthropic Claude, checked by OpenAI Codex) at my request. I reviewed it before posting.

@pcshrosbree

Copy link
Copy Markdown
Contributor Author

@steveisok @tommcdon thanks for merging this. Could one of you run /backport to release/11.0 on it? The bot needs write access, so I can't start it myself. The cherry-pick applies cleanly to release/11.0 (checked with merge-tree against 11f94c29c02), and the servicing template is filled in under "Servicing" in the description, ready to paste into the backport PR. For 10.0, 9.0 and 8.0 the per-branch patches are on #134714; the 10.0 one does not cherry-pick cleanly, so if you want those serviced I'd rather open the PRs from my fork than have the bot produce conflicts.

The two small follow-ups in the same code, #134987 (check the _1_RESULT hr in CordbModule::RefreshMetaData) and #135114 (CORDBG_E_DEBUGGER_TRANSPORT_NOT_FOUND on attach), are also waiting for a first look whenever you have a moment.

@steveisok

Copy link
Copy Markdown
Member

/backport to release/11.0

@github-actions

Copy link
Copy Markdown
Contributor

Started backporting to release/11.0 (link to workflow run)

@steveisok

Copy link
Copy Markdown
Member

@pcshrosbree thanks for the contribution! I kicked off a backport to 11. I'm not opposed to trying to work this into 10, but will need to hone a good justification for it to meet the bar. It would be helpful if you could share from your perspective how this would be blocking you if we didn't take it into release/10.0.

@pcshrosbree

pcshrosbree commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor Author

@steveisok thanks, and for the 11 backport (#135597). I'll narrow the ask to release/10.0. 8.0 and 9.0 leave support on 2026-11-10, so I am not asking for those; the per-branch patches stay on #134714 if you judge a final 8/9 update worth it. 10 is supported until November 2028, so a fix on 11 alone would leave users who stay on unpatched 10 exposed to this crash until then; the first runtime to carry the fix would be 11, if #135597 lands before GA.

How it blocks us. We are building a profiler whose instrumentation rewrites methods at JIT time with SetILFunctionBody, and the rewrite adds locals. It is used during development, under a debugger. On an unpatched Linux or macOS runtime the ordinary workflow (stop at an exception in an instrumented method, the IDE fills its Variables pane) can kill the debuggee, and VS Code reports it as exited with code 0. In our attach test cells with the full profiler on official 8.0.31 and 10.0.12 it crashed at the first exception stop in every netcoredbg run (3/3 on each) and VS Code run (1/1 on each); in earlier VS Code runs with the full profiler it was about 1 run in 10. The minimal repro also crashes under Rider 2026.1, in the mcr.microsoft.com/dotnet/sdk:10.0.301 container, and on official 10.0.12 linux-musl-x64. macOS is the same crash: today, on Apple silicon (macOS 27.0.1), the minimal repro on official 10.0.12 and 8.0.31 osx-arm64 under netcoredbg 3.2.0 lost the debuggee in 9 of 9 local runs while every control read the locals, and the crash reports macOS wrote show SIGSEGV in Debugger::SerializeModuleMetaData on the .NET Debugger thread, the Linux stack. No released runtime contains the fix, so a user who hits it has nothing to update.

Without a 10.0 fix we cannot reliably combine our JIT-time instrumentation with debugging on Linux or macOS. The choices are to decline instrumenting any method that needs a new local-signature row while a managed debugger might attach, which for a development-time tool is the main scenario, or to ship the workaround we have been building: a build step that writes the expected local signatures into the assembly on disk, so the debugger's metadata copy already holds the rows and does not request a refresh for them. Coverage depends on the exact required signature already being present in each loaded assembly. If it is absent, for example in an assembly the build step did not prepare, the guard declines the method or uses a hook that adds no local. The same exposure applies to any profiler whose rewrite adds a local.

Why it services well. Both halves ship in the runtime package. On every launch kind I measured (local launch, local attach, self-contained, container, musl), vsdbg from the C# extension 2.160.4 and Rider 2026.1 loaded the target runtime's own libmscordbi.so, never a bundled copy, so running the fixed runtime updates both sides for those configurations without a debugger update; self-contained apps need republishing and container images rebuilding. Risk is as in the template: three events grow to their declared payload size, _2_RESULT is classified as zero-payload, and no struct or framing changes.

Logistics. The main commit does not cherry-pick cleanly onto release/10.0 (merge-tree conflicts at today's tip 73ae2764c38), but the 10.0 patch on #134714 applies cleanly there. If you are willing to take it to Tactics, I'll open the 10.0 PR from my fork with the template filled in, or add anything above to the justification.

Added 2026-10-11, after posting: one point I should have made explicit. This is not only a development-time problem. We also reproduced this crash after local debugger attach on Linux: the attach cells above were exactly that, a late attach to a running process with the full profiler, and the process died at the first exception stop under both netcoredbg and VS Code on each of 8.0.31 and 10.0.12. In production, the same exception-stop locals request could kill the attached process. In the observed crashes, application output stopped mid-run; SIGSEGV appeared only in the system's core-dump log. Our production configuration therefore switches the managed debugger off (DOTNET_EnableDiagnostics_Debugger=0), so our default configuration prevents live managed-debugger attach. Supporting that workflow on unpatched 10 requires the instrumentation restrictions or workaround described above.

Note

This comment was prepared with AI assistance (Anthropic Claude, checked by OpenAI Codex) at my request. I reviewed it before posting.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area-Diagnostics-coreclr community-contribution Indicates that the PR has been added by a community member

Projects

None yet

Development

Successfully merging this pull request may close these issues.

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

4 participants