YourClassUsingUnsafe.java:15: warning: Unsafe is internal
proprietary API and may be removed in a future release
Outrepasser les interdits avec sun.misc.Unsafe
Thomas SCHWENDER
GitHub / @thomasschwender
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
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.
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
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
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 are high optimized (mostly hand written assembler) code which are used instead of normal JIT compiled code.
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.
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 :
Avec Unsafe, on peut écrire en dehors des plages mémoires allouées…
Avec Unsafe, on peut stocker un int dans un type référence…
Avec Unsafe, on peut faire lancer une checked exception à une méthode qui ne la déclare ou ne la catch pas…
Par exemple, en libérant la mémoire d’une plage d’adresses réservée…
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…
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.
Résultat :
Une grosse levée de boucliers de sociétés dont Hazelcast, Azul Systems, OpenHFT pour ne citer qu’elles.
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
.
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
Constructor<Unsafe> c = Unsafe.class.getDeclaredConstructor();
c.setAccessible(true);
Unsafe unsafe = c.newInstance();
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 :
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
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
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.
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.
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 .
Slides : ardemius.github.io/unsafe-wizardry/unsafe-wizardry-slides.html
Code des slides : github.com/Ardemius/unsafe-wizardry
Ces slides ont été générés avec Asciidoctor et le backend reveal.js
Toutes les références utilisées pour la création de ce quicky sont listées ici.