runtime: WPF Applications crashing when loading DWrite

  • .NET Core Version: 3.0.100-preview6-012012, regression from preview5, exact build that regresses unknown at this time.
  • Windows version: N/A
  • Does the bug reproduce also in WPF for .NET Framework 4.8?:No

Problem description: When a WPF application loads DirectWriteForwarder, the module constructor attempts to load DirectWrite and instantiate the appropriate factory for use with font rendering. In this build, the invocation of the creation function for the factory fails with an invalid cast HResult.

The offending function is in Factory,cpp on line 122 (pasted here as this source is not currently available):

  __declspec(noinline) void Factory::Initialize(
                                                 FactoryType factoryType
                                                 )
    {
        IUnknown* factoryTemp;
        DWRITECREATEFACTORY pfnDWriteCreateFactory = (DWRITECREATEFACTORY)GetDWriteCreateFactoryFunctionPointer();

        HRESULT hr = (*pfnDWriteCreateFactory)(
            DWriteTypeConverter::Convert(factoryType),
            (REFIID)(*(_guidForIDWriteFactory->Value)),
            &factoryTemp
            );
        
        ConvertHresultToException(hr, "Factory::Initialize");

        _pFactory = (IDWriteFactory*)factoryTemp;
    }

Actual behavior: Invalid cast exception.

Expected behavior: DirectWrite is initialized.

Minimal repro: https://github.com/rladuca/DWriteIssue

@dotnet/wpf-developers

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 1
  • Comments: 30 (23 by maintainers)

Most upvoted comments

@rladuca @vatsan-madhavan If you need a quick fix you may want to just pass the IID directly, without involving _guidForIDWriteFactory. The justification for that complication that I see in the reference source

            /// This variable stores the GUID of the IDWriteFactory interface.
            /// The reason we are not using __uuidof(IDWriteFactory) is because the complier generates a global
            /// variable and a static method to initialize it which is not annotated properly with security tags.
            /// This makes the static method fail NGENing and causes Jitting which affects perf.
            /// If the complier gets fixed then we can remove this scheme and use __uuidof(IDWriteFactory).

is likely irrelevant in .NET Core where security is no longer relevant.

This is a bug in the JIT compiler. The static constructor of Factory

static Factory::Factory()
{
    System::Guid guid = System::Guid("b859ee5a-d838-4b5b-a2e8-1adc7d93db48");
    _GUID* pGuidForIDWriteFactory = new _GUID();
    *pGuidForIDWriteFactory = Native::Util::ToGUID(guid);
    _guidForIDWriteFactory = gcnew NativePointerWrapper<_GUID>(pGuidForIDWriteFactory);  
}

contains a 16 byte cpblk

	IL_0034: ldloc.0
	IL_0035: ldloca.s 3
	IL_0037: ldc.i4.s 16
	IL_0039: unaligned. 4
	IL_003c: cpblk

that copies a _GUID to *pGuidForIDWriteFactory. The JIT gets confused by the fact that the _GUID struct contains a single int field and copies only 4 bytes instead of 16.

fgMorphCopyBlock:block assignment to morph:
               [000037] ----G+-N----              /--*  LCL_VAR   struct(AX)(P) V03 loc3         
                                                  /--*    int    V03.<alignment member> (offs=0x00) -> V08 tmp3         
               [000041] -A-XG-------              *  ASG       struct (copy)
               [000040] ---X-+-N----              \--*  BLK(16)   struct
               [000036] -----+------                 \--*  LCL_VAR   long   V00 loc0         
 (srcDoFldAsg=true) using field by field assignments.

fgMorphCopyBlock (after):
               [000096] ----G--N----              /--*  LCL_VAR   int   (AX) V08 tmp3         
               [000097] -A-XG+------              *  ASG       int   
               [000095] *--X---N----              \--*  IND       int   
               [000093] ------------                 |  /--*  CNS_INT   long   0 Fseq[<alignment member>]
               [000094] ------------                 \--*  ADD       byref 
               [000092] ------------                    \--*  LCL_VAR   long   V00 loc0         

@CarolEidt FYI I’ll take look. In theory this shouldn’t be difficult to fix but I wouldn’t surprised to find that it is - we need to distinguish between an actual cpblk (which under no circumstances can ignore holes) and a cpobj which may do field by field copy.

It would be nice to have minimal repro (ideally without WPF), so that JIT team can take closer look.

C# only (no WPF, no C++/CLI):

unsafe class Program
{
    [StructLayout(LayoutKind.Sequential, Size = 16)]
    struct GUID
    {
        private int align;
    }

    static void Main()
    {
        GUID g = default;
        Test(&g);
        Console.WriteLine(Unsafe.As<GUID, Guid>(ref g));
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    static GUID GetGUID(ref Guid guid) => Unsafe.As<Guid, GUID>(ref guid);

    [MethodImpl(MethodImplOptions.NoInlining)]
    static unsafe void Test(GUID* result)
    {
        Guid g = Guid.NewGuid();
        GUID guid = GetGUID(ref g);
        //*result = guid;
        Unsafe.CopyBlock(result, &guid, (uint)sizeof(GUID));
    }
}

This prints something like 02551bc7-0000-0000-0000-000000000000, like the IID passed to DWriteCreateFactory this has 4 valid bytes and the rest are all 0.

Still using cpblk, like in the C++/CLI version. The issue can be reproduced without cpblk but it’s debatable if copying such a struct requires copying the entire struct or only its sole field. Obviously this is not up for debate in the case of cpblk, it must copy all 16 bytes.

Since it happens that I run into this exception myself I took a quick look out of curiosity. It would seem that DWriteCreateFactory is called with an invalid IID and thus the call fails with E_NOINTERFACE.

The IID doesn’t seem to be completely invalid, it’s more like truncated - 4 bytes are fine and the rest are 0. That smalls like bad code generation to me. I’ll try to take a closer look.