Skip to content

RyuJIT losing top bits of handles #122013

Closed
Closed

Description

@MichalStrehovsky

In #121697 I'm seeing a failure in the interfacestatics test.

The problem is that in this part of the program:

ldsfld class [System.Runtime]System.Type class IFoo<object>::O
ldtoken object[]
call class [System.Runtime]System.Type class [System.Runtime]System.Type::GetTypeFromHandle(valuetype [System.Runtime]System.RuntimeTypeHandle)
ceq
brtrue IFoo_object_O_Okay
ldc.i4.1
ret

we decide that the IFoo_object_O_Okay jump is never taken and the entire Main should compile to return 1.

The reason why we didn't see this failure before #121697 is that with TrimMode=partial, compiler is forced to assume the IFoo<object>::O field could be a target of reflection and therefore we can't optimize with the otherwise constant value of the field. With TrimMode full the optimizations start kicking in, and misfire.

Here's the JitDump:

dumpy.txt

The problematic part is:

Folding operator with constant nodes into a constant:
               [000004] J---G--N---                         *  EQ        int   
               [000000] H----+-----                         +--*  CNS_INT(h) ref     'ILCompiler.DependencyAnalysis.FrozenRuntimeTypeNode'
               [000069] H----+-----                         \--*  CNS_INT(h) ref     'ILCompiler.DependencyAnalysis.FrozenRuntimeTypeNode'
Bashed to int constant:
               [000004] -----------                         *  CNS_INT   int    0

and the reason why it got folded this way is that RyuJIT lost the top bits of the CNS_INT handle in one side of the comparison.

As to why we have top bits set in the handle:

#if DEBUG
private static readonly IntPtr s_handleHighBitSet = (sizeof(IntPtr) == 4) ? new IntPtr(0x40000000) : new IntPtr(0x4000000000000000);
#endif
private IntPtr ObjectToHandle(object obj)
{
// MethodILScopes need to go through ObjectToHandle(MethodILScope methodIL).
Debug.Assert(obj is not MethodILScope);
return ObjectToHandleUnchecked(obj);
}
private IntPtr ObjectToHandleUnchecked(object obj)
{
// SuperPMI relies on the handle returned from this function being stable for the lifetime of the crossgen2 process
// If handle deletion is implemented, please update SuperPMI
IntPtr handle;
if (!_objectToHandle.TryGetValue(obj, out handle))
{
handle = (IntPtr)(handleMultiplier * _handleToObject.Count + handleBase);
#if DEBUG
handle = new IntPtr((long)s_handleHighBitSet | (long)handle);
#endif
_handleToObject.Add(obj);
_objectToHandle.Add(obj, handle);
}
return handle;
}

Notice we set top bits, but do not enforce the top bits are present when the handle is used, so handles with stripped top bits are as good as any.

Is the bug that this should look at the pointer size of the target, not at the pointer size of the host?

private static readonly IntPtr s_handleHighBitSet = (sizeof(IntPtr) == 4) ? new IntPtr(0x40000000) : new IntPtr(0x4000000000000000);

Or is RyuJIT expected to preserve the top bits like this and the bug is in RyuJIT?

Cc @davidwrighton

Activity

  1. dotnet-policy-service commented on Nov 27, 2025

    @dotnet-policy-service
    Contributor

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

  2. jakobbotsch commented on Nov 27, 2025

    @jakobbotsch
    Member

    I recalled discussing this previously, and I dug up a comment chain about it: #77593 (comment)

  3. MichalStrehovsky commented on Nov 27, 2025

    @MichalStrehovsky
    MemberAuthor

    Can the fix just be deleting s_handleHighBitSet? It doesn't seem to serve any purpose, except breaking things. If we were validating the bit is present it would be a different thing but we don't so the only thing it can do is cause these subtle bugs.

  4. jkotas commented on Nov 27, 2025

    @jkotas
    Member

    David added it in https://github.com/dotnet/runtime/pull/41126/files#diff-4039de0dc0aa123aac5c4aadc4155f56e9dda87549b351875447c5ba73444213R401 . This change introduced support for cross-bitness cross-compilation among other things. I assume that s_handleHighBitSet was added to make it easier to catch cross-bitness bugs.

    Deleting will fix this bug, but it may allow different bugs to sneak in unnoticed.

    validating the bit is present

    It would be nice to have that validation.

  5. removed
    untriagedNew issue has not been triaged by the area owner
    on Dec 3, 2025
  6. added this to the 11.0.0 milestone on Dec 3, 2025
  7. jakobbotsch commented on Dec 8, 2025

    @jakobbotsch
    Member

    Going to give this to Egor since it's CORINFO_OBJECT_HANDLE stuff and it was the intended design when we discussed it previously.

  8. EgorBo commented on Jun 10, 2026

    @EgorBo
    Member

    @MichalStrehovsky Just for my understanding. So this is only 64bit host -> 32bit target specific, right? NAOT may (or may not) give JIT a handle with upper bit set. Does JIT have to preserve those bits when it calls some other NAOT API with that handle?

    Today, for nint-sized constants which are backed by SSIZE_T (host) for crossgen-x64 compiling for a 32bit target depends on the normization of the upper bits (either zero or sign extension), I am not sure just masking a few places like #129034 does solves the problem. opinions @dotnet/jit-contrib @SingleAccretion ?

    I kind of suspect without some major rewrite the only fix is to normalize the handles on the VM side?

  9. pinned and unpinned a comment on Jun 10, 2026
  10. SingleAccretion commented on Jun 10, 2026

    @SingleAccretion
    Contributor

    opinions

    We have two kinds of handles in the Jit-EE interface, and these handles change nature depending on the reloc setting.

    With relocs off, these handles are obviously TARGET_POINTER_SIZE values that represent the symbols they reference directly. It would be odd for them to be host-sized (this case is the altjit CoreCLR VM 'abuse').

    With relocs on, these handles are obviously host-sized values. Imagine a 'native' managed VM (like old crossgen) where the handles are some CompilerEntity*.

    So from the interface point of view the fact we overload the meaning of handles is what makes this confusion/conflict inevitable. But we can keep going with the current scheme since our managed VM doesn't need true host-sized handles.

    I kind of suspect without some major rewrite the only fix is to normalize the handles on the VM side?

    I think that'd be the minimal kind of fix. Make the contract be that with relocs on, the handles always fit into a 32 bit value (and the upper bits are not significant). In the Jit internally that'd allow us to get rid of the special case of 'denormalized' CNS_INT nodes.

  11. EgorBo commented on Jun 10, 2026

    @EgorBo
    Member

    In the Jit internally that'd allow us to get rid of the special case of 'denormalized' CNS_INT nodes.

    What special case are you referring to? Because I'm pretty sure if we introduce some kind of a random poisoning for GT_CNS_INT's upper bits (not just handles, but for everything) it will crash badly?

  12. SingleAccretion commented on Jun 10, 2026

    @SingleAccretion
    Contributor

    What special case are you referring to? Because I'm pretty sure if we introduce some kind of a random poisoning for GT_CNS_INT's upper bits (not just handles, but for everything) it will crash badly?

    The managed VM adds an upper bit (the s_handleHighBitSet already mentioned in this thread), which migrates into gtIconVal. but is obviously not right w.r.t. the sign extension identity. It happens to mostly work because we already need to treat handle values "opaquely".

  13. EgorBo commented on Jun 10, 2026

    @EgorBo
    Member

    What special case are you referring to? Because I'm pretty sure if we introduce some kind of a random poisoning for GT_CNS_INT's upper bits (not just handles, but for everything) it will crash badly?

    The managed VM adds an upper bit (the s_handleHighBitSet already mentioned in this thread), which migrates into gtIconVal. but is obviously not right w.r.t. the sign extension identity. It happens to mostly work because we already need to treat handle values "opaquely".

    @SingleAccretion I mean you're basically suggesting "if this is a handle and relocs are on - upper bits can be garbage and we should ignore it, so we need surgical fixes in various VNs, morphs, etc". Otherwise, upper 32-bits do matter, because it might be either a real host VM's pointer or any integer constant where we read it without normalizing to target type?

  14. SingleAccretion commented on Jun 10, 2026

    @SingleAccretion
    Contributor

    we should ignore it

    I am not suggesting that the VM should produce 'any' host-sized values, only values that fit into a 32 bit integer, whether sign-extended or zero-extended, sign extension would be more convenient for the Jit as currently constructed, but it shouldn't matter either way. Indeed we could say these should be values < int.MaxValue, which side-steps the extension question entirely (and would work just as well practically). So on the Jit side we wouldn't need to do any normalizing or masking (and in the < int.MaxValue version, no masking needed on VM side either).

  15. MichalStrehovsky commented on Jun 10, 2026

    @MichalStrehovsky
    MemberAuthor

    @MichalStrehovsky Just for my understanding. So this is only 64bit host -> 32bit target specific, right? NAOT may (or may not) give JIT a handle with upper bit set. Does JIT have to preserve those bits when it calls some other NAOT API with that handle?

    When @davidwrighton did the crossbitness JIT change, he added the s_handleHighBitSet poison to poison the top bits of the host size handle. He did not add any verification that the JIT didn't lose it somewhere and JitInterface strips it before using it so a handle without the bit is as good as the original.

    We now have bugs where JIT sometimes loses it and sometimes doesn't lose it and two handles that should be equal are not equal. These bugs are not nice.

    I proposed just deleting the poison (#122013 (comment)) because I do not see the point of poisoning and never checking. I would absolutely be okay with just deleting the poison and fixing the bug that way. I just want the bug fixed.

  16. jkotas commented on Jun 10, 2026

    @jkotas
    Member

    I would absolutely be okay with just deleting the poison and fixing the bug that way

    +1

  17. EgorBo commented on Jun 10, 2026

    @EgorBo
    Member

    I proposed just deleting the poison (#122013 (comment)) because I do not see the point of poisoning and never checking. I would absolutely be okay with just deleting the poison and fixing the bug that way. I just want the bug fixed.

    Can you do that in your PR then?

    I think we need some major clean up in JIT anyway:

    1. Remove GenTreeLngCon
    2. Change GenTreeIntCon 's gtIcon to be unconditionally int64_t instead of ssize_t (host) and do sign extension for any possible constant, even if that GenTreeIntCon is storing some TYP_SHORT or whatever, no undefined bits ever. Alternatively, we can follow LLVM's approach with APInt
    3. Move all kinds of handles to a separate node type - this also a step towards fixing occasional "JIT performed an ADD over a relocable constant and produced a non-relocable".

    opinions @dotnet/jit-contrib ?

  18. SingleAccretion commented on Jun 11, 2026

    @SingleAccretion
    Contributor

    opinions

    SGTM. Point 2 may need a bit of finessing with TP. I don't think we need to go as far as APInt, since we don't need to support arbitrary-sized constants.

  19. jakobbotsch commented on Jun 11, 2026

    @jakobbotsch
    Member

    That sounds good to me as well. There is an open issue for 3 in #104636.

  20. JulieLeeMSFT commented on Jul 30, 2026

    @JulieLeeMSFT
    Member

    Fixed.

  21. locked and limited conversation to collaborators on Aug 30, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

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

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions