Это статья в жанре «неправильный перевод» с отсебятиной, потому что я художник, я так вижу =) Ссылки на источники – как всегда, в низу текста.
Пять лет назад Питер опубликовал блогпост на венгерском про то, как хакнуть IntegerCache в JDK. Это просто маленький эксперимент рантаймом, не имеющий никакого практического применения кроме повышения эрудиции, понимания как работает reflection, и как устроен класс Integer.
Глядите, генерация реально рандомных чисел зависит от энтропии системы [2]. Некоторые утверждают, что это можно сделать честным броском кубика [3].
Другие считают, что на помощь нам придет переопределение тела метода 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)
yizraor
06.05.2017 11:48+4Спасибо за интересную публикацию!
Читал с удовольствием, узнал немало нового…
Вот только слово «хачим» режет слух, и ассоциации вызывает несколько не те, которые надо :)
И пусть «интуитивно понятно», что имел в виду автор, но может лучше не выпендриваться и написать «хакаем» хотя бы в заголовке?
23derevo
06.05.2017 14:06+1Прочитал на одном дыхании! Олег, жаль, что ты не часто пишешь на хабре :)
olegchir
06.05.2017 15:18Алексей, моя способность писать посты напрямую зависит от физического здоровья.
Например, что я сделал сегодня для Хабры?
Проехал 67 километров на велике по каким-то лютым загородным дорогам, и даже вляпался в одну помойку.
Пруфы: https://www.strava.com/activities/973207781
Полученные от поездки силы обещаю потратить на еще какую-нибудь статью.
Согласен, что это очень странная логика, но в моем случае она иногда работает.
Artem_zin
06.05.2017 16:27+5Прочитал на одном дыхании!
Чот вообще не верю :)
Человека с парой лет опыта работы с Java такое может удивить, остальным скорее должно быть просто интересно, что поменялось в девятке относительно такого использования reflection (и о чудо, все изменения ожидаемы, даже если не читать детали jigsaw), а уж у инженера, работавшего в Oracle где-то около JRE/JDK (ну и по совместительству лидера JUG.ru и организатора кучи Java конференций), эта статья наверн просто должна вызвать реакцию типа "опять пишут про то как менять циферки в Integer пуле" (:
Сама по себе статья норм though.
olegchir
07.05.2017 09:14+2Вам нужен лонгрид по Пиле? Будет вам лонгрид. Stay tuned.
В даненом случае, цель была в маленьком победоносном примере, потому что обычно люди не хотят ввязываться в подробности, а хотят увидеть общий вывод, выраженный в три строчки.Artem_zin
07.05.2017 13:30+2Олег, у меня к статье никаких претензий, просто иногда личность автора важнее контента, а это убирает объективность из коммьюнити и приводит к печальке.
Причина моего первого комментария в том, что у мистера 23derevo (которого я, кстати, считаю оч интересным человеком, если что) огромный вес в русскоязычном Java комьюнити и вот такие комменты типа "Прочитал на одном дыхании! Олег, жаль, что ты не часто пишешь на хабре :)" это явный триггер другим "Хочешь рейтингов в статьях, одобрения от комьюнити и первые слоты на конференциях? Надо дружить с правильными людьми!".
У меня нет сомнений в вашей экспертизе, но хочется объективности, пишите лонгрид!
23derevo
07.05.2017 13:51-1Я написал в комментарии ровно то, что думаю. А ваш комментарий глуп и неуместен. Мы с olegchir дружим лет десять, постоянно на связи и у нас очень много точек пересечения. Насчет выступлений — Олег недавно выступал у нас на JBreak и JPoint, так что с этим никаких проблем нет.
Artem_zin
07.05.2017 15:01+2Я, конечно, слоупок, но вроде как вторая часть этого комментария только подтверждает мои слова :)
// Комментариям в этом треде оценки не ставил, если что.
23derevo
07.05.2017 15:13Смотрите, Олега я знаю лет 10. Конференции я делаю 5 лет. И только в этом году Олег выступил. Так что нет никакой связи. Конечно, при прочих равных мне гораздо приятнее работать по докладам с человеком, которого я знаю, которому я доверяю и т.д. и т.п.
Artem_zin
07.05.2017 19:39Ну и хорошо тогда :)
Я не с целью обидеть или рассердить это пишу. Просто боюсь потерять хорошее русскоязычное Java комьюнити, последние несколько лет наблюдаю печальные трансформации в Android Dev мире (в основном зарубежном), где кучка "экспертов" оккупировала все конференции, дайджесты и тд, вот пытаюсь хоть тут это остановить.
Энивей, спасибо за хорошие конференции и вот это всё, просто не забывайте про объективность! (особенно по отношению к участникам Разбора-_Полетов, благо у них самоирония в порядке)
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.
asm0dey
06.05.2017 16:27Посмотрим, станет ли оно реальностью: http://mail.openjdk.java.net/pipermail/jpms-spec-observers/2017-May/000874.html
izzholtik
06.05.2017 17:28+1Вообще говоря, это существенно снижает польу от рефлекшн апи.
А как обстоят дела с javaagent'ами? Они теперь тоже не смогут ничего полезного делать?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. Запрос в ютуб не пишу, сам придумаешь :)
DarkGenius
06.05.2017 18:10+2Какая практическая польза от описанных в статье манипуляций с IntegerCache?
olegchir
07.05.2017 09:52+2Обычно древний легаси код чуть менее чем полностью состоит из подобных хаков. Особенно для тестирования, да. Практическая польза не в конкретных манипуляциях, а в понимании принципа.
APXEOLOG
Так ведь уже запланировано?