Details
-
JEP
-
Status: Draft
-
P3
-
Resolution: Unresolved
-
None
-
None
-
Ron Pressler
-
Feature
-
Open
-
SE
Description
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
Prepare for a future release that restricts the use of JNI to load native libraries and call native code. Restricting the use of JNI means that the user will have to enable the use of JNI on the command line.
Align JNI and the Foreign Function & Memory (FFM) API, so that libraries can migrate from JNI to FFM without changing the command line option used to enable each of them.
Non-Goals
There is no plan to remove JNI from the Java Platform.
It is not a goal to restrict the behavior of native code called via JNI. For example, all the native JNI functions will remain usable by native code.
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:
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.
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
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 <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">String</code> objects are immutable, but the following C code mutates a
String
object by writing to an array referenced by aprivate
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);
Native code which uses certain functions of the JNI API incorrectly (principally <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">GetPrimitiveArrayCritical</code> and <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">GetStringCritical</code>) 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:
- Restricting the loading of native libraries via
java.lang.System
andjava.lang.Runtime
. - 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 <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">JNI_OnLoad</code> function, which contains arbitrary native code:
- <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">System.load</code>
- <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">Runtime.load</code>
- <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">System.loadLibrary</code>
- <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">Runtime.loadLibrary</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:
The warning is given only when a
native
method is bound, which happens the first time that thenative
method is called. The warning is not given every time that thenative
method is called.The warning is given the first time that a
native
method declared in a particular module is bound. Afterward, binding othernative
methods declared in the same module will not cause a warning.
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 <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">ModuleLayer.Controller.enableNativeAccess</code> 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:
To help identify libraries that use JNI, the
jdeps
tool may be extended to report where Java code loads native libraries or bindsnative
methods, similar to how the tool's static analysis currently reports dependencies on JDK modules.To promote reliable configuration, allow a module's declaration to express that the module needs to use JNI. At startup, the Java runtime would check if the use of JNI was enabled on the command line, and if not, would refuse to load any module which needs to use JNI.
JNI allows native code to break the encapsulation of Java code, which could interfere with future JVM optimizations in ways that use of the FFM API does not. A future JDK release may offer an additional flag that enables the use of only FFM and not JNI.
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:
Loading a native library: The restriction on
System.loadLibrary
and related methods is analogous to the restriction on FFM's <code class="prettyprint" data-shared-secret="1695759071168-0.5634843703604367">SymbolLookup.libraryLookup</code>.Binding a
native
method: The restriction on binding anative
method declared in a Java class -- which is implicitly done the first time the method is called -- is analogous to a Java class obtaining a "downcall handle" with FFM'sLinker.downcallHandle
, which is a restricted method.Calling a
native
method: There is no restriction on calling anative
method (after it has been bound), and this is analogous to how invoking a downcall handle in the FFM API is not restricted.
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 is risky, and can lead to undefined behavior. 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.
Attachments
Issue Links
- relates to
-
JDK-8305968 Integrity and Strong Encapsulation
-
- Draft
-