From a7f091d22986ef449d7d2e3fc4288e0813c77ace Mon Sep 17 00:00:00 2001 From: Andy Ayers Date: Sun, 7 Jun 2026 10:04:33 -0700 Subject: [PATCH] [Wasm R2R] Report tracked GC vars as untracked in GC info The wasm JIT does not emit tracked GC slot lifetimes (noTrackedGCSlots is true on wasm), but the untracked-vars encoding loop in gcMakeRegPtrTable was still skipping any lvTracked local. Those vars therefore fell through both encoding paths and were never reported to the runtime, leaving the shadow-stack slots that hold their values invisible to the precise GC scan. A GC during a method that kept GC refs in tracked locals would collect or move those objects without updating the slots, producing spurious IndexOutOfRangeException, AV, or silent corruption on R2R. Fix: - gcencode.cpp: gate the 'skip tracked vars' branches in gcMakeRegPtrTable on !noTrackedGCSlots, so on wasm tracked on-frame GC locals and tracked register-arg GC params are reported as untracked (live for the whole method). - codegencommon.cpp: force lvMustInit for any on-frame GC ptr local on wasm, so the wasm prolog zero-inits those slots. Without this, a GC reached before the first assignment would scan stale shadow-stack memory as a GC root. Fixes #128234. --- src/coreclr/jit/codegencommon.cpp | 8 ++++++++ src/coreclr/jit/gcencode.cpp | 8 ++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/codegencommon.cpp b/src/coreclr/jit/codegencommon.cpp index f12c269b773a12..95b2df06924de8 100644 --- a/src/coreclr/jit/codegencommon.cpp +++ b/src/coreclr/jit/codegencommon.cpp @@ -3799,6 +3799,14 @@ void CodeGen::genCheckUseBlockInit() JITDUMP("must init a tracked V%02u because it a struct with a GC ref\n", varNum); mustInitThisVar = true; } +#ifdef TARGET_WASM + else if (hasGCPtr) + { + // On wasm all GC vars are reported as untracked, so the slot must be zero-inited. + JITDUMP("must init V%02u because wasm reports tracked GC vars as untracked\n", varNum); + mustInitThisVar = true; + } +#endif else { // We are done with tracked or GC vars, now look at untracked vars without GC refs. diff --git a/src/coreclr/jit/gcencode.cpp b/src/coreclr/jit/gcencode.cpp index f48e73726ec1b2..4016723ad02806 100644 --- a/src/coreclr/jit/gcencode.cpp +++ b/src/coreclr/jit/gcencode.cpp @@ -4134,7 +4134,9 @@ void GCInfo::gcMakeRegPtrTable( // If it is pinned, it must be an untracked local. assert(!varDsc->lvPinned || !varDsc->lvTracked); - if (varDsc->lvTracked || !varDsc->lvOnFrame) + // When noTrackedGCSlots is true (e.g. on wasm) tracked on-frame GC vars + // must be reported here as untracked, since gcVarPtrList is not populated. + if ((varDsc->lvTracked && !noTrackedGCSlots) || !varDsc->lvOnFrame) { continue; } @@ -4161,7 +4163,9 @@ void GCInfo::gcMakeRegPtrTable( } else { - if (varDsc->lvIsRegArg && varDsc->lvTracked) + // When noTrackedGCSlots is true (e.g. on wasm) tracked register args + // must fall through and be reported as untracked. + if (varDsc->lvIsRegArg && varDsc->lvTracked && !noTrackedGCSlots) { // If this register-passed arg is tracked, then // it has been allocated space near the other