Sorcellerie en Java

Outrepasser les interdits avec sun.misc.Unsafe


Thomas SCHWENDER
GitHub / @thomasschwender
Logo StarTECH

Au programme

  • Mais c’est quoi Unsafe ?

  • Actualités, plan de suppression et levée de boucliers

  • Comment l’utiliser

  • Use cases

    • Instancier une classe sans passer par le constructeur

    • Tableaux XXL en mémoire Off Heap

    • Corruption de la mémoire

  • L’avenir d’Unsafe

Vous avez dit Unsafe ?

  • Comme tous les packages sun.*, sun.misc.Unsafe est une internal proprietary API.

  • Seul Oracle est censé l’utiliser pour l’implémentation de SA plateforme Java (JVM HotSpot)

The sun.* packages are not part of the supported, public interface.
…​
In general, writing java programs that rely on sun.* is risky: those classes are not portable, and are not supported.

— Oracle Java FAQ

L’API est néanmoins utilisable par tous, MAIS :

  • son suivi n’est donc pas assuré par Oracle

  • elle n’est pas documentée

  • ne dispose d'aucune garantie de portabilité d’une plateforme Java à une autre

  • peut être modifiée à tout moment

Depuis Java 6, le compilateur vous met en garde quand vous utilisez une classe de ce type :

YourClassUsingUnsafe.java:15: warning: Unsafe is internal
proprietary API and may be removed in a future release

Ici et nulle part ailleurs !

Certaines fonctionnalités d’Unsafe ne sont disponibles nulle part ailleurs dans Java :

  • lecture depuis / écriture à des adresses mémoires

  • accès à la mémoire Off Heap

I’m REALLY fast!

Quasiment toutes les méthodes d’Unsafe sont des intrinsics (ou intrinsified methods), d’où des performances généralement bien meilleures que celles des méthodes "classiques".

Intrinsics ?

Intrinsics are high optimized (mostly hand written assembler) code which are used instead of normal JIT compiled code.

— Java Bug Database JDK-8076112

C’est donc le JIT compiler qui, si l’optimisation est disponible, va optimiser notre code en le remplaçant par du code assembleur spécifique.

power and responsibility

L’utilisation d'Unsafe n’est pas sans risque

Ses méthodes très bas niveau ne respectent les barrières de sécurité classiques de Java.
Parmi les risques encourus, on trouve, entre autres :

Violation d’accès mémoire

Avec Unsafe, on peut écrire en dehors des plages mémoires allouées…​

Violation de la sureté des types

Avec Unsafe, on peut stocker un int dans un type référence…​

Violation des contrats de méthodes

Avec Unsafe, on peut faire lancer une checked exception à une méthode qui ne la déclare ou ne la catch pas…​

Tout simplement faire crasher la JVM…​

Par exemple, en libérant la mémoire d’une plage d’adresses réservée…​

Demandez le journal !

En juillet 2015, du fait du travail sur Jigsaw visant à rendre Java plus modulaire (voir la JEP 260), Oracle laisse entendre que l’API pourrait ne plus être directement accessible avec le JDK 9, puis définitivement supprimée avec le JDK 10.

Le problème est que cette API, même si ce n’aurait normalement pas du être le cas, est depuis longtemps utilisée par de nombreux projets et outils, et qu’elle ne dispose pas encore de véritables solutions de remplacement pour toutes ses fonctionnalités.

De plus, Oracle ne s’est pas montré très enclin à négocier sur le sujet…​

Unsafe must die in a fire

Let me be blunt — sun.misc.Unsafe must die in a fire. It is — wait for it — Unsafe. It must go. Ignore any kind of theoretical rope and start the path to righteousness now.

— Donald Smith - Oracle's director of product management

Résultat :
Une grosse levée de boucliers de sociétés dont Hazelcast, Azul Systems, OpenHFT pour ne citer qu’elles.

Comment utiliser Unsafe ?

Unsafe ne peut être instanciée directement : la classe est final, et son constructeur privé.

De plus, Unsafe.getUnsafe(), qui renvoie une instance d’Unsafe, est pour ainsi dire protégée
Si vous l’appelez vous aurez probablement une SecurityException.

Utilisation de la réflexion

Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);

ou encore

Constructor<Unsafe> c = Unsafe.class.getDeclaredConstructor();
c.setAccessible(true);
Unsafe unsafe = c.newInstance();

Qui fait quoi avec Unsafe ?

Unsafe regroupe au total 120 méthodes publiques (JDK 1.8.0_40).

La liste complètes des use cases associés est longue, voir :

De notre côté, nous allons voir les suivants :

Instancier une classe sans passer par le constructeur

Ce use case est fortement utilisé par Spring Core, Objenesis et Mockito.

A l’aide de la méthode allocateInstance(), on peut créer une instance d’une classe :

  • sans invoquer son constructeur

  • ni ses initialisations de variables

Cela fonctionne également pour des classes dont le constructeur est privé
(Ayons une petite pensée pour les Singletons…​)

class UnsafeTest {
    private int someInt = 42;

    public UnsafeTest(){
        this.someInt = 20;
    }

    public int getSomeInt(){
        return this.someInt;
    }
}
// constructor
UnsafeTest o1 = new UnsafeTest();
o1.getSomeInt(); // prints 20

// reflection
UnsafeTest o2 = UnsafeTest.class.newInstance();
o2.getSomeInt(); // prints 20

// unsafe
UnsafeTest o3 = (UnsafeTest) unsafe.allocateInstance(UnsafeTest.class);
o3.getSomeInt(); // prints 0

Tableaux XXL en mémoire Off Heap

Ce use case est utilisé par Neo4J et OrientDB, 2 bases de données NoSQL de type graphe, et MapDB, une solution hybride entre le framework de collections et le moteur de base de données.

Les tableaux en Java sont indexés par des int, et dès lors limités à Integer.MAX_VALUE éléments (231).

En utilisant la méthode allocateMemory d’Unsafe, il est possible de créer de vastes structures de données, en dehors de la Heap (mémoire Off Heap), non soumises à ces limitations.

class SuperArray {
    private final static int BYTE = 1;

    private long size;
    private long address;

    public SuperArray(long size) {
        this.size = size;
        address = getUnsafe().allocateMemory(size * BYTE);
    }

    public void set(long idx, byte value) {
        getUnsafe().putByte(address + i * BYTE, value);
    }

    public int get(long idx) {
        return getUnsafe().getByte(address + idx * BYTE);
    }

    public long size() {
        return size;
    }
}
long SUPER_SIZE = (long)Integer.MAX_VALUE * 2;
SuperArray array = new SuperArray(SUPER_SIZE);

System.out.println("Array size:" + array.size()); // 4294967294

for (int i = 0; i < 100; i++) {
    array.set((long) Integer.MAX_VALUE + i, (byte) 3);
    sum += array.get((long) Integer.MAX_VALUE + i);
}

System.out.println("Sum of 100 elements:" + sum);  // 300

Attention !

  • La mémoire Off Heap n’est PAS gérée par le Garbage Collector !

  • Vous devez la nettoyer à l’aide de freeMemory()
    Des crashs de la JVM sont possibles en cas de manque de ressources.

Corruption de la mémoire

Ce use case traite de problématiques de sécurité.

class Guard {
    private int ACCESS_ALLOWED = 1;

    public boolean giveAccess() {
        return 42 == ACCESS_ALLOWED;
    }

    [...]
}

Dans un code client très sécurisé, giveAccess() est appelée régulièrement pour vérifier les droits d’accès, et renvoie systématiquement false pour la quasi-totalité des utilisateurs.

Néanmoins, avec Unsafe…​

Guard guard = new Guard();
guard.giveAccess(); // false, no access

// bypass
Unsafe unsafe = getUnsafe();
Field f = guard.getClass().getDeclaredField("ACCESS_ALLOWED");
unsafe.putInt(guard, unsafe.objectFieldOffset(f), 42); // memory corruption

guard.giveAccess(); // true, access granted

A l’aide des méthodes putInt et objectFieldOffset(), il est possible d'écraser la valeur de ACCESS_ALLOWED à son emplacement en mémoire.

L’avenir d’Unsafe

  • Un flag de la ligne de commande permettrait de rendre accessible certaines APIs propriétaires, dont Unsafe.

  • Les Variables Handles (JEP 193, au scope de Java 9) sont censées remplacer les fonctionnalités d’Unsafe touchant à l’accès à la mémoire .

Ressources & références

Des questions ?

Merci !

last slide