JEP 451: Prepare to Disallow the Dynamic Loading of Agents
Подготовка к запрету динамической загрузки агентов
| Автор | Ron Pressler & Alex Buckley |
| Ответственный | Ron Pressler |
| Тип | Feature |
| Область | SE |
| Статус | Closed / Delivered |
| Выпуск | 21 |
| Компонент | hotspot / svc |
| Обсуждение | jigsaw dash dev at openjdk dot org, serviceability dash dev at openjdk dot org |
| Рецензенты | Alan Bateman, Dan Heidinga |
| Одобрен | Mark Reinhold, Serguei Spitsyn |
| Создан | 2023/04/18 09:39 |
| Обновлён | 2026/07/23 17:19 |
| Задача | 8306275 |
Аннотация
Выдавать предупреждения при динамической загрузке агентов в работающую JVM. Эти предупреждения должны подготовить пользователей к будущему выпуску, в котором динамическая загрузка агентов будет по умолчанию запрещена, чтобы повысить целостность по умолчанию. Инструменты обслуживания, которые загружают агенты при запуске, ни в одном выпуске не будут вызывать предупреждений.
Цели
-
Подготовиться к будущему выпуску JDK, в котором загрузка агентов в работающую JVM будет по умолчанию запрещена.
-
Пересмотреть баланс между обслуживаемостью (serviceability), которая предполагает ситуативные изменения работающего кода, и целостностью (integrity), которая исходит из того, что работающий код не изменяется произвольным образом.
-
Гарантировать, что большинство инструментов — которым не нужно загружать агенты динамически — не будут затронуты.
-
Привести возможность динамической загрузки агентов в соответствие с другими так называемыми «суперспособностями», такими как глубокая рефлексия.
Что не является целью
-
Цель не в том, чтобы запретить загрузку агентов при запуске JVM с помощью параметров командной строки
-javaagentили-agentlib, и не в том, чтобы выдавать предупреждения при таком использовании. -
Цель не в том, чтобы объявить устаревшими или удалить части Attach API, которые динамически загружают агенты; цель только в том, чтобы подготовиться к тому, что их использование будет по умолчанию запрещено.
-
Цель не в том, чтобы изменить части Attach API, которые позволяют инструментам обслуживания подключаться к работающей JVM для мониторинга и управления. Такие инструменты, как
jcmdиjconsole, продолжат работать без параметров командной строки и без предупреждений.
Мотивация
Агенты в платформе Java
Агент — это компонент, который может изменять код приложения во время работы приложения. Агенты появились в JDK 5 вместе с Java Platform Profiling Architecture как способ, с помощью которого инструменты, в частности профилировщики, могут инструментировать классы. Это означает изменять код класса так, чтобы он генерировал события, которые потребляет инструмент вне приложения, но в остальном не менять поведение кода. Агенты делают это либо преобразуя классы во время загрузки классов, либо переопределяя классы, загруженные ранее. Их можно писать на Java с помощью API java.lang.instrument («Java-агенты») или в нативном коде с помощью JVM Tool Interface («агенты JVM TI»).
Агенты проектировались в расчёте на безобидное инструментирование, при котором добавление инструментирования не влияет на поведение приложения. Однако опытные разработчики нашли сценарии использования, такие как аспектно-ориентированное программирование, которые произвольным образом изменяют поведение приложения. Кроме того, ничто не мешает агенту изменять код вне приложения, например код самого JDK. Чтобы гарантировать, что владелец приложения одобрил использование агентов, JDK 5 требовал указывать агенты в командной строке параметрами -javaagent или -agentlib и загружал агенты сразу при запуске. Это было явным предоставлением привилегий со стороны владельца приложения.
Обслуживаемость и динамически загружаемые агенты
Обслуживаемость — это возможность для системного оператора отслеживать, наблюдать, отлаживать приложение и диагностировать проблемы во время его работы. Превосходная обслуживаемость платформы Java давно служит поводом для гордости.
Для поддержки инструментов обслуживания в JDK 6 появился Attach API. Attach API не является частью платформы Java, а представляет собой API JDK, поддерживаемый для внешнего использования. Он позволяет инструменту, запущенному с соответствующими привилегиями операционной системы, подключиться к работающей JVM, локальной или удалённой, и взаимодействовать с этой JVM, чтобы наблюдать за её работой и управлять ею. Attach API включён по умолчанию, но его можно отключить параметром -XX:+DisableAttachMechanism в командной строке.
Примеры инструментов, использующих Attach API:
-
Инструменты мониторинга и управления, такие как
jcmdиjconsole, которые отслеживают метрики приложения и изменяют конфигурацию. Например, если приложение использует APIjava.util.logging, оператор может с помощьюjconsoleдинамически изменить уровень логирования. Эти инструменты используют специализированные протоколыjcmd, JMX и JDK Flight Recorder (JFR). -
Отладчики, которым требуется встроенный в JVM агент, включаемый при запуске параметром
-agentlib:jdwp. Затем они взаимодействуют с агентом по некоторому каналу IPC, но также могут пользоваться возможностями Attach API. -
Профилировщики и, в более общем смысле, инструменты Application Performance Monitoring (APM), которые используют агент, загружаемый при запуске, чтобы инструментировать код приложения так, чтобы он генерировал события JFR для потребления JDK Mission Control или другими клиентами.
Attach API также позволяет инструменту динамически загрузить агент в работающую JVM. Эта возможность поддерживает сложные сценарии использования, связанные с изменением произвольного кода на лету. Примеры инструментов, которые динамически загружают агенты:
-
Профилировщики, которые подключаются к работающей JVM и динамически загружают агент, чтобы инструментировать код приложения.
-
Инструменты ситуативной диагностики, которые читают и изменяют состояние приложения во время выполнения. Динамически загруженный агент либо использует JVM TI для исследования состояния работающей программы, либо преобразует и инструментирует загруженные классы.
(Очень опытные разработчики иногда исправляют ошибки в производственных средах: пишут агент, который исправляет ошибочный код, и загружают этот агент динамически. Однако это не поддерживаемый сценарий использования, и он никогда не рекомендовался. Возможность агента переопределять загруженные классы подвержена ограничениям, поэтому возможность исправлять ошибки таким способом ограничена. Более того, агент не может сохранить сделанные им изменения, поэтому перезапуск приложения отменит изменение.)
Динамически загружаемые агенты дают инструментам обслуживания суперспособность изменять работающее приложение. Однако подключение инструмента инициирует человек-оператор с соответствующими учётными данными операционной системы. Этот человек в контуре управления даёт одобрение на изменение приложения, поэтому на инструменты обслуживания не распространяются ограничения целостности, налагаемые на другой код. Поэтому динамическая загрузка агентов по умолчанию разрешена, хотя в JDK 9 и более поздних версиях её можно запретить параметром -XX:-EnableDynamicAgentLoading в командной строке.
Агенты и библиотеки
Несмотря на концептуальное разделение ответственности между библиотеками и инструментами, некоторые библиотеки предоставляют функциональность, которая опирается на доступную агентам суперспособность изменять код. Например, библиотека для создания mock-объектов может переопределять классы приложения, чтобы обойти инварианты бизнес-логики, а библиотека для тестирования методом белого ящика может переопределять классы JDK так, чтобы рефлексия над полями private была всегда разрешена. Чтобы получить эти возможности, библиотека может использовать агент, который получает от JVM всемогущий объект Instrumentation и передаёт его библиотеке.
Некоторые такие библиотеки гарантируют, что владелец приложения одобряет изменение приложения, требуя указывать агент библиотеки в командной строке параметром -javaagent. Примером библиотеки, которая так делала, был Quasar — ранний прототип того, что позже стало Virtual Threads (виртуальные потоки) (JEP 444).
Другие библиотеки используют более сомнительный подход и получают возможности без одобрения владельца приложения. Они используют Attach API, чтобы незаметно подключиться к JVM, в которой работают, и динамически загрузить агенты, фактически выдавая себя за инструменты обслуживания. Чтобы сохранить целостность, JDK 9 и более поздние выпуски по умолчанию не позволяют коду подключаться к текущей JVM. (Такие подключения можно разрешить с помощью -Djdk.attach.allowAttachSelf=true.) Однако эта мера оказалась недостаточной: некоторые библиотеки теперь запускают вторую JVM, которая подключается к первой и загружает агент туда, рядом с библиотекой.
Если библиотека использует агенты, чтобы незаметно переопределять классы JDK, обходя тем самым строгую инкапсуляцию, то ни на один из инвариантов, обеспечиваемых строгой инкапсуляцией, нельзя полагаться. Целостность утрачена.
К целостности по умолчанию
Чтобы обеспечить целостность, нам нужны более строгие меры, которые не позволят библиотекам злоупотреблять динамически загружаемыми агентами. К сожалению, мы не нашли простого и автоматического способа отличить инструмент обслуживания, который динамически загружает агент, от библиотеки, которая динамически загружает агент. Предоставить полную свободу инструментам означало бы предоставить полную свободу библиотекам, что равносильно отказу от целостности по умолчанию.
Поэтому мы предлагаем требовать, чтобы динамическую загрузку агентов одобрял владелец приложения, — так же, как начиная с JDK 5 мы требуем, чтобы загрузку агентов при запуске одобрял владелец приложения. Это изменение приблизит платформу Java к долгосрочной концепции целостности по умолчанию. На практике владельцу приложения придётся явно разрешить динамическую загрузку агентов с помощью параметра командной строки.
К счастью, большинство инструментов обслуживания не зависят от динамически загружаемых агентов. Однако запрет динамической загрузки агентов по умолчанию означает, что приёмы ситуативной диагностики, требующие динамической загрузки агента, больше не будут работать «из коробки». Если возникнет необходимость динамически загрузить агент, JVM нужно будет перезапустить с соответствующим параметром командной строки, чтобы выразить одобрение владельца приложения.
Последствия этого изменения смягчаются тем, что большинство современных серверных приложений спроектированы с резервированием, поэтому отдельные узлы можно при необходимости перезапустить с этим параметром командной строки. Особые случаи — например, JVM, которую никогда нельзя останавливать для обслуживания, или канареечный процесс новой версии ПО, который находится под пристальным наблюдением, — обычно можно определить заранее, чтобы динамическая загрузка агентов была разрешена с самого начала.
Требование к владельцам приложений одобрять динамически загружаемые агенты позволит экосистеме Java воплотить концепцию целостности по умолчанию, не ограничивая существенно обслуживаемость.
Описание
В JDK 21 динамическая загрузка агентов разрешена, но когда она происходит, JVM выдаёт предупреждение. Например:
WARNING: A {Java,JVM TI} agent has been loaded dynamically (file:/u/bob/agent.jar)
WARNING: If a serviceability tool is in use, please run with -XX:+EnableDynamicAgentLoading to hide this warning
WARNING: If a serviceability tool is not in use, please run with -Djdk.instrument.traceUsage for more information
WARNING: Dynamic loading of agents will be disallowed by default in a future release
Чтобы инструменты могли динамически загружать агенты без предупреждений, пользователи должны запускать приложение с параметром -XX:+EnableDynamicAgentLoading в командной строке.
При запуске с -Djdk.instrument.traceUsage методы API java.lang.instrument при использовании выводят сообщение и трассировку стека. Это помогает выявить библиотеки, которые неправильно используют динамически загружаемые агенты вместо агентов, загружаемых при запуске. Сопровождающим библиотек, которые загружают агенты динамически, рекомендуется обновить документацию и описать, как пользователи могут загружать агенты при запуске; различные варианты развёртывания описаны в java.lang.instrument API.
В одном из будущих выпусков динамическая загрузка агентов будет по умолчанию запрещена. В конфигурации по умолчанию любая попытка использовать Attach API для динамической загрузки агента будет приводить к выбросу исключения:
com.sun.tools.attach.AgentLoadException: Failed to load agent library: \
Dynamic agent loading is not enabled. Use -XX:+EnableDynamicAgentLoading \
to launch target VM.
Чтобы разрешить динамическую загрузку агентов, когда она по умолчанию запрещена, пользователи должны запускать приложение с -XX:+EnableDynamicAgentLoading в командной строке.
Чтобы подготовиться к изменению поведения по умолчанию в будущем выпуске, пользователи JDK 9 или любого более позднего выпуска могут явно запретить динамическую загрузку агентов, запуская приложение с -XX:-EnableDynamicAgentLoading в командной строке.
Эти изменения не затрагивают инструменты, использующие агенты, загружаемые при запуске. Смысл и работа параметра -javaagent, параметра -agentlib и атрибута JAR-файла Launcher-Agent-Class не меняются.
Эти изменения не затрагивают инструменты, которые используют Attach API не для динамической загрузки агента, а для других целей.
Библиотеки не должны загружать агенты динамически. Библиотеки, использующие агент, должны загружать его при запуске с помощью параметров -javaagent/-agentlib.
Историческая справка
Изначально запретить динамическую загрузку агентов по умолчанию было предложено в 2017 году, когда в JDK 9 в платформу добавляли модули. Предложение звучало так:
В одном из будущих выпусков динамическая загрузка агентов JVM TI будет по умолчанию отключена. Чтобы подготовиться к этому изменению, мы рекомендуем приложениям, которые допускают динамические агенты, начать использовать параметр
-XX:+EnableDynamicAgentLoadingи явно разрешать такую загрузку.
В 2017 году было принято общее решение перенести это изменение из JDK 9 в более поздний выпуск, чтобы у разработчиков инструментов было время предупредить своих пользователей. Тем не менее раньше, когда мы усиливали инкапсуляцию, мы выводили предупреждения в предшествующем выпуске, чтобы заранее сообщить о предстоящем изменении. Этот JEP следует той же процедуре.
Риски и допущения
-
Мы предполагаем, что в большинстве сценариев обслуживания используются
jcmd,jconsole, отладчики, JFR и APM-инструменты. Они не загружают агенты динамически, поэтому эти изменения их не затронут. -
Мы предполагаем, что разработчики библиотек, которые динамически загружают агент, обновят свою документацию и попросят владельцев приложений загружать агент при запуске с помощью параметра
-javaagentили разрешить динамическую загрузку агентов параметром-XX:+EnableDynamicAgentLoading.
Дальнейшая работа
-
Продвинутые профилировщики нативного кода используют агенты JVM TI лишь для доступа к внутренним механизмам HotSpot, которые помогают профилировать. При профилировании приложения в продакшене они могут загружать агент динамически. Этот сценарий лучше всего решить, расширив возможности JFR, чтобы он выполнял эту задачу совсем без агента. JFR может взаимодействовать с JIT-компилятором HotSpot и собирать большие пакеты трассировок стека гораздо эффективнее, чем позволяет что-либо, доступное через JVM TI API или через внутренний недокументированный метод
AsyncGetCallTrace, который обычно используют продвинутые профилировщики. -
Некоторые интересные сценарии работы с кодом можно было бы поддержать, если предоставлять библиотекам объект
Instrumentation, соблюдающий инкапсуляцию, напрямую, без участия агента. Тогда библиотека могла бы преобразовывать или переопределять классы в модулях, открытых для модуля этой библиотеки.
Альтернативы
-
По умолчанию выводить предупреждение только при динамической загрузке нативных агентов JVM TI, а возможности динамически загружаемых Java-агентов по умолчанию (то есть когда параметр
-XX:+EnableDynamicAgentLoadingне задан) ограничить так, чтобы при попытке изменить классы в именованных модулях выводилось предупреждение, а классы в безымянных модулях они могли изменять без предупреждения.Этот подход сложнее и не позволяет поддержать существенно больше практически полезных агентов для инструментов. Кроме того, он не мешает Java-агенту использовать JNI, чтобы получить дополнительные возможности.
-
Использовать механизм аутентификации, который отличает инструмент, управляемый человеком, от библиотеки, выдающей себя за инструмент, чтобы по умолчанию разрешать инструменту динамически загружать агент без предупреждения, но выводить предупреждение, когда агент динамически пытается загрузить библиотека.
Мы изучили несколько подходов в этом направлении, но все они либо были сложными, либо требовали специальной настройки в командной строке, а это не уменьшило бы влияние на инструменты, которые динамически загружают агент.