Skip to content
This repository has been archived by the owner on Jan 23, 2023. It is now read-only.

Marshal blittable structs via memcpy even if nested within non-blittable struct #20194

Merged
merged 22 commits into from
Oct 3, 2018

Conversation

jkoritzinsky
Copy link
Member

@jkoritzinsky jkoritzinsky commented Sep 28, 2018

In master, when a fixed buffer is marshaled as part of a non-blittable structure, the marshaling code treats the type of a fixed buffer field fixed byte buffer[2] as a regular structure. However, the custom structure generated for a fixed buffer field has only one field of the type of the buffer. See the IL below for an example:

.class nested public sequential ansi sealed beforefieldinit '<number2>e__FixedBuffer'
	extends [System.Runtime]System.ValueType
{
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.CompilerGeneratedAttribute::.ctor() = (
		01 00 00 00
	)
	.custom instance void [System.Runtime]System.Runtime.CompilerServices.UnsafeValueTypeAttribute::.ctor() = (
		01 00 00 00
	)
	.pack 0
	.size 2

	// Fields
	.field public uint8 FixedElementField

} // end of class <number2>e__FixedBuffer

So, when this member is marshaled as a regular structure only the first member is marshaled since technically it is the only member of the structure.
As a result, the rest of the data is lost.

This PR marshals nested blittable structs via a memcpy. This enables fixed buffers like the one above to be correctly marshaled across managed<-> native. This should also allow us to marshal opaque structs nested inside non-blittable structs across since they are technically blittable.

This PR does not enable correctly marshaling non-blittable fixed buffers. We decided (in PR conversation below) to not support that feature.

Fixes #18521.


HRESULT hRESULT = pInternalImport->GetCustomAttributeByName(field, "System.Runtime.CompilerServices.FixedBufferAttribute", NULL, NULL);
return hRESULT == S_OK;
return false;
Copy link
Member

Choose a reason for hiding this comment

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

Unreachable code

@jkotas
Copy link
Member

jkotas commented Sep 28, 2018

Should this rather cover all opaque structs with explicit layout, not just "fixed buffers" ?

Consider:

[StructLayout(LayoutKind.Explicit, Size = 16)]
struct OpaqueStruct
{
}

struct MyStruct1
{
   OpaqueStruct _f;
   int _i;
}

struct MyStruct2
{
   OpaqueStruct _f;
   string _s;
}

I expect that the opaque struct will be copied over in the first case, but it won't be copied over in the second case today. Is that correct?

@jkotas
Copy link
Member

jkotas commented Sep 29, 2018

If you run following with your change on checked runtime:

using System;
using System.Runtime.InteropServices;

unsafe struct MyStruct
{
    fixed char X[1];
}

unsafe class My {
    static void Main() {
        Console.WriteLine(Marshal.SizeOf(typeof(MyStruct)));
    }
}

It will hit assert:

Assert failure(PID 22880 [0x00005960], Thread: 14996 [0x3a94]): pEEClassLayoutInfoOut->m_ManagedLargestAlignmentRequirementOfAllMembers == pEEClassLayoutInfoOut->m_LargestAlignmentRequirementOfAllMembers

I am starting to worry that this change has subtle butterfly effects that it is impossible to reason about. It may be too risky to fix in the interop marshalling rules built into the runtime.

@luqunl
Copy link

luqunl commented Oct 1, 2018

CC: @jeffschwMSFT , @AaronRobinsonMSFT

{
ULONG classSize = 0;

if (pInternalImport->GetClassTotalSize(thNestedType.GetMethodTable()->GetCl(), &classSize) >= 0)
Copy link

Choose a reason for hiding this comment

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

nit: Please Use if(SUCCEEDED(...

src/vm/nsenums.h Outdated
@@ -73,4 +73,6 @@ DEFINE_NFT(NFT_ILLEGAL, 1, true)
DEFINE_NFT(NFT_WINDOWSFOUNDATIONIREFERENCE, sizeof(IUnknown*), true) // Windows.Foundation.IReference`1 is marshaled to System.Nullable`1.
#endif // FEATURE_COMINTEROP

DEFINE_NFT(NFT_FIXEDBUFFER, 0, true) // Inline fixed fields
Copy link

Choose a reason for hiding this comment

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

This should be false instead of true, since FixedBufferAttribute isn't a valid winrt attribute.

@@ -2807,6 +2807,24 @@ bool MethodTable::ClassifyEightBytesWithNativeLayout(SystemVStructRegisterPassin

continue;
}
else if (cls == NFT_FIXEDBUFFER)
Copy link

@luqunl luqunl Oct 1, 2018

Choose a reason for hiding this comment

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

This code path is same as NFT_NESTEDVALUECLASS, how about
else if(cls == NFT_NESTEDVALUECLASS || cls == NFT_FIXEDBUFFER)

@@ -1982,6 +1989,132 @@ class FieldMarshaler_DateTimeOffset : public FieldMarshaler

#endif // FEATURE_COMINTEROP

class FieldMarshaler_FixedBuffer : public FieldMarshaler
Copy link

Choose a reason for hiding this comment

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

is it possible to update FieldMarshaller_NestedValueClass marshaller instead of adding a new one? For me, These two marshaller are very similar.

Copy link
Member Author

Choose a reason for hiding this comment

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

Possibly. I'll try to combine them.

memcpyNoGCRefs(pNative, pCLR, sizeof(size));
}

VOID ScalarUpdateCLRImpl(const VOID *pNative, LPVOID pCLR) const
Copy link

Choose a reason for hiding this comment

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

I have a concern for these ScalarUpdateXXXImpl method. such as bool type, in managed side it is 1 byte and in native side(by default, it is WinBool), it is 4 bytes. memcpy may mess up its value.

@jkoritzinsky
Copy link
Member Author

jkoritzinsky commented Oct 1, 2018

@jkotas I figured out why that's failing on the checked build. I mistakenly assumed that all fixed buffers were blittable, forgetting that char and bool buffers won't be.
@luqunl This is the same issue as your review comment. I have some ideas based off the fact that a scalar copy won't work for how to combine FixedBuffer and NestedValueClass marshalers.

CONTRACTL_END;

HRESULT hRESULT = pInternalImport->GetCustomAttributeByName(field, g_FixedBufferAttribute, NULL, NULL);
return hRESULT == S_OK;
Copy link
Member

Choose a reason for hiding this comment

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

There is some subtly here. The result of this operation is not an instance of type BOOL, but rather the C++ type bool. The difference is as follows: sizeof(BOOL) == 4 and sizeof(bool) == 1. In this case there is an implicit conversion, but that is less than ideal most of the time. In this these cases I normally do hResult == S_OK ? TRUE : FALSE so types are correct. I am okay with it in this case, but there is a difference and it should be known.

}
CONTRACTL_END;

HRESULT hRESULT = pInternalImport->GetCustomAttributeByName(field, g_FixedBufferAttribute, NULL, NULL);
Copy link
Member

Choose a reason for hiding this comment

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

Please change the name to hr or hResult.

Copy link

Choose a reason for hiding this comment

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

I would prefer hr. since most of our code use hr. only 1 instance use hResult.


FieldMarshaler_FixedBuffer(ULONG bufferSize, MethodTable* pMT)
{
size = bufferSize;
Copy link
Member

@AaronRobinsonMSFT AaronRobinsonMSFT Oct 1, 2018

Choose a reason for hiding this comment

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

Please us initializer list so size can be made const. (i.e. size{ bufferSize })

…eld to pretend that there are multiple fields consecutively in the structure.
@jkoritzinsky jkoritzinsky changed the title Marshal fixed buffers via a memcpy. Marshal fixed buffers by emulating the implied fields. Oct 1, 2018
@jkoritzinsky
Copy link
Member Author

jkoritzinsky commented Oct 1, 2018

@jkotas @luqunl @AaronRobinsonMSFT I've re-implemented my fix in a new direction. Since not all fixed buffers are blittable, a scalar copy doesn't work.

So, I decided to treat a fixed buffer member as though it has multiple consecutive fields of the same type as the first field (as it is described in the C# language feature). The buffer length is pulled from the FixedBufferAttribute. I re-use the FieldMarshaler instance to help emulate the second and onwards fields.

This fix also corrects the crash in the checked build mentioned above.

@jkoritzinsky
Copy link
Member Author

@jkotas I've identified the bug that causes the heap overflow. It's another CoreCLR bug with handling fixed buffer fields that has not been found because #18521 has been masking it.

When calculating the native size of a fixed buffer structure, the runtime doesn't account for the fact that the native size of each field in a fixed buffer might be more than the managed size. Since the runtime sees only one field, it calculates the "native size of struct based on fields" = native size of one element of the fixed buffer. The struct managed size is encoded into the metadata by the compiler as (managed size of element * number of elements). The struct size is set to max("native size of struct based on fields", "size in metadata"). So, if the native size of one element of the fixed buffer < size in the metadata or the fixed buffer has more than one element, then the runtime incorrectly calculates the native size of the fixed buffer. Since the current marshaling code in master doesn't write more than the first element, this mis-allocation isn't discovered without also discovering #18521. This fix discovers the issue since it (as of what I've currently pushed) assumes that the runtime correctly calculated the native size of the structure.

I've added some asserts on my local copy to discover this issue when marshaling. I'm currently brainstorming fixes for the masked bug.

@jkoritzinsky jkoritzinsky changed the title Marshal fixed buffers by emulating the implied fields. WIP: Marshal fixed buffers by emulating the implied fields. Oct 2, 2018
@jkotas
Copy link
Member

jkotas commented Oct 2, 2018

Even once you fix that other bug, the change is still going to be changing the behavior in potentially breaking way - imagine that somebody had a hardcoded size or called sizeof instead of Marshal.SizeOf.

@jkoritzinsky
Copy link
Member Author

@jkotas I've talked with @AaronRobinsonMSFT and we've decided that the cleanest way to do this would be to revert to the current behavior when the type of the fixed buffer is non-blittable (i.e. char and bool). The usage of fixed buffers of chars and bools will stay the same as today (avoiding the writing out of bounds in the marshaler bug and a possible breaking change as you mentioned), but the use of fixed buffers of blittable types as done in #18521 will work correctly.

@jkotas
Copy link
Member

jkotas commented Oct 2, 2018

we've decided that the cleanest way to do this would be to revert to the current behavior when the type of the fixed buffer is non-blittable

Sounds good.

@jkoritzinsky jkoritzinsky changed the title WIP: Marshal fixed buffers by emulating the implied fields. Marshal fixed buffers by emulating the implied fields. Oct 2, 2018
@@ -659,7 +661,15 @@ do \
{
if (IsStructMarshalable(thNestedType))
{
INITFIELDMARSHALER(NFT_NESTEDVALUECLASS, FieldMarshaler_NestedValueClass, (thNestedType.GetMethodTable()));
LONG numBufferElements;
if (IsFixedBuffer(pfwalk->m_MD, pInternalImport, &numBufferElements) && thNestedType.GetMethodTable()->IsBlittable())
Copy link
Member

@jkotas jkotas Oct 2, 2018

Choose a reason for hiding this comment

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

Does it still make sense to use NFT_NESTEDVALUECLASS for this now that it is limited to blittable types only? It seems to be just an expensive way to do memcpy now.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can revert it back to being a new NFT type. If I do that though, I have to replicate the non-marshaling codepaths that use NFTs to use the same code as NFT_NESTEDVALUECLASS. For reference, blittable types contained within non-blittable types still go through the NestedValueClass field marshaler.

Copy link
Member

@jkotas jkotas Oct 2, 2018

Choose a reason for hiding this comment

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

I do not think we need a new NFT type for this. I think that the fix is a lot more complex (and worse perf) than what it needs to be.

Can we fix it by just changing FieldMarshaler_NestedValueClass::NestedValueClassUpdateCLRImpl to:

    MethodTable* pMT = GetMethodTable();

    // would be better to detect this at class load time (that have a nested value
    // class with no layout) but don't have a way to know
    if (!pMT->GetLayoutInfo())
        COMPlusThrow(kArgumentException, IDS_NOLAYOUT_IN_EMBEDDED_VALUECLASS);

    if (pMT->IsBlittable())
    {
        memcpyNoGCRefs((BYTE*)(*ppProtectedCLR) + startoffset, pNative, pMT->GetNativeSize());
    }
    else
    {
        LayoutUpdateCLR( (LPVOID*)ppProtectedCLR,
                             startoffset,
                             GetMethodTable(),
                             (BYTE *)pNative);
    }
}

And similar change in FieldMarshaler_NestedValueClass::NestedValueClassUpdateNativeImpl ?

Copy link
Member Author

Choose a reason for hiding this comment

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

That should work. I'll try it out.

@luqunl
Copy link

luqunl commented Oct 2, 2018

imagine that somebody had a hardcoded size or called sizeof instead of Marshal.SizeOf

The usage of fixed buffers of chars and bools will stay the same as today (avoiding the writing out of bounds in the marshaler bug and a possible breaking change as you mentioned)

if customer is use fixed char/bool[] and sizeof, it will buffer overrun(likely) either before this change or after this change. so I am not too worried that it will break customer.

BTW, I would like to know what testcase matrix we want to add for this change..

@jkoritzinsky
Copy link
Member Author

One of the reasons we came up with for not supporting non-blittable fixed buffers at all is that the metadata needed to correctly calculate the native size of the generated fixed buffer structure available at that time, and it would require some weird special casing in the type loader to work around that. And also in the process we'd likely have to change the behavior of opaque structs that contain char or bool fields (which also probably don't currently work as expected anyway).

@luqunl
Copy link

luqunl commented Oct 2, 2018

and it would require some weird special casing in the type loader to work around that.

Just curious, why it need type loader to workaround? maybe I miss something. We know its(the struct generated by roslyn) field marshaller(such as bool/char) and its fieldmarshaller will give us its nativesize(m_cbNativeSize), is it?

@jkoritzinsky
Copy link
Member Author

The native size calculation is done while loading the class into the runtime, so that's why I mentioned the type loader. The code is technically in fieldmarshaler.cpp. So we don't actually know that the structure generated by Roslyn is for a fixed buffer field. We can determine it from heuristics, (the attributes listed above, the field name, the struct name being non-legal C#), but the actual attribute that specifies that a field is a fixed buffer is on the field itself. So, we basically have one of two options as far as I can see it.

  1. When the fixed buffer type is non-blittable, ignore the .size metadata on the struct when calculating its native size and re-calculate it to just be the calculated native size of the structure. Then, when loading the type with the fixed field, re-adjust the size of the fixed buffer type to the correct size by using the metadata.
  2. Don't do anything special when loading a structure if it is non-blittable. When loading a field that is a fixed buffer, check if the field in the generated structure is non-blittable. If it is non-blittable, recalculate the size of the buffer type and update it.

@jkoritzinsky
Copy link
Member Author

The third option would be to use a heuristic to determine that a structure was generated by the compiler to represent a fixed buffer and then take size in metadata / managed size of field type * native size of field type to get the correct native size.

I really don't like the heuristic idea though because it makes us even more dependent on C# compiler output of generated structures for fixed buffers not changing.

@jkoritzinsky jkoritzinsky changed the title Marshal fixed buffers by emulating the implied fields. Marshal blittable structs via memcpy even if nested within non-blittable struct Oct 2, 2018
@jkotas
Copy link
Member

jkotas commented Oct 3, 2018

@dotnet-bot test OSX10.12 x64 Checked Innerloop Build and Test please

@jkoritzinsky jkoritzinsky merged commit 302630e into dotnet:master Oct 3, 2018
@jkoritzinsky jkoritzinsky deleted the fixes/coreclr/18521 branch October 3, 2018 16:32
@AaronRobinsonMSFT
Copy link
Member

@jkoritzinsky This is a relatively new piece of functionality and extends a scenario that needs additional test coverage. What is the plan for testing this and where is that PR? Please link that PR ASAP.

@jkoritzinsky
Copy link
Member Author

So currently the PR dotnet/corefx#32538 has the testing for this functionality. I'll add in a test there for the opaque struct case as well.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
4 participants