Для многих переход на 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
Поделиться с друзьями
-->

Комментарии (22)


  1. APXEOLOG
    06.05.2017 11:38
    +1

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

    Так ведь уже запланировано?


  1. yizraor
    06.05.2017 11:48
    +4

    Спасибо за интересную публикацию!
    Читал с удовольствием, узнал немало нового…

    Вот только слово «хачим» режет слух, и ассоциации вызывает несколько не те, которые надо :)
    И пусть «интуитивно понятно», что имел в виду автор, но может лучше не выпендриваться и написать «хакаем» хотя бы в заголовке?


  1. 23derevo
    06.05.2017 14:06
    +1

    Прочитал на одном дыхании! Олег, жаль, что ты не часто пишешь на хабре :)


    1. olegchir
      06.05.2017 15:18

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

      Например, что я сделал сегодня для Хабры?
      Проехал 67 километров на велике по каким-то лютым загородным дорогам, и даже вляпался в одну помойку.
      Пруфы: https://www.strava.com/activities/973207781

      Полученные от поездки силы обещаю потратить на еще какую-нибудь статью.
      Согласен, что это очень странная логика, но в моем случае она иногда работает.


    1. Artem_zin
      06.05.2017 16:27
      +5

      Прочитал на одном дыхании!

      Чот вообще не верю :)


      Человека с парой лет опыта работы с Java такое может удивить, остальным скорее должно быть просто интересно, что поменялось в девятке относительно такого использования reflection (и о чудо, все изменения ожидаемы, даже если не читать детали jigsaw), а уж у инженера, работавшего в Oracle где-то около JRE/JDK (ну и по совместительству лидера JUG.ru и организатора кучи Java конференций), эта статья наверн просто должна вызвать реакцию типа "опять пишут про то как менять циферки в Integer пуле" (:


      Сама по себе статья норм though.


      1. olegchir
        07.05.2017 09:14
        +2

        Вам нужен лонгрид по Пиле? Будет вам лонгрид. Stay tuned.

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


        1. Artem_zin
          07.05.2017 13:30
          +2

          Олег, у меня к статье никаких претензий, просто иногда личность автора важнее контента, а это убирает объективность из коммьюнити и приводит к печальке.


          Причина моего первого комментария в том, что у мистера 23derevo (которого я, кстати, считаю оч интересным человеком, если что) огромный вес в русскоязычном Java комьюнити и вот такие комменты типа "Прочитал на одном дыхании! Олег, жаль, что ты не часто пишешь на хабре :)" это явный триггер другим "Хочешь рейтингов в статьях, одобрения от комьюнити и первые слоты на конференциях? Надо дружить с правильными людьми!".


          У меня нет сомнений в вашей экспертизе, но хочется объективности, пишите лонгрид!


          1. 23derevo
            07.05.2017 13:51
            -1

            Я написал в комментарии ровно то, что думаю. А ваш комментарий глуп и неуместен. Мы с olegchir дружим лет десять, постоянно на связи и у нас очень много точек пересечения. Насчет выступлений — Олег недавно выступал у нас на JBreak и JPoint, так что с этим никаких проблем нет.


            1. Artem_zin
              07.05.2017 15:01
              +2

              Я, конечно, слоупок, но вроде как вторая часть этого комментария только подтверждает мои слова :)


              // Комментариям в этом треде оценки не ставил, если что.


              1. 23derevo
                07.05.2017 15:13

                Смотрите, Олега я знаю лет 10. Конференции я делаю 5 лет. И только в этом году Олег выступил. Так что нет никакой связи. Конечно, при прочих равных мне гораздо приятнее работать по докладам с человеком, которого я знаю, которому я доверяю и т.д. и т.п.


                1. Artem_zin
                  07.05.2017 19:39

                  Ну и хорошо тогда :)


                  Я не с целью обидеть или рассердить это пишу. Просто боюсь потерять хорошее русскоязычное Java комьюнити, последние несколько лет наблюдаю печальные трансформации в Android Dev мире (в основном зарубежном), где кучка "экспертов" оккупировала все конференции, дайджесты и тд, вот пытаюсь хоть тут это остановить.


                  Энивей, спасибо за хорошие конференции и вот это всё, просто не забывайте про объективность! (особенно по отношению к участникам Разбора-_Полетов, благо у них самоирония в порядке)


          1. Borz
            07.05.2017 18:43

            не тот триггер вы увидели.
            Лично я увидел триггер " olegchir, пиши ещё — у тебя хорошее повествование получается". При этот совершенно не важно знал 23derevo раньше про техническую часть из статьи или нет.


  1. DmitriyKotov
    06.05.2017 15:50
    +2

    Напоминаем, что Integer содержит приватный внутренний класс IntegerCache, содержащий объекты типа Integer, для диапазона от -127 до 128


    А не наоборот? Из javadoc:
    * Cache to support the object identity semantics of autoboxing for values between
    * -128 and 127 (inclusive) as required by JLS.



    1. olegchir
      06.05.2017 15:50

      да, это истина, поправил. Опечатка.


  1. asm0dey
    06.05.2017 16:27

    Посмотрим, станет ли оно реальностью: http://mail.openjdk.java.net/pipermail/jpms-spec-observers/2017-May/000874.html


  1. izzholtik
    06.05.2017 17:28
    +1

    Вообще говоря, это существенно снижает польу от рефлекшн апи.
    А как обстоят дела с javaagent'ами? Они теперь тоже не смогут ничего полезного делать?


    1. olegchir
      06.05.2017 19:03
      +1

      Есть класс Instrumentation. Его расширили новым методом. Сигнатура красноречиво показывает, что оно может добавлять:

      void redefineModule(Module module,
                          Set<Module> extraReads,
                          Map<String,Set<Module>> extraExports,
                          Map<String,Set<Module>> extraOpens,
                          Set<Class<?>> extraUses,
                          Map<Class<?>,List<Class<?>>> extraProvides);
      


      Более того, ClassFileTransformer API теперь умеет понимать Module

      default byte[] transform(Module module,
                               ClassLoader loader,
                               String className,
                               Class<?> classBeingRedefined,
                               ProtectionDomain protectionDomain,
                               byte[] classfileBuffer)
                        throws IllegalClassFormatException; 
      


      Сами джава-агенты всё так же грузятся по класспасу без модуляризации, но это может измениться в будущем.

      За дополнительными вопросами лучше всего обратиться к известному деятелю Rafael Winterhalter, он про это знает всё.

      (Если интересна тема джава-агентов, у него есть на ютубе куча разных видео, в том числе для jugru, jpoint, joker. Запрос в ютуб не пишу, сам придумаешь :)


  1. DarkGenius
    06.05.2017 18:10
    +2

    Какая практическая польза от описанных в статье манипуляций с IntegerCache?


    1. izzholtik
      06.05.2017 18:49

      тестирование.


    1. olegchir
      07.05.2017 09:52
      +2

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


  1. vlanko
    06.05.2017 18:48

    Unsafe должны были запретить, но многим не понравилось.


  1. lany
    07.05.2017 11:53
    +5

    Ещё есть волшебный ключик --permit-illegal-access.