openjdk.ruOpenJDK на русском

JEP draft: Integrity by Default

Целостность по умолчанию

AuthorsRon Pressler, Alex Buckley, & Mark Reinhold
ОтветственныйRon Pressler
ТипInformational
ОбластьSE
СтатусDraft
Связан сJEP 261: Module System
JEP 260: Encapsulate Most Internal APIs
JEP 396: Strongly Encapsulate JDK Internals by Default
JEP 403: Strongly Encapsulate JDK Internals
JEP 451: Prepare to Disallow the Dynamic Loading of Agents
JEP 498: Warn upon Use of Memory-Access Methods in sun.misc.Unsafe
JEP 471: Deprecate the Memory-Access Methods in sun.misc.Unsafe for Removal
JEP 500: Prepare to Make Final Mean Final
JEP 472: Prepare to Restrict the Use of JNI
Создан2023/04/13 16:06
Обновлён2025/11/20 01:24
Задача8305968

Аннотация

Разработчики ожидают, что их код и данные защищены от нежелательного или неразумного использования. Однако в платформе Java есть небезопасные API, которые могут подорвать это ожидание и тем самым нанести ущерб корректности, сопровождаемости, масштабируемости, безопасности и производительности приложений. В дальнейшем мы ограничим небезопасные API так, чтобы по умолчанию библиотеки, фреймворки и инструменты не могли ими пользоваться. Авторы приложений смогут переопределить это поведение по умолчанию.

Что такое целостность?

Оксфордский словарь английского языка (Oxford English Dictionary) определяет «integrity» (целостность) как «состояние цельности и неразделённости; состояние прочности конструкции».

В контексте компьютерной программы целостность означает, что конструкции, из которых мы строим программу, — и в конечном счёте сама программа — одновременно цельны и прочны. Такие конструкции, будь то низкоуровневые средства языка, например циклы for, или компоненты более высокого уровня, например классы или модули, имеют и спецификации, и реализации. Поэтому целостность требует от вычислительной конструкции двух вещей:

  • Её спецификация должна говорить всё, что нужно сказать, чтобы эффективно использовать конструкцию (цельность), и

  • Её реализация должна удовлетворять её спецификации (прочность).

В более привычных терминах мы говорим, что вычислительная конструкция обладает целостностью тогда и только тогда, когда её спецификация полна, а её реализация корректна относительно этой спецификации.

Например, спецификация массивов Java говорит, что к массиву можно обращаться только в пределах границ, заданных при его создании. Это ограничение гарантирует JVM, которая выбрасывает исключение при его нарушении.

Спецификация массивов Java содержит и много других утверждений: например, что длина массива никогда не меняется, что первый элемент массива всегда имеет индекс ноль и что обращение к элементу массива после присваивания этому элементу некоторого значения возвращает ровно то же значение (с поправкой на конкурентность). JVM гарантирует все эти утверждения — следовательно, массивы корректны. Более того, вместе эти утверждения охватывают всё, что нам нужно знать, чтобы рассуждать о любом конкретном использовании массивов, — следовательно, массивы полны. Нам не нужно гадать, например, не увеличит ли массив незаметно все свои элементы в полночь каждой второй среды, потому что его спецификация ничего не говорит ни о полуночи, ни о средах, а на деле из его спецификации следует, что такая абсурдная ситуация невозможна. Таким образом, мы можем сказать, что массивы Java обладают целостностью.

(Разумеется, у целостности есть практические пределы: JVM не может помешать нативному коду, внешним отладчикам или космическим лучам изменить содержимое массива. Говоря здесь о целостности, мы имеем в виду целостность в контексте платформы Java.)

Платформа Java содержит не только массивы, но и много других полезных конструкций — как в языке, так и во встроенных библиотеках. У всех этих конструкций есть и спецификации, и реализации, которые в совокупности дают спецификацию и реализацию самой платформе. Естественно, мы стремимся к тому, чтобы платформа в целом обладала целостностью: её спецификация говорит всё, что нужно сказать, чтобы эффективно рассуждать о её использовании (полнота), а её реализация ведёт себя в соответствии со спецификацией (корректность). Целостность платформы позволяет нам рассуждать о корректности нашего собственного кода, отталкиваясь от спецификаций конструкций платформы.

Преимущества целостности

На целостности платформы Java основаны многие её ключевые преимущества.

  • Платформа предписывает, что переменные, поля и массивы инициализируются до использования, поэтому начальное состояние программы чётко определено.

  • Платформа предписывает автоматическое управление памятью, поэтому в программе никогда не возникают ошибки использования после освобождения.

  • Язык Java и виртуальная машина Java (Java Virtual Machine) специфицированы так, чтобы гарантировать типобезопасность, поэтому программа не может выполнять недопустимые операции над данными, например обращаться с String как с Socket.

  • API платформы (начиная с Java 20) не позволяет произвольно останавливать потоки, поэтому многопоточная программа никогда не видит объекты в несогласованном состоянии.

Без целостности мы не можем полагаться ни на одно из этих ценных свойств.

Целостность через инкапсуляцию

Язык Java предоставляет встроенные конструкции, позволяющие строить собственные конструкции на всё более высоких уровнях абстракции, скрывая лишние детали. Мы объединяем операторы в методы, методы и поля — в классы, классы — в пакеты, пакеты — в модули и, наконец, модули — в целые программы.

Абстракция позволяет нам контролировать сложность программы: мы можем показать, что реализация конструкции более высокого уровня соответствует её спецификации, рассуждая исключительно на основе спецификаций конструкций более низкого уровня, на которых она построена; нет необходимости учитывать детали реализации низкоуровневых конструкций или спецификации каких-либо других конструкций. Точно так же пользователям конструкции более высокого уровня, рассуждая о собственном коде, достаточно обращаться к спецификациям этой конструкции и других используемых ими конструкций; нет необходимости учитывать детали реализации высокоуровневой конструкции или спецификации каких-либо других конструкций. В конечном счёте с помощью таких рассуждений мы в принципе можем показать, что вся программа соответствует своей спецификации.

Чтобы всё это работало, наши высокоуровневые конструкции сами должны обладать целостностью: они должны быть полными и корректными. Ключевой инструмент для этого — инкапсуляция.

Например, допустим, мы хотим построить абстракцию счётчика, значение которого всегда чётное и никогда не нечётное. Представим, что в языке Java нет инкапсуляции, так что ко всем полям и методам можно обращаться откуда угодно, как если бы всё было public. Мы могли бы объявить класс EvenCounter так:

/**
 * Specification:
 *   - value() initially returns zero
 *   - incrementByTwo() increments value() by two
 *   - decrementByTwo() decrements value() by two
 *   - value() is always even, never odd
 */
/*public*/ final class EvenCounter {
    /*public*/ int x = 0;
    /*public*/ int value() { return x; }
    /*public*/ void incrementByTwo() { x += 2; }
    /*public*/ void decrementByTwo() { x -= 2; }
}

Мы легко можем показать, что класс EvenCounter сам по себе соответствует своей спецификации, следовательно, он корректен. Однако его спецификация не полна: она не говорит всего, что нужно сказать, чтобы эффективно использовать класс. Дело в том, что код вне класса может в любой момент записать в поле x нечётное число, из-за чего метод value() нарушит спецификацию класса. Чтобы показать, что использование класса корректно, мы должны проанализировать каждую строку всей программы и убедиться, что никакой код вне класса не изменяет это поле. Вместо простого локального рассуждения о каждом таком использовании требуется сложное глобальное рассуждение. Получается, будто спецификация класса EvenCounter включает дополнительное требование о том, что

*   - No code external to this class modifies the x field

В настоящем языке Java, разумеется, в такой сложности нет необходимости, поскольку язык предоставляет конструкции инкапсуляции — ключевые слова private и public, — которые позволяют защитить данные от намеренного или ненамеренного изменения.

public final class EvenCounter {
    private int x = 0;
    public int value() { return x; }
    public void incrementByTwo() { x += 2; }
    public void decrementByTwo() { x -= 2; }
}

Здесь мы используем ключевое слово private, чтобы защитить поле x от внешнего доступа. Ключевое слово private обладает целостностью: его спецификация говорит, что приватное поле может изменять только код того же класса, и компилятор Java и JVM гарантируют выполнение этой спецификации во всей программе. Поэтому, сделав поле x приватным, мы избавляемся от необходимости анализировать всю программу, рассуждая о корректности любого использования класса EvenCounter. Иными словами, достаточно локального рассуждения о каждом таком использовании. Таким образом, исходная спецификация класса, приведённая выше, полна; мы уже знаем, что класс корректен относительно этой спецификации, следовательно, класс обладает целостностью.

Абстракция позволяет нам создавать вычислительные конструкции более высокого уровня; инкапсуляция позволяет наделить целостностью эти конструкции, а в конечном счёте и целые программы. Это даёт огромную пользу.

  • Корректность — корректность программы может опираться на целостность класса EvenCounter, в частности на то, что значение в экземпляре всегда чётное. Например, приложение могло бы использовать EvenCounter для учёта деловой активности, где каждой покупке должна соответствовать продажа, так что число транзакций получается чётным. Инкапсуляция, наделяющая класс целостностью, гарантирует, что код вне класса не сможет подорвать корректность.

  • Сопровождаемость — инкапсуляция защищает код по мере его развития. Мы считаем, что поля и методы private — это детали реализации, которые можно безопасно менять, не ломая клиентов. Клиентский код не может обратиться к приватному полю класса EvenCounter, поэтому мы можем менять его как угодно, пока сохраняем корректность. Например, мы могли бы переименовать поле или изменить его тип на Integer. Инкапсуляция даёт классам целостность, необходимую для независимого развития их внутреннего устройства.

  • Масштабируемость — инкапсуляция является краеугольным камнем крупномасштабного программирования, потому что обеспечивает целостность, которая делает возможным локальное рассуждение о поведении вычислительных конструкций. Программы можно строить из независимо разработанных компонентов, которые взаимодействуют только через свои публичные API и ведут себя в соответствии со своими спецификациями. Благодаря этому масштабироваться как совокупность независимых взаимодействующих компонентов могут не только отдельные программы, но и вся экосистема Java.

  • Безопасность — инкапсуляция необходима для любой надёжной защиты. Допустим, например, что класс в JDK ограничивает доступ к чувствительной операции:

    if (isAuthorized())
        doSensitiveOperation();

    Это ограничение надёжно, только если мы можем гарантировать, что doSensitiveOperation() вызывается исключительно после успешной проверки isAuthorized(). Если мы объявим doSensitiveOperation() как private в объявляющем его классе, то будем знать, что никакой код в любом другом классе не может вызвать этот метод напрямую; иными словами, объявляющий класс обладает целостностью в отношении этого метода. Поэтому рецензентам кода достаточно убедиться, что всем вызовам метода внутри объявляющего класса предшествует проверка isAuthorized(); весь остальной код программы они могут не учитывать.

  • Производительность — в среде выполнения Java многие оптимизации могут использовать целостность, которую обеспечивает инкапсуляция. Например, JVM может выполнять оптимизации свёртки констант, когда определяет, что значение поля private никогда не меняется. Более того, инструмент вроде jlink мог бы удалять неиспользуемые методы private на этапе компоновки, чтобы уменьшить размер образа и время загрузки классов.

Подрыв целостности

Инкапсуляция — ключевой инструмент обеспечения целостности. На ней основаны корректность, сопровождаемость, масштабируемость, безопасность и производительность. Однако в JDK есть четыре API, которые позволяют её обойти.

  • Метод AccessibleObject::setAccessible(boolean) в пакете java.lang.reflect делает возможной глубокую рефлексию, то есть рефлексию над полями и методами без учёта границ инкапсуляции. Этот метод появился в Java 1.2 для поддержки сериализации и десериализации объектов, но на практике любой код может использовать его, чтобы вызывать методы private любого класса, читать и записывать поля private любого объекта и даже записывать поля final.

  • Класс sun.misc.Unsafe содержит методы, которые могут обращаться к методам и полям private и записывать поля final, подобно глубокой рефлексии.

  • Java Native Interface (JNI) позволяет нативному коду взаимодействовать с объектами Java без учёта границ инкапсуляции. Нативный код может обращаться к методам и полям private и записывать поля final, подобно глубокой рефлексии.

  • Instrumentation API позволяет компонентам, называемым агентами, изменять байт-код любого метода в любом классе. Например, агент может переписать метод incrementByTwo класса EvenCounter так, чтобы перед увеличением значения x записывать его старое значение в файл.

Мы называем эти API небезопасными, поскольку они нарушают целостность конструкций инкапсуляции языка Java и тем самым нарушают целостность не только самой платформы, но и каждого компонента и каждой программы, построенных поверх неё. Например, поле private в объекте EvenCounter может быть изменено извне класса с помощью глубокой рефлексии, sun.misc.Unsafe или нативного кода, в результате чего его значение станет нечётным, что нарушит спецификацию класса. Агент может переопределить методы public класса EvenCounter так, чтобы они увеличивали поле private на единицу вместо двух, что опять же приведёт к нечётному значению.

Из-за того что конструкции инкапсуляции языка лишены целостности, становится невозможно рассуждать о корректности программы локально. Чтобы показать, что использование инкапсулированного компонента корректно, мы должны проанализировать каждый класс в пути классов, в пути модулей или загружаемый динамически и либо исключить использование небезопасных API, либо убедиться, что их использование не нарушает спецификацию компонента. Такой анализ непрактичен, поэтому любой код, корректность которого зависит от чётности объектов EvenCounter, может работать неправильно, и любой клиент этого кода может работать неправильно, и так далее.

Даже если библиотека использует небезопасный API с благими намерениями и явно не нарушает спецификацию какого-либо другого компонента, она всё равно может сделать возможными нарушения спецификации в приложении, которое её использует. Например, библиотека сериализации JSON может десериализовать объект EvenCounter, устанавливая значение приватного поля объекта с помощью глубокой рефлексии в обход публичного API EvenCounter. Само по себе это не нарушает спецификацию класса EvenCounter. Однако если приложение не позаботится о явной проверке того, что входные данные JSON не содержат нечётного числа, то чтение таких данных приведёт к нечётному EvenCounter. Библиотека сериализации явно не нарушает спецификацию EvenCounter, но, обходя защитный механизм EvenCounter — его инкапсуляцию, — она делает его уязвимым для косвенных нарушений спецификации.

Особенно серьёзна эта проблема для компонентов, критичных для безопасности. Уязвимость в библиотеке, использующей небезопасный API, ставит под угрозу целостность каждого компонента приложения и может позволить злоумышленнику манипулировать входными данными приложения так, чтобы подорвать безопасность.

Небезопасные API в JDK нарушают целостность не только тех конструкций языка, которые связаны с инкапсуляцией. В частности, конструкции для доступа к массивам и объектам специфицированы так, чтобы обеспечивать безопасность памяти: к массиву нельзя обратиться за его границами, а к объекту — после того, как занимаемая им память освобождена. Мы десятилетиями полагались на безопасность памяти платформы Java, но небезопасные API могут её нарушить, что приводит к неопределённому поведению и даже к аварийному завершению JVM.

  • JNI позволяет выполнять нативный код, который может нарушать безопасность памяти. Нативный код также может создать байтовый буфер, обёртывающий произвольные участки памяти, а значит, любой код на Java, который обращается к этому буферу, может вызвать неопределённое поведение.

  • Foreign Function & Memory API (FFM, JEP 454) позволяет выполнять нативный код, который может нарушать безопасность памяти. FFM API также позволяет коду на Java создавать сегмент памяти, обёртывающий произвольные участки памяти. Любой код на Java, обращающийся к такому сегменту, может вызвать неопределённое поведение.

  • Класс sun.misc.Unsafe содержит методы, которые могут читать и записывать произвольные участки памяти как в куче JVM, так и вне её. Так, к массиву можно обратиться за его границами, а к памяти объекта — спустя долгое время после того, как её освободил сборщик мусора: классическая ошибка использования после освобождения (use-after-free).

Целостность платформы Java — а значит, корректность, сопровождаемость, масштабируемость, безопасность и производительность наших программ — требует, чтобы мы не допускали обхода инкапсуляции и нарушения безопасности памяти. Как совместить это с наличием небезопасных API, которые призваны дать разработчикам библиотек, фреймворков и инструментов особые сверхспособности для редких ситуаций, когда решить задачу иначе невозможно? Ответ таков: мы должны перейти к целостности по умолчанию.

Целостность по умолчанию

Целостность по умолчанию означает, что каждая конструкция платформы Java обладает целостностью, если только это не отменено явно на самом верхнем уровне программы. То есть разработчик приложения может отказаться от отдельных видов целостности в пределах этого приложения, а разработчик библиотеки, фреймворка или инструмента — не может. Например, разработчик приложения может настроить среду выполнения Java так, чтобы разрешить библиотеке сериализации использовать небезопасные API, сознательно соглашаясь на потерю целостности, потому что функциональность библиотеки незаменима. Однако без такого явного разрешения эта библиотека не может сама по себе нарушить ни один аспект целостности платформы или приложения.

Начиная с JDK 9 мы постепенно продвигаем платформу Java к целостности по умолчанию. Для этого мы избирательно ослабляем или ставим под контроль возможность небезопасных API подрывать целостность. У этой работы три направления.

  • Код JDK строго инкапсулирован в модулях. По умолчанию глубокая рефлексия не может обойти строгую инкапсуляцию.

  • Небезопасные API, которые являются стандартными в платформе Java, ограничены. По умолчанию код на Java не может обойти инкапсуляцию, переопределяя методы с помощью Instrumentation API, или нарушить инкапсуляцию или безопасность памяти, вызывая нативный код через JNI или FFM.

  • Нестандартные небезопасные API удаляются, когда появляются стандартные API на замену. API на замену спроектированы так, что по умолчанию не могут подорвать целостность.

Строгая инкапсуляция: противоядие от глубокой рефлексии

В JDK 9 в язык Java были добавлены модули. Модуль — это набор пакетов, предназначенных для совместной работы и повторного использования. Если пакет экспортирован, его публичные элементы можно использовать за пределами модуля; если пакет не экспортирован, его публичные элементы можно использовать только внутри модуля.

Модули обеспечивают строгую инкапсуляцию: рефлексия из кода вне модуля не может обращаться к приватным элементам какого-либо класса внутри модуля. То есть метод setAccessible соблюдает границы модулей. Например, если публичный класс EvenCounter объявлен в явном модуле, то его приватное поле x нельзя изменить с помощью глубокой рефлексии, инициированной кодом вне модуля.

Ограничения стандартных небезопасных API

Большинство небезопасных API — setAccessible, JNI, FFM и Instrumentation — по-прежнему поддерживаются в платформе Java. Хотя код приложений редко использует их напрямую, они необходимы относительно небольшому числу библиотек, фреймворков и инструментов, основную функциональность которых невозможно реализовать иначе. Среди примеров:

  • фреймворки для модульного тестирования и внедрения зависимостей (DI), которые с помощью глубокой рефлексии обращаются к полям и методам private классов приложения;

  • библиотеки сериализации, которые с помощью глубокой рефлексии обращаются к полям private классов приложения;

  • библиотеки для создания mock-объектов, которые переопределяют методы классов приложения с помощью Instrumentation API;

  • библиотеки-обёртки над нативным кодом, которые вызывают методы native через JNI или вызывают дескрипторы методов для нисходящих вызовов (downcall method handles) через FFM; и

  • инструменты мониторинга производительности приложений (Application Performance Monitoring, APM), которые с помощью агентов внедряют в код приложения журналирование и счётчики производительности.

Компонент, использующий небезопасный API, нарушает целостность платформы Java: он создаёт возможность обхода инкапсуляции или нарушения безопасности памяти и тем самым делает спецификацию платформы неполной. Если у платформы нет целостности, то нет целостности и у построенных на ней компонентов, и у самих приложений. Политика целостности по умолчанию закрепляет идею о том, что разработчик библиотеки, фреймворка или инструмента не может в одностороннем порядке решить нарушить целостность, используя небезопасный API. Это право — и соответствующая ответственность — принадлежит исключительно разработчику приложения (или, возможно, тому, кто развёртывает приложение, по совету разработчика). Разработчик приложения отвечает перед конечными пользователями за поведение приложения; разработчики библиотек, фреймворков и инструментов, напротив, не отвечают.

Мы не можем считать само по себе включение в приложение библиотеки или фреймворка, использующих небезопасные API, согласием разработчика приложения на нарушение целостности. Разработчик может не знать, что компонент использует небезопасный API. Разработчик может даже не знать о наличии компонента, поскольку тот может быть косвенной зависимостью, отстоящей от самого приложения на несколько уровней. Поэтому разработчик приложения должен явно настроить среду выполнения Java так, чтобы разрешить выбранным компонентам использовать небезопасные API. Если среда выполнения не настроена соответствующим образом, использование небезопасного API приводит к выбросу исключения. Иными словами, использование небезопасных API по умолчанию ограничено.

Разрешить использование небезопасных API в среде выполнения Java можно с помощью различных параметров командной строки:

  • --add-opens позволяет коду в указанных модулях использовать setAccessible для элементов private других модулей. Связанный параметр --add-exports позволяет коду в указанных модулях обращаться к элементам public пакетов, которые иначе не экспортируются.

  • --enable-native-access позволяет коду в указанных модулях использовать FFM для создания произвольных сегментов памяти, а также для поиска и вызова нативного кода. В будущем этот параметр также потребуется для использования JNI.

  • -javaagent позволяет агенту использовать Instrumentation API. Связанный параметр -XX:+EnableDynamicAgentLoading позволяет инструментам загружать агенты динамически.

Разработчики приложений могут задать эти параметры несколькими способами:

  • передать их напрямую средству запуска java в скрипте, который запускает приложение,

  • передать их средству запуска java косвенно, задав переменную окружения JDK_JAVA_OPTIONS,

  • поместить их в файл аргументов, который скрипт или конечный пользователь передаёт средству запуска java (например, java @config),

  • поместить соответствующие записи манифеста в исполняемый JAR-файл приложения (Add-Opens, Add-Exports, Enable-Native-Access и Launcher-Agent-Class; записи манифеста, соответствующей -agentlib, не существует)

  • настроить среду выполнения программно, как описано в разделе Переход к целостности по умолчанию.

Независимо от способа задания эти параметры настраивают среду выполнения Java при её запуске, что позволяет JVM определить, каким образом будет подорвана целостность и какие оптимизации следует включить или отключить. Эти параметры также позволяют разработчикам приложений легко проверить использование небезопасных API и понять, какие риски оно несёт для корректности, сопровождаемости, масштабируемости, безопасности и производительности. Если ни один из этих параметров не используется, разработчик приложения может быть уверен, что ни приложение, ни его зависимости не нарушают целостность платформы, зависимостей приложения или самого приложения.

Адаптация к ограничениям небезопасных API

Большинство небезопасных API платформы Java использовались годами, прежде чем их признали небезопасными, поэтому ограничивать их без предупреждения нецелесообразно: приложения неожиданно перестали бы работать. Кроме того, настройка среды выполнения Java для разрешения использования небезопасных API библиотеками, фреймворками и инструментами не входит в привычную практику разработчиков. Чтобы предупредить разработчиков приложений о необходимости настраивать среду выполнения Java, мы ограничиваем использование уже существующего небезопасного API постепенно:

  • В одном из выпусков JDK API можно использовать как обычно.
  • В более позднем выпуске JDK API по-прежнему можно использовать, но при этом выводится предупреждение. В предупреждении указана библиотека, которая использовала API, а само использование названо «недопустимым» («illegal»). Также в предупреждении объясняется, как настроить среду выполнения Java, чтобы разрешить такое использование, например с помощью --add-opens. Предупреждение выводится только при первом использовании API конкретным модулем; дальнейшее использование кодом того же модуля новых предупреждений не вызывает.
  • В конечном счёте, в ещё одном выпуске JDK, API по умолчанию использовать нельзя. Вызов API приводит к выбрасыванию исключения, если среда выполнения Java не настроена так, чтобы разрешить такое использование.

Обычно этот процесс занимает несколько лет. В это время JDK предоставляет временный параметр командной строки, которым разработчик приложения может «ускорить» или «замедлить» процесс. Например, у --add-opens был временный аналог --illegal-access. У временного параметра три значения:

  • allow (или permit) — разрешить использование API без предупреждений.
  • warn — разрешить использование API с предупреждениями.
  • deny — запретить использование API и выбрасывать исключение.

Как правило, по умолчанию временный параметр имеет значение allow в первом выпуске JDK, затем warn в более позднем выпуске JDK и в конечном счёте deny. Разработчики приложений могут в любой момент «ускорить» процесс, установив параметр в deny и тем самым воспроизведя долгосрочное поведение, запланированное для среды выполнения Java. «Замедлить» процесс, напротив, можно лишь в ограниченной мере: когда значение по умолчанию — warn, параметр можно установить в allow, но когда значением по умолчанию становится deny, параметр можно установить только в warn, но не в allow. Когда значение по умолчанию уже какое-то время равно deny, временный параметр удаляется.

Удаление нестандартных небезопасных API

Класс sun.misc.Unsafe содержит методы, которые выполняют разнообразные низкоуровневые операции без каких-либо проверок безопасности. Начиная с JDK 9 мы добавляем стандартные API, которые предлагают более безопасную замену этой функциональности. Например, низкоуровневые операции с объектами в куче JVM теперь можно выполнять безопаснее с помощью API VarHandle, а операции с данными в памяти вне кучи — с помощью API MemorySegment из FFM.

Некоторые элементы sun.misc.Unsafe, у которых теперь есть стандартная замена в API, мы уже пометили как Deprecated for Removal (устаревший, будет удалён), а позже удалили. Мы продолжим делать это в будущих выпусках. В конечном счёте мы пометим как Deprecated for Removal сам sun.misc.Unsafe, а затем удалим его.

Переход к целостности по умолчанию

Во многих ситуациях библиотеки, фреймворки и инструменты могут избавить разработчиков приложений от части работы по настройке среды выполнения Java.

  • Разработчики фреймворков внедрения зависимостей могут попросить разработчиков приложений предоставить доступ к полям и методам приложения с модификатором private прямо в коде. Один из способов — попросить разработчиков приложений открыть пакеты своих модулей для модуля фреймворка, поместив, например,

    opens com.example.app to org.framework;

    в объявления своих модулей. Глубокая рефлексия может обращаться к любому элементу открытого пакета, даже к элементам с модификатором private. При необходимости фреймворк может передать свои права доступа другому компоненту с помощью Module::addOpens.

    Способ лучше — попросить разработчиков приложений создавать объекты поиска для дескрипторов методов (method-handle lookup objects) и передавать их фреймворку, например:

    Framework.grantAccess(MethodHandles.lookup());

    Объект поиска даёт доступ к элементам с модификатором private, доступным коду, который его создал, поэтому фреймворк может с помощью объекта поиска выполнять глубокую рефлексию над кодом приложения, не требуя открывать какие-либо пакеты приложения.

  • Библиотеки сериализации стали причиной многих уязвимостей безопасности, поскольку обращались к закрытым полям классов приложения с помощью глубокой рефлексии. Вообще говоря, библиотекам не следует сериализовать и десериализовать объект без участия его класса. Такие объекты, как строки, Records (записи), перечисления и коллекции, легко сериализовать и десериализовать, поскольку их классы предоставляют методы доступа и конструкторы с модификатором public. Для других объектов библиотеки сериализации должны определять протоколы, по которым классы могут раскрывать своё состояние при сериализации, а при десериализации принимать и проверять новое состояние. Чтобы это работало, разработчикам приложений может потребоваться предоставить доступ к классам в неэкспортируемых пакетах, открыв пакеты или передав объекты поиска.

    Некоторые классы уже сами отвечают за свою сериализацию и десериализацию, реализуя интерфейс java.io.Serializable. Библиотеки сериализации могут этим воспользоваться, вызывая методы writeObject и readObject таких классов через класс sun.reflect.ReflectionFactory, который поддерживается для этой цели. В JDK 24 в этот класс были добавлены новые методы.

    В долгосрочной перспективе мы ожидаем, что платформа Java предложит более совершенную сериализацию.

  • Фреймворки модульного тестирования и библиотеки для создания mock-объектов могут интегрироваться с инструментами сборки, такими как Maven и Gradle, чтобы настраивать среду выполнения Java автоматически. Например, инструменты сборки могли бы запускать тесты с параметрами, необходимыми для обхода инкапсуляции (--add-opens, --add-exports), изменения модулей (--patch-module) и установки агентов (-javaagent).

Более сложные фреймворки и приложения, которым нужно управлять инициализацией и начальной загрузкой среды выполнения и/или компонентов, могут программно разрешить коду использовать небезопасные API:

  • Приложение с собственным нативным модулем запуска, который загружает JVM через JNI invocation API, может программно передать JVM такие параметры, как --add-opens, --enable-native-access или -javaagent.

  • Фреймворк или приложение, которое динамически загружает модули компонентов, может разрешить компоненту использовать небезопасные API с помощью методов addExports, addOpens и enableNativeAccess из API ModuleLayer.Controller.

Целостность за пределами платформы Java

Код на Java может с помощью стандартных средств платформы выходить за пределы среды выполнения Java и нарушать целостность. Например, код на Java может изменить содержимое class-файла в файловой системе до загрузки класса. Однако в вопросах целостности действует хороший принцип:

Целостность компонентов лучше всего обеспечивает инфраструктура, которая их предоставляет.

За целостность файловой системы и её содержимого отвечает операционная система, а не среда выполнения Java. ОС или, если это уместно, контейнер уровня ОС всегда следует настраивать так, чтобы защитить целостность файлов и памяти среды выполнения Java, а также целостность файлов приложения, независимо от того, какие меры принимает среда выполнения Java для защиты собственной целостности и целостности выполняемого ею приложения.

Почему сейчас?

Экосистема Java прекрасно обходилась без строгой инкапсуляции и ограничений на небезопасные API почти три десятилетия. Почему же мы сейчас переходим к целостности по умолчанию, что добавляет работы некоторым разработчикам библиотек, фреймворков, инструментов и приложений?

Ответ в том, что за последние годы изменились и JDK, и среда, в которой работают приложения на Java.

  • Корректность — исторически среда выполнения Java могла обеспечивать целостность многих низкоуровневых конструкций, поскольку они были реализованы в нативном коде, вне досягаемости небезопасных API. Однако всё большая часть самой среды выполнения Java пишется или переписывается на Java. Это значит, что всё большая часть целостности платформы зависит от целостности кода на Java, которую небезопасные API могут нарушить.

  • Сопровождаемость — чтобы добавлять новые возможности и не утонуть в сопровождении, нам нужно иметь возможность удалять устаревшие компоненты из JDK и свободно рефакторить его реализацию. К сожалению, со временем различные библиотеки, фреймворки и инструменты стали зависеть от некоторых внутренних API JDK, считая их стабильными. В результате экосистеме становилось всё труднее переходить на новые выпуски. Мы могли либо смириться с замедлением развития платформы, либо ещё один, последний раз причинить трудности при миграции — в JDK 17, строго инкапсулировав внутренние API JDK. (Мы разделили JDK на модули в JDK 9, но полностью включили строгую инкапсуляцию только в JDK 17.)

  • Безопасность — в связи с предстоящим удалением Security Manager (JEP 411) нам нужна строгая инкапсуляция, чтобы можно было создавать надёжные уровни безопасности, защищённые от вмешательства другого кода, как было показано выше. Без строгой инкапсуляции любой уязвимый код в приложении может поставить под угрозу безопасность.

  • Производительность — растёт потребность в сокращении времени запуска, времени прогрева и размера образа, что важно для развёртывания приложений на Java в современных облачных средах. Некоторые способы достижения этих целей требуют, чтобы классы не менялись со временем, например в результате переопределения через Instrumentation API. Другие важные оптимизации, такие как свёртка констант, требуют целостности таких конструкций, как поля с модификатором final, чтобы их значения действительно были неизменными и не могли быть модифицированы.

Коротко: использование внутренних API JDK вызывало серьёзные проблемы при миграции, в текущих условиях не было практичного механизма, обеспечивающего надёжную безопасность, а новые требования невозможно было выполнить. Несмотря на ценность небезопасных API для библиотек, фреймворков и инструментов, сохраняющееся отсутствие целостности недопустимо. Решение — строгая инкапсуляция и ограничение небезопасных API по умолчанию.