JEP 472:准备限制 JNI 的使用
概括
发出有关Java 本机接口 (JNI)使用的警告,并调整外部函数和内存 (FFM) API以一致的方式发出警告。所有此类警告旨在帮助开发者为未来版本做好准备,该版本通过统一限制 JNI 和 FFM API来默认确保完整性。应用开发者可以通过在必要时选择性启用这些接口来避免当前警告和未来限制。
目标
-
保留 JNI 作为与本机代码互操作的标准方式的状态。
-
为 Java 生态系统的未来版本做好准备,该版本默认禁止与本机代码进行互操作,无论是通过 JNI 还是 FFM API。从该版本开始,应用开发者必须在启动时明确启用 JNI 和 FFM API。
-
协调 JNI 和 FFM API 的使用,以便库维护者可以从一个迁移到另一个,而无需应用程序开发人员更改任何命令行选项。
非目标
-
我们的目的并不是弃用 JNI 或者从 Java 平台中删除 JNI。
-
我们的目标并不是限制通过 JNI 调用的本机代码的行为。例如,所有本机JNI 函数仍可供本机代码使用。
动机
Java 本机接口(JNI)是在 JDK 1.1 中引入的,作为 Java 代码和本机代码之间互操作的主要方式,通常用 C 语言编写。JNI 允许 Java 代码调用本机代码(向下调用)和本机代码调用 Java 代码(向上调用)。
不幸的是,Java 代码和本机代码之间的任何交互都是有风险的,因为它可能会损害应用程序和 Java 平台本身的完整性。根据默认完整性策略,所有能够破坏完整性的 JDK 功能都必须获得应用程序开发人员的明确批准。
以下是四种常见的相互作用及其风险:
-
调用本机代码可能会导致任意未定义行为,包括 JVM 崩溃。Java 运行时无法阻止此类问题,也不会引发 Java 代码捕获的异常。
例如,以下 C 函数接受
long
从 Java 代码传递的值并将其视为内存中的地址,并将值存储在该地址:void Java_pkg_C_setPointerToThree__J(jlong ptr) {
*(int*)ptr = 3;
}调用此 C 函数可能会损坏 JVM 使用的内存,导致 JVM 在不可预测的时间(C 函数返回后很长时间)崩溃。此类崩溃和其他意外行为很难诊断。
-
本机代码和 Java 代码通常通过直接字节缓冲区交换数据,这些缓冲区是不受 JVM 垃圾收集器管理的内存区域。本机代码可以生成由无效内存区域支持的字节缓冲区;在 Java 代码中使用这样的字节缓冲区几乎肯定会导致未定义的行为。
例如,以下 C 代码从地址 0 开始构建一个包含 10 个元素的字节缓冲区并将其返回给 Java 代码。当 Java 代码尝试读取或写入字节缓冲区时,JVM 将崩溃:
return (*env)->NewDirectByteBuffer(env, 0, 10);
-
本机代码可以使用 JNI访问字段并调用方法,而无需 JVM 进行任何访问检查。本机代码甚至可以
final
在字段初始化很久之后使用 JNI 更改字段值。因此,调用本机代码的 Java 代码可能会破坏其他 Java 代码的完整性。例如,
String
对象被指定为不可变的,但是此 C 代码String
通过写入字段引用的数组来改变对象private
: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);再比如,数组被指定为不允许超出其边界的访问,但是这个 C 代码可以写入超出数组末尾的内容:
jbyte *a = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
a[500] = 3; // may be out of bounds
(*env)->ReleasePrimitiveArrayCritical(env, arr, a, 0); -
错误使用某些 JNI 函数的本机代码(主要是
GetPrimitiveArrayCritical
和GetStringCritical
)可能导致不良的垃圾收集器行为,这种行为可能在程序的生命周期中随时出现。
JDK 22 中引入的外部函数和内存 (FFM) API作为 JNI 的首选替代方案,具有第一和第二种风险。在 FFM API 中,我们采取了主动的方法来减轻这些风险,将可能影响完整性的操作与不影响完整性的操作区分开来。因此,FFM API 的某些部分被归类为受限方法,这意味着应用程序开发人员必须批准它们的使用并通过启动器命令行选项选择加入java
。JNI 应该遵循 FFM API 的示例,默认实现完整性。
准备限制 JNI 的使用是长期协调努力的一部分,旨在确保 Java 平台默认具有完整性。其他举措包括删除内存访问方法sun.misc.Unsafe
( JEP 471 ) 和限制代理的动态加载 ( JEP 451 )。这些努力将使 Java 平台更安全、性能更高。它们还将降低应用程序开发人员因更改不受支持的 API 时新版本上的库而陷入旧 JDK 版本的风险。
描述
在 JDK 22 及更高版本中,您可以通过 Java 本机接口 (JNI) 或外部函数和内存 (FFM) API 调用本机代码。无论哪种情况,您都必须先加载本机库并将 Java 构造链接到库中的函数。这些加载和链接步骤在 FFM API 中受到限制,这意味着它们默认会导致在运行时发出警告。在 JDK 24 中,我们将限制 JNI 中的加载和链接步骤,以便它们也默认会导致在运行时发出警告。
我们将对加载和链接本机库的限制称为_本机访问限制_。在 JDK 24 中,无论是使用 JNI 还是 FFM API 加载和链接本机库,本机访问限制都将统一适用。在 JNI 中加载和链接本机库的具体操作(现在受本机访问限制)将在后面介绍。
我们会逐渐加强本机访问限制的效果。未来的 JDK 版本将默认在 Java 代码使用 JNI 或 FFM API 加载和链接本机库时抛出异常,而不是发出警告。目的并非阻止使用 JNI 或 FFM API,而是确保应用程序和 Java 平台默认具有完整性。
启用本机访问
应用程序开发人员可以通过在启动时为选定的 Java 代码启用本机访问来避免警告(以及将来的异常)。启用本机访问表示确认应用程序需要加载和链接本机库并解除本机访问限制。
根据默认完整性策略,启用本机访问的是应用程序开发人员(或者根据应用程序开发人员的建议,可能是部署人员),而不是库开发人员。依赖 JNI 或 FFM API 的库开发人员应告知其用户,他们需要使用以下方法之一启用本机访问。
要为类路径上的所有代码启用本机访问,请使用以下命令行选项:
java --enable-native-access=ALL-UNNAMED ...
要启用模块路径上特定模块的本机访问,请传递以逗号分隔的模块名称列表:
java --enable-native-access=M1,M2,... ...
如果使用 JNI 的代码受到本机访问限制的影响
- 它调用
System::loadLibrary
、System::load
、Runtime::loadLibrary
或Runtime::load
或 - 它声明了一个
native
方法。
仅调用不同模块中声明的方法的代码native
不需要启用本机访问。
大多数应用程序开发人员会--enable-native-access
直接java
在启动脚本中将其传递给启动器,但还有其他技术可用:
-
您可以
--enable-native-access
通过设置环境变量间接传递给启动器JDK_JAVA_OPTIONS
。 -
您可以输入
--enable-native-access
由脚本或最终用户传递给启动器的参数文件,例如,java @config
-
您可以将 添加
Enable-Native-Access: ALL-UNNAMED
到可执行 JAR 文件的清单中,即通过 启动的 JAR 文件java -jar
。(清单条目唯一支持的值Enable-Native-Access
是ALL-UNNAMED
;其他值会导致抛出异常。) -
如果您为应用程序创建自定义 Java 运行时,则可以通过选项将
--enable-native-access
选项传递给,以便为生成的运行时映像启用本机访问。jlink``--add-options
-
如果您的代码动态创建模块,则可以通过该方法启用它们的本机访问
ModuleLayer.Controller::enableNativeAccess
,该方法本身是一种受限制的方法。代码可以通过该Module::isNativeAccessEnabled
方法动态检查其模块是否具有本机访问权限。 -
JNI 调用 API允许本机应用程序在其自己的进程中嵌入 JVM。使用 JNI 调用 API 的本机应用程序可以通过
--enable-native-access
在创建 JVM时传递选项来启用对嵌入式 JVM 中模块的本机访问。
更有选择性地启用本机访问
该--enable-native-access=ALL-UNNAMED
选项是粗粒度的:它解除了类路径上所有类的 JNI 和 FFM API 的本机访问限制。为了限制风险并实现更高的完整性,我们建议将使用 JNI 或 FFM API 的 JAR 文件移至模块路径。这样可以专门为这些 JAR 文件启用本机访问,而不是为整个类路径启用。JAR 文件可以从类路径移动到模块路径而无需模块化;Java 运行时会将其视为自动模块,其名称基于其文件名。
控制本机访问限制的影响
如果未对模块启用本机访问,则该模块中的代码执行受限操作是非法的。尝试执行此类操作时 Java 运行时采取的操作由新的命令行选项控制,该选项在精神和形式上与 JDK 9 中JEP 261引入的选项--illegal-native-access
类似。其工作原理如下:--illegal-access
-
--illegal-native-access=allow
允许操作继续进行。 -
--illegal-native-access=warn
允许操作,但在特定模块中第一次发生非法本机访问时发出警告。每个模块最多发出一次警告。此模式是 JDK 24 中的默认模式。它将在未来的版本中逐步淘汰,并最终被删除。
-
--illegal-native-access=deny
对每个非法的本机访问操作抛出IllegalCallerException
异常。此模式将成为未来版本中的默认模式。
当deny
成为默认模式时allow
将被删除但warn
至少会保留一个版本支持。
为了为未来做好准备,我们建议您使用该deny
模式运行现有代码,以识别需要本机访问的代码。
调整 FFM API
在 JDK 24 之前,如果一个或多个模块通过--enable-native-access
选项启用了本机访问,则尝试从任何其他模块调用受限的 FFM 方法IllegalCallerException
都会导致抛出。
为了使 FFM API 与 JNI 保持一致,我们将放宽此行为,以便 FFM API 对非法本机访问操作的处理方式与 JNI 完全相同。这意味着,在 JDK 24 中,此类操作将导致警告而不是异常。
您可以使用以下选项组合恢复旧行为:
java --enable-native-access=M,... --illegal-native-access=deny ...
加载本机库时的警告
本机库通过类的load
和方法在 JNI 中加载。(类的同名便捷方法和仅调用系统范围实例的相应方法。)loadLibrary
java.lang.Runtime
load
loadLibrary
java.lang.System``Runtime
加载本机库是有风险的,因为它可能导致本机代码运行:
-
如果本机库定义了初始化函数,则操作系统在加载库时运行它们;这些函数包含任意本机代码。
-
如果本机库定义了一个
JNI_OnLoad
函数,那么 Java 运行时会在加载该库时调用该函数;该函数还包含任意本机代码。
由于存在风险,load
和loadLibrary
方法在 JDK 24 中受到限制,就像SymbolLookup::libraryLookup
方法在 FFM API 中受到限制一样。
当从未启用本机访问的模块调用受限方法时,JVM 将运行该方法,但默认情况下会发出标识调用者的警告:
WARNING: A restricted method in java.lang.System has been called
WARNING: System::load has been called by com.foo.Server in module com.foo (file:/path/to/com.foo.jar)
WARNING: Use --enable-native-access=com.foo to avoid a warning for callers in this module
WARNING: Restricted methods will be blocked in a future release unless native access is enabled
对于任何特定模块,最多会发出一个这样的警告,并且仅当该模块尚未发出警告时才会发出。警告将写入标准错误流。
链接本机库时的警告
native
首次调用某个方法时,它会自动链接到本机库中的相应函数。此链接步骤称为_绑定_,在 JDK 24 中是一项受限制的操作,就像获取向下调用方法句柄在 FFM API 中是一项受限制的操作一样。
首次调用native
未启用本机访问的模块中声明的方法时,JVM 会绑定该native
方法,但默认情况下会发出标识调用者的警告:
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 (file:/path/to/foo.jar)
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
对于任何特定模块,最多会发出一个这样的警告。具体来说:
-
native
仅当方法被绑定时才会发出警告,这种情况发生在第一次调用该方法时。每次调用native
该方法时都不会发出警告。native
-
第一次绑定特定模块中声明的任何方法时都会发出警告
native
,除非已经针对该模块发出了警告。
警告被写入标准错误流。
识别本机代码的使用
-
JFR 事件
jdk.NativeLibraryLoad
并jdk.NativeLibraryUnload
跟踪本机库的加载和卸载。 -
为了帮助识别使用 JNI 的库,一个新的 JDK 工具(暂定名为
jnativescan
)会静态扫描提供的模块路径或类路径中的代码,并报告受限方法的使用和native
方法的声明。
未来工作
-
为了提高配置的可靠性,允许模块声明断言该模块需要本机访问,无论是通过 JNI 还是 FFM API。在启动时,Java 运行时将拒绝加载任何需要本机访问但未在命令行上启用本机访问的模块。
-
为了允许使用 FFM API 而不允许使用 JNI,请提供一个命令行选项,允许使用前者而不允许使用后者。JNI 允许本机代码破坏 Java 代码的封装,这可能会干扰未来的 JVM 优化,而使用 FFM API 则不会。
风险和假设
-
JNI 自 JDK 1.1 以来一直是 Java 平台的一部分,因此现有应用程序可能会受到 JNI 使用限制的影响。对 Maven Central 上的工件进行分析发现,大约 7% 的现有工件依赖于本机代码。其中,约 25% 直接使用 JNI;其余则依赖于直接或间接使用 JNI 的其他工件。
-
我们假设,应用程序直接或间接依赖本机代码的开发人员将能够配置 Java 运行时以通过 启用 JNI 的使用
--enable-native-access
,如上所述。这类似于他们已经可以通过 来配置 Java 运行时以禁用模块的强封装--add-opens
。
替代方案
native
JVM 可以在本机代码使用JNI 函数访问 Java 字段和方法时应用访问控制规则,而不是限制本机库的加载和方法的绑定。但是,这不足以保持完整性,因为任何使用本机代码的行为都可能导致未定义的行为,无论它是否使用 JNI 函数。FFM API 的部分内容出于同样的原因受到限制,尽管 FFM API 不提供从本机代码访问 Java 对象的权限。