Skip to content

Intermittent "Internal CLR error. (0x80131506)" on Windows 11 ARM #131636

Open
PowerShell/PowerShell
#28186

Description

@martincostello

Description

Originally reported here: #86775 (comment)

I have recently seen an error happening seemingly randomly on Windows 11 ARM GitHub Actions runners as part of the actions/setup-dotnet action (example):

Current runner version: '2.336.0'
Runner Image Provisioner
  Hosted Compute Agent
  Version: 20260707.563
  Commit: 02667638d2b423fbc733a8e32a88b44996a3ba6e
  Build Date: 2026-07-07T19:33:50Z
  Worker ID: {b6f9993e-1226-410e-abd5-7b6c5c143708}
  Azure Region: westus2
Operating System
  Microsoft Windows 11
  10.0.26200
  Enterprise
Runner Image
  Image: windows-11-arm64
  Version: 20260719.114.1
  Included Software: https://github.com/actions/runner-images/blob/win11-arm64/20260719.114/images/windows/Windows11-Arm64-Readme.md
  Image Release: https://github.com/actions/runner-images/releases/tag/win11-arm64%2F20260719.114

Run actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68
"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & 'C:\a\_actions\actions\setup-dotnet\a98b56852c35b8e3190ac28c8c2271da59106c68\externals\install-dotnet.ps1' -SkipNonVersionedFiles -Runtime dotnet -Channel LTS
dotnet-install: .NET Core Runtime with version '10.0.10' is already installed.
"C:\Program Files\PowerShell\7\pwsh.exe" -NoLogo -Sta -NoProfile -NonInteractive -ExecutionPolicy Unrestricted -Command & 'C:\a\_actions\actions\setup-dotnet\a98b56852c35b8e3190ac28c8c2271da59106c68\externals\install-dotnet.ps1' -SkipNonVersionedFiles -Channel 8.0
Fatal error.
Internal CLR error. (0x80131506)
Error: Failed to install dotnet, exit code: 3221225477. Fatal error.
Internal CLR error. (0x80131506)

A separate more detailed occurrence (example):

Current runner version: '2.336.0'
Runner Image Provisioner
  Hosted Compute Agent
  Version: 20260707.563
  Commit: 02667638d2b423fbc733a8e32a88b44996a3ba6e
  Build Date: 2026-07-07T19:33:50Z
  Worker ID: {86c09818-4216-413f-beed-e895ad04d8f9}
  Azure Region: westus2
Operating System
  Microsoft Windows 11
  10.0.26200
  Enterprise
Runner Image
  Image: windows-11-arm64
  Version: 20260727.122.1
  Included Software: https://github.com/actions/runner-images/blob/win11-arm64/20260727.122/images/windows/Windows11-Arm64-Readme.md
  Image Release: https://github.com/actions/runner-images/releases/tag/win11-arm64%2F20260727.122

Fatal error.
Internal CLR error. (0x80131506)
   at System.Runtime.CompilerServices.CastHelpers.ChkCastAny_NoCacheLookup(Void*, System.Object)
   at System.Runtime.CompilerServices.CastHelpers.ChkCast_Helper(Void*, System.Object)
   at Microsoft.PowerShell.TaskbarJumpList.CreateElevatedEntry(System.String)
   at Microsoft.PowerShell.TaskbarJumpList+<>c.<CreateRunAsAdministratorJumpList>b__0_0()
   at System.Threading.Thread+StartHelper.Callback(System.Object)
   at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)
Error: Failed to install dotnet, exit code: 3221225477. Fatal error.
Internal CLR error. (0x80131506)
   at System.Runtime.CompilerServices.CastHelpers.ChkCastAny_NoCacheLookup(Void*, System.Object)
   at System.Runtime.CompilerServices.CastHelpers.ChkCast_Helper(Void*, System.Object)
   at Microsoft.PowerShell.TaskbarJumpList.CreateElevatedEntry(System.String)
   at Microsoft.PowerShell.TaskbarJumpList+<>c.<CreateRunAsAdministratorJumpList>b__0_0()
   at System.Threading.Thread+StartHelper.Callback(System.Object)
   at System.Threading.ExecutionContext.RunInternal(System.Threading.ExecutionContext, System.Threading.ContextCallback, System.Object)

Reproduction Steps

Run actions/setup-dotnet on a windows-11-arm GitHub Actions runner.

Expected behavior

The action does not error.

Actual behavior

The CLR fails with an internal error Internal CLR error. (0x80131506).

Regression?

No response

Known Workarounds

Retry the job.

Configuration

.NET 10.0.10 running on Microsoft Windows 11 10.0.26200 Enterprise on ARM.

Other information

No response

Activity

  1. dotnet-policy-service commented on Jul 31, 2026

    @dotnet-policy-service
    Contributor

    Tagging subscribers to this area: @agocke
    See info in area-owners.md if you want to be subscribed.

  2. martincostello commented on Sep 29, 2026

    @martincostello
    MemberAuthor

    Still ongoing with the 10.0.401 SDK.

    This is quite disruptive to the OpenTelemetry .NET SDK CI when it causes merge queue builds to fail.

  3. martincostello commented on Oct 12, 2026

    @martincostello
    MemberAuthor

    Note

    The dump analysis in this comment was done with the assistance of Claude Code.


    We captured a full crash dump of this on a windows-11-arm runner and did some analysis. This is only one dump, but it points at the runtime rather than setup-dotnet or PowerShell script logic.

    Environment

    What crashed

    The faulting thread is the unnamed thread that PowerShell starts for TaskbarJumpList.CreateRunAsAdministratorJumpList (a new STA Thread running CreateElevatedEntry, which makes several COM calls).

    The fault is at coreclr+0x604ac, a null dereference (ldr x8,[x8,#0x38] with x8 == 0). The .NET Runtime event (ID 1023) reports the same IP, 0x00007FF9EE4304AC in a module based at 0x00007FF9EE3D0000.

    Matching the disassembly against the v10.0.12 sources, this is StubHelpers::GetCOMIPFromRCW (src/coreclr/vm/stubhelpers.cpp), the FCALL used for every CLR-to-COM call:

    RCW* pRCW = pSrc->PassiveGetSyncBlock()->GetInteropInfoNoCreate()->GetRawRCW();
    • The JIT'd IL stub passes x0 = this, x1 = MethodDesc (a CLR-to-COM method desc with slot 2) and x2 = &ppTarget.
    • PassiveGetSyncBlock() reads the 32-bit object header at obj-4, checks bit 27 (BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX), and uses the low 26 bits as an index into g_pSyncTable. If the bit isn't set, the index is 0, and g_pSyncTable[0].m_SyncBlock is NULL.
    • The ->GetInteropInfoNoCreate() that follows is the faulting dereference (SyncBlock + 0x38). There is no null check.

    The receiver's header was lost

    The receiver is a 24-byte System.__ComObject at 0x1a457ca34b0. Judging by the call order in CreateElevatedEntry, it looks like the out object instance from the second CoCreateInstance, and the call is probably pShortCutCollection.AddObject(...). I inferred that from the method slot and call order, not from symbols.

    Its header word is 0x00000000, but everything the runtime keeps for it is intact:

    Item Value
    g_pSyncTable[9] { m_SyncBlock = 0x1a453a234c8, m_Object = 0x1a457ca34b0 }, so the sync table points back at this object
    Sync block m_dwSyncIndex = 0x80000009, m_pInteropInfo (+0x38) non-null
    InteropInfo RCW pointer (+0x18) non-null, as for the other COM objects
    Object header (obj-4) 0x00000000, but it should be 0x08000009

    The adjacent COM objects created earlier by the same method have correct headers: 0x08000004, 0x08000005 and 0x08000006 (two __ComObjects and the CShellLink). So CreateObjectRef did GetSyncBlock(), created the RCW and registered it, and the header index was later zero.

    SyncBlock::SetIndex is a compare-exchange loop, so I don't think the initial write was torn. Something cleared the header afterwards. ObjHeader::ResetIndex and GCResetIndex exist, so a cleanup path or a race with another runtime thread would be consistent with this. I can't tell which from one dump without symbols. Two other runtime threads were mid-execution in coreclr when the dump was taken.

    Caveats

    • This is one dump. I can't say every occurrence has the same lost-header state.
    • The stack printed by the runtime in the original report (ChkCastAny_NoCacheLookup called from CreateElevatedEntry) differs from where this dump faults (GetCOMIPFromRCW). I suspect the printed managed stack isn't reliable here because the FCALL has no frame, but I haven't proven it.
    • The ARM64 coreclr.pdb for this build returns 404 from msdl.microsoft.com, so I worked from exports and the public source.
    • I opened the dump with an x64 cdb.exe, so I couldn't use SOS or the DAC on an ARM64 dump. I wasn't able to name the type of the object or inspect GC state.

    Capturing a dump

    WER LocalDumps and the Application log (IDs 1000/1026) showed nothing, because the runtime's fatal-error path terminates the process itself. These environment variables, set before setup-dotnet, produced the dump (and the createdump output in the step log):

    DOTNET_DbgEnableMiniDump=1
    DOTNET_DbgMiniDumpType=4
    DOTNET_DbgMiniDumpName=%RUNNER_TEMP%\crash-dumps\%e-%p.dmp
    DOTNET_CreateDumpDiagnostics=1
    

    Setting these from a cmd step avoids depending on pwsh, which is the process that crashes.

    What would help

    • Someone with the internal coreclr.pdb for 10.0.1226.41902 (ARM64), or access to an ARM64 machine to run !syncblk and !dumpheap against the dump.
    • The dump file can be downloaded from this job's artifacts.
  4. martincostello commented on Oct 12, 2026

    @martincostello
    MemberAuthor

    Further Claude Code analysis from a second dump.


    Update: second dump, same signature, now confirmed by SOS

    A second failure (CoreCLR 10.0.1226.41902, ARM64, same image) gave a second full dump with the same fault offset (coreclr+0x604ac, GetCOMIPFromRCW null deref). Running SOS on an ARM64 runner against it, verifyheap reports:

    SyncBlockMismatch  Object 225deca3a88 System.__ComObject should have a SyncBlock index of 9 but instead had no SyncBlock
    110,975 objects verified, 1 error.
    
    • It's the same object as in the first dump: the third __ComObject created by TaskbarJumpList.CreateElevatedEntry, sync block index 9 both times. Its sync table entry, sync block, interop info and RCW are intact, but its object header is zero.
    • The two earlier COM objects and the CShellLink from the same method have correct headers (0x08000003..05 in this dump).
    • The object is in gen1, so a GC ran after it was created. The crashing thread's allocation context is empty.
    • The three words before it (the preceding 0x20-byte object's two fields and this object's header) are all zero, in both dumps.
    • GCResetIndex (syncblk.h) is a plain, non-atomic read-modify-write that clears the index bits and is called from SyncBlockCache::GCWeakPtrScanElement. I couldn't see what cleared this header, but a GC that leaves a live object's header zeroed would explain the null dereference.

    It reproduced about 1 in 560 job runs (38 successes before the failure). I haven't tested whether GC frequency matters; I'm planning to try DOTNET_GCgen0MaxBudget to see if it raises the rate.

  5. martincostello commented on Oct 12, 2026

    @martincostello
    MemberAuthor

    Additional detail from a stress run with Claude Code.


    Update: this reproduces on x64 with a small gen0 budget, and it doesn't depend on the GC implementation

    I can reproduce this on a plain x64 Windows 11 machine (PowerShell 7.6.6 / CoreCLR 10.0.12) with no CI involved. The crash needs PowerShell's jump-list thread, and a smaller gen0 budget makes it much more likely.

    Minimal repro (Node because it spawns without hiding the window, so PowerShell runs TaskbarJumpList; launching from cmd/pwsh doesn't set the show-window startup flags and the jump list is skipped):

    const { spawn } = require('node:child_process');
    let n = 0, stop = false;
    function one() { return new Promise(res => {
      const c = spawn('pwsh', ['-NoLogo','-Sta','-NoProfile','-NonInteractive','-ExecutionPolicy','Unrestricted','-Command','exit 0'], { windowsHide: false, stdio: ['ignore','pipe','pipe'] });
      let out = ''; c.stdout.on('data', d => out += d); c.stderr.on('data', d => out += d);
      c.on('close', code => { n++; if (code !== 0 && !stop) { stop = true; console.log(`launch ${n} exit ${code}\n${out}`); } res(); });
    }); }
    async function w() { while (!stop && n < 3000) await one(); }
    Promise.all([w(), w(), w(), w()]);

    Run it with DOTNET_GCgen0MaxBudget=40000 (256 KiB, hex). It crashes within a few seconds to a minute with Fatal error. Internal CLR error (0x80131506) and the ChkCastAny_NoCacheLookup / CreateElevatedEntry stack from the report.

    Results (all against the same pwsh build):

    • default settings, x64 local: no crash in 2,361 launches
    • default settings, windows-11-arm runners: 2 crashes in 3,965 launches
    • DOTNET_GCgen0MaxBudget=100000 (1 MiB), ARM64: 4 crashes in 493 launches
    • DOTNET_GCgen0MaxBudget=40000 (256 KiB), ARM64: 6 crashes in 143 launches
    • DOTNET_GCgen0size=4000000 (64 MiB), ARM64: 0 crashes in 9,624 launches
    • DOTNET_GCgen0MaxBudget=40000 also crashes with DOTNET_gcConcurrent=0 and with DOTNET_GCName=clrgc.dll (the segments GC), so it is not specific to the regions GC or to background GC
    • launching with a hidden window (jump list skipped) and the same small budget: 0 crashes in 1,017 launches

    In every dump I analysed, verifyheap reports SyncBlockMismatch ... System.__ComObject should have a SyncBlock index of N but instead had no SyncBlock. The object is always a 24-byte __ComObject in gen1. Sync table entry, sync block and RCW are intact; only the object header's sync-block index is gone (header word 0x00000000). The two crash sites I see are the same corruption observed by different readers:

    • StubHelpers::GetCOMIPFromRCW reads PassiveGetSyncBlock()->GetInteropInfoNoCreate() with no null check, so it dereferences null.
    • RCWHolder::Init (reached from ChkCastAny_NoCacheLookup via ComObject::SupportsInterface) calls the creating GetSyncBlock(), which sees no index and allocates a second, empty sync block. In one dump the same __ComObject owns sync blocks 6 (with the RCW) and 8 (empty), and GetInteropInfoNoCreate() returns null.

    I haven't found what clears the header. ObjHeader::GCResetIndex() (a non-atomic read-modify-write) is the only header-clearing code I found besides the shutdown-only ResetIndex, but I can't show it runs on these objects.

  6. martincostello commented on Oct 12, 2026

    @martincostello
    MemberAuthor

    Note

    The analysis in this comment was done with the assistance of Claude Code.


    Root cause found: PowerShell's PropVariant overflows into the header of the next object (standalone repro included)

    This is not a runtime bug, and I think this is actually an issue for PowerShell/PowerShell.

    What happens

    Microsoft.PowerShell.PropVariant (source) is a managed class with explicit-layout fields at offsets 0 and 8, so a managed instance holds 16 bytes of data. It is passed to ole32 PropVariantClear as a native PROPVARIANT, which is 24 bytes on 64-bit. PropVariantClear zeroes the whole structure, so it overwrites the 8 bytes after the managed object. Those 8 bytes are the header word of the next object allocated, which holds its sync block index.

    In TaskbarJumpList.CreateElevatedEntry the next object is the __ComObject returned by the second CoCreateInstance. Once its header is zeroed the runtime can no longer find its sync block, so the next cast or COM call on it crashes (ChkCastAny_NoCacheLookup, or a null interop info in GetCOMIPFromRCW). PropVariant has a finalizer that calls Dispose, and CreateElevatedEntry never disposes it, so the clear usually happens on the finalizer thread after a GC finds the object dead. That is why the crash rate follows GC frequency (on the ARM64 runners I saw it go from about 1 in 2,000 pwsh launches to 1 in 24 with DOTNET_GCgen0MaxBudget=40000, and to none in 9,624 launches with DOTNET_GCgen0size=4000000).

    This matches everything in the crash dumps I captured on windows-11-arm runners: verifyheap reports SyncBlockMismatch ... System.__ComObject should have a SyncBlock index of N but instead had no SyncBlock on a 24-byte __ComObject in gen1, the sync table entry, sync block and RCW are intact, and the three words before the object are all zero (a cleared 16 byte object followed by the object's own header).

    Repro without PowerShell

    Requirements: Windows and the .NET 10 SDK (single-file apps need dotnet run file.cs). I ran it on .NET 10.0.12 x64; the original crashes were on ARM64 runners, and I have not run the repro there.

    1. Save the file below as PropVariantOverflow.cs.
    2. Run the commands in the table from the same directory.
    Command Expected result
    dotnet run PropVariantOverflow.cs Victim object header goes from 0x08000001 to 0x00000000, prints REPRODUCED, exit code 1.
    dotnet run PropVariantOverflow.cs -- --crash Same, then the process dies with Fatal error. Internal CLR error. (0x80131506) and at System.Runtime.CompilerServices.CastHelpers.ChkCastAny_NoCacheLookup, as in the original report (exit code 139).
    dotnet run PropVariantOverflow.cs -- --crash --finalizer Same crash, but triggered by a non-compacting GC and the finalizer instead of calling Dispose, which is how it happens in pwsh.
    Any of the above plus --fixed Uses a PropVariant with a 24 byte managed layout. The victim header is unchanged and the program exits normally.

    Notes:

    • The program reads the header word (the sync block index lives at object address - 4) with a small unsafe helper to show the corruption before the crash. The victim is the object allocated directly after the PropVariant, which is why the output prints the distance between them (32 means adjacent).
    • A plain object as the victim only works without --finalizer: any GC legitimately discards the empty sync block of a plain object, whereas a COM object keeps its sync block because it holds the RCW.
    • #:property PublishAot=false is needed because file-based apps otherwise disable built-in COM interop (Built-in COM has been disabled via a feature switch).
    • Before the PropVariant is allocated the program makes one throwaway CoCreateInstance call, because CreateElevatedEntry has already made one (for the destination list) and the first call allocates helper objects that would sit between the two objects.
    PropVariantOverflow.cs
    #:property AllowUnsafeBlocks=true
    #:property TargetFramework=net10.0
    #:property PublishAot=false
    
    // Repro for https://github.com/dotnet/runtime/issues/131636 that does not need PowerShell.
    //
    // Usage (Windows, .NET 10 SDK):
    //
    //   dotnet run PropVariantOverflow.cs                         deterministic overflow into a plain object
    //   dotnet run PropVariantOverflow.cs -- --crash              crash exactly like pwsh (exit code 139)
    //   dotnet run PropVariantOverflow.cs -- --crash --finalizer  same crash, triggered by a GC and the finalizer
    //   dotnet run PropVariantOverflow.cs -- --fixed [...]        same, with the fix applied (no corruption)
    //
    // Background: Microsoft.PowerShell.PropVariant (used by PowerShell's taskbar jump list) is a managed class with
    // 16 bytes of instance data, but it is passed to PropVariantClear() as a native PROPVARIANT, which is 24 bytes
    // on 64-bit. PropVariantClear() zeroes the whole structure, so it overwrites the 8 bytes after the managed
    // object. Those bytes are the header word of the object allocated next, which holds its sync block index. If
    // that object is a COM object (RCW), the runtime later cannot find its sync block and the process dies with
    // "Fatal error. Internal CLR error. (0x80131506)" in ChkCastAny_NoCacheLookup.
    //
    // Options:
    //   --crash       use the COM object returned by CoCreateInstance as the victim, then cast it and call it like
    //                 PowerShell does
    //   --finalizer   clear the PropVariant from its finalizer after a non-compacting GC instead of calling
    //                 Dispose(), which is how it happens in PowerShell (only meaningful with --crash: a GC
    //                 legitimately discards the empty sync block of a plain object)
    //   --fixed       use a PropVariant with a 24 byte managed layout (the fix)
    
    using System.Runtime.CompilerServices;
    using System.Runtime.InteropServices;
    
    if (!OperatingSystem.IsWindows())
    {
        Console.WriteLine("This repro needs Windows (ole32 PropVariantClear and COM).");
        return 2;
    }
    
    bool crash = args.Contains("--crash");
    bool finalizer = args.Contains("--finalizer");
    bool fixedLayout = args.Contains("--fixed");
    
    Console.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription} ({RuntimeInformation.ProcessArchitecture})");
    Console.WriteLine($"Managed PropVariant instance data: {(fixedLayout ? 24 : 16)} bytes, native PROPVARIANT: {(IntPtr.Size == 8 ? 24 : 16)} bytes");
    Console.WriteLine();
    
    // As in PowerShell's CreateElevatedEntry, a shell link is created first and later added to a collection.
    object? link = crash ? new CShellLink() : null;
    
    if (crash)
    {
        // PowerShell has already called CoCreateInstance once by this point (for the destination list), so the
        // interop stub is warm and allocates nothing between the PropVariant and the object it returns.
        CreateCollection();
    }
    
    IDisposable? propVariant = null;
    nint propVariantAddress = 0;
    object victim;
    
    if (finalizer)
    {
        victim = AllocateVictimAndDropPropVariant(crash, fixedLayout);
    }
    else
    {
        victim = AllocateVictim(crash, fixedLayout, out propVariant);
        ReadHeader(propVariant, out propVariantAddress);
    }
    
    // A COM object already has a sync block (it holds the RCW). Give a plain object one too (PulseAll needs one).
    if (!crash)
    {
        lock (victim)
        {
            Monitor.PulseAll(victim);
        }
    }
    
    uint before = ReadHeader(victim, out nint victimAddress);
    
    Console.WriteLine(propVariant is null
        ? $"Victim ({victim.GetType().Name}) at 0x{victimAddress:x}; the PropVariant is unreferenced and awaiting a GC"
        : $"PropVariant at 0x{propVariantAddress:x}, victim ({victim.GetType().Name}) at 0x{victimAddress:x} (distance {victimAddress - propVariantAddress}; 32 means adjacent)");
    Console.WriteLine($"Victim header before: 0x{before:x8}  {Describe(before)}");
    
    if (finalizer)
    {
        // Find the PropVariant dead without moving the victim, so that the finalizer thread runs ~PropVariant and
        // therefore PropVariantClear() on the object that sits directly before the victim.
        GC.Collect(0, GCCollectionMode.Forced, blocking: true, compacting: false);
        GC.WaitForPendingFinalizers();
        Console.WriteLine($"Finalizer ran {Finalized.Count} time(s)");
    }
    else
    {
        propVariant!.Dispose();
    }
    
    uint after = ReadHeader(victim, out _);
    Console.WriteLine($"Victim header after:  0x{after:x8}  {Describe(after)}");
    Console.WriteLine();
    
    if (HasSyncBlockIndex(before) && !HasSyncBlockIndex(after))
    {
        Console.WriteLine("REPRODUCED: PropVariantClear overwrote the header of the object allocated after the PropVariant.");
    
        if (crash)
        {
            Console.WriteLine("Casting the victim to IObjectCollection and calling AddObject, as PowerShell does. The runtime is expected to fail now...");
            Console.Out.Flush();
            ((IObjectCollection)victim).AddObject(link!);
            Console.WriteLine("The call succeeded (the runtime recovered).");
        }
    
        return 1;
    }
    
    Console.WriteLine("Not reproduced (the victim header was unchanged).");
    return 0;
    
    [MethodImpl(MethodImplOptions.NoInlining)]
    static object AllocateVictim(bool crash, bool fixedLayout, out IDisposable propVariant)
    {
        // Allocate the PropVariant and then, immediately after it, the victim object. In PowerShell the victim is the
        // RCW returned by the second CoCreateInstance call (for the enumerable object collection).
        propVariant = fixedLayout ? new PropVariantFixed("Run as Administrator") : new PropVariant("Run as Administrator");
        return crash ? CreateCollection() : new Victim();
    }
    
    [MethodImpl(MethodImplOptions.NoInlining)]
    static object AllocateVictimAndDropPropVariant(bool crash, bool fixedLayout)
    {
        // The PropVariant is unreferenced once this returns, as it is in PowerShell once SetValue() has returned.
        return AllocateVictim(crash, fixedLayout, out _);
    }
    
    static object CreateCollection()
    {
        var clsid = new Guid("2d3468c1-36a7-43b6-ac24-d3f02fd9607a"); // CLSID_EnumerableObjectCollection
        var iid = new Guid("00000000-0000-0000-C000-000000000046"); // IID_IUnknown
        int hr = CoCreateInstance(ref clsid, null!, 1 | 2, ref iid, out object instance);
        return hr >= 0 ? instance : throw new InvalidOperationException($"CoCreateInstance failed with 0x{hr:x8}.");
    }
    
    [DllImport("ole32.dll")]
    static extern int CoCreateInstance(
        ref Guid clsid,
        [MarshalAs(UnmanagedType.IUnknown)] object inner,
        uint context,
        ref Guid uuid,
        [MarshalAs(UnmanagedType.IUnknown)] out object returnedComObject);
    
    static bool HasSyncBlockIndex(uint header) => (header & 0x0C000000) == 0x08000000 && (header & 0x03FFFFFF) != 0;
    
    static string Describe(uint header) =>
        HasSyncBlockIndex(header) ? $"(sync block index {header & 0x03FFFFFF})" :
        header == 0 ? "(no sync block index: the header is zero)" : "(no sync block index)";
    
    // Reads the 32-bit header word that holds the sync block index (at address - 4) of an object, pinning it while doing so.
    static unsafe uint ReadHeader(object o, out nint address)
    {
        ref byte data = ref Unsafe.As<RawData>(o).Data;
        fixed (byte* p = &data)
        {
            // The first field is at the object address + 8 (after the method table pointer).
            address = (nint)(p - 8);
            return *(uint*)(p - 12);
        }
    }
    
    sealed class RawData
    {
        public byte Data;
    }
    
    static class Finalized
    {
        public static int Count;
    }
    
    sealed class Victim
    {
        public long Value = 1;
    }
    
    // Copy of Microsoft.PowerShell.PropVariant (MIT licensed, https://github.com/PowerShell/PowerShell).
    [StructLayout(LayoutKind.Explicit)]
    sealed class PropVariant : IDisposable
    {
        [FieldOffset(0)]
        private readonly ushort _valueType;
    
        [FieldOffset(8)]
        private readonly IntPtr _ptr;
    
        internal PropVariant(string value)
        {
            _valueType = (ushort)VarEnum.VT_LPWSTR;
            _ptr = Marshal.StringToCoTaskMemUni(value);
        }
    
        public void Dispose()
        {
            PropVariantClear(this);
            GC.SuppressFinalize(this);
        }
    
        ~PropVariant()
        {
            Interlocked.Increment(ref Finalized.Count);
            Dispose();
        }
    
        [DllImport("Ole32.dll", PreserveSig = false)]
        private static extern void PropVariantClear([In, Out] PropVariant pvar);
    }
    
    // The same class with room for all 24 bytes of the native PROPVARIANT, which is the fix for PowerShell.
    // Note that [StructLayout(LayoutKind.Explicit, Size = 24)] is not enough: it does not change the size of the
    // managed object, so a real field is needed.
    [StructLayout(LayoutKind.Explicit)]
    sealed class PropVariantFixed : IDisposable
    {
        [FieldOffset(0)]
        private readonly ushort _valueType;
    
        [FieldOffset(8)]
        private readonly IntPtr _ptr;
    
        // The rest of the native PROPVARIANT union, which PropVariantClear() also overwrites.
        [FieldOffset(16)]
        private readonly IntPtr _padding;
    
        internal PropVariantFixed(string value)
        {
            _valueType = (ushort)VarEnum.VT_LPWSTR;
            _ptr = Marshal.StringToCoTaskMemUni(value);
        }
    
        public void Dispose()
        {
            PropVariantClear(this);
            GC.SuppressFinalize(this);
        }
    
        ~PropVariantFixed()
        {
            Interlocked.Increment(ref Finalized.Count);
            Dispose();
        }
    
        [DllImport("Ole32.dll", PreserveSig = false)]
        private static extern void PropVariantClear([In, Out] PropVariantFixed pvar);
    }
    
    // The shell link coclass that PowerShell also creates (a COM object, so it is an RCW with a sync block).
    [ComImport]
    [Guid("00021401-0000-0000-C000-000000000046")]
    [ClassInterface(ClassInterfaceType.None)]
    class CShellLink
    {
    }
    
    // Declared only as far as AddObject (slot 2 after IUnknown), which is all PowerShell calls.
    [ComImport]
    [Guid("5632B1A4-E38A-400A-928A-D4CD63230295")]
    [InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
    interface IObjectCollection
    {
        // From IObjectArray
        void GetCount(out uint cObjects);
        void GetAt(uint iIndex, ref Guid riid, [Out, MarshalAs(UnmanagedType.Interface)] out object ppvObject);
    
        // IObjectCollection
        void AddObject([MarshalAs(UnmanagedType.Interface)] object pvObject);
    }

    Suggested fix for PowerShell

    Give the managed class the full size of the native structure, for example by adding a field at offset 16:

    [FieldOffset(16)]
    private readonly IntPtr _padding;

    [StructLayout(LayoutKind.Explicit, Size = 24)] is not enough: the CLR ignores Size for the managed size of a class, so the instance stays at 16 bytes (I checked: the distance to the next object stays at 32). Disposing the instance explicitly in CreateElevatedEntry would not help, because Dispose calls PropVariantClear as well, so the size fix is the one that matters.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    • Status
      No status

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions