Я хотел сломать Java и я это сделал

от автора

На написание этой статьи, меня натолкнул разбор результата изменения полей объекта, лежащего в HashSet. Я развил идею и привнёс альтернативную математику в Java.

Ломаем

В Java существуют примитивные типы и их объектные версии. Для оптимизации JVM заранее создаёт и кеширует Boolean, Byte, Short и часть диапазона Integer, чтобы вместо создания нового объекта использовать существующий в кеше.

Взглянем на Integer.java

public final class Integer extends Number         implements Comparable<Integer>, Constable, ConstantDesc {     private final int value;      @IntrinsicCandidate     public static Integer valueOf(int i) {         if (i >= IntegerCache.low && i <= IntegerCache.high)             return IntegerCache.cache[i + (-IntegerCache.low)];         return new Integer(i);     } }

В нём поле value объявлено как final private. И если второе можно обойти рефлексией, то против final она бессильна… но не для UB Unsafe. Замена 4 на 22 тривиальна.

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); theUnsafe.setAccessible(true); Unsafe unsafe = (Unsafe) theUnsafe.get(null);  Field integerValueField = Integer.class.getDeclaredField("value"); long integerValueOffset = unsafe.objectFieldOffset(integerValueField); unsafe.putInt(4, integerValueOffset, 22);  Integer a = 2; Integer b = 2; System.out.println(a + " + " + b + " = " + (Integer) (a + b));

В консоль выведет

2 + 2 = 22

Приведение в 11 строке к Integer обязательно, так как сумма вычисляется в int и равна четырём, при боксинге 4 будет получен Integer, в котором value заменено на 22. Для сумм выше диапазона кэша данный трюк не сработает.

Теперь я определяю математику

Теперь я определяю математику

Не только числа

Java кеширует короткие строки, поэтому возможна их подмена. Класс String хранит строку как массив байт. Массив — это объект, замена через unsafe:

Field stringValueField = String.class.getDeclaredField("value"); long stringValueOffset = unsafe.objectFieldOffset(stringValueField); unsafe.putObject("Manchester", stringValueOffset, "Liverpool".getBytes());  System.out.println("Пишется Manchester, говорится Liverpool  "                    + ("Manchester".equals("Liverpool")));

На байты строки «Liverpool» ссылаются строки «Liverpool» и «Manchester». Ожидаемый вывод:

Пишется Manchester, говорится Liverpool? true

На сладенькое:

Field booleanValueField = Boolean.class.getDeclaredField("value"); long booleanValueOffset = unsafe.objectFieldOffset(booleanValueField); unsafe.putBoolean(Boolean.FALSE, booleanValueOffset, true);  boolean eq = new Boolean(true) == Boolean.TRUE;  System.out.println("new Boolean(true) == Boolean.TRUE  " + eq); System.out.println("Как Boolean  " + (Boolean) eq); System.out.println("TRUE equals FALSE  " + Boolean.TRUE.equals(false));

Вывод в консоль

new Boolean(true) == Boolean.TRUE  false Как Boolean  true TRUE equals FALSE  true
Почему в первом случае false

Оператор == сравнивает ссылки, а не значения объектов. Для сравнения по значению используется equals. Конструктор Boolean объявлен Deprecated с 9 версии, при использовании Boolean.valueOf в первом случае будет true.

Итог

Полный код
import java.lang.reflect.Field;  import sun.misc.Unsafe;   public class Main {      public static void main(String[] args) throws Exception {         Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");         theUnsafe.setAccessible(true);         Unsafe unsafe = (Unsafe) theUnsafe.get(null);          Field integerValueField = Integer.class.getDeclaredField("value");         long integerValueOffset = unsafe.objectFieldOffset(integerValueField);         unsafe.putInt(4, integerValueOffset, 22);          Integer a = 2;         Integer b = 2;         System.out.println(a + " + " + b + " = " + (Integer) (a + b));          Field stringValueField = String.class.getDeclaredField("value");         long stringValueOffset = unsafe.objectFieldOffset(stringValueField);         unsafe.putObject("Manchester", stringValueOffset, "Liverpool".getBytes());         System.out.println("Пишется Manchester, говорится Liverpool  "                 + ("Manchester".equals("Liverpool")));           Field booleanValueField = Boolean.class.getDeclaredField("value");         long booleanValueOffset = unsafe.objectFieldOffset(booleanValueField);         unsafe.putBoolean(Boolean.FALSE, booleanValueOffset, true);          boolean eq = new Boolean(true) == Boolean.TRUE;          System.out.println("new Boolean(true) == Boolean.TRUE  " + eq);         System.out.println("Как Boolean  " + (Boolean) eq);         System.out.println("TRUE equals FALSE  " + Boolean.TRUE.equals(false));     } }

Чтобы поведение кода не становилось непредвиденным — читайте документацию, не нарушайте контракты, не превращайте Unsafe в undefined behaviour.

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

Узнали ли вы что-нибудь новое?

49.34% Узнал про Unsafe75
27.63% Знал про Unsafe, не знал что можно заменить final42
23.03% Ничего нового про Unsafe и его применение35

Проголосовали 152 пользователя. Воздержались 24 пользователя.

ссылка на оригинал статьи https://habr.com/ru/articles/767114/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *