Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Never use heap for return buffers #112060

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
2 changes: 2 additions & 0 deletions src/coreclr/inc/corinfo.h
Original file line number Diff line number Diff line change
Expand Up @@ -594,6 +594,8 @@ enum CorInfoHelpFunc
CORINFO_HELP_VALIDATE_INDIRECT_CALL, // CFG: Validate function pointer
CORINFO_HELP_DISPATCH_INDIRECT_CALL, // CFG: Validate and dispatch to pointer

CORINFO_HELP_ENSURE_NONHEAP, // Ensure that the target was not in the heap.

CORINFO_HELP_COUNT,
};

Expand Down
1 change: 1 addition & 0 deletions src/coreclr/inc/jithelpers.h
Original file line number Diff line number Diff line change
Expand Up @@ -341,6 +341,7 @@
JITHELPER(CORINFO_HELP_VALIDATE_INDIRECT_CALL, NULL, METHOD__NIL)
JITHELPER(CORINFO_HELP_DISPATCH_INDIRECT_CALL, NULL, METHOD__NIL)
#endif
JITHELPER(CORINFO_HELP_ENSURE_NONHEAP, JIT_EnsureNonHeapTarget,METHOD__NIL)

#undef JITHELPER
#undef DYNAMICJITHELPER
Expand Down
3 changes: 2 additions & 1 deletion src/coreclr/jit/compiler.h
Original file line number Diff line number Diff line change
Expand Up @@ -4904,7 +4904,7 @@ class Compiler
Statement** pAfterStmt = nullptr,
const DebugInfo& di = DebugInfo(),
BasicBlock* block = nullptr);
GenTree* impStoreStructPtr(GenTree* destAddr, GenTree* value, unsigned curLevel);
GenTree* impStoreStructPtr(GenTree* destAddr, GenTree* value, unsigned curLevel, GenTreeFlags indirFlags = GTF_EMPTY);

GenTree* impGetNodeAddr(GenTree* val, unsigned curLevel, GenTreeFlags* pDerefFlags);

Expand Down Expand Up @@ -10759,6 +10759,7 @@ class Compiler
STRESS_MODE(POISON_IMPLICIT_BYREFS) \
STRESS_MODE(STORE_BLOCK_UNROLLING) \
STRESS_MODE(THREE_OPT_LAYOUT) \
STRESS_MODE(NONHEAP_RET_BUFFER) \
Copy link
Member

@jkotas jkotas Feb 7, 2025

Choose a reason for hiding this comment

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

Do we run any of these JIT stress modes with naot? If yes, we may need the helper implemented for naot too.

Also, I am not sure about the durable value of this stress mode and helper. I understand that the helper was useful when implementing the change. Do you think that there is high enough probability that we will passing the heap pointers for return buffers by mistake without noticing it in other ways?

Copy link
Member Author

Choose a reason for hiding this comment

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

Do we run any of these JIT stress modes with naot? If yes, we may need the helper implemented for naot too.

Can't find any evidence that we run jitstress for NAOT even in outerloop and we definitely have no GCStress for it (#107850)

Also, I am not sure about the durable value of this stress mode and helper. I understand that the helper was useful when implementing the change. Do you think that there is high enough probability that we will passing the heap pointers for return buffers by mistake without noticing it in other ways?

I had it locally for R2R too. It seems my test apps fail badly if I remove the importer code that makes local copies instead of passing heap pointer, even without any explicit stress mode (and the helper), so I presume I can delete it

STRESS_MODE(COUNT)

enum compStressArea
Expand Down
11 changes: 11 additions & 0 deletions src/coreclr/jit/flowgraph.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2457,6 +2457,17 @@ PhaseStatus Compiler::fgAddInternal()

noway_assert(!dbgHandle || !pDbgHandle);

#if DEBUG
// JitStress: Insert a helper call to ensure that the return buffer is not on the GC heap.
if (compStressCompile(STRESS_NONHEAP_RET_BUFFER, 50) && (info.compRetBuffArg != BAD_VAR_NUM) &&
!opts.IsReadyToRun())
{
GenTree* retBuffAddr = gtNewLclvNode(info.compRetBuffArg, TYP_BYREF);
fgNewStmtAtBeg(fgFirstBB, gtNewHelperCallNode(CORINFO_HELP_ENSURE_NONHEAP, TYP_VOID, retBuffAddr));
madeChanges = true;
}
#endif

if (dbgHandle || pDbgHandle)
{
// Test the JustMyCode VM global state variable
Expand Down
63 changes: 54 additions & 9 deletions src/coreclr/jit/importer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,25 @@ GenTree* Compiler::impStoreStruct(GenTree* store,
// TODO-Bug?: verify if flags matter here
GenTreeFlags indirFlags = GTF_EMPTY;
GenTree* destAddr = impGetNodeAddr(store, CHECK_SPILL_ALL, &indirFlags);
NewCallArg newArg = NewCallArg::Primitive(destAddr).WellKnown(wellKnownArgType);

// Make sure we don't pass something other than a local address to the return buffer arg.
// It is allowed to pass current's method return buffer as it is a local too.
if (!impIsAddressInLocal(destAddr) &&
(!destAddr->OperIsScalarLocal() ||
(destAddr->AsLclVarCommon()->GetLclNum() != impInlineRoot()->info.compRetBuffArg)))
{
unsigned tmp = lvaGrabTemp(false DEBUGARG("stack copy for value returned via return buffer"));
lvaSetStruct(tmp, srcCall->gtRetClsHnd, false);

GenTree* spilledCall = gtNewStoreLclVarNode(tmp, srcCall);
GenTree* comma = gtNewOperNode(GT_COMMA, store->TypeGet(), spilledCall,
gtNewLclvNode(tmp, lvaGetDesc(tmp)->TypeGet()));
store->Data() = comma;
comma->AsOp()->gtOp1 = impStoreStruct(spilledCall, curLevel, pAfterStmt, di, block);
return impStoreStruct(store, curLevel, pAfterStmt, di, block);
}

NewCallArg newArg = NewCallArg::Primitive(destAddr).WellKnown(wellKnownArgType);

if (destAddr->OperIs(GT_LCL_ADDR))
{
Expand Down Expand Up @@ -953,6 +971,31 @@ GenTree* Compiler::impStoreStruct(GenTree* store,
// TODO-Bug?: verify if flags matter here
GenTreeFlags indirFlags = GTF_EMPTY;
GenTree* destAddr = impGetNodeAddr(store, CHECK_SPILL_ALL, &indirFlags);

// Make sure we don't pass something other than a local address to the return buffer arg.
// It is allowed to pass current's method return buffer as it is a local too.
if (!impIsAddressInLocal(destAddr) &&
(!destAddr->OperIsScalarLocal() ||
(destAddr->AsLclVarCommon()->GetLclNum() != impInlineRoot()->info.compRetBuffArg)))
{
unsigned tmp = lvaGrabTemp(false DEBUGARG("stack copy for value returned via return buffer"));
lvaSetStruct(tmp, call->gtRetClsHnd, false);
destAddr = gtNewLclVarAddrNode(tmp, TYP_BYREF);

// Insert address of temp into existing call
NewCallArg retBufArg = NewCallArg::Primitive(destAddr).WellKnown(WellKnownArg::RetBuffer);
call->gtArgs.InsertAfterThisOrFirst(this, retBufArg);

// Now the store needs to copy from the new temp instead.
store->Data() = gtNewLclvNode(tmp, lvaGetDesc(tmp)->TypeGet());
EgorBo marked this conversation as resolved.
Show resolved Hide resolved
call->gtType = TYP_VOID;
src->gtType = TYP_VOID;
store = impStoreStruct(store, CHECK_SPILL_ALL, pAfterStmt, di, block);

// Wrap with the actual call
return gtNewOperNode(GT_COMMA, TYP_VOID, src, store);
}

call->gtArgs.InsertAfterThisOrFirst(this,
NewCallArg::Primitive(destAddr).WellKnown(WellKnownArg::RetBuffer));

Expand Down Expand Up @@ -1010,21 +1053,22 @@ GenTree* Compiler::impStoreStruct(GenTree* store,
// impStoreStructPtr: Store (copy) the structure from 'src' to 'destAddr'.
//
// Arguments:
// destAddr - address of the destination of the store
// value - value to store
// curLevel - stack level for which a spill may be being done
// destAddr - address of the destination of the store
// value - value to store
// curLevel - stack level for which a spill may be being done
// indirFlags - flags to be used on the store node
//
// Return Value:
// The tree that should be appended to the statement list that represents the store.
//
// Notes:
// Temp stores may be appended to impStmtList if spilling is necessary.
//
GenTree* Compiler::impStoreStructPtr(GenTree* destAddr, GenTree* value, unsigned curLevel)
GenTree* Compiler::impStoreStructPtr(GenTree* destAddr, GenTree* value, unsigned curLevel, GenTreeFlags indirFlags)
{
var_types type = value->TypeGet();
ClassLayout* layout = (type == TYP_STRUCT) ? value->GetLayout(this) : nullptr;
GenTree* store = gtNewStoreValueNode(type, layout, destAddr, value);
GenTree* store = gtNewStoreValueNode(type, layout, destAddr, value, indirFlags);
store = impStoreStruct(store, curLevel);

return store;
Expand Down Expand Up @@ -11195,18 +11239,19 @@ bool Compiler::impReturnInstruction(int prefixFlags, OPCODE& opcode)

if (info.compRetBuffArg != BAD_VAR_NUM)
{
var_types retBuffType = lvaGetDesc(info.compRetBuffArg)->TypeGet();
// Assign value to return buff (first param)
GenTree* retBuffAddr =
gtNewLclvNode(info.compRetBuffArg, TYP_BYREF DEBUGARG(impCurStmtDI.GetLocation().GetOffset()));
gtNewLclvNode(info.compRetBuffArg, retBuffType DEBUGARG(impCurStmtDI.GetLocation().GetOffset()));

op2 = impStoreStructPtr(retBuffAddr, op2, CHECK_SPILL_ALL);
op2 = impStoreStructPtr(retBuffAddr, op2, CHECK_SPILL_ALL, GTF_IND_TGT_NOT_HEAP);
impAppendTree(op2, CHECK_SPILL_NONE, impCurStmtDI);

// There are cases where the address of the implicit RetBuf should be returned explicitly.
//
if (compMethodReturnsRetBufAddr())
{
op1 = gtNewOperNode(GT_RETURN, TYP_BYREF, gtNewLclvNode(info.compRetBuffArg, TYP_BYREF));
op1 = gtNewOperNode(GT_RETURN, retBuffType, gtNewLclvNode(info.compRetBuffArg, retBuffType));
}
else
{
Expand Down
2 changes: 1 addition & 1 deletion src/coreclr/jit/lclvars.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -485,7 +485,7 @@ void Compiler::lvaInitRetBuffArg(InitVarDscInfo* varDscInfo, bool useFixedRetBuf
info.compRetBuffArg = varDscInfo->varNum;

LclVarDsc* varDsc = varDscInfo->varDsc;
varDsc->lvType = TYP_BYREF;
varDsc->lvType = TYP_I_IMPL;
varDsc->lvIsParam = 1;
varDsc->lvIsRegArg = 0;

Expand Down
1 change: 1 addition & 0 deletions src/coreclr/jit/utils.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1770,6 +1770,7 @@ void HelperCallProperties::init()
isNoGC = true;
FALLTHROUGH;
case CORINFO_HELP_ASSIGN_REF_ENSURE_NONHEAP:
case CORINFO_HELP_ENSURE_NONHEAP:
case CORINFO_HELP_BULK_WRITEBARRIER:
mutatesHeap = true;
break;
Expand Down
2 changes: 2 additions & 0 deletions src/coreclr/tools/Common/JitInterface/CorInfoHelpFunc.cs
Original file line number Diff line number Diff line change
Expand Up @@ -284,6 +284,8 @@ which is the right helper to use to allocate an object of a given type. */
CORINFO_HELP_VALIDATE_INDIRECT_CALL, // CFG: Validate function pointer
CORINFO_HELP_DISPATCH_INDIRECT_CALL, // CFG: Validate and dispatch to pointer

CORINFO_HELP_ENSURE_NONHEAP, // Ensure that the target was not in the heap.

CORINFO_HELP_COUNT,
}
}
10 changes: 10 additions & 0 deletions src/coreclr/vm/gchelpers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1466,6 +1466,16 @@ extern "C" HCIMPL2_RAW(VOID, JIT_WriteBarrierEnsureNonHeapTarget, Object **dst,
}
HCIMPLEND_RAW

extern "C" HCIMPL1_RAW(VOID, JIT_EnsureNonHeapTarget, void *dst)
{
STATIC_CONTRACT_MODE_COOPERATIVE;
STATIC_CONTRACT_THROWS;
STATIC_CONTRACT_GC_NOTRIGGER;

_ASSERT(!GCHeapUtilities::GetGCHeap()->IsHeapPointer(dst));
}
HCIMPLEND_RAW

// This function sets the card table with the granularity of 1 byte, to avoid ghost updates
// that could occur if multiple threads were trying to set different bits in the same card.

Expand Down
1 change: 1 addition & 0 deletions src/coreclr/vm/jitinterface.h
Original file line number Diff line number Diff line change
Expand Up @@ -231,6 +231,7 @@ extern "C" FCDECL2(VOID, JIT_CheckedWriteBarrier, Object **dst, Object *ref);

extern "C" FCDECL2(VOID, JIT_WriteBarrier, Object **dst, Object *ref);
extern "C" FCDECL2(VOID, JIT_WriteBarrierEnsureNonHeapTarget, Object **dst, Object *ref);
extern "C" FCDECL1(VOID, JIT_EnsureNonHeapTarget, void *dst);

// ARM64 JIT_WriteBarrier uses special ABI and thus is not callable directly
// Copied write barriers must be called at a different location
Expand Down
31 changes: 29 additions & 2 deletions src/coreclr/vm/reflectioninvocation.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,9 @@ extern "C" void QCALLTYPE RuntimeMethodHandle_InvokeMethod(
// If an exception occurs a gc may happen but we are going to dump the stack anyway and we do
// not need to protect anything.

// Allocate a local buffer for the return buffer if necessary
PVOID pLocalRetBuf = nullptr;

{
BEGINFORBIDGC();
#ifdef _DEBUG
Expand All @@ -501,8 +504,19 @@ extern "C" void QCALLTYPE RuntimeMethodHandle_InvokeMethod(
// Take care of any return arguments
if (fHasRetBuffArg)
{
PVOID pRetBuff = gc.retVal->GetData();
*((LPVOID*) (pTransitionBlock + argit.GetRetBuffArgOffset())) = pRetBuff;
_ASSERT(hasValueTypeReturn);
PTR_MethodTable pMT = retTH.GetMethodTable();
size_t localRetBufSize = retTH.GetSize();

// Allocate a local buffer. The invoked method will write the return value to this
// buffer which will be copied to gc.retVal later.
pLocalRetBuf = _alloca(localRetBufSize);
ZeroMemory(pLocalRetBuf, localRetBufSize);
*((LPVOID*) (pTransitionBlock + argit.GetRetBuffArgOffset())) = pLocalRetBuf;
if (pMT->ContainsGCPointers())
{
pValueClasses = new (_alloca(sizeof(ValueClassInfo))) ValueClassInfo(pLocalRetBuf, pMT, pValueClasses);
}
}

// copy args
Expand Down Expand Up @@ -572,6 +586,19 @@ extern "C" void QCALLTYPE RuntimeMethodHandle_InvokeMethod(
// Call the method
CallDescrWorkerWithHandler(&callDescrData);

if (fHasRetBuffArg)
{
// Copy the return value from the return buffer to the object
if (retTH.GetMethodTable()->ContainsGCPointers())
{
memmoveGCRefs(gc.retVal->GetData(), pLocalRetBuf, retTH.GetSize());
}
else
{
memcpyNoGCRefs(gc.retVal->GetData(), pLocalRetBuf, retTH.GetSize());
}
}

// It is still illegal to do a GC here. The return type might have/contain GC pointers.
if (fConstructor)
{
Expand Down
Loading