На написание этой статьи, меня натолкнул разбор результата изменения полей объекта, лежащего в 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.
Комментарии (9)
Stiver
12.10.2023 17:55+2Ух. Помнится во времена былинные, когда Java была еще closed source, я на Unsafe самомодифицирующийся код писал. Почти 20 лет прошло, спасибо за приступ легкой ностальгии :)
Unsafe Java I — Небезопасная жаба
Unsafe Java II — Мутагенез земноводных
isden
12.10.2023 17:55+5Но зачем?
Andrey_Solomatin
12.10.2023 17:55+2Не могу ответить за автора, но я такие вещи делаю для лучшего понимания, как это работает внутри и где тут есть приделы. Главное в рабочих проектах таким не заниматься.
ris58h
https://pedrorijo.com/blog/java-integer-cache/
ris58h
Это я к тому, что работоспособность от версии Java зависит, а в статье об этом ни слова.