Команда Spring АйО перевела статью про ужесточение контроля за динамической загрузкой агентов, ограничения доступа к опасным методам работы с памятью и JNI в новых версиях JDK.
Java продолжает развиваться, делая упор на стабильность и устойчивость. Новые версии JDK отражают этот тренд: ограничивается динамическая загрузка агентов, прекращается поддержка опасных методов доступа к памяти, усиливается контроль над использованием JNI. В JDK 22 появилось Foreign Function and Memory (FFM) API, которое упрощает вызов внешнего кода и обеспечивает безопасный доступ к памяти без управления со стороны JVM. Эти изменения поддерживают концепцию "целостности по умолчанию" и формируют более предсказуемую и надёжную экосистему Java. В этой статье мы подробно разберём, как эти обновления укрепляют платформу и делают е безопаснее.
Целостность в платформе Java
В программировании "целостность" (integrity) — это гарантия, что конструкции, используемые для создания программ, завершенные и надежные. В Java это означает, что спецификации платформы полностью охватывают предметную область, а их реализации строго соответствуют этим спецификациям. Такая заложенная в основу платформы целостность позволяет разработчикам уверенно создавать приложения, опираясь на проверенные механизмы платформы.
JEP "Целостность по умолчанию" (Integrity by Default) акцентирует внимание на защите кода и данных от нежелательного вмешательства. Хотя инкапсуляция — ключевой инструмент для обеспечения целостности, платформа Java всё ещё содержит небезопасные API, которые могут эту целостность подорвать. Это способно негативно повлиять на корректность, простоту в поддержке, безопасность, производительность и масштабируемость приложений или библиотек:
Instrumentation API позволяет агентам модифицировать байткод любого метода в любом классе.
Метод AccessibleObject::setAccessible(boolean) позволяет игнорировать границы инкапсуляции при рефлексии. Он предназначен для поддержки сериализации и десериализации объектов, но открывает доступ к вызову приватных методов, чтению и изменению приватных полей, а также записи в final поля. Это делает возможным вмешательство в структуру любого класса, нарушая принципы инкапсуляции.
Класс
sun.misc.Unsafeвключает методы, которые могут получать доступ к private методам и полям и записывать в final поля, игнорируя границы инкапсуляции.Java Native Interface (JNI) позволяет нативному коду взаимодействовать с объектами Java. Однако он также предоставляет доступ к private методам и полям, а также возможность изменять final поля, обходя ограничения инкапсуляции.
Чтобы предотвратить некорректное использование небезопасных API, JEP предполагает ограничение прямого доступа к ним, чтобы библиотеки, фреймворки и инструменты не могли обращаться к ним по умолчанию. Кроме того, текущие JEP должны постепенно решить проблемы, связанные с использованием таких API, предлагая альтернативы для миграции и позволяя обходить вводимые по умолчанию ограничения, если это действительно необходимо.
Постепенное ограничение доступа к небезопасным API и альтернативы
Организуйтесь, чтобы запретить динамическую загрузку агентов
В качестве развития темы целостности JDK 21, JEP 451 сфокусировался на рисках, связанных с динамической загрузкой агентов в работающую JVM. Агенты, появившиеся в JDK 5, позволяют оснащать классы инструментами, такими как профайлеры, для мониторинга приложений. Например, для отладки удаленного приложения вы, возможно, используете опцию -agentlib:jdwp, позволяющую включить встроенный в JVM агент при запуске. Внутри Java использует Attach API, который с соответствующими привилегиями на уровне ОС позволяет инструментам подключаться к работающей JVM.
Некоторые библиотеки используют Attach API, чтобы незаметно подключаться к JVM, загружать агенты динамически и получить практически неограниченные возможности в плане изменения кода на лету. Однако такие действия несут риски для целостности приложения. Начиная с JDK 21, JVM предупреждает о динамической загрузке агентов, чтобы информировать о возможных угрозах и подготовить к будущим релизам, где такие действия могут быть запрещены по умолчанию. Это изменение не затрагивает большинство инструментов, которые не требуют динамической загрузки агентов.
💡Для соблюдения best practices библиотекам рекомендуется загружать агенты при запуске приложения с использованием опций -javaagent или -agentlib. Такой подход обеспечивает баланс между удобством обслуживания и сохранением целостности вашего приложения.
Принимайте меры против использования методов с небезопасным способом доступа к памяти
В течение долгого времени класс sun.misc.Unsafe предоставлял разработчикам доступ к операциям низкого уровня, в особенности к таким задачам, как:
Выполнение операций непосредственно с памятью, чтобы достичь лучшей производительности
Управление off-heap памятью без ограничения на
ByteBufferВыполнение атомарных операций типа compare-and-swap.
Однако, использование этих методов сопряжено с рисками:
Они могут вы��ывать непредсказуемое поведение, сбои приложения или снижение производительности, обходя оптимизации JVM.
Они раскрывают низкоуровневые детали внутренней структуры JVM, что создает проблемы совместимости между версиями Java.
Их небезопасная природа усложняет поддержку и снижает уровень безопасности.
Начиная с Java 23, JDK постепенно выводит из оборота методы доступа к памяти внутри sun.misc.Unsafe, по причине рисков и ограничений, связанных с небезопасными операциями. Вместо них предлагаются безопасные альтернативы: VarHandle API (введен в JDK 9) и Foreign Function and Memory API (введен в JDK 22).
Например, вы могли использовать Unsafe для атомарных обновлений при работе с on-heap памятью. Чтобы избежать рисков, рекомендуется заменить Unsafe на VarHandle, который позволяет достичь той же цели более безопасным способом:
// Migration example from Unsafe
private static final Unsafe UNSAFE = ...;
private static final long OFFSET;
static {
try {
OFFSET = UNSAFE.objectFieldOffset(Point.class.getDeclaredField("x"));
} catch (Exception ex) {
throw new AssertionError(ex);
}
}
private int x;
public boolean update(int newValue) {
return UNSAFE.compareAndSwapInt(this, OFFSET, x, newValue);
}
/// Use VarHandle to achieve the same
private static final VarHandle HANDLE = MethodHandles.lookup().findVarHandle(Point.class, "x", int.class);
public boolean update(int newValue) {
return HANDLE.compareAndSet(this, x, newValue);
}Аналогично, если вы использовали Unsafe для работы с off-heap памятью, рекомендуется перенести этот код на конструкции FFM API, такие как MemorySegment, и управлять жизненным циклом памяти с помощью Arena:
// Using Unsafe for off-heap memory
long address = UNSAFE.allocateMemory(1024);
UNSAFE.putInt(address, 42);
int value = UNSAFE.getInt(address);
UNSAFE.freeMemory(address);
// Using MemorySegment from FFM API to achieve the same
try (Arena arena = Arena.ofShared()) {
long byteSize = ValueLayout.JAVA_INT.byteSize();
MemorySegment segment = arena.allocate(byteSize);
segment.set(ValueLayout.JAVA_INT, 0, 42);
int value = segment.get(ValueLayout.JAVA_INT, 0);
}Эти стандартные API предлагают вам безопасную, производительную альтернативу для большинства use cases, гарантируя совместимость с современными и будущими версиями Java.
💡 Вот несколько инструментов, которые помогут выявить зависимости от Unsafe:
Обращайте внимание на предупреждения компилятора
javac.Используйте JDK Flight Recorder (JFR) — событие
jdk.DeprecatedInvocationфиксирует вызовы терминально устаревших методов.Начиная с JDK 23, запускайте приложение с новой опцией командной строки
--sun-misc-unsafe-memory-access={allow|warn|debug|deny}, чтобы отслеживать влияние депрекации и удаления этих методов на ваши зависимости.
В будущих релизах JDK методы доступа к памяти в sun.misc.Unsafe будут постепенно выводиться из использования. План выглядит так:
JDK 23: методы помечены как deprecated, с предупреждениями во время компиляции и выполнения (по умолчанию
--sun-misc-unsafe-memory-access=allow).JDK 24: согласно JEP 498, предупреждения в рантайме включены по умолчанию (по умолчанию
--sun-misc-unsafe-memory-access=warn).JDK 26: неподдерживаемые операции будут выбрасывать исключения (по умолчанию --
sun-misc-unsafe-memory-access=deny).После JDK 26: методы будут полностью удалены, а опция
--sun-misc-unsafe-memory-accessигнорироваться.
При миграции с методов доступа к памяти из sun.misc.Unsafe избегайте использования неподдерживаемых внутренних возможностей JDK. Это снизит риск сбоев приложения при изменениях в платформе.
Готовьтесь к ограничениям на использование JNI
С момента появления в JDK 1.1 Java Native Interface (JNI) упрощает взаимодействие между Java и нативным кодом. Однако, несмотря на его несомненную полезность, использование JNI может подорвать целостность приложения:
Вызов нативного кода может привести к непредсказуемому поведению, включая сбои JVM. Обмен данными между нативным кодом и Java часто осуществляется через прямые байтовые буферы — области памяти, неподконтрольные сборщику мусора JVM. Использование буфера, связанного с невалидной областью памяти, неизбежно приведёт к ошибкам и нестабильности.
Нативный код может получать доступ к полям через JNI, а также вызывать методы, обходя проверки доступа, выпол��яемые JVM.
Нативный код может некорректно использовать функции JNI, такие как GetPrimitiveArrayCritical, что приводит к нежелательному поведению сборщика мусора, проявляющемуся на всем протяжении работы программы.
JNI нельзя отключить, поэтому невозможно полностью исключить вызовы нативного кода с использованием опасных функций JNI. JVM загружает нативные библиотеки через методы load и loadLibrary класса java.lang.Runtime. Аналогичные методы в классе java.lang.System просто перенаправляют вызовы к Runtime. Загрузка нативной библиотеки сопряжена с риском, так как библиотека может выполнять нативный код через функции инициализации или метод JNI_OnLoad, вызываемый Java runtime. Из-за этих рисков в JDK 24 введены ограничения на использование методов load и loadLibrary.
В отличие от JNI, большинство функций Foreign Function and Memory (FFM) API безопасны по умолчанию. Многие сценарии, ранее реализованные через JNI и нативный код, теперь можно перенести на FFM API, который не нарушает целостность платформы Java.
При загрузке и связывании нативных библиотек через FFM API код Java может указать параметры, несовместимые с типами внешней функции. Вызов такого метода downcall может привести к сбоям виртуальной машины или непредсказуемому поведению. Однако небезопасные методы FFM API менее рискованны, чем функции JNI: например, они не позволяют изменять значения final полей в Java-объектах. По умолчанию использование небезопасных методов в FFM API разрешено, но при этом в рантайме отображается предупреждение:
WARNING: A restricted method in java.lang.foreign.Linker has been called
WARNING: java.lang.foreign.Linker::downcallHandle has been called by ReadFileWithFopen 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Указанный фрагмент предупреждает, что код Java использует небезопасный и ограниченный метод FFM API. Учитывая риск непредсказуемого поведения и сбоев JVM, разработчикам следует осторожно разрешать нативный доступ для кода Java на этапе запуска. Такое разрешение подтверждает необходимость загрузки и связывания нативных библиотек, снимая наложенные ограничения.
💡Если библиотека использует JNI или FFM API, её документация должна информировать пользователей (например, разработчиков приложений) о необходимости явного разрешения нативного доступа. При разработке или деплойменте ответственность за выдачу такого разрешения ложится на вас. Например, если ваше приложение использует библиотеку, требующую опции --enable-native-access=ALL-UNNAMED в рантайме, помните, что эта опция снимает ограничения на нативный доступ через JNI и FFM API для всех классов внутри class path.
Опция --enable-native-access=ALL-UNNAMED имеет широкий охват, поэтому для минимизации рисков и улучшения целостности рекомендуется переместить JAR-файлы, использующие JNI или FFM API, в модульный путь. Это позволит включать нативный доступ только для этих JAR-файлов, а не для всего class path. Если вы переместите JAR-файл из class path в модульный путь без модуляризации, Java runtime будет рассматривать его как автоматический модуль и назначит ему имя на основании имени файла.
Если нативный доступ для модуля не объявлен, любые попытки выполнить ограниченные операции в этом модуле считаются нелегальными. Реакция Java runtime на такие операции регулируется опцией --illegal-native-access и может быть настроена следующим образом:
--illegal-native-access=allow— разрешает выполнение операций без предупреждений или исключений.--illegal-native-access=warn— позволяет выполнение операций, но выдает предупреждение при первом фиксировании нелегального доступа в модуле. Только одно предупреждение выдается на модуль. В JDK 24 этот режим установлен по умолчанию.--illegal-native-access=deny— выбрасывает исключение IllegalCallerException для каждой операции с нелегальным нативным доступом.
До JDK 24 попытки вызвать ограниченные методы FFM из модулей без разрешённого нативного доступа (через опцию --enable-native-access) приводили к выбросу IllegalCallerException. В JDK 24 поведение было смягчено для большей согласованности с традициями JNI: теперь нелегальный нативный доступ в FFM API вызывает предупреждения вместо исключений. Чтобы вернуть прежнее поведение, можно использовать следующую комбинацию опций:
java --enable-native-access=Module1,... --illegal-native-access=deny ...Чтобы подготовить ваш код к предстоящим изменениям, рекомендуется запускать существующий код с опцией --enable-native-access=deny. Это поможет выявить участки кода, которые требуют нативного доступа.
Заключение
Приверженность Java Platform принципу целостности очевидна: это подтверждают как предложения по улучшению JDK, так и изменения в ее релизах. Внедряя современные безопасные альтернативы и постепенно отказываясь от небезопасных функций, платформа неизменно движется к соответствию best practices в разработке программного обеспечения.

Присоединяйтесь к русскоязычному сообществу разработчиков на Spring Boot в телеграм - Spring АйО, чтобы быть в курсе последних новостей из мира разработки на Spring Boot и всего, что с ним связано.
