Я хотел сломать Java и я это сделал
На написание этой статьи, меня натолкнул разбор результата изменения полей объекта, лежащего в HashSet. Я развил идею и привнёс альтернативную математику в Java.
Ломаем
В Java существуют примитивные типы и их объектные версии. Для оптимизации JVM заранее создаёт и кеширует Boolean, Byte, Short и часть диапазона Integer, чтобы вместо создания нового объекта использовать существующий в кеше.
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
Оператор == сравнивает ссылки, а не значения объектов. Для сравнения по значению используется 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.