JDK-8339725 : Concurrent GC crashed due to GetMethodDeclaringClass
  • Type: Bug
  • Component: hotspot
  • Sub-Component: jvmti
  • Affected Version: 21,23,24
  • Priority: P3
  • Status: Resolved
  • Resolution: Fixed
  • Submitted: 2024-09-09
  • Updated: 2024-09-30
  • Resolved: 2024-09-14
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 21 JDK 23 JDK 24
21.0.6Fixed 23.0.2Fixed 24 b16Fixed
Related Reports
Relates :  
Description
Here is a reproduced test case from async profiler:
https://github.com/async-profiler/async-profiler/pull/981

Both G1 and ZGC crash. The stack traces are G1/ZGC concurrent marking or G1 full gc marking.

Main.java:
===========================================================
import java.util.Base64;

public class Main extends Thread {
    public static void main(String[] args) throws Exception {
        long last = System.nanoTime();
        for (int i = 0;; i++) {
            CustomClassLoader loader = new CustomClassLoader();
            Class<?> k = loader.findClass("TemplateFFFFFFFF");
            Object o = k.getDeclaredConstructor().newInstance();

            // call gc every ~1 second.
            if ((System.nanoTime() - last) >= 1e9) {
                System.gc();
                last = System.nanoTime();
            }
        }
    }
}

class CustomClassLoader extends ClassLoader {
    @Override
    public Class findClass(String name) throws ClassNotFoundException {
        /*
         * Bytecode for:
         * public class TemplateFFFFFFFF {
         *   public void doTemplateFFFFFFFF() {
         *     return;
         *   }
         * }
         */
        byte[] b = Base64.getDecoder()
                .decode("yv66vgAAADQADgoAAwALBwAMBwANAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJU" +
                        "YWJsZQEAEmRvVGVtcGxhdGVGRkZGRkZGRgEAClNvdXJjZUZpbGUBABVUZW1wbGF0ZUZGRkZGRkZG" +
                        "LmphdmEMAAQABQEAEFRlbXBsYXRlRkZGRkZGRkYBABBqYXZhL2xhbmcvT2JqZWN0ACEAAgADAAAA" +
                        "AAACAAEABAAFAAEABgAAAB0AAQABAAAABSq3AAGxAAAAAQAHAAAABgABAAAAAQABAAgABQABAAYA" +
                        "AAAZAAAAAQAAAAGxAAAAAQAHAAAABgABAAAAAwABAAkAAAACAAo=");
        return defineClass(name, b, 0, b.length);
    }
}
=========================================================

repro.cpp:
=========================================================
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <jvmti.h>
#include <jni.h>
#include <pthread.h>

static jvmtiEnv *jvmti;
static JavaVM *_jvm;
static JNIEnv *_rb_env;

#ifndef WITH_GetClassSignature
#define WITH_GetClassSignature 1
#endif

#ifndef WITH_DeleteLocalRef
#define WITH_DeleteLocalRef 0
#endif

#define BUFFER_SIZE 100000
static size_t ring_buffer[BUFFER_SIZE] = {0};
static volatile int ring_buffer_idx = 0;
static int reader_created = 0;

void *get_method_details(void *arg)
{
    jmethodID method = (jmethodID)arg;

    jclass method_class;
    char *class_name = NULL;

    jvmtiError err = JVMTI_ERROR_NONE;

    // For JVM 17, 21, 22 calling GetMethodDeclaringClass is enough.
    if ((err = jvmti->GetMethodDeclaringClass(method, &method_class)) == 0)
    {
        if (WITH_DeleteLocalRef)
        {
            _rb_env->DeleteLocalRef(method_class);
        }

        if (WITH_GetClassSignature)
        {
            // JVM 8 needs this to crash
            jvmti->GetClassSignature(method_class, &class_name, NULL);
            jvmti->Deallocate((unsigned char *)class_name);
        }
    }
}

void *read_ringbuffer(void *arg)
{
    JNIEnv *env;
    _jvm->AttachCurrentThread((void **)&env, NULL);
    _rb_env = env;

    for (;;)
    {
        size_t id = ring_buffer[rand() % BUFFER_SIZE];
        if (id > 0)
        {
            get_method_details((void *)id);
        }
    }
}

static void JNICALL ClassPrepareCallback(jvmtiEnv *jvmti_env,
                                         JNIEnv *jni_env,
                                         jthread thread,
                                         jclass klass)
{
    if (reader_created == 0)
    {
        pthread_t tid;
        pthread_create(&tid, NULL, read_ringbuffer, NULL);

        reader_created = 1;
    }

    // Get the list of methods
    jint method_count;
    jmethodID *methods;
    if (jvmti_env->GetClassMethods(klass, &method_count, &methods) == JVMTI_ERROR_NONE)
    {
        for (int i = 0; i < method_count; i++)
        {
            ring_buffer[ring_buffer_idx++] = (size_t)methods[i];
            ring_buffer_idx = ring_buffer_idx % BUFFER_SIZE;
        }
        jvmti_env->Deallocate((unsigned char *)methods);
    }
}

JNIEXPORT jint JNICALL Agent_OnLoad(JavaVM *jvm, char *options, void *reserved)
{
    jvmtiEventCallbacks callbacks;
    jvmtiError error;

    _jvm = jvm;

    if (jvm->GetEnv((void **)&jvmti, JVMTI_VERSION_1_0) != JNI_OK)
    {
        fprintf(stderr, "Unable to access JVMTI!\n");
        return JNI_ERR;
    }

    // Set up the event callbacks
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.ClassPrepare = &ClassPrepareCallback;

    // Register the callbacks
    error = jvmti->SetEventCallbacks(&callbacks, sizeof(callbacks));
    if (error != JVMTI_ERROR_NONE)
    {
        fprintf(stderr, "Error setting event callbacks: %d\n", error);
        return JNI_ERR;
    }

    // Enable the ClassPrepare event
    error = jvmti->SetEventNotificationMode(JVMTI_ENABLE, JVMTI_EVENT_CLASS_PREPARE, NULL);
    if (error != JVMTI_ERROR_NONE)
    {
        fprintf(stderr, "Error enabling ClassPrepare event: %d\n", error);
        return JNI_ERR;
    }

    return JNI_OK;
}
=============================================================

Steps to reproduce:

javac Main.java
gcc -shared -I"$JAVA_HOME/include" -I"$JAVA_HOME/include/linux" -fPIC repro.cpp -orepro.so

# Low Xmx to pressure GC into unloading classes sooner.
java -agentpath:"$(pwd)/repro.so" -Xmx100m Main
Comments
[jdk21u-fix-request] Approval Request from Liang Mao Backport of fixing concurrent GC crashed due to GetMethodDeclaringClass. Not clean because of make file. The fix is quite safe with only 1 line to keep class alive so the risk is very low.
25-09-2024

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk21u-dev/pull/1004 Date: 2024-09-24 03:22:59 +0000
24-09-2024

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk23u/pull/117 Date: 2024-09-23 03:40:37 +0000
23-09-2024

[jdk23u-fix-request] Approval Request from Liang Mao Backport of concurrent GC crash due to GetMethodDeclaringClass. The backport is not clean because of make file. The risk is very low that the fix is only 1 line in JVM to keep the class alive.
23-09-2024

Changeset: c91fa278 Branch: master Author: Liang Mao <lmao@openjdk.org> Date: 2024-09-14 05:36:47 +0000 URL: https://git.openjdk.org/jdk/commit/c91fa278fe17ab204beef0fcef1ada6dd0bc37bb
14-09-2024

If I understand the test correctly you are storing a jMethodID in a ringbuffer that will be read by another thread and then used to pass GetMethodDeclaringClass, but the class for which the jMethodID was obtained could have been unloaded by then.
09-09-2024

Does this bug belong in hotspot/jvmti or hotspot/runtime?
09-09-2024

[~dholmes], the test case is attached and I created the PR. Could you pleae take a review? Thanks.
09-09-2024

A pull request was submitted for review. Branch: master URL: https://git.openjdk.org/jdk/pull/20907 Date: 2024-09-09 05:33:35 +0000
09-09-2024

Hi [~dholmes], I'm still working on it, please wait for a while.
09-09-2024

Can you elaborate please as I can't tell what I'm supposed to be looking at in that PR that provides a test case here. ??
09-09-2024