JEP 472: Prepare to Restrict the Use of JNI
Подготовка к ограничению использования JNI
| Автор | Ron Pressler & Alex Buckley |
| Ответственный | Ron Pressler |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 24 |
| Компонент | core-libs |
| Обсуждение | jdk dash dev at openjdk dot org |
| Связан с | JEP 454: Foreign Function & Memory API |
| Рецензенты | Dan Heidinga, Jorn Vernee, Mark Reinhold, Maurizio Cimadamore |
| Одобрен | Alan Bateman |
| Создан | 2023/05/03 09:08 |
| Обновлён | 2025/11/08 12:35 |
| Задача | 8307341 |
Аннотация
Выдавать предупреждения при использовании Java Native Interface (JNI) и изменить Foreign Function & Memory (FFM) API так, чтобы он выдавал предупреждения единообразно. Все эти предупреждения должны подготовить разработчиков к будущему выпуску, который обеспечит целостность по умолчанию за счёт единообразного ограничения JNI и FFM API. Разработчики приложений могут избежать и нынешних предупреждений, и будущих ограничений, если выборочно разрешат эти интерфейсы там, где они необходимы.
Цели
-
Сохранить за JNI статус стандартного способа взаимодействия с нативным кодом.
-
Подготовить экосистему Java к будущему выпуску, в котором взаимодействие с нативным кодом по умолчанию запрещено, будь то через JNI или через FFM API. Начиная с этого выпуска разработчикам приложений придётся явно разрешать использование JNI и FFM API при запуске.
-
Согласовать использование JNI и FFM API так, чтобы сопровождающие библиотек могли переходить с одного на другой, не требуя от разработчиков приложений менять какие-либо параметры командной строки.
Что не является целью
-
Объявлять JNI устаревшим или удалять JNI из платформы Java не является целью.
-
Ограничивать поведение нативного кода, вызываемого через JNI, не является целью. Например, все нативные функции JNI останутся доступными для нативного кода.
Мотивация
Java Native Interface (JNI) появился в JDK 1.1 как основное средство взаимодействия между кодом на Java и нативным кодом, обычно написанным на C. JNI позволяет коду на Java вызывать нативный код (нисходящий вызов, downcall), а нативному коду — вызывать код на Java (восходящий вызов, upcall).
К сожалению, любое взаимодействие между кодом на 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 создаёт байтовый буфер из 10 элементов, начинающийся с адреса 0, и возвращает его коду на Java. JVM аварийно завершится, когда код на Java попытается прочитать или записать этот буфер:
return (*env)->NewDirectByteBuffer(env, 0, 10); -
Нативный код может с помощью JNI обращаться к полям и вызывать методы без каких-либо проверок доступа со стороны JVM. С помощью JNI нативный код может даже менять значения полей
finalспустя долгое время после их инициализации. Поэтому код на 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, может вызвать нежелательное поведение сборщика мусора, которое может проявиться в любой момент за время работы программы.
Foreign Function & Memory (FFM) API, появившийся в JDK 22 как предпочтительная альтернатива JNI, несёт первый и второй риски. В FFM API мы заранее приняли меры для снижения этих рисков, отделив действия, угрожающие целостности, от безопасных. Поэтому некоторые части FFM API отнесены к ограниченным методам: разработчик приложения должен одобрить их использование и явно разрешить его параметром командной строки лаунчера java. JNI следует последовать примеру FFM API на пути к целостности по умолчанию.
Подготовка к ограничению использования JNI — часть долгосрочной согласованной работы по обеспечению целостности по умолчанию в платформе Java. К другим инициативам относятся удаление методов доступа к памяти в sun.misc.Unsafe (JEP 471) и ограничение динамической загрузки агентов (JEP 451). Эта работа сделает платформу Java безопаснее и производительнее. Кроме того, она снизит риск того, что разработчики приложений застрянут на старых выпусках JDK из-за библиотек, которые ломаются на новых выпусках при изменении неподдерживаемых API.
Описание
В JDK 22 и более поздних выпусках нативный код можно вызывать через Java Native Interface (JNI) или через Foreign Function & Memory (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, следует сообщить пользователям, что тем потребуется разрешить нативный доступ одним из описанных ниже способов.
Чтобы разрешить нативный доступ для всего кода в class path, используйте следующий параметр командной строки:
java --enable-native-access=ALL-UNNAMED ...
Чтобы разрешить нативный доступ для конкретных модулей в module path, передайте список имён модулей через запятую:
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 Invocation API позволяет нативному приложению встроить JVM в собственный процесс. Нативное приложение, использующее JNI Invocation API, может разрешить нативный доступ для модулей встроенной JVM, передав параметр
--enable-native-accessпри создании JVM.
Более избирательное разрешение нативного доступа
Параметр --enable-native-access=ALL-UNNAMED действует грубо: он снимает ограничения нативного доступа для JNI и FFM API со всех классов в class path. Чтобы ограничить риск и добиться более высокой целостности, мы рекомендуем перенести JAR-файлы, использующие JNI или FFM API, в module path. Тогда нативный доступ можно будет разрешить именно для этих JAR-файлов, а не для class path целиком. JAR-файл можно перенести из class path в module path, не превращая его в модуль; среда выполнения Java будет считать его автоматическим модулем, имя которого определяется по имени файла.
Управление действием ограничений нативного доступа
Если для модуля не разрешён нативный доступ, то коду в этом модуле запрещено выполнять ограниченные операции. Что делает среда выполнения Java при попытке выполнить такую операцию, определяет новый параметр командной строки --illegal-native-access, близкий по духу и форме к параметру --illegal-access, появившемуся в JDK 9 в JEP 261. Он работает так:
-
--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 ...
Предупреждения при загрузке нативных библиотек
В JNI нативные библиотеки загружаются методами load и 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 входит в платформу Java начиная с JDK 1.1, поэтому существует риск, что ограничения на использование 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.