Хачим 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
    Поделиться публикацией
    Ой, у вас баннер убежал!

    Ну. И что?
    Реклама
    Комментарии 24
    • +1
      Может быть стоит запретить еще и Unsafe?

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

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

        Вот только слово «хачим» режет слух, и ассоциации вызывает несколько не те, которые надо :)
        И пусть «интуитивно понятно», что имел в виду автор, но может лучше не выпендриваться и написать «хакаем» хотя бы в заголовке?
        • +1
          Прочитал на одном дыхании! Олег, жаль, что ты не часто пишешь на хабре :)
          • 0
            Алексей, моя способность писать посты напрямую зависит от физического здоровья.

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

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

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


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


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

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

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

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


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


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

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

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


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

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

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


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


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

                    • 0

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

                  • 0
                    Привет еще раз. Пацан сказал — пацан сделал: https://habrahabr.ru/post/329120/

                    Конечно, уже не про набивший оскомину jigsaw, но тоже лонгрид по формату :)
                • +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.


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

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

                    • НЛО прилетело и опубликовало эту надпись здесь
                      • +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. Запрос в ютуб не пишу, сам придумаешь :)
                      • +2

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

                        • НЛО прилетело и опубликовало эту надпись здесь
                          • +2
                            Обычно древний легаси код чуть менее чем полностью состоит из подобных хаков. Особенно для тестирования, да. Практическая польза не в конкретных манипуляциях, а в понимании принципа.
                          • 0
                            Unsafe должны были запретить, но многим не понравилось.
                            • +5

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

                              • 0
                                Тот-Чье-Имя-Нельзя-Называть:

                                Over time, as we've gotten closer and closer to the JDK 9 GA date, more
                                and more developers have begun paying attention to the actual changes
                                in this release. The strong encapsulation of JDK-internal APIs has, in
                                particular, triggered many worried expressions of concern that code that
                                works on JDK 8 today will not work on JDK 9 tomorrow, yet no advance
                                warning of this change was given in JDK 8.

                                To help the entire ecosystem migrate to the modular Java platform at a
                                more relaxed pace I hereby propose to allow illegal reflective access
                                from code on the class path by default in JDK 9, and to disallow it in
                                a future release.

                                In short, the existing «big kill switch» of the `--permit-illegal-access`
                                option [1] will become the default behavior of the JDK 9 run-time system,
                                though without as many warnings. The current behavior of JDK 9, in which
                                illegal reflective-access operations from code on the class path are not
                                permitted, will become the default in a future release. Nothing will
                                change at compile time.

                              Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                              Самое читаемое