Pull to refresh

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

Level of difficultyMedium
Reading time3 min
Views8K

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

Only registered users can participate in poll. Log in, please.
Узнали ли вы что-нибудь новое?
49.34% Узнал про Unsafe75
27.63% Знал про Unsafe, не знал что можно заменить final42
23.03% Ничего нового про Unsafe и его применение35
152 users voted. 24 users abstained.
Tags:
Hubs:
Total votes 21: ↑16 and ↓5+18
Comments9

Articles