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


  1. ris58h
    12.10.2023 17:55
    +7

    https://pedrorijo.com/blog/java-integer-cache/

    $ jshell
    |  Welcome to JShell -- Version 11.0.2
    |  For an introduction type: /help intro
    
    jshell> import java.lang.reflect.Field;
    
    jshell> Field value = Integer.class.getDeclaredField("value");
    value ==> private final int java.lang.Integer.value
    
    jshell> value.setAccessible(true);
    
    jshell> value.set(2, 3);
    
    jshell> 1 + 1
    $6 ==> 3


    1. ris58h
      12.10.2023 17:55

      Это я к тому, что работоспособность от версии Java зависит, а в статье об этом ни слова.


  1. Stiver
    12.10.2023 17:55
    +2

    Ух. Помнится во времена былинные, когда Java была еще closed source, я на Unsafe самомодифицирующийся код писал. Почти 20 лет прошло, спасибо за приступ легкой ностальгии :)


    Unsafe Java I — Небезопасная жаба
    Unsafe Java II — Мутагенез земноводных


    1. iamkisly
      12.10.2023 17:55

      Я только из-за этих статей начал Java изучать )


  1. isden
    12.10.2023 17:55
    +5

    Но зачем?


    1. Andrey_Solomatin
      12.10.2023 17:55
      +2

      Не могу ответить за автора, но я такие вещи делаю для лучшего понимания, как это работает внутри и где тут есть приделы. Главное в рабочих проектах таким не заниматься.


  1. 18741878
    12.10.2023 17:55

    Но зачем?

    Просто мсье понимает толк в разврате


  1. masyaman
    12.10.2023 17:55
    +1

    По сравнению с ChuckNorrisException это так, детские игрушки :)


  1. breninsul
    12.10.2023 17:55

    По этому Unsafe так и называется и вообще не Public API