На написание этой статьи, меня натолкнул разбор результата изменения полей объекта, лежащего в 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.