From 705a29ac6ed281fc028ff41de02003ddab5c5dcd Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Sun, 27 Sep 2026 22:15:28 -0500 Subject: [PATCH 1/4] Report untracked GC slots for aborted Wasm R2R frames Wasm R2R code has no interruptible ranges and reports all of its frame GC refs as untracked (pinned) slots. When an exception is thrown from a funclet, that frame is ExecutionAborted, so EnumerateLiveSlots reported nothing for it. Its parent frames share the same frame slots and are skipped as already reported by the funclet, so no frame reported the method's untracked slots during exception dispatch. A GC in that window could free objects still held in those slots, and a later report of the slot then hits a dangling reference. Keep reporting untracked slots for aborted Wasm frames that have no interruptible ranges. Interpreter code always has an interruptible range and native targets are unchanged. This fixes the SanityCheck assert in nested EH tests on browser-wasm R2R (Test_throwinfinallynestedintry_30, throwincascadedexcept_d, throwincascadedexceptnofin_d), which reproduces deterministically with DOTNET_GCStress=0x1. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/vm/gcinfodecoder.cpp | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/src/coreclr/vm/gcinfodecoder.cpp b/src/coreclr/vm/gcinfodecoder.cpp index 6460eace52685a..89cadf1e3bfd87 100644 --- a/src/coreclr/vm/gcinfodecoder.cpp +++ b/src/coreclr/vm/gcinfodecoder.cpp @@ -736,6 +736,9 @@ template bool TGcInfoDecoder::Enumerat { unsigned executionAborted = (inputFlags & ExecutionAborted); +#ifdef TARGET_WASM + bool reportUntrackedOnly = false; +#endif // TARGET_WASM // In order to make ARM more x86-like we only ever report the leaf frame // of any given function. We accomplish this by having the stackwalker @@ -807,8 +810,20 @@ template bool TGcInfoDecoder::Enumerat _ASSERTE(countIntersections <= 1); if(countIntersections == 0 && executionAborted) { - LOG((LF_GCROOTS, LL_INFO100000, "Not reporting this frame because it is aborted and not fully interruptible.\n")); - goto ExitSuccess; +#ifdef TARGET_WASM + // Wasm R2R code has no interruptible ranges and reports all frame GC refs as untracked + // slots, which an aborted funclet shares with parent frames that are skipped. + if (m_NumInterruptibleRanges == 0) + { + LOG((LF_GCROOTS, LL_INFO100000, "Reporting only untracked slots because this frame is aborted and not interruptible.\n")); + reportUntrackedOnly = true; + } + else +#endif // TARGET_WASM + { + LOG((LF_GCROOTS, LL_INFO100000, "Not reporting this frame because it is aborted and not fully interruptible.\n")); + goto ExitSuccess; + } } } #else // !PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED @@ -829,6 +844,11 @@ template bool TGcInfoDecoder::Enumerat slotDecoder.DecodeSlotTable(m_Reader); +#ifdef TARGET_WASM + if (reportUntrackedOnly) + goto ReportUntracked; +#endif // TARGET_WASM + { UINT32 numSlots = slotDecoder.GetNumTracked(); From bfb58d6f11a0f3894b30f361942a0098d3193f4c Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 01:09:28 -0500 Subject: [PATCH 2/4] Key aborted-frame untracked reporting on the GC info encoding Address review feedback: - Add a HAS_INTERRUPTIBLE_RANGES trait to each GcInfoEncoding, false only for Wasm32GcInfoEncoding. The interpreter encoding is also used on Wasm and does define interruptible ranges, so TARGET_WASM is not the right gate. - In EnumerateLiveSlots, skip the interruptible range handling for encodings without interruptible ranges and report only untracked slots, including for aborted frames. Assert the invariant in the decoder and the encoder. - Mirror the change in the cDAC GCInfoDecoder and document it in GCInfo.md. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 2 ++ src/coreclr/gcinfo/gcinfoencoder.cpp | 2 ++ src/coreclr/inc/gcinfotypes.h | 8 ++++++ src/coreclr/vm/gcinfodecoder.cpp | 28 +++++++------------ .../Contracts/GCInfo/GCInfoDecoder.cs | 7 +++++ .../GCInfo/PlatformTraits/IGCInfoTraits.cs | 3 ++ 6 files changed, 32 insertions(+), 18 deletions(-) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index ec8e820e248f84..11e8a44494b097 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -647,6 +647,8 @@ For each chunk, the encoding stores: To determine liveness at the target offset: start from the chunk's final state, then apply any transitions that occur *after* the target offset (toggling the state backwards). A slot is live if its final state (after toggle adjustment) is 1. +For encodings without interruptible ranges (`HAS_INTERRUPTIBLE_RANGES` is false, as for Wasm R2R), this step is replaced by reporting only untracked slots, including for `ExecutionAborted` frames. An aborted funclet shares those slots with parent frames that are skipped. + **Step 4 — Report untracked slots**: Untracked slots are always live (they represent stack locations the JIT doesn't track at each safe point). They are reported unconditionally unless `ParentOfFuncletStackFrame` or `NoReportUntracked` flags are set. Untracked slots are reported with `reportScratchSlots=true` since the JIT may produce untracked scratch register slots for interior pointers. **Slot filtering**: Before reporting any slot, the algorithm checks: diff --git a/src/coreclr/gcinfo/gcinfoencoder.cpp b/src/coreclr/gcinfo/gcinfoencoder.cpp index 34ddc23e9c42b6..86d589afa9ffcc 100644 --- a/src/coreclr/gcinfo/gcinfoencoder.cpp +++ b/src/coreclr/gcinfo/gcinfoencoder.cpp @@ -612,6 +612,8 @@ template void TGcInfoEncoder::WriteSlo template void TGcInfoEncoder::DefineInterruptibleRange( UINT32 startInstructionOffset, UINT32 length ) { + _ASSERTE(GcInfoEncoding::HAS_INTERRUPTIBLE_RANGES); + UINT32 stopInstructionOffset = startInstructionOffset + length; UINT32 normStartOffset = GcInfoEncoding::NORMALIZE_CODE_OFFSET(startInstructionOffset); diff --git a/src/coreclr/inc/gcinfotypes.h b/src/coreclr/inc/gcinfotypes.h index 682c294014aa63..2ef36ca12051ef 100644 --- a/src/coreclr/inc/gcinfotypes.h +++ b/src/coreclr/inc/gcinfotypes.h @@ -618,6 +618,7 @@ struct AMD64GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #elif defined(TARGET_ARM) @@ -676,6 +677,7 @@ struct ARM32GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #elif defined(TARGET_ARM64) @@ -737,6 +739,7 @@ struct ARM64GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #elif defined(TARGET_LOONGARCH64) @@ -796,6 +799,7 @@ struct LoongArch64GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #elif defined(TARGET_RISCV64) @@ -856,6 +860,7 @@ struct RISCV64GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #elif defined(TARGET_X86) @@ -911,6 +916,7 @@ struct X86GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = true; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #elif defined(TARGET_WASM) && !defined(TARGET_64BIT) @@ -968,6 +974,7 @@ struct Wasm32GcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = false; + static const bool HAS_INTERRUPTIBLE_RANGES = false; }; #else // No target defined @@ -1031,6 +1038,7 @@ struct InterpreterGcInfoEncoding { static const int LIVESTATE_RLE_RUN_ENCBASE = 2; static const int LIVESTATE_RLE_SKIP_ENCBASE = 4; static const bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = false; + static const bool HAS_INTERRUPTIBLE_RANGES = true; }; #endif // FEATURE_INTERPRETER diff --git a/src/coreclr/vm/gcinfodecoder.cpp b/src/coreclr/vm/gcinfodecoder.cpp index 89cadf1e3bfd87..31bc38f25905e7 100644 --- a/src/coreclr/vm/gcinfodecoder.cpp +++ b/src/coreclr/vm/gcinfodecoder.cpp @@ -382,6 +382,7 @@ TGcInfoDecoder::TGcInfoDecoder( { m_NumInterruptibleRanges = (UINT32) m_Reader.DecodeVarLengthUnsigned(GcInfoEncoding::NUM_INTERRUPTIBLE_RANGES_ENCBASE); } + _ASSERTE(GcInfoEncoding::HAS_INTERRUPTIBLE_RANGES || m_NumInterruptibleRanges == 0); #ifdef PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED if(flags & (DECODE_GC_LIFETIMES | DECODE_INTERRUPTIBILITY)) @@ -736,9 +737,7 @@ template bool TGcInfoDecoder::Enumerat { unsigned executionAborted = (inputFlags & ExecutionAborted); -#ifdef TARGET_WASM bool reportUntrackedOnly = false; -#endif // TARGET_WASM // In order to make ARM more x86-like we only ever report the leaf frame // of any given function. We accomplish this by having the stackwalker @@ -780,6 +779,13 @@ template bool TGcInfoDecoder::Enumerat m_Reader.DecodeVarLengthUnsigned( GcInfoEncoding::INTERRUPTIBLE_RANGE_DELTA2_ENCBASE ); } } + else if constexpr (!GcInfoEncoding::HAS_INTERRUPTIBLE_RANGES) + { + // Outside of safe points only untracked slots can be reported. Report them for aborted + // frames too: an aborted funclet shares them with parent frames that are skipped. + _ASSERTE(m_NumInterruptibleRanges == 0); + reportUntrackedOnly = true; + } else { // @@ -810,20 +816,8 @@ template bool TGcInfoDecoder::Enumerat _ASSERTE(countIntersections <= 1); if(countIntersections == 0 && executionAborted) { -#ifdef TARGET_WASM - // Wasm R2R code has no interruptible ranges and reports all frame GC refs as untracked - // slots, which an aborted funclet shares with parent frames that are skipped. - if (m_NumInterruptibleRanges == 0) - { - LOG((LF_GCROOTS, LL_INFO100000, "Reporting only untracked slots because this frame is aborted and not interruptible.\n")); - reportUntrackedOnly = true; - } - else -#endif // TARGET_WASM - { - LOG((LF_GCROOTS, LL_INFO100000, "Not reporting this frame because it is aborted and not fully interruptible.\n")); - goto ExitSuccess; - } + LOG((LF_GCROOTS, LL_INFO100000, "Not reporting this frame because it is aborted and not fully interruptible.\n")); + goto ExitSuccess; } } #else // !PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED @@ -844,10 +838,8 @@ template bool TGcInfoDecoder::Enumerat slotDecoder.DecodeSlotTable(m_Reader); -#ifdef TARGET_WASM if (reportUntrackedOnly) goto ReportUntracked; -#endif // TARGET_WASM { UINT32 numSlots = slotDecoder.GetNumTracked(); diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index 26beed0ee971be..0fae7fa8a0f34d 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -976,6 +976,13 @@ private bool EnumerateLiveSlots( { // We have a safe point match — skip interruptible range computation } + else if (!TTraits.HAS_INTERRUPTIBLE_RANGES) + { + // Outside of safe points only untracked slots can be reported. Report them for aborted + // frames too: an aborted funclet shares them with parent frames that are skipped. + Debug.Assert(_interruptibleRanges.Count == 0); + return ReportUntrackedAndSucceed(); + } else { // Compute pseudoBreakOffset from interruptible ranges diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs index a91e495fd8d0c9..4c6b0f75a8544a 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/PlatformTraits/IGCInfoTraits.cs @@ -50,6 +50,9 @@ internal interface IGCInfoTraits static abstract bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA { get; } + // False for encodings whose code is never interruptible (Wasm R2R). + static virtual bool HAS_INTERRUPTIBLE_RANGES => true; + /// /// Returns true if the given register is a scratch (volatile) register. /// Scratch register slots should only be reported for the active (leaf) stack frame. From cbaff5fd3c4651a5d8610aedf9ac3a437ca74c8d Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 18:17:53 -0500 Subject: [PATCH 3/4] Omit the interruptible range count from Wasm GC info Address review feedback: encodings without interruptible ranges no longer serialize NumInterruptibleRanges in the fat header. The encoder, the runtime decoder, the cDAC decoder and R2RDump all key off HAS_INTERRUPTIBLE_RANGES, which is false only for Wasm32. The asserts on the decoded count are dropped since it is no longer read. This changes the Wasm GC info format, so bump the R2R major version to 31. On browser-wasm CoreLib this saves 1,088 bytes: 2 bits per fat header, which shrinks a blob only when it crosses a byte boundary, and identical GC info blobs are shared. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- docs/design/datacontracts/GCInfo.md | 7 +++++-- src/coreclr/gcinfo/gcinfoencoder.cpp | 2 +- src/coreclr/inc/gcinfoencoder.h | 2 +- src/coreclr/inc/readytorun.h | 4 +++- src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h | 2 +- src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs | 2 +- .../aot/ILCompiler.Reflection.ReadyToRun/Amd64/GcInfo.cs | 2 +- .../aot/ILCompiler.Reflection.ReadyToRun/GCInfoTypes.cs | 2 ++ src/coreclr/vm/gcinfodecoder.cpp | 4 +--- .../Contracts/GCInfo/GCInfoDecoder.cs | 5 +++-- 10 files changed, 19 insertions(+), 13 deletions(-) diff --git a/docs/design/datacontracts/GCInfo.md b/docs/design/datacontracts/GCInfo.md index 11e8a44494b097..a114899650afda 100644 --- a/docs/design/datacontracts/GCInfo.md +++ b/docs/design/datacontracts/GCInfo.md @@ -220,7 +220,7 @@ The fat header is used for methods that cannot be encoded using the compact slim | ReversePInvokeFrameSlot | `REVERSE_PINVOKE_FRAME_ENCBASE` | Normalized reverse P/Invoke frame slot | If GC_INFO_REVERSE_PINVOKE_FRAME | | SizeOfStackOutgoingAndScratchArea | `SIZE_OF_STACK_AREA_ENCBASE` | Size of stack parameter area | Platform dependent | | NumSafePoints | `NUM_SAFE_POINTS_ENCBASE` | Number of safe points/callsites | #ifdef PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED | -| NumInterruptibleRanges | `NUM_INTERRUPTIBLE_RANGES_ENCBASE` | Number of interruptible ranges | | +| NumInterruptibleRanges | `NUM_INTERRUPTIBLE_RANGES_ENCBASE` | Number of interruptible ranges | If `HAS_INTERRUPTIBLE_RANGES` (omitted for Wasm R2R) | ##### Header Flags @@ -517,7 +517,10 @@ if HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA: // platform-dependent fixedStackParameterScratchArea = DenormalizeSizeOfStackArea(DecodeVarLengthUnsigned(...)) numSafePoints = DecodeVarLengthUnsigned(NUM_SAFE_POINTS_ENCBASE) -numInterruptibleRanges = DecodeVarLengthUnsigned(NUM_INTERRUPTIBLE_RANGES_ENCBASE) +if HAS_INTERRUPTIBLE_RANGES: // false for Wasm R2R, which never has interruptible ranges + numInterruptibleRanges = DecodeVarLengthUnsigned(NUM_INTERRUPTIBLE_RANGES_ENCBASE) +else: + numInterruptibleRanges = 0 ``` #### Body Decoding diff --git a/src/coreclr/gcinfo/gcinfoencoder.cpp b/src/coreclr/gcinfo/gcinfoencoder.cpp index 86d589afa9ffcc..f69eb1154a8780 100644 --- a/src/coreclr/gcinfo/gcinfoencoder.cpp +++ b/src/coreclr/gcinfo/gcinfoencoder.cpp @@ -1110,7 +1110,7 @@ template void TGcInfoEncoder::Build() m_NumCallSites = numCallSites; #endif // PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED - if (slimHeader) + if (slimHeader || !GcInfoEncoding::HAS_INTERRUPTIBLE_RANGES) { _ASSERTE(numInterruptibleRanges == 0); } diff --git a/src/coreclr/inc/gcinfoencoder.h b/src/coreclr/inc/gcinfoencoder.h index 81bc90ef10465b..7e705bb8c516a7 100644 --- a/src/coreclr/inc/gcinfoencoder.h +++ b/src/coreclr/inc/gcinfoencoder.h @@ -43,7 +43,7 @@ - ReversePInvokeFrameSlot (if any) - SizeOfStackOutgoingAndScratchArea (#ifdef FIXED_STACK_PARAMETER_SCRATCH_AREA) - NumCallSites (#ifdef PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED) - - NumInterruptibleRanges + - NumInterruptibleRanges (fat header only; omitted if !HAS_INTERRUPTIBLE_RANGES) 2. Call sites offsets (#ifdef PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED) 3. Fully-interruptible ranges diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index 4097043a143da2..01704ade2154ad 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -19,7 +19,7 @@ // src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. -#define READYTORUN_MAJOR_VERSION 30 +#define READYTORUN_MAJOR_VERSION 31 #define READYTORUN_MINOR_VERSION 0x0000 #define MINIMUM_READYTORUN_MAJOR_VERSION 26 @@ -78,6 +78,8 @@ // consumes the scaffolding, so the flag is only ever set on WebAssembly images. // R2R Version 29.3 adds READYTORUN_HELPER_BulkWriteBarrierSmall. // R2R Version 30 requires implicit byref arguments to always be outside of the GC heap +// R2R Version 31 omits the interruptible range count from Wasm GC info, since Wasm code has no interruptible ranges. +// R2R 31 is not backward compatible with 30.x or earlier for Wasm images. struct READYTORUN_CORE_HEADER { diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h index 6451837e3a861b..f1d9763d3a9b10 100644 --- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h +++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h @@ -11,7 +11,7 @@ struct ReadyToRunHeaderConstants { static const uint32_t Signature = 0x00525452; // 'RTR' - static const uint32_t CurrentMajorVersion = 30; + static const uint32_t CurrentMajorVersion = 31; static const uint32_t CurrentMinorVersion = 0; }; diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs index 4d7f33de921d82..42eb0ae45f2097 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs @@ -15,7 +15,7 @@ internal struct ReadyToRunHeaderConstants { public const uint Signature = 0x00525452; // 'RTR' - public const ushort CurrentMajorVersion = 30; + public const ushort CurrentMajorVersion = 31; public const ushort CurrentMinorVersion = 0; } #if READYTORUN diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/Amd64/GcInfo.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/Amd64/GcInfo.cs index 111129841a5ddf..b38be1644ff478 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/Amd64/GcInfo.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/Amd64/GcInfo.cs @@ -174,7 +174,7 @@ public GcInfo(NativeReader imageReader, int offset, Machine machine, int version // PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED (this macro is always defined in _gcInfoTypes.h) NumSafePoints = imageReader.DecodeVarLengthUnsigned(_gcInfoTypes.NUM_SAFE_POINTS_ENCBASE, ref bitOffset); - if (!_slimHeader) + if (!_slimHeader && _gcInfoTypes.HAS_INTERRUPTIBLE_RANGES) { NumInterruptibleRanges = imageReader.DecodeVarLengthUnsigned(_gcInfoTypes.NUM_INTERRUPTIBLE_RANGES_ENCBASE, ref bitOffset); } diff --git a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/GCInfoTypes.cs b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/GCInfoTypes.cs index ad59bf379bf006..6dd5387285826f 100644 --- a/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/GCInfoTypes.cs +++ b/src/coreclr/tools/aot/ILCompiler.Reflection.ReadyToRun/GCInfoTypes.cs @@ -120,6 +120,7 @@ public class GcInfoTypes internal int LIVESTATE_RLE_SKIP_ENCBASE { get; } = 4; internal int NUM_NORM_CODE_OFFSETS_PER_CHUNK_LOG2 { get; } = 6; internal bool HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA { get; } = true; + internal bool HAS_INTERRUPTIBLE_RANGES { get; } = true; internal GcInfoTypes(Machine machine, bool denormalizeCodeOffsets) { @@ -180,6 +181,7 @@ internal GcInfoTypes(Machine machine, bool denormalizeCodeOffsets) LIVESTATE_RLE_RUN_ENCBASE = 2; LIVESTATE_RLE_SKIP_ENCBASE = 4; HAS_FIXED_STACK_PARAMETER_SCRATCH_AREA = false; + HAS_INTERRUPTIBLE_RANGES = false; break; case Machine.I386: diff --git a/src/coreclr/vm/gcinfodecoder.cpp b/src/coreclr/vm/gcinfodecoder.cpp index 31bc38f25905e7..4777ef53ebe5c2 100644 --- a/src/coreclr/vm/gcinfodecoder.cpp +++ b/src/coreclr/vm/gcinfodecoder.cpp @@ -374,7 +374,7 @@ TGcInfoDecoder::TGcInfoDecoder( m_SafePointIndex = m_NumSafePoints; #endif - if (slimHeader) + if (slimHeader || !GcInfoEncoding::HAS_INTERRUPTIBLE_RANGES) { m_NumInterruptibleRanges = 0; } @@ -382,7 +382,6 @@ TGcInfoDecoder::TGcInfoDecoder( { m_NumInterruptibleRanges = (UINT32) m_Reader.DecodeVarLengthUnsigned(GcInfoEncoding::NUM_INTERRUPTIBLE_RANGES_ENCBASE); } - _ASSERTE(GcInfoEncoding::HAS_INTERRUPTIBLE_RANGES || m_NumInterruptibleRanges == 0); #ifdef PARTIALLY_INTERRUPTIBLE_GC_SUPPORTED if(flags & (DECODE_GC_LIFETIMES | DECODE_INTERRUPTIBILITY)) @@ -783,7 +782,6 @@ template bool TGcInfoDecoder::Enumerat { // Outside of safe points only untracked slots can be reported. Report them for aborted // frames too: an aborted funclet shares them with parent frames that are skipped. - _ASSERTE(m_NumInterruptibleRanges == 0); reportUntrackedOnly = true; } else diff --git a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs index 0fae7fa8a0f34d..b5ac9c24c5fa9b 100644 --- a/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs +++ b/src/native/managed/cdac/Microsoft.Diagnostics.DataContractReader.Contracts/Contracts/GCInfo/GCInfoDecoder.cs @@ -493,7 +493,9 @@ private IEnumerable DecodeFatHeader() _numSafePoints = _reader.DecodeVarLengthUnsigned(TTraits.NUM_SAFE_POINTS_ENCBASE, ref _bitOffset); } - _numInterruptibleRanges = _reader.DecodeVarLengthUnsigned(TTraits.NUM_INTERRUPTIBLE_RANGES_ENCBASE, ref _bitOffset); + _numInterruptibleRanges = TTraits.HAS_INTERRUPTIBLE_RANGES + ? _reader.DecodeVarLengthUnsigned(TTraits.NUM_INTERRUPTIBLE_RANGES_ENCBASE, ref _bitOffset) + : 0; } private void EnsureDecodedTo(DecodePoints point) @@ -980,7 +982,6 @@ private bool EnumerateLiveSlots( { // Outside of safe points only untracked slots can be reported. Report them for aborted // frames too: an aborted funclet shares them with parent frames that are skipped. - Debug.Assert(_interruptibleRanges.Count == 0); return ReportUntrackedAndSucceed(); } else From 1eedf1188e87f334575652d86a3e3415184aa133 Mon Sep 17 00:00:00 2001 From: Larry Ewing Date: Mon, 28 Sep 2026 19:28:07 -0500 Subject: [PATCH 4/4] Revert the R2R version bump Wasm has not shipped, so Wasm-specific format changes don't need an R2R version bump. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- src/coreclr/inc/readytorun.h | 4 +--- src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h | 2 +- src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/coreclr/inc/readytorun.h b/src/coreclr/inc/readytorun.h index 01704ade2154ad..4097043a143da2 100644 --- a/src/coreclr/inc/readytorun.h +++ b/src/coreclr/inc/readytorun.h @@ -19,7 +19,7 @@ // src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h // If you update this, ensure you run `git grep MINIMUM_READYTORUN_MAJOR_VERSION` // and handle pending work. -#define READYTORUN_MAJOR_VERSION 31 +#define READYTORUN_MAJOR_VERSION 30 #define READYTORUN_MINOR_VERSION 0x0000 #define MINIMUM_READYTORUN_MAJOR_VERSION 26 @@ -78,8 +78,6 @@ // consumes the scaffolding, so the flag is only ever set on WebAssembly images. // R2R Version 29.3 adds READYTORUN_HELPER_BulkWriteBarrierSmall. // R2R Version 30 requires implicit byref arguments to always be outside of the GC heap -// R2R Version 31 omits the interruptible range count from Wasm GC info, since Wasm code has no interruptible ranges. -// R2R 31 is not backward compatible with 30.x or earlier for Wasm images. struct READYTORUN_CORE_HEADER { diff --git a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h index f1d9763d3a9b10..6451837e3a861b 100644 --- a/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h +++ b/src/coreclr/nativeaot/Runtime/inc/ModuleHeaders.h @@ -11,7 +11,7 @@ struct ReadyToRunHeaderConstants { static const uint32_t Signature = 0x00525452; // 'RTR' - static const uint32_t CurrentMajorVersion = 31; + static const uint32_t CurrentMajorVersion = 30; static const uint32_t CurrentMinorVersion = 0; }; diff --git a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs index 42eb0ae45f2097..4d7f33de921d82 100644 --- a/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs +++ b/src/coreclr/tools/Common/Internal/Runtime/ModuleHeaders.cs @@ -15,7 +15,7 @@ internal struct ReadyToRunHeaderConstants { public const uint Signature = 0x00525452; // 'RTR' - public const ushort CurrentMajorVersion = 31; + public const ushort CurrentMajorVersion = 30; public const ushort CurrentMinorVersion = 0; } #if READYTORUN