Sécurité et vérificateur de classe

L'article de ce mois-ci poursuit la discussion sur le modèle de sécurité de Java commencée en août "Under the Hood". Dans cet article, j'ai donné un aperçu général des mécanismes de sécurité intégrés à la machine virtuelle Java (JVM). J'ai également examiné de près un aspect de ces mécanismes de sécurité: les fonctionnalités de sécurité intégrées de la JVM. Dans "Under the Hood" de septembre, j'ai examiné l'architecture du chargeur de classe, un autre aspect des mécanismes de sécurité intégrés de la JVM. Ce mois-ci, je vais me concentrer sur le troisième volet de la stratégie de sécurité de la JVM: le vérificateur de classe.

Le vérificateur de fichiers de classe

Chaque machine virtuelle Java dispose d'un vérificateur de fichier de classe, qui garantit que les fichiers de classe chargés ont une structure interne appropriée. Si le vérificateur de fichier de classe découvre un problème avec un fichier de classe, il lève une exception. Étant donné qu'un fichier de classe n'est qu'une séquence de données binaires, une machine virtuelle ne peut pas savoir si un fichier de classe particulier a été généré par un compilateur Java bien intentionné ou par des crackers louches déterminés à compromettre l'intégrité de la machine virtuelle. En conséquence, toutes les implémentations JVM ont un vérificateur de fichier de classe qui peut être appelé sur des classes non approuvées, pour s'assurer que les classes peuvent être utilisées en toute sécurité.

L'un des objectifs de sécurité que le vérificateur de fichiers de classe aide à atteindre est la robustesse du programme. Si un compilateur bogué ou un cracker averti générait un fichier de classe contenant une méthode dont les bytecodes incluaient une instruction pour sauter au-delà de la fin de la méthode, cette méthode pourrait, si elle était invoquée, faire planter la machine virtuelle. Ainsi, dans un souci de robustesse, il est important que la machine virtuelle vérifie l'intégrité des bytecodes qu'elle importe.

Bien que les concepteurs de machines virtuelles Java soient autorisés à décider à quel moment leurs machines virtuelles effectueront ces vérifications, de nombreuses implémentations effectueront la plupart des vérifications juste après le chargement d'une classe. Une telle machine virtuelle analyse les bytecodes (et vérifie leur intégrité) une fois, avant qu'ils ne soient jamais exécutés. Dans le cadre de sa vérification des bytecodes, la machine virtuelle Java s'assure que toutes les instructions de saut - par exemple, goto(sauter toujours),ifeq(saut si le haut de la pile zéro), etc. - provoque un saut vers une autre instruction valide dans le flux de bytecode de la méthode. Par conséquent, la machine virtuelle n'a pas besoin de rechercher une cible valide à chaque fois qu'elle rencontre une instruction de saut lorsqu'elle exécute des bytecodes. Dans la plupart des cas, vérifier tous les bytecodes une fois avant qu'ils ne soient exécutés est un moyen plus efficace de garantir la robustesse que de vérifier chaque instruction de bytecode à chaque exécution.

Un vérificateur de fichier de classe qui effectue sa vérification le plus tôt possible fonctionne très probablement en deux phases distinctes. Au cours de la première phase, qui a lieu juste après le chargement d'une classe, le vérificateur de fichier de classe vérifie la structure interne du fichier de classe, y compris la vérification de l'intégrité des bytecodes qu'il contient. Au cours de la phase deux, qui a lieu lorsque les bytecodes sont exécutés, le vérificateur de fichier de classe confirme l'existence de classes, champs et méthodes référencés symboliquement.

Première phase: contrôles internes

Au cours de la première phase, le vérificateur de fichier de classe vérifie tout ce qui est possible pour archiver un fichier de classe en examinant uniquement le fichier de classe lui-même (sans examiner les autres classes ou interfaces). La première phase du vérificateur de fichier de classe s'assure que le fichier de classe importé est correctement formé, cohérent en interne, respecte les contraintes du langage de programmation Java et contient des bytecodes qui pourront être exécutés en toute sécurité par la machine virtuelle Java. Si le vérificateur de fichier de classe trouve que l'un de ces éléments n'est pas vrai, il renvoie une erreur et le fichier de classe n'est jamais utilisé par le programme.

Vérification du format et de la cohérence interne

En plus de vérifier l'intégrité des bytecodes, le vérificateur effectue de nombreuses vérifications du format de fichier de classe approprié et de la cohérence interne au cours de la première phase. Par exemple, chaque fichier de classe doit commencer par les mêmes quatre octets, le nombre magique: 0xCAFEBABE. Le but des nombres magiques est de permettre aux analyseurs de fichiers de reconnaître facilement un certain type de fichier. Ainsi, la première chose qu'un vérificateur de fichier de classe vérifie probablement est que le fichier importé commence effectivement par 0xCAFEBABE.

Le vérificateur de fichier de classe vérifie également que le fichier de classe n'est ni tronqué ni amélioré avec des octets de fin supplémentaires. Bien que différents fichiers de classe puissent avoir des longueurs différentes, chaque composant individuel contenu dans un fichier de classe indique sa longueur ainsi que son type. Le vérificateur peut utiliser les types de composants et les longueurs pour déterminer la longueur totale correcte pour chaque fichier de classe individuel. De cette manière, il peut vérifier que le fichier importé a une longueur cohérente avec son contenu interne.

Le vérificateur examine également les composants individuels pour s'assurer qu'ils sont des instances bien formées de leur type de composant. Par exemple, un descripteur de méthode (le type de retour de la méthode et le nombre et les types de ses paramètres) est stocké dans le fichier de classe sous la forme d'une chaîne qui doit adhérer à une certaine grammaire sans contexte. L'une des vérifications que le vérificateur effectue sur les composants individuels est de s'assurer que chaque descripteur de méthode est une chaîne bien formée de la grammaire appropriée.

De plus, le vérificateur de fichier de classe vérifie que la classe elle-même adhère à certaines contraintes qui lui sont imposées par la spécification du langage de programmation Java. Par exemple, le vérificateur applique la règle selon laquelle toutes les classes, à l'exception de la classe Object, doivent avoir une superclasse. Ainsi, le vérificateur de fichier de classe vérifie au moment de l'exécution certaines des règles du langage Java qui auraient dû être appliquées au moment de la compilation. Comme le vérificateur n'a aucun moyen de savoir si le fichier de classe a été généré par un compilateur bienveillant et sans bogue, il vérifie chaque fichier de classe pour s'assurer que les règles sont suivies.