Skip to content

Commit 6ccbb31

Browse files
committed
Fix vtable slot bounds checking for refined receiver types
When receiver types are refined during compilation (e.g., from an interface to a concrete implementor), the vtable slot offset computed against the original resolved class may fall outside the bounds of the refined class's vtable. This can lead to reading arbitrary memory values as J9Method pointers, causing crashes or incorrect devirtualization. This commit adds vtable slot validation in three critical locations: 1. TR_PersistentCHTable::findSingleAbstractImplementer - Added bounds check 2. TR_J9VMBase::getResolvedVirtualMethod - Added bounds check 3. TR_J9VirtualCallSite::findCallSiteForAbstractClass - Added name/signature verification This fixes the root cause of a class of failures observed with invalid vtable slot indices being used post-refinement, and we run into such issues in the scenario described below: - A virtual call is resolved against one class (e.g., interface implementor A) - The receiver type is refined to a different class (e.g., implementor B) - The vtable slot from class A is used to index into class B's vtable - Class B's vtable may be smaller or have different layout, causing OOB access Signed-off-by: Nazim Bhuiyan <nubhuiyan@ibm.com>
1 parent 7458bef commit 6ccbb31

3 files changed

Lines changed: 57 additions & 4 deletions

File tree

runtime/compiler/env/PersistentCHTable.cpp

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -498,6 +498,22 @@ TR_ResolvedMethod *TR_PersistentCHTable::findSingleAbstractImplementer(TR_Opaque
498498
if (TR::Compiler->cls.isInterfaceClass(comp, thisClass))
499499
return 0;
500500

501+
// vftSlot is a vtable offset computed against the class the call was resolved against, whereas
502+
// thisClass is the receiver's (possibly refined) type. For an interface accessor, any
503+
// receiver type that was refined may fall outside thisClass's vtable.
504+
// Skipping this for JITServer mode, as thisClass is a client-side class pointer
505+
// that must not be dereferenced here. The per-class read happens on the client, where it is
506+
// guarded by the equivalent check in TR_J9VMBase::getResolvedVirtualMethod.
507+
if (!comp->isOutOfProcessCompilation()) {
508+
J9Class *j9thisClass = (J9Class *)TR::Compiler->cls.convertClassOffsetToClassPtr(thisClass);
509+
UDATA vTableSlot = (UDATA)comp->fej9()->virtualCallOffsetToVTableSlot(vftSlot);
510+
UDATA vTableNumMethods = J9VTABLE_HEADER_FROM_RAM_CLASS(j9thisClass)->size;
511+
UDATA firstSlot = sizeof(J9Class) + sizeof(J9VTableHeader);
512+
UDATA lastSlot = firstSlot + (0 == vTableNumMethods ? 0 : (vTableNumMethods - 1)) * sizeof(UDATA);
513+
if ((0 == vTableNumMethods) || (vTableSlot < firstSlot) || (vTableSlot > lastSlot))
514+
return 0;
515+
}
516+
501517
TR_ResolvedMethod *implArray[2]; // collect maximum 2 implementers if you can
502518
comp->enterHeuristicRegion();
503519
int32_t implCount

runtime/compiler/env/j9method.cpp

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7402,13 +7402,36 @@ TR_OpaqueMethodBlock *TR_J9VMBase::getResolvedVirtualMethod(TR_OpaqueClassBlock
74027402
if (isInterfaceClass(classObject))
74037403
return 0;
74047404

7405-
J9Method *ramMethod = *(J9Method **)((char *)TR::Compiler->cls.convertClassOffsetToClassPtr(classObject)
7406-
+ virtualCallOffsetToVTableSlot(virtualCallOffset));
7405+
J9Class *j9class = (J9Class *)TR::Compiler->cls.convertClassOffsetToClassPtr(classObject);
7406+
UDATA vTableSlot = (UDATA)virtualCallOffsetToVTableSlot(virtualCallOffset);
7407+
7408+
TR::Compilation *comp = _compInfoPT->getCompilation();
7409+
7410+
// virtualCallOffset is a vtable slot computed against the class the call was resolved
7411+
// against, while classObject is the receiver's (possibly refined) type. When those
7412+
// differ, such as when an interface accessor whose vtable slot was taken from one
7413+
// implementor which was then refined to a narrower implementor of the same interface,
7414+
// the slot can be out of range of the classObject's vtable, holding an arbitrary
7415+
// non-J9Method value (e.g. a heap object reference).
7416+
J9VTableHeader *vftHeader = J9VTABLE_HEADER_FROM_RAM_CLASS(j9class);
7417+
UDATA vTableNumMethods = vftHeader->size;
7418+
UDATA firstSlot = sizeof(J9Class) + sizeof(J9VTableHeader);
7419+
UDATA lastSlot = firstSlot + (0 == vTableNumMethods ? 0 : (vTableNumMethods - 1)) * sizeof(UDATA);
7420+
if ((0 == vTableNumMethods) || (vTableSlot < firstSlot) || (vTableSlot > lastSlot)) {
7421+
OMR::Logger *log = comp->log();
7422+
logprintf(comp->getOption(TR_TraceOptDetails), log,
7423+
"getResolvedVirtualMethod: vtable slot %d out of bounds for class %p (vtable holds %d methods, "
7424+
"valid byte range [%d..%d], virtualCallOffset=%d); failing query\n",
7425+
(int32_t)vTableSlot, (void *)j9class, (int32_t)vTableNumMethods, (int32_t)firstSlot, (int32_t)lastSlot,
7426+
virtualCallOffset);
7427+
return 0;
7428+
}
7429+
7430+
J9Method *ramMethod = *(J9Method **)((char *)j9class + vTableSlot);
74077431

74087432
TR_ASSERT(ramMethod, "getResolvedVirtualMethod should always find a ramMethod in the vtable slot");
74097433

74107434
// ramMethod might have been inherited from a superclass
7411-
TR::Compilation *comp = _compInfoPT->getCompilation();
74127435
comp->constProvenanceGraph()->addEdge(classObject, ramMethod);
74137436

74147437
if (ramMethod && (!(_jitConfig->runtimeFlags & J9JIT_RUNTIME_RESOLVE) || ignoreRtResolve)

runtime/compiler/optimizer/J9Inliner.cpp

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -441,6 +441,21 @@ bool TR_J9VirtualCallSite::findCallSiteForAbstractClass(TR_InlinerBase *inliner)
441441
&& !comp()->getOption(TR_DisableAbstractInlining)
442442
&& (implementer
443443
= chTable->findSingleAbstractImplementer(_receiverClass, _vftSlot, _callerResolvedMethod, comp()))) {
444+
// The (_receiverClass, _vftSlot) pair can be inconsistent when the receiver type
445+
// was refined. Only devirtualize when the implementer actually matches _initialCalleeMethod.
446+
if (_initialCalleeMethod
447+
&& (implementer->nameLength() != _initialCalleeMethod->nameLength()
448+
|| implementer->signatureLength() != _initialCalleeMethod->signatureLength()
449+
|| 0 != strncmp(implementer->nameChars(), _initialCalleeMethod->nameChars(), implementer->nameLength())
450+
|| 0
451+
!= strncmp(implementer->signatureChars(), _initialCalleeMethod->signatureChars(),
452+
implementer->signatureLength()))) {
453+
heuristicTrace(inliner->tracer(),
454+
"Abstract implementer %p does not match intended callee %p (name/signature mismatch); not "
455+
"devirtualizing",
456+
implementer, _initialCalleeMethod);
457+
return false;
458+
}
444459
heuristicTrace(inliner->tracer(), "Found a single Abstract Implementer %p, signature = %s", implementer,
445460
inliner->tracer()->traceSignature(implementer));
446461
TR_VirtualGuardSelection *guard
@@ -1358,4 +1373,3 @@ bool TR_ProfileableCallSite::findProfiledCallTargets(TR_CallStack *callStack, TR
13581373

13591374
return numTargets();
13601375
}
1361-

0 commit comments

Comments
 (0)