From 96f3f37f3166303ac7179ed666d6062083913878 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Tue, 6 Jan 2026 17:03:01 +0100 Subject: [PATCH] JIT: Fix GC info for fast tailcalls with contained targets For fast tailcalls we consume the operands as part of generating the `GT_CALL` node, yet we do not actually use values in the registers until after generating the epilog. This is incompatible with assumptions made by the code that implements the consumption side of operands. That code assumes that the registers will be used immediately and kills any GC information/local information immediately. Normally this is not a problem. We end up with a wrong GC information view in codegen, but since the emitter uses a lazy approach to register GC information, we don't end up actually reporting the wrong GC information. However, we can see a problem because of various constructs where the emitter ends up synchronizing its GC information view with the view that codegen had -- for example if a label with GC information was created. That started happening recently with #107283 when we started allowing tailcalls out of methods with GS cookie checks. Fix the situation by remarking the base/index of contained indirections as containing GC pointers if necessary. There is already corresponding logic for arguments that does the same remarking. The fix is only needed on x64/x86 since other platforms do not support contained indirections in calls and hence the target will not usually contain any GC pointers. --- src/coreclr/jit/codegenlinear.cpp | 4 ++-- src/coreclr/jit/codegenxarch.cpp | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/src/coreclr/jit/codegenlinear.cpp b/src/coreclr/jit/codegenlinear.cpp index b71d8efffefa13..1531987846f084 100644 --- a/src/coreclr/jit/codegenlinear.cpp +++ b/src/coreclr/jit/codegenlinear.cpp @@ -538,11 +538,11 @@ void CodeGen::genCodeForBBlist() nonVarPtrRegs &= ~RBM_ASYNC_CONTINUATION_RET; } - // For a tailcall arbitrary argument registers may be live into the + // For a tailcall arbitrary argument/target registers may be live into the // epilog. Skip validating those. if (block->HasFlag(BBF_HAS_JMP)) { - nonVarPtrRegs &= ~fullIntArgRegMask(CorInfoCallConvExtension::Managed); + nonVarPtrRegs = RBM_NONE; } if (nonVarPtrRegs) diff --git a/src/coreclr/jit/codegenxarch.cpp b/src/coreclr/jit/codegenxarch.cpp index 320d53d7cef864..0f40fe6085d8c1 100644 --- a/src/coreclr/jit/codegenxarch.cpp +++ b/src/coreclr/jit/codegenxarch.cpp @@ -6052,6 +6052,21 @@ void CodeGen::genCall(GenTreeCall* call) if (target->isContainedIndir()) { genConsumeAddress(target->AsIndir()->Addr()); + + // Consuming these registers will ensure the registers containing the state we need are available here, + // but it assumes we will use them immediately and will thus kill the live state. Since these registers + // are live into the epilog we need to remark them as live. + // This logic is similar to what genCallPlaceRegArgs does above for argument registers. + GenTreeIndir* indir = target->AsIndir(); + if (indir->HasBase() && indir->Base()->TypeIs(TYP_BYREF, TYP_REF)) + { + gcInfo.gcMarkRegPtrVal(indir->Base()->GetRegNum(), indir->Base()->TypeGet()); + } + + if (indir->HasIndex() && indir->Index()->TypeIs(TYP_BYREF, TYP_REF)) + { + gcInfo.gcMarkRegPtrVal(indir->Index()->GetRegNum(), indir->Index()->TypeGet()); + } } else {