Repository navigation
RyuJIT losing top bits of handles #122013
Description
In #121697 I'm seeing a failure in the interfacestatics test.
The problem is that in this part of the program:
runtime/src/tests/Regressions/coreclr/15647/interfacestatics.il
Lines 52 to 58 in 8c2b75f
| 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:
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:
runtime/src/coreclr/tools/Common/JitInterface/CorInfoImpl.cs
Lines 712 to 738 in 8c2b75f
| #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?
Activity
dotnet-policy-service commentedon Nov 27, 2025
Tagging subscribers to this area: @JulieLeeMSFT, @jakobbotsch
See info in area-owners.md if you want to be subscribed.
jakobbotsch commentedon Nov 27, 2025
I recalled discussing this previously, and I dug up a comment chain about it: #77593 (comment)
MichalStrehovsky commentedon Nov 27, 2025
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.jkotas commentedon Nov 27, 2025
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_handleHighBitSetwas 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.
It would be nice to have that validation.
jakobbotsch commentedon Dec 8, 2025
Going to give this to Egor since it's
CORINFO_OBJECT_HANDLEstuff and it was the intended design when we discussed it previously.EgorBo commentedon Jun 10, 2026
@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?
SingleAccretion commentedon Jun 10, 2026
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_SIZEvalues 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 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.
EgorBo commentedon Jun 10, 2026
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?
SingleAccretion commentedon Jun 10, 2026
The managed VM adds an upper bit (the
s_handleHighBitSetalready mentioned in this thread), which migrates intogtIconVal. 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".EgorBo commentedon Jun 10, 2026
@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?
SingleAccretion commentedon Jun 10, 2026
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.MaxValueversion, no masking needed on VM side either).MichalStrehovsky commentedon Jun 10, 2026
When @davidwrighton did the crossbitness JIT change, he added the
s_handleHighBitSetpoison 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.
jkotas commentedon Jun 10, 2026
+1
EgorBo commentedon Jun 10, 2026
Can you do that in your PR then?
I think we need some major clean up in JIT anyway:
GenTreeLngConGenTreeIntCon's gtIcon to be unconditionallyint64_tinstead ofssize_t(host) and do sign extension for any possible constant, even if thatGenTreeIntConis storing some TYP_SHORT or whatever, no undefined bits ever. Alternatively, we can follow LLVM's approach with APIntopinions @dotnet/jit-contrib ?
SingleAccretion commentedon Jun 11, 2026
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.
jakobbotsch commentedon Jun 11, 2026
That sounds good to me as well. There is an open issue for 3 in #104636.
JulieLeeMSFT commentedon Jul 30, 2026
Fixed.