На написание этой статьи, меня натолкнул разбор результата изменения полей объекта, лежащего в 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. Для сумм выше диапазона кэша данный трюк не сработает.
![Теперь я определяю математику Теперь я определяю математику](https://habrastorage.org/getpro/habr/upload_files/ffd/df8/7cc/ffddf87cc9a20d13b585d6b254e3c1f8.png)
Не только числа
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.