JEP draft: Prepare to Restrict The Use of JNI

OwnerRon Pressler
TypeFeature
ScopeSE
StatusDraft
Componentcore-libs
Reviewed byAlex Buckley, Jorn Vernee
Created2023/05/03 09:08
Updated2023/09/21 18:58
Issue8307341

Summary

Support the goal of integrity-by-default by issuing warnings when native code is invoked through the Java Native Interface (JNI). Warnings may be avoided by enabling the use of JNI on the command line. In a future release, invoking native code will cause an exception unless the use of JNI is enabled.

Goals

Non-Goals

Motivation

The Java Native Interface (JNI) was added in JDK 1.1 as the primary means for interoperating between Java code and native code, typically written in C. JNI lets Java code call native code (a "downcall") and lets native code call Java code (an "upcall").

Unfortunately, any interaction at all between Java code and native code is risky because it can compromise the integrity of applications and the Java Platform itself. According to the policy of integrity by default, all JDK features that are capable of breaking integrity must obtain explicit approval from the end user or the application assembler.

Here are four common interactions and their risks:

  1. Calling native code can lead to arbitrary undefined behavior, including JVM crashes. Such problems cannot be prevented by the Java runtime or caught by Java code.

    For example, the following C function takes a long value passed from Java and treats it as an address in memory, storing a value at that address:

    void Java_pkg_C_setPointerToThree__J(jlong ptr) {
        *(int*)ptr = 3;
    }

    Calling this C function could corrupt memory used by the JVM, causing the JVM to crash at an unpredictable time, long after the C function has returned. Such crashes (and other unexpected behaviors) cannot be easily localized to their cause.

  2. Native code and Java code often exchange data through byte buffers, which are regions of memory not managed by the JVM's garbage collector. Native code can produce a byte buffer that is backed by an invalid region of memory, and using the byte buffer in Java code is practically certain to cause undefined behavior.

    For example, the following C code constructs a 10-element byte buffer starting at address 0, and returns it to Java code. The JVM will crash when Java code attempts to read or write the byte buffer:

    ...
    return (*env)->NewDirectByteBuffer(env, NULL, 10);  // a buffer wrapping an illegal address
  3. Native code could use the JNI API to access fields and call methods without any access checks by the JVM or even change the values of final fields long after they are initialized because the implementation of JNI does not perform the appropriate checks. Java code that uses JNI could therefore circumvent any of the invariants established through strong encapsulation of any other Java code.

    For example, Java code frequently relies on the invariant that String objects are immutable, but the following C code mutates a String object by writing to an array referenced by a private field:

    jclass clazz = (*env)->FindClass(env, "java/lang/String");
    jfieldID fid = (*env)->GetFieldID(env, clazz , "value", "[B");
    jbyteArray contents = (jbyteArray)(*env)->GetObjectField(env, str, fid);
    jbyte b = 0;
    (*env)->SetByteArrayRegion(env, contents, 0, 1, &b);

    Additionally, the following code can violate the JVM's integrity invariant of never writing past the end of an array:

    jbyte *a = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
    a[500] = 3; // may be out of bounds
    (*env)->ReleasePrimitiveArrayCritical(env, arr, a, 0);
  4. Native code which uses certain functions of the JNI API incorrectly (principally GetPrimitiveArrayCritical and GetStringCritical) may cause undesirable behavior by the garbage collector that can manifest at any time during the program's lifetime.

The Foreign Function & Memory (FFM) API, introduced in JDK 19 as the preferred alternative to JNI, shares the first and second risks. FFM took a proactive approach to mitigate these risks, and separated actions that risk integrity from those that do not. Some parts of the FFM API are classified as restricted methods, which means the end user must approve their use and opt in via a command line option. JNI should follow FFM's example toward achieving integrity by default.

Description

Restricting the use of JNI means:

  1. Restricting the loading of native libraries via java.lang.System and java.lang.Runtime.
  2. Restricting the binding of native methods.

A future JDK release will restrict the loading of native libraries and the binding of native methods by throwing an exception when Java code tries to perform these activities. It will be possible to avoid the exception by enabling the use of JNI on the command line.

To prepare developers for these restrictions, JDK NN issues a warning the first time that Java code loads a native library or binds a native method. The warning can be avoided in JDK NN by enabling the use of JNI on the command line; this also prepares the Java code to run without exceptions on a future JDK release.

Restrictions on loading native libraries

The following methods in java.lang.System and java.lang.Runtime load a native library and invoke its JNI_OnLoad function, which contains arbitrary native code:

In addition, the native library may define initialization functions that are run by the operating system, and these also contain arbitrary native code.

Because of the risks of native code, these four methods are restricted in JDK NN. When a restricted method is called, the JVM runs the method but gives a warning that identifies the caller:

WARNING: A restricted method in java.lang.System has been called
WARNING: System::load has been called by com.foo.Server in an unnamed module
WARNING: Use --enable-native-access=ALL-UNNAMED to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled

The warning is issued to the standard error stream. The warning is given at most once for each module whose code calls a restricted method. (Code on the class path resides in the unnamed module, mentioned in the example above.)

Restrictions on binding native methods

When a native method in Java is first called by the Java program, the native method is linked to the corresponding native code in a native library. This linkage is called "binding" the native method. (The correspondence between the native method and the native code is described here.)

When a native method is bound, the JVM binds the method but gives a warning that identifies the caller:

WARNING: A native method in org.baz.services.Controller has been bound
WARNING: Controller::getData in module org.baz has been called by com.foo.Server in an unnamed module
WARNING: Use --enable-native-access=org.baz to avoid a warning for native methods declared in org.baz
WARNING: Native methods will be blocked in a future release unless native access is enabled

This warning is given at most once for a module which declares native methods. Specifically:

Enabling use of JNI

Warnings about restricted methods and native methods can be avoided by enabling the use of JNI, in effect acknowledging the program's need to load native libraries and bind native methods. This is done at startup, via a command line option:

java --enable-native-access=M ...

where M is a comma-separated list of modules that should be allowed to load native libraries and bind native methods. To avoid warnings for code on the class path, use:

java --enable-native-access=ALL-UNNAMED ...

When the --enable-native-access option is present in JDK NN, any attempt to load a native library or bind a native method by a module outside the list of specified modules will cause an IllegalCallerException rather than a warning.

As an alternative to the --enable-native-access option, the following attribute may be added to the manifest of a JAR file that is used with the -jar flag (an "executable JAR"):

Enable-Native-Access: ALL-UNNAMED

ALL-UNNAMED is the only value supported in the attribute. Other module names will be ignored.

When a module is created programmatically, its use of JNI can be enabled via the ModuleLayer.Controller.enableNativeAccess method.

Disabling use of JNI

Given the risks of native code, some developers would like to ensure that their application's dependencies do not use native code. One way to achieve this is to run with:

java --enable-native-access=java.base ...

This enables the use of JNI by the java.base module only; any other Java code that tries to load native libraries or bind native methods will cause an IllegalCallerException in JDK NN. In effect, this simulates the behavior of the future JDK release which will throw an exception unless Java code is identified explicitly via --enable-native-access.

JNI Invocation API

The JNI Invocation API allows a native application to host the JVM in-process. In JDK NN, Java code that runs in a hosted JVM is, by default, restricted in its use of JNI (i.e., the use causes warnings), as if the JVM had been launched from the command line without enabling native access.

A native application which uses the JNI Invocation API can choose to enable native access for the hosted JVM by passing the --enable-native-access=... option when creating the JVM. If this occurs, then Java code that runs in the hosted JVM can use JNI without warnings, as if the JVM had been launched from the command line with --enable-native-access=....

Future work

A release after JDK NN may include the following functionality:

Risks and Assumptions

JNI has been part of the Java Platform since JDK 1.1, so there is a risk that some applications will be impacted by restrictions on the use of JNI. The restrictions are designed to be analogous to those placed on use of the FFM API:

Alternatives