Java
May 2017 6

Хачим IntegerCache в Java 9

Для многих переход на Java 9 выглядит как нечто абстрактное. Давайте переведем это в практическую плоскость одним коротким победоносным примером, который привел в своей статье Питер Варгас [1].

Это статья в жанре «неправильный перевод» с отсебятиной, потому что я художник, я так вижу =) Ссылки на источники – как всегда, в низу текста.

Пять лет назад Питер опубликовал блогпост на венгерском про то, как хакнуть IntegerCache в JDK. Это просто маленький эксперимент рантаймом, не имеющий никакого практического применения кроме повышения эрудиции, понимания как работает reflection, и как устроен класс Integer.

Глядите, генерация реально рандомных чисел зависит от энтропии системы [2]. Некоторые утверждают, что это можно сделать честным броском кубика [3].

image

Другие считают, что на помощь нам придет переопределение тела метода java.math.Random.nextInt().

Для тех кто не в курсе древнего баяна [4]. На хакатоне нидерландского JPoint в 2013 году обсуждалась сборка и изменение OpenJDK. После того, как Roy van Rijn научился собирать его под Windows (как сделать это в 2017 году я писал здесь [5]), он сразу же приступил к делу и сделал свой первый коммит.

Вместо того, чтобы менять ядро OpenJDK (которое всё в нативных кодах, для этого нужно быть доктором наук), он обнаружил, что базовые библиотеки – просто классы на джаве, и они беззащитны против его харизмы. Если заглянуть в [openjdk]/jdk/src/share/classes, можно обнаружить привычные директории-пакеты типа “java.*”, “javax.*” и даже “sun.*”. Поэтому можно грязными сапогами влезть в [openjdk]/jdk/src/share/classes/java/util/Random.java, и сделать очевидное изменение:

public int nextInt() {
  return 14;
}

После пересборки JDK, все вызовы new Random().nextInt() действительно будут возвращать 14.

Но это всё полная фигня. Реальные пацаны знают, что настоящий способ добавить энтропии – это переписать java.lang.Integer.IntegerCache на старте JVM (и ниже мы покажем – как).

Напоминаем, что Integer содержит приватный внутренний класс IntegerCache, содержащий объекты типа Integer, для диапазона от -128 до 127. Когда код боксится в Integer, и имеет значение из этого диапазона, рантайм использует кэш вместо создания нового Integer. Всё это ради оптимизации по скорости, и подразумевая, что в реальных программах числа постоянно укладываются в этот диапазон (взять хотя бы индексацию массивов).

Сайд эффектом этого является известный факт, что оператор сравнения можно использования для сравнения значений интов, пока чиселка находится в указанном диапазоне. Забавно, что такой код (будучи написанным неправильно) обычно работает во всевозможных юнит-тестах (написанных неправильно, чтобы быть последовательными), но свалится при реальном использовании сразу же, как значения выйдут за 128. Автор данного хабропоста недоумевает, почему эта деталь реализации была вытянута на свет божий и поселилась в тестах к собеседованиям, накрепко испортив неоркрепшую детскую психику многим хорошим людям.

Внимание, опасносте. Если похачить IntegerCache через reflection, это может привести к магическим сайд-эффектам и окажет эффект не только на конкретное место, а на всё содержимое этой JVM. То есть, если сервлет поменяет какие-то кусочки кэша, то и всем другим сервлетам в том же Томкате придется несладко. Олсо, мы предупреждали.

Хорошо, давайте возьмем бетку Java 9 и попробуем совершить над ней то же непотребство, которое прокатывало в Java 8. Скопипастим код из статьи Лукаса [2]:

import java.lang.reflect.Field;
import java.util.Random;
  
public class Entropy {
  public static void main(String[] args) 
  throws Exception {
  
    // Вытаскиваем IntegerCache через reflection
    Class<?> clazz = Class.forName(
      "java.lang.Integer$IntegerCache");
    Field field = clazz.getDeclaredField("cache");
    field.setAccessible(true);
    Integer[] cache = (Integer[]) field.get(clazz);
  
    // Переписываем Integer cache
    for (int i = 0; i < cache.length; i++) {
      cache[i] = new Integer(
        new Random().nextInt(cache.length));
    }
  
    // Проверяем рандомность!
    for (int i = 0; i < 10; i++) {
      System.out.println((Integer) i);
    }
  }
}

Как и было обещано, этот код получает доступ к IntegerCache с помощью reflection, и наполняет его случайными значениями. Какая чудесное грязное решение!

Теперь мы запускаем тот же код под Девяткой. Плохие новости для грязных мальчишек, праздника не будет. При попытке унизить её, Девятка реагирует куда серьезней:

Exception in thread "main" java.lang.reflect.InaccessibleObjectException:
  Unable to make field static final java.lang.Integer[]
  java.lang.Integer$IntegerCache.cache
  accessible: module java.base does not "opens java.lang" to unnamed module @1bc6a36e

Мы получили исключение, которого не существовало в Восьмерке. Оно говорит, что объект недоступен потому, что модуль java.base, являющийся частью рантайма JDK и автоматически импортирующийся любой java-программой, не «открывает» (sic) нужный нам модуль для unnamed module. Ошибка падает на той строчке, где мы пытаемся сделать поле accessible.

Объект, до которого мы спокойно могли достучаться в Восьмерке, больше недоступен, потому что защищен системой модулей. Код может получить доступ к полям, методам, итп, используя reflection, только если класс находится в том же самом модуле, или если этот модуль открывает доступ для доступа по рефлекшену для всего мира, или какого-то конкретного модуля.

Это делается в файле с названием module-info.java, примерно так:

module randomModule {
    exports ru.habrahabr.module.random;
    opens ru.habrahabr.module.random;
}

Модуль java.base не дает нам доступа, поэтому мы сосем лапу. Если хочется увидеть более красивую ошибку, можно создать модуль для нашего кода, и увидеть его имя в тексте ошибки.

А можем ли мы программно открыть доступ? Там в java.lang.reflect.Module есть какой-то метод addOpens, это проканает? Плохие новости — нет. Оно может открыть пакет в модуле А для модуля Б, только если этот пакет уже открыт для модуля Ц, который зовёт этот метод. Таким образом модули могут передавать друг другу те права, которые уже имеют, но не могут открывать закрытое.

Но это же можно считать и хорошими новостями. Java растет над собой, Девятку не так просто поломать как Восьмерку. По крайней мере, вот эту маленькую дырку закрыли. Джава всё более становится профессиональным инструментом, а не игрушкой. Скоро мы сможем переписать на неё весь серьезный софт, сейчас написанный IBM RPG и COBOL.

Ах да, это всё равно можно сломать вот так:

public class IntegerHack {
 
    public static void main(String[] args)
            throws Exception {
        // Вытаскиваем IntegerCache через reflection
        Class usf = Class.forName("sun.misc.Unsafe");
        Field unsafeField = usf.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        sun.misc.Unsafe unsafe = (sun.misc.Unsafe)unsafeField.get(null);
        Class<?> clazz = Class.forName("java.lang.Integer$IntegerCache");
        Field field = clazz.getDeclaredField("cache");
        Integer[] cache = (Integer[])unsafe.getObject(unsafe.staticFieldBase(field), unsafe.staticFieldOffset(field));

        // Переписываем Integer cache
        for (int i = 0; i < cache.length; i++) {
            cache[i] = new Integer(
                    new Random().nextInt(cache.length));
        }
 
        // Проверяем рандомность!
        for (int i = 0; i < 10; i++) {
            System.out.println((Integer) i);
        }
    }
}

Может быть стоит запретить еще и Unsafe?

Btw, если вы боитесь писать комментарии здесь, то можно переползти в мой фб, или вживую встретиться на каком-нибудь Joker 2017, или просто пересечься рядом с БЦ Кронос или Гусями в Новосибирске, попить пива со смузи и обсудить еще какую-нибудь забавную дичь. Больше дичи богу дичи!

P.S. меня попросили вставить в статью котиков. Поэтому вот вам редкая фотка улыбающегося Марка Рейнхолда:



Источники:

[1] Исходная статья
[2] Человек, реанимировавший код из статьи на венгерском
[3] Всем известная картинка про рандомные числа
[4] Как переопределить nextInt
[5] Как собрать джаву под Windows

+29
12.2k 48
Comments 24
Top of the day