[Atom] [Mail] [Twitter]
Liens : C · git · hacks · divers · cabale · à propos +
Au menu
\/

Appeler Emacs depuis un navigateur, sous un Debian-like

#. Par gasche dans Administrivia. Mis à jour le 04.12 2011 à 13:29. 3 commentaires.
<. Utiliser et configurer XMonad
administration emacs firefox navigateurs

Un petit billet dans la section administrativia pour me souvenir d’une astuce qui pourra resservir.

Aujourd’hui j’ai voulu reproduire sur un ordinateur une fonctionnalité que j’ai sur le mien et qui m’arrange bien : pouvoir facilement appeler Emacs depuis un navigateur web (en l’occurence Firefox) pour remplir un champ de texte.

Je ne prétends pas faire de la façon la plus efficace ou commode possible ; n’hésitez pas à le signaler si vous pensez qu’il y a une manière plus « propre » de faire.

L’idée d’ensemble est la suivante :

  1. J’utilise l’extension It's All Text qui permet d’appeler n’importe quel éditeur pour remplir un champ de texte ; si vous n’utilisez pas Firefox, il faudra trouver une extension correspondante.

  2. Je m’arrange pour que la commande editor, gérée par update-alternatives sous Debian (et Ubuntu), appelle Emacs, en utilisant emacsclient pour qu’il s’ouvre rapidement.

La première étape est simple, et marche sous toutes les versions de Firefox que j’ai testées. La seconde est un peu plus délicate, et détaillée ci-ensuite.

Pour commencer, le système update-alternatives n’accepte qu’un nom d’exécutable, sans qu’on puisse lui passer de paramètres. C’est dommage parce que pour que emacsclient soit agréable à utiliser, il faut passer l’option -c, ou --create-frame, pour qu’il crée une nouvelle fenêtre au lieu d’aller embêter une fenêtre existante.1 Il est donc nécessaire de se créer un petit script intermédiaire, à placer où vous voulez, chez moi c’est $HOME/etc/emacs.sh :

#!/bin/sh
emacsclient --create-frame $@

1 NdRZ : Personnellement, j’aime bien ouvrir ça dans une fenêtre existante. Two cents.

Ensuite il faut convaincre update-alternatives qu’on a envie d’utiliser ce script comme alternative préférée pour le programme editor — qui existe déjà et est géré par update-alternatives sur les Debian et Ubuntu récentes ; sur la machine cible il pointait vers nano par défaut. Il faut commencer par « installer » l’alternative, puis la choisir. La syntaxe d’installation est la suivante:

sudo update-alternatives --install <link> <name> <path> <priority>

<link> désigne le chemin de l’exécutable final, ici /usr/bin/editor, <name> son nom, editor, <path> le chemin du vrai programme à exécuter, donc votre script, et <priority> un nombre arbitraire, le plus grand est choisi par défaut. Chez moi les autres programmes étaient entre -100 et 20, donc j’ai mis 20, mais de toute façon ensuite je fais un choix manuel.

sudo update-alternatives --install /usr/bin/editor editor $HOME/etc/emacs.sh 20
sudo update-alternatives --set editor $HOME/etc/emacs.sh

Vous l’aurez compris, la dernière ligne désactive le choix automatique (par les priorités) pour forcer un choix manuel. Je ne sais pas pourquoi il faut répéter deux fois ce chemin, c’est un peu mal fichu, mais c’est la vie. On peut vérifier que tout a bien marché en lui demandant ce qu’il a en base pour editor :

% update-alternatives --display editor
editor - manual mode
  link currently points to /home/gabriel/etc/emacs.sh
/bin/ed - priority -100
  slave editor.1.gz: /usr/share/man/man1/ed.1.gz
/bin/nano - priority 40
  slave editor.1.gz: /usr/share/man/man1/nano.1.gz
/home/gabriel/etc/emacs.sh - priority 10
/usr/bin/emacs23 - priority 0
  slave editor.1.gz: /usr/share/man/man1/emacs.emacs23.1.gz
/usr/bin/emacsclient - priority 10
Current 'best' version is '/bin/nano'.

J’ai retiré quelques alternatives et quelques « esclaves » pour raccourcir la sortie, mais vous voyez l’idée. Maintenant il suffit de donner /usr/bin/editor à votre extension It’s All Text, et le tour est joué.

emacsclient s’utilise en ayant lancé au moins une fois, au démarrage de votre ordinateur, la commande emacs --daemon ; elle n’ouvre pas de fenêtre emacs mais lance un serveur en fond de tâche pour les clients suivants. Vous pouvez l’ajouter à vos scripts de démarrage de session X, ou ce que vous voulez. Si vous l’oubliez, It’s All Text se plaindra poliment d’une erreur à l’ouverture de l’éditeur (code de retour -1).

Avec cette configuration, emacs est facile et rapide à ouvrir depuis Firefox, et tout marche bien. Je m’en sers pour rédiger des messages un peu longs, en particulier tout ce qui est technique ; j’utilise beaucoup les facilités d’Emacs pour taper des caractères unicodes (M-x set-input-mode,2 TeX). C’est aussi utile quand on rédige un message avec des bouts de code : on peut tester dans un buffer, et facilement ouvrir des buffers déjà ouverts depuis une autre fenêtre.

2 NdRZ : M-x set-input-mode qui s’active aussi avec la commande C-x RET C-\. Une fois sélectionné, vous pouvez basculer entre le mode choisi et le mode normal par défaut avec C-\. Si votre disposition de touches ne permet pas un accès rapide et aisé à C-\, je vous conseille de vous créer un alias de touches, vers C-4 par exemple :

(define-key key-translation-map (kbd "C-4") (kbd "C-\\"))
[ tag:blog.huoc.org,2009:posts/convenient-browser-emacs ]
Voir les commentaires · Commenter

/\ \/

Génération de tests, compilateurs, et preuve formelle

#. Par gasche dans Réactions. Publié le 19.04 2011 à 23:09. 12 commentaires.
<. Sécurité et interface utilisateur
analyse_statique bugs compilation réaction

Quand il est sorti sur Lambda the Ultimate, j’ai lu avec intérêt l’article Finding and Understanding Bugs in C Compilers (PDF, 12 pages), de Xuejun Yang, Yang Chen, Eric Eide, et John Regehr, 2011. Je compte en faire un petit résumé, mentionner quelques points de l’article qui m’ont intéressé, et surtout faire une longue digression sur un sujet très légèrement abordé par l’article mais qui m’intéresse beaucoup : les rapports entre tests de fonctionnement et preuves de correction dans le développement logiciel.

Pendant la relecture, rz0 fait des remarques sur la forme et des choses à reformuler pour améliorer l’article, mais parfois il se contente de donner simplement son avis. J’ai décidé de garder ses remarques dans le texte, au lieu de me contenter de les lire pour moi et, si l’inspiration vient, d’y répondre par mail.

Le cœur de l’article

L’idée de base est de générer aléatoirement des programmes C, pour trouver des bugs dans les compilateurs. L’efficacité de la technique repose sur deux bonnes idées, une déjà connue et une nouvelle.

Concrètement, générer des programmes aléatoires permet de repérer les plantages (le compilateur lance une exception au lieu de produire un résultat), mais si le compilateur produit du code, comment savoir s’il est correct ? Alors déjà, il faut bien comprendre qu’on ne regarde pas directement le programme assembleur produit pour savoir s’il est correct ; cela demanderait de savoir raisonner sur du code assembleur quelconque, ce qui est bien trop compliqué. Non, on exécute le programme généré, et on regarde le résultat de l’exécution (donc le résultat du résultat du générateur de programmes…).

Que peut-on déduire du résultat de cette exécution ? D’habitude, on génère des tests dont on connaît le résultat (« l’exécution de ce programme va afficher 3 »), mais là on ne sait pas forcément prévoir le résultat correct à partir du programme C généré (pour cela, il faudrait dans le cas général avoir à disposition un compilateur C sans bugs…). C’est là qu’entre en jeu la première idée : on va comparer les sorties des différents compilateurs (GCC, Clang/LLVM, ICC, MSVC…), et supposer l’existence d’un bug quand ils se contredisent. Comme ça, on n’a pas besoin d’un « oracle » qui connaît la bonne réponse. C’est le principe des « tests différentiels » (differential testing).

Mais ce n’est pas aussi simple qu’il y paraît. Dans un grand nombre de cas, quand le programme en entrée n’est pas un « bon » programme, la norme ne définit pas uniquement le comportement que les compilateurs doivent adopter. Par exemple, si le programme commet une erreur grave comme une divison par 0 ou un accès hors d’un tableau, les compilateurs sont autorisés à produire un programme qui fait n’importe quoi (undefined behavior). Dans d’autres cas, il y a plusieurs choix possibles, comme par exemple pour l’ordre d’évaluation, et la norme ne tranche pas entre les différentes possibilités, elle laisse cet aspect non spécifié (unspecified behavior). Les auteurs de l’article ont recensé, dans la norme C99, 191 situations de undefined behavior et 52 cas de unspecified behavior. Si on prend un code C qui contient de tels comportements mal définis, deux compilateurs peuvent produire des résultats différents et être tout de même corrects, puisqu’ils respectent la norme.

La deuxième bonne idée de l’article est alors de vérifier que les programmes générés ne contiennent pas de comportement mal déterminé. Pour cela, ils utilisent une combinaison d’analyse statique du programme et des tests à l’exécution.

Les tests n’utilisent pas toutes les fonctionnalités du langage C, mais seulement un sous-ensemble raisonnable du langage pour lequel ils ont su mettre ces analyses au point. Ils ont sorti le grand jeu : data flow analysis pour déterminer le comportement des pointeurs, typage (qui prend en compte const et volatile), analyses d’effets… Il faut choisir des analyses qui rejettent tous les programmes mal définis ; mais elles rejettent alors nécessairement, on ne peut pas tout avoir dans la vie, certains programmes bien définis. Plus l’analyse est fine, moins elle rejette de programmes corrects, plus ils génèrent des tests compliqués et plus ils trouvent de bugs.

En pratique, ça marche bien : ils ont trouvé « des centaines de bugs » dans les compilateurs testés, dont 79 dans GCC et 202 dans LLVM ; certains des bugs trouvés sont détaillés dans l’article. Ces derniers correspondent en général à des optimisations trop aggressives des compilateurs, qui négligent certains cas et changent la sémantique, ou parfois à des erreurs d’implémentation dans le code de l’optimisation.

Quelques points de détail

Leur outil s’est révélé plus efficace quand il génère de gros programmes : « the largest number of bugs is found at a surprisingly large program size: about 81 KB ». Il n’y a pas de méthode complètement automatique pour réduire un programme-test en un programme plus petit qui préserve le bug sans introduire de comportement mal spécifié ; ils ont dû faire ça à la main. Ça pourrait s’automatiser, mais c’est sans doute un problème aussi difficile que la génération initiale de tests corrects.

Ils profitent de leur article pour critiquer gentiment certaines méthodologies de test, en particulier la métrique de couverture de code (code coverage). C’est une pratique très répandue dans l’industrie du test (et utilisée comme critère par exemple par les autorités de certification de code critique), qui consiste à utiliser un jeu de tests tel que chaque instruction du programme est exécutée au moins une fois (c’est le critère instruction coverage ; il y a d’autres critères de couverture, cf. Wikipédia) ; le terme « couverture » désigne ici la proportion du code du programme qui est exécuté au moins une fois par la batterie de tests. Ils montrent que leurs tests générés aléatoirement n’améliorent pas la couverture obtenue par les batteries de tests déjà existantes des compilateurs, c’est-à-dire qu’ils ont trouvé plein de bugs sans améliorer la couverture.

Enfin, ils ont même évalué le « retour sur investissement » de leur méthode : ils ont additionné les salaires de tous les chercheurs et étudiants qui ont travaillé sur cette méthode, le coût de l’équipement, de l’administration, etc., et obtenu un « coût » de $1000 par bug. Visiblement c’est considéré comme très rentable dans cette industrie (on imagine que les bugs dans les compilateurs peuvent être critiques et très difficiles à trouver) ; et maintenant que les outils sont développés, ils peuvent être utilisés à bien moindre coût.

Je suis quand même étonné de la quantité de bugs qui persistent dans ces compilateurs utilisés industriellement. Il ne s’agit pas de nouvelles fonctionnalités C99 qui viennent d’être implémentées (leur outil ne les prend pas en compte) mais de comportements bizarres sur un overflow dans une optimisation arithmétique, le genre de chose qu’on teste depuis des années. Pour la petite touche historique, ils mentionnent la longue histoire des tests par génération aléatoire de programme, qui a commencé en 1962 sur des compilateurs COBOL.

Remarque de rz0 :

Ça m’étonne pas, en vrai ; je pense que la plupart des programmes tendent à se cantonner à une partie plutôt bien huilée du langage et c’est ça qu’on teste bien. D’autre part, les programmes ont eux-mêmes des bugs, et en général, on n’accuse pas le compilo, du coup, il peut persister des problèmes que les gens workaround plutôt que de passer des heures à fouiller dans les normes et les forums pour savoir si c’est effectivement légal, pas légal, bien, mal défini, etc. La norme est sujette à interprétation, comme tu le sais, et sans être spécialiste, c’est très dur de savoir si ce que tu fais est légitime ou pas, et c’est nettement plus rentable de contourner le problème…

Le point le plus difficile de leur travail est la conception de ces analyses qui évitent les comportements mal définis et leur intégration dans le générateur de programmes. On peut se demander si cette difficulté est inhérente au langage C, et s’il faut prendre cet aspect en compte lors de la conception d’un langage de programmation : définir une sémantique déterministe rend le compilateur plus facile à tester. Sur la discussion de Lambda the Ultimate, un membre indique que les générateurs aléatoires de programme ont été très utiles pour les implémentations du langage Common Lisp ; est-ce lié à un plus petit nombre de comportement mal définis dans ce langage ?

Pour ma part, l’idée qu’un programme puisse se mettre à faire absolument n’importe quoi en cas d’erreur me semble désastreuse, mais je suis plutôt favorable à certains unspecified behavior, comme par exemple le fait de ne pas spécifier l’ordre d’évaluation des arguments d’une fonction, pour décourager les programmeurs d’écrire du mauvais code qui reposerait sur cet ordre. Mais des gens plus expérimentés que moi ne sont pas d’accord ; je crois que certains des concepteurs de Caml, par exemple, considèrent comme une erreur le fait de ne pas avoir fixé un ordre d’évaluation. La question n’est pas tranchée.

Remarque de rz0 :

C’est pas exactement dans le sujet, mais en fait, je pense qu’il faut mitiger cet aspect « comportement mal défini », parce qu’au final, du point de vue de l’implémenteur, il y a un choix à faire, et ce choix est rarement « on fait n’importe quoi ». Ce que je veux dire par là, c’est que l’implémenteur restreint fortement le nombre de comportements vraiment « désastreux », comme tu dis, et surspécifie le langage qu’il va prendre en charge, par rapport à la norme. Par exemple, Clang compile vers de l’assembleur intermédiaire LLVM, qui a des types entiers fixés (i8, i16, i32, etc.) avec une représentation prédéterminées (complément à deux) et des opérations arithmétiques bien définies dans tous les cas. Donc le fait que le langage C lui-même ne spécifie pas toutes ces choses-là ne devrait pas du tout affecter la qualité de l’implémentation.

CompCert, tests et preuves de correction

L’article mentionne le compilateur CompCert, un compilateur prouvé correct écrit en Coq. Je ne suis pas tout à fait d’accord avec le traitement qui en est fait, et je voudrais développer un peu ce point.

The striking thing about our CompCert results is that the middle- end bugs we found in all other compilers are absent. As of early 2011, the under-development version of CompCert is the only compiler we have tested for which Csmith cannot find wrong-code errors. This is not for lack of trying: we have devoted about six CPU-years to the task. The apparent unbreakability of CompCert supports a strong argument that developing compiler optimizations within a proof framework, where safety checks are explicit and machine-checked, has tangible benefits for compiler users.

Aucun bug trouvé dans un compilateur certifié. C’est normal, mais vous étiez quand même un peu inquiets, non ? Il se trouve qu’ils ont en fait trouvé des bugs, avant « early 2011 », dans le front-end de Coq, qui utilise CIL, une bibliothèque externe codée en OCaml qui parse le code C et lui applique quelques transformations. Cette bibliothèque non vérifiée semble effectivement contenir quelques bugs. Depuis, Xavier Leroy a réécrit cette partie de CompCert pour rétrécir la partie non-prouvée ; mais il en reste un peu, car pour l’instant on ne sait même pas vraiment comment spécifier un parseur (décrire formellement son comportement).

Les auteurs du présent article concluent en substance : bon, la vérification c’est très sympa et ça enlève plein de bugs, mais ça reste complémentaire au test, parce qu’il y a toujours des petits détails qui coincent. On se souvient de la célèbre phrase de Knuth : « Beware of bugs in the above code ; I have only proved it correct, not tried it. » Mais fondamentalement, ils ne semblent pas vraiment convaincus ; ils font aussi remarquer, à juste titre, qu’on est encore loin d’avoir un compilateur certifié pour un langage aussi délirant que C++0X (la prochaine norme du langage C++, en cours de finalisation ces mois-ci).

Cette attitude sceptique par défaut se retrouve aussi chez les autorités de certification de logiciel critique. Les gens qui travaillent là-bas sont chargés d’expertiser du logiciel pour vérifier son respect de normes de qualité très strictes, permettant ensuite l’utilisation dans les avions, voitures, centrales nucléaires, etc. Les expertises consistent essentiellement à analyser le code et les tests qui l’accompagnent mais aussi le processus de conception, la documentation produite, etc. Il y a des critères de tests divers ; l’idée n’est même pas vraiment de vérifier que les tests passent correctement (c’est une évidence), mais que leur présence montre l’existence d’une démarche qualité appropriée chez les gens qui ont écrit le code.

Ces autorités de certification n’ont pas d’expérience en vérification formelle : les outils d’analyse de programme sont déjà utilisés dans l’industrie pour trouver des erreurs, mais jusqu’à présent aucun programme n’était entièrement certifié. Les outils formels prouvaient par exemple l’absence d’overflow, mais pas un lien entre le code et une spécification. L’idée commence à germer et des questions se posent : quel degré de confiance accorder à un programme prouvé en Coq ? Une preuve remplace-t-elle la construction des jeux de tests (processus qui a sa part de coûts et de lourdeurs) ? Il y a un début de débat, mais la question est loin d’être tranchée, et ce sont des milieux évidemment assez conservateurs.

À l’opposé, j’ai rencontré des utilisateurs de Coq pour qui la réponse était claire : quand un programme est « prouvé en Coq », il ne peut évidemment pas contenir d’erreur, et il est inutile de le tester. Pour eux, le test est une méthode peu coûteuse (il est facile d’écrire un test) mais peu fiable (un test qui passe ne garantit en rien l’absence de bugs) de prendre confiance en la correction d’un logiciel, et la preuve formelle est une méthode coûteuse (il est très difficile d’écrire une preuve complète) mais absolument fiable.

Ce point de vue est malheureusement un leurre ; ce n’est pas un idéal qu’on veut atteindre un jour et pour lequel on fait des progrès, c’est tout simplement un état qui ne pourra jamais exister. La preuve formelle permet de s’assurer que tel théorème (théorème qui mentionne le programme que l’on veut vérifier) est absolument, mathématiquement juste. Mais le théorème peut être une trivialité qui dit « le programme marche, ou bien le programme ne marche pas ». Quand on a une preuve d’un théorème vérifiée par ordinateur, on est sûr que la preuve est juste, mais il faut toujours vérifier que le théorème est bien celui que l’on croit. On aurait très bien pu se tromper dans sa formulation (et c’est déjà arrivé en pratique), même si le fait d’écrire une preuve permet la plupart du temps de détecter des incohérences dans l’énoncé.

Je pense donc que l’idée de tester un programme prouvé n’est pas la bonne approche, et que les six ans de CPU n’ont pas été très utiles. On devrait plutôt essayer de tester la spécification établie par la preuve, l’énoncé du théorème de correction. Qu’est-ce que ça veut dire, tester une formule mathématique ? Je ne sais pas exactement, mais j’ai quelques idées. Par exemple, on pourrait montrer à la main qu’un système simple et faux ne vérifie pas cette spécification. On a inversé la situation : au lieu de tester le programme en disant « telle entrée est acceptée par le programme comme prévu », on teste la spécification en disant « la spécification rejette tel programme comme prévu ».

Bref, je pense que oui, il faut continuer à « tester » un programme prouvé correct, mais qu’on ne le teste pas de la même façon. En substance, prouver un programme revient à réduire sa « surface de confiance » : au lieu de devoir mettre en doute l’ensemble du programme (lire le code et se convaincre qu’il fait bien ce qu’on pense), on doit ausculter sa spécification qui est, espère-t-on, plus abstraite, plus simple et plus concise. Mais la possibilité d’erreur est toujours là, et elle ne disparaîtra jamais : il y aura toujours un gouffre entre la description mécanique du programme donnée à l’ordinateur et l’idée informelle que s’en font ses concepteurs et utilisateurs ; ce sont deux mondes différents.

Une force de la preuve formelle que je n’ai pas mentionnée est son aspect progressif. On peut commencer par ne rien montrer du tout sur un programme, puis prouver qu’il vérifie telle propriété plus ou moins simple (par exemple qu’il termine sur toutes les entrées bien formées), puis une autre propriété un peu plus subtile. Par exemple, pour un programme qui renvoie les racines d’un polynôme, on peut commencer par montrer que tous les résultats renvoyés sont bien des racines, car c’est nettement plus facile que de montrer que toutes les racines sont bien renvoyées. Petit à petit, on peut progresser vers la preuve de correction « totale » où la spécification décrit exactement ce que le concepteur aimerait que son programme fasse. Une autre façon de s’assurer d’une spécification est alors de vérifier qu’elle implique les énoncés plus faibles précédents ; par opposition aux tests précédents, c’est une forme de preuve de spécification.

De nombreux articles d’opinions ont été écrits sur la question de la vérification formelle des logiciels. Je vous recommande chaudement la lecture de Social Process and Proofs of Theorems and Programs, Richard De Millo, Richard Lipton et Alan Perlis, 1979. Cet article explique avec fermeté que la vérification formelle est un rêve qui n’existe pas en réalité, même chez les mathématiciens, et que nous n’arriverons jamais à prouver formellement de « vrais programmes ». Je ne suis pas d’accord, mais ça le rend d’autant plus intéressant à lire. Plus neutre, et moins pessimiste, j’ai apprécié le court article de vulgarisation Formal Proof – Theory and Practice, de John Harrison, 2008.

[ tag:blog.huoc.org,2009:posts/generation-aleatoire-bugs-compilateurs-C ]
Voir les commentaires · Commenter

/\ \/

Un peu de crypto

#. Par bluestorm. Publié le 03.04 2011 à 00:52. Un commentaire.
cryptographie débutants sécurité

Cette année, j’ai eu l’occasion de faire un peu de cryptologie.

Alors moi, dans l’idée, calculer n^(pq) dans un groupe cyclique d’ordre d, ou décaler-xorer les bits 17 fois pour calculer un hash cryptographique, ça ne me tente pas du tout.

J’ai cependant découvert qu’il y a d’autres aspects de la crypto qui ne concernent pas l’analyse technique des algorithmes, mais plutôt une analyse « sémantique » de leur utilisation. Par exemple, on a beau avoir un super algorithme de chiffrement, si on envoie la clé en clair avec le message chiffré, ce n’est pas sécurisé. Il y a des façons moins évidentes de se tromper et des gens qui cherchent comment éviter ces pièges.

Dans ce billet, je présenterai juste un exemple assez simple. Ne vous attendez pas à des merveilles tout de suite, mais elles viendront peut-être plus tard.

Étude formelle des protocoles cryptographiques

On fait une différence entre l’étude « formelle » et l’étude « calculatoire » des systèmes utilisant la cryptographie. Dans le modèle formel, on considère les algorithmes cryptographiques comme des boîtes noires, qui marchent toujours et sont incassables, et on étudie l’utilisation qui est faite de ces boîtes noires : sous des conditions données, est-ce que telle utilisation est sûre ?

Dans le modèle calculatoire, on met les mains dans le cambouis en regardant dans les boîtes, dans le but d’avoir des estimations quantitatives de la difficulté calculatoire pour casser une utilisation aux paramètres de sécurité (taille des clés, etc.) donnés. Par exemple, si vous utilisez tel algorithme de telle façon et espérez garantir telle propriété de sécurité, il faudra au moins 280 calculs à un attaquant pour la casser.

Je me limiterai au modèle formel, en utilisant certaines des boîtes noires et notations suivantes :

chiffrement symétrique

On peut encoder ou décoder un message M avec la clé K. Sans la clé, il n’y aucun moyen de décoder le message. Je note senc(M,K) le message chiffré.

chiffrement asymétrique

Un utilisateur U possède une clé publique pk(U) et une clé secrète sk(U). Il diffuse la clé publique, et garde la clé secrète… secrète. On peut chiffrer un message avec une clé publique pk(X), et alors pour le déchiffrer il faut avoir sk(X). Cela permet de faire des échanges sans partager un secret commun, comme dans le cas symétrique. Je note aenc(M,pk(X)) le message chiffré.

signature

Toujours dans un cadre asymétrique, U peut « signer » un message M dans le but de garantir qu’il vient bien de lui. Il utilise pour cela sa clé secrète, et obtient une signature qui peut être vérifiée par tout possesseur de la clé publique pk(U). On note sign(M, sk(X)) la signature, et il y a une opération check(M, S, pk(X)) qui renvoie vrai si et seulement si S est bien la signature du message M avec la clé privée correspondant à pk(X). Bien sûr, on ne peut pas récupérer sk(X) à partir de sign(M, sk(X))

Ça c’est ce que peut faire tout le monde, en particulier les « gentils », pour utiliser la crypto en espérant arriver à leurs fins. Mais que peuvent faire les méchants ? On fait en général des hypothèses paranoïaques : on considère que tout message envoyé sur le réseau peut être lu et même stoppé (avant qu’il arrive à son destinataire) par un méchant attaquant.

Un cas d’école : le protocole d’authentification de Needham-Schroeder

Le cas d’école d’étude des protocoles cryptographiques est le protocole d’authentification de Needham-Schroeder. C’est un protocole destiné à « authentifier » des agents qui communiquent entre eux, c’est-à-dire à garantir qu’ils sont bien en train de parler à la personne à laquelle ils pensent parler.

L’idée générale est d’envoyer un « défi » (challenge) à la personne avec laquelle on parle : un nombre aléatoire qu’on vient de générer, donc introuvable (on appelle un tel nombre un nonce, affreux anglicisme proche du mot « hapax »), chiffré avec la clé de la personne avec laquelle on pense parler. Si elle arrive à déchiffrer ce nonce et à nous le renvoyer, elle est authentifiée.

Pour toute paire d’agents A et B, le protocole procède comme suit :

A → B : aenc((N_A, A), pk(B))
B → A : aenc((N_A, N_B), pk(A))
A → B : aenc(N_B, pk(B))

A génère un nonce N_A et envoie la paire (N_A, A) (ici A représente une donnée qui l’identifie : son nom, son adresse mail…) à B. Il génère à son tour un nonce N_B et renvoie les deux à A, qui répond en renvoyant N_B. Si tout se passe bien, les deux ont bien répondu à leurs défis respectifs et se sont mutuellement authentifiés.

Il est crucial que les défis soient générés aléatoirement, à chaque session d’authentification. Sinon un attaquant pourrait écouter les messages et les « rejouer » ensuite dans le futur, en se faisant passer pour l’un des participants. Par exemple si N_B était une constante choisie une fois pour toute par B, un attaquant C ayant observé cet échange pourrait entamer la session en prétendant être A, et à la troisième étape renvoyer le message aenc(N_B, pk(B)), faisant ainsi croire à B qu’il est à nouveau en train de parler avec A.

La notation que j’ai utilisée pour le protocole est relativement informelle. On peut la formaliser un peu mieux, ou encore passer à des descriptions plus fines, qui décrivent par exemple précisément le moment de génération d’un nonce. Les cryptographes utilisent pour cela des variantes du π-calcul, un calcul de processus développé au départ comme base théorique de la programmation distribuée.

Il y a aussi de l’implicite au niveau du lien entre une identité A, et ses clés pk(A) et sk(A). En particulier, sk n’est pas une simple fonction, car on veut que seul A puisse calculer sk(A). La façon dont on associe pk(A) à A peut aussi être modélisée. Ici j’ai considéré que les participants « connaissent » les clés publiques, mais on peut aussi supposer un (ou plusieurs) serveurs de certifications, supposés de confiance, et qui indiquent la clé publique associée à une identité. Cela complique un peu le protocole, de façon inessentielle ici.

Enfin, il convient de faire une dernière remarque sur ce protocole : après l’authentification, N_A et N_B sont des secrets partagés entre A et B. Il a donc été suggéré d’utiliser ces secrets partagés pour une communication entre A et B, par exemple comme clé d’un chiffrage symétrique. En plus d’authentifier A et B, on souhaiterait donc garantir le secret de ces nonces.

… pas si sûr qu’il n’y paraît

Le protocole que j’ai décrit plus haut est en fait non sûr : il ne garantit pas l’authenticité des participants. Imaginons un ensemble d’agents qui s’authentifient mutuellement en utilisant ce protocole. Certains, comme A et B, sont honnêtes, mais d’autres, comme I, sont mauvais et cherchent à subvertir le protocole à leurs fins propres.

Si A entame une communication avec I en essayant de s’authentifier, I peut en profiter pour se faire passer pour A auprès de B. Je noterai Î pour l’agent I se faisant passer pour A ; c’est toujours I, mais l’intention est différente :

A → I : aenc((N_A, A), pk(I))
Î → B : aenc((N_A, A), pk(B))
B → Î : aenc((N_A, N_B), pk(A))
I → A : aenc((N_A, N_B), pk(A))
A → I : aenc(N_B, pk(I))
Î → B : aenc(N_B, pk(B))

L’idée de I est proche des man in the middle attacks : transférer les paquets sans trop y toucher, dans le but que les personnes aux deux bouts ne se rendent pas compte du problème et fassent tout le travail pour lui. Il envoie la demande de connexion de A à B, et attend la réponse de B. Il ne peut pas la lire puisqu’elle est chiffrée avec la clé de A (B pense parler à A). Mais il peut se servir de A pour la déchiffrer à sa place ! Il envoie ainsi la réponse de B, qui contient le défi N_B, à A, comme si c’était son propre défi. A le déchiffre et renvoie N_B à I, qui peut alors répondre triomphalement à B.

L’authentification est brisée, puisque B pense avoir conclu une session avec A alors que ce dernier n’a jamais parlé à B. De plus, si les nonce étaient ensuite utilisés comme clé de chiffrement de messages post-authentification, cela pourrait avoir des conséquences graves. Par exemple si B est une banque et A un de ses clients :

Î → B : senc("I'm A, please transfer all my money to I", (N_A, N_B))

Quand on parle de « cas d’école », on veut dire qu’il a été très étudié, pas que c’est un exemple fait sur mesure pour les étudiants. Le protocole de Needham-Schroeder a été publié en 1978, et la faille n’a été découverte par Gavin Lowe qu’en 1995. Inutile de dire que les personnes ayant utilisé le protocole entre temps ont dû être très, très stressées…

Gavin Lowe, simultanément à son attaque, a proposé de corriger le protocole de la façon suivante :

A → B : aenc((N_A, A), pk(B))
B → A : aenc((N_A, B, N_B), pk(A))
A → B : aenc(N_B, pk(B))

Dans la deuxième étape, B envoie son identité en plus du défi. Ainsi, A peut bien vérifier qu’il provient bien de la personne auprès de laquelle elle essaie de s’authentifier. Dans le cas de l’attaque, où A pense parler à I qui s’en sert pour le mettre en communication avec B, la réponse de I à A, issue de B, deviendrait alors :

I → A : aenc((N_A, B, N_B), pk(A))

et A, voyant que le message ne provient pas de I, interromprait immédiatement la tentative d’authentification.

Gavin Lowe conclut ainsi son article (PS, 5 pages) :

We conjecture that the revised protocol is safe against all attacks — at least, those attacks not dependent upon properties of the encryption method used. Proving this formally is the topic of current research.

C’était current research en 1995, maintenant on sait faire… tant qu’on sait ce qu’on doit prouver. Ce sera peut-être le sujet d’un prochain billet.

[ tag:blog.huoc.org,2009:posts/cryptographie-semantique ]
Voir les commentaires · Commenter

/\ \/

Cofoja contracts, checkpoints and recursion

#. Par rz0. Publié le 20.03 2011 à 10:51. 3 commentaires.
cofoja contracts java

Since the release of Cofoja, there have been several changes, some important, some less so. One of the major things I’ve been working on recently (and which is, as I write, under review and should make it to the main branch shortly) is a new implementation of contract checkpoints: that is, where contracts are checked at run time in the final code.

In this short blog post, I will go through the various options we have considered, what we did before, what we will do now, and how it is implemented.

A matter of semantics

On the surface, contract semantics are pretty straightforward, even more so in Cofoja as we have tried to keep it simple by sticking to the most fundamental constructs:

  • preconditions must hold just before the call;
  • postconditions must hold just after the call;
  • and invariants act as both pre-and-postconditions.

While preconditions and postconditions are mostly just that, this description of invariants hides a small but important issue: they act as both pre-and-postconditions to what? Since invariants are characteristic of a class and not a method, we can legitimately ask: should invariants apply to all methods within the given class? And the answer is, as with most matters of fine semantics, that there is no definite answer, it depends on what we want. The general idea is that we want invariants to hold at those points in time where the object is in a "stable" state, one where it is expected to be behave consistently with respect to its specification. So we want to preclude any internal manipulation that goes on inside our object from checking invariants: they most probably wouldn’t hold there. The real question is thus what constitutes an internal operation. I am pretty sure the question has been discussed to death in contract programming circles; since I am not really a contract programming specialist, here I will simply give a not-so-brief overview of the different options that have been considered for Cofoja, with some Java examples to illustrate what problems they tried to address, and what we settled with in the end.

Perhaps the most important distinction that can be made between strategies lies in the decision to treat all calls to a method in the same way or apply invariant checks to some and not others. It should be noted that while it is tempting to associate whole-method semantics to a compile-time choice and per-call discrimination to a run-time behavior, some call-site-oriented approaches do not require information beyond what would be (easily) accessible to a compiler (such an example is given below).

A simple static strategy

Originally, Cofoja, similarly to its predecessor Modern Jass, used a simple rule: invariants would apply to all public or package-private methods. I know, True Java Hackers will probably hate us because of the inconsistency of that rule: package-private methods have a lesser scope than protected methods, so why check contracts on these and not protected ones? It doesn’t make sense!

The reason we had such a rule was because, we’ve noticed that, in practice, classes with methods internal to a package have their own intra-package specifications that we want to enforce with regard to other classes in the package. Conversely, protected methods, even though strictly broader in scope than their package-private counterparts, are most often used to provide utility routines to implementors who may want to write child classes; in such cases, these protected methods are likely to be called from deep within child classes, when the object is possibly in an inconsistent state. Hence the weird rule. But as awkward as it is, it did work pretty well. There were a few shortcomings, though, that we set out to fix.

Most issues with this strategy revolved around the idea that you may have public methods that may also be called from within child classes. Of course, the workaround is to make a protected method and call that from your public one, but it wasn’t totally satisfactory, especially in the case of constructors. It’s often the case that the default constructor is public; this means that child constructors can pull the invariant checking by calling the default parent constructor implicitly!

A static per-call approach

A possibly better strategy could be to consider all calls originating from a class that shares an ancestry link with the target to be internal, and all others external. This characterization has the advantage that it can be determined locally and without running the program. Its main limitation is that it requires call sites rather than methods to be altered in order to insert contract checks.

Instrumenting call sites is perfectly doable but suffers from a number of unwanted side effects. There are a few ways call sites can be modified to add contracts: we can rewrite internal calls to point to a shadow method that does not have contracts enabled, or we make external calls refer to a wrapper method (which can be inlined, in which case the method implementing the contracts are invoked directly from the caller).

In any case, the transformation means that it is impossible to distribute pre-woven libraries that check their contracts even against an application that does not, since all new call sites need to be instrumented. (New external call points appear naturally as the class is used, and new internal call sites are the result of users extending library classes.)

Another issue, which is perhaps more problematic, is interoperability with other tools that are not aware of our little splitting and wrapping tricks. Beside potentially polluting the stack trace with user-unfriendly names and references, the risk would be confusing other bytecode-reading citizens of the Java world, such as profilers and boilerplate-laying frameworks.

For these reasons, this solution was not retained and a totally dynamic strategy that could be implemented on the caller’s side was considered instead.

A dynamic per-call scheme

In the end, we decided on a dynamic strategy that can be described simply as: invariants are checked the first time we enter a contracted object. By "enter", I mean that it is the first call dispatched to that object from another object; if in the middle of one of its methods, execution leaves the instance and then comes back (A calls B, which calls A), it is not counted as "entering" the object.

Obviously, this is most easily accomplished dynamically, when information about instances is fully available. The problem of checking whether we are entering an object is equivalent to determining whether the current instance is already present in the stack trace. A straightforward way to implement that behavior is by means of a per-object flag that we set to true on entry and false on exit of the method that set it to true. Such a technique implies however that the flagged object cannot have its methods called concurrently from different threads. Such a constraint is fine for most classes and object, but for those that need it, let’s take a look at a thread-safe alternative.

The basic idea here is that we want to associate a flag per object and per thread. Making the per-object flag a ThreadLocal object is not a solution since it would introduce an unbearable number of thread-local variables to manage. Instead, it is possible to maintain our own thread-local set of currently entered objects: this map only needs to hold all contracted objects currently on the stack, which hopefully isn’t so many. My soon-to-be-merged patch implements this exclusion mechanism through a per-thread IdentityHashMap that records which objects have been seen in the current stack trace.

Actually, Cofoja will only use the thread-safe version. While the cost of accessing a thread-local variable is high compared to a simple field, it is shared with another flag, which must already reside in per-thread storage: the recursion prevention flag.

A word about recursion

Another issue with contract checking is the matter of contract recursion: when contracts access other objects, should these objects have their contracts enforced? If there is no other mechanism preventing it, we may end up in an infinite loop trying to check contracts in circle.

Hence, a simple solution is to prevent all kinds of contracts within contracts from activating: this requires a per-thread variable.

But you may ask: doesn’t the same-object exclusion rule presented in the previous section disallow recursion already? Indeed, should the same contract be checked twice in the same trace, the second one will be inhibited. However, it is true only of invariants and it is easy to write postconditions that reference each others’ methods, for example.

Nonetheless, I decided to test without the flag and came upon an interesting situation. The problem was revealed by the following excerpt from the Cofoja source code (when running Cofoja with its own contracts enabled):

@Invariant({
  "getEnclosingElement() == null " +
      "|| getEnclosingElement().getEnclosedElements().contains(this)",
  "Iterables.all(getEnclosedElements(), isValidEnclosedElement())"
})
public abstract class ElementModel

What it does is check that the current element is effectively a child of its parent and has children of its own of the proper kind. When considered alone, this contract is quite nice and very local: it examines the direct parent and children of one node. However, if contracts within contracts are honored, the invariant will trigger itself on the parent as well as all the child objects.

A quick glance at the contract may suggest that this extends the check to the full tree, which is not such a bad thing per se. But a closer look will reveal that it actually introduces many redundant checks. The core of the issue lies in the first clause: the one targeting the parent; it is hidden in the method contains. contains will scan through the entire list of children of the parent object (which are siblings of the current object) in search of one that is equal to the current element. Unfortunately, such a traversal will trigger contracts on each visited instance, hence spawning a new check that will scan all of its siblings. This scan is bound to stop once all the siblings have been seen once in the current trace; it will however repeat the same idiotic ritual with the next, and so on and so forth. This will also propagate to the whole tree.

If there is something to be learnt from this experience, for me, it was that a seemingly very legitimate contract could wreck havoc assuming the new rule only. And it was also very difficult to debug: the program just seemed to hang forever, and if stopped, it would just show a normal stack trace with a couple of nodes being checked, nothing unusual. Hence we decided that even if we could extend the same-object exclusion mechanism to pre-and-postconditions, we shouln’t do it, and instead keep the simple flag that excludes all contracts within contracts, because there shouldn’t be such an easy way for our users to write innocent-looking contracts that would blow the system.

[ tag:blog.huoc.org,2009:posts/contracts-checkpoints-recursion ]
Voir les commentaires · Commenter

/\ \/

Sécurité et interface utilisateur

#. Par gasche dans Réactions. Mis à jour le 19.03 2011 à 21:54. 6 commentaires.
<. Cohérence des effets de bord
>. Génération de tests, compilateurs, et preuve formelle
conception débutants interface_utilisateur pola réaction sécurité

J’écris un petit billet pour vous parler d’un article que j’ai lu récemment et qui m’a bien plu. Il s’agit de l’article User Interaction Design for Secure Systems, Ka-Ping Yee, 2002 (PDF, 16 pages).

The security of any computer system that is configured and operated by human beings critically depends on the information conveyed by the user interface, the decisions of the computer users, and the interpretation of their actions. We establish some starting points for reasoning about security from a user-centred point of view, by modelling a system in terms of actors and actions and introducing the concept of the subjective actor-ability state. We identify ten key principles for user interaction design in secure systems and give case studies to illustrate and justify each principle, describing real-world problems and possible solutions. We anticipate that this work will help guide the design and evaluation of secure systems.

[…]

Usability issues are often considered to “trade off” against security. Among many designers, there is the pervasive assumption that improving security necessarily degrades usability, and vice versa; the decision of whether to favour one or the other is typically seen as a regrettable compromise. In the end, these judgement calls are made somewhat arbitrarily because there seems to be no good answer. We believe that usability and security goals rarely need to be at odds with each other. In fact, often it is rather the opposite: a system that’s more secure is more predictable, more reliable, and hence more usable.

Ce n’est pas un article très technique, car il concerne uniquement les questions d’interface utilisateur. Il est donc facile à lire, même s’il est parfois formulé en termes assez abstraits : les exemples aident à bien comprendre l’application des concepts présentés.

Si vous avez besoin d’un peu plus d’information avant d’ouvrir le PDF, voici la liste des dix principes présentés dans l’article. Évidemment, c’est mieux avec les explications et les exemples.

Path of Least Resistance

To the greatest extent possible, the natural way to do any task should also be the secure way.

Appropriate Boundaries

The interface should expose, and the system should enforce, distinctions between objects and between actions along boundaries that matter to the user.

Explicit Authority

A user’s authorities must only be provided to other actors as a result of an explicit action that is understood by the user to imply granting.

Visibility

The interface should allow the user to easily review any active authority relationships that would affect security-relevant decisions.

Revocability

The interface should allow the user to easily revoke authorities that the user has granted wherever revocation is possible.

Expected Ability

The interface must not generate the impression that it is possible to do something that cannot actually be done.

Trusted Path

The interface must provide an unspoofable and faithful communication channel between the user and any entity trusted to manipulate authorities on the user’s behalf.

Identifiability

The interface should enforce that distinct objects and distinct actions have unspoofably identifiable and distinguishable representations.

Expressiveness

The interface should provide enough expressive power (a) to describe a safe security policy without undue difficulty; and (b) to allow users to express security policies in terms that fit their goals.

Clarity

The effect of any security-relevant action must be clearly apparent to the user before the action is taken.

L’intérêt inattendu de la sécurité

Cet article établit une synergie entre la sécurité et une bonne conception de l’interface utilisateur. Je ne suis pas surpris par cette approche, car j’ai déjà constaté des coïncidences entre sécurité et bonne conception dans de nombreux domaines :

  • Au sein de l’organisation d’un programme, il y a des liens forts entre sécurité et modularité : toutes deux favorisent l’encapsulation, la réduction des dépendances entre composants, etc.

    Par ailleurs, le parallélisme et la concurrence sont des sujets à la mode en ce moment ; Mark Miller, qui travaille de la sécurité par capabilities, expose dans sa thèse, Robust Composition: Towards a Unified Approach to Access Control and Concurrency Control, un lien entre la conception de programmes sûrs et de programmes distribués.

  • Au niveau de la conception des langages de programmation, on observe là encore une corrélation forte entre certains principes de conception généraux des langages de programmation, et la conception de langages adaptés à la sécurité. Par exemple les références mutables globales, habituellement considérés comme néfastes à cause des effets de bords qu’elles permettent, sont vues comme des canaux cachés d’un point de vue sécurité.

    Les langages de programmation fonctionnels modernes sont ainsi, sans le savoir, propices à une programmation sûre. C’est une des choses que l’on peut retenir par exemple de l’expérience Emily que j’ai déjà mentionnée.

La sécurité est un domaine qui, au départ, ne m’intéressait pas des masses. La plupart des erreurs de sécurité exploitables sur les logiciels grand public sont liés à des défauts technologiques maintenant évidents (buffer overflow, SQL injection…) qui ravivent ma crispation devant l’inertie de certaines couches logicielles arriérées.

Mais cette synergie entre la sécurité et des aspects de conception qui m’intéressent beaucoup me pousse à m’y pencher de plus près. En particulier, les risques liés à la sécurité sont beaucoup plus évidents et perceptibles que les bénéfices de la bonne conception, et ils constituent donc un bon moyen de pression pour pousser les gens à améliorer leurs technologies.

Cette année, j’ai eu l’occasion de m’intéresser à nouveau aux capabilities, ainsi qu’au minimum vital de cryptographie. J’espère que j’aurai l’occasion d’écrire quelques billets à ce sujet.

[ tag:blog.huoc.org,2009:posts/securite-interface-utilisateur ]
Voir les commentaires · Commenter

/\

Annotation processing + instrumentation = language extensions

#. Par rz0. Mis à jour le 20.11 2010 à 12:19. Un commentaire.
annotations bytecode instrumentation java jvm languages

As promised, this is the first blog post about my work at Google on contract programming for Java.1 Today, I’m going to discuss the general rationale behind our choice of a combination of annotation processing and instrumentation to bring contracts to Java, the alternatives, and how it might or might not work for you if you’re also into extending the language.

This article is mainly for people interested in Java as a language, and its compilation-related aspects, but are not too familiar with the development environment itself. I fall into that category as well so don’t expect any mind-blowing technical tricks. :) And if you find errors, you’re welcome to report them.

The project I’ve worked on is actually based on Modern Jass. We chose this framework as a basis because we agreed this was the best approach to adding contracts to the Java language; as a result, many of the design decisions discussed below were originally explored in Johnannes Rieken’s work on Modern Jass.

The basic idea is to use two complementary interfaces offered by the Java development environment to achieve the goal of specifying and enforcing contracts on Java code:

  • annotation processing, as specified by JSR 2692 ;
  • and on-the-fly bytecode instrumentation, as introduced in Java 6.

Annotation processing lets you handle arbitrary compile-time constant information added to elements of code in Java source files, while bytecode instrumentation empowers you with the ability to rewrite classes entirely before they get loaded into the run-time environment. These techniques can be used in many different ways, and were not exactly meant to be mixed together either… So why these two, and why do we need both of them? Let’s take a look at a couple of usage patterns that appear to solve our problem.

1 You may want to read more about Design by Contract if you don’t know about it, to help understand the motivation behind the various techniques presented here. Wikipedia is a good starting point, and I’ve also personally replied to a comment on Reddit about the uses and purposes of contracts; you may want to have a look.

2 That’s Java 6 annotation processing. For those of you who have not kept track of this evolution, Java 5 had annotations, which basically had the same syntax, but vastly differing processing API based on Sun library extensions. Java 6 introduced a standardized annotation processing API under javax.annotation.processing and related name spaces.

What if we only had annotation processing?

Java 6 annotation processing, as specified by JSR 269, is a symbiotic pass that runs alongside the compiler. The Java compiler exposes a partially reified representation of classes to one or more annotation processors. These are free to generate new classes, however, the well-known limitation of this interface is that annotation processors cannot alter the classes being compiled. In fact, the program representation is read-only.

Consider the following example:

@A(x="foo")
class C {
  void f() { ... }
  int g(int) { ... }
}

The code above will be reified into a TypeElement (under javax.lang.model.element) object representing C, containing instances of ExecutableElement for each of the two methods, and referencing an AnnotationMirror object that, as its name implies, mirrors the annotation A.

The annotation processor would then be perfectly free to generate a class say C$froboz and get the Java compiler to process it along the way. But altering C directly, e.g. to add a method or a field, is prohibited under the regular API: the class as reflected through the javax.lang.model.element model is what will be compiled, regardless of the actions of the annotation processor (well, unless it halts the JVM or something).

Does that mean that it is impossible to add behavior to objects through annotation processing alone? Well, yes and no. Technically speaking, classes cannot be modified, that’s a fact; but it is also well-known that through cleverly twisting the inheritance hierarchy, it is possible to affect instances of a class. The pattern is officially encouraged; it is described in the class documentation of the Filer class, which manages the creation of source, class or resource files by annotation processors, as a decorator pattern.

The trick is that by extending and instantiating from classes that are generated by the annotation processor, it is possible to add to the interface (superclass) and implementation (subclass) of a given class that has the proper layout. For example:

@D(S.class)
class C extends C$$super {
  ...
}

In the above code, an annotation processor could catch the D annotation, and generate the class C$$super as follows to add a hello method to C:

@Generated
class C$$super extends S {
  void hello() {
    System.out.println("hello, world!");
  }
}

For a working example, you can take a look at this proof of concept of the decorator pattern applied to properties.

The main drawback is that it requires some strong cooperation from user code; also, composability is pretty bad: what if there are two frameworks that need to generate two different child classes? For contracts, which need to be enabled and disabled transparently, mandating that user code should interact with specific subclasses was never an option. The only use case I see is for mocking tools, but then, there are better ways. (But if you have an interesting case, feel free to prove me wrong, here. :)

Conclusion: Annotation processors are good at generating new classes, so if your language extension is purely generative by nature (e.g. some kind of templating), then it may be the most suitable option.

Interlude 1: Contract specifications as annotations

Up to now, we have assumed that contract specifications would be written as annotations. But what does it mean, really? First, what is a contract? Well, you could say it’s a kind of predicate that must hold at some points in time, e.g. before or after execution of a method, or between public method calls of an object.

So what we want to specify is basically a predicate. There are more than one way to do that. We can write predicates as code, or we could reify the concept into some data structure (e.g. com.google.common.base.Predicate).

The main constraint, on the technical side, is that annotations are compile-time constants, and initializations of annotation attributes must have a constant expression on the right-hand side.

@Foo(x + y)           // Looks cool but won't do.
@Foo("x + y")         // Ugly but OK.

This means that if we’re opting for the first solution, plain arbitrary code, it must be encoded as a string. As a consequence, any syntactic or semantic checks won’t be handled by the compiler, and must be done independently.

Yet, the second solution does not solve this problem completely either. Class constants are fine (think .class constructs), but references to members and method parameters, which are needed as arguments to the predicates, are not, and would need to be encoded as strings again.

All things considered, we decided to stick with the first approach, if only because it was the most natural way to write predicates to begin with!

So what if we don’t use an annotation processor at all?

After all, annotations are fine, but what use do we have for an annotation processor if all it can do is take a peek at the annotations? After all, we could just as well use the classical reflection API (java.lang.reflect) to access classes and their annotations at run time.

Then, using bytecode rewriting or generative techniques, it would be possible to do the same, or more, than with an annotation processor, without having to do anything at compile time. All the magic would be hidden, it’d be just like a library! Great!

Well, one major drawback with this approach is that any complex semantic checks are left till run time. In the case of contracts, that means any errors in the specifications will not be reported until the code gets executed. This is akin to spotting syntax errors while the code is running: it’s bad user experience, especially for a statically-typed language like Java, where users expect such things to be reported during compilation.

Conclusion: So, it seems like we’re reaching a first milestone here. We can have an annotation processor take care of all the static sanity checks, while delegating the effective work to a run-time component. This strategy was actually used in the original Modern Jass framework; and to be honest, I like it. I’d say, if your language extension can be implemented this way, go ahead, it’s pretty cool and clean.

Midway through my project though, I discovered there were some things that couldn’t be handled with this method…

Interlude 2: Why run-time contract compilation was not enough

The technique we’ve just discussed is fine if your features don’t depend on any aspect of Java that differs (significantly) between the source and bytecode representation. As discussed in a previous introductory blog post about the JVM bytecode (in French), the Java bytecode closely mirrors constructs of the Java language… to a point. It sticks enough to the language so that it’s a pain if you want to implement anything else on top of the JVM, but not enough so that all features of the language are reflected onto the JVM bytecode.

Worth mentioning in this category are: inner classes, anonymous classes, generics, as well as various little "details" such as covariant return types.

Well, hopefully you don’t need to deal with these, but in my case, it’s these little details that made us switch to a more complex contract compilation model (described below).

More specifically, contracts follow inheritance rules that closely match those of the actual source hierarchy. The problem was that the bytecode did not reflect that hierarchy exactly. The JVM has a simplified concept of inheritance, in which a method overrides another if they have the exact same signature. This is of course not true for Java, because of covariant returns and specialized generic extension.

class A {
  X f() { ... }
}

class B extends A {
  // With Y extends X, this is a covariant return override.
  Y f() { ... }
}

class G<T> {
  void f(T x) { ... }
}

class S extends G<Integer> {
  // Specialized override.
  void f(Integer x) { ... }
}

The Java compiler copes with that by spitting a bunch of bridge methods (see for example this Stack Overflow thread for more information on bridge methods). And while it is possible to infer the original override relationship from a bridge, it requires a lot of work, whereas the annotation processing API does that already for you, and for free. And with a source-level abstraction, too! That means if the Java language evolves to include more weirdness in the way it handles inheritance, at least this part maybe will not need to be upgraded. :)

And so, we ended up not only checking but also compiling contracts through our annotation processor. The run-time part was reduced to only handle weaving of the separately compiled bytecode (remember: we can’t alter classes being compiled) into the actual classes. The details of how this is accomplished deserve a blog post of their own, though, so stay around if you’re interested!

The combo: annotation processing and instrumentation

We’ve tried one and the other separately, so the last step is logically to combine both into something greater and better. The basic pattern becomes:

source files --[javac]-> class files  --[java]--> execution
                ||                       ||
annotations  --[apt]---> intermediate --[agent]-> run-time
                         information              behavior

In words: An annotation processor compiles annotations into some intermediate form suitable for exploitation by the run-time agent, reporting any errors to the user; at run time, an instrumentation agent uses the intermediate information to alter the behavior of the program (e.g. through bytecode rewriting).

How much information should be precompiled into the intermediate files and how much should be retrieved directly from the classes at run time by the agent depends on the application and is basically a design choice. Rationally, it’d be best to do as much offline as possible, to lower the overhead at run time, but some things may be more practical for the agent, so in the end, it’s up to the developer to pick the best compromise.

Conclusion: The most flexible among the three, if you need it. Otherwise, something simpler may prove more manageable.

Alternatives: There are

Annotation processing and bytecode instrumentation is one way (actually, we’ve seen three) to do it, but there are, of course, other approaches.

You could, for example, write a preprocessor that outputs Java from an augmented Java syntax. This is probably a bad idea, though, because Java, even less so than C, is not really designed to be an intermediate language. You won’t find any convenient #line directive, or anything like that. It also means you’ll have to edit the debug records to match your original source information after compiling the resulting Java code. I wouldn’t recommend this on a large scale.

Another possibility would be to roll out your own compiler. This is what JML did, but this has many problems, because basically it means you’re forking the language into your own. Upstream changes to the languages will need to be integrated back into your customized version, and it won’t play nicely with other extensions. So, unless you’re really really motivated and have the resources to afford this kind of scenario, I wouldn’t recommend this either.

A more modest alternative would be to replace the run-time instrumentation agent with an offline bytecode rewriter that produces standalone class files with modified bytecode. This was also implemented in our framework, as an optional compilation method.

Lastly, you could take the party to break the JSR 269 abstraction and dive into the collection of classes of the underlying compiler, typically OpenJDK Javac, which offer way more power, including full abstract syntax tree access (see annex B below for more details) and the ability to alter classes being compiled.

Limitations: What annotations and instrumentation cannot do

There are valid reasons, though, to pursue the route of having your own parser or compilation suite: If you need to alter the syntax in a way annotations do not permit.

Aside from the fact that writing code in double-quoted strings is a pain, annotation-based language extensions have other limitations. The main issue that you will run into is that annotation processing does not reflect the structure of classes below the level of members: that is, no code. In the same vein, anonymous classes are not represented. If you want to add something to these elements, then you need another way…

Similarly, instrumentation is great for optional functionality, such as contract checking, but for mandatory behavior, that is, the stuff that may create random crashes if not enabled, you’re probably better off with an offline compilation or bytecode modification strategy.

Conclusion: What’s best for your language extension needs?

Well, that depends on your needs. I don’t know if there is anyone reading this who doesn’t either already know about this whole thing or else didn’t understand anything. As I said, I kind of wrote it for people like me, who are not really Java experts but already have a good grip on compilation-related concepts. Why would anyone like that be interested in the subject, you ask? I guess out of curiosity, to keep in touch with what’s going on in the field, or maybe because, like me, they’ve been hired to do Java-related work even though they’re C programmers to the core. :)

In keeping with that pragmatic approach, I’d say a good rule of the thumb is probably to pick the simplest solution that works for you. If a single annotation processor will do, go for an annotation processor; if you only need a run-time agent, then only write a run-time agent; otherwise, you can always aim for both. And if you have any experiences or questions you’d like to share with me, you’re welcome to comment below!

[ tag:blog.huoc.org,2009:posts/java-annotations-instrumentation ]
Voir les commentaires · Commenter

>> Page : 0 1 2 3 4 5 6 7 8 9 10 11 12