JEP 486: Permanently Disable the Security Manager
Окончательное отключение Security Manager
| Автор | Sean Mullan & Alex Buckley |
| Ответственный | Sean Mullan |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 24 |
| Компонент | security-libs / java.security |
| Обсуждение | security dash dev at openjdk dot java dot net |
| Трудоёмкость | L |
| Длительность | L |
| Связан с | JEP 411: Deprecate the Security Manager for Removal |
| Рецензенты | Alan Bateman, Mark Reinhold, Stuart Marks |
| Одобрен | Alan Bateman |
| Создан | 2024/08/19 23:43 |
| Обновлён | 2025/09/02 15:58 |
| Задача | 8338625 |
Аннотация
Security Manager уже много лет не является основным средством защиты клиентского кода на Java, его редко использовали для защиты серверного кода, а его поддержка обходится дорого. Поэтому в Java 17 мы объявили его Deprecated for Removal (устаревший, будет удалён) в рамках JEP 411 (2021). Следующим шагом к удалению Security Manager мы изменим спецификацию платформы Java так, чтобы разработчики не могли его включить, а другие классы платформы на него не ссылались. Это изменение не затронет подавляющее большинство приложений, библиотек и инструментов. Security Manager API мы удалим в одном из будущих выпусков.
Цели
-
Убрать возможность включать Security Manager при запуске среды выполнения Java (
java -Djava.security.manager ...). -
Убрать возможность устанавливать Security Manager во время работы приложения (
System.setSecurityManager(...)). -
Упростить сопровождение сотен классов JDK, которые сейчас передают решения о доступе к ресурсам в Security Manager.
-
Изменить спецификацию Security Manager API так, чтобы все её реализации вели себя так, как будто Security Manager никогда не включён.
-
Сохранить Security Manager API в этом выпуске, чтобы у тех, кто сопровождает зависящий от него код, было время от него отказаться.
Что не является целью
- Целью не является замена какой-либо функциональности Security Manager, в частности возможности изолировать код на Java в песочнице или перехватывать вызовы API платформы Java.
Мотивация
Security Manager входит в платформу Java с самого первого выпуска. Он основан на принципе минимальных привилегий: по умолчанию коду не доверяют, поэтому он не может обращаться к таким ресурсам, как файловая система или сеть, а разработчики оказывают доверие конкретному коду, выдавая ему разрешения на доступ к конкретным ресурсам. Теоретически это может защитить машины и приложения от кода, который содержит случайные уязвимости или был написан со злым умыслом. На практике, однако, схема разрешений настолько сложна, что Security Manager всегда был отключён по умолчанию, а используется он крайне редко.
Хотя Security Manager по умолчанию отключён, модель минимальных привилегий делает библиотеки платформы Java чрезвычайно сложными. От работы с сетью, ввода-вывода и JDBC до XML, AWT и Swing — библиотеки должны реализовывать модель минимальных привилегий на случай, если Security Manager включён:
-
Более 1000 методов должны проверять наличие разрешения на доступ к ресурсам, когда Security Manager включён. Например, конструкторы класса
FileOutputStreamобращаются к Security Manager, который по сложному алгоритму определяет, разрешён ли доступ. -
Более 1200 методов должны повышать свои привилегии, когда Security Manager включён. Например, если у приложения нет разрешения на чтение файлов, но оно вызывает
java.time.LocalDateTime.now(), то кодjava.timeдолжен заявить собственное, более сильное разрешение, чтобы прочитать внутренний файл базы данных часовых поясов JDK.
OpenJDK Core Libraries Group тратит много времени и сил на рецензирование каждого изменения любого из этих методов. Каждый новый API нужно проектировать, а его реализацию тщательно проверять с учётом модели минимальных привилегий. Однако Security Manager на деле включает лишь ничтожно малое число приложений. Хуже того, по нашему опыту, большинство из них не глядя выдают своему коду все разрешения и тем самым лишаются преимуществ модели минимальных привилегий.
Поэтому в Java 17 мы объявили Security Manager устаревшим, с последующим удалением, в рамках JEP 411 (2021). Помимо того что Security Manager API и связанные с ним API были окончательно объявлены устаревшими, мы также изменили JDK так, чтобы при включении Security Manager выводились предупреждения. Эти изменения должны были подготовить пользователей и разработчиков к удалению Security Manager в одном из будущих выпусков.
Объявление Security Manager устаревшим почти ни на что не повлияло
JDK 17 и последующие выпуски получили широкое распространение: разработчики и компании переходили на них с JDK 8 и JDK 11. Мы почти не видели в экосистеме Java обсуждений предупреждений, которые эти выпуски выводят при включении Security Manager. Это говорит о том, что для современных разработчиков на Java Security Manager почти полностью утратил значение. По-видимому, мы были правы, когда написали в JEP 411, что
«За четверть века, прошедшую с появления Security Manager, он получил слабое распространение»,
и
«Итак, интереса к разработке современных приложений на Java с Security Manager практически нет».
После выхода JDK 17 разработчики некоторых из немногих фреймворков и инструментов, поддерживавших Security Manager, отказались от его поддержки; среди них Derby, Ant, SpotBugs и Tomcat. Разработчики Jakarta EE отменили требование, чтобы EE-приложения поддерживали Security Manager. Нам не известно ни об одном новом проекте, который поддерживает Security Manager.
Дальнейшие шаги
Подавляющее большинство приложений, библиотек и инструментов не требуют Security Manager, не рекомендуют Security Manager, не используют Security Manager и не работают, если Security Manager использует другой код. Экосистеме Java пора сделать следующий шаг и полностью отказаться от Security Manager.
Поэтому мы изменим спецификацию Security Manager так, чтобы разработчики не могли его включить, и изменим спецификации других библиотек платформы Java так, чтобы они не передавали ему решения о доступе к ресурсам. Минимальную версию класса java.lang.SecurityManager мы оставим для совместимости с немногими приложениями, библиотеками и инструментами, которые всё ещё его используют. Этот класс мы удалим в одном из будущих выпусков.
Удаление Security Manager повысит безопасность Java
Мы считаем, что подавляющее большинство разработчиков на Java предпочли бы, чтобы OpenJDK Core Libraries Group сосредоточилась на практических средствах безопасности, нужных приложениям, работающим в интернете. Описанные выше изменения спецификации позволят нам удалить реализацию Security Manager из кодовой базы JDK вместе с тысячами проверок разрешений и повышений привилегий. Благодаря этому у участников останется больше времени и сил на другую работу, например:
- реализацию новых протоколов, таких как TLS 1.3 и HTTP/3,
- реализацию современных, более стойких криптографических алгоритмов, таких как HSS/LMS, SHA-3, RSASSA-PSS и EdDSA,
- объявление устаревшими и отключение слабых криптографических протоколов и алгоритмов, а также
- введение криптографических API для инкапсуляции ключей и формирования ключей, которые станут основой для поддержки постквантовых криптографических алгоритмов.
Большинство современных угроз безопасности связаны с вредоносными данными, от которых Security Manager защищает плохо. Удаление реализации Security Manager освободит время и силы участников для средств безопасности, которые напрямую защищают от вредоносных данных, например:
-
Более безопасная сериализация — десериализация подразумевает интерпретацию потока данных, который мог быть создан со злым умыслом. В 2017 году в Java 9 появились фильтры десериализации, с помощью которых разработчики могут вообще не допустить десериализации вредоносных данных. В более долгосрочной перспективе уже ведётся работа над более совершенным подходом к сериализации.
-
Более строгая обработка XML — XML-документы могут ссылаться на определения типа документа (Document Type Definitions, DTD), расположенные где угодно в интернете, из-за чего JDK открывает сетевые соединения с недоверенными машинами. В JDK 23 разработчики приложений могут ограничить обработку XML, запуская среду выполнения Java с
java -Djava.xml.config.file=...и указывая файл конфигурации, который запрещает исходящие соединения.
Изоляция кода на Java в песочнице
Изоляция в песочнице — это возможность выполнять часть кода на Java с иными разрешениями, чем остальной код. Разрешения для каждого фрагмента кода определяются политикой безопасности, соблюдение которой обеспечивает Security Manager. О коде, выполняемом с ограниченными разрешениями, например о недоверенном или потенциально враждебном коде, часто говорят, что он работает в песочнице.
Исторически Security Manager использовался для изоляции апплетов в песочнице; мы никогда не рекомендовали его для изоляции целых приложений. Приложения на Java следует изолировать так же, как нативные приложения, — с помощью технологий вне JDK, таких как контейнеры, гипервизоры, и механизмов ОС, таких как macOS App Sandbox или функция seccomp в Linux. Как и Security Manager, эти технологии могут ограничивать использование приложениями локальных и удалённых ресурсов; например, они могут не дать коду обратиться к сети, чтобы вывести данные наружу. Однако, в отличие от Security Manager, они широко распространены, и их относительно легко освоить и эффективно применять.
Перехват вызовов API платформы Java
Небольшое число приложений использовали Security Manager не для соблюдения политики безопасности, а как средство перехвата вызовов API платформы Java. В отсутствие политики безопасности и проверки разрешений вызовы API платформы Java сами по себе уже не представляют проблемы для безопасности. У действительно вредоносного кода есть бесчисленное множество способов обойти перехват вызовов API через Security Manager. Тем не менее некоторым приложениям перехват полезен, в частности для блокировки вызовов таких методов, как System::exit.
Мы спроектировали, создали прототипы и оценили различные механизмы, которые приложение могло бы использовать вместо Security Manager для перехвата вызовов API платформы Java. Мы пришли к выводу, что сценарии использования слишком широки, а требования слишком размыты, чтобы оправдать введение такого механизма. OpenJDK Core Libraries Group не готова бессрочно поддерживать по всему JDK произвольное количество точек перехвата API с нечётко определёнными требованиями.
В большинстве случаев оказалось, что задачи, для которых, казалось бы, нужен перехват, можно адекватно решить вне JDK — с помощью таких приёмов, как изменение исходного кода, статический анализ и переписывание кода или динамическое переписывание кода при загрузке класса с помощью агента. Пример агента, который с помощью динамического переписывания кода перехватывает вызовы System::exit, приведён в приложении.
Security Manager в старых выпусках Java
Security Manager останется доступным во всех выпусках до JDK 24. Те, кто развёртывает приложения и с осторожностью переходит на новые выпуски, потому что превыше всего ценит стабильность, вряд ли когда-либо перейдут на JDK 24, а значит, изменения Security Manager в JDK 24 и последующих выпусках их никогда не затронут.
Описание
В JDK 24 мы:
- уберём возможность включать Security Manager при запуске,
- уберём возможность устанавливать собственный Security Manager во время выполнения и
- сделаем Security Manager API нефункциональным, заранее, до удаления этого API в одном из будущих выпусков.
Включение Security Manager в JDK 24 является ошибкой
В JDK 24 нельзя включить Security Manager при запуске и нельзя установить собственный Security Manager во время выполнения.
-
Включение Security Manager при запуске является ошибкой, например так:
$ java -Djava.security.manager -jar app.jar $ java -Djava.security.manager="" -jar app.jar $ java -Djava.security.manager=allow -jar app.jar $ java -Djava.security.manager=default -jar app.jar $ java -Djava.security.manager=com.foo.CustomSM -jar app.jarПри такой попытке JVM сообщает об ошибке и завершает работу:
Error occurred during initialization of VM java.lang.Error: A command line option has attempted to allow or enable the Security Manager. Enabling a Security Manager is not supported. at java.lang.System.initPhase3(java.base@24/System.java:2067)Это сообщение об ошибке нельзя подавить или свести к предупреждениям, которые выводились в JDK с 17 по 23.
(Пять показанных выше вызовов java -D... присваивают системному свойству
java.security.managerсоответственно пустую строку, пустую строку, строкуallow, строкуdefaultи имя класса собственного Security Manager.) -
Запрет на установку собственного Security Manager во время выполнения ошибкой не является. Например, так:
$ java -jar app.jar $ java -Djava.security.manager=disallow -jar app.jarПри запуске не выводится ни предупреждений, ни сообщений об ошибке, и приложение работает без Security Manager, как и раньше.
(Значение
java.security.managerпо умолчанию равноdisallowначиная с JDK 18, поэтомуjava -jar app.jarозначает то же, что иjava -Djava.security.manager=disallow -jar app.jar.) -
Установка Security Manager во время выполнения вызовом
System::setSecurityManagerявляется ошибкой. При такой попытке JVM выбрасываетUnsupportedOperationExceptionс сообщениемSetting a Security Manager is not supported
Как определить, включает ли приложение Security Manager
Если вы не уверены, включает ли ваше приложение Security Manager, выяснить это можно несколькими способами:
-
Проверьте скрипты или документацию. Выясните, запускается ли приложение с разрешённым или включённым Security Manager через параметры командной строки и требует ли оно установки и настройки файлов политик.
-
Запустите приложение на одной из версий JDK с 17 по 23 и поищите в консоли предупреждения о том, что Security Manager помечен как Deprecated (устаревший) и будет удалён в одном из будущих выпусков.
-
Запустите приложение на одной из версий JDK с 17 по 23 с параметром командной строки
-Djava.security.manager=disallow. Если приложение устанавливает собственный Security Manager через методSystem::setSecurityManager, JVM выброситUnsupportedOperationException. -
С помощью инструмента
jdeprscanиз JDK с 17 по 23 найдите использования API Security Manager, помеченных как Deprecated, напримерSystem::setSecurityManagerилиjava.security.Policy::setPolicy.
Отключение функциональности Security Manager API
Security Manager API состоит из следующих частей:
- методы класса
java.lang.SecurityManager, - методы классов
AccessController,AccessControlContext,PolicyиProtectionDomainпакетаjava.security, а также - методы
getSecurityManagerиsetSecurityManagerклассаjava.lang.System.
Мы не удаляем эти методы из Java 24, а ограничиваем их поведение. В зависимости от метода они будут возвращать null или false, передавать запрос вызывающего кода дальше или безусловно выбрасывать SecurityException или UnsupportedOperationException. Эти изменения поведения совместимы с большинством библиотек, использующих Security Manager API, но несовместимы с очень небольшим числом библиотек: подробности см. в рекомендациях для сопровождающих библиотек. Полный перечень изменений поведения доступен здесь. Мы удалим Security Manager API в одном из будущих выпусков.
Помимо изменения поведения API, мы внесём следующие изменения:
-
Удалим системный файл политик
conf/security/java.policy. -
Будем игнорировать системные свойства, относящиеся к Security Manager, в частности
java.security.policyиjdk.security.filePermCompat. Полный список затронутых системных свойств мы опишем в документации позже. -
Будем игнорировать свойства безопасности, относящиеся к Security Manager, в частности
policy.provider,package.accessиpackage.definition. Полный список затронутых свойств безопасности мы опишем в документации позже. -
Будем игнорировать параметры
accessиpolicyсистемного свойстваjava.security.debug, поскольку они больше неприменимы. Как использовать это системное свойство, см. в разделе Troubleshooting Security документа Security Developer's Guide.
Изменения в других частях API платформы Java
Примерно для 1000 конструкторов и методов платформы в спецификации указано, что они выбрасывают SecurityException, если Security Manager включён, а соответствующие разрешения не выданы. Они относятся к 264 классам, 73 пакетам и 25 модулям. Например, у java.base есть 640 методов, для которых указано, что они выбрасывают SecurityException.
В Java 24 мы пересмотрим спецификации всех таких конструкторов и методов и уберём упоминания SecurityException, поскольку это исключение теперь никогда не выбрасывается. Полный список пересмотренных конструкторов и методов доступен здесь.
Вот пример изменения спецификации конструктора в java.io.FileOutputStream (зачёркнутый текст удалён):
public FileOutputStream(String name)
throws FileNotFoundException
Creates a file output stream to write to the file with the specified
name. A new FileDescriptor object is created to represent this file
connection.
First, if there is a security manager, its checkWrite method is
called with name as its argument.
If the file exists but is a directory rather than a regular file,
does not exist but cannot be created, or cannot be opened for any
other reason then a FileNotFoundException is thrown.
Implementation Requirements:
Invoking this constructor with the parameter name is equivalent
to invoking new FileOutputStream(name, false).
Parameters:
name - the system-dependent file name.
Throws:
FileNotFoundException - if the file exists but is a directory
rather than a regular file, does not exist but cannot be created,
or cannot be opened for any other reason
SecurityException - if a security manager exists and its checkWrite
method denies write access to the file.
See Also:
SecurityManager.checkWrite(java.lang.String)
Рекомендации для сопровождающих библиотек, поддерживающих Security Manager
Небольшое число библиотек было спроектировано так, чтобы использовать Security Manager, если он включён. Обычно такие библиотеки применяют две идиомы:
-
вызов
System::getSecurityManager, чтобы проверить, включён ли Security Manager, и если да, вызовSecurityManager::checkPermission, чтобы проверить, следует ли разрешить или запретить операцию:SecurityManager sm = System.getSecurityManager(); if (sm != null) { sm.checkPermission(...); } -
вызов
AccessController::doPrivileged, чтобы выполнить код с разрешениями, отличными от разрешений вызывающего кода:SomeReturnValue v = AccessController.doPrivileged(() -> { ... return theResult; });
В JDK 24, где Security Manager никогда не бывает включён, методы System::getSecurityManager и AccessController::doPrivileged ведут себя так же, как в JDK 17 при выключенном Security Manager:
System::getSecurityManagerвозвращаетnull, а- шесть методов
AccessController::doPrivilegedсразу выполняют переданное действие.
Соответственно, небольшое число библиотек, вызывающих эти методы, будет работать на JDK 24 без изменений. Однако мы настоятельно рекомендуем, чтобы новые выпуски этих библиотек не вызывали эти методы, которые мы удалим в одном из будущих выпусков.
В JDK 24 эти методы всегда реализуют среду выполнения, запрещающую доступ ко всем ресурсам. Поэтому методы ведут себя иначе, чем в JDK 17:
AccessController::checkPermissionвсегда выбрасываетAccessControlException,Policy::setPolicyвсегда выбрасываетUnsupportedOperationException.Permission::checkGuardвсегда выбрасываетSecurityException, а- методы
SecurityManager::check*всегда выбрасываютSecurityException,
Полную информацию об изменениях поведения Security Manager API см. здесь.
Дальнейшая работа
Удаление связанных API
В Java 24 мы не удаляем из API платформы Java ни классов, ни методов. В одном из будущих выпусков мы удалим API Security Manager, которые пометили как Deprecated в Java 17. Кроме того, в будущих выпусках мы можем пометить как Deprecated и удалить другие классы и методы в пакетах java.lang и java.security.
-
Сейчас мы не помечаем
SecurityExceptionкак Deprecated, потому что это исключение используется в других частях JDK в ситуациях, не связанных с Security Manager, хотя в его спецификации сказано: «Выбрасывается Security Manager для указания на нарушение безопасности». Вот примеры такого (неправильного) использования:-
java.lang.ClassLoader::defineClassвыбрасываетSecurityException, если имя определяемого класса начинается с «java.». -
java.lang.reflect.Constructor::setAccessibleвыбрасывает его, если вызван на объектеConstructorдля конструктораjava.lang.Class. -
java.util.java.JarInputStreamвыбрасывает его, если подписанные записи JAR подписаны неправильно.
После повторного анализа этих случаев неправильного использования мы можем пометить
SecurityExceptionкак Deprecated в одном из будущих выпусков. -
-
В одном из будущих выпусков мы пометим как Deprecated
Permissionи связанные классы, такие какBasicPermission,PermissionCollectionиPermissions, а также подклассыPermissionвне пакетаjava.security, такие какjava.lang.RuntimePermission,java.net.NetPermissionиjava.lang.reflect.ReflectPermission. -
В одном из будущих выпусков мы пометим как Deprecated
PrivilegedAction,PrivilegedExceptionActionиPrivilegedActionException. Мы не пометили эти классы как Deprecated в Java 17, потому что они встречаются в сигнатурах методовjavax.security.auth.Subject— класса, не связанного с Security Manager. В Java 18 мы добавили вjavax.security.auth.Subjectметоды на замену, не использующие классыPrivileged*. Со временем мы удалим и старые методы, и классыPrivileged*.
Удаление или пересмотр связанных возможностей
Ряд ранних возможностей платформы Java проектировался исходя из концепции мобильных объектов. Они использовали сериализацию для передачи кода и данных между JVM и предполагали, что приложения будут включать Security Manager для защиты от злонамеренно сериализованных объектов. Эта концепция так и не получила распространения. Учитывая фундаментальные недостатки сериализации и минимальное использование Security Manager, мы либо уже удалили эти возможности, либо планируем это сделать:
-
JMX Management Applets («m-lets») появились в Java 5 и позволяли динамически загружать и выполнять удалённые MBean при включённом Security Manager. M-lets практически не использовались. Мы пометили m-let API как Deprecated for Removal в Java 20 и удалили его в Java 23.
-
JNDI поддерживает восстановление объектов, сериализованных в базу данных LDAP (RFC 2713). Эта возможность JNDI по умолчанию отключена начиная с Java 6, но её можно включить с помощью системных свойств. Безопасное использование этой возможности зависит от включения Security Manager, поэтому после удаления Security Manager использовать её безопасно будет невозможно. Соответственно, в одном из будущих выпусков мы удалим эту возможность вместе с возможностью удалённой загрузки классов в поставщике службы JNDI RMI Registry.
-
RMI поддерживает динамическую загрузку кода, но она включена, только когда включён Security Manager. Эта возможность RMI по умолчанию отключена с 2013 года. После удаления Security Manager использовать эту возможность больше нельзя. Мы можем удалить её в одном из будущих выпусков.
Помимо этого, API javax.xml позволяют встраивать исходный код на Java непосредственно в документы XSLT и XPath в виде функций расширения. Эта возможность включена по умолчанию, но исторически отключалась при работе с Security Manager. В одном из будущих выпусков мы отключим её по умолчанию в рамках более широкой работы по ужесточению обработки XML.
Тестирование
Широта Security Manager API и глубина его поддержки в кодовой базе JDK отражаются примерно в 4000 тестов, разработанных для него начиная с JDK 1.0. Они делятся на три категории:
-
Тесты, напрямую проверяющие функциональность Security Manager, например правильность применения разрешений.
-
Тесты, связанные с уязвимостями безопасности. Обычно они проверяют, что конкретные эксплойты, позволявшие недоверенному коду (например, апплетам) выйти из песочницы, больше невозможны.
-
Тесты соответствия, проверяющие, что реализация Security Manager соответствует спецификации Security Manager API.
Окончательное отключение Security Manager сделает эти тесты ненужными, поскольку эта функциональность больше не будет поддерживаться, а понятия песочницы больше не будет. С учётом тестов мы удалим более 50 000 строк кода.
Альтернативы
Многочисленные методы check* в Security Manager API всегда выбрасывают исключение, чтобы не разрешать безусловно операции, которые раньше требовали проверки разрешений и, следовательно, могли быть не разрешены. Это может быть неудобно для сопровождающих приложений, которым, возможно, придётся принять корректирующие меры. Альтернативой было бы сделать так, чтобы эти методы всегда завершались успешно, но тогда приложение могло бы работать небезопасно, никак не уведомляя об этом сопровождающего.
Риски и допущения
-
В JDK 24 попытка включить Security Manager в командной строке сразу приведёт к сообщению об ошибке, и приложение не запустится. Если приложение не запускается, могут отказать зависящие от него системы и пострадать бизнес-процессы. Мы предполагаем, что сопровождающие приложений могут отреагировать на ошибку: изменить командные строки
javaтак, чтобы в них не было параметра-Djava.security.manager, и снизить риски безопасности с помощью других механизмов.(Когда мы удаляем возможность из JDK, мы по сложившейся практике отклоняем все связанные с ней параметры командной строки. Это касается и использования
java -D...для установки системного свойства, напримерjava.security.manager. Так, после удаления Extension Mechanism в JDK 9 установка системного свойстваjava.ext.dirsприводила к ошибке. Это вынуждает сопровождающих приложений быстро убирать устаревшие параметры и позволяет избежать ситуаций, когда JDK запускается с запутанным или вводящим в заблуждение набором параметров.) -
Существует риск, что фреймворки, использующие API
javax.security.auth, по-прежнему вызывают методы классаSubjectв статусе Deprecated, а именноdoAsиgetSubject. Мы перевели эти методы в статус Deprecated в Java 17 и 18, потому что в их сигнатурах используются классы Security Manager API в статусе Deprecated. Замены дляdoAsиgetSubjectмы добавили в Java 18. ПосколькуgetSubjectвыбрасываетUnsupportedOperationExceptionначиная с Java 23, мы предполагаем, что разработчики фреймворков уже знают о переводе этих методов в статус Deprecated и переходят на замены, например HADOOP-19212.
Приложение
Агент — это программа на Java, которая может изменять код приложения во время его работы. Для этого агенты преобразуют байт-код методов при загрузке классов или переопределяют уже загруженные классы.
Ниже приведён агент, который не даёт коду вызывать System::exit. Агент объявляет метод premain, который JVM запускает до метода main приложения. Этот метод регистрирует трансформер, преобразующий class-файлы при их загрузке из пути классов или пути модулей. Трансформер заменяет каждый вызов System.exit(int) на throw new RuntimeException("System.exit not allowed").
Трансформер читает и записывает байт-код в class-файлах с помощью Class-File API, который в JDK 23 находится в статусе Preview (предварительная версия). Подробности см. в описании пакета java.lang.classfile. Исходный код агента импортирует Class-File API и другие API Java с помощью Module Import Declarations (объявления импорта модулей), которые в JDK 23 тоже находятся в статусе Preview.
import module java.base;
import module java.instrument;
public class BlockSystemExitAgent {
/*
* Before the application starts, register a transformer of class files.
*/
public static void premain(String agentArgs, Instrumentation inst) {
var transformer = new ClassFileTransformer() {
@Override
public byte[] transform(ClassLoader loader,
String className,
Class<?> classBeingRedefined,
ProtectionDomain protectionDomain,
byte[] classBytes) {
if (loader != null && loader != ClassLoader.getPlatformClassLoader()) {
return blockSystemExit(classBytes);
} else {
return null;
}
}
};
inst.addTransformer(transformer, true);
}
/*
* Rewrite every invokestatic of System::exit(int) to an athrow of RuntimeException.
*/
private static byte[] blockSystemExit(byte[] classBytes) {
var modified = new AtomicBoolean();
ClassFile cf = ClassFile.of(ClassFile.DebugElementsOption.DROP_DEBUG);
ClassModel classModel = cf.parse(classBytes);
Predicate<MethodModel> invokesSystemExit =
methodModel -> methodModel.code()
.map(codeModel ->
codeModel.elementStream()
.anyMatch(BlockSystemExitAgent::isInvocationOfSystemExit))
.orElse(false);
CodeTransform rewriteSystemExit =
(codeBuilder, codeElement) -> {
if (isInvocationOfSystemExit(codeElement)) {
var runtimeException = ClassDesc.of("java.lang.RuntimeException");
codeBuilder.new_(runtimeException)
.dup()
.ldc("System.exit not allowed")
.invokespecial(runtimeException,
"<init>",
MethodTypeDesc.ofDescriptor("(Ljava/lang/String;)V"),
false)
.athrow();
modified.set(true);
} else {
codeBuilder.with(codeElement);
}
};
ClassTransform ct = ClassTransform.transformingMethodBodies(invokesSystemExit, rewriteSystemExit);
byte[] newClassBytes = cf.transform(classModel, ct);
if (modified.get()) {
return newClassBytes;
} else {
return null;
}
}
private static boolean isInvocationOfSystemExit(CodeElement codeElement) {
return codeElement instanceof InvokeInstruction i
&& i.opcode() == Opcode.INVOKESTATIC
&& "java/lang/System".equals(i.owner().asInternalName())
&& "exit".equals(i.name().stringValue())
&& "(I)V".equals(i.type().stringValue());
}
}
Агент нужно упаковать в JAR-файл и указать его в параметре -javaagent при запуске приложения:
# Compile the agent into the agentclasses directory, enabling preview features for JDK 23
$ javac --enable-preview --release 23 -d agentclasses BlockSystemExitAgent.java
# Create JAR file manifest in agent.mf
$ cat > agent.mf << EOF
Premain-Class: BlockSystemExitAgent
Can-Retransform-Classes: true
EOF
# Create the agent JAR (Note there is a period after -C agentclasses)
$ jar --create --file=BlockSystemExitAgent.jar --manifest=agent.mf -C agentclasses .
# Run application with the agent JAR, enabling preview features for JDK 23
$ java --enable-preview -javaagent:BlockSystemExitAgent.jar -jar app.jar