Сломать объект с помощью финализации

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

Вчера перевели в статус Candidate новый JEP 421: Deprecate Finalization for Removal. Путь к удалению механизма финализации из Java начался в Java 9, когда метод Object.finalize() был впервые объявлен deprecated. Рано или поздно механизм исчезнет из Java, поэтому если вы его используете, самое время задуматься об альтернативах. Однако статья не об этом.


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


Вот для примера возьмём стандартный библиотечный класс HashSet. Внутри него объявлено приватное поле map, потому что HashSet — это обёртка над HashMap. Поле инициализируется в конструкторе и после этого не меняется. Предположим, мы хотим сломать HashSet и записать в это поле null. В старые добрые времена, когда все друг другу доверяли, можно было сделать так:


HashSet<String> set = new HashSet<>();
Field map = HashSet.class.getDeclaredField("map");
map.setAccessible(true);
map.set(set, null);

Однако если включена строгая инкапсуляция, этот код упадёт с исключением вида


java.lang.reflect.InaccessibleObjectException: Unable to make field private transient java.util.HashMap java.util.HashSet.map accessible: module java.base does not "opens java.util" to unnamed module @682a0b20

Строгая инкапсуляция с Java 16 включена по дефолту, а с Java 17 её нельзя выключить вообще, только давать явные разрешения конкретным модулями через --add-opens. Да, у нас всё ещё есть лазейка в виде sun.misc.Unsafe из модуля jdk.unsupported. Мы можем сделать вот так:


HashSet<String> set = new HashSet<>();
Field map = HashSet.class.getDeclaredField("map");
Field unsafeField = Unsafe.class.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
unsafe.putObject(set, unsafe.objectFieldOffset(map), null);

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


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


Обычно если выполнение конструктора завершается исключением, то мы считаем, что объект никто не видит. Однако если объект содержит непустой метод finalize(), то он регистрируется для финализации до выполнения конструктора. Если конструктор завершился ошибочно, объект всё равно остался в куче, пусть на него и нету ссылок. А значит, сборщик мусора до него доберётся и добавит в очередь финализации, и тогда выполнится finalize(), который может оживить объект. Конечно, у HashSet нет своего метода finalize(), но ничего не мешает объявить его у наследника.


Уронить конструктор HashSet несложно, достаточно нарушить предусловие. Например, конструируя от коллекции, передать туда null. В итоге имеем:


AtomicReference<HashSet<String>> ref = new AtomicReference<>();
try {
  new HashSet<String>(null) {
    @Override
    protected void finalize() {
      ref.set(this);
    }
  };
} catch (NullPointerException e) {
}
while (ref.get() == null) {
  System.gc();
}
HashSet<String> set = ref.get();

Мы игнорируем NullPointerException, который вывалится из конструктора и вызываем сборку мусора пока finalize() не выполнится и не заполнит ссылку ref. В итоге мы получаем недоконструированный объект HashSet с нарушенным инвариантом.


В данном случае это несильно помогает что-нибудь сломать. Результирующий HashSet будет просто кидать NullPointerException на любую операцию. Однако могут быть и другие классы, экземпляры которых в недоинициализированном виде могут позволить сделать интересные вещи, которые нельзя сделать так просто. Как-то не хочется об этом постоянно думать, если вы разрабатываете особо безопасную библиотеку.


В общем, finalize позволяет делать грязные вещи не хуже Unsafe. Не используйте его и выкашивайте из кодовой базы. И на всякий случай объявляйте свои классы final (или sealed с Java 17), чтобы их не наследовал кто попало.

Источник: https://habr.com/ru/post/586994/


Интересные статьи

Интересные статьи

Привет! На связи zmax505, я бы хотел рассказать о своём опыте по созданию проекта по MOBA игре Dota 2. Мы с iory разработали модель машинного обучения, которая анализиру...
Подождите… что, что? Да, я уже слышал подобную реакцию на мое предложение использовать Kubernetes для построения кластеров Kubernetes.Но для автоматизации облачной инфрас...
Помните как некто cnlohr запустил передачу ТВ сигнала на ESP8266? Недавно мне попалось к просмотру это видео, стало интересно как это возможно и выяснил что автор видео разогнал ча...
werf — наша Open Source-утилита для сборки и деплоя приложений. Сегодня мы с радостью сообщаем, что werf научилась работать в распределенном режиме, начиная с версии v1.1.10 (дост...
В одной из предыдущих статей цикла про гипервизор Proxmox VE мы уже рассказывали, как выполнять бэкап штатными средствами. Сегодня покажем, как для этих же целей использовать отличн...