Skip to content

Consider that retbuf arg can point to GC heap in fgCreateCallDispatcherAndGetResult() - #39815

Merged
echesakov merged 6 commits into
dotnet:masterfrom
echesakov:Runtime_39581
Jul 30, 2020
Merged

echesakov merged 6 commits into
dotnet:masterfrom
echesakov:Runtime_39581

Conversation

@echesakov

@echesakov echesakov commented Jul 23, 2020 •

Copy link
Copy Markdown
Contributor

The issue manifests themselves when

  1. methods Callee() and Caller() have the following simple structure
S Callee()
{
  // produce and return instance of S
}

S Caller()
{
    return Callee();
}
  1. S is a value type with non-GC fields and has such size that the result values of Callee and Caller must passed via return buffer;
  2. call to Callee is transformed to tail call with helpers;
  3. Caller is invoked via Reflection.

JIT will transform Caller according to https://github.com/dotnet/runtime/blob/master/docs/design/features/tailcalls-with-helpers.md to

S result;
DispatchTailCalls(&IL_STUB_CallTailCallTarget, (IntPtr)&result, _AddressOfReturnAddress());
return result;

DispatchTailCalls expects result to be a pointer on the stack.

However, during reflection call the return buffer for S value type can be placed on the GC heap

if (fHasRetBuffArg)
{
// We stack-allocate this ret buff, to preserve the invariant that ret-buffs are always in the
// caller's stack frame. We'll copy into gc.retVal later.
TypeHandle retTH = gc.pSig->GetReturnTypeHandle();
MethodTable* pMT = retTH.GetMethodTable();
if (pMT->IsStructRequiringStackAllocRetBuf())
{
SIZE_T sz = pMT->GetNumInstanceFieldBytes();
pRetBufStackCopy = _alloca(sz);
memset(pRetBufStackCopy, 0, sz);
pValueClasses = new (_alloca(sizeof(ValueClassInfo))) ValueClassInfo(pRetBufStackCopy, pMT, pValueClasses);
*((LPVOID*) (pTransitionBlock + argit.GetRetBuffArgOffset())) = pRetBufStackCopy;
}
else
{
PVOID pRetBuff = gc.retVal->GetData();
*((LPVOID*) (pTransitionBlock + argit.GetRetBuffArgOffset())) = pRetBuff;
}
}

JIT makes a wrong assumption that if Caller and Callee both have return buffer arguments it can be simple passed from the former to the latter in

// Use existing retbuf if there is one.
if (origCall->HasRetBufArg())
{
JITDUMP("Transferring retbuf\n");
GenTree* retBufArg = origCall->gtCallArgs->GetNode();
assert((info.compRetBuffArg != BAD_VAR_NUM) && retBufArg->OperIsLocal() &&
(retBufArg->AsLclVarCommon()->GetLclNum() == info.compRetBuffArg));
retValArg = retBufArg;
if (origCall->gtType != TYP_VOID)
{
retVal = gtClone(retValArg);
}
}

As a consequence, when GC is called at DispatchTailCalls the object corresponding to the return value buffer can be moved but pointers passed to DispatchTailCalls will not be update that leads to Callee using the old non-updated location when writing the return value.

Solution: JIT needs to account for IsStructRequiringStackAllocRetBuf in fgCreateCallDispatcherAndGetResult in the similar way as it is done in fgMorphCall in

if (dest->gtType == TYP_BYREF && !(dest->OperGet() == GT_ADDR && dest->AsOp()->gtOp1->OperGet() == GT_LCL_VAR))
{
// We'll exempt helper calls from this, assuming that the helper implementation
// follows the old convention, and does whatever barrier is required.
if (call->gtCallType != CT_HELPER)
{
structHnd = call->gtRetClsHnd;
if (info.compCompHnd->isStructRequiringStackAllocRetBuf(structHnd) &&
!(dest->OperGet() == GT_LCL_VAR && dest->AsLclVar()->GetLclNum() == info.compRetBuffArg))
{
// Force re-evaluating the argInfo as the return argument has changed.
call->fgArgInfo = nullptr;

Thanks @erozenfeld for help with identifying the place with wrong logic in fgCreateCallDispatcherAndGetResult!

@echesakov echesakov added the NO-REVIEW Experimental/testing PR, do NOT review it label Jul 23, 2020
@danmoseley danmoseley added the area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI label Jul 23, 2020
Comment thread src/tests/JIT/Regression/JitBlue/Runtime_39581/Runtime_39581.ilproj Outdated
@echesakov echesakov removed the NO-REVIEW Experimental/testing PR, do NOT review it label Jul 28, 2020
@echesakov

Copy link
Copy Markdown
Contributor Author

@erozenfeld PTAL
cc @dotnet/jit-contrib

@echesakov echesakov changed the title Add regression test for #39581 Consider that retbuf arg can point to GC heap in fgCreateCallDispatcherAndGetResult() Jul 28, 2020
Comment thread src/coreclr/src/jit/morph.cpp Outdated
Comment thread src/coreclr/src/jit/morph.cpp
Comment thread src/tests/JIT/Regression/JitBlue/Runtime_39581/Runtime_39581.il Outdated
Comment thread src/tests/JIT/Regression/JitBlue/Runtime_39581/Runtime_39581.il Outdated
Comment thread src/coreclr/src/jit/morph.cpp Outdated

@erozenfeld erozenfeld left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM with a few minor suggestions.

@jakobbotsch

Copy link
Copy Markdown
Member

DispatchTailCalls expects result to be a pointer on the stack.

I don't think this needs to be a hard requirement, if the pointer can be a by-ref to GC heap then the mechanism could pass it through as such.

@echesakov

Copy link
Copy Markdown
Contributor Author

I don't think this needs to be a hard requirement, if the pointer can be a by-ref to GC heap then the mechanism could pass it through as such.

@jakobbotsch retBufArg points to the stack in most of the cases. Even during Reflection call we might end up with the return buffer allocated on the stack (e.g., when the corresponding return value type has a GC pointer field) rather than GC heap.

I am not sure if making DispatchTailCalls and TailCallCallTargetStub aware that retVal can point to GC heap (i.e. passing this parameter as byref instead of IntPtr) will be more beneficial. GC will need to check that these byref pointers point to GC heap anyway and in most of the cases they will be pointing to the stack.


if (origCall->gtType != TYP_VOID)
{
retVal = gtClone(retValArg);

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@jakobbotsch Do you remember why this handling was needed here? I understand that on System-V the method that uses return buffer must return an address to the return buffer in rax at the end. But do we use the value in the caller somehow?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I don't know if JIT uses the returned pointer, but even if not it is presumably necessary for reverse pinvoke.
This is already what the JIT does for functions with ret buffers so I just did the same.

@echesakov

Copy link
Copy Markdown
Contributor Author

All GCStress legs except win-arm64 are green with the change. The ones that failed in win-arm64 are due to #38518

@jakobbotsch

Copy link
Copy Markdown
Member

@jakobbotsch retBufArg points to the stack in most of the cases. Even during Reflection call we might end up with the return buffer allocated on the stack (e.g., when the corresponding return value type has a GC pointer field) rather than GC heap.

I am not sure if making DispatchTailCalls and TailCallCallTargetStub aware that retVal can point to GC heap (i.e. passing this parameter as byref instead of IntPtr) will be more beneficial. GC will need to check that these byref pointers point to GC heap anyway and in most of the cases they will be pointing to the stack.

I don't think one more interior pointer should move the needle much and it should simplify the JIT's handling of this.
But the current approach is also fine to me.

@echesakov
echesakov merged commit 1e23a06 into dotnet:master Jul 30, 2020
@echesakov
echesakov deleted the Runtime_39581 branch July 30, 2020 17:06
Jacksondr5 pushed a commit to Jacksondr5/runtime that referenced this pull request Aug 10, 2020
…erAndGetResult() (dotnet#39815)

Caller return buffer argument can point to GC heap while DispatchTailCalls expects the return value argument to point to the stack. We should use a temporary stack allocated return buffer to hold the value during the dispatcher call and copy the value back to the caller return buffer after that.
@karelz karelz added this to the 5.0.0 milestone Aug 18, 2020
@ghost ghost locked as resolved and limited conversation to collaborators Dec 8, 2020
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

area-CodeGen-coreclr CLR JIT compiler in src/coreclr/src/jit and related components such as SuperPMI

Projects

None yet

6 participants