JEP draft: Prepare to Restrict the Use of JNI

OwnerRon Pressler
TypeFeature
ScopeSE
StatusSubmitted
Componentcore-libs
Relates toJEP 454: Foreign Function & Memory API
Reviewed byAlex Buckley, Dan Heidinga, Jorn Vernee
Created2023/05/03 09:08
Updated2024/04/16 09:28
Issue8307341

Summary

Issue warnings about uses of the Java Native Interface (JNI). These warnings aim to prepare developers for a future release that restricts the use of JNI by default in order to improve integrity by default. The warnings issued in this release, and the exceptions thrown in that future release, can be suppressed via a command line option that selectively enables the use of JNI.

Goals

Non-Goals

Motivation

The Java Native Interface (JNI) was added in Java 1.1 as the primary means for interoperating between Java code and native code, typically written in C. JNI allows Java code to call native code (a downcall) and native code to 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 of 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 application's developer.

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, nor do they provoke exceptions for Java code to catch.

    For example, this C function takes a long value passed from Java code 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 returns. Such crashes, and other unexpected behaviors, are difficult to diagnose.

  2. Native code and Java code often exchange data through direct 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; using such a byte buffer in Java code is practically certain to cause undefined behavior.

    For example, this 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, 0, 10);
  3. Native code can use JNI to access fields and call methods without any access checks by the JVM. Native code can even use JNI to change the values of final fields long after they are initialized. Java code that calls native code can therefore violate the integrity of other Java code.

    For example, String objects are specified to be immutable, but this 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);

    For another example, arrays are specified to disallow access beyond their bounds, but this C code can write 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 JNI functions incorrectly, principally GetPrimitiveArrayCritical and GetStringCritical, can cause undesirable garbage collector behavior that can manifest at any time during the program's lifetime.

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

Description

A future JDK release will restrict the loading of native libraries and the binding of native methods by throwing exceptions when Java code tries to perform these operations. It will be possible to avoid these exceptions 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 by enabling the use of JNI on the command line; this also prepares the Java code to run without the exceptions that will be thrown in future JDK releases.

We may add the following related features in a later release:

Warnings on loading native libraries

Native libraries are loaded via the load and loadLibrary methods of the java.lang.Runtime class. (The identically named convenience methods load and loadLibrary of the java.lang.System class merely invoke the corresponding methods of the system-wide Runtime instance.)

If a native library defines initialization functions then the operating system runs them when loading the library; these functions contain arbitrary native code.

If a native library defines a JNI_OnLoad function then the Java runtime invokes it when loading the library; this function can also contain arbitrary native code.

Because the load and loadLibrary methods can cause native code to run, and because native code has risks, these methods are restricted in JDK NN. When a restricted method is called, the JVM runs the method but issues 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 written to the standard error stream. At most one such warning is issued for any particular module. (The unnamed module, mentioned in this example warning, is the module containing code on the class path.)

Warnings on binding native methods

When a native method in Java code is first called, the method is linked to the corresponding native code in a native library. This operation 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 issues 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

At most one such warning is issued for any particular module. Specifically:

Enabling the use of JNI

Both types of warnings can be avoided by enabling the use of JNI, in effect acknowledging the program's need to load native libraries and bind native methods. Do this at startup, via the 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 ...

Enabling native access for the unnamed module is coarse-grained: It grants native access to all classes on the class path. To make the most out of the integrity benefits of this feature, we recommend moving JAR files that use JNI to the module path so that native access can be granted only to them and not to the unnamed module. This both limits risk and helps keep track of which components use JNI. The JARs do not need to be modular; if they are not modular then they will be treated as automatic modules.

When the --enable-native-access option is specified, any attempt by a module outside the given list of modules to load a native library or bind a native method causes an IllegalCallerException to be thrown.

As an alternative to the --enable-native-access option, you can add this attribute to the manifest of an executable JAR file, i.e., a JAR file that is launched via the java -jar option:

Enable-Native-Access: ALL-UNNAMED

ALL-UNNAMED is the only supported value; other module names cause an exception to be thrown.

When a module is created programmatically, you can enable its use of JNI via the ModuleLayer.Controller::enableNativeAccess method.

Disabling the use of JNI

Given the risks of native code, you might like to ensure that your 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 causes an IllegalCallerException to be thrown. In effect, this simulates the behavior of future JDK releases which will throw an exception unless Java code is enabled explicitly to use JNI via --enable-native-access.

JNI Invocation API

The JNI Invocation API allows a native application to embed the JVM in its own process. In JDK NN, Java code that runs in an embedded JVM is, by default, restricted in its use of JNI. Warnings about the use of JNI will be issued, just 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 enable native access for the embedded JVM by passing the --enable-native-access=... option when creating the JVM. Java code that runs in the embedded JVM can then use JNI without warnings, as if the JVM had been launched from the command line with --enable-native-access=....

Risks and Assumptions

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

We assume that developers whose applications rely directly or indirectly on native code will be able to configure the Java runtime to enable the use of JNI, via --enable-native-access. This is similar to how developers can configure the Java runtime to disable strong encapsulation for modules, via --add-opens.

Alternatives

Rather than restrict the loading of native libraries and the binding of native methods, the JVM could apply access control rules when native code uses JNI functions to access Java fields and methods. However, this is insufficient to maintain integrity because any use of native code can lead to undefined behavior, whether or not it uses JNI functions. Portions of the FFM API are restricted for the same reason, even though the FFM API does not offer access to Java objects from native code.