From 6bed9c3de3edf2c9040acd0566f7d2eeeea8e25f Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 14:14:18 +0100 Subject: [PATCH 1/7] JIT: Add a stress mode that poisons implicit byrefs This stress mode poisons all implicit byrefs before returns from the method. GC pointers are nulled out and other parts of the structs are filled with 0xcd bytes. This should help expose incorrectly elided copies in the recently added last-use copy elision optimization. --- src/coreclr/jit/compiler.cpp | 7 +++ src/coreclr/jit/compiler.h | 22 +++++---- src/coreclr/jit/importer.cpp | 88 ++++++++++++++++++++++++++++++++++++ src/coreclr/jit/lclvars.cpp | 6 +++ src/coreclr/jit/morph.cpp | 10 +++- 5 files changed, 123 insertions(+), 10 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index f86e1923f89dc..f2b5e2cb60016 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -1775,6 +1775,8 @@ void Compiler::compInit(ArenaAllocator* pAlloc, // set this early so we can use it without relying on random memory values verbose = compIsForInlining() ? impInlineInfo->InlinerCompiler->verbose : false; + + compPoisoningAnyImplicitByrefs = false; #endif #if defined(DEBUG) || defined(LATE_DISASM) || DUMP_FLOWGRAPHS || DUMP_GC_TABLES @@ -10179,6 +10181,10 @@ void Compiler::EnregisterStats::RecordLocal(const LclVarDsc* varDsc) m_dispatchRetBuf++; break; + case AddressExposedReason::STRESS_WRITE_IMPLICIT_BYREFS: + m_stressWriteImplicitByrefs++; + break; + default: unreached(); break; @@ -10274,5 +10280,6 @@ void Compiler::EnregisterStats::Dump(FILE* fout) const PRINT_STATS(m_osrExposed, m_addrExposed); PRINT_STATS(m_stressLclFld, m_addrExposed); PRINT_STATS(m_dispatchRetBuf, m_addrExposed); + PRINT_STATS(m_stressWriteImplicitByrefs, m_addrExposed); } #endif // TRACK_ENREG_STATS diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 46413b54eb59f..7b546a9c98d30 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -467,13 +467,14 @@ enum class DoNotEnregisterReason enum class AddressExposedReason { NONE, - PARENT_EXPOSED, // This is a promoted field but the parent is exposed. - TOO_CONSERVATIVE, // Were marked as exposed to be conservative, fix these places. - ESCAPE_ADDRESS, // The address is escaping, for example, passed as call argument. - WIDE_INDIR, // We access via indirection with wider type. - OSR_EXPOSED, // It was exposed in the original method, osr has to repeat it. - STRESS_LCL_FLD, // Stress mode replaces localVar with localFld and makes them addrExposed. - DISPATCH_RET_BUF // Caller return buffer dispatch. + PARENT_EXPOSED, // This is a promoted field but the parent is exposed. + TOO_CONSERVATIVE, // Were marked as exposed to be conservative, fix these places. + ESCAPE_ADDRESS, // The address is escaping, for example, passed as call argument. + WIDE_INDIR, // We access via indirection with wider type. + OSR_EXPOSED, // It was exposed in the original method, osr has to repeat it. + STRESS_LCL_FLD, // Stress mode replaces localVar with localFld and makes them addrExposed. + DISPATCH_RET_BUF, // Caller return buffer dispatch. + STRESS_WRITE_IMPLICIT_BYREFS, // This is an implicit byref we want to poison. }; #endif // DEBUG @@ -4183,6 +4184,7 @@ class Compiler void impLoadArg(unsigned ilArgNum, IL_OFFSET offset); void impLoadLoc(unsigned ilLclNum, IL_OFFSET offset); bool impReturnInstruction(int prefixFlags, OPCODE& opcode); + void impPoisonImplicitByrefsBeforeReturn(); // A free list of linked list nodes used to represent to-do stacks of basic blocks. struct BlockListNode @@ -9080,7 +9082,9 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX bool fgNormalizeEHDone; // Has the flowgraph EH normalization phase been done? size_t compSizeEstimate; // The estimated size of the method as per `gtSetEvalOrder`. size_t compCycleEstimate; // The estimated cycle count of the method as per `gtSetEvalOrder` -#endif // DEBUG + bool compPoisoningAnyImplicitByrefs; // Importer inserted IR before returns to poison implicit byrefs + +#endif // DEBUG bool fgLocalVarLivenessDone; // Note that this one is used outside of debug. bool fgLocalVarLivenessChanged; @@ -9600,6 +9604,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX STRESS_MODE(GENERIC_CHECK) \ STRESS_MODE(IF_CONVERSION_COST) \ STRESS_MODE(IF_CONVERSION_INNER_LOOPS) \ + STRESS_MODE(POISON_IMPLICIT_BYREFS) \ STRESS_MODE(COUNT) enum compStressArea @@ -10136,6 +10141,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX unsigned m_stressLclFld; unsigned m_dispatchRetBuf; unsigned m_wideIndir; + unsigned m_stressWriteImplicitByrefs; public: void RecordLocal(const LclVarDsc* varDsc); diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index a09f831ceb270..14c5f4416b2d3 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -6985,6 +6985,7 @@ void Compiler::impImportBlockCode(BasicBlock* block) case CEE_RET: prefixFlags &= ~PREFIX_TAILCALL; // ret without call before it + RET: if (!impReturnInstruction(prefixFlags, opcode)) { @@ -10887,6 +10888,12 @@ bool Compiler::impReturnInstruction(int prefixFlags, OPCODE& opcode) { assert(lvaInlineeReturnSpillTemp != BAD_VAR_NUM); } + + if (!compIsForInlining() && ((prefixFlags & (PREFIX_TAILCALL_EXPLICIT | PREFIX_TAILCALL_STRESS)) == 0) && + compStressCompile(STRESS_POISON_IMPLICIT_BYREFS, 25)) + { + impPoisonImplicitByrefsBeforeReturn(); + } #endif // DEBUG GenTree* op2 = nullptr; @@ -11204,6 +11211,87 @@ bool Compiler::impReturnInstruction(int prefixFlags, OPCODE& opcode) return true; } +#ifdef DEBUG +//------------------------------------------------------------------------ +// impPoisonImplicitByrefsBeforeReturn: +// Spill the stack and insert IR that poisons all implicit byrefs. +// +void Compiler::impPoisonImplicitByrefsBeforeReturn() +{ + bool spilled = false; + for (unsigned lclNum = 0; lclNum < info.compArgsCount; lclNum++) + { + if (!lvaIsImplicitByRefLocal(lclNum)) + { + continue; + } + + compPoisoningAnyImplicitByrefs = true; + + if (!spilled) + { + for (unsigned level = 0; level < verCurrentState.esStackDepth; level++) + { + impSpillStackEntry(level, BAD_VAR_NUM DEBUGARG(true) DEBUGARG("Stress writing byrefs before return")); + } + + spilled = true; + } + + LclVarDsc* dsc = lvaGetDesc(lclNum); + // Be conservative about this local to ensure we do not eliminate the poisoning. + lvaSetVarAddrExposed(lclNum, AddressExposedReason::STRESS_WRITE_IMPLICIT_BYREFS); + + assert(varTypeIsStruct(dsc)); + ClassLayout* layout = dsc->GetLayout(); + assert(layout != nullptr); + + auto poisonBlock = [this, lclNum](unsigned start, unsigned count) { + if (count <= 0) + { + return; + } + + GenTree* addr; + if (start > 0) + { + addr = gtNewLclFldAddrNode(lclNum, start, TYP_BYREF); + } + else + { + addr = gtNewLclVarAddrNode(lclNum, TYP_BYREF); + } + + GenTree* blk = new (this, GT_BLK) GenTreeBlk(GT_BLK, TYP_STRUCT, addr, typGetBlkLayout(count)); + GenTree* op = gtNewBlkOpNode(blk, gtNewIconNode(0xcd)); + impAppendTree(op, CHECK_SPILL_NONE, DebugInfo()); + }; + + unsigned startOffs = 0; + unsigned numSlots = layout->GetSlotCount(); + for (unsigned curSlot = 0; curSlot < numSlots; curSlot++) + { + unsigned offs = curSlot * TARGET_POINTER_SIZE; + var_types gcPtr = layout->GetGCPtrType(curSlot); + if (!varTypeIsGC(gcPtr)) + { + continue; + } + + poisonBlock(startOffs, offs - startOffs); + + GenTree* zeroField = gtNewAssignNode(gtNewLclFldNode(lclNum, gcPtr, offs), gtNewZeroConNode(gcPtr)); + impAppendTree(zeroField, CHECK_SPILL_NONE, DebugInfo()); + + startOffs = offs + TARGET_POINTER_SIZE; + } + + assert(startOffs <= lvaLclExactSize(lclNum)); + poisonBlock(startOffs, lvaLclExactSize(lclNum) - startOffs); + } +} +#endif + /***************************************************************************** * Mark the block as unimported. * Note that the caller is responsible for calling impImportBlockPending(), diff --git a/src/coreclr/jit/lclvars.cpp b/src/coreclr/jit/lclvars.cpp index 04411de16624c..eec8e0d1d800b 100644 --- a/src/coreclr/jit/lclvars.cpp +++ b/src/coreclr/jit/lclvars.cpp @@ -1988,6 +1988,12 @@ bool Compiler::StructPromotionHelper::CanPromoteStructVar(unsigned lclNum) return false; } + if (varDsc->IsAddressExposed()) + { + JITDUMP(" struct promotion of V%02u is disabled because it has already been marked address exposed\n", lclNum); + return false; + } + CORINFO_CLASS_HANDLE typeHnd = varDsc->GetStructHnd(); assert(typeHnd != NO_CLASS_HANDLE); diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index a969f9502a0fd..ab9e88b662dba 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -4810,7 +4810,7 @@ GenTree* Compiler::fgMorphExpandImplicitByRefArg(GenTreeLclVarCommon* lclNode) { if (argNodeType == TYP_STRUCT) { - newArgNode = gtNewObjNode(argNodeLayout, addrNode); + newArgNode = gtNewStructVal(argNodeLayout, addrNode); } else { @@ -6199,10 +6199,16 @@ GenTree* Compiler::fgMorphPotentialTailCall(GenTreeCall* call) #ifdef DEBUG if (opts.compGcChecks && (info.compRetType == TYP_REF)) { - failTailCall("COMPlus_JitGCChecks or stress might have interposed a call to CORINFO_HELP_CHECK_OBJ, " + failTailCall("DOTNET_JitGCChecks or stress might have interposed a call to CORINFO_HELP_CHECK_OBJ, " "invalidating tailcall opportunity"); return nullptr; } + + if (compPoisoningAnyImplicitByrefs) + { + failTailCall("STRESS_WRITE_IMPLICIT_BYREFS has introduced IR after tailcall opportunity, invalidating"); + return nullptr; + } #endif // We have to ensure to pass the incoming retValBuf as the From 22f1bc523517589ed142db85bacb4c57489bf48a Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 14:18:42 +0100 Subject: [PATCH 2/7] Nit --- src/coreclr/jit/importer.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 14c5f4416b2d3..0fdca70ecf9da 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -6985,7 +6985,6 @@ void Compiler::impImportBlockCode(BasicBlock* block) case CEE_RET: prefixFlags &= ~PREFIX_TAILCALL; // ret without call before it - RET: if (!impReturnInstruction(prefixFlags, opcode)) { From 6cfa28bcb5d85d7ffce1f837d7524c797f1908b8 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 14:22:01 +0100 Subject: [PATCH 3/7] Fix name --- src/coreclr/jit/morph.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index ab9e88b662dba..b8efb47971d69 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -6206,7 +6206,7 @@ GenTree* Compiler::fgMorphPotentialTailCall(GenTreeCall* call) if (compPoisoningAnyImplicitByrefs) { - failTailCall("STRESS_WRITE_IMPLICIT_BYREFS has introduced IR after tailcall opportunity, invalidating"); + failTailCall("STRESS_POISON_IMPLICIT_BYREFS has introduced IR after tailcall opportunity, invalidating"); return nullptr; } #endif From ade7a3a108bb777a5bf2926d386e92671cdccfdd Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 14:31:11 +0100 Subject: [PATCH 4/7] Fix some more renaming --- src/coreclr/jit/compiler.cpp | 6 +++--- src/coreclr/jit/compiler.h | 18 +++++++++--------- src/coreclr/jit/importer.cpp | 4 ++-- 3 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/coreclr/jit/compiler.cpp b/src/coreclr/jit/compiler.cpp index f2b5e2cb60016..065d0aaf6ae27 100644 --- a/src/coreclr/jit/compiler.cpp +++ b/src/coreclr/jit/compiler.cpp @@ -10181,8 +10181,8 @@ void Compiler::EnregisterStats::RecordLocal(const LclVarDsc* varDsc) m_dispatchRetBuf++; break; - case AddressExposedReason::STRESS_WRITE_IMPLICIT_BYREFS: - m_stressWriteImplicitByrefs++; + case AddressExposedReason::STRESS_POISON_IMPLICIT_BYREFS: + m_stressPoisonImplicitByrefs++; break; default: @@ -10280,6 +10280,6 @@ void Compiler::EnregisterStats::Dump(FILE* fout) const PRINT_STATS(m_osrExposed, m_addrExposed); PRINT_STATS(m_stressLclFld, m_addrExposed); PRINT_STATS(m_dispatchRetBuf, m_addrExposed); - PRINT_STATS(m_stressWriteImplicitByrefs, m_addrExposed); + PRINT_STATS(m_stressPoisonImplicitByrefs, m_addrExposed); } #endif // TRACK_ENREG_STATS diff --git a/src/coreclr/jit/compiler.h b/src/coreclr/jit/compiler.h index 7b546a9c98d30..cbd5c85d2f812 100644 --- a/src/coreclr/jit/compiler.h +++ b/src/coreclr/jit/compiler.h @@ -467,14 +467,14 @@ enum class DoNotEnregisterReason enum class AddressExposedReason { NONE, - PARENT_EXPOSED, // This is a promoted field but the parent is exposed. - TOO_CONSERVATIVE, // Were marked as exposed to be conservative, fix these places. - ESCAPE_ADDRESS, // The address is escaping, for example, passed as call argument. - WIDE_INDIR, // We access via indirection with wider type. - OSR_EXPOSED, // It was exposed in the original method, osr has to repeat it. - STRESS_LCL_FLD, // Stress mode replaces localVar with localFld and makes them addrExposed. - DISPATCH_RET_BUF, // Caller return buffer dispatch. - STRESS_WRITE_IMPLICIT_BYREFS, // This is an implicit byref we want to poison. + PARENT_EXPOSED, // This is a promoted field but the parent is exposed. + TOO_CONSERVATIVE, // Were marked as exposed to be conservative, fix these places. + ESCAPE_ADDRESS, // The address is escaping, for example, passed as call argument. + WIDE_INDIR, // We access via indirection with wider type. + OSR_EXPOSED, // It was exposed in the original method, osr has to repeat it. + STRESS_LCL_FLD, // Stress mode replaces localVar with localFld and makes them addrExposed. + DISPATCH_RET_BUF, // Caller return buffer dispatch. + STRESS_POISON_IMPLICIT_BYREFS, // This is an implicit byref we want to poison. }; #endif // DEBUG @@ -10141,7 +10141,7 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX unsigned m_stressLclFld; unsigned m_dispatchRetBuf; unsigned m_wideIndir; - unsigned m_stressWriteImplicitByrefs; + unsigned m_stressPoisonImplicitByrefs; public: void RecordLocal(const LclVarDsc* varDsc); diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 0fdca70ecf9da..14c51dbb7868c 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -11231,7 +11231,7 @@ void Compiler::impPoisonImplicitByrefsBeforeReturn() { for (unsigned level = 0; level < verCurrentState.esStackDepth; level++) { - impSpillStackEntry(level, BAD_VAR_NUM DEBUGARG(true) DEBUGARG("Stress writing byrefs before return")); + impSpillStackEntry(level, BAD_VAR_NUM DEBUGARG(true) DEBUGARG("Stress poisoning byrefs before return")); } spilled = true; @@ -11239,7 +11239,7 @@ void Compiler::impPoisonImplicitByrefsBeforeReturn() LclVarDsc* dsc = lvaGetDesc(lclNum); // Be conservative about this local to ensure we do not eliminate the poisoning. - lvaSetVarAddrExposed(lclNum, AddressExposedReason::STRESS_WRITE_IMPLICIT_BYREFS); + lvaSetVarAddrExposed(lclNum, AddressExposedReason::STRESS_POISON_IMPLICIT_BYREFS); assert(varTypeIsStruct(dsc)); ClassLayout* layout = dsc->GetLayout(); From a959b75d0d1a72c9e61d59e4136f8df10be5e569 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 15:22:25 +0100 Subject: [PATCH 5/7] Address feedback --- src/coreclr/jit/importer.cpp | 22 +++++++++------------- 1 file changed, 9 insertions(+), 13 deletions(-) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index 14c51dbb7868c..f03ecf1b71aaf 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -11251,19 +11251,12 @@ void Compiler::impPoisonImplicitByrefsBeforeReturn() return; } - GenTree* addr; - if (start > 0) - { - addr = gtNewLclFldAddrNode(lclNum, start, TYP_BYREF); - } - else - { - addr = gtNewLclVarAddrNode(lclNum, TYP_BYREF); - } + GenTreeLclFld* lhs = + new (this, GT_LCL_FLD) GenTreeLclFld(GT_LCL_FLD, TYP_STRUCT, lclNum, start, typGetBlkLayout(count)); + lhs->gtFlags |= GTF_GLOB_REF; - GenTree* blk = new (this, GT_BLK) GenTreeBlk(GT_BLK, TYP_STRUCT, addr, typGetBlkLayout(count)); - GenTree* op = gtNewBlkOpNode(blk, gtNewIconNode(0xcd)); - impAppendTree(op, CHECK_SPILL_NONE, DebugInfo()); + GenTree* asg = gtNewAssignNode(lhs, gtNewOperNode(GT_INIT_VAL, TYP_INT, gtNewIconNode(0xcd))); + impAppendTree(asg, CHECK_SPILL_NONE, DebugInfo()); }; unsigned startOffs = 0; @@ -11279,7 +11272,10 @@ void Compiler::impPoisonImplicitByrefsBeforeReturn() poisonBlock(startOffs, offs - startOffs); - GenTree* zeroField = gtNewAssignNode(gtNewLclFldNode(lclNum, gcPtr, offs), gtNewZeroConNode(gcPtr)); + GenTree* gcField = gtNewLclFldNode(lclNum, gcPtr, offs); + gcField->gtFlags |= GTF_GLOB_REF; + + GenTree* zeroField = gtNewAssignNode(gcField, gtNewZeroConNode(gcPtr)); impAppendTree(zeroField, CHECK_SPILL_NONE, DebugInfo()); startOffs = offs + TARGET_POINTER_SIZE; From 95f1c8452dc045d99b548ab6b4dffe77c3175e7e Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 15:38:26 +0100 Subject: [PATCH 6/7] Add a comment --- src/coreclr/jit/importer.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/coreclr/jit/importer.cpp b/src/coreclr/jit/importer.cpp index f03ecf1b71aaf..489174745547a 100644 --- a/src/coreclr/jit/importer.cpp +++ b/src/coreclr/jit/importer.cpp @@ -11215,6 +11215,13 @@ bool Compiler::impReturnInstruction(int prefixFlags, OPCODE& opcode) // impPoisonImplicitByrefsBeforeReturn: // Spill the stack and insert IR that poisons all implicit byrefs. // +// Remarks: +// The memory pointed to by implicit byrefs is owned by the callee but +// usually exists on the caller's frame (or on the heap for some reflection +// invoke scenarios). This function helps catch situations where the caller +// reads from the memory after the invocation, for example due to a bug in +// the JIT's own last-use copy elision for implicit byrefs. +// void Compiler::impPoisonImplicitByrefsBeforeReturn() { bool spilled = false; From ebb3102c1b804634c493a8a4a848613887051999 Mon Sep 17 00:00:00 2001 From: Jakob Botsch Nielsen Date: Mon, 16 Jan 2023 18:54:53 +0100 Subject: [PATCH 7/7] Do not skip explicit tailcalls when poisoning --- src/coreclr/jit/morph.cpp | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/coreclr/jit/morph.cpp b/src/coreclr/jit/morph.cpp index b8efb47971d69..c57ca7143000a 100644 --- a/src/coreclr/jit/morph.cpp +++ b/src/coreclr/jit/morph.cpp @@ -6203,12 +6203,6 @@ GenTree* Compiler::fgMorphPotentialTailCall(GenTreeCall* call) "invalidating tailcall opportunity"); return nullptr; } - - if (compPoisoningAnyImplicitByrefs) - { - failTailCall("STRESS_POISON_IMPLICIT_BYREFS has introduced IR after tailcall opportunity, invalidating"); - return nullptr; - } #endif // We have to ensure to pass the incoming retValBuf as the @@ -6256,6 +6250,18 @@ GenTree* Compiler::fgMorphPotentialTailCall(GenTreeCall* call) return nullptr; } +#ifdef DEBUG + // For explicit tailcalls the importer will avoid inserting stress + // poisoning after them. However, implicit tailcalls are marked earlier and + // we must filter those out here if we ended up adding any poisoning IR + // after them. + if (isImplicitOrStressTailCall && compPoisoningAnyImplicitByrefs) + { + failTailCall("STRESS_POISON_IMPLICIT_BYREFS has introduced IR after tailcall opportunity, invalidating"); + return nullptr; + } +#endif + bool hasStructParam = false; for (unsigned varNum = 0; varNum < lvaCount; varNum++) {