Skip to content

Remove ExInfo::m_hThrowable - use direct pointer for exception objects#127300

Merged
max-charlamb merged 15 commits intomainfrom
dev/max-charlamb/exception-direct-pointer
May 1, 2026
Merged

Remove ExInfo::m_hThrowable - use direct pointer for exception objects#127300
max-charlamb merged 15 commits intomainfrom
dev/max-charlamb/exception-direct-pointer

Conversation

@max-charlamb
Copy link
Copy Markdown
Member

@max-charlamb max-charlamb commented Apr 22, 2026

Note

This PR was authored with the assistance of GitHub Copilot.

Summary

Replace the GCHandle-based m_hThrowable field in ExInfo with direct use of the existing m_exception OBJECTREF field, matching NativeAOT's approach.

Motivation

CoreCLR's ExInfo stored exception objects via two redundant fields: OBJECTHANDLE m_hThrowable (GC handle table indirection) and OBJECTREF m_exception (direct pointer used by the new EH path shared with NativeAOT). NativeAOT only has m_exception. The handle added allocation/deallocation overhead (~5 interlocked ops per throw) and an extra pointer indirection on every read, but none of the 15 consumers actually required OBJECTHANDLE guarantees - they all ran in cooperative GC mode and immediately dereferenced the handle.

Key Changes

  • Remove OBJECTHANDLE m_hThrowable from ExInfo, saving 8 bytes (x64) / 4 bytes (x86)
  • Add GC root scanning of ExInfo chain in ScanStackRoots (gcenv.ee.cpp), mirroring NativeAOT's GcScanRootsWorker - this keeps superseded exception objects alive without handles
  • Remove GCPROTECT_BEGIN(exInfo.m_exception) from all 3 dispatch entry points - the chain scanning already reports &m_exception to the GC, and reporting the same location twice corrupts the GC's relocation logic (clr-code-guide.md section 2.1.5). Debug OBJECTREF tracking is satisfied via Thread::ObjectRefProtected in the ExInfo constructor.
  • Add ExInfo::GetThrowableAsPseudoHandle() - returns the target address of m_exception as a pseudo-handle (not a real GC handle table entry). Uses PTR_HOST_MEMBER_TADDR for correct DAC target address computation. The slot is updated during GC by the ExInfo chain scanner.
  • Remove GetThrowableAsHandle() - replaced by GetThrowableAsPseudoHandle() on ExInfo, ThreadExceptionState, and Thread
  • Remove SetThrowable() entirely - managed EH code writes m_exception directly; the SetThrowableErrorChecking enum and STEC_* constants are also removed.
  • Remove GetMyThread() - dead function with zero callers
  • Merge AppendElementImpl into AppendElement - only one caller remained after removing the OBJECTHANDLE overload. The merged function handles foreign exception semantics and preallocated exception checks.
  • Remove OBJECTHANDLE overload of AppendElement - all callers now pass OBJECTREF directly (including prestub.cpp and threads.cpp via ObjectFromHandle)
  • Fix StackTraceInfo::AppendElement preallocated-exception check: changed from IsPreallocatedExceptionHandle to IsPreallocatedExceptionObject(ObjectFromHandle(...))
  • Update AsmOffsets constants for the new field layout (validated by static_asserts)
  • Update Interop propagation callback to take OBJECTREF instead of OBJECTHANDLE
  • Update DAC code: GetCurrentException reads from ExInfo first via GetThrowableAsPseudoHandle(), falls back to m_LastThrownObjectHandle. GetThreadException uses same pattern.
  • Update cDAC: ThrownObjectHandle -> ThrownObject (direct pointer, no handle dereference); GetCurrentExceptionHandle returns field address as pseudo-handle for backward compatibility

What stays unchanged

Thread::m_LastThrownObjectHandle remains as an OBJECTHANDLE - it is required by the ICorDebug managed debugging protocol (SendExceptionHelperAndBlock is MODE_ANY, right-side debugger reads through handle cross-process via BuildFromGCHandle).

Efficiency

Per exception throw, this eliminates:

  • ~5 interlocked operations (handle alloc/destroy)
  • 1 handle table slot allocation + bookkeeping
  • 1 pointer indirection per throwable read (2-hop -> 1-hop)
  • 8 bytes from ExInfo struct (x64)
  • 3 GCFrame constructions per dispatch entry point

The only added cost is ~2 pointer reads per thread per GC for ExInfo chain walking - negligible since exception chains are almost always 1-2 nodes deep.

Testing

  • Checked + Release CLR builds: 0 errors, 0 warnings
  • GCStress=0xC + HeapVerify=1: 100/100 nested exception iterations pass (20 levels, 100 iterations)
  • All 1731 cDAC tests pass
  • AsmOffset static_asserts validate all field offsets

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR removes ExInfo::m_hThrowable (GC-handle indirection) and standardizes on the existing direct OBJECTREF m_exception path, aligning CoreCLR with NativeAOT and updating DAC/cDAC consumers accordingly.

Changes:

  • Remove m_hThrowable usage and migrate exception-object reads to m_exception across EH, interop propagation, debugger/DAC paths.
  • Add explicit GC root scanning for the ExInfo chain to keep superseded exception objects alive without handles.
  • Update cDAC contracts/tests and DAC exception state plumbing to reflect the new representation.

Reviewed changes

Copilot reviewed 25 out of 25 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
src/native/managed/cdac/tests/ThreadTests.cs Updates tests to use the new thrown-object representation.
src/native/managed/cdac/tests/MockDescriptors/MockDescriptors.Thread.cs Updates mock ExceptionInfo layout to expose ThrownObject instead of ThrownObjectHandle.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Data/ExceptionInfo.cs Reads ThrownObject as a pointer field instead of a handle wrapper.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Thread_1.cs Returns current exception “handle” using ThrownObject and updates Watson bucket lookup accordingly.
src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/Exception_1.cs Updates nested exception info to return direct thrown object pointer.
src/coreclr/vm/threads.h Removes handle-based throwable accessors and adjusts HasException/IsThrowableNull logic.
src/coreclr/vm/threads.cpp Updates last-thrown synchronization to use OBJECTREF throwable.
src/coreclr/vm/interoplibinterface_shared.cpp Changes propagating-exception callback signature/GC mode to take OBJECTREF.
src/coreclr/vm/interoplibinterface_objc.cpp Switches ObjC propagation callback to accept OBJECTREF and removes handle dereference.
src/coreclr/vm/interoplibinterface.h Updates declarations to match OBJECTREF callback signatures.
src/coreclr/vm/gcenv.ee.cpp Adds GC root scanning of ExInfo chain for direct exception object references.
src/coreclr/vm/exstate.h Removes GetThrowableAsHandle from ThreadExceptionState.
src/coreclr/vm/exstate.cpp Removes handle-based throwable retrieval; GetThrowable returns m_exception.
src/coreclr/vm/exinfo.h Removes m_hThrowable and returns m_exception directly from GetThrowable().
src/coreclr/vm/exinfo.cpp Drops handle lifecycle management; clears m_exception during resource release.
src/coreclr/vm/exceptionhandling.cpp Updates DAC memory enumeration and stacktrace append paths to use m_exception.
src/coreclr/vm/excep.cpp Updates stacktrace appending to preserve foreign/preallocated semantics with OBJECTREF.
src/coreclr/vm/eedbginterfaceimpl.cpp Switches debugger exception retrieval logic to rely on m_LastThrownObjectHandle.
src/coreclr/vm/datadescriptor/datadescriptor.inc Updates cDAC descriptor field to ThrownObject at offsetof(ExInfo, m_exception).
src/coreclr/debug/ee/debugger.cpp Uses m_LastThrownObjectHandle for force-catch-handler lookup.
src/coreclr/debug/daccess/task.cpp Changes ClrDataExceptionState::m_throwable type to TADDR and passes &m_exception.
src/coreclr/debug/daccess/request.cpp Reads exception object directly from m_exception and updates Watson bucket retrieval.
src/coreclr/debug/daccess/dacimpl.h Updates ClrDataExceptionState signature/storage for TADDR throwable.
src/coreclr/debug/daccess/dacdbiimpl.cpp Switches “current exception” debugger handle to m_LastThrownObjectHandle.
src/coreclr/System.Private.CoreLib/src/System/Runtime/ExceptionServices/AsmOffsets.cs Updates managed EH asm offsets to reflect the new ExInfo layout.

Comment thread src/coreclr/vm/exceptionhandling.cpp Outdated
Comment thread src/coreclr/vm/threads.h Outdated
@max-charlamb max-charlamb force-pushed the dev/max-charlamb/exception-direct-pointer branch from db309be to e6688b3 Compare April 22, 2026 21:54
Copilot AI review requested due to automatic review settings April 22, 2026 22:12
@max-charlamb max-charlamb force-pushed the dev/max-charlamb/exception-direct-pointer branch from e6688b3 to 2812642 Compare April 22, 2026 22:12
@max-charlamb max-charlamb force-pushed the dev/max-charlamb/exception-direct-pointer branch from 2812642 to 9d54eec Compare April 22, 2026 22:15
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 3 comments.

Comment thread src/native/managed/cdac/tests/ThreadTests.cs
Comment thread src/native/managed/cdac/tests/ThreadTests.cs Outdated
@max-charlamb max-charlamb force-pushed the dev/max-charlamb/exception-direct-pointer branch from 9d54eec to e761d3e Compare April 23, 2026 02:35
Copilot AI review requested due to automatic review settings April 23, 2026 03:01
@max-charlamb max-charlamb force-pushed the dev/max-charlamb/exception-direct-pointer branch from e761d3e to 34f7963 Compare April 23, 2026 03:01
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 1 comment.

Comment thread src/coreclr/vm/exstate.cpp Outdated
Replace the GCHandle-based m_hThrowable field in ExInfo with direct use
of the existing m_exception OBJECTREF field, matching NativeAOT's approach.

Key changes:
- Remove OBJECTHANDLE m_hThrowable from ExInfo, saving 8 bytes (64-bit)
- Update AsmOffsets constants for the new field layout
- Add GC root scanning of ExInfo chain in ScanStackRoots (gcenv.ee.cpp),
  mirroring NativeAOT's GcScanRootsWorker pattern
- Simplify GetThrowable() to return m_exception directly
- SetThrowable() no longer creates GC handles for ExInfo
- Remove GetThrowableAsHandle() entirely — all callers migrated to use
  GetThrowable() (OBJECTREF) or m_LastThrownObjectHandle (real handle)
- Update StackTraceInfo::AppendElement OBJECTREF overload to preserve
  foreign-exception semantics and preallocated exception checks
- Update Interop propagation callback to take OBJECTREF instead of handle
- Update DAC code (request.cpp, task.cpp, dacdbiimpl.cpp) to use
  m_exception directly
- Update debugger code (eedbginterfaceimpl.cpp, debugger.cpp) to use
  m_LastThrownObjectHandle for handle-based APIs
- Update cDAC: ThrownObjectHandle -> ThrownObject (direct pointer)
- Update cDAC contracts, data classes, and tests

This eliminates ~5 interlocked handle alloc/destroy ops per exception
throw, removes OOM fallback paths, and unblocks cDAC unification.
Thread::m_LastThrownObjectHandle remains as-is (separate work item).

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 24, 2026 01:38
@max-charlamb max-charlamb force-pushed the dev/max-charlamb/exception-direct-pointer branch from adffa37 to 7aae554 Compare April 24, 2026 01:38
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 25 out of 25 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/vm/gcenv.ee.cpp
Comment thread src/coreclr/vm/exceptionhandling.cpp Outdated
The ExInfo::m_exception field was being reported to the GC twice:
once via GCPROTECT_BEGIN and once via ExInfo chain scanning in
ScanStackRoots. The CLR code guide (section 2.1.5) explicitly states
that reporting the same location twice corrupts the GC's relocation
logic. Remove the GCPROTECT_BEGIN/END for m_exception and rely solely
on chain scanning (matching NativeAOT's model). Add
Thread::ObjectRefProtected calls in checked builds to satisfy the
debug OBJECTREF tracking table.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@max-charlamb max-charlamb requested a review from jkotas April 24, 2026 18:58
@max-charlamb max-charlamb marked this pull request as ready for review April 24, 2026 18:58
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

@max-charlamb
Copy link
Copy Markdown
Member Author

max-charlamb commented Apr 29, 2026

Results for ubuntu24_azure_turin

BenchmarkDotNet v0.15.8, Linux Ubuntu 24.04.4 LTS (Noble Numbat)
AMD EPYC 9V45 2.60GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK 11.0.100-preview.5.26228.123
  [Host]     : .NET 10.0.7 (10.0.7, 10.0.726.21808), X64 RyuJIT x86-64-v4
Method Toolchain Mean Error Ratio
ThrowCatch PR #127300 680.8 ns 9.82 ns 1.00
ThrowCatch main 715.2 ns 4.02 ns 1.05
NestedCatchThrowNew PR #127300 12,216.4 ns 119.83 ns 1.00
NestedCatchThrowNew main 12,446.6 ns 112.60 ns 1.02

Full logs

@max-charlamb max-charlamb requested a review from janvorli April 29, 2026 20:43
Copy link
Copy Markdown
Member

@janvorli janvorli left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM, thank you!

Comment thread src/coreclr/vm/excep.cpp Outdated
@janvorli
Copy link
Copy Markdown
Member

@max-charlamb, unless you already did, can you also please run the private diagnostic tests before merging this PR?

AppendElementImpl had only one caller after removing the OBJECTHANDLE
overload. Inline it into AppendElement and remove the separate function.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@github-actions

This comment has been minimized.

Comment thread src/coreclr/vm/threads.h Outdated
Co-authored-by: Jan Kotas <jkotas@microsoft.com>
Copilot AI review requested due to automatic review settings April 30, 2026 17:54
@github-actions

This comment has been minimized.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 2 comments.

Comment thread src/coreclr/debug/daccess/request.cpp Outdated
@max-charlamb
Copy link
Copy Markdown
Member Author

@max-charlamb, unless you already did, can you also please run the private diagnostic tests before merging this PR?

Verified on the private diagnostics suite

dac_cast<TADDR>(&m_exception) returns the host address in DAC builds,
but we need the target address. PTR_HOST_MEMBER_TADDR correctly
computes the target address of the field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@max-charlamb max-charlamb enabled auto-merge (squash) April 30, 2026 19:46
@github-actions
Copy link
Copy Markdown
Contributor

Note

This review was generated by Copilot.

🤖 Copilot Code Review — PR #127300

Holistic Assessment

Motivation: The PR removes the GC handle indirection (m_hThrowable) for exception objects in ExInfo, replacing it with a direct OBJECTREF that is GC-reported via ExInfo chain scanning in ScanStackRoots. This eliminates handle allocation/deallocation overhead, saves 8 bytes per ExInfo on 64-bit, and aligns CoreCLR with NativeAOT's exception handling pattern. The motivation is sound — handle-based exception storage was a legacy design.

Approach: The approach is well-reasoned. GC reporting is moved to an explicit scan of the ExInfo chain in ScanStackRoots, GCPROTECT is removed (to avoid double-reporting which corrupts GC relocation), and DAC/debugger consumers get a pseudo-handle (the target address of the m_exception field) that semantically behaves like a handle for read-through purposes. The cDAC and test updates are consistent with the native changes.

Summary: ⚠️ Needs Human Review. The core GC mechanism appears correct and the implementation is clean, but there are two subtle concerns around the debugger pseudo-handle contract that warrant human verification from someone familiar with the ICorDebug right-side and the ForceCatchHandlerFoundTable interaction.


Detailed Findings

✅ GC Correctness — ExInfo chain scanning is sound

The new scanning loop in gcenv.ee.cpp:209-220 correctly walks the ExInfo chain and reports each m_exception slot. Key verification:

  • m_exception is initialized to NULL in the ExInfo constructor member initializer list before the ExInfo is linked into the chain (line 40 of exinfo.cpp).
  • The GC's Promote function safely handles NULL object pointers (interface.cpp:1057is_in_find_object_range(NULL) returns false, causing early exit).
  • The constructor links the ExInfo into the chain (m_pCurrentTracker = this) before any exception object is stored, so there's no window where a non-null m_exception exists in an unscanned ExInfo.
  • Removal of GCPROTECT_BEGIN/END is correct since double-reporting the same slot corrupts GC relocation logic.
  • USE_CHECKED_OBJECTREFS debug registration (Thread::ObjectRefProtected) ensures checked builds validate the root.
  • The comment correctly references NativeAOT's GcScanRootsWorker as the analogous pattern.

✅ Cleanup Completeness — No leftover references

Verified: no remaining references to m_hThrowable, SetThrowable, STEC_All, SetThrowableErrorChecking, AppendElementImpl, or GetThrowableAsHandle exist in the codebase. The removal is thorough.

⚠️ Debugger Integration — ForceCatchHandlerFoundTable lookup with pseudo-handle (advisory, not merge-blocking)

In debugger.cpp:7870-7871:

OBJECTHANDLE objHandle = pThread->GetThrowableAsPseudoHandle();
OBJECTHANDLE retrievedHandle = m_pForceCatchHandlerFoundEventsTable->Lookup(objHandle);

The ForceCatchHandlerFoundSHashTraits::Equals and Hash functions (debugger.h:503-513) call ObjectFromHandle on both the stored handles (real long weak handles created via CreateLongWeakHandle) and the lookup key (pseudo-handle). Since ObjectFromHandle just dereferences *(Object**)handle, both a real handle and the pseudo-handle correctly resolve to the same Object* when the same exception is active. This works correctly.

However, this creates an implicit contract: the pseudo-handle must be dereferenceable whenever ShouldSendCatchHandlerFound is called. Since this only happens during active exception dispatch (when the ExInfo is on the stack and the thread is in COOP mode), this appears safe. But the coupling between a debugger hash table expecting real GC handles and a novel stack-address pseudo-handle is subtle.

Suggestion: Consider adding a brief comment at the ShouldSendCatchHandlerFound usage noting that the pseudo-handle is safe to pass to Lookup because ObjectFromHandle only dereferences the pointer (identical to dereferencing a real handle).

⚠️ DAC Pseudo-Handle Lifetime — GetCurrentException returns stack address to right side (advisory, not merge-blocking)

In dacdbiimpl.cpp:4958-4970, GetCurrentException returns the pseudo-handle (address of ExInfo::m_exception on the stack) as a VMPTR_OBJECTHANDLE to the debugger right side. Previously this was a GC handle table entry (long-lived heap memory).

Since DAC operates on point-in-time snapshots of a suspended process, this is safe for typical scenarios. However, if any right-side debugger code caches this handle value across process resume/suspend cycles, it could reference a dangling stack address (the ExInfo may have been popped). This is unlikely given the ICorDebug protocol re-queries state after resume, but warrants confirmation from someone familiar with the right-side implementation.

✅ Interop Changes — ObjC exception propagation correctly updated

The GetPropagatingExceptionCallback signature change from OBJECTHANDLE to OBJECTREF is correct. The caller in exceptionhandling.cpp:3957 passes pTopExInfo->m_exception directly. The callee properly GCPROTECT_BEGINs the OBJECTREF before making calls that could trigger GC. The mode contract correctly changed from MODE_PREEMPTIVE to MODE_COOPERATIVE.

✅ AsmOffsets — Field offset adjustments are consistent

Removing the 8-byte m_hThrowable field shifts subsequent field offsets down by 8 bytes on 64-bit (0xa8→0xa0, 0xb0→0xa8, etc.) and 4 bytes on 32-bit (0x5c→0x58, 0x60→0x5c, etc.). The AsmOffsets.cs changes reflect exactly this shift.

✅ cDAC Contract Updates — Correct adaptation

The cDAC changes correctly:

  • Change ExceptionInfo.ThrownObjectHandle (ObjectHandle) to ThrownObject (pointer)
  • Compute the pseudo-handle by adding the field offset to the ExInfo address
  • Short-circuit to null when ThrownObject == TargetPointer.Null
  • Tests are updated to verify read-through semantics (asserting the pointer dereferences to the expected object)

✅ Thread Safety — No races in the scanning path

The ExInfo chain is only modified on the owning thread (exception dispatch is per-thread), and GC scanning only occurs when threads are suspended. There is no race between writing m_exception and reading it during GC.

💡 Minor — prestub.cpp AppendElement calls

In prestub.cpp:1929 and 2134, the code now does StackTraceInfo::AppendElement(ObjectFromHandle(ohThrowable), ...). The handle here is LastThrownObjectHandle() (a real GC handle, not a pseudo-handle), so this is correct. Just noting the pattern is intentionally different from the ExInfo path.

Generated by Code Review for issue #127300 ·

The ExInfo::m_exception slot is GC-reported via the ExInfo chain scanner in ScanStackRoots (gcenv.ee.cpp). Reporting the same OBJECTREF location to the GC twice (once via GCPROTECT, once via the chain scanner) corrupts the relocate phase: the slot gets relocated twice, producing a garbage pointer that AVs on the next read.

This was missed in the earlier conversion of the three sites in exceptionhandling.cpp. It was reproducible on x86 checked builds running JitTest_throw_SEH, where SEH (hardware faults) routes through HandleManagedFault.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings May 1, 2026 04:07
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated no new comments.

@max-charlamb
Copy link
Copy Markdown
Member Author

/ba-g Test failures are unrelated to PR changes

@max-charlamb max-charlamb merged commit dc3d5a0 into main May 1, 2026
107 of 119 checks passed
@max-charlamb max-charlamb deleted the dev/max-charlamb/exception-direct-pointer branch May 1, 2026 14:30
max-charlamb added a commit to max-charlamb/runtime that referenced this pull request May 1, 2026
Two changes responding to PR review feedback:

1. Remove dead CEEInfo::HandleException.

   The function has been unreachable since 2016 (commit 4d9f4b8 `Remove SEH interactions between the JIT and the EE'') which replaced the old ICorJitInfo::FilterException/HandleException pair with runWithErrorTrap. The function is private, non-virtual, not part of the ICorJitInfo interface, and has zero callers in coreclr, the JIT, the AOT thunks, or SuperPMI (the SuperPMI Packet_HandleException slot is commented out). Removing it also retires the long-stale comment about `sync between the LTO and the exception tracker'' that pre-dates the ExInfo redesign and the lazy-LTO model from dotnet#127300/dotnet#127649.

2. Reorder declarations in threads.h so SetLastThrownObject precedes SetSOForLastThrownObject, matching the order of the definitions in threads.cpp.

Also update an unrelated stale comment in ExInfo::PopExInfos: the `unmanaged thread'' rationale is incorrect because both UMThunkUnwindFrameChainHandler and CallDescrWorkerUnwindFrameChainHandler short-circuit unmanaged threads before reaching PopExInfos, and the function carries a MODE_COOPERATIVE contract.

No behavior change.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
hoyosjs added a commit that referenced this pull request May 5, 2026
…xception dispatch (#127741)

After #127300 removed ExInfo::m_hThrowable and SetThrowable(),
Thread::m_LastThrownObjectHandle is no longer updated during active
exception dispatch. This causes a staleclastThrownObjectHandle to ge
returned by GetThreadData when a debugger breaks into the target
mid-dispatch.

When debugging a .NET 11 process with SOS (e.g. via dotnet-dump or
WinDbg), the following commands fail if the debuggee is stopped during
exception dispatch (for example, with SXE CLR):

- !PrintException — reports "Invalid exception object" because it
dereferences the stale handle and gets a null or recycled pointer
- !Threads — shows no exception column for threads that are actively
throwing
- !clrstack -a with exception context — may show incomplete or missing
exception info
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants