From 6987a3593fc7581f04992b034d3dbb0469d09f1f Mon Sep 17 00:00:00 2001 From: David CARLIER Date: Fri, 17 Jul 2026 05:02:17 +0000 Subject: [PATCH] 8387718: JVMTI GetLocal/SetLocal: slot bounds check overflows for long/double slots Reviewed-by: dholmes, sspitsyn --- src/hotspot/share/prims/jvmtiImpl.cpp | 4 +- .../GetSetLocalSlotOverflow.java | 77 ++++++++++++ .../libGetSetLocalSlotOverflow.cpp | 113 ++++++++++++++++++ 3 files changed, 192 insertions(+), 2 deletions(-) create mode 100644 test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/GetSetLocalSlotOverflow.java create mode 100644 test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/libGetSetLocalSlotOverflow.cpp diff --git a/src/hotspot/share/prims/jvmtiImpl.cpp b/src/hotspot/share/prims/jvmtiImpl.cpp index c0a4ca949c9..96366cceaff 100644 --- a/src/hotspot/share/prims/jvmtiImpl.cpp +++ b/src/hotspot/share/prims/jvmtiImpl.cpp @@ -379,7 +379,7 @@ bool VM_BaseGetOrSetLocal::check_slot_type_lvt(javaVFrame* jvf) { if (!method->has_localvariable_table()) { // Just to check index boundaries. jint extra_slot = (_type == T_LONG || _type == T_DOUBLE) ? 1 : 0; - if (_index < 0 || _index + extra_slot >= method->max_locals()) { + if (_index < 0 || _index >= method->max_locals() - extra_slot) { _result = JVMTI_ERROR_INVALID_SLOT; return false; } @@ -451,7 +451,7 @@ bool VM_BaseGetOrSetLocal::check_slot_type_no_lvt(javaVFrame* jvf) { Method* method = jvf->method(); jint extra_slot = (_type == T_LONG || _type == T_DOUBLE) ? 1 : 0; - if (_index < 0 || _index + extra_slot >= method->max_locals()) { + if (_index < 0 || _index >= method->max_locals() - extra_slot) { _result = JVMTI_ERROR_INVALID_SLOT; return false; } diff --git a/test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/GetSetLocalSlotOverflow.java b/test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/GetSetLocalSlotOverflow.java new file mode 100644 index 00000000000..09073d99bfb --- /dev/null +++ b/test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/GetSetLocalSlotOverflow.java @@ -0,0 +1,77 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +/* + * @test + * @bug 8387718 + * @summary VM_GetOrSetLocal slot bounds check overflows for long/double slots, + * allowing an out-of-bounds StackValueCollection access when slot == INT_MAX. + * @requires vm.jvmti + * @compile GetSetLocalSlotOverflow.java + * @run main/othervm/native -agentlib:GetSetLocalSlotOverflow GetSetLocalSlotOverflow + */ + +/* + * Regression test / reproducer for the signed-overflow in + * VM_BaseGetOrSetLocal::check_slot_type_no_lvt (jvmtiImpl.cpp). + * + * For a T_LONG/T_DOUBLE access, the bounds check is + * if (_index < 0 || _index + extra_slot >= method->max_locals()) + * with extra_slot == 1. When the agent passes slot == INT_MAX, the + * sub-expression _index + extra_slot overflows to INT_MIN, which is < max_locals(), + * so the guard passes and the code goes on to index locals->at(INT_MAX). + * + * Expected (fixed) behavior: GetLocalLong/Double and SetLocalLong/Double with + * slot == INT_MAX return JVMTI_ERROR_INVALID_SLOT. + * + * On an unfixed VM this test does not merely fail: the out-of-bounds access + * crashes the VM (assertion failure in fastdebug, SIGSEGV / silent corruption + * in product). A clean PASS is only possible once the bounds check is fixed. + */ + +public class GetSetLocalSlotOverflow { + + // Invoked from runner(); the agent inspects the runner() frame at depth 1. + // Returns false if any accessor did not return JVMTI_ERROR_INVALID_SLOT. + static native boolean testOverflow(Thread thread); + + public static void main(String[] args) throws Exception { + if (!runner()) { + throw new RuntimeException("Test GetSetLocalSlotOverflow failed"); + } + } + + // A Java frame holding a few locals. The agent targets this frame (depth 1) + // with slot == INT_MAX. The actual local contents are irrelevant: the + // overflow happens in the slot bounds check, before any local is read. + public static boolean runner() { + long l = 0xCAFEBABEL; + double d = 3.14d; + boolean ok = testOverflow(Thread.currentThread()); + // Keep locals live across the native call. + if (l == 0 && d == 0) { + throw new AssertionError("unreachable"); + } + return ok; + } +} diff --git a/test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/libGetSetLocalSlotOverflow.cpp b/test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/libGetSetLocalSlotOverflow.cpp new file mode 100644 index 00000000000..98872ddf0de --- /dev/null +++ b/test/hotspot/jtreg/serviceability/jvmti/GetLocalVariable/libGetSetLocalSlotOverflow.cpp @@ -0,0 +1,113 @@ +/* + * Copyright (c) 2026, Oracle and/or its affiliates. All rights reserved. + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. + * + * This code is free software; you can redistribute it and/or modify it + * under the terms of the GNU General Public License version 2 only, as + * published by the Free Software Foundation. + * + * This code is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License + * version 2 for more details (a copy is included in the LICENSE file that + * accompanied this code). + * + * You should have received a copy of the GNU General Public License version + * 2 along with this work; if not, write to the Free Software Foundation, + * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. + * + * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA + * or visit www.oracle.com if you need additional information or have any + * questions. + */ + +#include +#include +#include "jvmti.h" +#include "jvmti_common.hpp" + +#ifdef __cplusplus +extern "C" { +#endif + +// The runner() frame at depth 1; INT_MAX makes (slot + extra_slot) overflow +// for the long/double accessors. +static const jint Depth = 1; +static const jint OverflowSlot = INT_MAX; // 0x7fffffff + +static jvmtiEnv *jvmti = nullptr; + +// Each access below MUST come back as JVMTI_ERROR_INVALID_SLOT. On an unfixed +// VM the overflowing bounds check is bypassed and the subsequent +// locals->at(INT_MAX) access crashes the VM before we ever see a return code. +static bool expect_invalid_slot(const char* what, jvmtiError err) { + if (err == JVMTI_ERROR_INVALID_SLOT) { + LOG(" PASS: %s returned JVMTI_ERROR_INVALID_SLOT (%d) for slot=INT_MAX\n", what, err); + return true; + } + LOG(" FAIL: %s returned %d for slot=INT_MAX, expected JVMTI_ERROR_INVALID_SLOT (%d)\n", + what, err, JVMTI_ERROR_INVALID_SLOT); + return false; +} + +JNIEXPORT jboolean JNICALL +Java_GetSetLocalSlotOverflow_testOverflow(JNIEnv *env, jclass cls, jobject thread) { + if (jvmti == nullptr) { + LOG("JVMTI client was not properly loaded!\n"); + return JNI_FALSE; + } + + jlong lval = 0; + jdouble dval = 0; + + // T_LONG / T_DOUBLE => extra_slot == 1 => INT_MAX + 1 overflows to INT_MIN. + bool ok = true; + ok &= expect_invalid_slot("GetLocalLong", jvmti->GetLocalLong(thread, Depth, OverflowSlot, &lval)); + ok &= expect_invalid_slot("GetLocalDouble", jvmti->GetLocalDouble(thread, Depth, OverflowSlot, &dval)); + ok &= expect_invalid_slot("SetLocalLong", jvmti->SetLocalLong(thread, Depth, OverflowSlot, (jlong)0)); + ok &= expect_invalid_slot("SetLocalDouble", jvmti->SetLocalDouble(thread, Depth, OverflowSlot, (jdouble)0)); + return ok ? JNI_TRUE : JNI_FALSE; +} + +static jint Agent_Initialize(JavaVM *jvm, char *options, void *reserved) { + jint res; + jvmtiError err; + static jvmtiCapabilities caps; + + res = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_9); + if (res != JNI_OK || jvmti == nullptr) { + LOG("Wrong result of a valid call to GetEnv!\n"); + return JNI_ERR; + } + caps.can_access_local_variables = 1; + + err = jvmti->AddCapabilities(&caps); + if (err != JVMTI_ERROR_NONE) { + LOG("AddCapabilities: unexpected error: %d\n", err); + return JNI_ERR; + } + err = jvmti->GetCapabilities(&caps); + if (err != JVMTI_ERROR_NONE) { + LOG("GetCapabilities: unexpected error: %d\n", err); + return JNI_ERR; + } + if (!caps.can_access_local_variables) { + LOG("Warning: Access to local variables is not implemented\n"); + return JNI_ERR; + } + return JNI_OK; +} + +JNIEXPORT jint JNICALL +Agent_OnLoad(JavaVM *jvm, char *options, void *reserved) { + return Agent_Initialize(jvm, options, reserved); +} + +JNIEXPORT jint JNICALL +Agent_OnAttach(JavaVM *jvm, char *options, void *reserved) { + return Agent_Initialize(jvm, options, reserved); +} + +#ifdef __cplusplus +} +#endif