Skip to content

Conversation

@jkoritzinsky
Copy link
Member

For C++/CLI types with C-style array fields (int x[N];), the JIT was introducing extra copies without calling the copy constructor.

This was discovered through JitStress, which forcibly set the flag that the C++/CLI compiler sets when it sees a C-style array field.

To avoid adding a new API to the JIT interface just for usage by C++/CLI, I made a targeted fix to disable the GS cookie checking in Reverse P/Invoke stubs as we control the IL and we don't emit IL that would interact with these fields in a dangerous manner. As this fix was in CoreCLR and not propagated to the JIT, I still had to disable the tests that were failing from JitStress as setting the flag for the local in the JIT will still cause the bad behavior.

Alternatively, we could enhance the JIT interface to enable the JIT to discover when a type has a copy constructor and do one of the following:

  • Skip GS cookie checks
  • Create GT_CALL nodes to the copy constructor and destructor instead of generating a GT_STORE_LCL_VAR node.

If we were to go the second route, then I'd prefer moving all the "copy construct into the arg's stack slot" logic into the JIT and remove the code we added in the IL marshallers that supports that case.

I think a fix for this bug is worth considering backporting (as it would likely lead to hard-to-diagnose bugs and violates C++/CLI semantics). We may want a more targeted fix like this instead of a more expansive fix for backporting, I'm not sure.

Fixes #100751
Related to #100033

@jkoritzinsky
Copy link
Member Author

cc: @jkotas @AaronRobinsonMSFT for thoughts on the best way to fix this.

@AaronRobinsonMSFT
Copy link
Member

/cc @dotnet/jit-contrib

@AaronRobinsonMSFT
Copy link
Member

Create GT_CALL nodes to the copy constructor and destructor instead of generating a GT_STORE_LCL_VAR node.

Agree.

Can we do this for .NET 9?

@jkotas
Copy link
Member

jkotas commented Aug 1, 2024

we could enhance the JIT interface

What would this JIT/EE interface modification look like?

// can break C++/CLI's copy-constructor semantics by missing copies.
if (pClass->IsUnsafeValueClass()
&& !(m_pMethodBeingCompiled->IsILStub()
&& dac_cast<PTR_DynamicMethodDesc>(m_pMethodBeingCompiled)->GetILStubType() == DynamicMethodDesc::StubNativeToCLRInterop))
Copy link
Member

Choose a reason for hiding this comment

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

I think it may be better to introduce a new bit that says "do not create a shadow copies of this argument, this argument must be stored in its natural location".

Fixing it like this on the VM side looks reasonable to me.

@jkoritzinsky
Copy link
Member Author

I was thinking of using a bit on CorInfoTypeWithMod to represent "must use copy-constructor helper" and populate that depending on the IsCopyConstructed modreq. Then use a JIT helper (that either looks up in a hash table or uses an intrinsic implementation like RuntimeHelpers.IsReferenceOrContainsReferences<T>()) to do the copy with the copy constructor + destructor semantics instead of GT_STORE_LCL_VAR.

I don't like fixing it by dropping the shadow variable because it defeats the GS cookie buffer overflow protections.

@jkotas
Copy link
Member

jkotas commented Aug 1, 2024

Then use a JIT helper (that either looks up in a hash table or uses an intrinsic implementation like RuntimeHelpers.IsReferenceOrContainsReferences()) to do the copy with the copy constructor + destructor semantics instead of GT_STORE_LCL_VAR.

Does it meant that we would or would not call the user provided constructor/destructor depending on the optimization settings? It does not sound right to me.

@jkoritzinsky
Copy link
Member Author

No, we'd always call the copy constructor no matter the optimization level. Because this flag would only be set in the IL stubs (due to when the C++/CLI compiler emits it), we'd only do this particular optimization in places where the JIT emits a copy that the IL stub cannot predict (or predict easily, for the P/Invoke case).

Basically, we'd utilize the flag in at most two places:

  • In the GS checks shadow copying
  • Into the P/Invoke arg slots

Since the VM will control the IL that the JIT sees with this modreq (as it is only used on P/Invokes and Reverse P/Invokes where the C++/CLI compiler can't emit the copy constructor calls itself) and we won't emit code that would do copies depending on optimization level, the number of times we call the copy constructor or destructor will not differ based on optimization level.

@jkotas
Copy link
Member

jkotas commented Aug 1, 2024

Does it mean that there would be a new call of the copy constructor that is not there today? (ie the change would be user observable)

@jkoritzinsky
Copy link
Member Author

Yes, there would be a new call to the copy constructor that isn't there today (as we're currently missing a call). Previously we have not been concerned about introducing additional copies where necessary for validity.

(Additionally, if we were to implement the second of the two items above, we should be able to remove a call from the P/Invoke stubs).

@jkotas
Copy link
Member

jkotas commented Aug 2, 2024

Does it mean that the fix would assume that the user type constructors are correctly implemented according to the C++ rules? I am not sure whether it is safe to assume that and how much real-world code it can break.

@jkoritzinsky
Copy link
Member Author

The only assumptions the fix would make would be about the IL we emit (which I think we could guard with badCode or asserts in the JIT).

We already don't assume that copy constructors are implemented in any particular way. We just make sure to call them each time we make a copy that could eventually be passed out of the stub.

Copy link
Member

@jkotas jkotas left a comment

Choose a reason for hiding this comment

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

LGTM

@jkoritzinsky
Copy link
Member Author

/azp run runtime

@azure-pipelines
Copy link

Azure Pipelines successfully started running 1 pipeline(s).

@jkoritzinsky
Copy link
Member Author

/backport to release/8.0-staging

@github-actions
Copy link
Contributor

github-actions bot commented Aug 2, 2024

Started backporting to release/8.0-staging: https://github.com/dotnet/runtime/actions/runs/10222891618

@github-actions
Copy link
Contributor

github-actions bot commented Aug 2, 2024

@jkoritzinsky backporting to release/8.0-staging failed, the patch most likely resulted in conflicts:

$ git am --3way --ignore-whitespace --keep-non-patch changes.patch

Applying: Add failing test case from distilled down stress repro
Applying: Disable GS checks in Reverse P/Invoke stubs to avoid missing these copies.
Applying: Since the fix is in the CoreCLR VM side, we need to disable the tests in JitStress (as they won't be fixed when the JIT introduces these extra checks when not asking the VM)
error: sha1 information is lacking or useless (src/tests/Interop/IJW/CopyConstructorMarshaler/CopyConstructorMarshaler.cs).
error: could not build fake ancestor
hint: Use 'git am --show-current-patch=diff' to see the failed patch
hint: When you have resolved this problem, run "git am --continue".
hint: If you prefer to skip this patch, run "git am --skip" instead.
hint: To restore the original branch and stop patching, run "git am --abort".
hint: Disable this message with "git config advice.mergeConflict false"
Patch failed at 0003 Since the fix is in the CoreCLR VM side, we need to disable the tests in JitStress (as they won't be fixed when the JIT introduces these extra checks when not asking the VM)
Error: The process '/usr/bin/git' failed with exit code 128

Please backport manually!

@github-actions
Copy link
Contributor

github-actions bot commented Aug 2, 2024

@jkoritzinsky an error occurred while backporting to release/8.0-staging, please check the run log for details!

Error: git am failed, most likely due to a merge conflict.

@jkoritzinsky jkoritzinsky merged commit ccfd70d into dotnet:main Aug 5, 2024
@jkoritzinsky jkoritzinsky deleted the copy-ctor-unsafevaluetype branch August 5, 2024 16:54
@github-actions github-actions bot locked and limited conversation to collaborators Sep 7, 2024
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Test failure: Interop/IJW/CopyConstructorMarshaler/CopyConstructorMarshaler/CopyConstructorMarshaler.cmd

3 participants