Skip to content

[Java] CUDA/TensorRT applyToNative() releases the value string's UTF chars with the key string's jstringΒ #31706

Closed

Description

@K-ANOY

Describe the issue

Both provider options bindings acquire the key and value UTF-8 buffers from the correct arrays, but the release loop fetches value from jKeyArr instead of jValueArr, so values[i] is released against the wrong Java string object.

Files:

  • java/src/main/native/ai_onnxruntime_providers_OrtCUDAProviderOptions.c
  • java/src/main/native/ai_onnxruntime_providers_OrtTensorRTProviderOptions.c

Function: applyToNative (both files)

The acquire loop is correct β€” key comes from jKeyArr, value comes from jValueArr (CUDA, lines 49-54):

// Copy out strings into UTF-8.
for (jsize i = 0; i < keyLength; i++) {
  jobject key = (*jniEnv)->GetObjectArrayElement(jniEnv, jKeyArr, i);
  keys[i] = (*jniEnv)->GetStringUTFChars(jniEnv, key, NULL);
  jobject value = (*jniEnv)->GetObjectArrayElement(jniEnv, jValueArr, i);
  values[i] = (*jniEnv)->GetStringUTFChars(jniEnv, value, NULL);
}

The release loop, nine lines below, reads both operands from jKeyArr (CUDA, lines 58-63):

// Release allocated strings.
for (jsize i = 0; i < keyLength; i++) {
  jobject key = (*jniEnv)->GetObjectArrayElement(jniEnv, jKeyArr, i);
  (*jniEnv)->ReleaseStringUTFChars(jniEnv,key,keys[i]);
  jobject value = (*jniEnv)->GetObjectArrayElement(jniEnv, jKeyArr, i);   // <-- should be jValueArr
  (*jniEnv)->ReleaseStringUTFChars(jniEnv,value,values[i]);
}

So values[i], whose chars were obtained from the value string, is released while passing the key string as the owning object. The two loops sit in the same function and disagree with each other, which is why this looks like a copy-paste slip in the release loop rather than an intentional choice.

The TensorRT binding is character-for-character the same: acquire at lines 48-53 uses jValueArr, release at lines 57-62 uses jKeyArr twice.

The JNI specification requires ReleaseStringUTFChars() to be called with the same jstring that GetStringUTFChars() was called on; the pair identifies which string's buffer is being released. On HotSpot, GetStringUTFChars() always returns a freshly allocated copy and the matching release just frees that pointer, ignoring the jstring, so on that JVM the mismatch is latent rather than immediately harmful. It is still worth correcting: -Xcheck:jni reports the mismatched pair, and on any JVM where GetStringUTFChars() can return a pinned pointer into the object (isCopy == JNI_FALSE), releasing against the wrong object unpins the key string a second time and leaves the value string pinned for the lifetime of the object.

The path is ordinary API usage, not an edge case: any caller that configures OrtCUDAProviderOptions or OrtTensorRTProviderOptions with at least one option reaches it, since StringConfigProviderOptions.applyToNative() passes the accumulated key and value arrays straight down.

Expected behavior: each UTF-8 buffer is released against the jstring it was acquired from β€” keys[i] against the element of jKeyArr, values[i] against the element of jValueArr.

Actual behavior: values[i] is released against the element of jKeyArr. Correctness currently depends on HotSpot's copying implementation of GetStringUTFChars().

Suggested fix β€” one identifier in each of the two release loops:

jobject value = (*jniEnv)->GetObjectArrayElement(jniEnv, jValueArr, i);
(*jniEnv)->ReleaseStringUTFChars(jniEnv, value, values[i]);

A more robust version keeps the jstring references from the acquire loop and releases against those, which removes the possibility of the two loops drifting apart again. That restructuring also addresses the local references these loops leave behind, which I have filed separately.

To reproduce

This is a source-level JNI contract violation, confirmed by reading the code at the commit below. On HotSpot it does not produce incorrect behavior today, which is why functional tests do not catch it.

Static confirmation:

  1. Open java/src/main/native/ai_onnxruntime_providers_OrtCUDAProviderOptions.c, function applyToNative.
  2. In the acquire loop (lines 49-54), confirm value is read from jValueArr.
  3. In the release loop (lines 58-63), confirm value is read from jKeyArr while values[i] is what gets released.
  4. Repeat for ai_onnxruntime_providers_OrtTensorRTProviderOptions.c, lines 48-53 and 57-62.

Runtime observation (optional):

  1. Build the Java API with CUDA or TensorRT enabled.
  2. Construct OrtCUDAProviderOptions (or OrtTensorRTProviderOptions), call add(key, value) with at least one pair whose key and value are different strings, and apply it to a SessionOptions.
  3. Run under java -Xcheck:jni and observe the warning about ReleaseStringUTFChars being called with a string that does not match the acquisition.

Urgency

No response

Platform

Linux

OS Version

Ubuntu 22.04.5 LTS

ONNX Runtime Installation

Built from Source

ONNX Runtime Version or Commit ID

cf5e9eb

ONNX Runtime API

Python

Architecture

X64

Execution Provider

Default CPU

Execution Provider Library Version

No response

Activity

  1. added
    ep:CUDAissues related to the CUDA execution provider
    ep:TensorRTissues related to TensorRT execution provider
    api:Javaissues related to the Java API
    on Aug 7, 2026
  2. Saad-Mallebhari commented on Aug 7, 2026

    @Saad-Mallebhari

    Hi sx-KAN (@K-ANOY)

    Looking at java/src/main/native/ai_onnxruntime_providers_OrtCUDAProviderOptions.c (lines 58–63) and java/src/main/native/ai_onnxruntime_providers_OrtTensorRTProviderOptions.c (lines 57–62), the release loop in applyToNative() fetches value from jKeyArr instead of jValueArr prior to calling ReleaseStringUTFChars().

    Because the JNI specification requires ReleaseStringUTFChars() to receive the exact same jstring passed to GetStringUTFChars(), releasing values[i] against the key element violates the JNI ownership contract. This triggers -Xcheck:jni mismatch warnings and can leak pinned handles on JVM implementations where GetStringUTFChars() returns pinned buffers without copying (isCopy == JNI_FALSE).

    Fixing the array lookup in both files ensures each UTF-8 buffer is released against its acquiring array element:

    // Release allocated strings.
    for (jsize i = 0; i < keyLength; i++) {
      jobject key = (*jniEnv)->GetObjectArrayElement(jniEnv, jKeyArr, i);
      (*jniEnv)->ReleaseStringUTFChars(jniEnv, key, keys[i]);
      jobject value = (*jniEnv)->GetObjectArrayElement(jniEnv, jValueArr, i);
      (*jniEnv)->ReleaseStringUTFChars(jniEnv, value, values[i]);
    }

    Would you like me to open a Pull Request with this fix?

  3. added a commit that references this issue on Sep 3, 2026
    ff8d83b
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    api:Javaissues related to the Java APIep:CUDAissues related to the CUDA execution providerep:TensorRTissues related to TensorRT execution provider

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions