Laurent Hubert

8 downloads 5827 Views 1MB Size Report
L'initialisation en Java pose des difficultés, que ce soit pour les champs, les objets ou ... Ainsi, nous décrivons une analyse de pointeurs nuls qui suit finement  ...
´ 2010 ANNEE

No d’ordre : 4243

` ´ DE RENNES 1 THESE / UNIVERSITE sous le sceau de l’Universit´e Europ´eenne de Bretagne pour le grade de ´ DE RENNES 1 DOCTEUR DE L’UNIVERSITE Mention : Informatique ´ Ecole doctorale Matisse pr´esent´ee par

Laurent Hubert pr´epar´ee au sein de l’´equipe Celtique `a l’IRISA (UMR 6074) Institut de Recherche en Informatique et Syst`emes Al´eatoires UFR Informatique et Electronique (ISTIC)

Foundations and Implementation of a

Th` ese soutenue ` a Rennes le 17 d´ ecembre 2010 devant le jury compos´e de :

Jean-Marc JEZEQUEL

Tool Bench for Static Analysis of Java Bytecode Programs

Professeur `a l’Universit´e de Rennes 1 / pr´esident

Erik POLL Associate Professor `a Radboud University Nijmegen / rapporteur

Anindya BANERJEE Research Professor `a IMDEA Software/examinateur

¨ Mario SUDHOLT

´ Professeur `a l’Ecole des Mines de Nantes / examinateur

Thomas JENSEN Directeur de recherche `a l’INRIA / directeur de th`ese

David PICHARDIE Charg´e de recherche `a l’INRIA/co-directeur de th`ese

ii

Remerciements Je souhaite tout d’abord remercier chaleureusement le jury, pour l’int´erˆet qu’il a port´e ` a mon travail et ` a ma pr´esentation, et pour le rapport qu’il a r´edig´e. Je souhaite particuli`erement ´ remercier Pierre-Etienne Moreau et Erik Poll d’avoir accept´e d’´evaluer mon rapport de th`ese et d’avoir pr´esent´e autant d’int´erˆet pour ce document, avec une mention sp´eciale pour Erik qui a en plus dˆ u venir des Pays-Bas pour ´evaluer la soutenance. Merci aussi `a Anindya Banerjee, qui a accept´e d’ˆetre examinateur ` a ma soutenance et donc de venir de Madrid pour l’occasion. Merci ensuite ` a Mario S¨ udholt d’avoir lui aussi accept´e d’ˆetre examinateur `a ma soutenance. Enfin, merci ` a Jean-Marc J´ezequel qui a accept´e de pr´esider ce jury. Je n’aurai probablement pas fait un doctorat si un certain nombre de personnes ne m’avait pas incit´e ` a faire ce choix, et je tiens a` les en remercier car cela a ´et´e une exp´erience tr`es riche. J’aurai ainsi pu ne pas faire de Master de recherche pour pouvoir faire un stage en Espagne. Je tiens donc ` a remercier Mireille Ducass´e qui, en me proposant un stage de recherche `a l’UPM, m’a permis de faire un Master de Recherche avec un stage `a Madrid. Je tiens aussi `a remercier le groupe de recherche CLIP, qui m’a accueilli `a l’UPM et qui a largement contribuer `a mon orientation vers le doctorat, et en particulier Manuel Hermenegildo, Germ´an Puebla, Elivra Albert, Astrid Beascoa et Samir Genaim. Enfin, David et Thomas ont aussi pris de leur temps pour me convaincre, et je les en remercie. Ces trois ann´ees de doctorat ont ´et´e intenses mais plaisantes. Le travail a ´et´e int´eressant, et je tiens `a remercier mes deux directeurs de th`ese pour les directions qu’ils m’ont indiqu´ees. J’ai aussi appr´eci´e travailler en ´equipe et je les remercie me l’avoir permis, que ce soit directement avec David, ou en encadrant des stagiaires et ing´enieurs. Merci d’ailleurs `a ces deux ing´enieurs, Nicolas et Vincent, avec qui il a ´et´e agr´eable de travailler. Ces trois ann´ees n’auraient pas non plus ´et´e les mˆemes sans leurs voyages. Merci `a David et Thomas de m’avoir permis de partir, mais aussi ` a Lydie et Christiane pour le support. Ensuite, il n’y a pas que le travail qui a ´et´e int´eressant, la bonne ambiance dans l’´equipe Lande/Celtique a aussi largement contribu´e `a rendre ces ann´ees plaisantes. Merci `a tous, avec une mention particuli`ere `a Florence et Pierre-Emmanuel, deux “co-bureau” bien sympathiques, et `a Benoˆıt, qui, en soutenant sa th`ese en mˆeme temps que moi, m’a permis de me sentir moins seul face `a la r´edaction et ` a l’administration. Je souhaite ´egalement remercier ceux qui ´etaient pr´esent lors de ma soutenance. Je ne me risquerai pas ` a une liste se voulant exhaustive de peur d’oublier quelqu’un, mais je tiens tout particuli`erement ` a remercier mes parents, mon oncle et ma tante d’ˆetre venu de Nantes pour me soutenir. Enfin, merci ` a Charlotte pour sa pr´esence et son soutient durant ces ann´ees et tout particuli`erement ` a l’approche de la soutenance.

iii

iv

REMERCIEMENTS

Abstract In this thesis we study the static analysis of Java bytecode and its semantics foundations. The initialization of an information system is a delicate operation where security properties are enforced and invariants installed. Initialization of fields, objects and classes in Java are difficult operations. These difficulties may lead to security breaches and to bugs, and make the static verification of software more difficult. This thesis proposes static analyses to better master initialization in Java. Hence, we propose a null pointer analysis that finely tracks initialization of fields. It allows proving the absence of dereferencing of null pointers (NullPointerException) and refining the intra-procedural control flow graph. We present another analysis to refine the inter-procedural control flow due to class initialization. This analysis directly allows inferring more precise information about static fields. Finally, we propose a type system that allows enforcer secure object initialization, hence offering a sound and automatic solution to a known security issue. We formalize these analyses, their semantic foundations, and prove their soundness. Furthermore, we also provide implementations. We developed several tools from our analyses, with a strong focus at having sound but also efficient tools. To ease the adaptation of such analyses, which have been formalized on idealized languages, to the full-featured Java bytecode, we have developed a library that has been made available to the community and is now used in other research labs across Europe.

v

vi

ABSTRACT

R´ esum´ e Dans cette th`ese, nous nous int´eressons `a l’analyse statique du bytecode Java. L’initialisation d’un syst`eme d’information est une phase d´elicate o` u des propri´et´es de s´ecurit´e sont v´erifi´ees et des invariants install´es. L’initialisation en Java pose des difficult´es, que ce soit pour les champs, les objets ou les classes. De ces difficult´es peuvent r´esulter des failles de s´ecurit´e, des erreurs d’ex´ecution (bugs), ou une plus grande difficult´e `a valider statiquement ces logiciels. Cette th`ese propose des analyses statiques r´epondant aux probl`emes d’initialisation de champs, d’objets et de classes. Ainsi, nous d´ecrivons une analyse de pointeurs nuls qui suit finement l’initialisation des champs et permet de prouver l’absence d’exception de pointeurs nuls (NullPointerException) et de raffiner le graphe de flot de contrˆole intra-proc´edural. Nous proposons aussi une analyse pour raffiner le graphe de flot de contrˆole inter-proc´edural li´ee `a l’initialisation de classe et permettant de mod´eliser plus finement le contenu des champs statiques. Enfin, nous proposons un syst`eme de type permettant de garantir que les objets manipul´es sont compl`etement initialis´es, et offrant ainsi une solution formelle et automatique `a un probl`eme de s´ecurit´e connu. Les fondations s´emantiques de ces analyses sont donn´ees. Les analyses sont d´ecrites formellement et prouv´ees correctes. Pour pouvoir adapter ces analyses, formalis´ees sur de petits langages, au bytecode, nous avons d´evelopp´e une biblioth`eque logicielle. Elle nous a permis de produire des prototypes efficaces g´erant l’int´egralit´e du bytecode Java.

vii

viii

´ ´ RESUM E

R´ esum´ e´ etendu Introduction Les fautes, ou bugs, sont fr´equentes dans le logiciel, si fr´equentes que les d´eveloppeurs et ´editeurs de logiciel ne souhaitent pas ˆetre tenus pour responsables. Ainsi, les licences de logiciels comportent g´en´eralement des clauses visant `a limiter les garanties fournies et leurs responsabilit´es. L’extrait suivant provient de la licence CeCILL. La responsabilit´e du Conc´edant [...] ne saurait ˆetre engag´ee en raison notamment : (i) [...], (ii) des dommages directs ou indirects d´ecoulant de l’utilisation ou des performances du Logiciel subis par le Licenci´e et (iii) plus g´en´eralement d’un quelconque dommage indirect. En particulier, les Parties conviennent express´ement que tout pr´ejudice financier ou commercial (par exemple perte de donn´ees, perte de b´en´efices, perte d’exploitation, perte de client`ele ou de commandes, manque ` a gagner, trouble commercial quelconque) ou toute action dirig´ee contre le Licenci´e par un tiers, constitue un dommage indirect et n’ouvre pas droit a r´eparation par le Conc´edant. ` Le logiciel peut donc causer des pertes financi`eres ou commerciales pour l’utilisateur sans que le distributeur du logiciel ne soit inqui´et´e. En d´epit de ces clauses, les fautes logicielles coˆ utent g´en´eralement quand mˆeme aux d´eveloppeurs et distributeurs de logiciels. En effet, la faible qualit´e d’un logiciel peut coˆ uter en r´eputation au distributeur. Certains logiciels sont distribu´es avec des licences qui offrent plus de garanties ` a l’utilisateur et l’autorisent, par exemple, `a demander le remboursement du logiciel. Le coˆ ut peut aussi ˆetre en termes de ressources quand le d´eveloppeur doit corriger l’erreur et distribuer un correctif. Dans le cas de logiciel o` u le correctif doit ˆetre install´e sur du mat´eriel tr`es d´eploy´e et non connect´e (des voitures ou des chaˆınes hi-fi par exemple), ce coˆ ut peut ˆetre tr`es ´elev´e. Enfin, le d´eveloppeur peut ˆetre aussi l’utilisateur, auquel cas toutes les cons´equences du mauvais fonctionnement du logiciel sont support´ees directement par lui. Dans ce dernier cas, la fameuse phrase “ce n’est pas moi, c’est l’informatique” permet quand mˆeme de se d´edouaner quelque peu. Pour toutes ces raisons, et malgr´e les clauses limitant les risques encourus par les distributeurs, la plupart des entreprises d´eveloppant du logiciel investissent temps et argent dans la qualit´e logicielle. Am´ eliorer la qualit´ e logicielle Il existe plusieurs outils pour am´eliorer la qualit´e des logiciels. Celui dans lequel les entreprises investissent le plus est tr`es certainement le test. Tester un programme consiste ` a l’ex´ecuter sur un jeu de test, c’est-` a-dire un ensemble d’entr´ees, et contrˆoler la sortie du programme avec un oracle (qui peut ˆetre un humain, une version pr´ec´edente du programme, un ix

´ ´ ETENDU ´ RESUM E

x

mod`ele du programme, etc.). Le test n’est pas exhaustif : il n’est pas possible de prouver l’absence d’erreur par le test car il n’est pas possible de tester un programme sur toutes ses entr´ees. Par cons´equent, tester un programme permet de gagner en confiance dans la correction du programme, mais des bugs peuvent toujours ˆetre pr´esents. Une autre approche est de prouver enti`erement la correction fonctionnelle de la sp´ecification formelle d’un logiciel, et de g´en´erer le code ` a partir de la sp´ecification.1 Elle a ´et´e utilis´ee dans l’industrie avec l’Atelier B et la M´ethode B [Abr96], et dans des recherches plus acad´emiques avec des assistants ` a la preuve tels que Coq [Coq] ou Isabelle/HOL [NPW02]. Cette technique requiert un haut niveau d’expertise et est habituellement tr`es coˆ uteuse en temps. Dans l’industrie, elle est seulement utilis´ee dans les cas o` u le coˆ ut d’une erreur peut ˆetre prohibitif, tels que dans les transports o` u un bug peut causer la perte de centaines de personnes. Enfin, la technique qui est sans doute la plus utilis´ee, bien que cela soit relativement discret, est l’analyse statique (AS). En effet, la majorit´e des d´eveloppements sont faits dans des langages int´egrant des syst`emes de types tels C, C] ou Java, et les syst`emes de types sont des AS.2 Une analyse d’un logiciel est statique si elle se fait sans ex´ecuter le logiciel. C’est une technique puissante qui permet de v´erifier automatiquement que des programmes respectent des propri´et´es vari´ees pouvant porter aussi bien sur des consommations de ressources, des types de donn´ees ou la confidentialit´e de donn´ees. Contrairement au test, l’AS peut ˆetre exhaustive : elle peut donner des informations sur le logiciel valides pour toutes les ex´ecutions du programme, quelles que soient les entr´ees. Un avantage de l’AS sur la preuve de correction manuelle est que les AS sont g´en´eralement enti`erement automatiques. N´eanmoins, cet automatisme vient au prix de l’ind´ecidabilit´e dans le cas g´en´eral : un analyseur statique ne pourra prouver correct certains programmes pourtant corrects (dans le sens o` u ceux-ci respectent la propri´et´e attendue). Un analyseur statique v´erifie qu’un code (source ou machine, une m´ethode ou un programme complet, etc.) respecte une propri´et´e. Si l’analyseur trouve un point du code qui viole cette propri´et´e, alors il l`eve une alarme appel´ee positif. Un analyseur peut lever de nombreux positifs pour un morceau de code analys´e, par exemple, toutes les lignes du code source qui appellent une certaine m´ethode. A cause de l’ind´ecidabilit´e de la plupart des propri´et´es, les analyseurs ne peuvent trouver l’ensemble exact des points du code violant la propri´et´e. Il l`eve donc des faux positifs ou des faux n´egatifs. Un faux positif est une alarme qui est lev´ee alors que le code respecte la propri´et´e mais que l’analyseur n’a pas r´eussi `a le prouver. Un faux n´egatif est une alarme qui n’a pas ´et´e lev´ee alors que le code ne respecte pas la propri´et´e. Une analyse correcte n’a aucun faux n´egatif. Une analyse compl`ete n’a aucun faux positif. Plusieurs m´ethodes sont utilis´ees pour faire face au probl`eme de l’ind´ecidabilit´e. – Une premi`ere approche pragmatique est diff´erencier les alarmes qui sont probablement correctes de celles qui sont probablement incorrectes. Cela peut se faire en utilisant des heuristiques ou des annotations de l’utilisateur auxquelles l’analyse fait confiance. Seules les alarmes probablement correctes sont ensuite lev´ees. Bien que cette approche soit incorrecte (puisque cela introduit des faux n´egatifs), cela permet aux d´eveloppeurs de se concentrer sur les alarmes qui correspondent plus probablement `a des bugs r´eels. Ces outils sont connus sous le nom de trouveur d’erreurs ou bug finders. Un exemple 1

Une approche similaire est de prouver directement la correction fonctionnelle du code, par exemple dans le cadre de JML, mais cela peut ˆetre vu comme un cas particulier o` u la sp´ecification est le code. 2 L’analyse statique est aussi tr`es utilis´ee dans les compilateurs pour un autre objectif que l’absence d’erreurs : l’optimisation du code.

xi notable est FindBugs [HSP06]. – Une autre approche consiste ` a utiliser des annotations sans leur faire confiance. V´erifier une preuve est plus facile que de la faire, et les annotations peuvent ˆetre vues comme des preuves partielles que l’analyse peut v´erifier au lieu de les prouver. Ces annotations peuvent aussi ˆetre vues comme des indices r´eduisant l’espace de recherche de l’analyse et permettant ainsi des analyses plus pr´ecises. Par exemple, le compilateur Java n´ecessite que l’utilisateur annote chaque variable avec son type. Cette approche permet de r´eduire le nombre de faux positifs. – Les AS peuvent aussi s’appliquer sur des langages sur lesquels il est plus facile de raisonner. Par exemple, les donn´ees sont sans doute plus simples `a suivre dans un langage fonctionnel o` u, par d´efaut, il n’y a pas de r´ef´erences et les d´efinitions associent directement une valeur ` a un nom. En d´epit d’un syst`eme de types riche (qui rend l’espace de recherche plus important), il est possible d’inf´erer les types pour les programmes ´ecrits en ML. Inversement, en Java, toutes les variables sont mutables (mˆeme les champs final au niveau du bytecode), leur d´eclaration est s´epar´ee de leur initialisation, et l’initialisation des champs, objets et classes en Java est particuli`erement difficile comme nous le montrons dans cette th`ese. – Enfin, l’AS permet d’inf´erer des invariants qui peuvent ˆetre utilis´es pour aider le d´eveloppement (refactoring, reverse engineering), une preuve de correction assist´ee ou une autre AS. Comme cette th`ese le montre, Java (ou le bytecode Java) n’est pas un langage sur lequel il est facile de raisonner. C’est un langage incluant de nombreuses fonctionnalit´es, industriellement utilis´e, et utilisant des sch´emas d’initialisation complexes. Concevoir des analyses `a la fois correctes et pr´ecises pour Java n’est donc pas une chose facile. Cette th`ese propose des analyses dont la correction est formellement prouv´ee et des outils pour aider au d´eveloppement d’analyses correctes pour le bytecode Java. Ces contributions peuvent ˆetre utilis´ees directement pour assurer des propri´et´es de s´ecurit´e (telle que pr´esent´e Chapitre 7), ou comme fondation pour rendre les analyses plus pr´ecises et plus simples `a d´evelopper. Java et bytecode Java Java [GJSB05] est un langage source. Il est g´en´eralement compil´e vers du code objet, ou bytecode, qui est le langage de bas niveau interpr´et´e par la machine virtuel Java (JVM) [MD97]. Java poss`ede de nombreuses constructions aux effets similaires mais qui peuvent ˆetre plus o` u moins faciles ` a lire selon les situations. Java est aussi un langage qui ´evolue et de nouvelles ` l’inverse, le bytecode Java propose fonctionnalit´es sont r´eguli`erement ajout´ees au langage. A beaucoup moins de constructions syntaxiques et ´evolue beaucoup moins, les nouvelles fonctionnalit´es de Java ´etant compil´ees en utilisant des fonctionnalit´es pr´eexistantes du bytecode. De plus, on peut souhaiter analyser un programme sans en avoir le code source ; c’est par exemple le cas du v´erificateur de bytecode (BCV) qui v´erifie au chargement des classes par la JVM que celles-ci respectent le syst`eme de types de la JVM. C’est pour ces raisons que nous nous int´eressons dans cette th`ese au bytecode et non au code source.

Analyse de pointeurs nuls et initialisation des champs Les d´er´ef´erencements de pointeurs nuls en Java sont une source d’erreurs importante. Prouver leur absence apparaˆıt donc int´eressant. De plus, la pr´ecision des analyses statiques

xii

´ ´ ETENDU ´ RESUM E

d´epend de la pr´ecision du graphe de flot de contrˆole (CFG). Or, en Java, les d´er´ef´erencements de pointeurs nuls g´en`erent des exceptions qui sont la cause de branchements suppl´ementaires, soit des arc suppl´ementaires dans le CFG intra-proc´edural. Ces arcs sont pr´esents entre chaque instruction pouvant lever une exception et le gestionnaire d’exceptions correspondant (handler ), ou la fin de la m´ethode ou du programme s’il n’y a pas de gestionnaire d’exceptions. Bien que la plupart des instructions puissent lever des exceptions, la plupart sont g´en´eralement sˆ ures. Par exemple, en Java, chaque instruction du type o.f peut lever une exception si o est nul. Si une analyse peut prouver que o est toujours diff´erent de nul, il est alors possible de retirer un arc du CFG et ainsi d’am´eliorer la pr´ecision des analyses reposant sur la pr´ecision du CFG intra-proc´edural. L’une des difficult´es dans la conception d’une analyse de pointeurs nuls pour le bytecode Java est l’initialisation des champs. Ainsi, n’´ecrire que des valeurs non nulles dans un champ ne permet pas d’assurer que seulement des valeurs non nulles ne puissent ˆetre lues de ce champ. En effet, les champs sont tous nuls par d´efaut ; une analyse un peu simple inf´ererait donc que tous les champs peuvent ˆetre nuls sans plus de pr´ecision. L’une des id´ees cl´e ` a la base de cette analyse d’inf´erence est de suivre finement l’initialisation des champs dans les constructeurs et m´ethodes appel´ees `a partir des constructeurs. ` la fin d’un constructeur, tous les champs d´efinis dans la classe courante qui n’ont peut A ˆetre pas ´et´e explicitement initialis´es sont annot´es @Nullable, les autres champs ´etant annot´es conform´ement ` a la valeur avec laquelle ils ont ´et´e initialis´es (par exemple, @NonNull s’ils ont ´et´e initialis´es avec la r´ef´erence d’un objet). Pour la conception de notre analyse, nous avons d´efini un domaine abstrait State ] et une sp´ecification ` a base de contraintes qui contraint S ] ∈ State ] en fonction d’un programme P ] (´ecrit S |= P ). Une valeur du domaine abstrait S ] ∈ State ] abstrait l’ensemble des ´etats atteignables de P . Un composant essentiel de State ] est le domaine des valeurs Val ] . Val ] = {MayBeNull , NotNull , Raw } ∪ {Raw (C) | C ∈ Classes} ∀C1 , C2 , C1  C2 =⇒ NotNull v Raw (C1 ) v Raw (C2 ) v Raw v MayBeNull Le relation  est la relation de sous-typage sur les classes : C1  C2 si C2 est un parent (une super classe) de C1 . Raw abstrait les r´ef´erences non nulles vers des objets possiblement en cours d’initialisation. NotNull abstrait les r´ef´erences non nulles vers des objets ayant termin´e leurs constructeurs. MayBeNull d´esigne une r´ef´erence quelconque ou la constante null, c’est le maximum (>) de notre treillis. Raw (C) abstrait les r´ef´erences non nulles vers les objets ayant termin´e un constructeur de la classe C (et donc aussi un constructeur de chaque parent de C). Lors de la lecture d’un champ par une instruction o.f, si la variable o est de type Raw(C) (ou d’un sous-type) et que le champ f est d´eclar´e dans la classe C, alors l’abstraction du champ f est utilis´ee car l’objet est suffisamment initialis´e. Sinon, l’objet n’est peut-ˆetre pas suffisamment initialis´e et le champ f est consid´er´e comme pouvant ˆetre nul, donc abstrait par MayBeNull . Pour prouver la correction de notre analyse, nous avons proc´ed´e comme suit. – Nous avons d´efini un langage, proche du bytecode Java mais sans pile, avec une s´emantique exprim´ee sur un domaine concret State. – Nous avons donn´e l’interpr´etation du domaine abstrait State ] dans le domaine concret avec une relation ∼∈ State ] × State. – Nous avons d´efini la propri´et´e safe(JP K) qui est v´erifi´ee lorsque tous les ´etats accessibles du programme P sont sˆ urs (c’est-`a-dire qu’il ne peut y avoir d’exception de pointeur nul). Bien sˆ ur, JP K n’est pas calculable en g´en´eral.

xiii – Nous avons d´efini la propri´et´e safe ] (S ] ) qui est v´erifi´ee si S ] permet d’assurer safe(JP K) ´etant donn´e que S ] est une sur-approximation des ´etats de JP K, c’est-`a-dire que pour tout S ∈ JP K, S ] ∼ S est v´erifi´e. – Nous avons prouv´e la correction de l’analyse, c’est-`a-dire que si S ] est une solution du syst`eme de contraintes pour le programme P (S ] |= P ) et si safe ] (S ] ) est v´erifi´e, alors safe(JP K) est v´erifi´e (safe ] (S ] ) ∧ S ] |= P =⇒ safe(JP K)). Notre analyse ne n´ecessite aucune annotation de la part de l’utilisateur, on peut cependant la comparer au syst`eme de types propos´e par F¨ahndrich et Leino[FL03]. Nous avons montr´e que pour tout programme correct vis-`a-vis de leur syst`eme de types, notre analyse peut inf´erer des annotations S ] |= P telles que safe ] (S ] ) et donc montrer que le programme est sˆ ur safe(JP K). Comme de plus notre analyse est prouv´ee correcte, cela prouve indirectement la correction de leur syst`eme de types (ou plutˆot de la formalisation que nous proposons de leur syst`eme de types). Ces travaux ont principalement ´et´e r´ealis´es avec David Pichardie et publi´es dans la conf´erence internationale Formal Methods for Open Object-based Distributed Systems (FMOODS) [HJP08a].

Nit : un outil d’inf´ erence d’annotations de nullit´ e pour le bytecode Java Nous pr´esentons maintenant Nit (Nullability Inference Tool ), une impl´ementation de notre analyse de pointeurs nuls pr´esent´ee pr´ec´edemment. Cette analyse a ´et´e formellement d´efinie sur un petit langage id´ealis´e, relativement haut niveau et abstrayant de nombreux d´etails du bytecode Java. Pr´esenter l’analyse `a ce niveau est important pour avoir une pr´esentation concise, centr´ee sur l’essentiel, et facilitant la preuve de correction. N´eanmoins, l’implantation ne peut se faire ` a ce niveau et, comme expliqu´e dans pr´ec´edemment, l’outil analysera du bytecode Java.

Analyse d’alias L’une des particularit´es du langage haut niveau utilis´e pour la sp´ecification de l’analyse ´etait l’absence de pile. Le bytecode est quant `a lui un langage `a pile. Il inclut aussi des instructions qui permettent d’obtenir des informations sur la nullit´e de r´ef´erence. Par exemple, l’instruction ifnull jmp d´epile un ´el´ement de la pile et saute n octet d’instruction si l’´el´ement d´epil´e est nul. Pour tester la nullit´e d’une variable locale x, on empile le contenu de x (load x), puis l’instruction ifnull n permet de tester le contenu du sommet de pile. L’analyse peut donc inf´erer que si le test ´echoue, alors l’´el´ement d´epil´e est non nul. En revanche, sans information suppl´ementaire, elle n’a aucune information sur x. Nous proposons donc une analyse qui inf`ere des ´egalit´es entre variables locales et ´el´ements de pile.

Une nouvelle valeur abstraite Nous supposons avoir deux fonctions, une fonction d’abstraction α ∈ 2Val → Val ] et une fonction de concr´etisation γ ∈ Val ] → 2Val o` u Val est le domaine concret des r´ef´erences (incluant la constante null) et o` u Val ] est le domaine abstrait. Si une variable peut soit contenir une r´ef´erence de type NotNull soit la constante null, elle est alors abstraite par NotNull tα({null}) = MayBeNull . Un test de nullit´e peut alors ˆetre utilis´e pour retrouver de

xiv

´ ´ ETENDU ´ RESUM E

l’information mais MayBeNull abstrait aussi les objets en cours d’initialisation, la meilleure abstraction que l’on puisse retrouver est donc α(γ(MayBeNull ) \ {null}) = Raw . Cette configuration se produit fr´equemment dans les programmes et nous avons donc introduit une nouvelle valeur abstraite, MayBeNullInit qui permet de manipuler des valeurs pouvant ˆetre nulles sans introduire de valeur Raw .

Analyse des instructions instanceof Le bytecode Java poss`ede l’instruction instanceof qui met 1 sur la pile si le sommet de pile est une instance de C (et n’est donc pas nul), ou 0 sinon. Bien que cette instruction semble donner une information sur la nullit´e d’une variable, cette information n’est pas directe : on ne peut rien d´eduire tant qu’un test n’est pas effectu´e sur le sommet de pile. Or, l’analyse ne mod´elise pas les entiers (ni les bool´eens). Nous avons donc ajout´e une analyse suppl´ementaire qui calcule une abstraction de la pile telle que, pour chaque variable de pile, l’abstraction contient une sous-approximation de l’ensemble des variables locales qui doivent ˆetre non nulles si la variable de pile correspondante est ´egale `a 1.

Conclusion L’analyse globale est une analyse en trois ´etapes (analyse intra-proc´edurale d’alias, analyse intra-proc´edurale des instanceof, et analyse inter-proc´edurale de pointeurs nuls) ex´ecut´ee sur un programme complet. Pour passer `a l’´echelle, de nombreuses optimisations ont ´et´e faites. Il est maintenant possible d’analyser des programmes cons´equents (3.400 classes ou 26.000 m´ethodes) en 2 minutes. L’analyse permet d’inf´erer que pr`es de 53% des champs sont non nuls. L’objectif n’´etant pas 100% (car des champs sont effectivement nuls), il n’est pas simple d’´evaluer la pr´ecision de ces 53%. En revanche, avec les annotations inf´er´ees, il est possible de prouver que 80% des d´er´ef´erencements sont sˆ urs. En comparaison, sans les adaptations pr´esent´ees dans cette section, l’analyse permet de prouver 69% des d´er´ef´erencement sˆ urs. Bien que ces r´esultats soit insuffisants pour trouver des erreurs (bugs), ils permettent d’am´eliorer la pr´ecision du CFG et sont utiles pour de la documentation ou du reverse engineering. Nous avons d´evelopp´e pour cet outil un greffon (plug-in) pour pouvoir l’utiliser `a partir d’Eclipse. Ce greffon propose des options pour r´eduire le nombre de positifs et faciliter son utilisation pour trouver des bugs. Nit et le greffon ont ´et´e pr´esent´es `a la conf´erence JavaOne et sont disponibles sous licence GPL ` a http://nit.gforge.inria.fr. Ce travail ` a ´et´e publi´e ` a l’atelier ACM Program Analyis for Software Tools and Engineering (PASTE) [Hub08].

Sawja : atelier d’analyse statique pour Java Lors du d´eveloppement de Nit, une grande partie du code ´ecrit n’´etait pas propre ` a l’analyse d´evelopp´ee mais bien plus g´en´erale. Ce code permettait de fournir une repr´esentation OCaml des fichiers binaires .class contenant le bytecode Java, de naviguer facilement dans la hi´erarchie de classe et dans le graphe de flot de contrˆole, etc. Une partie importante de l’effort de d´eveloppement de Nit a ´et´e sur l’efficacit´e du code produit, et donc aussi sur ces couches les plus basses. Pour faciliter le d´eveloppement d’analyseurs statiques correctes et efficaces, nous avons donc d´ecid´e de d´evelopper Sawja. Sawja est une biblioth`eque logicielle d´evelopp´ee en OCaml ` a partir du code de Nit et de l’exp´erience acquise lors de ce d´eveloppement.

xv

Repr´ esentation des classes de haut niveau L’utilisation du langage OCaml permet l’utilisation du typage pour exprimer des contraintes structurelles. Par exemple, classe et interface, bien que simplement diff´erenci´ees par un drapeau au niveau binaire, utilisent deux structures diff´erentes au niveau OCaml. Quand on veut les manipuler indiff´eremment, cela reste possible car des fonctions sont fournies qui permettent d’acc´eder ` a leurs champs communs. Exprimer les contraintes structurelles facilite l’´ecriture de code car il n’est plus n´ecessaire de se prot´eger de nombreux cas impossibles. Par exemple, il n’est plus utile de g´erer le cas d’une interface non abstraite. Afin d’´eviter une trop forte duplication du code, nous avons utilis´e les variants disponibles en OCaml. Les variants sont un type d’´enum´eration permettant le partage des constructeurs. Par exemple, lorsqu’une valeur de type jvm type ou java basic type est attendue, un mˆeme constructeur peut ˆetre utilis´e pour le type entier 64 bits (‘Long) dans les deux cas. Afin d’´eviter l’analyse syntaxique des m´ethodes non accessibles, le parsing est paresseux. Le partage des constantes au sein d’une classe est assur´e au niveau bytecode grˆace `a une table (constant pool ). Les instructions contiennent alors des indices de cette table au lieu des donn´ees. Sawja garde ce partage en m´emoire mais cache l’indirection en maintenant un nouvel indi¸cage. De plus, le partage est ´etendu `a toutes les classes charg´ees : cela permet ainsi d’utiliser des tests d’´egalit´e physique l`a o` u des ´egalit´es structurelles auraient ´et´e n´ecessaires et l’indi¸cage permet d’utiliser des structures de donn´ees efficaces sur les entiers comme les arbres de Patricia [Mor68] ou les BDDs [Bry92].

Repr´ esentation interm´ ediaire Le bytecode Java est un langage `a pile et l’utilisation intensive de la pile d’op´erandes rend difficile l’adaptation des analyses statiques classiques qui ont ´et´e d´efinies sur un langage `a variables et expressions. Ainsi, plusieurs outils d’analyse et d’optimisation de bytecode Java travaillent en fait sur une repr´esentation interm´ediaire, rendant l’analyse plus ´ simple [BCF+ 99, VRCG+ 99]. Etonnamment, la correction des transformations du bytecode Java vers ces repr´esentations interm´ediaires ne semble pas avoir ´et´e ´etudi´ee formellement. Demange et Pichardie ont ´etudi´e les fondations s´emantiques de ces transformations et ont propos´e un langage interm´ediaire avec une transformation pour laquelle ils ont prouv´e un th´eor`eme de pr´eservation s´emantique. Le langage propos´e est sans pile, avec des expressions sans effets de bords (des variables suppl´ementaires peuvent donc ˆetre n´ecessaires). La cr´eation d’objets, qui est souvent une op´eration d´elicate pour les analyses statiques, se fait en deux ´etapes au niveau du bytecode Java. Elle est ramen´ee `a une unique op´eration au niveau de la repr´esentation interm´ediaire, comme au niveau Java, ce qui facilite, l`a aussi, l’implantation des analyses. Une validation exp´erimentale de la transformation a aussi ´et´e r´ealis´ee et montre qu’elle est 10 fois plus rapide que Soot, le principal concurrent, et comparable en nombre de variables introduites.

Programmes complets Un programme complet d´esigne l’ensemble du code accessible `a partir des points d’entr´ees du programme. Quand on analyse une m´ethode, par exemple, il est souvent n´ecessaire d’avoir une abstraction des entr´ees. Certaines analyses utilisent pour cela des annotations de l’utilisateur (types, invariants, pr´e- ou post-conditions), mais il est aussi possible de calculer cette

xvi

´ ´ ETENDU ´ RESUM E

information ` a partir des diff´erents contextes d’appel possibles, r´ecursivement. Avoir un programme complet permet de fournir une sur-approximation de l’ensemble des contextes d’appel. Sawja propose une repr´esentation des programmes complets avec une API permettant de naviguer dans le CFG du programme. Sawja propose aussi plusieurs analyses permettant de construire des programmes complets, entre autre, CRA, RTA et XTA. Nous avons con¸cu CRA (Class Reachability Analysis) pour charger tr`es rapidement des programmes pouvant ˆetre cons´equents en tirant partie du caract`ere paresseux du chargement des m´ethodes. CRA utilise en effet les informations contenues dans les tables des constantes des classes pour calculer une sur-approximation du code accessible. RTA [BS96] est une analyse connue et efficace qui nous permet de comparer la performance de Sawja `a celle de Wala : Wala prend trois fois plus de temps et consomme 75% de m´emoire en plus.

Conclusion Sawja est la premi`ere biblioth`eque proposant des outils pour le d´eveloppement d’analyseur statique pour le Java bytecode. Elle repr´esente un effort de codage de 1,5 homme-an et environ 22.000 lignes de code OCaml (commentaires inclus), dont 4.500 pour les interfaces. Forts de notre exp´erience sur Nit, nous avons con¸cu Sawja comme une biblioth`eque g´en´erique permettant ` a tout nouvel analyseur statique de b´en´eficier des mˆemes composants efficaces. Sawja a d´ej` a ´et´e utilis´ee dans deux prototypes pour l’ANSSI (Agence nationale de la s´ecurit´e des syst`emes d’information) dont l’un est l’implantation du syst`eme de types garantissant l’initialisation des objets pr´esent´ee ci-apr`es. Nit a aussi ´et´e port´e sur la version actuelle de Sawja, ce qui, d’apr`es nos premiers essais, a permis d’am´eliorer ses performances de 30%. Sawja est disponible sous licence GPL `a http://sawja.inria.fr/. Ces travaux ont ´et´e publi´es dans les actes de la conf´erence internationale Formal Verification of Object-Oriented Software (FoVeOOS) [HBB+ 10]. La couche la plus basse de Sawja (analyse syntaxique) a ´et´e initialement d´evelopp´ee par Nicolas Cannasse, la repr´esentation interm´ediaire est une contribution de Delphine Demange et David Pichardie, l’implantation de RTA est une contribution de Nicolas Barr´e, l’utilisation de variant OCaml pour la factorisation des types est une contribution de Tiphaine Turpin, et enfin d’autres personnes ont contribu´e avec de plus petits d´eveloppements, des correctifs et des discussions sur la concep´ tion de la biblioth`eque : Etienne Andr´e, Fr´ed´eric Besson, Florent Kirchner et Vincent Monfort. En d´epit de ces nombreuses contributions, je suis le d´eveloppeur principal de la biblioth`eque et ma contribution repr´esente environ 40% du code de la biblioth`eque.

Initialisation de classes En Java, l’initialisation des classes, et donc des champs statiques, est implicite et paresseuse. G´en´eralement, pour un programme, un grand nombre d’instructions est susceptible de d´eclencher l’initialisation d’une classe. Cela rend le flot de contrˆole tr`es peu intuitif pour un d´eveloppeur et tr`es impr´ecis et tr`es dense (car c’est une sur-approximation qui est consid´er´ee) pour une analyse statique. Nous proposons ici une solution pour am´eliorer la pr´ecision du graphe de flot de contrˆ ole tenant compte de l’initialisation des classes. Elle permet aussi une analyse plus fine des champs statiques et en particulier de d´etecter des utilisations de champs statiques avant leur initialisation.

xvii Les contributions de ces travaux sont les suivantes. (i) Nous rappelons que l’initialisation implicite et paresseuse rend le CFG difficile `a calculer. (ii) Nous identifions des exemples de code que l’on souhaite pouvoir rejeter et d’autre que l’on souhaite pouvoir accepter. (iii) Nous proposons un langage pour l’´etude de l’initialisation des classes et des champs statiques. Ce langage abstrait de nombreux d´etails du bytecode Java et rend explicite l’initialisation de classes en introduisant une instruction initialize(C) qui a pour effet de d´eclencher l’initialisation de la classe C si celle-ci n’a pas d´ej`a ´et´e commenc´ee. (iv) Nous proposons une analyse prouv´ee correcte pour am´eliorer la pr´ecision du CFG et calculer l’ensemble des champs statiques initialis´es ` a chaque point de programme. (v) Cette analyse n’´etant pas suffisamment pr´ecise, nous proposons une autre analyse plus pr´ecise, sensible au contexte. (vi) Nous d´etaillons quelques pistes pour une implantation efficace de cette seconde analyse.

Graphe de flot de contrˆ ole peu intuitif L’initialisation des classes est faite par des m´ethodes sp´ecifiques qui ne peuvent ˆetre appel´ees que par la JVM. Ces m´ethodes contiennent du code arbitraire et peuvent donc d´eclencher l’initialisation d’autres classes. Les seules instructions pouvant d´eclencher l’initialisation d’une classe sont la lecture et l’´ecriture d’un champ statique, l’appel d’une m´ethode statique et la cr´eation d’une instance d’une classe. L’initialisation d’une classe est d´eclench´ee lorsqu’une telle instruction est rencontr´ee et si la classe n’a pas d´ej`a ´et´e initialis´ee. Le flot de contrˆole li´e ` a l’initialisation de classes, ne d´epend donc pas de la syntaxe (comme les appels de m´ethodes statiques) ni des donn´ees (comme les appels de m´ethodes virtuelles), mais de l’historique des classes initialis´ees.

Analyse de l’´ etat d’initialisation des classes La seconde analyse est fond´ee sur une abstraction de l’´etat d’initialisation de chaque classe. Chaque classe peut ˆetre dans l’un des trois ´etats suivants. Une classe peut ne pas avoir d´ebuter son initialisation (´etat α). C’est l’´etat de toutes les classes au d´ebut du programme. Une classe peut ˆetre en cours d’initialisation (´etat β). Une classe est vue dans cet ´etat par tout le code accessible depuis l’initialiseur de la classe. Enfin, une classe peut ˆetre compl`etement ` l’ex´ecution, une classe ne peut ˆetre que dans un seul ´etat `a un instant initialis´ee (´etat γ). A donn´e. L’analyse abstrait cette information en calculant des ´etats d’initialisation abstraits du programme (IS ] ). IS ] = P(Classes × {α, β, γ}) Un ´etat d’initialisation associe ` a chaque classe les ´etats d’initialisation dans lesquelles la classe peut se trouver. L’analyse propos´ee est une analyse de flot de donn´ees, sensible au contexte, utilisant cette abstraction. La fonction de transfert essentielle est celle de l’instruction d’initialisation initialize(C). Le flot de donn´ees peut ˆetre propag´e `a l’initialiseur si la classe peut ˆetre dans l’´etat α d’apr`es l’abstraction de l’´etat d’initialisation courant IS ∈ IS] . Dans ce cas, γ est ajout´e ` a l’´etat d’initialisation au point de programme suivant et la post-condition calcul´ee pour l’initialiseur de classe peut ˆetre utilis´ee (assum´ee).

Conclusion L’initialisation de classes, et donc aussi des champs statiques, est donc plus complexe qu’il peut y paraˆıtre ` a un premier abord. Ainsi, bien que dans la plupart des cas le comportement

xviii

´ ´ ETENDU ´ RESUM E

r´eel correspond au comportement attendu, ce n’est pas toujours le cas, et une analyse statique correcte ne peut se contenter de supposer que l’initialisation d’une classe a toujours lieu avant son utilisation. Nous avons donc propos´e un langage pour l’´etude de ce probl`eme qui permet de s’abstraire du bytecode Java en conservant le m´ecanisme d’initialisation de classes ainsi qu’une analyse qui permet d’inf´erer, pour chaque point de programme, l’ensemble des ´etats d’initialisation des classes et l’ensemble des champs statiques qui ont d´ej`a ´et´e initialis´es. Une telle analyse peut ˆetre utilis´ee directement pour v´erifier que les champs statiques sont initialis´es avant leur premi`ere lecture. Elle peut aussi ˆetre utilis´ee pour am´eliorer l’analyse de pointeurs nuls pr´esent´ee pr´ec´edemment sachant que si un champ est initialis´e avant une lecture, alors l’abstraction du champ peut ˆetre l’union de l’ensemble des valeurs ´ecrites dans le champ. Une partie de ces travaux a ´et´e r´ealis´ee avec David Pichardie et publi´ee `a Bytecode Semantics, Verification, Analysis and Transformation (ByteCode) [HP09].

Garantir l’initialisation des objets L’initialisation d’un syst`eme d’information est une phase d´elicate o` u des propri´et´es de s´ecurit´e sont v´erifi´ees et des invariants install´es. Il est donc important de garantir que seulement des objets enti`erement initialis´es puissent ˆetre librement manipuler par le programme et que les objets partiellement initialis´es sont pr´ecis´ement suivis. Plusieurs failles de s´ecurit´e importantes du JRE (Java Runtime Environment) avaient pour cause des objets partiellement initialis´es. Nous proposons ici un syst`eme de types fond´e sur l’id´ee propos´ee par F¨ahndrich et Leino pour leur syst`eme de types de pointeurs nuls que nous avons d´ej`a aussi utilis´e pour notre analyse de pointeurs nuls : le suivi des objets en cours d’initialisation avec le type Raw (C). Ce syst`eme de types permet `a un d´eveloppeur d’exprimer une politique d’initialisation : quelles variables peuvent r´ef´erencer des objets en cours d’initialisation. Exprimer cette politique sous forme d’un syst`eme de types offre l’avantage qu’il devient possible de v´erifier automatiquement la politique. L’exemple suivant montre une classe qui durant l’ex´ecution de son constructeur s’assure que (i) l’utilisateur ` a la permission d’´ecrire dans le dossier /tmp (sinon la m´ethode checkPermission l`eve une exception) et (ii) initialise un champ non null field par l’interm´ediaire de la m´ethode inits.

class SensitiveClass { private Object non_null_field ; SensitiveClass (){ inits (); SecurityManager sm = System . g et Sec ur it yM an ag er (); if ( sm != null ){ sm . checkPermission ( new java . io . FilePermission ( " / tmp / - " ," write " )); } } protected void inits (){ this . non_null_field = new Object ();} public void sensitiveMethod (){...} }

Cette classe poss`ede plusieurs d´efauts de conception qui peuvent ne pas ˆetre ´evidents ` a voir au premier abord : elle poss`ede au moins deux failles permettant d’appeler la m´ethode sensitiveMethod sans avoir les droits d’´ecriture sur /tmp. L’exemple suivant propose une

xix classe Attacker exploitant ces deux vuln´erabilit´es. La premi`ere vuln´erabilit´e est li´ee `a l’utilisation d’une m´ethode virtuelle pour initialiser le champ non null field : il est possible de surcharger cette m´ethode et donc d’appeler la m´ethode sensible avant que les permissions soient v´erifi´ees. La seconde vuln´erabilit´e est li´ee `a la m´ethode finalize qui est appel´ee par le ramasse-miette (garbage collector ) avant la lib´eration m´emoire d’un objet. En effet, lorsque l’utilisateur n’a pas la permission d’´ecrire dans /tmp, la m´ethode checkPermission ´echoue, interrompant la construction de l’objet et le rendant normalement inaccessible, pouvant donc ˆetre collect´e par le ramasse miette. Lorsque la m´ethode finalize est appel´ee sur l’objet, elle peut appeler la m´ethode sensitiveMethod.

class Attacker extends SensitiveClass { protected void inits (){ this . sensitiveMethod (); }

void finalize (){ this . sensitiveMethod ();} public static void main ( String args []){ try { Attacker o = new Attacker ();} catch ( Throwable e ){...}

} }

Le probl`eme est connu et a ´et´e ` a l’origine de plusieurs failles de s´ecurit´e, mais aucune solution statique n’a ´et´e propos´ee pour r´esoudre ce probl`eme. Nous proposons un jeu d’annotations Java 5 pour que le d´eveloppeur puisse annoter son code pour sp´ecifier la politique qu’il souhaite voir assur´ee. V ANNOT ::= @Init | @Raw | @Raw(CLASS) R ANNOT ::= @Pre(V ANNOT) | @Post(V ANNOT)

Une annotation produite par la r`egle V ANNOT peut ˆetre utilis´ee pour les champs, les valeurs de retour et les param`etres des m´ethodes. Les receveurs des m´ethodes virtuelles peuvent avoir une annotation diff´erente au d´ebut et `a la fin de la m´ethode, d’o` u la r`egle de production R ANNOT. Dans l’exemple pr´ec´edent, il suffirait d’annoter la m´ethode sensitiveMethod avec @Pre(@Init). Nous avons formalis´e la v´erification statique de la coh´erence des annotations sous forme d’un syst`eme de types. Cela permet de rejeter les classes qui, comme la classe Attacker dans l’exemple pr´ec´edent, pourraient essayer d’acc´eder `a une m´ethode n´ecessitant un objet initialis´e alors que l’objet poss´ed´e est partiellement initialis´e. Un tel syst`eme est modulaire : les classes peuvent ˆetre v´erifi´ees une `a une et, quand une classe ne respecte pas la politique, le programme peut ˆetre arrˆet´e sans que la vuln´erabilit´e d´etect´ee n’ait pu ˆetre utilis´ee. Cependant, pour ˆetre correcte, l’analyse doit ˆetre ex´ecut´ee sur le programme complet, par exemple au fur et `a mesure du chargement par la JVM. Nous avons ´evalu´e exp´erimentalement le nombre d’annotations n´ecessaires pour v´erifier des classes existantes de la biblioth`eque Java. D’apr`es nos exp´eriences, pour v´erifier 380 classes des 381 classes des paquets java.lang, java.security et javax.security, seulement 43 annotations ont ´et´e ajout´ees sur les 131.486 lignes de code source Java. Une classe n’a pas pu ˆetre v´erifi´ee `a cause d’une limitation de notre syst`eme de types sur les tableaux : il est en effet impossible actuellement de stocker un objet partiellement initialis´e dans un tableau. Ces travaux ont ´et´e r´ealis´es avec Thomas Jensen, Vincent Monfort et David Pichardie, et publi´es dans les actes de la conf´erence internationale European Symposium on Research in

xx

´ ´ ETENDU ´ RESUM E

Computer Security (ESORICS) [HJMP10]

Conclusion Cette th`ese pr´esente des travaux allant de la formalisation de nouvelles analyses ou d’analyse d’inf´erence pour des syst`emes de types pr´eexistant, `a l’implantation de ces analyses pour l’ensemble du bytecode Java et ` a leurs ´evaluations exp´erimentales. F¨ahndrich et Leino proposent une analyse de pointeurs nuls qui mod´elise finement l’initialisation d’objets en ´etiquetant les objets en cours d’initialisation comme brut (Raw ). Une contribution de cette th`ese est de donner une fondation s´emantique `a cette id´ee en donnant une s´emantique au langage et ` a ces annotations. Cela nous permet de prouver que notre analyse d’inf´erence et leur syst`eme de types sont corrects. Une autre contribution de cette th`ese est d’avoir identifi´e cette propri´et´e comme solution pour rendre plus sˆ ure l’initialisation d’objets. Cette analyse peut ˆetre utilis´ee pour am´eliorer les garanties de s´ecurit´e que fournit le v´erificateur de bytecode (BCV). Avoir des fondations s´emantiques et formelles est une motivation importante de nos travaux, mais nous ne fournissons pas seulement des analyses correctes, nous fournissons aussi des implantations. ` partir de nos sp´ecifications formelles, qui abstraient de nombreux d´etails du bytecode A Java, nous avons produit plusieurs logiciels avec succ`es. – Nit est l’implantation de notre analyse de pointeurs nuls. Elle est disponible en licence GPL et a ´et´e t´el´echarg´ee plus de 930 fois. Rendre cette implantation efficace a ´et´e une partie importante du travail. En tant qu’analyseur de programme complet, elle a besoin d’une abstraction du flot de contrˆole, mais mˆeme une simple analyse de hi´erarchie de classes (CHA) [DGC95] n’est pas si facile `a implanter sur le bytecode car il y a 5 types d’appels de m´ethode diff´erents. Nit a ´et´e une importante source d’am´elioration pour Sawja. – Nit/Eclipse, le greffon (ou plug-in) pour utiliser Nit `a partir d’Eclipse, a ´et´e pr´esent´e `a JavaOne, qui est une conf´erence organis´ee par Sun/Oracle pour les utilisateurs de la technologie Java. Nit a re¸cu des retours positifs de la part des utilisateurs et s’est r´ev´el´e bien pratique pour faire la d´emonstration d’un analyseur statique. Vincent Monfort, ing´enieur dans l’´equipe Celtique, travaille `a rendre le greffon ind´ependant de Nit pour qu’il puisse ˆetre utilis´e par d’autre analyseur statique. Il pourrait ainsi ˆetre int´egr´e ` a Sawja. – Sawja est notre biblioth`eque de d´eveloppement d’analyseur statique pour le bytecode Java. Avec Javalib, son pr´ed´ecesseur et maintenant composant, ils sont disponibles en licence LGPL et ont ´et´e t´el´echarg´es plus d’un millier de fois. Pour une biblioth`eque g´erant du bytecode Java ` a partir d’OCaml, ce r´esultat nous semble plutˆot encourageant. Sawja est maintenant utilis´ee pour d´evelopper d’autre analyseur statique dans notre laboratoire, mais aussi par Julien Signoles et Philippe Hermann au CEA (Commissariat ´ `a l’Energie Atomique) et par Afshin Amighi et Dilian Gurov `a l’Institut Royal de Technologie (KTH) ` a Stockholm. – Notre analyse de sˆ uret´e d’initialisation d’objets a donn´e lieu `a un prototype pour l’ANSSI (Agence Nationale de la S´ecurit´e des Syst`emes d’Information) qui est aussi disponible sous forme d’un d´emonstrateur Web (http://www.irisa.fr/celtique/ext/ rawtypes/). Il a ´et´e int´egr´e ` a une version de la machine virtuelle JamVM [Lou]. En ciblant nos analyses vers le bytecode Java, nous avons d´ecouvert `a quel point le langage

xxi est compliqu´e. Par exemple, nous avons r´ealis´e la complexit´e de l’initialisation de classes durant le d´eveloppement de Nit. Au d´epart, nous pensions qu’un initialiseur ´etait similaire ` a un constructeur et que l’analyse de pointeurs nuls pourrait ˆetre ais´ement ´etendue aux champs statiques. Ce n’´etait d´efinitivement pas le cas. Une autre surprise a ´et´e la taille des programmes. Nos analyses de pointeurs nuls et d’initialisation de classes travaillent sur des programmes complets. Une telle analyse sur des programmes Java r´ev`ele des d´efis insoup¸conn´es. Par exemple, un simple programme hello world en Java utilise en fait des milliers de m´ethodes dans la biblioth`eque (runtime). Analyser mˆeme un petit programme requiert donc des outils performants. Nous avons apport´e des solutions `a ce probl`eme en d´eveloppant Nit puis Sawja. L’objectif principal de l’analyse statique est d’am´eliorer la qualit´e du logiciel. Cet objectif est vain tant que l’analyse statique n’est pas plus largement adopt´ee que pour des syst`emes de types `a minima. Une piste int´eressante pour d´evelopper l’adoption des analyses statiques est probablement (et paradoxalement) les analyses incorrectes. Grˆace `a leur faible taux de faux positifs, le coˆ ut n´ecessaire pour corriger l’ensemble des probl`emes relev´es par l’outil est relativement bas. Cela peut rendre ces outils relativement efficaces et devrait aider `a convaincre les d´ecideurs que les analyseurs statiques peuvent avoir un ROI (Return On Investment) suffisant pour ˆetre plus largement utilis´e. Une autre difficult´e pour l’adoption des analyseurs statiques est qu’ils semblent g´en´eralement ˆetre ´evalu´es relativement tard dans le processus de d´eveloppement. Les analyses statiques ont des difficult´es ` a g´erer pr´ecis´ement certains motifs de code (patterns), qui peuvent d´ependre de l’analyse. Par exemple, Nit fonctionne mieux si un champ est initialis´e dans son constructeur, mais certains d´eveloppeurs ont tendance `a initialiser les champs d’un objet juste apr`es leur cr´eation. Un autre exemple est la manipulation des champs. Si une m´ethode teste qu’un champ est diff´erent de nul avant de l’utiliser, il vaut mieux copier d’abord le champ dans une variable locale : cela ´evite `a l’analyse de devoir prouver qu’un autre processus (ou thread ) ne peut modifier le champ entre son test et son utilisation. Si l’analyseur statique est utilis´e d`es le d´ebut du d´eveloppement, ces patterns difficiles seront plus naturellement ´evit´es par les d´eveloppeurs. Une fois que les erreurs simples seront corrig´ees, une fois que les d´eveloppeurs auront pris l’habitude d’utiliser les analyseurs statiques et connaˆıtront les patterns `a ´eviter pour faciliter le travail de ces outils, alors des analyses plus correctes (trouvant plus d’erreurs) seront n´ecessaires. Une autre approche est donc probablement de d´evelopper d`es maintenant des analyses correctes, mais o` u des options permettent d’activer des suppositions incorrectes et o` u des priorit´es sont associ´ees aux alarmes. Par exemple, nous avons introduit dans Nit des options permettant de supposer que les tableaux ne contiennent que des valeurs non nulles. Une autre solution pourrait ˆetre de diff´erencier les valeurs nulles provenant des tableaux de celles provenant des champs. Ensuite, d´er´ef´erencer une valeur nulle de tableau d´eclencherait une alarme de priorit´e plus faible qu’un d´er´ef´erencement d’une valeur nulle de champ. Un autre axe qui peut ˆetre ´etudi´e est l’information donn´ee sur la cause d’une alarme. En effet, lorsqu’une alarme est lev´ee, le d´eveloppeur a besoin d’informations pour trouver l’origine de l’alarme (et ´eventuellement l’erreur). Par exemple, en montrant les annotations que Nit a trouv´ees, il est plus simple de comprendre pourquoi il peut reporter `a un certain point de programme une erreur. Cependant, comprendre pourquoi Nit a inf´er´e une annotation peut ˆetre fastidieux.

xxii

´ ´ ETENDU ´ RESUM E

Contents Remerciements

iii

Abstract

v

R´ esum´ e

vii

R´ esum´ e´ etendu

ix

1 Introduction 1.1 Improving Software Quality . . . . . . . . . . . . . 1.2 Java versus Java bytecode . . . . . . . . . . . . . . 1.3 Background on Static Analysis Through a Tutorial 1.4 Contents and Contributions . . . . . . . . . . . . . 2 BIR Language 2.1 Syntax . . . . . . . . . 2.2 Semantic Domains . . 2.3 Operational Semantics 2.4 Conclusion . . . . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

3 Non-Null References and Field Initialization 3.1 Introduction . . . . . . . . . . . . . . . . . . . 3.2 Related Work . . . . . . . . . . . . . . . . . . 3.2.1 Type Systems . . . . . . . . . . . . . . 3.2.2 Type Inference . . . . . . . . . . . . . 3.3 Non-Null Annotations . . . . . . . . . . . . . 3.4 Syntax and Semantics . . . . . . . . . . . . . 3.5 Null-Pointer Analysis . . . . . . . . . . . . . . 3.5.1 Modular Type Checking . . . . . . . . 3.5.2 Abstract Domains . . . . . . . . . . . 3.5.3 Inference Rules . . . . . . . . . . . . . 3.5.4 Example . . . . . . . . . . . . . . . . . 3.6 Correctness . . . . . . . . . . . . . . . . . . . 3.7 F¨ ahndrich and Leino’s Type System . . . . . 3.8 Conclusions . . . . . . . . . . . . . . . . . . . xxiii

. . . .

. . . . . . . . . . . . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

. . . .

. . . . . . . . . . . . . .

. . . .

1 1 3 4 8

. . . .

11 11 13 13 14

. . . . . . . . . . . . . .

17 17 19 19 19 20 21 22 22 23 24 27 29 31 35

xxiv

CONTENTS

4 A Non-Null Annotation Inferencer for Java Bytecode 4.1 Towards a Bytecode Analysis . . . . . . . . . . . . . . . 4.1.1 Alias Analysis . . . . . . . . . . . . . . . . . . . . 4.1.2 A New Abstract Value . . . . . . . . . . . . . . . 4.1.3 Analysis of instanceof Instructions . . . . . . . 4.2 Implementation . . . . . . . . . . . . . . . . . . . . . . . 4.3 The Nit/Eclipse Plug-in . . . . . . . . . . . . . . . . . . 4.4 Empirical Results . . . . . . . . . . . . . . . . . . . . . . 4.5 Related Work . . . . . . . . . . . . . . . . . . . . . . . . 4.6 Conclusion . . . . . . . . . . . . . . . . . . . . . . . . . 5 Sawja: Static Analysis Workshop for Java 5.1 Introduction . . . . . . . . . . . . . . . . . . . . . . 5.2 Existing Libraries for Manipulating Java Bytecode 5.3 High-level Representation of Classes . . . . . . . . 5.4 Intermediate Representation . . . . . . . . . . . . . 5.5 Complete Programs . . . . . . . . . . . . . . . . . 5.5.1 API of Complete Programs . . . . . . . . . 5.5.2 Construction of Complete Programs . . . . 5.6 Conclusion . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

6 Static Initialization 6.1 Introduction . . . . . . . . . . . . . . . . . . . . . . . . . 6.2 Why Static Analysis of Static Fields is Difficult? . . . . 6.3 The Language . . . . . . . . . . . . . . . . . . . . . . . . 6.3.1 Syntax . . . . . . . . . . . . . . . . . . . . . . . . 6.3.2 Semantics . . . . . . . . . . . . . . . . . . . . . . 6.4 A Must-Have-Been-Initialized Dataflow Analysis . . . . 6.4.1 Informal Presentation . . . . . . . . . . . . . . . 6.4.2 Formal Specification . . . . . . . . . . . . . . . . 6.4.3 Implementation . . . . . . . . . . . . . . . . . . . 6.5 A Three-Valued Initialization State Analysis . . . . . . . 6.5.1 MHBI Analysis is Too Dependent on the Control 6.5.2 Specification of the Analysis . . . . . . . . . . . . 6.6 Towards an Implementation . . . . . . . . . . . . . . . . 6.6.1 Handling the Full Bytecode . . . . . . . . . . . . 6.6.2 Scaling the Analysis . . . . . . . . . . . . . . . . 6.7 Related Work . . . . . . . . . . . . . . . . . . . . . . . . 6.8 Conclusion and Future Work . . . . . . . . . . . . . . . 7 Secure Object Initialization 7.1 Introduction . . . . . . . . . . . . . . . . . . . . . 7.2 Related Work . . . . . . . . . . . . . . . . . . . . 7.3 Context Overview . . . . . . . . . . . . . . . . . 7.3.1 Standard Java Object Construction . . . 7.3.2 Attack on the Class Loader and the Patch 7.4 The Right Way: A Type System . . . . . . . . .

. . . . . . . . .

. . . . . . . .

. . . . . . . . .

. . . . . . . .

. . . . . . . . .

. . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Flow . . . . . . . . . . . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

37 37 37 38 39 40 41 42 45 45

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

47 47 48 50 52 53 53 54 57

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . Analysis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . .

59 59 60 62 62 63 65 66 67 71 71 71 73 76 76 77 78 79

. . . . . .

81 81 82 82 83 83 85

. . . . . . . .

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . From Oracle . . . . . . . .

. . . . . . . .

. . . . . .

. . . . . . . .

. . . . . .

. . . . . . . .

. . . . . .

. . . . . . . .

. . . . . .

. . . . . .

. . . . . .

. . . . . .

CONTENTS 7.5

. . . . . . . . . . .

89 89 90 91 92 92 93 93 93 94 96

8 Conclusion 8.1 Outlook . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

97 99

7.6

7.7

7.8

Formal Study of the Type System . . 7.5.1 The language . . . . . . . . . . 7.5.2 Initialization Types . . . . . . 7.5.3 Typing Judgment . . . . . . . . Extensions . . . . . . . . . . . . . . . . 7.6.1 Introducing Dynamic Features 7.6.2 Handling Arrays . . . . . . . . Experimental Results . . . . . . . . . . 7.7.1 Implementation . . . . . . . . . 7.7.2 A Case Study: Oracle’s JRE . Conclusion and Future Work . . . . .

xxv . . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

xxvi

CONTENTS

Chapter 1

Introduction Software bugs are common, so common that developers and editors do not want to be responsible. Thus, software licenses usually include a disclaimer of warranty and liability such as the following one (extracted from the GNU General Public License). “In no event [...] will any copyright holder [...] be liable to you for damages, including any general, special, incidental or consequential damages arising out of the use or inability to use the program [...], even if such holder or other party has been advised of the possibility of such damages.” Software may not perform as promised, and may result in data or financial loss for the user. Even if the developer was informed of the defect, he often takes no responsibility for it. Despite these disclaimers and limitations, software bugs usually still cost to the developer. Bugs may cost its popularity to the developer. Bugs may also carry a cost because of a lesslimited warranty. E.g., some licenses offer to reimburse the loss directly caused by the software within the limit of the price of the software. Bugs also cost resources when the developer needs to fix the bug and distribute a patch. For largely-deployed unconnected devices, it may be very expensive. The developer may also be the user, in which case the developer directly suffers from the consequences of bugs in his software. For those reasons, despite warranty and liability disclaimers, most companies developing software do invest in software quality.

1.1

Improving Software Quality

Several tools exist to improve software quality. The one in which companies invest the most is certainly tests. During testing, the code is run on several test sets and the output of the program for those sets is checked by an oracle (which can be a human, a previous version of the program, a model of the program, etc.). Testing is not exhaustive: it is impossible to prove the absence of a class of bugs using testing because it is impossible to test programs on all their possible entries. Therefore, although testing allows gaining some confidence in the quality of the software, bugs may still happen on the software. Another kind of approach is to completely prove correct a formal specification of a program, and then extract the code from the specification.1 It has been industrially used with the Atelier B and the B Method [Abr96] and in more academic research with proof assistants such as Coq [Coq] or Isabelle/HOL [NPW02]. This approach requires a highly technical expertise and is usually 1

There is another approach where the proof is done directly on the code, for example within the JML framework, but we can see this approach as a special case where the code is the specification.

1

2

CHAPTER 1. INTRODUCTION

time consuming. In industry, it is only used where the cost of a bug may be huge, like in the transport industry where a bug may cost the lives of hundreds of people. It is also preventively introduce in domains where it may be required by law or standards in the future, such as in the aircraft or smart-card industry. Finally, the one that is arguably the most used, although people usually do not think about it, is probably static analysis (SA). Indeed, almost all developments are done in languages that integrate type systems, like C, C# or Java, which are a kind of SA. An analysis of software is static if it is performed without actually running the studied software. It is a powerful technique that enables automatic verification of programs with respect to various properties such as type safety or resource consumption. As opposed to test, SAs may be exhaustive: they can give information on the software that is valid for all executions of the program and in particular that does not depend on the inputs. An advantage over manual proofs of correctness is that SAs are usually fully automatic. However, this comes at the price of undecidability in the general case: a SA will inevitably fail to prove that some correct programs are indeed correct (in the sense that they respect the property of interest). A static analyzer checks that some code (source or machine code, a small piece of code or a complete program) respects some property. If the analyzer finds that the program violates the property at some point, it issues an alarm, called a positive. An analyzer may report many positives for one analyzed code. E.g., all the lines of the source code that call a particular method. As a result of undecidability, most analyzers cannot report the exact set of points that do not respect the property. Hence, they issue false positives or false negatives. A false positive is an alarm that is issued when, in fact, the code respects the property but the analyzer was unable to prove it. A false negative is an alarm that is not issued despite the code violates the property because the analyzer did not notice it. A sound analysis reports no false negatives. A complete analysis reports no false positives. To face incompleteness, different approaches may be used by SAs. • SAs may use a more pragmatic approach and try to differentiate programs that may be correct from programs that may be incorrect, using heuristics or user-trusted annotations, and only reporting the latter ones. Although this is unsound (it introduces false negatives), it also allows reducing the number of false positives. Developers then only concentrate on alarms that are very likely to be real issues. Those tools are known as bug finders. This is notably the case of FindBugs [HSP06]. • Some SAs rely on user-untrusted annotations. Checking a proof is often easier than proving, and user annotations may be seen as partial proof that an analysis checks instead of proving them. Untrusted annotations may also be seen as “tips” reducing the search space of the analysis and allowing for more precise analyses. E.g., the Java compiler requires the user to put type annotations on each variable. This reduces the number of false positives. • SAs may also focus on languages that are easier to reason about. For example, data are arguably easier to track in functional languages where, by default, there are no references, and definitions associate directly a value to a name. Despite a rich type system (which makes the search space extremely large), core ML is provided with type inference. Conversely, in Java, all variables are mutable (even final fields when looking at the Java bytecode), their declaration is separated from their initialization, and

1.2. JAVA VERSUS JAVA BYTECODE

3

initialization of fields, objects and classes in Java is particularly difficult, as this thesis will illustrate. • Finally, SA may be used to infer invariants, either helping the development (refactoring, reverse engineering), an assisted proof of correctness, or another SA. As this thesis will try to demonstrate, Java (or Java bytecode) is not a language easy to reason with. It is a full-featured language, used in the industry, and using complex initialization schemes. Building sound and precise analyses for Java is therefore a difficult task. This thesis proposes sound analyses and tools for the actual Java bytecode. They may be used directly to ensure security properties (as proposed in Chapter 7), or as a basis to make the development of other analyses easier and the analyses more precise.

1.2

Java versus Java bytecode

Java [GJSB05] is a source language. It is usually compiled to Java bytecode, the low-level programming language interpreted by the Java Virtual Machine (JVM) [LY99]. As a source language, Java has many constructs which have similar effects, but which may be easier to use or to read by developers. E.g., a for loop can be encoded into a while loop, and a switch instruction can be encoded into if/then/else conditionals. All loops in Java are compiled into conditional and unconditional jumps at the bytecode level. The same occurs with other language constructs and the Java bytecode contains therefore a lot less constructs.2 Another issue with Java is that it evolves more quickly. E.g., generics, annotations, asserts, autoboxing and unboxing, enum types, foreach loops, variable arity methods and static imports have been added to the Java language without changing the Java bytecode language. Finally, when one wants to analyze a program, the source may not be available. This may be the case if an external library is used, if the compiler is not trusted, or if a user wants to check code he has downloaded. E.g., the Java ByteCode Verifier (BCV), a checker integrated in the JVM, analyzes the bytecode before executing it, when the source code is no more available. For those reasons, the work presented in this thesis targets the Java bytecode, although part of it also applies to Java. Although Java bytecode is not a source language, it is not as low level as the assembly language of standard microprocessors: it is a typed and stack-based language. The stackbased architecture helps to ensure the independence from the hardware. E.g., the number of registers available in the microprocessor is not needed. Being typed means that all data are labeled with a type, e.g., ensuring that when a field is read from an object, as in o.f, the object o is an instance of a class that declares a field f. The Java bytecode, like Java, is a fullfeatured language: it supports objects, classes, interfaces, arrays, basic types such as 8, 16, 32 and 64-bit integers, 32 and 64-bit floating-point numbers, Booleans, characters, strings, multithreading, 5 different types of method calls, exceptions, subroutines, unstructured controlflow, etc. To compact the code and for efficiency reasons, an operation can be encoded in several ways. E.g., to push an integer onto the stack, one may use the instruction bipush, sipush, ldc or ldc w. Most of the time, those details do not affect the analyses we may build. Thus, although the analyses we present in this thesis are targeted to the Java bytecode, we do 2

Although there are more than 200 Java bytecode instructions, there is no expression in bytecode and many instructions have the same semantics and are merged by our parser, as explained in Section 5.3.

4

CHAPTER 1. INTRODUCTION

p c f m e ins

x, y ∈ Var jmp ∈ L = N ∈ Prog ::= { classes ∈ P(Class), main ∈ Meth, } ∈ Class ::= { super ∈ Class⊥ , fields ∈ P(Field)} ∈ Field ::= { ftype ∈ Type } ∈ Meth ::= { instrs ∈ Instr array} ∈ Expr ::= null | x | e.f ∈ Instr ::= x ← e | x.f ← y | x ← new c | if (?) jmp | return Figure 1.1: Language Syntax

not formalize the analyses directly on the bytecode but on another language, BIR (Bytecode Intermediate Representation), that will be presented in Chapter 2.

1.3

Background on Static Analysis Through a Tutorial

This thesis formalizes analyses using constraint systems, dataflows equations or type (and) effect systems. Each analysis is presented in the formalism we think is the most appropriate. Of course, theses analysis could all have been presented in the standard framework of abstract interpretation [CC77]. It could have been of interest to prove the optimality of our analyses or to introduce widening operators. However, having a proof of optimality was not one of our main objectives and we did not use any widening operator. In this section, we propose a tutorial to introduce SA. We present a language with an abstract syntax and show how a program in this language may look like with an example. We also introduce type systems with an example, and show that it is equivalent to a constraint system. Finally, we explain how such a constraint system can be solved. Figure 1.1 presents the (abstract) syntax of a very small language based on the one that is presented in Chapter 2. We use this language as a base to demonstrate the basics of SA. A program in this language is a record composed of a field classes, which contains the classes of the program, and a field main, which is the only method of the program. Having only one method avoids introducing method calls. A class c is composed of two fields: super, which contains the superclass if the class has one or ⊥ otherwise, and fields, which contains the set of fields defined in class c. A field only contains a type annotation. This type annotation is not yet specified and it can be changed depending on the type system we want to specify. A method contains an array of instructions. An expression is the null constant, a local variable, or a field read. An instruction may be an assignment of an expression to a local variable, an assignment of a local variable to a field, an assignment of a newly allocated object to a local variable, or a conditional. All instructions are standard but the object allocation, which runs no constructor, and the conditional, which is a non-deterministic jump: it abstracts standard conditionals and avoids introducing Booleans in our language. A program in this language may not be valid for several reasons. The instruction if (?) jmp may jump outside the instruction array if jmp is greater than the instruction array of the method, the super field may not describe a class hierarchy, the last instruction of the array may not be a return instruction, one may try to dereference the null constant, or one may try to access a field which is not defined. For the Java bytecode, Java ByteCode Verifier (BCV) checks that the code does not have

1.3. BACKGROUND ON STATIC ANALYSIS THROUGH A TUTORIAL

5

these flaws, with the exception of the dereferencing. Before executing the code, at load time, the Java Virtual Machine (JVM) executes the BCV on each class to check some properties on the code. Among these properties, the BCV ensures that there may be no jump outside the code array, the number of local variables used by each method is below the number of local variables it declares to use, that methods are given the right number of arguments when they are called, etc. The absence of null-pointer dereferencing is not checked at load time by the BCV, it is checked at run time and leads to NullPointerException when it occurs.3 To demonstrate static analysis, we assume that the code has already been partially checked and that there is no jump outside the instruction array, that the last instruction is a return and that the relation described by super is indeed a hierarchy. This allows us to define a simple analysis to check the latter property: when a field of an object is accessed, the field is indeed defined in the class of the object or in one of its superclasses. If the program contains a field access x.f , we need to check that each instance that x may point to is of a class that defines f . Like for the Java language, we consider an instance has a field if it is defined in its class or in one of its parents. To check this property, we could try to compute all the objects that may be referenced by x, and check that their class defines f ; but this is not computable in general, i.e. for some program, such an analysis would not terminate. The main issue is that the number of objects allocated by a program is unbounded (or only by an unknown and huge value, e.g., the number of objects that may fit in the memory of a computer). For the problem to be computable, we need to simplify it, and this can be achieved by abstracting some information. Many abstractions may be defined, we will use here a standard approach for object-oriented languages: we will abstract all objects that may be referenced by a variable by a single class. Let superclasss ∈ Class × Class be the relation such that c1 superclass c2 iff c2 .super = c1 . We call c1 the (direct) superclass of c2 . Let parent be the transitive closure of superclass. If c parent c0 , we say that c is a parent of c0 , or that c0 is a subclass of c, written ≺. We write  for the reflexive closure of ≺ (i.e., for all c, c  c). If super describes a class hierarchy, then  is an (partial) order relation (i.e., a reflexive, transitive and antisymmetric relation) and there is a unique greatest element (e.g., java.lang.Object in Java). We extend the domain of  to Class⊥ such that ⊥ is the smallest element of Class⊥ , i.e., ∀x ∈ Class, ⊥  x. On this same domain, we also define the join operator t that returns, given two classes c1 and c2 , the smallest class c that is a parent of both c1 and c2 , i.e., c1 t c2 = c ⇐⇒ c1  c ∧ c2  c ∧ (∀c0 , c1  c0 ∧ c2  c0 =⇒ c  c0 ). We rely on this relation and abstract each variable by c ∈ Class⊥ , where the variable may reference any object that is an instance of c or one of its subclasses. If a variable is annotated as ⊥, it cannot point to any object, thus it can only be null. This corresponds to the standard type-system in Java. For this type-system, we therefore define Type as Class⊥ . Note that, in Java, a field or another variable cannot be annotated with ⊥, because specifying that a variable can only contain the null constant would mean the variable is useless. Nevertheless, such a value is used internally in the bytecode verifier. 3

The issue of NullPointerException is studied in Chapter 3, where an analysis to statically prove their absence at load time is proposed.

6

CHAPTER 1. INTRODUCTION

l ` null : ⊥

l ` x : l(x)

l`e:c f ∈ c.fields l ` e.f : f.ftype

l ` e : τ0 τ0  τ l`e:τ

Figure 1.2: Expression Typability

l`e:τ ` x ← e : l → l[x 7→ τ ]

l`x:c

` x ← new c : l → l[x 7→ c]

f ∈ c.fields l ` y : f.ftype ` x.f ← y : l → l ` if (?) jmp : l → l

Figure 1.3: Instruction Typability The type system is defined in Figures 1.2 and 1.3. The first one presents the typing rule for expressions. They are presented in the form of inference rules: a horizontal line with any number of typing judgments (premises) above and one typing judgment (a conclusion) under the line. The meaning of such inference rule is that if the judgments above the line hold, then the judgment under the line holds. It is often used the other way around: to prove that the judgment under the line holds, we need to prove that the judgments above the line also hold. The typing judgments are sequents of the form l ` e : τ , where l ∈ Var → Class⊥ is a type environment that contains the type of each local variable. A sequent may be read as the expression e has type τ in the type environment l. We will often use the variable τ for types, but we may also use the variable c for a type if it is different from ⊥, i.e., if it is a class. The first rule of Figure 1.2 is the typing judgment for the null constant, which has type ⊥. Intuitively, the type of the null constant needs to be compatible with any other reference type as it is valid to assign the null constant to any variable. By definition, ⊥ is the smallest type element and therefore satisfies this property. The following rule is the typing judgment for local variables. A local variable x has the type given by the current typing environment. Then, a field read has the type that the field is annotated with, if the field access is valid. This is a key part of the type system: it is this rule that checks that f is defined. We need to retrieve the type c of the expression e, and check that f is a field of c. The last rule is a standard subtyping relation: if the expression e is of type τ 0 , then it is also of type τ if τ 0 is a subtype of τ . Figure 1.3 gives the typing rules for the instructions, except return. They are presented in the form of a type and effect system. Each judgment is of the form ` ins : l → l0 , where l and l0 represent the types of the local variables before and after the instruction, respectively. In other words, the typing judgment represents the effect of the instruction on l. (For a more detailed presentation, see Type and Effect Systems [NNH99], Chapter 5). The first rule is the typing judgment for the assignment of an expression to a local variable. If the expression is of type τ , then the effect of the instruction is to update the type of x to τ . The rule for field assignment has no effect on the local variable, but, to be valid, x.f needs to be typable and y needs to be of a subtype of f.ftype. The x ← new c instruction has the effect of updating the type of x to c. Finally, the if (?) jmp instruction has no particular effect: it simply propagates the type of the local variables. We may then define a well-typed program. For this definition, we need to be able to relax

1.3. BACKGROUND ON STATIC ANALYSIS THROUGH A TUTORIAL

p c c1 c2 f

= = = = =

{classes = {c; c1 ; c2 }; main = main} {super = ⊥; fields = {f }} {super = c; fields = ∅} {super = c; fields = ∅} {ftype = c; }

main = [ 0 : 1: 2: 3:

7

x ← new c1 ; if ? 3; x ← new c2 ; x.f ← x]

Figure 1.4: Program Example some constraints on the local variables. Thus, we extend the ordering relation on classes to the local variable typing functions. A local variable function l is smaller (or more precise) than another function l0 , written l v l0 , if for all variables x, l(x)  l0 (x). Similarly, we overload the t operator such that l1 t l2 is the smallest local environment that is greater than l1 and l2 . We also use the function succ ∈ L → P(L), which returns the successors of a given program point. This function returns the next program point if the instruction is different from return, and, if the current instruction is a conditional jump, it also returns the target of this jump. A program is said well typed if, and only if, there exists a function L ∈ L → Var → Class⊥ , which associates a local variable environment to each program point, such that: ∀i ∈ L, p.main.instrs(i) = ins ∧ ins 6= return =⇒ ∀j ∈ succ(i), ∃l0 , l0 v L(j) ∧ ` ins : L(i) → l0 L(i) is the abstraction of the local variables before the instruction at program point i. Note that, unless there is backward jump to 0, there is not constraint on L(0). Usually the entry point is constrained with an abstraction of the context in which the method is called (e.g., the abstraction of the parameters of the method). Here, the method has no parameter and no other context is needed. L(0) may therefore receive any value, but the smaller the better, so the function that associates ⊥ to all variables is the one that should be chosen. Let us look at the simple program example presented in Figure 1.4. p is a program with 3 classes, c, c1 and c2 , such that c is the superclass of c1 and c2 . The class c defines the field f , which is of type c. The method of p contains 4 instructions. This program may non-deterministically execute either instructions 0, 1 and 3 or 0, 1, 2 and 3. If we instantiate the definition of a well-typed program on the example, we obtain the constraints presented in Figure 1.5. The premises may also lead to further verifications presented in the column Checks, like for the x.f ← x instruction, where it is checked that the field f is defined in the class corresponding to the type of x (or one of its parents, thanks to the subtyping relation). If there is a function L such that the constraints and checks hold, then this program is well typed. If L is given, it is easy to check that the constraint system is verified, and therefore that the program is well typed. However, it does not explain how to compute L. In the previous figure, we obtain, for each program point, one constraint for each successor in the CFG that specifies an outgoing dataflow. We can “reverse” this system and look, for each program point, at the predecessors in CFG. The abstraction L(i) at program point i is then the union of the incoming dataflows. The result of such approach is presented in Figure 1.6(a).

8

CHAPTER 1. INTRODUCTION

Instruction Constraints 0 : x ← new c1 L(0)[x 7→ c1 ] v L(1) 1 : if ? 3 L(1) v L(2) L(1) v L(3) 2 : x ← new c2 L(2)[x 7→ c2 ] v L(3) 3 : x.f ← x L(3) v L(4)

Checks

∃c0 , L(3)(x)  c0 ∧ f ∈ c0 .fields ∧ L(3)(x)  f.ftype

4 : return Figure 1.5: Contraint system L(1) = L(0)[x 7→ c1 ] L(2) = L(1) L(3) = L(1) t L(2)[x 7→ c2 ] L(4) = L(3)

L(1) = L(1) t L(0)[x 7→ c1 ] L(2) = L(2) t L(1) L(3) = L(3) t L(1) L(3) = L(3) t L(2)[x 7→ c2 ] L(4) = L(4) t L(3)

(a) Canonical Transfer Functions

(b) Transfer Function Computed Forward

Figure 1.6: Transfer Functions Figure 1.6(b) presents another set of transfer functions, which does not need to compute the predecessors of each instruction and which is directly obtained from the constraints. For each constraint of the form f (L) ⊆ L(i), we build a transfer function L(i) = L(i) t f (L). The solution may then be computed by fixpoint iteration, i.e., starting from the smallest value of L, and then iterating on the transfer function until no transfer function modifies the previous value. This naive algorithm can obviously be improved and a lot of work has been focused on this issue and several ordering strategies have been proposed. For the implementations presented in this thesis, we only recompute the value of a transfer function when one of its parameters has been changed, but we have not implemented much more complicated strategies. For the iteration to terminate, the transfer functions need to be monotone, i.e., L vL L0 implies that f (L) v f (L0 ). While, in Figure 1.6(a), each function should be proven monotone for the termination of the whole computation to be proven, it is trivial in Figure 1.6(b).

1.4

Contents and Contributions

Chapter 2 presents the BIR language which has already been motivated: it is simple enough so analysis can be formalized and presented on this language and close enough to the bytecode language so implementations can target the Java bytecode. We will give in this chapter the syntax of the language, which will be left abstract: we will not define the concrete syntax for this language as there is no point in programming in a such language. This abstract syntax could be the result of a parser or a previous transformation, e.g., a transformation from Java bytecode. Chapter 3 presents a null pointer analysis. Null-pointer dereferencing is a common source of bug in Java software and proving their absence is an interesting property. Furthermore,

1.4. CONTENTS AND CONTRIBUTIONS

9

the precision of any static analyzer usually depends on the precision of the control flow graph (CFG). Runtime exceptions in Java are a source of branching: there is an edge in the CFG from any instruction that may throw an exception to the corresponding handler or to the end of the method. Although most instructions can throw an exception, most are usually safe. E.g., any o.f instruction in Java may throw an exception because the variable o may be null. If we prove that o is non-null, then we can simplify the CFG, thereby improving the precision of any analysis relying on precision of the intra-procedural CFG. One of the difficulties of a null pointer analysis in Java (bytecode) is the initialization of instance fields. Writing only non-null values in a field (being static or not) is not enough to ensure that only non-null values may be read from this field. Indeed, they are all null by default and a simple minded analysis would not be able to say more than all fields may be null. We will see that we can define a more precise property on fields which takes in account the specific initialization of instance fields. Our analysis requires no user annotations but we compare our analysis to a preexisting type system to evaluate our work. We proved our analysis sound and complete with respect to the type system. This work has been published in the International Conference on Formal Methods for Open Object-based Distributed Systems (FMOODS) [HJP08a]. Chapter 4 presents Nit (the Null-ability Inference Tool), the implementation of the former analysis. The former analysis is formalized on our BIR language, which abstracts some features of the Java bytecode. We will see in this chapter how we improved the analysis to be able to gain in precision, and we will evaluate the precision of both the former and the improved analyses. We will see that, despite the implementation of the former analysis is able to prove about 70% of dereferences safe, the new implementation reduces the number of unsafe dereferences by 1/3, now proving 80% of dereferences safe. This work has been published in the ACM workshop Program Analysis for Software Tools and Engineering (PASTE) [Hub08]. To implement Nit, we developed a lot of code that was not specific to our analyzer: this code was to manipulate the classes, the class hierarchy or to navigate the CFG. An important effort has been put on the efficiency of Nit, which is now able to analyze 28000 methods in less than 2 minutes. Part of this effort had in fact been on these lower parts. Therefore, we decided to build from this code and experience Sawja, a library on top of which can be developed efficient static analyzers. This library is presented in Chapter 5. This work has been published in The International Conference on Formal Verification of Object-Oriented Software (FoVeOOS) [HBB+ 10]. Chapter 6 presents our work on static (or class) initialization. In Java, classes and static fields are initialized implicitly and lazily. This makes the CFG very unintuitive for a developer, and very dense and imprecise for a static analysis: many instructions in the program may trigger the initialization of a particular class while, at run time, only one will actually trigger the initialization. We propose in this chapter two analyses that target this issue. We proved the first one sound but the implementation showed us that it was not precise enough to be practicable. We therefore propose another analysis, using a different abstraction and using context sensitivity to gain in precision. This work, with the exception of the second analysis, has been published in the international workshop on Bytecode Semantics, Verification, Analysis and Transformation (Bytecode) [HP09]. Invariants are usually established during object initialization. Therefore, classes should not allow partially initialized objects to be accessed. This is a known issue that has led to several vulnerabilities in the Java Runtime Environment (JRE). We capitalized on our knowledge of initialization in Java to propose a type system to express initialization pol-

10

CHAPTER 1. INTRODUCTION

icy, i.e., which variables may hold partially initialized objects, which can be typed checked. Chapter 7 presents this type system, the checker, and an experimental evaluation showing that few annotations are needed to verify existing code without modifying the code. This work has been published in the European Symposium on Research in Computer Security (ESORICS) [HJMP10]. The proofs sketched in this thesis are the contribution of David Pichardie, except for the proof of completeness of our nullness analysis with respect to the type system of F¨ahndrich and Leino presented in Chapter 3. The intermediate bytecode representation presented in Section 5.4 is a contribution of Delphine Demange and David Pichardie. Finally, I am the main contributor to Sawja (and Javalib), presented in Chapter 5, but not the only one. I have coded around 40% of the library, which is estimated to have needed 1.5 man-year coding ´ effort. The other contributors are, in alphabetical order, Etienne Andr´e, Nicolas Barr´e, Fr´ed´eric Besson, Nicolas Cannasse, Delphine Demange, Florent Kirchner, Vincent Monfort, David Pichardie and Tiphaine Turpin.

Chapter 2

BIR Language BIR is a simple language in-between Java source and Java bytecode but which abstracts some details of those languages. Our goal is to have a language close enough to the bytecode in order to easily obtain, from the specification, a naive implementation at the bytecode level while keeping a language easy to reason with. In particular it is stack-less: removing the operand stack avoid introducing an extra analysis to know if a stack variable is a copy of the local variable, e.g., the receiver this. Arrays are not yet analyzed by the analyses presented hereafter. Thus, arrays have not been introduced in our language, yet. Similarly, those analyses are focused on object-oriented features (objects, fields and class initialization) and arithmetic needs not to be taken into account. Note that the implementations of those analyses conservatively handle arithmetic and arrays. To accommodate the different analyses and type systems exposed in this thesis, we put some hooks in the syntax and in the semantics. The syntactic hooks are type information, which will only be needed when presenting the type systems. The semantic hooks are semantic domains which are left unspecified in this chapter and which will be instantiated differently depending on the analysis. They do not modify the behavior of the program but keep more information on the execution of the program. This extra information eases the definition of the correctness relations between the semantic domains and the abstract domains.

2.1

Syntax

This language is based on the decompiled language from Demange et al. [DJP09] that provides a stack-less representation of Java bytecode programs, and is also similar to the Grimp language of Soot [VRCG+ 99]. Figure 2.1 shows the syntax of the language. A program p is a record that handles a set of classes, a main class and a lookup operator lookup. This operator is used do determine during a virtual call the method (p.lookup c m) (if any) that is the first overriding version of a method m in the ancestor classes of the class c. A class is composed of a superclass (if any), a set of fields, a set of methods and a special constructor method init. The constructor is the method called after the allocation of a new object. We denote by ∈ Class × Class the reflexive transitive closure of the superclass relation. Types are left unspecified and will be refined later on when presenting the analyses. A method handles an array of instructions and four types for (i) the initial value of the variable this (m.pre), (ii) its final value (m.post), (iii) the type of its formal parameter1 (m.argtype) and 1

For the sake of simplicity, each method has a unique formal parameter arg.

11

12

CHAPTER 2. BIR LANGUAGE

p



c ∈ f m

∈ ∈

τ ∈ e ∈ ins ∈

x, y, r ∈ Var jmp ∈ L = N Prog ::= { classes ∈ P(Class), main ∈ Class, lookup ∈ Class → Meth → Meth⊥ } Class ::= { super ∈ Class⊥ , fields ∈ P(Field), methods ∈ P(Meth), init ∈ Meth} Field ::= { ftype ∈ Type } Meth ::= { instrs ∈ Instr array, pre ∈ Type, post ∈ Type, argtype ∈ Type, rettype ∈ Type} Type Expr ::= null | x | e.f Instr ::= x ← e | x.f ← y | x ← new c(y) | if (?) jmp | super(y) | x ← r.m(y) | return x

Figure 2.1: Language Syntax

Exc L V R = Var → V O = Field → (V × ADF) HO = Class × ADO × O H = L → HO⊥ CS S

3 3 3 3 3 3 3 3 3

e l v ρ o ho σ cs st

::= e | ⊥ ::= l | null

::= [c, ado, o] ::= (m, i, ρ, r) :: cs | ε ::= hm, i, ρ, σ, csie

(exception flag) (location) (value) (local variables) (object) (heap object) (heap) (call stack) (state)

Figure 2.2: Semantic Domains

(iv) the type of its return value (m.rettype). Like the receiver, the argument could also have a type annotation for its final value. We have chosen not to add post annotations to the argument, as it is less common to modify a parameter than the receiver. It is a trade off toward simplicity. The only expressions are the null constant, local variables and field reads. As already explained, arithmetic and arrays are not taken into account: we only manipulate objects. The instructions are the assignment to a local variable or to a field, object creation (new)2 , (non-deterministic) conditional jump, super constructor call, virtual method call and return. Without loss of generality, all method calls return a value. We express the semantics of our language as a transition function on a state representing actual program state. The domain of the state is described in the next section while the transition function is described in Section 2.3.

2.2. SEMANTIC DOMAINS

e ⇓ρ,σ l

13

null ⇓ρ,σ null ρ(x) = v x ⇓ρ,σ v σ(l) = [c, ado, o] o(f ) = (v, adf ) e.f ⇓ρ,σ v e ⇓ρ,σ null e.f ⇓ρ,σ ⊥ e ⇓ρ,σ ⊥ e.f ⇓ρ,σ ⊥

Figure 2.3: Expression Evaluation

2.2

Semantic Domains

Figure 2.2 shows the concrete domain used to model the program states. A state st is composed of the current method m, the current program point i in m (the index of the next instruction to be executed in m.instrs), the function ρ ∈ R for local variables (or registers), the heap σ, a call stack cs and an exception flag e. The heap is a partial function which associates to a location l a heap object ho = [c, ado, o] with c its type, ado an analysis specific value which will be used by the analyses presented later on and an object o. An object o is a function that associates to a field in Field a value in V and an annotation adf in ADF, which, like ado, will be used by the analyses presented later on. (In the sequel ho and o are sometimes used interchangeably). The local variable function ρ gives, for each local variable of the method x ∈ Var, its value v ∈ V. A value can either be a location l ∈ L or the null constant. The exception flag is used to handle exceptions: a state h· · ·ie with e ∈ Exc is reached after an exception e has been thrown. When equal to ⊥, the flag is omitted (normal state). The call stack records the program points (m, i) of the pending calls together with their local environments ρ and the variable r that will be assigned with the result of the call.

2.3

Operational Semantics

We now define the evaluation of expressions as a big-step transition relation ⇓ρ,σ ∈ Expr → V⊥ where σ is the current heap and ρ the current valuation of the local variables. The expression may evaluate to a value in V or to an error, represented by ⊥, if the null constant is dereferenced. Figure 2.3 presents the deduction rules defining the relation. An expression can be the null constant, in which case the rule is the identity, a variable, in which case the value is directly obtained from ρ, or a field read e.f . The latter distinguishes one normal case when e evaluates to a location, in which case the heap σ gives the value of the field for this location, and two error cases: one when e evaluates to null and another when e already evaluates to an error. We define the operational semantics of our language as a small-step transition relation over program states ⇒∈ S × S. A fixed program p is implicit in the rest of this section. Figure 2.4 2 Here, the same instruction allocates the object and calls the constructor. At bytecode level this gives raise to two separated instructions in the program (allocation and later constructor invocation) but the intermediate representation generator [DJP09] on which we rely is able to recover such construct.

14

CHAPTER 2. BIR LANGUAGE

presents the operational semantics of our language. Two cases may occur when assigning an expression: the expression may be undefined due to a null pointer exception, which leads to an exceptional state (assign 2), or the expression evaluates to a value (a location or null), in which case the local variable function is updated with the value (assign 1). Note that the constraint x 6= this is added to the semantics, as we do not want this to be overwritten. This is ensured by the Java Compiler but not by the JVM. We therefore need to check this constraint in our implementations. A field write to x.f leads to an exceptional state if x evaluates to null (putf ield 2), or to a new state (putf ield 1) where the heap contains a new value for the field f at location ρ(x). The analysis specific values of the object (ado) and of the field (adf ) are left unchanged. The constraint σ ` ρ(y) : f.ftype explicitly checks the type of the value corresponds to the type of the field. The execution is hence stuck when an attempt is made to set a field with a badly typed value. The type domain and the typing rules will be defined later on. The rule for the new instruction includes both the allocation and the call to the constructor. We use the auxiliary predicate Alloc(σ, c, l, σ 0 ) that allocates a fresh location l in heap σ with type c and all fields set equal to null. The constraint σ 0 ` ρ(y) : c.init.argtype explicitly asks the caller of the constructor to give a correct argument with respect to the type of the constructor. The rules for the non-deterministic jump are straightforward and either lead to the next instruction (cond 2) or to the target of the jump (cond 1). The call to the super constructor (super) requires the current method to be a constructor, and the receiver and the arguments to match the types of the constructor of the superclass. The current method, program point and local variables are stored in the call-stack and the new state points to the first instruction of the constructor of the superclass. A (standard) method call leads to an exception if the receiver is null (call 1). Otherwise (call 2), the lookup operator is used to find, from the method given by the instruction m0 and the dynamic type of the receiver c, the right method m00 to call. Two constraints ensure that the receiver and the argument are of the type expected by the method. The return instruction uses the same predicate when invoked in a constructor. For convenience we require each constructor to end with a return this instruction. Hence, all methods have a return value and it avoids introducing a special instruction return void.

2.4

Conclusion

We have presented an object-oriented language that, while being simple, features, e.g., a class hierarchy, instance constructors, fields and virtual method calls. We provide an (abstract) syntax and a formal small-step operational semantics. The language is at the same time rich enough to present real aspects of object-oriented languages, and simple enough to present analyses with their formalization.

2.4. CONCLUSION

15

m.instrs[i] = x ← e

x 6= this

e ⇓ρ,σ v

hm, i, ρ, σ, csi ⇒ hm, i+1, ρ[x 7→ v], σ, csi m.instrs[i] = x ← e

x 6= this

e ⇓ρ,σ ⊥

hm, i, ρ, σ, csi ⇒ hm, i, ρ, σ, csinp σ ` ρ(y) : f.ftype m.instrs[i] = x.f ← y ρ(x) = l σ(l) = [c, ado, o] o(f ) = [v, adf ] hm, i, ρ, σ, csi ⇒ hm, i+1, ρ, σ[l 7→ [c, ado, o[f 7→ (ρ(y), adf )]]], csi m.instrs[i] = x.f ← y

(assign 1)

(assign 2)

(putfield 1)

ρ(x) = null

hm, i, ρ, σ, csi ⇒ hm, i, ρ, σ, csinp m.instrs[i] = x ← new c(y) x 6= this Alloc(σ, c, l, σ 0 ) σ 0 ` ρ(y) : c.init.argtype σ 0 ` l : c.init.pre ρ0 = [· 7→ null][this 7→ l][arg 7→ ρ(y)] hm, i, ρ, σ, csi ⇒ hc.init, 0, ρ0 , σ 0 , (m, i, ρ, x) :: csi

(putfield 2)

(new )

m.instrs[i] = if (?) jmp hm, i, ρ, σ, csi ⇒ hm, jmp, ρ, σ, csi

(cond 1)

m.instrs[i] = if (?) jmp hm, i, ρ, σ, csi ⇒ hm, i+1, ρ, σ, csi m.instrs[i] = super(y) m = c.init c.super = c0 0 ρ(this) = l σ ` ρ(this) : c .init.pre σ ` ρ(y) : c0 .init.argtype 0 ρ = [· 7→ null][this 7→ ρ(this)][arg 7→ ρ(y)] hm, i, ρ, σ, csi ⇒ hc0 .init, 0, ρ0 , σ, (m, i, ρ, this) :: csi ρ0 = [· 7→ null][this 7→ l][arg 7→ ρ(y)] ρ(r) = null hm, i, ρ, σ, csi ⇒ hm, i, ρ, σ, csinp

(cond 2)

(super )

m.instrs[i] = x ← r.m0 (y)

m.instrs[i] = x ← r.m0 (y) ρ0 = [· 7→ null][this 7→ l][arg 7→ ρ(y)] ρ(r) = l σ(l) = [c, ado, o] p.lookup c m0 = m00 σ ` ρ(r) : m00 .pre σ ` ρ(y) : m00 .argtype x 6= this 00 0 hm, i, ρ, σ, csi ⇒ hm , 0, ρ , σ, (m, i, ρ, x) :: csi

(call 1)

(call 2)

m.instrs[i] = return x hm, i, ρ, σ, (m0 , i0 , ρ0 , r) :: csi ⇒ hm0 , i0 +1, ρ0 [r 7→ ρ(x)], σ, csi

Figure 2.4: Operational Semantics

(return)

16

CHAPTER 2. BIR LANGUAGE

Chapter 3

Non-Null References and Instance Field Initialization A common source of exceptional program behavior is the dereferencing of null references (also called null pointers), resulting in segmentation faults in C or null pointer exceptions in Java. Even if such exceptions are caught, the presence of exception handlers creates an additional amount of potential branching which in turn implies that: 1) fewer optimizations are possible and 2) verification is more difficult (bigger verification conditions, implicit flow in information flow verification, etc.). Furthermore, the Java virtual machine is obliged to perform run-time checks for non-nullness of references when executing a number of its bytecode instructions, thereby incurring a performance penalty.1 For all these reasons, a static program analysis that can guarantee before execution of the program that certain references will definitely be non-null is useful. At eBay, null pointer exceptions are considered as high priority issues, which should definitely be avoided [JCS07]. Note: This work is a joint work with David Pichardie and Thomas Jensen and has been published in the International Conference on Formal Methods for Open Object-based Distributed Systems (FMOODS) [HJP08a].

3.1

Introduction

Some non-null type systems for the Java language have been proposed [FL03, MPPD08]. Although non-null annotations are not used by the compiler yet, this could likely change in the future, in which case it becomes crucial to prove the soundness of the logic underlying the type checker. Furthermore, although non-null annotations are not yet mandatory, automatic type inference would help to retrofit legacy code, to lower the annotation burden on the programmer, to document the code and to verify existing code. There are three aspects that complicate non-nullness analysis: (i) fields can be accessed during object construction (before or after their initialization) which means all fields contain a default null value before their first assignment; (ii) no guarantee is given on the initialization of fields at the end of the constructor so if a field has not been initialized it contains the 1

Although hardware traps are used for free whenever possible, explicit non-nullness tests are still required as explained in [KKN00]

17

18

1

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

class A {

1

2

4

public A (){ this . m ();

5

}

3

Object f ; // NotNull

7

}

8

}

4

public B (){ this . f () = new Object ();

5

}

3

public void m (){ return ;

6

9

class B extends A {

2

public void m (){ ... this . f ...

6 7

}

8 9

}

Figure 3.1: Problem of Objects Under Construction default null value; and (iii) an object being initialized can be stored in fields or passed as an argument of methods. The first aspect means a naive flow-insensitive heap abstraction is not sufficient as all fields are null at some stage and hence would be annotated as possibly null. The second aspect makes a special initialization analysis necessary in order to track fields that are certain to be initialized during the construction. Those fields can safely be considered as non-null unless they are explicitly assigned a null value. All other fields might not have been initialized and must be considered as possibly null. The third aspect was observed in [FL03] and introduces a problem that might be easier to see on an example. Figure 3.1 shows an example where simply looking at class B, the code seems correct: the field f is always initialized to a non-null value and in the method B.m() the field f should always be initialized. However, although class A also seems correct, if class B inherits from class A, as in the example, the constructor of A, which is called at the beginning of the constructor of B, calls the method B.m(). This method dereferences field f before the constructor of B has had the possibility of initializing this field. To solve this problem, references to objects under construction need to be tracked, e.g., by a tag indicating their state. Contributions. butions.

The non-null reference analysis presented here makes the following contri-

• The analysis is fully automatic so there is no annotation burden for the programmer. • The soundness of the analysis is proven formally with respect to an operational semantics. This is the first formal correctness proof for this kind of analyses. Furthermore, this proof has been checked mechanically using the Coq proof assistant. • We provide a detailed comparison with F¨ahndrich and Leino’s type system, which is a reference among the nullness program analyses. The completeness with respect to their type system is proven. In this way, the correctness proof of our analysis also provides a formal proof of correctness of the type system of F¨ahndrich and Leino. We also show that our analysis can be slightly more precise than their type system. • The analysis can be applied to an open program: a program can be analyzed without analyzing the libraries if they have already been annotated.

3.2. RELATED WORK

19

• The checking can be done class by class, i.e., it is modular: the annotations inferred give enough information so it could be integrated into an extended BCV (Byte Code Verifier). Outline. Section 3.2 presents the related work. Section 3.3 informally introduces the semantics of nullness annotations. Section 3.4 presents the differences between the language on which the analysis is based and the language presented in Chapter 2. Section 3.5 presents the system of constraints of the analysis and Section 3.6 gives the proof of soundness. Section 3.7 then presents F¨ ahndrich and Leino’s type system and proves the completeness of our analysis with respect to their type system and Section 3.8 concludes.

3.2 3.2.1

Related Work Type Systems

Freund and Mitchell have proposed in [FM03] a type system combined with a data-flow analysis to ensure a property of correct initialization of objects. The goal is to formalize the initialization part of Java bytecode verification: every constructor must either call a constructor of the same class or a constructor of its superclass on all paths, except the constructor of java.lang.Object. In this respect it is different from our analysis, which is focused on field initialization and on their nullness property. F¨ahndrich and Leino in [FL03] have proposed another type system also combined with a data-flow analysis to ensure a correct manipulation of references with respect to a nullness property. This system is presented in Section 3.7 where we compare our inference analysis to their type system and give examples of how our analysis infers more precise types than what their system is able to check. More recently, F¨ahndrich and Xia [FX07] propose another analysis to deal with object initialization which can deal with circular data structures. However, the generality of their framework prevents their analysis from being as precise as ours on examples without circular data structures. Qi and Myers [QM09] proposed another type system for object initialization which is much more expressive than ours but which requires new language constructs and complicated type annotations. Spoto proposed another nullness inference analysis [Spo08] with a domain that expresses logical relations between variables. Logical relations allow expressing some properties that cannot be represented by our domain and vice versa. Unkel and Lam [UL08] infer stationary fields—an extension of final fields—by an analysis of the construction of objects similar to ours but that stops to precisely track objects when they are stored in the heap while ours stops at the end of the constructors.

3.2.2

Type Inference

Some works are focused on local type inference, i.e., inferring nullness properties for blocks of code, e.g. a method, independently from the rest of the code. This is notably the case of the work by Male et al. [MPPD08]. They infer a correct type for the local variables but they require annotations on method signatures and fields. To infer type annotations, Houdini [FL01] generates a set of possible annotations (non-null annotations among others) for a given program and uses ESC/Java [LSS00] to refute false assumptions. CANAPA [CFJJ06] lowers the burden of annotating a program by propagating some non-null annotations. It also relies on ESC/Java to infer where annotations are needed. Those two annotation assistants

20

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

class C {

class C {

@Nullable Object f; C(){

this.f = new Object(); } @Nullable Object m(@NonNull C x){ return x.f; } }

} (a) Too Weak Annotations

class C {

@NonNull Object f; C(){ m(this); this.f = new Object(); } @NonNull Object m(@NonNull C x){ return x.f; }

@NonNull Object f; C(){ m(this); this.f = new Object(); } @Nullable Object m(@Raw C x){ return x.f; } }

(b) Incorrect Annotation

(c) @Raw Annotations

Figure 3.2: Motivating Examples have a simplified handling of objects under construction and are intended to be used by the developer to debug and not to verify programs. Indeed, they rely on ESC/Java [LSS00], which is not sound (nor complete). JastAdd [EH06] is a tool to infer annotations for a simplified version of F¨ ahndrich and Leino’s type system with only one raw type, which does not take into account initialization done by methods (called from a constructor) and where fields cannot be declared as raw. Other works are focused on finding bugs, and mainly analyze programs backwards or on small blocks of code. Their priority is a low level of false positives. To this end, those analyses are not sound and have a non-null rate of false negatives. Examples of such works are FindBugs, by Hovemeyer et al. [HSP06, HP07] and the work of M. Nanda [NS09].

3.3

Non-Null Annotations

The main difficulty in building a precise non-null type system for Java is that all objects have their reference fields set to null at the beginning of their lifetime. Explicit initialization of fields usually occurs during the execution of the constructor, potentially in a method called from the constructor, but initialization is not mandatory and a field may be read before it is explicitly initialized—in which case it holds the null constant. We consider a field that has not been explicitly initialized during the execution of the constructor to be implicitly initialized to null at the end of the constructors. Figure 3.2(a) shows a class C while Figure 3.3 shows a model of the lifetime of an instance of class C. Assume no other method writes to C.f. The first part of the object’s lifetime is the execution of the constructor, which is mandatory and occurs to all objects. The field f is always explicitly initialized in the constructor and never written elsewhere, so any read of field f will yield a non-null reference. Despite that, if an annotation must be given for this field valid for the whole lifetime of the object, it will have to represent the non-null reference put in the field by the constructor, but also the default null constant present at the beginning of the object’s lifetime. Such an annotation is basically @Nullable where we would have clearly preferred more precise information, such as @NonNull. The solution is to consider that annotations on fields are only valid after the end of the constructor, during the rest of the object’s lifetime. The @Nullable annotations can now be replaced with @NonNull annotations. Figure 3.2(b) shows the same class where the @Nullable annotations have been replaced by @NonNull annotations and a call to the method m has been added to the constructor. The

3.4. SYNTAX AND SEMANTICS

21

initialization

class:C C.f:null

class:C C.f:v

rest of life

class:C C.f:w

Figure 3.3: Lifetime of an Object method m simply reads and returns the value of f. Although this method is not a constructor, the object x may still be in its construction phase, i.e., the constructor of the object from which the field is read may not have been fully executed, and the value actually read may be null in contradiction with the field annotation. In fact, for each variable, we need to know whether the reference may point to an object that is still in its construction phase. We annotate with @Raw such variables. As the invariant described by the annotations is not yet established during the object initialization, reading a field of an object annotated as @Raw may return a null value (@Nullable) whatever is the annotation on the field. The example in Figure 3.2(b) has been corrected in Figure 3.2(c). Note that we consider a field initialized when it has been assigned a value, whereas we consider an object initialized when it has returned from its constructor. We refine those @Raw annotations by indicating the set of classes for which a constructor has been executed. Annotations concerning fields declared in those classes can be considered as already valid despite the fact the initialization of the object is not yet completely finished. The set of classes for which a constructor has been executed can be represented by a single class as the execution order of the constructors is constrained by the class hierarchy.

3.4

Syntax and Semantics

Our analysis is based on the language presented in Chapter 2. In order to reason about object and field initialization, we instrument the semantics and the domains so (1) each field of each object has a flag which indicates if the field has been (explicitly or implicitly) initialized, and (2) the set of classes for which a constructor has been executed is attached to each object. This instrumentation is achieved by instantiating the semantic domains ADF and ADO, and by modifying the semantics of the instructions x.f ← y, return x and new c. The domains ADF and ADO are now defined as follow. ADF ADO

= {init, uninit} = P(Class)

The semantic rule for the new c instruction needs not be modified: only the auxiliary predicate Alloc(σ, c, l, σ 0 ), which allocates a fresh location l in heap σ with type c and all fields set equal to null, needs to be modified. A fresh object has all its fields flagged as uninitialized (uninit), and the set of classes for which a constructor has been executed is initially empty. Figure 3.4 presents the semantic rules for the instructions x.f ← y and return x. As in the original semantics presented in Figure 2.4, the semantics of the x.f ← y instruction needs two rules. Only the second one (getfield 2), which actually sets a field, is modified (now labeled as (getfield 20 )). Whatever is the initialized state adf of the field f , the field is considered as initialized (flagged as init) in the next state. The semantic rule

22

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

m.instrs[i] = x.f ← y

ρ(x) = null (getfield 1)

hm, i, ρ, σ, csi ⇒ hm, i, ρ, σ, csinp m.instrs[i] = x.f ← y

ρ(x) = l

σ(l) = [c, ado, o]

o(f ) = [v, adf ]

hm, i, ρ, σ, csi ⇒ hm, i+1, ρ, σ[l 7→ [c, ado, o[f 7→ (ρ(y), init)]]], csi m.instrs[i] = return x m = c.init σ(ρ(this)) = [c0 , ado, o] σ 0 = σ[ρ(this) 7→ [c0 , ado ∪ {c}, SetAllInit(o)]] hm, i, ρ, σ, (m0 , i0 , ρ0 , r) :: csi ⇒ hm0 , i0 +1, ρ0 [r 7→ ρ(x)], σ, csi ∀c, m 6= c.init

m.instrs[i] = return x 0

0

0

0

hm, i, ρ, σ, (m , i , ρ , r) :: csi ⇒ hm , i0 +1, ρ0 [r 7→ ρ(x)], σ, csi

(getfield 20 )

(return 0 1)

(return 0 2)

Figure 3.4: Instrumented Operational Semantics (return) is now replaced by 2 rules, (return 0 1) and (return 0 2). The first one corresponds to a return instruction in a constructor. In such a case, the current class is added in the set of classes for which a constructor has been executed, and all fields of the current class that were still flagged as uninit are now flagged as init with the auxiliary predicate SetAllInit. The rule (return 0 2) has the same effect as the original rule (return) but for methods that are not constructors. This instrumentation does not modify the behavior of the program but eases the correctness proof of the analysis.

3.5

Null-Pointer Analysis

We will now present the analysis that is able to infer the annotations previously presented and prove a program is null-pointer error safe. As previously mentioned, although the analysis annotates local variables, method parameters and return values, the main difficulty is to analyze the fields. The analysis infers properties of the code while the annotations previously presented were introduced to let the programmer expresses design intents. This explains some differences in the naming: the @NonNull, @Nullable and @Raw annotations correspond, respectively, to the NotNull, MayBeNull and Raw properties inferred by the analysis herein presented.

3.5.1

Modular Type Checking

The nullness properties inferred by our analysis can be added to the program using the Java annotations. If those annotations were used by the JIT (Just In Time compiler) to remove runtime checks or to optimize the native code, those annotations would need to be checked. Reflection in Java allows retrieving code from the network or dynamically generating code. Thus, the whole code may not be available before actually executing the program. Instead, code is made available class by class, and checked by the BCV (Java ByteCode Verifier) at load time. As the whole program is not available, the type checking must be modular: there must be enough information in a method to decide if this method is correct and, if an incorrect method is found, there must exist a safe procedure to end the program—usually throwing an

3.5. NULL-POINTER ANALYSIS Val] Init] TVal] Heap] LocalVar] Method] State]

= = = = = =

{Raw(Y ) | Y ∈ Class} ∪ {Raw− , NotNull, MayBeNull} {Init, MaybeUnInit} Field * Init] Field → Val] Var → Val] ∗ Meth → {| thisPre ∈ Val] × TVal] ; arg ∈ (Val] ) ; ] ] thisPost ∈ TVal ; ret ∈ Val |} 

= Method] × Heap] × Meth × L * TVal] × Meth × L * LocalVar]

23



Figure 3.5: Abstract domains exception, i.e., it must not be too late. To a have a modular type checker (while keeping annotations simple enough), method parameters, respectively return values, need to be contra-variant, respectively co-variant, i.e., the types of the overriding methods need to be at least as general as the types of the overridden method. Note that this is not surprising: the same applies in the Java language (although Java imposes the invariance of method parameters instead of the more general contra-variance). When a method call is found in a method, theses variance properties allow the analysis to rely on the types of the resolved method (as all the methods which may actually be called cannot be known before the whole program is loaded).

3.5.2

Abstract Domains

In this section we define an analysis that for each class of a well-typed program infers annotations about nullity of its fields together with pre- and post-conditions for its methods. The basic properties of interest are NotNull—meaning “definitely not-null”—and MayBeNull —meaning possibly a null reference. The field annotations computed by the analysis are represented as a heap abstraction H ] ∈ Heap] that provides an abstraction for all fields of all initialized objects and all initialized fields of objects being initialized. As explained in Section 3.3, object initialization requires a special treatment because all fields are null when the object is created so this would lead a simple-minded analysis to annotate all fields as MayBeNull. However, we want to infer non-null annotations for all fields that are initialized explicitly to be so in a constructor. In other words, fields that are not initialized in a constructor are annotated as MayBeNull and all other fields have a type compatible with the value they have been initialized with. To this end, the analysis tracks field initializations of the receiver in constructors (and methods called from constructors). This is done via an abstraction of the receiver by a domain TVal] , defined in Figure 3.5, which maps each of the fields declared in the current class to Init or MaybeUnInit. At the beginning of the object’s lifetime, the abstraction of the current object T ] ∈ TVal] associates to each field defined in the current class the abstract value MaybeUnInit. The abstraction then evolves as fields are initialized. The reason for limiting the information to fields defined in the current class (and not to all fields of instances of the current class) is to keep annotations more modular and easier to understand by a developer. References are then abstracted by a domain Val] which incorporates the “raw” references from [FL03]. A Raw− value denotes a non-null reference of an object being initialized, i.e., which does not yet respect his invariant (e.g., Raw− can be used as a property of this when

24

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

x ∈ Val] NotNull v x

Raw(X) v Raw−

XY Raw(X) v Raw(Y )

x ∈ Val] x v MayBeNull

Init v MaybeUnInit

Figure 3.6: Selected partial orders it occurs in constructors). If a reference is known to have all its fields declared in class X and in the parents of X initialized, then the reference is Raw(X). The inclusion of “raw” references allows the manipulation of objects during initialization because the analysis can use the fact that for an object of type Raw(X), fields declared in X and above have a valid annotation in the abstract heap H ] . A NotNull value denotes a non-null reference that has finished its initialization. As the annotations must be easy to read, they are context-insensitive, but, to achieve some precision, the analysis is inter-procedural and method signatures are inferred from the join of the calling contexts (as in 0-CFA [Shi88]). For any method m, M ] (m)[thisPre] is an approximation of the initialization state (the abstraction Val] and the set of already initialized fields) of the receiver before the execution of m, while M ] (m)[thisPost] gives the corresponding approximation at the end of the execution. M ] (m)[arg] approximates the parameter of the method, taking into account every context in which m may actually be invoked. M ] (m)[ret] approximates the return value of m. Those domains are then combined to form the abstract state that corresponds to all reachable states of the program. To be able to use strong updates [CWZ90], i.e., to precisely analyze assignments, the abstractions of the local variables (L] ∈ Meth × L → LocalVar] ) and the current object (T ] ∈ Meth × L * TVal] ) are flowsensitive while, as discussed earlier, the abstraction of the heap is flow-insensitive. We only give the definition of the partial order for Val] and Init] in Figure 3.6. The other orders are defined in a standard way using the canonical orders on functions, products and lists. The final domain State] is hence equipped with a straightforward lattice structure.

3.5.3

Inference Rules

The analysis uses two auxiliary operators defined in Figure 3.7. The function ρ transfers information from the domain Val] to the domain TVal] : ρ transforms an abstract reference Val] to a TVal] abstraction of a current object this in the class C. The notations ⊥C , respectively >C , correspond to the function that maps all fields defined in C to Init, respectively MaybeUnInit. The analysis also relies on an abstract evaluation JeK]C,H ] ,t] ,l] of expressions

parameterized by the current class C, an abstract heap H ] , an abstraction t] of the fields (declared in class C) of the receiver object and an abstraction l] of the local variables at the current program point. The first equation states that the type of a local variable is obtained from the L] function and, if the local variable is the receiver of the current method and it is sufficiently initialized, it can be refined from Raw(super(C)) to Raw(C). The second equation states that the type of a field is obtained from the heap abstraction if the reference points to an object sufficiently deeply initialized. Otherwise, it is MayBeNull. In the inference rules we write JeK] for JeK]class(m),H ] ,T ] (m,i),L] (m,i) . We also define the function class that takes as argument a method and returns the class in which it is defined (for any method m of the

3.5. NULL-POINTER ANALYSIS

25

ρ(c, NotNull)

=

(NotNull, ⊥c )

ρ(c, Raw(c0 ))

=

if c0  c then (Raw(c0 ), ⊥c ) else (Raw(c0 ), >c )

ρ(c, Raw− )

=

(Raw− , >c )

ρ(c, MayBeNull)

=

(MayBeNull, >c )

JxK]C,H ] ,t] ,l] Je.f K]C,H ] ,t] ,l]

 =

=

Raw(C) L] (x)

if x = this ∧ t] = ⊥c ∧ l] (x) = Raw(super(C)) otherwise

8 H ] (f ) > > > < > > > :

MayBeNull

if JeK]C,H ] ,t] ,l] = NotNull

or (e = this ∧ t] (f ) = Init ∧ class(f ) = C) or JeK]C,H ] ,t] ,l] = Raw(X) with X  class(f ) otherwise

Figure 3.7: Auxilary operators program, we have m ∈ class(m).methods). We overload this function with its counterpart for fields (we have f ∈ class(f ).fields for all field f of the program). The analysis is specified via a set of inference rules, shown in Figure 3.8, which defines a judgment M ] , H ] , T ] , L] ` (m, i) : instr for when the abstract state (M ] , H ] , T ] , L] ) ∈ State] is coherent with instruction instr at program point (m, i). For each such program point, this produces a set of constraints over the abstract domain State] , whose solutions constitute the correct analysis results. The rules make use of an abstract evaluation function for expressions (explained below) that we write JeK] . An example of a program with the corresponding constraints is given in Section 3.5.4. Assignment to a local variable (rule (1)) simply assigns the abstract value of the expression to the local variable in the abstract environment L] . Assignment to a field (2) can either be to a field of the current object and the current class, in which case the field becomes “initialized”, or to another object or a field of another class in which case no initialization information is learned (TVal] is unchanged). In both cases, the abstract heap H ] is augmented with the value of the expression as a possible value for the field. When a return instruction is encountered (3), the current abstraction of the receiver is propagated to the abstraction of the receiver at the end of the method (M ] (m)[thisPost]), the abstract value returned is propagated to the signature of the method (M ] (m)[ret]), and all still-uninitialized fields are explicitly set to MayBeNull. An object created by a new instruction (4) is initially completely uninitialized. As the constructor of its class is called on it, the abstraction for the receiver is set to (Raw− , >C ). The constructor is known to initialize the object; hence the variable in which the object is stored is set to NotNull. Conditional jumps (5) simply propagate the state before the instruction to both possible successor instructions. The method call (6) and (6’) distinguishes two cases, depending on whether the receiving object is the current object this and the call is to a method in the current class or not. In the first case, the abstraction of the current receiver (JrK] , T ] (m, i)) is used for the abstraction of the receiver of the method invoked, while in the second case the ρ function (defined in Figure 3.7) is used to built such an abstraction from the abstraction of the local variable r. In the first case, the post condition on the receiver of the method invoked (i.e., the set of fields initialized at the end of m0 ) is used to refine the abstraction of the receiver t] = M ] (m0 )[thisPost] u T ] (m, i). This abstraction is then explicitly used to refine the abstraction of the receiver in L] and passed to the next

26

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

T ] (m, i) v T ] (m, i + 1)

L] (m, i)[x 7→ JeK] ] v L] (m, i + 1)

(1) ` (m, i) : x ← e ...................................................................................................................... x = this ∧ f ∈ class(m).fields ¬(x = this ∧ f ∈ class(m).fields) T ] (m, i)[f 7→ Init] v T ] (m, i + 1) T ] (m, i) v T ] (m, i + 1) L] (m, i) v L] (m, i + 1) L] (m, i) v L] (m, i + 1) (2) JeK] v H ] (f ) JeK] v H ] (f ) M ] , H ] , T ] , L]

M ] , H ] , T ] , L] ` (m, i) : x.f ← e M ] , H ] , T ] , L] ` (m, i) : x.f ← e ...................................................................................................................... T ] (m, i) v M ] (m)[thisPost] JeK] v M ] (m)[ret] ` ´ m = class(m).init =⇒ ∀f ∈ c.fields. T ] (m, i)(f ) = MaybeUnInit =⇒ MayBeNull v H ] (f ) (3) M ] , H ] , T ] , L] ` (m, i) : return e ...................................................................................................................... m0 = c.init (Raw− , >C ) v M ] (m0 )[thisPre] JeK] v M ] (m0 )[arg] (4) L] (m, i)[x 7→ NotNull] v L] (m, i + 1) T ] (m, i) v T ] (m, i + 1) M ] , H ] , T ] , L] ` (m, i) : x ← new c(e) ...................................................................................................................... L] (m, i) v L] (m, i + 1) L] (m, i) v L] (m, jmp) T ] (m, i) v T ] (m, i + 1) T ] (m, i) v T ] (m, jmp) (5) M ] , H ] , T ] , L] ` (m, i) : if (?) jmp ...................................................................................................................... (r = this ∧ class(m) = class(m0 )) (JrK] , T ] (m, i)) v M ] (m0 )[thisPre] JeK] v M ] (m0 )[arg] ] = M ] (m0 )[thisPost] u T ] (m, i) t iˆ h (6) ˜ L] (m, i) r 7→ JrK]class(m),H ] ,t] ,l] x 7→ M ] (m0 )[ret] v L] (m, i + 1) t] v T ] (m, i + 1)

` (m, i) : x ← r.m0 (e) ...................................................................................................................... ¬(r = this ∧ class(m) = class(m0 )) ρ(class(m0 ), JrK] ) v M ] (m0 )[thisPre] ] ] 0 ˆ JeK v M (m )[arg] ˜ (60 ) L] (m, i) x 7→ M ] (m0 )[ret] v L] (m, i + 1) T ] (m, i) v T ] (m, i + 1) M ] , H ] , T ] , L]

M ] , H ] , T ] , L] ` (m, i) : x ← r.m0 (e) ...................................................................................................................... m0 = class(m).super.init ] (this , T ] (m, i)) v M ] (m0 )[thisPre] JeK] v M ] (m0 )[arg] (7) ] L (m, i) [this 7→ Raw(class(m).super)] v L] (m, i + 1) T ] (m, i) v T ] (m, i + 1) M ] , H ] , T ] , L] ` (m, i) : super(e) ...................................................................................................................... ∀m, m0 , overrides(m0 , m) =⇒ M ] (m)[arg] v M ] (m0 )[arg] ∀m, m0 , overrides(m0 , m) =⇒ (NotNull, >class(m) ) v M ] (m)[thisPost] ∀m, m0 , overrides(m0 , m) =⇒ (NotNull, >class(m0 ) ) v M ] (m0 )[thisPre] ` ´ ∀m, v, t, (v, t) = M ] (m)[thisPre] =⇒ t v T ] (m, 0) ∧ [this 7→ v][arg 7→ M ] (m)[arg]] v L] (m, 0) (8) ∀m, M ] (m)[arg] v L] (m, 0)(arg) ∀m, x, x 6= arg =⇒ MayBeNull v L] (m, 0) ] − ] >class(main) v T (main, 0) Raw v L (main, 0)(this) ∀m, ∀i, M ] , H ] , T ] , L] ` (m, i) : Pm [i] M ] , H ] , T ] , L] ` P

Figure 3.8: Inference rules

3.5. NULL-POINTER ANALYSIS

1 2 3

27

class A { private Object f ; private Object g ;

4

A ( Object o ){ f = o ;}

5 6

void m (){ g = f ;}

7 8

public static void main ( String args []){ Object o = new Object (); A a = new A ( o );

9 10 11

a . m ();

12

}

13 14

}

Figure 3.9: Java source

program point. One can notice that, without loss of generality, the inference rule handles expressions where the language only authorizes local variables. The whole constraint system of a program P is then formally defined by the judgment ` P . The predicate overrides(m0 , m) holds when the method m0 overrides m. In such case, as explained in Section 3.5.1, we require a contravariant property between the parameters of m and m0 . Any overriding overrides(m0 , m) needs also to invalidate (in terms of precision) M ] (m)[thisPost] because a virtual call to m could lead to the execution of m0 which is not able (by definition of TVal] ) to track the initialization of fields declared in the class of m. A similar constraint is required for M ] (m0 )[thisPre] because for a virtual call to m we have only constrained M ] (m)[thisPre] and not M ] (m0 )[thisPre]. The constraints on T ] (main, 0) and L] (main, 0)(this) ensure a correct initialization of the main method.

M ] , H ] , T ] , L]

Expressing the analysis in terms of monotone constraints over lattices has the immediate advantage that the least solution of the system can be computed by standard iterative techniques. Finally an abstract state is said safe (written safe]P (M ] , H ] , T ] , L] )) when for each program point (m, i), if Pm [i] is of the form x.f ← e or x ← r.m(e) then L] (m, i)(x) 6= MayBeNull, and for all expressions e which appear in the instruction Pm [i], any dereferenced sub-expression e0 has an abstract evaluation strictly lower than MayBeNull.

3.5.4

Example

The Java source code of our example is provided in Figure 3.9 while Figure 3.10 shows the code in the syntax defined in Chapter 2 and the constraints obtained from the rules defined in Section 3.5.3. This example is fairly simple and, for conciseness, the code of the main method has been omitted, but the generated constraints should be sufficient to give an idea of how the inference works. The fixpoint iteration on those constraints yields the following heap abstraction and

28

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

1: class A { 2: Object f; 3: Object g; 4: 5: o){ ´ ˜ ˆvoid init(Object ` 6: this 7→ snd M ] (A.init)[thisPre] ; o 7→ M ] (A.init)[arg] v L] (A.init, 0) ] ] 7: f st(M (A.init)[thisPre]) v T (A.init, 0) 8: 0: super(null); 9: (JthisK] , T ] (A.init, 0)) v M ] (Object.init)[thisPre] 10: JnullK] v M ] (Object.init)[arg] 11: L] (A.init, 0)[this 7→ Raw(Object)] v L] (A.init, 1) 12: T ] (A.init, 0) v T ] (A.init, 1) 13: 1: this.f = o; 14: JoK] v H ] (A.f) 15: T ] (A.init, 1)[A.f 7→ Init] v T ] (A.init, 2) 16: L] (A.init, 1) v L] (A.init, 2) 17: 2: return this; 18: if T ] (A.init, 2)(A.f) = MaybeUnInit then MayBeNull v H ] (A.f) 19: if T ] (A.init, 2)(A.g) = MaybeUnInit then MayBeNull v H ] (A.g) 20: JthisK] v M ] (A.init)[ret] 21: T ] (A.init, 2) v M ] (A.init)[thisPost] 22: } 23: 24: ˆvoid m(Object ` arg){ ´ ˜ 25: this 7→ snd M ] (A.m)[thisPre] ; arg 7→ M ] (A.m)[arg] v L] (A.m, 0) 26: f st(M ] (A.m)[thisPre]) v T ] (A.m, 0) 27: 0: this.g = this.f 28: T ] (A.m, 0)[A.g 7→ Init] v T ] (A.m, 1) 29: L] (A.m, 0) v L] (A.m, 1) 30: Jthis.fK] v H ] (A.g) 31: 1: return this.f 32: T ] (A.m, 1) v M ] (A.m)[thisPost] 33: Jthis.fK] v M ] (A.m)[ret] } }

Figure 3.10: Code with constraints

3.6. CORRECTNESS

29

method signatures: H] M]

= {f  7→ NotNull, g 7→ MayBeNull} A.init 7→ {| thisPre = (>A , Raw− ); arg = NotNull;    thisPost = ⊥A ; ret = Raw(A) |}; = A.m → 7 {| thisPre = (⊥A , NotNull); arg = MayBeNull;    thisPost = ⊥A ; ret = NotNull |};

      

Lines 6, 7, 25 and 26 list the constraints obtained from rule (8) in Figure 3.8. They “initialize” the flow-sensitive abstractions L] (m, 0) and T ] (m, 0) with the information of the method signatures. All other constraints are directly deduced from the rule corresponding to the instruction. Lines 9 to 12 list the constraints obtained from rule (7). Lines 14 to 16 and 28 to 30 correspond to the rule (2), a field assignment on this of a field defined in the current class. Lines 18 to 21 correspond to a return x instruction (rule (3)) of a constructor, which adds the value MayBeNull in the abstract heap for all (maybe) uninitialized fields, while lines 32 and 33 correspond to a return x instruction of a non-constructor method. The methods are called from the main method of Figure 3.9. M ] (A.init)[thisPre] is equal to (>A , Raw− ) as it is called on a completely uninitialized value. The constraint line 11 then refines Raw− to Raw(Object) so if a field of Object were accessed in the rest of the method, the abstraction of the heap would be used. The NotNull abstraction of the argument of the init method is first transferred to a local variable at line 6 and then copied from o to H ] (A.f) at line 14. At the same time, line 15 records that the field f is initialized. Then, line 18, as f has been initialized, H ] (A.f) is not modified whereas, line 19, T ] (A.init, 2)(A.g) = MaybeUnInit so H ] (A.g) is constrained to MayBeNull.

3.6

Correctness

We first define the link between the abstract domains and the concrete domains. The two first rules of Figure 3.11 define the relation between the abstract domain Init] and the concrete domain (V × ADF). Init is a correct abstraction of (v, d) if v ∈ V and d = init, while MaybeUnInit is a correct abstraction of any (v, d) ∈ (V × ADF). The next four rules define the relation between the abstract domain Val] and the concrete domain V. The relation ∼ is parameterized by a concrete heap σ. NotNull correctly abstracts v if v points to an object o that has all its fields initialized. Raw(A) may abstract v if v points to an object o and all fields defined in parents of A have already been initialized. MayBeNull may abstract any value v ∈ V and Raw− may abstract any location v ∈ L. The next rule is also parameterized by a concrete heap σ and defines the relation between the concrete domain of local variables R and its abstract counterpart LocalVar] . A function l] may abstract a local variable function l if the content of l] correctly abstracts the content for l for all variables. The rule parameterized by a concrete heap σ and the class of the current method c defines a correct abstraction of a receiver. t] ∈ TVal] correctly abstracts a receiver r ∈ V if r points to an object o, t] is defined for all fields of c and for all fields of c the abstraction t] correctly abstracts the content of the object o. A concrete heap σ may be abstracted by H ] if, for all objects o of the concrete heap, the abstract heap H ] correctly abstracts all the fields that have been initialized. Finally, the last two rules define the relation between an abstract state (M ] , H ] , T ] , L] ) ∈ State] and a concrete state hm, iρ, σ, csie ∈ S. First, any abstract state correctly abstracts a concrete error state hm, iρ, σ, csinp . Second, an abstract state correctly abstracts hm, iρ, σ, csi if H ] correctly

30

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

v∈V d ∈ {init, uninit} MaybeUnInit ∼ (v, d)

v∈V Init ∼ (v, init)

∀f ∈ dom(o), o(f ) = ( , init) NotNull ∼σ v S σ(v) = ( , , o) ∀f ∈ Ac c.fields, o(f ) = ( , init)

σ(v) = ( , , o)

Raw(A) ∼σ v

v∈V MayBeNull ∼σ v v∈L Raw− ∼σ v

∀x, l] (x) ∼σ ρ(x) l] ∼σ ρ σ(r) = ( , , o)

dom(t] ) ∩ dom(o) ⊇ c.fields

∀f ∈ c.fields, t] (f ) ∼ o(f )

t] ∼σ,c r ∀l, σ(l) = ( , , o) =⇒ ∀f ∈ dom(o), o(f ) = (v, d) =⇒ d = uninit ∨ H ] (f ) ∼σ v H] ∼ σ (M ] , H ] , T ] , L] ) ∼ hm, i, ρ, σ, csinp L] (m, i) ∼σ ρ H] ∼ σ T ] (m, i) ∼σ,class(m) ρ(this) 0 0 0 0 ] 0 0 0 ] ∀(m , i , ρ , r ) ∈ cs, (L (m , i ) ∼σ ρ ∧ T (m0 , i0 ) ∼σ,class(m0 ) ρ0 (this))) (M ] , H ] , T ] , L] ) ∼ hm, i, ρ, σ, csi

Figure 3.11: Correctness relations abstracts σ, if the abstractions T ] and L] at program point (m, i) correctly abstract ρ(this) and ρ, respectively, and if all ρ0 in the call stack are correctly abstracted by T ] and L] at their respective program points (m0 , i0 ). Theorem 3.1 (constraint system soundness) If there exists (M ] , H ] , T ] , L] ) such that M ] , H ] , T ] , L] ` P and safe]P (M ] , H ] , T ] , L] ) holds then P is null-pointer error safe. Proof 3.1 We first prove that if a program state is correctly abstracted by an abstract state resulting from the constraint solving, then after another step of the semantics, the new state is also correctly abstracted by the abstract state. We also prove another lemma stating that if an abstract state S ] correctly abstracts a concrete state s and if safe]P (S ] ) then s is a safe concrete state. The correctness of the analysis has been originally proven on a language that had the same observational behavior but with different semantics. The semantics of the language on which the analysis has been proven was expressed as a small-step relation except for method calls, which were big-step. This changes the correctness relations and the proofs. Although the correctness relations are joint work with David Pichardie and have been updated to our new language in this document, the correctness proof had been done by David Pichardie and will therefore not be adapted to be put in this document. Yet, the interested reader can refer to our technical report [HJP08b] containing the proof, or to the Coq development available

¨ 3.7. FAHNDRICH AND LEINO’S TYPE SYSTEM

L ` x : L(x)

c  class(f ) L ` e : Raw(c) L ` e.f : f.ftype

L ` e : Raw− L ` e.f : MayBeNull

31

L`e:A AvB L`e:B

Figure 3.12: F¨ ahndrich and Leino’s Type System: Expression Typability at http://www.irisa.fr/celtique/pichardie/ext/np/, which contains the proof that has been verified in the Coq proof assistant.

3.7

F¨ ahndrich and Leino’s Type System

In this section we compare our analysis with the type system proposed by F¨ahndrich and Leino [FL03]. In their paper, the authors only give an informal presentation of their type system. Thus, the formalization herein presented is our interpretation of their work. Type systems require type annotations in the source program. We therefore extend the language presented in Chapter 2 by implementing the type domain Type. Field types f.ftype, method receiver types m.pre, method argument types m.argtype and method return types m.rettype take their values in Val] . The same domain is used in F¨ahndrich and Leino’s type system and in our analysis. Indeed, we have chosen the same annotation domain for our analysis to be able to compare our analysis and their type system, which was at the state of the art. Some methods, called from constructors, are meant to initialize some fields of their receiver. F¨ ahndrich and Leino have introduced a type annotation for this special purpose. A method may have an inits F type if the method initializes all the fields in F , where F is a subset of the set of fields defined in the current class. We use the field m.post ∈ P(Field) for this type annotation. If a method has no inits F type annotation then m.post = ∅. Figure 3.12 presents the typing rules for expressions. A typing judgment for an expression e is of the form L ` e : τ with L a type annotation for local variables and τ a type in Val] . The type of a local variable is simply the type stored in L for this variable. If an expression e is of type Raw(c) (or one of its subtype), then e.f has type f.ftype if f is declared in class c or in a parent of c. Otherwise, if the expression e is not sufficiently initialized neither, then e.f has type MayBeNull. Note that e.f is not typable if e is of type MayBeNull: null pointer exceptions are removed. The last rule presents a standard sub-typing rule. Figure 3.13 presents the typing rules for instructions. Each instruction instr is associated to a type judgment m ` instr : L → L0 where m is the current method, L the current annotation for local variables and L0 a valid annotation after the execution of instr . The typing rule for a local variable assignment checks that expression is typable, and propagates this type to the local variable at the next program point. The judgment for field assignments checks two properties: that the type f.pre of the field f subsumes the type τ of the expression e, and that the type of the reference x is not MayBeNull. The rule for the x ← new c(y) instruction checks that the constructor accepts a Raw− receiver and that the argument is of a correct type. The new instruction is guaranteed to return a reference to a completely initialized object and therefore x is propagated with a NotNull type to the next program point. The judgment for the call to the super constructor checks that receiver and the argument are of the type expected by the super constructor and that the constructor expects a non-null receiver. Because the super constructor initializes the objects down to the class c0 , the type of

32

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

L`e:τ m ` x ← e : L → L[x 7→ τ ]

L`e:τ

τ v f.ftype L(x) 6= MayBeNull m ` x.f ← e : L → L

L ` y : c.init.argtype c.init.pre = Raw− m ` x ← new c(y) : L → L[x 7→ NotNull] c0 = class(m).super

c0 .init.pre 6= MayBeNull m ` this : c0 .init.pre m ` y : c0 .init.argtype 0 m ` super(y) : L → L[this 7→ L(this) u Raw(c )]

0 c0 = class(m ) m0 .pre 6= MayBeNull m ` r : m0 .pre m ` y : m.argtype  0 0 0 L(r) u Raw(c ) if fields(c ) ⊆ m .post ∧ class(m) = c0 ∧ r = this τ0 = L(r) otherwise

m ` x ← r.m0 (y) : L → L[r 7→ τ 0 ][x 7→ m0 .rettype] m ` if (?) jmp : L → L

Figure 3.13: F¨ ahndrich and Leino’s Type System: Instruction Typability the receiver in the current method at the next program point may be refined to Raw(c0 ). Like for the former rule, the typing judgment for the method call checks that the method only accepts a non-null receiver and that the receiver and the argument have types compatible with the expected types of the method. If the method is declared to initialize all the fields defined in the current class and is called on the receiver of the current class, then the type of the receiver may be refined to Raw(c0 ) where c0 is the type of the current method. This type system is coupled with a dataflow analysis to ensure that all fields not declared as MayBeNull in class c are sure to be initialized at the end of all constructors of c. It is a standard dataflow analysis, described in Figure 3.14. The function F ∈ Meth × L * P(Field) represents, for each program point (m, i), the fields that are not yet initialized. At the beginning of every method m that is either a constructor or declared to initialize a nonempty set of fields, F(m, 0) is constrained to the set of fields defined in the class of m. For field assignment on the receiver this, the field is not propagated to the next node if is it defined in the current class. For method calls to methods that declare to initialize some fields, fields in m0 .post are not propagated to the next node. When a return instruction is encountered, if the method is a constructor, then it is checked that if a field has not yet been initialized, then its type subsumes MayBeNull, or if the method is annotated as inits F , then it checks that all fields that are not yet initialized are not in F . Figure 3.15 shows the definition of a well-typed method and a well-typed program. For a method to be well typed with respect to L, the following constraints are required. • There exists L such that for all methods m and for all program points i, either Pm [i] = return e and m ` e : m.rettype, or for all successor points j of i, there exists L0 such that L0 v L(m, j) and m ` Pm [i] : L(m, i) → L0 . • The method requires at least a Raw− receiver (the receiver must not be MayBeNull). • The fields the method claims to initialize are defined in the current class. • All local variables are null at the first program point of the method except this and arg, which have types compatible with m.pre and m.arg, respectively.

¨ 3.7. FAHNDRICH AND LEINO’S TYPE SYSTEM

F(m, i) \ {f } ⊆ F(m, i + 1) F ` (m, i) : this.f ← e F(m, i) ⊆ F(m, i + 1) F ` (m, i) : x ← e

33

x 6= this F(m, i) ⊆ F(m, i + 1) F ` (m, i) : x.f ← e F(m, i) ⊆ F(m, i + 1) F ` (m, i) : x ← new c(y)

F (m, i) ⊆ F(m, i + 1) F ` (m, i) : super(y) class(m) = class(m0 ) r = this F(m, i) \ m0 .post ⊆ F(m, i + 1) F ` (m, i) : x ← r.m0 (y)

¬ (class(m) = class(m0 ) ∧ r = this) F(m, i) ⊆ F(m, i + 1) F ` (m, i) : x ← r.m0 (y)

F(m, i) ⊆ F(m, i + 1) F(m, i) ⊆ F(m, jmp) F ` (m, i) : if (?) jmp m = class(m).init ∀f ∈ F(m, i).(MayBeNull v f.ftype) F ` (m, i) : return e

m 6= class(m).init F(m, i) ∩ m.post = ∅ F ` (m, i) : return e

∀m, m = class(m).init ∨ m.post 6= ∅ =⇒ fields(class(m)) ⊆ F(m, 0) ∧ ∀i, F ` (m, i) : Pm [i] F`P

Figure 3.14: Dataflow Analysis to Ensure All Non-MayBeNull Fields Are Initialized • If the method claims to initialize some fields, then the constructors above the current class should have been run. This last constraint is not really necessary, but a different annotation would be useless and would probably be symptomatic of an error. If m ∈ c.methods initializes f ∈ c.fields and requires a receiver of type m.pre v Raw(c), i.e., more precise than Raw(c.super), then it means that f has already been initialized, either directly or because the constructor of class c has returned and f.ftype is MayBeNull. If m requires a receiver of type m.pre w Raw(c.super.super), then the method may be called from the constructor of c.super but this constructor only tracks the initialization of fields defined in c.super, so the information m.post becomes useless. A program is said well typed with respect to L and F if there are no overridden methods annotated as [inits F ] (hence m.post = ∅), receivers and arguments are contravariant, return values are covariant, all methods are well typed with respect to L and F is a solution of the data-flow analysis (F ` P ). Theorem 3.2 If P is FL-typable with respect to F and L then there exists (M ] , H ] , T ] , L] ) such that M ] , H ] , T ] , L] ` P and safe]P (M ] , H ] , T ] , L] ) holds. Proof 3.2 We first define a conversion function Conv that given the type annotation in P , L and F , returns an abstract state (M ] , H ] , T ] , L] ). We then prove by induction on expressions that if an expression e is FL-typable with type τ , then the abstract evaluation of e given the abstract state Conv(P, L, F ) is more precise than τ and safe]expr (e) holds. We also prove that if a program is FL-typable then Conv(P, L, F ) ` (m, i) : Pm [i] holds for all program points (m, i) and safe] (M ] , H ] , T ] , L] ) holds. This proof is done by a case analysis on the instructions. We then conclude that (M ] , H ] , T ] , L] ) ` P holds.

34

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

m.pre 6= MayBeNull m.post ⊆ class(m).fields m.post 6= ∅ =⇒ m.pre = Raw(class(m).super) [· 7→ null][arg 7→ m.argtype][this 7→ m.pre] v L(m, 0) ∀i, Pm [i] = return e =⇒ L(m, i) ` e : m.rettype ∀i, j, j ∈ succ(i) =⇒ (∃L0 , L0 v L(m, j) ∧ m ` Pm [i] : L(m, i) → L0 ) L, m ` P ∀m, m0 , overrides(m, m) =⇒ m0 .post = ∅ ∀m, m0 , overrides(m, m) =⇒ m0 .pre v m.pre ∀m, m0 , overrides(m, m) =⇒ m0 .argtype v m.argtype ∀m, m0 , overrides(m, m) =⇒ m.rettype v m0 .rettype ∀m, L, m ` P F`P L, F ` P

Figure 3.15: Program Typability A detailed proof is given in Appendix D of [HJP08b]. By proving our type system complete with respect to F¨ahndrich and Leino’s type system, as we have also proven our type system sound, we directly obtain that the type system proposed by F¨ ahndrich and Leino is also sound (or at least our formalization of their type system). Corollary 3.1 (FL type system soundness) If P is FL-typable then P is null-pointer error safe with respect to the preconditions given by the type annotations in P . Proof 3.3 Direct consequence of Theorem 3.2 and Theorem 3.1. This type system is combined with a dataflow analysis. An advantage of the unified framework we use is a gain in precision compared to the type system thanks to a better communication between the domains. Theorem 3.3 There exists P such that P is not FL-typable and there exists (M ] , H ] , T ] , L] ) such that M ] , H ] , T ] , L] ` P and safe]P (M ] , H ] , T ] , L] ) holds. Proof 3.4 As shown in Figure 3.8, the analysis of expressions is parameterized on the abstraction of this in order to know if a field has already been initialized or not. In F¨ ahndrich and Leino’s analysis, the type checking is separated from the data-flow analysis that knows which fields have already been initialized. For example, in class C { Object f; //NotNull Object g; //MayBeNull or NotNull? public C(){ this.f = new Object(); this.g = this.f;} } our analysis benefits from the abstraction of this and knows this.f has been initialized before it is assigned to this.g. In F¨ ahndrich and Leino’s analysis, an intermediate local variable set to the new object and then assigned to f and g, or an explicit cast operator that checks the

3.8. CONCLUSIONS

35

initialization at run-time would be needed in order to type the program. Our abstraction of this can also be passed from a method to another, here also keeping some more information as the intra-procedural data-flow of F¨ ahndrich and Leino. F¨ahndrich and Leino also propose several extensions to their type system: dynamic features like casting, an extension for arrays (which relies on dynamic checks) and another for static fields, which is deliberately unsound. We did not formalize those extensions. They also proposed another annotation to specify that a method may take a Raw receiver but which needs to have a specific field already initialized. This case is perfectly handled by our inference analysis with the value in TVal] stored in the thisPre field of method abstractions, but to keep the type system sufficiently simple, we did not formalize this extension of their type system. They also proposed to allow strengthened (co-variant) return types and helper initializers (inits methods), which are both included in the formalization herein presented. Gain in practice. To evaluate the practical gain of our analysis with respect to the type system of F¨ ahndrich and Leino, we count the number of program points that evaluate Je.f K] where ¬(JeK] = NotNull ∨ JeK] = Raw(X) with X  class(f )), to avoid the cases handled by the type system, and e = this ∧ T ] (f ) = Init where our analysis uses a more precise information than the type system, i.e., the inferred type instead of MayBeNull. In the Soot suite we have analyzed [VRCG+ 99] (see the next section), on 800 analyzed constructors, 391 program points have been found in 45 different constructors (of which 16 were part of the runtime). This is an under-approximation of the gain: it does not take in account the gain obtained in methods called from a constructor. We can see that the number of cases where the inference analysis is more precise than the type system is substantial.

3.8

Conclusions

We have defined a semantics-based analysis and inference technique for automatically inferring non-null annotations for fields. This extends and complements the seminal paper of F¨ahndrich and Leino in which is proposed an extended type system for verifying non-null type annotations. Other work has proposed inference techniques for such annotations, using either dataflow techniques or proof assistants, such as Houdini [FL01] and JastAdd [EH06]. F¨ahndrich and Leino’s approach mixes type system and data-flow analysis. In our work, we follow an abstract interpretation methodology to gain strong semantic foundations and a goal-directed inference mechanism to find a minimal (i.e., principal) non-null annotation. By the same token we also gained in precision thanks to a better communication between abstract domains. The analysis has been proven correct, the correctness proof has been machine-checked in the proof assistant Coq, and we also proved its completeness with respect to F¨ahndrich and Leino’s type system. The analysis has been implemented in the Nit tool. Although the implementation works directly at the Java bytecode level, annotations on fields and methods can be propagated at the source level with a minor effort. Nit is safe2 with respect to multi-threading and handles static fields, exceptions and arrays conservatively. The tool is further presented in Chapter 4.

2

Although this is not formally proven.

36

CHAPTER 3. NON-NULL REFERENCES AND FIELD INITIALIZATION

Chapter 4

A Non-Null Annotation Inferencer for Java Bytecode As seen in Chapter 3, null pointer exceptions in Java may result in additional branching which can make verification more difficult (bigger verification conditions, implicit flow in information flow verification, etc.), may disable some optimizations and oblige the JVM perform run-time checks for non-nullness of references when executing a number of its bytecode instructions, thereby incurring a performance penalty. We proposed in Chapter 3 a formal definition of a sound data-flow analysis to prove the absence of null pointer exceptions in (unannotated) Java bytecode programs. We now present the tool Nit (Nullability Inference Tool), an implementation resulting from our provably sound analysis. This analysis has been designed on a relatively high level language, well suited for the definition of the analysis and the proofs but too far from the target language of the implementation: the Java bytecode. Nit targets the Java bytecode for the reason detailed in the introduction: the bytecode evolves more slowly than Java source, the source may not be available and the bytecode contains less language constructions to handle. We herein describe the improvements we have brought to the former analysis and then present the implementations of both analyses and compare the results of the two implementations on practical benchmarks. Note: This work has been published in the ACM workshop Program Analysis for Software Tools and Engineering (PASTE) [Hub08].

4.1

Towards a Bytecode Analysis

This section proposes three modifications brought to the analysis described in Chapter 3 to improve its precision on the Java bytecode.

4.1.1

Alias Analysis

The Java bytecode is a stack language and includes some instructions to test variables for the null constant, such as ifnull n, which pops the top of the stack and jumps n bytes of instructions if the popped reference is null. From such an instruction, the analysis infers that, when the test fails, the popped value is non-null; but this information is useless to the analysis 37

38 CHAPTER 4. A NON-NULL ANNOTATION INFERENCER FOR JAVA BYTECODE load x ifnull n load x

x 7→ MayBeNull x 7→ Raw− x 7→ Raw− ...

Figure 4.1: Recovering Information from Tests S ∈ Meth × L * ((N * Var> ) × N)> ⊥ S(m, i) = (s, t) (s[t 7→ x], t + 1) ⊆ S(m, succ(i)) P ` (m, i) : load x S(m, i) = (s, t) ({(n, y) | (n, y) ∈ s ∧ y 6= x ∧ n ≤ t}, t − 1) ⊆ S(m, succ(i)) S ` (m, i) : store x

Figure 4.2: A Simple Data-Flow Analysis to Infer Equalities Between Local and Stack Variables. as this value cannot be reread. For the information to be exploitable, the analysis must know to which local variable the popped value was equal to, so the information can be propagated to this local variable, which may be re-read. We implemented a simple intra-procedural analysis to infer equalities between stack and local variables. For example, in Figure 4.1, assuming x is annotated as @Nullable at the beginning, our analysis allow inferring that the second load loads a non-null (@Raw) value. The analysis implemented is a simple data-flow analysis. An extract of its specification is given in Figure 4.2. The function S represents for each program point (m, i) ∈ Meth × L an abstraction (s, t) of the stack. s is a map from stack variables represented by their indices to local variable represented by their name in Var (note that the implementation uses integers for Var). t−1 represents the index of top of the stack. When loading a variable onto the stack, we associate the name of the loaded local variable with the top of the stack. When storing a stack variable into a local variable, the stack variables that were equal to the overwritten local variable are removed from S(m, i) and the top of the stack is popped.

4.1.2

A New Abstract Value

To present the new abstract value, we will rely on abstraction and concretization functions, usually called α and γ, respectively. They have been introduced with Abstract Interpretation by Cousot and Cousot [CC77]. An abstraction function α computes the smallest abstraction (for the order given by the abstract lattice) for a set of concrete values. A concretization function γ computes the set of all concrete values corresponding to an abstract value. We herein assume we have two functions α ∈ 2Val → Val] and γ ∈ Val] → 2Val . In Figure 4.1, assume that x is either non-null and fully initialized or null at the beginning of the example. The analysis abstracts such a value by NotNull t α({null}) = MayBeNull .

4.1. TOWARDS A BYTECODE ANALYSIS

39

The test allows recovering some information but, as MayBeNull also abstract raw references, the most that can be recovered is α(γ(MayBeNull) \ {null}) = Raw− . Such configurations often occur in real programs as implicitly initialized fields are always annotated with MayBeNull, despite they may never contain any raw value. To solve this, we add MayBeNullInit, a new abstract value that abstract values that may not point to raw objects. We have NotNull @ MayBeNullInit @ MayBeNull α(γ(MayBeNullInit) \ {null}) = NotNull . It allows annotating more references as @NonNull (instead of @Raw) and therefore to gain in precision as field annotations can then be trusted. It also allows a more direct gain in precision. A variable annotated as @NullableInit may not point to an object that is not fully initialized, so field annotations can also be trusted when reading fields of variables annotated with @NullableInit.

4.1.3

Analysis of instanceof Instructions

The Java bytecode includes the instanceof C instruction, which pushes 1 on the stack if the top of the stack is non-null and is an instance of the class C, otherwise it pushes 0. A conditional jump generally occurs few instructions after. Recovering information from such an instruction is not trivial: both the instanceof instruction and the jump are needed, they may be separated by some other instructions, and they interact in the concrete semantics through integer values. To be able to use this information, we have defined another analysis, which computes an abstraction of the stack such that, for each stack variable, it contains an under-approximation of the set of local variable that must be non-null if the corresponding stack variable is equal to 1. Figure 4.3 shows an example of Java bytecode with an instanceof instruction. Each line of the array gives an instruction and an extract of the abstract state before the following instruction. This example starts with an empty stack and a local variable x abstracted as MayBeNullInit. When this variable is loaded onto the stack, the alias analysis records that the stack variable 0 is equal to x. Then the instanceof instruction pops the top of the stack and pushes an integer that is either 1 or 0. At this point, if the top of the stack is 1, this means that the top of the stack before the instruction was non-null. As the alias analysis knows that the top of the stack was equal to x, this means that if the top of the stack is 1 then x is non-null. Then the ifeq0 instruction has 2 successors: either 5 + jmp (which is not represented) if the top of the stack is 0, or the next instruction (8). If the instruction at program point 8 is executed, this means that the top of the stack was different from 0. Furthermore, the InstanceOf analysis tells us that, at program point 5, the top of the stack is the result of an instanceof instruction on a stack variable which was equal to x. Therefore, if instruction at program point 8 is executed, this means that, before the instruction at program point 8 is executed, x is different from the null constant. It can therefore be refined from MayBeNullInit to NotNull. Finally, when a possibly null value is dereferenced, if the control flow reaches the next instruction it means that, at this point, the reference is non-null. Therefore, it is possible to refine all instructions that dereference variables so those variables are inferred as non-null on outgoing non-exceptional edges of the control flow graph.

40 CHAPTER 4. A NON-NULL ANNOTATION INFERENCER FOR JAVA BYTECODE

0: 2: 5: 8:

load x instanceof C ifeq0 jmp load x

Alias (∅, 0) ({0 7→ x}, 1) (∅, 1) (∅, 0) ({0 7→ x}, 1)

Instance Of (∅, 0) (∅, 1) ({0 7→ x}, 1) (∅, 0) (∅, 1)

Nullness x 7→ MayBeNullInit x 7→ MayBeNullInit x 7→ MayBeNullInit x 7→ NotNull x 7→ NotNull

Figure 4.3: Recovering Information From instanceof Instructions

4.2

Implementation

The global analysis is a whole program analysis composed of three analyses: the alias analysis, the analysis of instanceof instructions and the nullness analysis described in Chapter 3 with the addition of the new abstract value described in the previous section. The non-null analysis uses the results of both the alias and the instanceof analyses and the instanceof analysis uses the results of the alias analysis. Those communications between the analyses impose the simple scheduling of running first the alias analysis, then the instanceof analysis and in the end the non-null analysis. The three analyses have been implemented in a similar standard fashion. First we iterate over all instructions of the program to build transfer functions that take as argument a part of the abstract state and that return the parts of the abstract states that have been modified by the instructions. We store the functions in a hash-map with their dependencies as keys and we apply a work list algorithm to compute the fixpoint. The result of each analysis is the fixpoint computed. The global result contains, for each program point, three abstractions, one of which, the non-null abstraction, containing already a lot of information. Such an analysis cannot be implemented without taking care of memory consumption. The implementation is based on an early version of Javalib, the current version being presented in Section 5. We have put a lot of coding effort in reducing the memory consumption. We have implemented LocalVar] , TVal] and Heap] as balanced binary trees, which, as well as being efficient, have easily allowed us not to store bottom values (NotNull and Init). This is especially important as non-reference types are coded as bottom and most variables are non-null. Constraints are only computed for reachable methods, and only reachable methods are parsed thanks to a lazy parsing. We use sharing extensively and functional programming has greatly helped us herein. E.g., the stack is implemented as a list and, between two instructions, the part of the stack that is unchanged by the instruction is shared in memory and, to some extent, the same applies to maps. Using sharing has also improved efficiency: it is then possible to use physical equality tests instead of structural equality tests in some places. The result of the alias and instanceof analyses are compacted to remove the information for the instructions we know the results will not be used. E.g., for the result of the instanceof analysis, only the abstractions of stacks at conditional jumps (∼ 5% of the instructions) are kept. Despite the extra computation, our experiments showed that the extra computation were compensated by less memory stress and, in the end, improved both the memory consumption and the overall computation time.

4.3. THE NIT/ECLIPSE PLUG-IN

41

Figure 4.4: The Nit View with the Inferred Documentation

4.3

The Nit/Eclipse Plug-in

We developed a plug-in to be able to use Nit from the Integrated Development Environment (IDE) Eclipse. Nit is able to output the inferred annotations in an XML file. It can also output in this same file the program points where a variable of type MayBeNull or MayBeNullInit is dereferenced, i.e., where there may be a NullPointerException. The plug-in launches Nit on the project being developed under Eclipse when a compilation is requested and recovers this XML file. It parses the file and displays back the results in the Eclipse environment. Figure 4.4 shows an extract of a screenshot the interface of Eclipse with our plug-in. The panel on the left is for the configuration of the plug-in. The first part from the top is for the basic parameters like the project name (to recover where the files are located on the disk) or the main class (needed because it is a whole program analysis). The next two parts allow activating unsound assumptions. Giving a sound information for static fields is a difficult problem, as presented in Chapter 6. Nit therefore gives a sound but imprecise information by default. The same also applies to arrays. These options allow assuming, e.g., that all static fields and arrays only contain non-null references, or that references returned by native methods may not point to raw objects. To reduce even more the number of warnings displayed by the tool, it is even possible to assume that all methods return non-null values or are called with non-null arguments. This allows reducing the number of false positives (although the number of false negatives is also increased) and to focus on the warnings that are more likely to be actual bugs. The last part select which information may be displayed: information and warnings about nullness and initialization. The right part of the screenshot, with the code, shows an example of documentation. When hovering over a field, a method

42 CHAPTER 4. A NON-NULL ANNOTATION INFERENCER FOR JAVA BYTECODE

Figure 4.5: Nit Displays Warnings for Potential NullPointerExceptions argument or a method name, it displays information in a box over the source. It is the same box that is used by Eclipse to display JavaDoc documentation. It gives information about the initialization status of the objects that may referenced and about the nullness of the reference. Eclipse allows external tools to report warnings and errors. They are gathered and may be displayed by Eclipse in a separate panel (accessible by tab Problems) or directly in the source code: the part of the source responsible for the problem are underlined and a mark is placed in the margin. When hovering the mark, a box with the message associated to the error or warning is displayed. This feature is used, e.g., to report the errors reported by the compiler. We use the same feature to report potential NullPointerExceptions and accesses to raw objects. Figure 4.5 shows an example of a such warning. Nit/Eclipse has been developed with Nicolas Barr´e and demonstrated at JavaOne in June 2009, at the Inria booth. The feedback we obtained was encouraging, and people liked the information on raw objects. This part on raw object was not present at the beginning and Nit has been modified to report such information. This can be seen as a preliminary to the work object initialization presented in Chapter 7.

4.4

Empirical Results

The benchmarks include production applications such as the Java bytecode optimization framework Soot 2.2.4 [VRCG+ 99], the JDT Core Compiler of Eclipse 3.3.3 [Ecl], the static analysis framework of Spoto, Julia 1.4 [jul], the static checker based on theorem proving ESC/Java 2.0b4 [CK], and the rule engine and scripting environment Jess 7.1p1 [jes]. It also includes some other smaller applications such as the parser generator JavaCC [jav], the

4.4. EMPIRICAL RESULTS

43

Figure 4.6: Annotation Results: this graph shows, for fields, method parameters and method return values, the percentage of annotations that denotes non-null values (either Raw or NotNull )

Figure 4.7: Dereferencing Results: this graph shows the percentage of fields reads, field writes, method calls and array operations that our analysis proved safe

44 CHAPTER 4. A NON-NULL ANNOTATION INFERENCER FOR JAVA BYTECODE Java bytecode assembler Jasmin [MD97], the VNC client TightVNC Viewer [Tig] and the 10 programs constituting the SPEC JVM98 benchmarks [SPE]. The implementation used for those benchmarks is Nit 0.4, coded in OCaml 3.10.2 [LDG+ 07], and uses the Javalib 1.7 library, the predecessor of the Sawja library, presented in Chapter 5. We performed the whole-program analysis with the Java Runtime Environment of GNU gcj 3.4.6 [gcj07] on a MacBook Pro with a 2.4 GHz Intel Core 2 Duo processor with 4 GB of RAM. Figure 4.6 gives the percentage of non-null annotations (either @Raw or @NonNull) for fields, method parameters (except this which is always non-null) and method return values. The columns All software correspond to the average results on all software we tested. Despite only proving 49% of parameters non-null and the difficulties of proving a field non-null, our analysis proves almost 53% of fields non-null. (We do not count fields for which no reachable read have been found: this would otherwise give greater numbers but they would probably be less representative.) Globally, 51% of annotations are non-null annotations. Chalin and James experimentally confirmed [CJ07] that at least two thirds of annotations in Java programs in general are supposed to be non-null. With regard to their experiment, our results already represent a significant fraction. The purpose of annotations is to reduce the number of potential exceptions, so we instrumented our inferencer to count the number of dereferences that are proved safe with the inferred annotations. Figure 4.7 shows the percentage of safe dereferences over the total number of dereferences for field reads, field writes, method calls and array operations (load, store and length). In the studied benchmarks, our analysis proved 80% of dereferences safe. The main difficulty for our analysis lies in the manipulation of arrays (where only 57% of dereferences are proven safe). This could have been expected as the analysis has not been designed to be precise with arrays. This is left for future work. Nit includes switches to disable the modifications we proposed in this chapter that may interact with the precision. The precision then reflects more closely the analysis presented in Chapter 3. The results obtained with all the modifications disabled are shown in the figures as All software (Basic). The results in Figure 4.6 show that the modifications proposed allow improving a little the results for field and return values annotations (respectively 4 and 6 points), but much more for parameters (13 points, from 36% of non-null annotations to 49%). This is reflected in Figure 4.7, where, the percentage of safe dereferences grows from 69% with the original analysis to 80% with Nit. This is equivalent to say that from the 31% left unproved by the original analysis, Nit proved 35% of them correct. Finally, Tab. 4.1 gives the memory and CPU usage for the most expensive benchmarks and the sum and the maximum memory and CPU usage for the other benchmarks. Assuming enough processors and memory, the analyses can be run in parallel so the resources needed correspond to the sum of the memory usages but the maximum of CPU time, while if the analyses are run sequentially, the resources needed are the maximum memory usage but the sum of CPU time. For the 16 programs, the analysis use an average of 195 MB and 30 s, and the worst case is the analysis of Jess (804,111 bytecode instructions including libraries and excluding dead code), which needs 887 MB and 144 s. Our results indicate that the implementation scales.

4.5. RELATED WORK

Jess Soot ESC/Java Julia JDTCore JavaCC sum Others max

45 Program Size #Classes #Methods 460 2,745 3,441 26,371 713 6,939 1,098 10,469 1,380 17,201 140 995

Space (MB)

Time (s)

887 634 421 363 331 310 1,642 253

144 122 51 44 36 34 159 30

Table 4.1: CPU and Memory Usage: program sizes are the number of classes and methods distributed ; it does not include the runtime but may include full libraries where only part of it are used.

4.5

Related Work

We herein only discuss related work where the implementation was presented. Work related to the theoretical part of the nullness analysis is presented in Section 3.2. Papi et al. propose a framework [PAL+ 08] for Java source code annotations and, as an example, provide a checker for non-null annotations based on [FL03]. Ekman et al. propose a plug-in [EH07] for JastAdd [Ekm06] to infer and check the same non-null annotations. As their work predates the improvements we proposed in [HJP08a] and as we further improved the analysis in this paper, our analysis is strictly more precise. To infer type annotations, Houdini [FL01] generates a set of possible annotations (non-null annotations among others) for a given program and uses ESC/Java [LSS00] to refute false assumptions. CANAPA [CFJJ06] lowers the burden of annotating a program by propagating some non-null annotations. It also relies on ESC/Java to infer where annotations are needed. Those two annotation assistants have a simplified handling of objects under construction and rely on ESC/Java [LSS00], which is neither sound nor complete. Some other work has focused on local type inference, i.e., inferring nullness properties for small blocks of code like methods. One example hereof is the work of Male et al. [MPPD08]. Spoto proposed another nullness inference analysis [Spo08] with a different domain that expresses logical relations between nullness of variables. He compared his implementation with an old version of our tool Nit that did not include improvements on precision and performance we made. We herein included in the benchmarks some of the programs used in [Spo08] and we can notice that, despite the context sensitivity of the analysis in [Spo08], both analyses have very close practical results. FindBugs [HSP06, HP07] and Jlint [AH04] use static analyses to find null pointer bugs. To keep the false positive and false negative rates low they are neither sound nor complete.

4.6

Conclusion

We proposed an improved version of our provably sound inference analysis along with an efficient implementation. We compared the results of both analyses and showed that the improvements we proposed allow reducing by a third the number of dereferences that were not yet proven safe by our former analysis. The precision of the analysis is not yet sufficient to verify existing code without handwork afterwards, but it is still of interest for code documen-

46 CHAPTER 4. A NON-NULL ANNOTATION INFERENCER FOR JAVA BYTECODE tation, for reverse engineering and for improving the precision of control flow graphs, which is useful to native code compilers and other program verifications and static analyses. We also showed that, despite being a whole-program analysis, it is possible to infer annotations for production programs within minutes. To achieve such results, we put a lot of coding effort in the optimization of our tool. The tool has been released under the GNU General Public License and is available at http://nit.gforge.inria.fr.

Chapter 5

Sawja: Static Analysis Workshop for Java When developing Nit, presented in the previous chapter, we faced numerous issues that were time consuming. Developing an analysis for a realistic language such as Java is a major engineering task, either to build robust commercial tools or for the research scientists, like us, who want to quickly develop prototypes for demonstrating new analyses. The efficiency and the precision of any static analysis depend on the low-level components that manipulate the class hierarchy, the call graph, the intermediate representation (IR), etc. To develop Nit, we needed to implement these components, which were not specific to our nullness analysis. Nevertheless, if they are not carefully implemented, they may weaken the overlying analysis (in terms of efficiency or precision). We argue that such components should not be reimplemented in an ad hoc fashion for each new analyzer and that it is an integral part of automated software verification to address the issue of how to program a static analysis platform that is at the same time efficient, precise and generic, and that can facilitate the subsequent implementation of specific analyzers. Note: This work has been published in The International Conference on Formal Verification of Object-Oriented Software (FoVeOOS) [HBB+ 10]. The first version of Javalib has been initially developed by Nicolas Cannasse, the intermediate representation is a contribution of Delphine Demange and David Pichardie, the implementation of RTA is a contribution of Nicolas Barr´e, the factorization presented in Section 5.3 is a contribution of Tiphaine Turpin, and other contributors have helped with smaller developments, patches and discussions on ´ the library: Etienne Andr´e, Fr´ed´eric Besson, Florent Kirchner and Vincent Monfort. Despite those contributions, I am the main maintainer of the library and I have coded around 40% of the library.

5.1

Introduction

When developing Nit, we tried different approaches to efficiently implement our tool (sets and maps either based on balanced binary trees or based on Patricia trees, classes indexed by their name or by an integer using another level of indirection, etc.). From the experience we gained during this development, we developed the Sawja library—and enriched its sub-component Javalib—which provides OCaml modules for efficiently manipulating Java 47

48

CHAPTER 5. SAWJA: STATIC ANALYSIS WORKSHOP FOR JAVA

bytecode programs. Sawja, like Nit, is implemented in OCaml [LDG+ 07], a functional language whose automatic memory management (garbage collector), strong typing and pattern-matching facilities are particularly well suited for implementing program-processing tools. In particular, it has been successfully used for programming compilers (e.g., Esterel [PAM+ 09]) and static analyzers (e.g., Astr´ee [BCC+ 03]). Of course, choosing a programming language paradigm is also a matter of taste and the merit of Sawja is to fill a lack for those who want to develop their static analyses with OCaml without losing the features that other libraries may offer. The main contribution of the Sawja library is to provide, in a unified framework, several features that allow rapid prototyping of efficient static analyses while handling all the subtleties of the Java Virtual Machine (JVM) specification [LY99]. The main features of Sawja are: • parsing of .class files into OCaml structures and unparsing of those structures back into .class files; • decompilation of the bytecode into a high-level stack-less IR; • sharing of complex objects both for memory saving and efficiency purpose (structural equality becomes equivalent to pointer equality and indexation allows fast access to tables indexed by class, field or method signatures, etc.); • the determination of the set of classes constituting a complete program (using several algorithms, including Rapid Type Analysis (RTA) [BS96]); • a careful translation of many common definitions of the JVM specification, e.g., about the class hierarchy, field and method resolution and look-up, and intra- and interprocedural control flow graphs. This chapter describes the main features of Sawja and their experimental evaluation. Section 5.2 gives an overview of existing libraries for manipulating Java bytecode. Section 5.3 describes the representation of classes, Section 5.4 presents the intermediate representation of Sawja and Section 5.5 presents the parsing of complete programs.

5.2

Existing Libraries for Manipulating Java Bytecode

Several similar libraries have already been developed so far and some of them provide features similar to some of Sawja’s. All of them, except Barista, are written in Java. The Byte Code Engineering Library1 (BCEL) and ASM2 are open source Java libraries for generating, transforming and analyzing Java bytecode classes. These libraries can be used to manipulate classes at compile-time but also at run-time, e.g., for dynamic class generation and transformation. ASM is particularly optimized for this latter case: it provides a visitor pattern that makes possible local class transformations without even building an intermediate parse-tree. Those libraries are well adapted to instrument Java classes but lack important features essential for the design of static analyses. For instance, unlike Sawja, neither BCEL nor ASM propose a high-level intermediate representation (IR) of bytecode 1 2

http://jakarta.apache.org/bcel/ http://asm.ow2.org/

5.2. EXISTING LIBRARIES FOR MANIPULATING JAVA BYTECODE

49

instructions. Moreover, there is no support for building the class hierarchy and analyzing complete programs. The data structures of Javalib and Sawja are also optimized to manipulate large programs. ˜o Optimizing Compiler [BCF+ 99], which is now part of the Jikes RVM, The Jalapen relies on two IR (low and high-level IR) in order to optimize bytecode. The high-level IR is a 3-address code. It is generated using a symbolic evaluation technique described in [Wha99]. The algorithm we use to generate our IR is similar. Our algorithm works on a fixed number ˜o high-level IR lanof passes on the bytecode while their algorithm is iterative. The Jalapen guage provides explicit check instructions for common run-time exceptions (e.g., null check, bound check), so that they can be easily moved or eliminated by optimizations. We use similar explicit checks but to another end: static analyses definitely benefit from them as they ensure expressions are error-free. Soot [VRCG+ 99] is a Java bytecode optimization framework providing three IR: Baf, Jimple and Grimp. Optimizing Java bytecode consists in successively translating bytecode into Baf, Jimple, and Grimp, and then back to bytecode, while performing diverse optimizations on each IR. Baf is a fully typed, stack-based language. Jimple is a typed stack-less 3-address code and Grimp is a stack-less representation with tree expressions, obtained by collapsing Jimple instructions. The IR in Sawja and Soot are very similar but are obtained by different transformation techniques. They are experimentally compared in Section 5.4. Sawja only targets static analysis tools and does not propose inverse transformations from IR to bytecode. Several state-of-the-art control-flow analyses, based on points-to analyses, are available in Soot through Spark [LH03] and Paddle [LH08]. Such libraries represent a coding effort of several man-years. To this respect, Sawja is less mature and only proposes simple (but efficient) control-flow analyses. Wala [IBM] is a Java library dedicated to static analysis of Java bytecode. The framework is very complete and provides several modules like control flow analyses, slicing analyses, an inter-procedural dataflow solver and an IR in SSA form. Wala also includes a front-end for other languages like Java source and JavaScript. Wala and its IBM predecessor DOMO have been widely used in research prototypes. It is the product of the long experience of IBM in the area. Compared to it, Sawja is a more recent library with fewer components, especially in terms of static analyses examples. Nevertheless, the results presented in Section 5.5 show that Sawja loads programs faster and uses less memory than Wala. For the moment, no SSA IR is available in Sawja but this is foreseen for the future releases. Julia [Spo05] is a generic static analysis tool for Java bytecode based on the theory of abstract interpretation. It favors a particular style of static analysis specified with respect to denotational fixpoint semantics of Java bytecode. Initially free software, Julia is not available anymore. Barista [Cle] is an OCaml library used in the OCaml-Java project. It is designed to load, construct, manipulate and save Java class files. Barista also features a Java API to access the library directly from Java. There are two representations: a low-level representation, structurally equivalent to the class file format as defined by Oracle, and a higher level representation in which the constant pool indices are replaced by the actual data and the flags are replaced by enumerated types. Both representations are less factorized than in Javalib and, unlike Javalib, Barista does not encode the structural constraints into the OCaml structures. Moreover, it is mainly designed to manipulate single classes and does not offer the optimizations required to manipulate sets of classes (lazy parsing, hash-consing, etc).

50

5.3

CHAPTER 5. SAWJA: STATIC ANALYSIS WORKSHOP FOR JAVA

High-level Representation of Classes

Sawja is built on top of Javalib, a Java bytecode parser providing basic services for manipulating class files, i.e., an optimized high-level representation of class files, pretty printing and unparsing of class files.3 Javalib handles all aspects of class files, including stackmaps (J2ME and Java 6) and Java 5 annotation attributes. It is made of three modules: Javalib , JBasics , and JCode 4 . Representing class files constitutes the low-level part of a bytecode manipulation library. Our design choices are driven by a set of principles that are explained below. Strong typing. We use the OCaml type system to make explicit as much as possible the structural constraints of the class file format. For example, interfaces are only signaled by a flag in the Java class file format and this requires to check several consistency constraints between this flag and the content of the class (interface methods must be abstract, the superclass must be java.lang.Object, etc.). Our representation distinguishes classes and interfaces and these constraints are therefore expressed and enforced at the type level. This has two advantages. First, this lets the user concentrate on admissible class files, by reducing the burden of handling illegal cases. Second, for the generation (or transformation) of class files, this provides good support for creating correct class files. Factorization. Strong typing sometimes lacks flexibility and can lead to unwanted code duplication. An example is the use of several, distinct notions of types in class files at different places (JVM types, Java types, and JVM array types). We factorize common elements as much as possible, sometimes by a compromise on strong typing, and by relying on specific language features such as polymorphic variants5 . Figure 5.1 describes the hierarchy formed by these types. This factorization principle applies in particular to the representation of op-codes: many instructions exist whose names only differ in the JVM type of their operand, and variants exist for particular immediate values (e.g., iload, aload, aload n, etc.). In our representation they are grouped into families with the type given as a parameter (OpLoad of jvm_type * int). Lazy parsing. To minimize the memory footprint, method bodies are parsed on demand when their code is first accessed. This is almost transparent to the user thanks to the Lazy OCaml library but is important when dealing with very large programs. It follows that dead code (or method bodies not needed for a particular analysis) does not cause any time or space penalty. Hash-consing of the constant pool. For a Java class file, the constant pool is a table that gathers all sorts of data elements appearing in the class, such as Unicode strings, field and method signatures, and primitive values. Using the constant pool indices instead of actual 3 Javalib is a sub-component of Sawja, which, while being tightly integrated in Sawja, can also be used as an independent library. It was initiated by Nicolas Cannasse before 2004 but, since 2007, we have largely extended the library. We are the current maintainers of the library. 4 In the following, we use boxes around Javalib and Sawja module names to make clickable links to the on-line API documentation 5 Polymorphic variants are a particular notion of enumeration that allows the sharing of constructors between types.

5.3. HIGH-LEVEL REPRESENTATION OF CLASSES

51

jvm_return_type `Void|jvm_type jvm_type `Object|jvm_basic_type jvm_basic_type `Int2Bool|other_num

jvm_array_type `Int|`Short|`Char| `ByteBool|`Object| other_num

java_basic_type `Int|`Short|`Char| `Byte|`Bool|`Object| other_num

other_num `Long|`Float|`Double

Figure 5.1: Hierarchy of Java bytecode types. Links represent the sub-typing relation enforced by polymorphic variants (for example, the type jvm_type is defined by type jvm_type = [ |‘Object |jvm_basic_type ]). data reduces the class files size. This low-level aspect is abstracted away by the Javalib library, but the sharing is retained and actually strengthened by the use of hash-consing. Hash-consing [Ers58] is a general technique for ensuring maximal sharing of data-structures by storing all data in a hash table. It ensures unicity in memory of each piece of data and allows replacing structural equality tests by tests on pointers. In Javalib, it is used for constant pool items that are likely to occur in several class files, i.e., class names, and field and method signatures. Hash-consing is global: a class name like java.lang.Object is therefore shared across all the parsed class files. For Javalib, our experience shows that hash-consing is always a winning strategy; it reduces the memory footprint and is almost unnoticeable in terms of running time6 . We implement a variant that assigns hash-consed values a unique (integer) identifier. It enables optimized algorithms and data-structures. In particular, the Javalib API features sets and maps of hash-consed values based on Patricia trees [Mor68], which are a type of prefix tree. Patricia trees are an efficient purely functional data-structure for representing sets and maps of integers, i.e., identifiers of hash-consed values. They exhibit good sharing properties that make them very space efficient. Patricia trees have been proved very efficient for implementing flow-sensitive static analyses where sharing between different maps at different program points is crucial. On a very small benchmark computing the transitive closure of a call graph, the indexing makes the computation time four times smaller. Similar data-structures have been used with success in the Astr´ee analyzer [BCC+ 03]. Visualization. Sawja includes functions to print the content of a class into different formats. A first one is simply raw text, very close to the bytecode format as output by the javap command (provided with Oracle’s JDK). A second format is compatible with Jasmin [MD97], a Java bytecode assembler. This format can be used to generate incorrect class files (e.g., during a Java virtual machine testing), which are difficult to generate with our framework. The idea is then, using a simple text editor, to manually modify the Jasmin files output by Sawja and then to assemble them with Jasmin, which does not check classes for structural constraints. Finally, Sawja provides an HTML output. It allows displaying class files where the method code can be folded and unfolded simply by clicking next to the method name. It 6

The indexing time is compensated by a reduced stress on the garbage collector.

52

CHAPTER 5. SAWJA: STATIC ANALYSIS WORKSHOP FOR JAVA 0: if (x:I != 0) goto 8

0:

iload_1

1:

ifne!

4:

new!#2;//class B

7:

dup

2: notzero y:I

8:

iload_1

3: mayinit A

9:

iload_2

24

1: mayinit B

10: idiv

4: $irvar0 := new A()

11: new!#3;//class A

5: $irvar1 := new B(x:I/y:I,$irvar0:O)

14: dup 15: invokespecial #4;//Method A."":()V 18: invokespecial #5;//Method B."":(ILA;)V 21: goto!

25

24: aconst_null 25: areturn

6: $T0_25 := $irvar1:O 7: goto 9 8: $T0_25 := null 9: return $T0_25:O

Figure 5.2: Example of bytecode (left) and its corresponding IR (right) with colors to make explicit the boundaries of related code fragments also makes it possible to open the declaration of a method by clicking on its signature in a method call, and to know which method a method overrides, or by which methods a method is overridden, etc. User information can also be displayed along with the code, such as the result of a static analysis. From our experience, it allows a faster debugging of static analyses.

5.4

Intermediate Representation

The JVM is a stack-based virtual machine and the intensive use of the operand stack makes it difficult to adapt standard static analysis techniques that have been first designed for more classic variable-based codes. Hence, several bytecode optimization and analysis tools work on a bytecode intermediate representation (IR) that makes analyses simpler [BCF+ 99, VRCG+ 99]. Surprisingly, the semantic foundations of these transformations have received little attention. The transformation that is informally presented here has been formally studied and proved semantics-preserving in [DJP09]. This work is not my contribution, but as it has been integrated into Sawja and as I am the main maintainer of Sawja, I will overview the language. Figure 5.2 gives the bytecode and IR versions of the simple method B f ( int x , int y ) { return ( x ==0)?( new B ( x /y , new A ())): null ;}

The bytecode version reads as follows: the value of the first argument x is pushed on the stack at program point 0. At point 1, depending on whether x is zero or not, the control flow jumps to point 4 or 24 (in which case the value null is returned). At point 4, a new object of class B is allocated in the heap and its reference is pushed on top of the operand stack. Its address is then duplicated on the stack at point 7. Note the object is not initialized yet. Before the constructor of class B is called (at point 18), its arguments must be computed: lines 8 to 10 compute the division of x by y, lines 11 to 15 construct an object of class A. At point 18, the non-virtual method B is called, consuming the three top elements of the stack. The remaining reference of the B object is left on the top of the stack and represents from now on an initialized object.

5.5. COMPLETE PROGRAMS

53

The right side of Figure 5.2 illustrates the main features of the IR language.7 First, it is stack-less and manipulates structured expressions, where variables are annotated with types. For instance, at point 0, the branching instruction contains the expression x:I, where I denotes the type of Java integers. Another example of recovered structured expression is x:I/y:I (at point 5). Second, expressions are error-free thanks to explicit checks: for instance, the instruction notzero y:I at point 2 ensures that evaluating x:I/y:I will not raise any error. Explicit checks additionally guarantee that the order in which exceptions are raised in the bytecode is preserved in the IR. Next, the object creation process is syntactically simpler in the IR because the two distinct phases of (i) allocation and (ii) constructor call are merged by folding them into a single IR instruction (see point 4). In order to simplify the design of static analyses on the IR, we forbid side effects in expressions. Hence, the nested object creation at source level is decomposed into two intermediate assignments ($irvar0 and $irvar1 are temporary variables introduced by the transformation). Notice that because of side-effect free expressions, the order in which the A and B objects are allocated must be reversed. Still, the IR code is able to preserve the class initialization order using the dedicated instruction mayinit that calls the static class initializer whenever it is required. Another important feature, which is not demonstrated by the example, is that Java subroutines (bytecodes jsr/ret) are inlined. Subroutines have been pointed out by the research community as raising major static analysis difficulties [SA98] and are less and less generated by modern Java compilers. Our restricted inlining algorithm cannot handle nested subroutines but it is sufficient to inline all subroutines from the Oracle’s Java 7 runtime. The efficiency of the implementation has been experimentally validated by Demange and Pichardie [HBB+ 10]. It performs more than 10 times faster than Soot, even when Soot does not try to optimize the code it produces (otherwise Soot is even slower). Furthermore, the number of local variables introduced by the intermediate representation is kept manageable and comparable to Soot when Soot uses other analyses to reduce their number.

5.5

Complete Programs

We use the term complete program for the code (classes, methods or opcodes) that is accessible from the entry points of the program. When analyzing a piece of code, e.g., a method or a class, static analyses often need information about its inputs (including execution context). Some analyses rely on annotations provided by the user (e.g., types, invariants, pre- or postconditions) but this information can also be inferred from the calling contexts, recursively. Having the complete program allows taking in account all potential calling contexts and therefore to give a safe over-approximation of the input of the piece of code to analyze. Sawja proposes a representation of complete programs with an API for navigating their control-flow graph and includes several control-flow analyses to construct preliminary complete programs.

5.5.1

API of Complete Programs

Sawja represents a complete program by a record. The field classes maps a class name to a class node in the class hierarchy. The class hierarchy is such that any class referenced in the program is present. The field parsed_methods maps a fully qualified method name 7

For a complete description of the IR language syntax, please refer to the API documentation of the JBir

module. A 3-address representation called A3Bir is also available where each expression is of height at most 1.

54

CHAPTER 5. SAWJA: STATIC ANALYSIS WORKSHOP FOR JAVA

to the class node declaring the method and the implementation of the method. The field static_lookup_method returns the set of target methods of a given field. As it is computed statically, the target methods are an over-approximation. The API allows navigating the intra-procedural graph of a method taking into account jumps, conditionals and exceptions. Although conceptually simple, field and method resolution and the different method look-up algorithms (corresponding to the instructions invokespecial, invokestatic, invokevirtual, invokeinterface) are critical for the soundness of inter-procedural static analyses. In Sawja, great care has been taken to ensure an implementation fully compliant with the JVM specification.

5.5.2

Construction of Complete Programs

Computing the exact control-flow graph of a Java program is not computable in general and computing a precise (over-)approximation of it is still computationally challenging. It is a field of active research (see for instance [LH08, BS09]). Several possibilities exist to compute an over-approximation of a complete program. A first solution is to consider the whole code available, but one should note that the more precise the complete program is, the more precise the result of the overlying analysis will be. Computing a more precise over-approximation of a complete program is not trivial, especially because of callbacks: it makes data-flow and control-flow interdependent. In the following example, to prove that CG1.toString() is not part of the program, we need to prove that CG1.F cannot point to a sub-type of CG1. As CG2.m2() sets CG1.F to an object of type CG1, we also need to prove that CG2.m2() is unreachable, etc. 1 2 3 4 5 6 7 8 9 10

class CG2 { static void m (){ CG1 . F . toString ();} static void m2 (){ CG1 . F = new CG1 ();}} class CG1 { static Object F ; public String toString (){ throw new RuntimeException ();} public static void main ( String [] args ) { CG1 . F = new Object (); CG2 . m (); System . out . println ( " OK " );}}

A complete program is usually computed by: (1) initializing the set of reachable code to the entry points of the program, (2) computing the new call graph, and (3) if a (new) edge of the call graph points to a new node, adding the node to the set of reachable code and repeating from step (2). The set of code obtained when this iteration stops is an over-approximation of the complete program. Computing the call graph is done by resolving all reachable method calls. Here, we use the functions provided in the Sawja API presented in Section 5.5.1. While invokespecial and invokestatic instructions do not depend on the data of the program, the function used to compute the result of invokevirtual and invokeinterface needs to be given the set of object types on which the virtual method may be called. The analysis needs to have an overapproximation of the types (classes) of the objects that may be referenced by the variable on which the method is invoked. There exists a rich hierarchy of control-flow analyses trading time for precision [TP00, GC01]. Sawja implements the fastest and most cost-effective control-flow analyses, namely

5.5. COMPLETE PROGRAMS

55

Rapid Type Analysis (RTA) [BS96], XTA [TP00] and Class Reachability Analysis (CRA), a variant of Class Hierarchy Analysis [DGC95]. Soundness. Our implementation is subject to the usual caveats with respect to reflection and native methods. As these methods are not written in Java, their code is not available for analysis and their control-flow graph cannot be safely abstracted. Note that our analyses are always correct for programs that use neither native methods nor reflection. Moreover, to alleviate the problem, our RTA implementation can be parameterized by a user-provided abstraction of native methods specifying the classes it may instantiate and the methods it may call. A better account of reflection would require an inter-procedural string analysis [LWL05] that is currently not implemented. Implemented Class Analyses CRA. This algorithm computes the complete program without actually computing the call graph or resolving methods: it considers a class as accessible if it is referenced in another class of the program, and considers all methods in reachable classes as also reachable. When a class references another class, the first one contains in its constant pool the name of the latter one. Combining the lazy parsing of our library with the use of the constant pool allows quickly returning a complete program without even parsing the content of the methods. When an actual method resolution or a call graph is needed, the Class Hierarchy Analysis (CHA) [DGC95] is used. Although parts of the program returned by CRA will be parsed during the overlying analysis, dead code will never by parsed. To our knowledge, CRA is a new algorithm, cheaper than CHA in practice while keeping the same precision. RTA. An object is abstracted by its class and all program variables by the single set of the classes that may have been instantiated, i.e., this set abstracts all the objects accessible in the program. When a virtual call needs to be resolved, this set is taken as an approximation of the set of objects that may be referenced by the variable on which the method is called. This set grows as the set of reachable methods grows. Sawja’s implementation of RTA is highly optimized. While static analyses are often implemented in two steps (a first step in which constraints are built, and a second step for computing a fixpoint), here, the program is unknown at the beginning and constraints are added on the fly. For a faster resolution, we cache all reachable virtual method calls, the result of their resolution and intermediate results. When needed, these caches are updated at every computation step. The cached results of method resolutions can then be reused afterwards, when analyzing the program. XTA. As in RTA, an object is abstracted by its class and to every method and field is attached a set of classes representing the set of objects that may be accessible from the method or field. An object is accessible from a method if: (i) it is accessible from its caller and it is of a sub-type of a parameter, or (ii) it is accessible from a static field that is read by the method, (iii) it is accessible from an instance field that is read by the method and there is an object of a sub-type of the class in which the instance field is declared that is already accessible, or (iv) it is returned by a method that may be called from the current method. To facilitate the implementation, we built this analysis on top of another analysis to refine a previously computed complete program. This allows us using the aforementioned standard

56

CHAPTER 5. SAWJA: STATIC ANALYSIS WORKSHOP FOR JAVA

technique (build then solve constraints). For the implementation, we need to represent many class sets. As classes are indexed, these sets can be implemented as sets of integers. We need to compute fast union and intersection of sets and we rarely look for a class in a set. For those reasons, the implementation of sets available in the standard library in OCaml, based on balanced trees, was not well adapted. Instead we used a purely functional set representation based on Patricia trees [Mor68], and another based on BDDs [Bry92] (using the external library BuDDy available at http://buddy.sourceforge.net). Our current implementation is incredibly faster than our first implementation. Our first implementation could not complete the analysis of Soot within several hours; our current implementation needs about 3 minutes. To improve our first implementation, we use profiling to detect some hot spots (parts of the code that are the most time consuming). Among the operations we optimized is the refinement of a set of instances I given a list of types T . Our first algorithm was to compute for each type t, the set of instances of I that were extending t and then to compute the union of those sets. This was very expensive. Then, our next algorithm was to test, for each instance i of I, if there was a type t in T such that i extends t, and if so to add i in the result set. This improved the performance. By adding a test at the beginning to first check that T is not empty we gained even more. But this operation was not run given random arguments: although I was the current set of objects of a method, and was therefore different at each application of the constraint, T corresponds to the types of the arguments of a method signature, which are common to all applications of the same constraints. We therefore improved again our algorithm by computing on the first application of the constraint the union T 0 of the cones defined by T , i.e., the set of all classes extending a type t of T . Then, the refinement of I is simply the intersection of I with T 0 , which is efficiently computed using BDDs or Patricia trees. There has been loads of other optimizations: lazy computations of some data, sharing of other data between constraints, improving our Patricia tree library on some specific inputs, removing the polymorphism of some functions to allow low level optimizations by the compiler, replacing some structural equalities by physical equalities when possible, using dynamic programming for the BDD traversal algorithm, improving the selection of the next constraint to apply in our constraint solver, etc.

Experimental Evaluation We evaluate the precision and efficiency of the class analyses implemented in Sawja on several pieces of Java software8 and present our results in Table 5.1. We compared the precision of the 3 algorithms used to compute complete programs (CRA, RTA and XTA) with respect to the number of reachable methods in the call graph and its number of edges. We also give the number of classes loaded by CRA and RTA. We provide some results obtained with Wala (version r3767 from the repository). Although precision is hard to compare9 , it indicates that, on average, Sawja uses half the memory and time used by Wala per reachable method with RTA. 8

Soot (2.3.0), Jess (7.1p1), JML (5.5), TightVNC Java Viewer (1.3.9), ESC/Java (2.0b0), Eclipse JDT Core (3.3.0) and Javacc (4.0). 9 Because both tools are unsound, a greater number of methods in the call graph either mean there is a precision loss or that native methods are better handled.

5.6. CONCLUSION

C

M

E

T

S

CRA RTA CRA W-RTA RTA XTA W-0CFA CRA W-RTA RTA XTA W-0CFA CRA W-RTA RTA XTA W-0CFA CRA W-RTA RTA XTA W-0CFA

Soot 5,198 4,116 49,810 32,652 32,800 14,251 37,768 2,159,590 2,788,533 1,400,958 297,754 856,180 8 74 13 187 2,303 87 248 132 810 708

57 Jess 5,576 2,222 47,122 4,303 12,561 10,043 9,927 799,081 78,444 141,910 94,189 183,191 8 7 4 18 209 83 44 60 198 238

Jml 2,943 1,641 26,906 17,740 11,697 9,408 15,414 418,951 614,216 149,209 103,126 187,177 4 23 4 16 40 51 128 54 184 215

VNC 5,192 1,736 44,678 ? 9,218 6,534 ? 694,451 ? 79,029 48,817 ? 7 ? 3 11 ? 80 ? 51 153 ?

ESC/Java 2,656 1,388 23,229 9,560 8,305 7,039 9,088 354,234 279,232 101,257 74,007 87,163 4 12 3 10 27 45 84 43 147 132

JDTCore 2,455 1,163 23,579 7,378 9,137 8,186 6,830 347,388 146,119 114,454 86,794 77,875 5 12 4 14 26 47 101 52 157 134

Javacc 2,172 792 19,389 3,247 4,029 3,250 3,009 258,674 34,192 35,727 26,844 21,475 4 7 2 5 16 36 42 26 112 125

Table 5.1: Comparison of algorithms generating a program call graph (with Sawja and Wala): the algorithms of Sawja (CRA, RTA and XTA) are compared to Wala (W-RTA and W-0CFA) with respect to the number of loaded classes (C), reachable methods (M) and number of edges (E) in the call graph, their execution time (T) in seconds and memory used (S) in megabytes. Question marks (?) indicate clearly invalid results.

5.6

Conclusion

We have presented the Sawja library, the first OCaml library providing state-of-the-art components for writing Java static analyzers in OCaml. The library represents an effort of 1.5 man-year and approximately 22,000 lines of OCaml (including comments) of which 4,500 are for the interfaces. Many design choices are based on our earlier work with the Nit analyzer. Using our experience from the Nit development, we designed Sawja as a generic framework to allow every new static analysis prototype to share the same efficient components as Nit. Indeed, Sawja has already been used in two implementations for the ANSSI (The French Network and Information Security Agency) [JP10, HJMP10] and Nit has been ported to the current version of Sawja, improving its performances by 30% in our first tests. The classes analyses presented in Section 5.5.2 rely on the underlying features and can be seen as use cases of Sawja. We made Sawja available under he GNU Lesser General Public License at http://sawja.inria.fr/.

58

CHAPTER 5. SAWJA: STATIC ANALYSIS WORKSHOP FOR JAVA

Chapter 6

Static Initialization Program analyses often rely on the data manipulated by programs and can therefore depend on their static fields. Unlike instance fields, static fields are unique to each class and one would like to benefit from this uniqueness to infer precise information about their content. Note: The work presented in sections 6.2, 6.3, 6.4 and 6.7 is a joint work with David Pichardie and has been published in the international workshop on Bytecode Semantics, Verification, Analysis and Transformation (Bytecode) [HP09].

6.1

Introduction

When reading a variable, be it a local variable or a field, being sure it has been initialized beforehand is a nice property. Although the Java bytecode (and the BCV) ensures this property for local variables, it is not ensured for static and instance fields, which have default values. E.g., this implies that writing only non-null values to a field does not ensure that null will never be read from that same field. Initialization of instance fields has been studied in Chapter 3, but instance fields and static fields are not initialized the same way. Instance fields are usually initialized in constructors, which are explicitly called, whereas static fields are initialized in class initializers, which are implicitly and lazily invoked. This makes the control flow graph much less intuitive: it does not depend on the syntax (unlike, e.g., static method calls) nor on the data (unlike, e.g., virtual method calls) but on the history of classes that have been initialized. The contributions of this work are the followings. • We recall that implicit lazy static field initialization makes the control flow graph hard to compute. • We identify some code examples that would need to be ruled out and some other examples that would need not to be ruled out. • We propose a language to study the initialization of classes and static fields. • We propose a formal analysis to infer an under-approximation of the set of static fields that have already been initialized for each program point. • We show the weaknesses of this analysis and propose another more precise analysis. 59

60

CHAPTER 6. STATIC INITIALIZATION

The rest of this chapter is organized as follows. We recall in Section 6.2 that the actual control flow graph, which includes the calls to the class initializers, is not intuitive and give some examples. We present in Section 6.3 the syntax and semantics of the language we have chosen to formalize our analysis. Section 6.4 then presents the first analysis, first giving an informal description and then its formal definition. We show in Section 6.5 that this analysis is not precise enough to be practicable and propose another more precise analysis. Section 6.6 then details how the analysis is extended to handle other features of the Java bytecode language and how to scale the implementation. Finally, we discuss the related work in Section 6.7 and conclude in Section 6.8.

6.2

Why Static Analysis of Static Fields is Difficult?

The analysis we herein present works at the bytecode level but, for sake of simplicity, code examples are given in Java. As this chapter is focused on static fields, all fields are assumed to be static unless otherwise stated. In Java, a field declaration may include its initial value, such as A.f in Figure 6.1: it is called in-line initialization. A field can also be initialized in a special method called a class initializer, which is identified in the Java source code with the static keyword followed by no signature and a method body, such as in class B in the same figure. If a field is initialized with a compile-time constant expression, the compiler from Java to bytecode may translate the initialization into a field initializer (cf. [LY99], Section 4.7.2), which is an attribute of the field. At run time, the field should be set to this value before running the class initializer. In-line initializations that have not been compiled as field initializers are prepended in textual order to the class initializer, named at the bytecode level. For this analysis, we do not consider field initializers but focus on class initializers as they introduce the main challenges. Although this simplification is sound, it is less precise and we explain how to extend our analysis to handle field initializers in Section 6.6.1. The class initialization process is not explicitly handled by the user: it is forbidden to explicitly call a method. Instead, every access (read or write) to a field of a particular class, static method call or the creation of an instance of that same class requires that the JVM has invoked the class initializer of that class (but it may have not yet terminate). In particular, if the class is being initialized, then the class initializer is not invoked and static fields may be accessed before they have been initialized. Class initializers can contain arbitrary code and may trigger the initialization of other classes and so on. The JVM specification [LY99] requires class initializers to be invoked lazily. This implies that the order in which classes are initialized depends on the execution path. It is therefore not decidable in general. The JVM specification also requires class initializers to be invoked at most once. This avoids infinite recursions in the case of circular dependencies between classes, but it also implies that when reading a field it may not yet contain its “initial” value. For example, in Figure 6.1, the class initializer of A creates an instance of B and therefore requires that the class initializer of B has been invoked. The class initializer of B reads a field of A and therefore requires that the class initializer of A has been invoked. • If B. is invoked before A., then the read access to the field A.f triggers the invocation of A.. Then, as B. has already been invoked, A. carries on normally and creates an instance of class B, store its reference to

6.2. WHY STATIC ANALYSIS OF STATIC FIELDS IS DIFFICULT?

1 2 3 4

class A extends Object { static B f = new B ();} class B extends Object { static B g ; static { g = A.f;

5

}

6 7

61

}

Figure 6.1: Initial values can depend on foreign code: in this example, the main program should first use B for the initialization to start from B to avoid B.g being null

2

class A extends Object { public static int CST = B . SIZE ;

3

}

1

5

class B extends Object { public static int SIZE = A . CST +5;

6

}

4

Figure 6.2: Integer initial values can also depend on foreign code

the field A.f and returns. Back in B., the field A.f is read and the reference to the new object is also affected to B.g.

• If A. is invoked before B., then before allocating a new instance of B, the JVM has to initialize the class B by calling B.. In B., the read access to A.f does not trigger the initializer of A because A. has already been started. B. then reads A.f, which has not been initialized yet. B.g is therefore set to the default value of A.f, which is the null constant.

This example shows that the order in which classes are initialized modifies the semantics. The issue shown in Figure 6.1 is not limited to reference fields. In the example in Figure 6.2, depending on the initialization order, A.CST will be either 0 or 5, while B.SIZE will always be 5. One could notice that those problems are related to the notion of circular dependencies between classes and may think that circular dependencies should be avoided. Figure 6.3 shows an example with a single class. In (a), A.ALL is read in the constructor before it has been initialized and it leads to a NullPointerException. (b) is the correct version, where the initializations of ALL and EMPTY have been switched. This example is an extract of java.lang.Character$UnicodeBlock of Oracle’s Java Runtime Environment (JRE) that we have simplified: we want the analysis to handle such cases. If the analysis considers that A depends on itself, then the analysis forbids way too much programs. If it does not consider that A depends on itself and it does not reject (a) then the analysis is incorrect. Hence, an analysis cannot rely on circular dependencies between classes.

62

1 2 3 4

CHAPTER 6. STATIC INITIALIZATION

class A extends Object { static A EMPTY = new A ( " " ); static HashMap ALL = new HashMap ();

1 2 3 4

5

public A ( String name ){ this . name = name ; ALL . add ( name , this );

7 8 9

}

10 11

5

String name ;

6

class A extends Object { static HashMap ALL = new HashMap (); static A EMPTY = new A ( " " ); String name ;

6

public A ( String name ){ this . name = name ; ALL . add ( name , this );

7 8 9

}

10

}

11

(a) An uninitialized field read leads to a NullPointerException

} (b) No uninitialized field is read

Figure 6.3: The issue can arise with a single class

6.3

The Language

In this section we present the program model we consider in this chapter, which is not based on the language presented in Chapter 2. Indeed, to shorten the subsequent description of the analysis, we preferred a higher level description of bytecode programs focused on the specific problem of static field initialization.

6.3.1

Syntax

We assume a set P of program points, a set F of field names, a set C of class names and a set M of method names. For each method m, we write m.first the first program point of the method m. For convenience, we associate to each method a distinct program point m.last that models the output point of the method. For each class C, we write C. the name of the class initializer of C. We only consider five kinds of instructions. • put(f ) updates a field f ∈ F. • invoke calls a method (we do not mention the name of the target method because it would be redundant with the flow inter information described below). • initialize(C) calls the class initializer of C if it has not been called yet. • return returns from a method. • any models any other intra-procedural instruction that does not affect static fields. We made explicit the program points where class initialization can occur by adding a special initialize instruction, which is implicit in Java bytecode. As the semantics presented below will demonstrate, any instruction of the standard sequential Java bytecode can be represented by these instructions, namely, one of {put(f ), invoke, return, any}, possibly preceded by an initialize(C) instruction. E.g., a new C in Java bytecode would be translated in two of these instructions: first initialize(C) to make explicit the requirements that before creating an instance of a class, this class needs to have been initialized, and any as instance creation cannot be modeled by the other instructions. In practice, an initialize(C) instruction will

6.3. THE LANGUAGE

63

be inserted for each bytecode instruction of the form new C, putstatic C.f , getstatic C.f or invokestatic C.m (see Section 2.17.4 of [LY99]).1 The program model we consider is based on control flow relations that must have been computed by some standard control flow analysis. Definition 6.1 A program is a 4-tuple (m0 , instr , flow intra , flow inter ) where: • m0 ∈ M is the method where the program execution starts; • instr ∈ P * {put(f ), initialize, invoke, return, any} is a partial function that associates program points to instructions; • flow intra ⊆ P × P is the set of intra-procedural edges; • flow inter ⊆ P × M is the set of inter-procedural edges, which capture dynamic method calls; and such that instr and flow intra satisfy the following property: For any method m, for any program point l ∈ P that is reachable from m.first in the intra-procedural graph given by flow intra , and such that instr (l) = return, (l, m.last) belongs to flow intra . Figure 6.4 presents an example of program with its two control flow relations. In this program, the main method m0 contains two distinct paths that lead to the call of a method m. In the first one, the class A is initialized first and its initializer triggers the initialization of B. In the second path, A is not initialized but B is. The corresponding Java code would look a bit awkward because this example is built to be short while being sufficiently rich to show how the analysis deals with such features.

6.3.2

Semantics

The analysis we consider does not take into account the content of the heaps, the local variables or the operand stacks. To simplify the presentation, we hence choose an abstract semantics for our language that is a conservative abstraction of the standard concrete semantics and does not explicitly manipulate these domains. The domains the semantics manipulates are presented in Figure 6.5. The domain of the values is left abstract. A field contains either a value or a default value represented by the symbol Ω. Since a class initializer cannot be called twice in a same execution, we need to remember the set of classes whose initialization has been started (but not necessarily ended). This is the purpose of the element h ∈ History. Our language is given a small-step operational semantics with states of the form , where the label l uniquely identifies the current program point, cs is a call stack that keeps track of the program points where method calls have occurred, s associates to each field its value or Ω and h is the history of class initializer calls. 1

To be completely correct we also need to add an edge from the beginning of the static void main method to the initializer of its class. We can also correctly handle superclass and interface initialization without modification of the current formalization (cf. Sect 6.6.1). Some classes may also be initialized by the virtual machine before the initialization of the main class (such as java.lang.System, java.lang.String, java.lang.Exception, etc.), but those initializations are implementation dependent.

64

CHAPTER 6. STATIC INITIALIZATION

l 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

flow intra flow inter

instr (l) any initialize(A) invoke initialize(B) invoke any initialize(B) put(f ) initialize(A) return

m0

l first l last A.

0

6

1

3

2

4

7

9

B.

11

5

12 m

13

return

8

14

.. .

return

(a) Program Instructions

(b) Control Flow Graphs

Figure 6.4: A Program Example

v s h

∈ ∈ ∈ ∈

Value Static History State

(abstract) = F → Value + {Ω} def = P(C) def = P × (P? ) × Static × History def

Figure 6.5: Semantic Domains

10

6.4. A MUST-HAVE-BEEN-INITIALIZED DATAFLOW ANALYSIS

65

instr (l) = initialize(C) C 6∈ h → instr (l) = initialize(C) flow intra (l, l0 ) C ∈ h → instr (l) = put(f ) flow intra (l, l0 ) v ∈ Value → instr (l) = any flow intra (l, l0 ) → instr (l) = invoke flow inter (l, m) → instr (l) = return flow intra (l0 , l00 ) → Figure 6.6: Operational semantics The small-step relation → ⊆ State × State is given in Figure 6.6 (we left implicit the program (m0 , instr , flow intra , flow inter ) that we consider). The relation → is defined by six rules, two for the initialize instruction and one for each other instruction. In the first one, the class initializer of class C needs to be called. We hence jump to the first point of C., push on the call stack the previous program point and record C in the history h. In the second rule, there is no need to initialize a class; hence we simply jump to a next program point (given by flow intra ). The third one corresponds to a field update put(f ): an arbitrary value v is stored in field f . The fourth rule illustrates that the instruction any does not affect the visible elements of the state. For a method call (fifth rule), the current program point is pushed on the call stack and the control is transferred to one of the targets of the inter-procedural edges. At last, the instruction return simply jumps to the intra-procedural successor of the caller. We end this section with the formal definition of the set of reachable states during a program execution. An execution starts in the main method m0 with an empty call stack, an empty historic and with all fields associated to the default value Ω. Definition 6.2 (Reachable States) The set of reachable states of a program p such that p = (m0 , instr , flow intra , flow inter ) is defined by JpK = { | →? }

6.4

A Must-Have-Been-Initialized Dataflow Analysis

In this section we present a sound dataflow analysis that, assuming precise enough control flow relations (flow intra and flow inter ), allows proving a static field has already been initialized at a particular program point. Next section will show that this analysis is not precise enough, but nevertheless, this analysis has the advantages of being simpler than the following one,

66

CHAPTER 6. STATIC INITIALIZATION

1 2 3 4

class A { static int f = 1;} class B { static int g = A . f ;} class C { public static void main ( String [] args ){ ... = B . g ; ... = A . f ; ... = B . g ;

5 6 7

}

8 9

}

Figure 6.7: Motivating the Must set to use the same formalism but to present a different abstraction. We first give an informal presentation of the analysis, then present its formal definition and we finish with the statement of a soundness theorem.

6.4.1

Informal Presentation

For each program point we want to know as precisely as possible, which fields must have been initialized. Since static fields are generally not initialized in the method where they are read, we need an inter-procedural analysis to compute method summaries. A summary should contain the set of fields Wf that must have been initialized at the end of the method. Hence at each program point l where a method is called, be it a class initializer or another method, we will use this summary of the method to be called to improve our local knowledge about initialized fields. However, in the case of a call to a class initializer, we need to be sure the class initializer will be effectively executed if we want to use its summary. Indeed, at execution time, when we reach a point l with an initialize(C) instruction, two exclusive cases may happen: 1. C. has not been called yet: C. is immediately called. 2. C. has already been called (and may still be in progress): C. will not be called a second time. Using the initialization information given by C. is safe only in case (i). To detect case (i), we keep track in a flow-sensitive manner of the class initializer that may have been called during all execution reaching a given program point. We denote by May this set. Here, if C is not in May, we are sure to be in case (i). May is computed by gathering, in a flow sensitive way, all classes that may be initialized starting from the main method. Implicit calls to class initializer need to be taken in account, but the smaller May is, the better. For simplicity’s sake we consider in this section a context-insensitive analysis where for each method, all its calling contexts are merged at its entry point. In Section 6.5, we will propose a context-sensitive analysis. Consider the Java program example given in Figure 6.7. Before line 5, May only contains C, the class of the main method. There is an implicit flow from line 5 to the class initializer of B. At the beginning of the class initializer of B, May equals to {B, C}. We compute the set of fields initialized by A., which is {A.f}. As A is not in May at the beginning of B., we can assume the class initializer will be fully executed before the actual read to A.f occurs, so it is a safe read. However, when we

6.4. A MUST-HAVE-BEEN-INITIALIZED DATAFLOW ANALYSIS

67

carry on line 7, the May set contains A, B and C. If we flow this information to B., then the merged calling context of B. is now {A, B, C}, which makes impossible to assume anymore that A. is called at line 2. To avoid such an imprecision, we need to propagate as few calling context as possible to class initializers by computing in a flowsensitive manner a second set of class whose initializer must have already been called in all execution reaching a given program point. We denote by Must this set. Each time we encounter an initialization edge for a class C, we add C to Must since C. is either called at this point, or has already been called before. If C ∈ Must before an initialization edge for C, we are sure C. will not be called at this point and we can avoid propagating a useless calling context to C.. To sum up, our analysis manipulates, in a flow sensitive manner, three sets May, Must and Wf . May and Must correspond to a control flow analysis that allows refining the initialization graph given by the initialize(C) instructions. The more precise control flow graph allows a finer tracking of field initialization and therefore a more precise Wf .

6.4.2

Formal Specification

In this part we consider a given program p = (m0 , instr , flow intra , flow inter ). We write Cp , respectively Fp , the finite set of classes, respectively fields, that appears in p. For each program point l ∈ P, we compute before and after the current point three sets of data (May, Must, Wf ) ∈ P(Cp ) × P(Cp ) × P(Fp ) where P(A) denotes the power set of A. Since May is a may information, and Must and Wf are must information, the underlying lattice of the dataflow analysis is given by the following definition. Definition 6.3 (Analysis lattice) The analysis lattice is (A] , v, t, u, ⊥, >) where: • A] = P(Cp ) × P(Cp ) × P(Fp ). • ⊥ = (∅, Cp , Fp ). • > = (Cp , ∅, ∅). • for all (May 1 , Must 1 , Wf 1 ) and (May 2 , Must 2 , Wf 2 ) in A] , (May 1 , Must 1 , Wf 1 ) v (May 2 , Must 2 , Wf 2 ) iff May 1 ⊆ May 2 , Must 1 ⊇ Must 2 and Wf 1 ⊇ Wf 2 (May 1 , Must 1 , Wf 1 ) t (May 2 , Must 2 , Wf 2 ) = (May 1 ∪ May 2 , Must 1 ∩ Must 2 , Wf 1 ∩ Wf 2 ) (May 1 , Must 1 , Wf 1 ) u (May 2 , Must 2 , Wf 2 ) = (May 1 ∩ May 2 , Must 1 ∪ Must 2 , Wf 1 ∪ Wf 2 ) Each element in A] expresses properties on fields and on an initialization historic. This is formalized by the following correctness relation. Definition 6.4 (Correctness relation) (May, Must, Wf ) is a correct approximation of (s, h) ∈ Static × History, written (May, Must, Wf ) ∼ (s, h) iff: Must ⊆ h ⊆ May and Wf ⊆ { f ∈ F | s(f ) 6= Ω } This relation expresses that

68

CHAPTER 6. STATIC INITIALIZATION

F Ain (l) =  A0 (l) t Afirst (l)F t {Aout (l0 ) | flow intra (l0 , l)} Fcall (Ain (l), {Ain (m.last) | flow inter (l, m)}) if instr (l) = invoke Aout (l) = Finstr (l) (Ain (l)) otherwise where  A0 (l) =

(∅, ∅, ∅) if l = m0 .first ⊥ otherwise

Afirst =  init  F (l) (C, Ain (l0 )) | instr (l0 ) = initialize(C) if l = C..first  F Fcall {Ain (l0 ) | flow inter (l0 , m)} if l = m.first  ⊥ otherwise init ∈ and Freturn , Fany , Fput(f ) , Finitialize(C) ∈ A] → A] , Fcall Fcall ∈ A] × A] → A] are transfer functions defined by:

C × A] → A] , and

Freturn (May, Must, Wf ) = Fany (May, Must, Wf ) = (May, Must, Wf ) Fput(f ) (May, Must, Wf ) = (May, Must, Wf ∪ {f }) Finitialize(C) (a)= Finitialize(C) (May, Must, Wf ) =  Fcall (a, Ain (C..last)) a  init Fcall (C, a) t Fcall (a, Ain (C..last)) init Fcall (C, (May, Must, Wf ))

 =

if C 6∈ May if C ∈ Must if C ∈ May and C 6∈ Must

⊥ if C ∈ Must (May ∪ {C}, Must ∪ {C}, Wf ) otherwise

Fcall ((May 1 , Must 1 , Wf 1 ), (May 2 , Must 2 , Wf 2 )) = (May 2 , Must 1 ∪ Must 2 , Wf 1 ∪ Wf 2 ) Figure 6.8: Dataflow analysis 1. May contains all the classes for which we may have called the method since the beginning of the program (although they may not be finished yet). 2. Must contains all the classes for which we must have called the method since the beginning of the program (although they may not be finished yet neither). 3. Wf contains all the fields for which we are sure they have been written at least once. The analysis is then specified as a dataflow problem. Definition 6.5 (Dataflow solution) A Dataflow solution of the Must-Have-BeenInitialized analysis is any couple of maps Ain , Aout ∈ P → A] that satisfies the set of equations presented in Figure 6.8, for all program point l of the program p. In this equation system, Ain (l) is the abstract union of three kinds of dataflow information: (i) A0 (l) gives the abstraction of the initial states if l is the starting point of the main method

6.4. A MUST-HAVE-BEEN-INITIALIZED DATAFLOW ANALYSIS

69

m0 ; (ii) Afirst (l) is the abstract union of all calling contexts that may be transferred to l if it is the starting point of a method m. We distinguish two cases, depending on whether m is the class initializer of a class C. If it is, incoming calling contexts are transformed with init which filters unfeasible calling edges with Must and adds C to May and Must otherwise. Fcall Otherwise, incoming calling contexts are simply merged. (iii) At last, we merge all incoming dataflows from predecessors in the intra-procedural graph. The equation on Aout (l) distinguishes two cases, depending on whether instr (l) is a method call. If it is, we merge all dataflows from the end of the potentially called methods and combine them with the dataflows facts Ain (l) using Fcall described below. Otherwise, we transfer the dataflow Ain (l), found at the entry of the current instruction, with Finstr (l) , which handles the effect of the instruction instr (l). While Freturn , Fany and Fput(f ) are straightforward, the transfer function Finitialize(C) is defined with tree distinct cases. (i) In the first case, we are sure the class initializer C. will be called because it has never been called before. We can hence use safely the last dataflow of C. but we combine it with a using the operator Fcall described below for more precise results. (ii) In the second case, we are sure that no class initializer will be called because we know that C. has already been called and therefore simply transfer a. (iii) In the last case, we merge the two previous cases. Either a call to C. occurs, hence Fcall (a, Ain (C..last)), or C. has already been called but the init to recover this information. information has been lost (C 6∈ Must), so we apply Fcall At last, Fcall is an operator that combines dataflows about calling contexts and calling returns. It allows recovering some must information that may have been discarded during the method call because of spurious calling contexts. It is based on the monotony of Must and Wf : these sets are under-approximations of initialization history and initialized fields but since such sets only increase during execution a correct under-approximation (Must, Wf ) at a point l is still a correct approximation at every point reachable from l. The least solution of the dataflow problem corresponding the example previously shown in Section 6.3.1 is shown in Figure 6.9. In this example, A. has two potential callers in 1 and 8 but we don’t propagate the dataflow facts from 8 to 6 because we know that A. has already been called at this point, thanks to Must. At point 2, the method m is called but we don’t propagate in Aout (2) the exact values found in Ain (m.last) because we would lose the fact that A ∈ Must before the call. That is why we combine Ain (2) and Ain (m.last) with Fcall in order to refine the must information Must and Wf . Theorem 6.1 (Computability) The least dataflow solution for the partial order v is computable by the standard fixpoint iteration techniques. Proof 6.1 This is consequence of the facts that each equation is monotone, that there is a finite number of program points in p and that (A] , v, t, u) is a finite lattice. Theorem 6.2 (Soundness) If (Ain , Aout ) is a dataflow solution then for all reachable states ∈ JpK, Ain (i) ∼ (s, h) holds. Proof sketch 6.1 We first define an intermediate semantics which is shown equivalent to the small-step relation → but in which method calls are big-steps: for each point l where a method m is called we go in one step to the intra-procedural successor of l using the result of the transitive closure of . Such a semi-big-step semantics is easier to reason with method calls. Once is defined, we prove a standard subject reduction lemma between and ∼ and we conclude.

70

CHAPTER 6. STATIC INITIALIZATION

l 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

instr (l) any initialize(A) invoke initialize(B) invoke any initialize(B) put(f ) initialize(A) return

flow intra flow inter m0

l first l last A.

0

6

1

3

2

4

7

8

9

Aout (l) Must ∅ {A, B} {A, B} {B} {B} {B} {A, B} {A, B} {A, B} {A, B} {A, B} {B} {B} {B} {B}

Wf ∅ ∅ {f } ∅ ∅ ∅ ∅ {f } {f } {f } {f } ∅ ∅ ∅ ∅

B.

11

5

12 m

return

13

14

.. .

return

(a) Program example

l 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14

May ∅ ∅ {A, B} ∅ {A, B} {A, B} {A} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B}

Ain (l) Must ∅ ∅ {A, B} ∅ {B} {B} {A} {A, B} {A, B} {A, B} {A, B} {B} {B} {B} {B}

Wf ∅ ∅ {f } ∅ ∅ ∅ ∅ ∅ {f } {f } {f } ∅ ∅ ∅ ∅

May ∅ {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B} {A, B}

(b) Least dataflow solution of the program example

Figure 6.9: Analysis example

10

6.5. A THREE-VALUED INITIALIZATION STATE ANALYSIS

1 2 3

class A { static Object f = new Object ();} class C { public static void main ( String [] args ){ Object o ;

4

if ( args . length >1){

5

o = A.f; // OK } else { // nothing } o = A.f; // not proven OK with MHBI analysis

6 7 8 9 10 11

}

12 13

71

}

Figure 6.10: Example where the MHBI analysis fails

6.4.3

Implementation

This analysis led to an implementation based on an early version of Sawja (cf. Chapter 5) and using the same constraint solver as in Nit (cf. Chapter 4). The analysis had been extended to work at the bytecode level. The initialize instructions were therefore no more explicit. The sets of the abstract domains were efficiently implemented using Patricia trees. This implementation did not scale as easily as we expected mainly because the sets were huge and it converged very slowly to the solution. We were able to run this implementation on midlets (programs targeted to cell phones) and on our own examples. This implementation also showed us that the analysis was not precise enough to be practicable.

6.5 6.5.1

A Three-Valued Initialization State Analysis MHBI Analysis is Too Dependent on the Control Flow Analysis

The precision of the Must-Have-Been-Initialized analysis (MHBI) depends on the precision of the control flow graphs, but this analysis is very sensitive and too much information may be lost on even simple examples. In Figure 6.10, on the second read of A.f (l. 10), depending on the branch taken in the conditional, A has either been fully initialized or not at all. Thus, after the dataflows have been merged (l. 10), MHBI analysis only knows that A may have been initialized, which does not allow proving the safety of this field read. Note that this example feature a conditional, but the same problem arise at all join points, namely method entry point (merging call context), virtual method call points (merging the results of all methods overriding the given method), at the exit of loops if the body of the loop may not be run (merging the result of the loop with the state before the loop). As a solution, without deep modifications of MHBI analysis, we could add path-sensitivity to the analysis [DLS02]. It would be as analyzing the program in Figure 6.11 where the code occurring after the conditional has been duplicated for each branch. Although this approach works well for conditionals and bounded loops, it would certainly be expensive, would not solve the issue for virtual method calls, and would not solve some other problems. As in the official specification [LY99], MHBI analysis distinguishes two possible states: either a class has not started its class initializer, or it has. It is also possible to split the

72

CHAPTER 6. STATIC INITIALIZATION

1 2 3

class A { static Object f = new Object ();} class C { public static void main ( String [] args ){ Object o ;

4

if ( args . length >1){

5

o = A.f; o = A.f; } else { o = A.f; }

6 7 8 9 10

// OK

}

11 12

// OK // OK

}

Figure 6.11: “Path-sensitive” MHBI analysis

latter case and distinguish three states in the initialization process, as for objects: 1) a class that has not started its class initializer is uninitialized (α state), 2) a class that has started it class initializer but not returned from it yet is under initialization (β state), and 3) a class that has returned for its class initializer is initialized (γ state). Having a distinction between β and γ is important since we want to be very careful when manipulating classes in β state: the invariant ensured by the class initializer may not be verified yet. With such an analysis with these states, the example shown in Figure 6.10 can now be proven safe. Before line 6, A is in state α and is therefore fully initialized on line 6 before the field read (intuitively, only the code reachable from the class initializer of some class C can see C in its β state). Before the second field read (l. 10), A can be in γ (then branch) or α state (else branch). Both states ensure that the class initializer will be fully executed before the field read. We can therefore re-assume the post-condition of A., which states that A.f has been written. For more complex examples, we need to take the initialization context in account. In Figure 6.12, C. can either be called from main or from A.clinit. In the first case, the initialization state {A 7→ α; C 7→ β} combined with the fact that A. initializes A.f should allow proving that the read of A.f at line 13 is safe. In the second case, A.f has been initialized before calling C.. It should also allow proving that the read of A.f at line 13 is safe. Although both cases can be proven safe independently, the analysis merges the two contexts at the entry of C.. The initialization state at the entry of C. is {C 7→ {β}; A 7→ {α; β}} and the set of fields that must have been initialized is empty. This abstraction includes the case where A is under initialization (β state) and A.f is not initialized. Therefore, it does not allow proving the safety of the field read on line 13. The solution we propose is to be context-sensitive, using the initialization state of all the classes of the program for the context. In the example in Figure 6.12, A. would be analyzed twice: once in the context where {C 7→ {β}; A 7→ {α}} and starting from an empty set of written fields, and a second time in the context where {C 7→ {β}; A 7→ {β}} but starting {A.f } as set of written fields. This allows proving the example safe.

6.5. A THREE-VALUED INITIALIZATION STATE ANALYSIS

1 2 3 4 5 6 7

8

main (){ if (*){ C .*; } else { A .*; } }

9 10 11

class C static static static

C . f = new X (); C . g = A . f ();

12 13

}

14 15

{ X f; X g; {

16 17 18 19

class A static static static

23

{ X f; X g; {

A . f = new X (); A . g = C . f ();

20 21

}

22

}

73

}

Figure 6.12: Context sensitivity is needed

6.5.2

Specification of the Analysis

We herein propose an improvement of MHBI analysis with context sensitivity and with three possible initialization state: α for uninitialized classes, β for classes under initialization, and γ for initialized classes. We first introduce some basic definitions that will be used later on. |S is the standard domain restriction operator. For any relation R ∈ (A × B) and any set S ⊆ A, R |S = {(a, b) | (a, b) ∈ R ∧ a ∈ S} We also need to be able to decide if a program point l belongs to a method m. It is usually trivial as a program point is usually identified by a 3-tuple containing the class and the method in which it is to be found with an integer representing the program counter. We therefore suppose that the predicate belongsT o(l, m) is defined and holds if and only if l belongs to m. The set of classes under initialization underInit(st) for a given program state st can then be defined using the call stack. underInit() = {c ∈ C | ∃l0 ∈ l :: cs, belongsT o(l0 , c.)} def

The analysis is formalized on the same language as MHBI analysis, presented in Section 6.3. The analysis is formalized as a context-sensitive data-flow analysis over the lattice (A] , v, t, u, ⊥, >) where: • A] = IS] × P(Fp ) • IS] = P(Cp × {α, β, γ}) • ⊥A] = (∅, Fp ) • >A] = (Cp × {α, β, γ}, ∅) • for all (IS1 , SF1 ) and (IS2 , SF2 ) in A] , (IS1 , SF1 ) v (IS2 , SF2 ) ⇐⇒ IS1 ⊆ IS2 ∧ SF1 ⊇ SF2 (IS1 , SF1 ) u (IS2 , SF2 ) = (IS1 ∩ IS2 , SF1 ∪ SF2 ) (IS1 , SF1 ) t (IS2 , SF2 ) = (IS1 ∪ IS2 , SF1 ∩ SF2 ) Informally, a value (IS, SF) ∈ A] gives for any class C its possible initialization states IS(C) ⊆ {α, β, γ} and the set of fields SF that must have been initialized. We formalized this relation between A] and State as a correctness relation.

74

CHAPTER 6. STATIC INITIALIZATION

Definition 6.6 (Correctness Relation) (IS, SF) ∈ A] is a correct approximation of st =∈ State iff : • SF ⊆ {f ∈ Fp | s(f ) 6= Ω} • IS |{α} ⊇ Cp \ h • IS |{β} ⊇ underInit(st) • IS |{γ} ⊇ h \ underInit(st) This definition also formalizes the 3 initialization states: (i) classes in α state have not yet started their initialization (Cp \h); (ii) classes in β state are under initialization, i.e., a program point of their initializer is in the call stack; and (iii) classes in γ state have completed their initialization, i.e., they have started their initialization but their initializer cannot be found anymore in the call stack. Definition 6.7 (Analysis Lattice) The analysis domain A = P × IS] → A] is the set of all the total functions that associate, to a program point and an initialization context, a value in the basic lattice. This domain is equipped with the standard ordering over function to form a lattice: ⊥A = λ(l, is).⊥A] >A = λ(l, is).>A] and for all a1 and a2 in A: a1 vf a2 ⇔ a1 (l, IS) v a2 (l, IS) a1 uf a2 = {(l, i) | i = a1 (l) u a2 (l)} a1 tf a2 = {(l, i) | i = a1 (l) t a2 (l)} The dataflow solution (Ain , Aout ) ∈ A × A is then computed by fixpoint iteration on the dataflow equations given in Figure 6.13 starting from (⊥A , ⊥A ). As for the previous analysis, Ain represents the abstract union of three sources of dataflow information. (i) A0 (l) gives the abstraction of the entry point of the program if l is the entry point of the program and the initialization context is empty. (ii) Afirst (l, IS) gives an abstraction for the method entry points. It distinguishes 2 cases. If l is the entry point of some class initializer of C, then it merges the dataflows of all instructions initialize(C) where C may be in α state, and it register that C is now in β state. If l is the entry point of some other method, then it simply merges the dataflows of all incoming edges where the abstraction of the initialization states is IS. (iii) At last, we merge all incoming dataflows from the predecessors in the intra-procedural graph with the same initialization context IS. The equation Aout (l, IS) returns the result of the function specific to the instruction Finstr(l) applied to the current program point l and to the current initialization state Ain (l, IS). One can note that the effect of the instruction Finstr(l) is independent of the initialization context. The functions Freturn and Fany are straightforward and simply return the incoming initialization state and set of written fields. Fput(f) returns the incoming initialization state but also add f to the set of written fields SF. The function Finvoke (l, IS) computes the results Ain (m.last, IS) for each method m that may be called from l and where IS is the initialization state before the execution of the invoke instruction. Those results are then merged and filter

6.5. A THREE-VALUED INITIALIZATION STATE ANALYSIS

Ain (l, IS) = A0 (l, IS) t Afirst (l, IS) t

F

75

{Aout (l0 , IS) | flow intra (l0 , l)}

Aout (l, IS) = Finstr(l) (l, Ain (l, IS)) where:  A0 (l, IS) =

({(C, α) | C ∈ C}, ∅) if l = m0 .first and IS = ∅ otherwise ⊥A]

A = F (l, IS) first 0 0 {(IS , SF ) | ∃IS00 .( (IS, SF0 ) = Ain (l0 , IS00 )     ∧ IS0 = IS[C 7→ β]    ∧ instr (l0 ) = initialize(C))} F {(IS, SF) | flow inter (l0 , m)      ∧ ∃IS0 .((IS, SF) = Ain (l0 , IS0 ))}   ⊥A]

if l = C..first ∧ α ∈ IS(C)

if l = m.first otherwise

Freturn = Fany = λ(l, x).x Fput(f) (l, (IS, SF)) = (IS, SF ∪ {f}) F Finvoke (l, (IS, SF)) = Fcall ((IS, SF), {Ain (m.last, IS) | flow inter (l, m)}) do (l, (IS, SF), A (C..last, IS)) t F is (l, (IS, SF), C) Finitialize(C) (l, (IS, SF)) = Finit in init  (ISinit [C 7→ γ], SF ∪ SFinit ) if α ∈ IS(C) do (l, (IS, SF), (IS Finit init , SFinit )) = ⊥A] otherwise

is (l, (IS, SF), C) Finit

 =

(IS \ {(C, α)}, SF) ⊥A]

if β ∈ IS(C) ∨ γ ∈ IS(C) otherwise

Fcall ((IS1 , SF1 ), (IS2 , SF2 )) = (IS2 , SF1 ∪ SF2 ) Figure 6.13: Dataflow Analysis

76

CHAPTER 6. STATIC INITIALIZATION

with Fcall , which may recover some information based on the monotony of SF: this set is an under-approximation of the initialized fields and, since a such set may only increase during execution, a correct under-approximation at a point l is still a correct approximation at every point reachable from l. Finally, Finitialize(C) is the union of 2 dataflows corresponding to 2 cases. First, if C may be in α state before the instruction, then the result of C. is used to compute the result of the initialize(C) instruction, where C is now in γ state, i.e., fully initialized. Second, if C may be in β or γ state, then we may not call the initializer and the initialization state before the instruction is propagated as a result of the initialize instruction, but we are now sure that C is no more in α state and the initialization state is modified accordingly.

6.6

Towards an Implementation

From this paper and pen analysis to an (hopefully efficient) implementation, there are two important steps. First, for sake of simplicity in the reasoning and in the presentation, the language presented is not the real Java bytecode and the missing features need to be handled correctly. Second, the analysis is flow-sensitive, context-sensitive, and it manipulates a map from class to initialization state (and a field set) that has a cardinality in the order of the number of classes (or static fields) of the program. Such an analysis is expensive and for the implementation to scale, special cares need to be taken.

6.6.1

Handling the Full Bytecode

Multi-threading For sake of simplicity our semantics is only sequential but we conjecture the result of the analysis is still correct with multi-threading. The abstraction of initialized field SF is monotone. Thus, although another thread may initialize some fields without the analysis taking these initializations in account, it does not invalidate the abstraction. Unlike MHBI analysis, the evolution of the abstraction is here not monotone, but we conjecture the analysis is still safe and we try to give our intuition about the reason of the correctness. When the analysis considers a class in β or γ state, the state of the class cannot be changed by another thread. The analysis should therefore be correct for classes in those states. When the analysis considers a class in α state, the class may in fact have been initialized by another thread. Nevertheless, because all operations that give the opportunity to observe the initialization state of a class (getstatic, putstatic and invokestatic) also trigger this same initialization, the TVIS analysis will therefore encounter an initialize instruction and will then consider the class in γ state. This could be an issue: if the current thread were returning as soon as it notices that another thread is initializing the current class, this would invalidate our analysis. However, Section 2.17.5 Detailed Initialization Procedure of the JVM Specification [LY99] describe the class initialization process, which consider multi-threading, and the protocol guarantees that if the class to be initialized is already being initialized by another thread, then the current thread waits for the completion of the initialization. The class may therefore safely be considered as in γ state although the initialization did not occur in the current thread.

6.6. TOWARDS AN IMPLEMENTATION

77

Exceptions From the point of view of this analysis, exceptions only change the control flow graph. As the control flow graph is computed separately, it should not change the analysis herein described. However, if we really need to be conservative, loads of instructions may throw runtime exceptions (IndexOutOfBoundException, NullPointerException, etc.) or even errors (OutOfMemoryError, etc.) and so there will be edges in the control flow graph from most program points to the exit point of the methods, making the analysis very imprecise. There are several ways to improve the precision while safely handling exceptions. First, we can prove the absence of exception for some of those (e.g., the analysis describe in Chapter 4 would remove most NullPointerExceptions and [BGS00] would remove most IndexOutOfBoundExceptions). Then it is cheap to analyze, for each method, the exceptions that may be caught if they are thrown by the method: if there is no handler for some exception in the callers (recursively) of a particular method, then there is no need to take in account this exception in the control flow graph of this method. Indeed, if such an exception were thrown it would mean the termination of the program execution so not taking in account the exception may only add potential behaviors, which is safe. Inheritance In the presence of a class hierarchy, the initialization of a class starts by the initialization of its superclass if it has not been done yet. There is an implicit initialize instruction at the beginning of each method to the method of its superclass (except for Object.), which can be made explicit for consistency with the rest of the analysis. Note that it is not required to initialize the interfaces a class implements, nor the super interfaces an interface extends (cf. Section 2.17.4 of [LY99]). Note also that some classes may not have a class initializer but the initialization of these classes still require to initialize their superclass. The easiest solution is probably to add to theses classes a class initializer that simply contains an initialize instruction to trigger the initialization of their superclass. Field Initializers and Initialization Order If a field is initialized with a compile-time constant expression, the compiler from Java to bytecode may translate the initialization into a field initializer (cf. [LY99], Section 4.7.2), which is an attribute of the field. Although the official JVM specification states (Section 2.17.4) that the initialization of the superclass should be done before the initialization of the current class and that the field initializers are part of the initialization process, in HotSpot (Oracle’s Java Virtual Machine) the field initializers are used to set the corresponding fields before starting the initialization of the superclass. This changes the semantics but removes potential defects as it is impossible for some code to read the field before it has been set. If the analysis is targeted to such a JVM implementation, depending on the application of the analysis, the fields which have a field initializer can be safely ignored when displaying the warnings found, or be added to Wf either before initializing the superclass or just after.

6.6.2

Scaling the Analysis

To scale the analysis, we will focus on several points.

78

CHAPTER 6. STATIC INITIALIZATION • Reducing the size of the data manipulated (class map and field set). This will reduce the memory consumption and will speed up the operations on those data. • Using efficient data structures, as Binary-Decision Diagrams (BDD) [Bry92], to improve memory consumption (with sharing) and speed up basic operations (with tabulation). • Reducing the number of program points, which should mainly speed up the constraint resolution (because of sharing, it should not further reduce memory consumption).

Using a Pre-analysis We implemented a simple dataflow analysis to compute a rough set of unsafe static field read. This analysis is based on two principles. (i) A field is safely considered initialized if it is either initialized with a constant attribute or directly in its corresponding class initializer and there is no jump in the class initializer. (ii) A field read is considered safe if it is not reachable from the class initializer and if the field is considered initialized. For this analysis, we use the naive control flow due to class initializers: any initialize(C) instruction may call the initializer of C. This definition of initialized field is very restrictive. Nevertheless, our experiments on the JRE suggest that such an approximation is already quite precise in practice. In the JRE 1.6, 70% of static fields are initialized with a constant attribute and 28% are initialized in their corresponding class initializer method. The 2% remaining are initialized with native methods, reflexion, or the default value is left on purpose (and the field is checked for the default value when used). This analysis shows that less than 5% of classes have at least one field that may be unsafely read and that only 20% of static fields may be unsafely read. We could use this information to only track the initialization state of those classes and consider the other classes may be in any state. This would still be safe (even if our pre-analysis is unsound) and it would divide by 20 the cardinality of the class map manipulated. If our pre-analysis is sound, by only tracking fields that are not considered as safe by the pre-analysis, the cardinality of the field sets can also be divided by 5. Simplifications of methods In order to lower the cost of the analysis, and specially to reduce the number of program points, methods could be transformed as in CLA [HT01]. Such a transformation would be in-between the intermediate representation and the summary function. All irrelevant operations (all but getstatic, putstatic and method calls) would be removed and calls to class initializers would be made explicit. getstatic that have been declared safe by the pre-analysis and putstatic for which there is no unsafe field read could also be removed. This would allow to drastically reduce method sizes, therefore reducing the number of program points. It would also allow removing cycles where only irrelevant operations are done, which means it should converge faster.

6.7

Related Work

Kozen and Stillerman studied in [KS02] eager class initialization for Java bytecode and proposed a static analysis based on fine-grained circular dependencies to find an initialization order for classes. If their analysis finds an initialization order, then our analysis will be able

6.8. CONCLUSION AND FUTURE WORK

79

to prove all fields are initialized before being read. If their analysis finds a circular dependency, it fails to find an initialization order and issues an error while our analysis considers the initialization order implied by the main program and may prove that all fields are written before being read. The related work on instance field initialization is discussed in Section 3.2, but instance field initialization offers different challenges from the one of static fields: the instance initialization method (constructor) is explicitly called soon after the object allocation. Several formalizations of the Java bytecode have been proposed that, among other features, handled class initialization such as the work of Debbabi et al. in [DTH02] or Belblidia and Debbabi in [NB07]. Their work is focused on the dynamic semantics of the Java bytecode while our work is focused on its analysis. B¨oerger and Schulte [BS99] propose another dynamic semantics of Java. They consider a subset of Java including initialization, exceptions and threads. They have exhibited [BS00] some weaknesses in the initialization process as far as the threads are used. They pointed out that deadlocks could occur in such a situation. Harrold and Soffa [HS94] propose an analysis to compute inter-procedural definition-use chains. They have not targeted the Java bytecode language and therefore neither the class initialization problems we have faced but then, our analysis can be seen as a lightweight interprocedural definition-use analysis where all definitions except the default one are merged. Hirzel et al. [HDH04] propose a pointer analysis that target dynamic class loading and lazy class initialization. Their approach is to analyze the program at run time, when the actual classes have been loaded and to update the data when a new class is loaded and initialized. The issue of statically computing the initialization order of the classes is avoided by waiting for the classes to be actually loaded and initialized by the JVM.

6.8

Conclusion and Future Work

We have shown that class initialization is a complex mechanism and that, although in most cases it works as excepted, in some more complicated examples it can be complex to understand in which order the code will be executed. More specifically, some static fields may be read before being initialized, despite being initialized in their corresponding class initialization methods. A sound analysis may need to address this problem to infer precise and correct information about the content of static fields. We proposed a language to study static initialization and then an analysis to compute for each program point the set of class that may be initialized, the set of class that must be initialized and the set of static field that must be initialized. We showed that this analysis is not precise enough to be practicable and propose another analysis based on a different abstraction and which is context-sensitive. Based on our experience and on a study of the Java Runtime Environment, we give some suggestions to efficiently implement the analysis. Such an analysis can directly be used to check that static fields are always written before being read. It could also be used to infer more precise information about static fields in the sound null-pointer analysis presented in Chapter 3.

80

CHAPTER 6. STATIC INITIALIZATION

Chapter 7

Enforcing Secure Object Initialization in Java The initialization of an information system is usually a critical phase where essential defense mechanisms are being installed and a coherent state is being set up. In object-oriented software, granting access to partially initialized objects is consequently a delicate operation that should be avoided or at least closely monitored. Indeed, the CERT, which develops and promotes the use of appropriate technology and systems management practices to resist attacks on networked systems, to limit damage, and to ensure continuity of critical services [CER], clearly requires for secure Java development [CER10] to not allow partially initialized objects to be accessed (guideline OBJ04-J). The CERT has assessed the risk if this recommendation is not followed and has considered the severity as high and the likelihood as probable. They consider this recommendation as a first priority on a scale of three levels. Note: This work is principally a joint work with David Pichardie and has been published in the European Symposium on Research in Computer Security (ESORICS) [HJMP10].

7.1

Introduction

The Java language and the Java Byte Code Verifier (BCV) enforce some properties on object initialization, e.g., about the order in which the constructors of an object may be executed, but they do not directly enforce the CERT recommendation. Instead, Oracle provides a guideline that enforces the recommendation. Conversely, failing to apply this guideline may silently lead to security breaches. In fact, a famous attack [DFW96] used a partially initialized class loader for privilege elevation. Despite the general recommendation, manipulating an object under construction may be legitimate in some cases, e.g., some constructors call other methods on the object being initialized. Completely forbidding methods (except constructors) to manipulate partially initialized objects is not practicable. The issue is then, is the object sufficiently initialized for such a method to be called? The answer to the question is often not trivial as initialization is not only about setting fields but also about logging operations, checking permissions, etc. Thus, in a first proposition, we leave this issue to the developer. We therefore propose a twofold solution. First, a modular type system for the developer to express the initialization policy of a library or program, i.e., which methods may access 81

82

CHAPTER 7. SECURE OBJECT INITIALIZATION

partially initialized objects and, conversely, which may not. Second, a type checker, which can be integrated into the BCV, to statically check the program at load time. To validate our approach, we have formalized our type system, machine checked its soundness proof using the Coq proof assistant, and experimentally validated our solution on a large number of classes from Oracle’s Java Runtime Environment (JRE). Section 7.2 presents some related work. Section 7.3 then overviews object initialization in Java and its impacts on security. Section 7.4 then informally presents our type system, which is then formally described in Section 7.5. Section 7.6 presents two extensions to the type system and Section 7.7 finally presents the experimental results we obtained on Oracle’s JRE.

7.2

Related Work

Object initialization has been studied from different points of view. Freund and Mitchell [FM03] have proposed a type system that formalizes and enforces the initialization properties ensured by the BCV, which are not sufficient to ensure that no partially initialized object is accessed. Unlike local variables, instance fields have a default value (null, false or 0), which may be then replaced by the program. The challenge is then to check that the default value has been replaced before the first access to the field (e.g. to ensure that all field reads return a non-null value). This is has been studied in its general form by F¨ahndrich and Xia [FX07], and Qi and Myers [QM09]. Those works are focused on enforcing invariants on fields and finely track the different fields of an object. They also try to follow the objects after their construction to have more information on initialized fields. This is overkill in our context. Unkel and Lam studied another property of object initialization: stationary fields [UL08]. A field may be stationary if all its reads return the same value. Their analysis also tracks fields of objects and not the different initialization state of an object. In contrast to our analysis, they stop to track any object stored into the heap. Other work has targeted the order in which methods are called on objects. The specifications of such properties are usually based on typestate [SY86, FYD+ 08] and inference of such specification has been studied in the context of rare events (e.g., to detect anomalies, including intrusions). We refer the interested reader to the survey of Chandola et al. [CBK09]. Those works are mainly interested in the order in which methods are called but not about the initialization status of arguments. While we guarantee that a method taking a fully initialized receiver is called after its constructor, this policy cannot be locally expressed with an order on method calls as the methods (constructors) which need to be called on a object to initialize it depend on the dynamic type of the object.

7.3

Context Overview

Figure 7.1 represents an extract of class ClassLoader of Oracle’s Java Runtime Environment (JRE) as it was before 1997. The class loader is a very sensitive class in Java: it is run with full permissions, it allows loading new code in the JVM, it maps permissions to the Class objects, it defines namespaces within the JVM, etc. Only code with sufficient privileges should be able to instantiate and use new objects of this class. The security policy that needs to be ensured is that resolveClass, a security sensitive method, may be called only if the security check line 5 has succeeded. To ensure this security property, this code relies on the

7.3. CONTEXT OVERVIEW

1 2 3

public abstract class ClassLoader { private ClassLoader parent ; protected ClassLoader () { SecurityManager sm = System . g et Sec ur it yM an ag er ();

4

if ( sm != null ) { sm . c h e c k C r e a t e C l a s s L o a d e r ();} this . parent = ClassLoader . g e t S y s t e m C l a s s L o a d e r ();

5 6

}

7

protected final native void resolveClass ( Class c );

8 9

83

}

Figure 7.1: Extract of the ClassLoader of Oracle’s JRE properties enforced on object initialization by the Java Byte Code Verifier (BCV).

7.3.1

Standard Java Object Construction

In Java, an object is initialized by calling a class-specific constructor that is supposed to establish an invariant on the newly created object. The BCV enforces two properties related to these constructors. These two properties are necessary but, as we shall see, not completely sufficient to avoid security problems due to object initialization. Property 7.1 Before accessing an object, (i) a constructor of its dynamic type has been called and (ii) each constructor either calls another constructor of the same class or a constructor of the superclass on the object under construction, except for java.lang.Object, which has no superclass. This implies that when creating an instance of class C, if there is no infinite recursion nor a run-time exception, at least one constructor of C and of each superclass of C is called: it is not possible to bypass a level of constructor. To deal with exceptional behavior during object construction, the BCV enforces another property—concisely described in The Java Language Specification [GJSB05], Section 12.5, or implied by the type system described in the JSR202 [Buc06]). Property 7.2 If one constructor finishes abruptly, then the whole construction of the object finishes abruptly. Thus, if the construction of an object finishes normally, then all constructors called on this object have finished normally. Failure to implement this verification properly led to a famous attack [DFW96] in which it was exploited that if code such as try {super();} catch(Throwable e){} in a constructor is not rejected by the BCV, then malicious classes can create security-critical classes such as class loaders.

7.3.2

Attack on the Class Loader and the Patch From Oracle

However, even with these two properties enforced, it is not guaranteed that uninitialized objects cannot be used. References to uninitialized objects can be stored in global variables and accessed subsequently, even if construction terminates abnormally. In Figure 7.1, if the check fails, the method checkCreateClassLoader throws an exception and therefore terminates the construction of the object, but the garbage collector then calls a finalize() method,

84

1 2 3 4 5 6 7

CHAPTER 7. SECURE OBJECT INITIALIZATION

class Attacker extends ClassLoader { static Attacker copy ; Attacker (){ super ();} // this call will fail public static void main ( String args []){ try { Attacker o = new Attacker ();} catch ( Throwable e ){ while ( true ){ System . gc ();} }

8

}

9

protected void finalize (){

10

resolveClass (...); // Attacker exploit !

11

}

12 13

}

Figure 7.2: Example of an attacker class which is an instance method and has the object to be collected as receiver (cf. Section 12.6 of [GJSB05]). Figure 7.2 shows a class that extends java.lang.ClassLoader. We suppose this class is run in a right-restricted context, e.g., an applet. The constructor of ClassLoader will therefore fail. The class defines a finalize() method, which will be called by the garbage collector before the collection of the object. This method can call the method resolveClass, breaking the security of Java. This flaw has been successfully used for privilege escalation The initialization policy enforced by the BCV is in fact too weak: when a method is called on an object, there is no guarantee that the construction of an object has been successfully run. An ad-hoc solution to this problem is proposed by Oracle [Sun10] in its Guideline 4-3 Defend against partially initialized instances of non-final classes: the developer should implement his own monitor by adding a special Boolean field to each object for which he wants to ensure it has been sufficiently initialized. This field, set to false by default, should be private and should be set to true at the end of the constructor. Then, every method that relies on the invariant established by the constructor must test whether this field is set to true and fail otherwise. If initialized is true, the construction of the object up to the initialization of initialized has succeeded. Checking if initialized is true allows ensuring that sensitive code is only executed on classes that have been initialized up to the constructor of the current class. Figure 7.3 shows the same extract as in Figure 7.1 but with the needed instrumentation (this is the current implementation as of JRE 1.6.0 16). The private Boolean field initialized has been added to the class, it is initialized at the end of the constructor and checked in resolveClass before calling the native method resolveClass0 (which was called resolveClass in the previous figure). Note that resolveClass0 is private to ensure that it is not called directly from outside, bypassing the check in resolveClass. The attack on the class loader relied on the finalize() method, but this method is not the only way to access partially initialized objects an attacker may find. The example shown in Figure 7.4 defines a class SensitiveClass that exhibits another flaw. In its constructor, this class install an invariant, which is that its field non_null_field is non-null and that the caller has the permission to create a file in the directory /tmp. This class calls a virtual method (inits) in its constructor to initialize its field non_null_field. This issue is that, being a virtual method, it may be overridden in a subclass. Therefore, we cannot rely on

7.4. THE RIGHT WAY: A TYPE SYSTEM

1 2 3 4

public abstract class ClassLoader { private volatile boolean initialized ; private ClassLoader parent ; protected ClassLoader () { SecurityManager sm = System . g et Sec ur it yM an ag er ();

5

if ( sm != null ) { sm . c h e c k C r e a t e C l a s s L o a d e r ();} this . parent = ClassLoader . g e t S y s t e m C l a s s L o a d e r (); this . initialized = true ;} private void check () { if (! initialized ) { throw new Sec urityE xcepti on (

6 7 8 9 10 11

" ClassLoader object not initialized " );}}

12

protected final void resolveClass ( Class c ){ this . check (); this . resolveClass0 ( c );} private native void resolveClass0 ( Class c );

13 14 15 16 17

85

}

Figure 7.3: Extract of the ClassLoader of Oracle’s JRE a such method to initialize some fields and this method may execute arbitrary code before the security check. A fix could be to make the method private or static (so the method call would not be a virtual call anymore), or, probably the best solution, to initialize all the fields directly in the constructor when possible. Although there are some exceptions and some methods are designed to access partially initialized objects (for example to initialize the object), most methods should not access partially initialized objects. Following the remediation solution proposed in the CERT’s recommendation or Oracle’s guideline 4-3, a field should be added to almost every class and most methods should start by checking this field. This is resource consuming and error prone because it relies on the programmer to keep track of what is the semantic invariant, without providing the adequate automated software development tools. It may therefore lead not to functional bugs but to security breaches, which are harder to detect. In spite of being known since 1997, this pattern is not always correctly applied to all places where it should be. This has lead to security breaches, see, e.g., the Secunia Advisory SA10056 [Sec03].

7.4

The Right Way: A Type System

We propose a twofold solution: first, a way to specify the security policy which is simple and modular, yet more expressive than a single Boolean field; second, a modular type checker, which could be integrated into the BCV, to check that the whole program respects the policy. We have chosen static type checking over dynamic checking for at least two reasons. Static type checking allows for more efficiency (except for some rare cases), as the complexity of static type checking is linear in the code size, whereas the complexity of dynamic type checking is linear in the execution time. Static type checking also improves reliability of the code: if a code passes the type checking, then the code is correct with respect to its policy, whereas the dynamic type checking only ensures the correction of a particular execution. As explain in Section 3.5.1, the type checker needs to be modular to be integrated in the BCV, and a

86

CHAPTER 7. SECURE OBJECT INITIALIZATION

class SensitiveClass { private Object non_null_field ; SensitiveClass (){ inits (); SecurityManager sm = System . g et Sec ur it yM an ag er (); if ( sm != null ){ sm . checkPermission ( new java . io . FilePermission ( " / tmp / - " ," write " )); } } protected void inits (){ this . non_null_field = new Object ();} public void sensitiveMethod (){ // we rely on the invariant that non_null_field is // non - null and that we have write permission on / tmp } }

class Attacker extends SensitiveClass { static Attacker copy ; // Attack on the virtual call weakness of SensitiveClass protected void inits (){ // We have full control of the object , // 1: we may not initialize C . f // 2: we may keep a copy of the receiver for future use Attacker . copy = this ; // 3: we may directly call the method before the security check this . sensitiveMethod (); // 4: we may force the end of the initialization ( prevents logging ) throw new RuntimeException (); }

public static void main ( String args []){ try { Attacker o = new Attacker ();} catch ( Throwable e ){...} } }

Figure 7.4: A Sensitive but Unsafe Class and its Attacker

7.4. THE RIGHT WAY: A TYPE SYSTEM 1 2

class Ex1A { private Object f ; Ex1A ( Object o ){ s e c u r i t y M a n a g e r C h e c k () this . f = o ;} @Pre ( @Raw ( Ex1A )) getF (){ return this . f ;}

3 4 5 6 7 8

9

87

class Ex1B extends Ex1A { Ex1B (){

10

super (); ... = this . getF ();

11 12

}

13 14

}

}

Figure 7.5: Motivations for Raw(CLASS) annotations modular type checker needs the types on method arguments and return values to be contraand co-variant, respectively. Specifying an Initialization Policy with Annotations We rely on Java annotations and on one instruction to specify our initialization policy. We herein give the grammar of the annotations we use. V ANNOT ::= @Init | @Raw | @Raw(CLASS) R ANNOT ::= @Pre(V ANNOT) | @Post(V ANNOT)

We introduce two main annotations: @Init, which specifies that a reference can only point to a fully initialized object or the null constant, and @Raw, which has the same semantics as in Chapter 3: it specifies that a reference may point to a partially initialized object. Like in Chapter 3, @Raw(CLASS) allows specifying that the object may be partially initialized but that all constructors up to and including the constructor of CLASS must have been fully executed. E.g., when one checks that initialized contains true in ClassLoader.resolveClass, one checks that the receiver has the type @Raw(ClassLoader). The annotations produced by the V_ANNOT rule are used for fields, method arguments and return values. In the Java language, instance methods implicitly take another argument: a receiver, which is reachable through the variable this. As it is not possible yet to directly annotate the receivers in the Java 6 framework, we introduce a @Pre annotation to specify the type of the receiver at the beginning of the method. Some methods, usually called from constructors, are meant to initialize their receiver. We have therefore added the possibility to express this by adding a @Post annotation for the type of the receiver at the end of the method. These annotations take as argument an initialization level produced by the rule V_ANNOT. Figure 7.5 shows an example of @Raw annotations. Class Ex1A has an instance field f, a constructor and a getter getF. This getter requires the object to be initialized at least up to Ex1A as it accesses a field initialized in its constructor. The constructor of Ex1B uses this getter, but the object is not yet completely initialized: it has the type Raw(Ex1A) as it has finished the constructor of Ex1A but not yet the constructor Ex1B. If the getter had been annotated with @Init it would not have been possible to use it in the constructor of Ex1B. Another part of the security policy is the SetInit instruction1 , which mimics the instruction this.initialized = true in Oracle’s guideline. It is implicitly put at the end of every constructor but it can be explicitly placed before. It declares that the current object has completed its initialization up to the current class. Note that the object is not yet considered 1

To avoid modifying the Java compiler, the SetInit instruction is expressed in the source code as

ChangeType.SetInit();, a call to a special static method that has an empty body.

88

1 2 3 4 5 6

CHAPTER 7. SECURE OBJECT INITIALIZATION

public C () { ... s e c u r i t y M a n a g e r C h e c k (); // perform dynamic security checks SetInit ; // declare the object initialized up C Global . register ( this ); // the object is used with a method } // that only accept Raw ( C ) parameters

Figure 7.6: An Example with SetInit 1 2

public abstract class ClassLoader { @Init private ClassLoader parent ; @Pre ( @Raw ) @Post ( @Raw ( ClassLoader ))

3

protected ClassLoader () {

4

SecurityManager sm = System . g et Sec ur it yM an ag er ();

5

if ( sm != null ) { sm . c h e c k C r e a t e C l a s s L o a d e r ();} this . parent = ClassLoader . g e t S y s t e m C l a s s L o a d e r ();

6 7

} @Pre ( @Init ) @Post ( @Init )

8 9

protected final native void resolveClass ( @Init Class c );

10 11

}

Figure 7.7: Extract of the ClassLoader of Oracle’s JRE fully initialized as it might be called as a parent constructor in a subclass. The instruction can be used in a constructor, like in Figure 7.6, after checking some properties and before calling some other method. The SetInit instruction is not really an instruction because it has no effect on the execution of the program, it is meant to be used to express the initialization policy of the code (library or program). We have not chosen to use an annotation because Java annotations cannot be placed in the body of methods and because we use an instrumented semantics to formalize the type system and although, the SetInit has no effect on the execution of the program, it has an effect on the instrumentation data. Figure 7.7 shows class ClassLoader with its policy specification. The policy ensured by the current implementation of Oracle is slightly weaker: it does not ensure that the receiver is fully initialized when invoking resolveClass but simply checks that the constructor of ClassLoader has been fully run. On this example, we can see that the constructor has the annotations @Pre(@Raw), meaning that the receiver may be completely uninitialized at the beginning, and @Post(@Raw(ClassLoader)), meaning that, on normal return of the method, at least one constructor for each parent class of ClassLoader and a constructor of ClassLoader have been fully executed. We define as default values the most precise type that may be used in each context. This gives a safe by default policy and lowers the burden of annotating a program. Table 7.1 gives the default annotations for methods. Fields, method parameters and return values reference fully initialized objects by default (@Init annotations are therefore not needed). Constructors take a receiver uninitialized at the beginning (@Pre(@Raw)) and initialized up to the current class at the end (@Post(@Raw(C)) if in the class C). Except for constructors, method receivers have the same type at the end as at beginning of the method (i.e., if the method has the annotation @Pre(A), then it has the annotation @Post(A) by default). Other methods take a

7.5. FORMAL STUDY OF THE TYPE SYSTEM Methods

void C.(...) void C.finalize() void C.readObject (java.io.ObjectInputStream) void C.readObjectNoData (java.io.ObjectInputStream) other methods (default)

89

Receiver (@Pre) @Raw @Raw

Receiver (@Post) @Raw(C) @Raw

Arguments @Init —

Return — —

@Raw(super(C))

@Raw(C)

@Init



@Raw(super(C))

@Raw(C)

@Init



@Init

@Init

@Init

@Init

Table 7.1: Default and Hard-coded Annotations fully initialized receiver (@Pre(@Init)). There are some special methods that are called from native code (which is not analyzed, cf. paragraph Limitations in Section 7.7.1) and that may be called (or are meant to be called) on partially initialized objects. Unlike default annotations, which may be changed by the developer, these methods have annotations that are hard-coded in the type-checker. These methods are the finalize() method, which may be called by the virtual machine at any time and may therefore be called on a @Raw object, and the ReadObject and ReadObjectNoData methods which are called to initialize objects from a stream during deserialization2 . These latter methods are called on object initialized down-to the superclass, i.e. after the constructor with no argument of the superclass or the method ReadObject of the superclass has returned. If we remove from Figure 7.7 the default annotations, we obtain the original code in Figure 7.1. It shows that despite choosing the strictest (and safest) initialization policy as default, the annotation burden can be kept low. It also shows that our type system accommodates original code, it does not need the code to be modified. Although this is only an example, the experimental evaluation presented in Section 7.7 will confirm that only few annotations and no code modifications are necessary to validate a vast majority of classes.

7.5

Formal Study of the Type System

The purpose of this work is to provide a type system that enforces at load time an important security property. The semantic soundness of such mechanism is hence crucial for the global security of the Java platform. In this section, we formally define the type system and prove its soundness with respect to an operational semantics. All the results of this section have been machine-checked with the Coq proof assistant3 .

7.5.1

The language

We modify the language presented in Chapter 2. The type domain, which was left undefined in the syntax, Section 2.1, is now defined as follow. τ ∈ Type ::= Init | Raw(c) | Raw⊥ The analysis-specific domain ADO, which was also left undefined, is defined as Class⊥ . 2

Serialization is the feature that allows transmitting objects from an instance of one process to another, either with the same program later in time (as a storage feature) or with another concurrent process (as a communication feature). 3 The development can be downloaded at http://www.irisa.fr/celtique/ext/rawtypes/

90

CHAPTER 7. SECURE OBJECT INITIALIZATION

ADF is left undefined. This gives us the following object domain: O = Class × Class⊥ × (Field → V × ADF) 3 o ::= [c, cinit , o]

(object)

ADO records the initialization level of the object. An initialization cinit ∈ Class means that at least one constructor of cinit and of each of its superclasses have been called on the object and have returned without abrupt termination. We also add a new instruction, SetInit ∈ Instr, with the following semantics. m.instrs[i] = SetInit m = c.init ρ(this) = l SetInit(σ, c, l, σ 0 ) hm, i, ρ, σ, csi ⇒ hm, i+1, ρ, σ 0 , csi The SetInit instruction updates the initialization level of the object in this. It relies on the predicate SetInit(σ, c, l, σ 0 ) which specifies that σ 0 is a copy of σ where the object at location l has now the initialization tag set to c if the previous initialization was c.super. It forces the current object (this) to be considered as initialized up to the current class (i.e., as if the constructor of the current class had returned, but not necessarily the constructors of the subsequent classes). This may be used in the constructor, once all fields that need to be initialized have been initialized and if some method requiring a sufficiently initialized object needs to be called. Note that this instruction is really sensitive: using this instruction too early in a constructor may break the security of the application. We also need to update the return instruction; it has now the following semantics. ρ(this) = l ((m 6= class(m).init) ⇒ σ = σ 0 ) m.instrs[i] = return x (m 6= class(m).init ⇒ SetInit(σ, c, l, σ 0 ) ∧ x = this) hm, i, ρ, σ, (m0 , i0 , ρ0 , r) :: csi ⇒ hm0 , i0 +1, ρ0 [r 7→ ρ(x)], σ 0 , csi

7.5.2

Initialization Types

We distinguish three different kinds of initialization types. Given a heap σ we define a value type judgment h ` v : τ between values and types with the following rules.

σ ` null : τ

σ ` l : Raw



σ(l) = [c, c, o] σ ` l : Init

σ(l) = [cdyn , cinit , o] ∀c0 , cdyn  c0 ∧ c  c0 ⇒ cinit  c0 σ ` l : Raw(c) The relation  here denotes the reflexive transitive closure of the relation induced by the super element of each class. Raw⊥ denotes a reference to an object that may be completely uninitialized (at the very beginning of each constructor). Init denotes a reference to an object that has been completely initialized (it has returned for the constructor called by the new instruction). Between those two “extreme” types, a value may be typed as Raw(c) if at least one constructor of c and of each parent of c has been executed on all objects of a subtype of c that may be referenced from this value. The rule also handles the case where the dynamic type of the object (cdyn ) and c are not comparable. This generality makes the proof easier but is irrelevant in practice. We can derive from this definition the sub-typing relation Init v Raw(c) v Raw(c0 ) v Raw⊥ if c  c0 . It satisfies the important monotony property ∀σ ∈ H, ∀v ∈ V, ∀τ1 , τ2 ∈ Type, τ1 v τ2 ∧ σ ` v : τ1 ⇒ σ ` v : τ2

7.5. FORMAL STUDY OF THE TYPE SYSTEM

L ` e.f : f.ftype

91

L ` x : L(x)

L ` null : Init

Figure 7.8: Expression Typability L(y) v f.ftype m ` x.f ← y : L → L

L`e:τ x 6= this m ` x ← e : L → L[x 7→ τ ]

m ` if (?) jmp : L → L c0 = c.super L(y) v c0 .init.argtype L0 = L[this 7→ Raw(c0 )] c.init ` super(y) : L → L0 L(this) v Raw(c.super) L0 = L[this 7→ Raw(c)] c.init ` SetInit : L → L0

L(y) v c.init.argtype m ` x ← new c(y) : L → L[x 7→ Init] L(r) v m0 .pre L(y) v m0 .argtype L0 = L[r 7→ m0 .post][x 7→ m0 .rettype] m ` x ← r.m0 (y) : L → L0 L(this) v m.post L(x) v m.rettype ∀c, m = c.init ⇒ L(this) v Raw(c.super) m ` return x : L → L

Figure 7.9: Instruction Typability Note that the sub-typing judgment is disconnected from the static type of the object. In a first approach, we could expect to manipulate a pair (c, τ ) with c the static type of an object and τ its initialization type and consider equivalent both types (c, Raw(c)) and (c, Init). Such a choice would however impact deeply on the standard dynamic mechanism of a JVM: each dynamic cast from A to B (or a virtual call on a receiver) would require to check that an object has not only an initialization level set up to A but also set up to B.

7.5.3

Typing Judgment

Typing judgments for expressions are given in Figure 7.8. A typing judgment for an expression is of the form L ` e : τ where L is a function giving the type of each local variable at the current program point. Each instruction ins of a method m is attached a typing rule of the form m ` ins : L → L0 given in Figure 7.9 that constrains the type of variable before (L) and after (L0 ) the execution of ins. Definition 7.1 (Well-typed Method) A method m is well typed if there exist flow sensitive variable types L ∈ L → Var → Type such that • m.pre v L(0, this) and m.argtype v L(0, arg), • for all instructions ins at point i in m and every successor j of i, there exists a map of variable types L0 ∈ Var → Type such that L0 v L(j) and the typing judgment m ` ins : L(i) → L0 holds. The typability of a method can be decided by turning the set of typing rules into a standard dataflow problem, as show in Section 1.3.

92

CHAPTER 7. SECURE OBJECT INITIALIZATION At last, we can give all the constraints that must be fulfilled by a well-typed program.

Definition 7.2 (Well-typed Program) A program p is well typed if all its methods are well-typed and the following constraints hold: 1. for every method m that is overridden by a method m0 ( i.e. there exists c, such that (p.lookup c m = m0 )), m.pre v m0 .pre ∧ m.argtype v m0 .argtype ∧ m.post w m0 .post ∧ m.rettype w m0 .rettype 2. in each method, every first point and jump target contain an instruction and every instruction (except return) has a next instruction, 3. the default constructor c.init of each class c is unique. In this definition only point 1 is really specific to the current type system. The other points are necessary to establish the progress theorem of the next section. Type Soundness We rely on an auxiliary notion of well-formed states that capture the semantic constraints enforced by the type system. A state hm, i, ρ, σ, csi is said well-formed (wf) if there exists a type annotation Lp ∈ (Meth × L) → (Var → Type) such that ∀l ∈ L, ∀o ∈ O, σ(l) = o ⇒ σ ` o(f ) : f.ftype (wf. heap) ∀x ∈ Var, σ ` ρ(x) : Lp [m, i](x) (wf. local variables) ∀(m0 , i0 , ρ0 , r) ∈ cs, ∀x, σ ` ρ0 (x) : Lp [m0 , i0 ](x) (wf. call stack) Given a well-typed program p we then establish two key theorems. First, any valid transition from a well-formed state leads to another well-formed state (preservation) and then, from every well-formed state there exists at least a transition (progress). As a consequence we can establish that starting from an initial state (which is always well-formed) the execution is never stuck, except on final configurations. This ensures that all initialization constraints given in the operational semantics are satisfied without requiring any dynamic verification.

7.6 7.6.1

Extensions Introducing Dynamic Features

Such a static and modular type checking introduces some necessary loss of precision that cannot be completely avoided because of computability issues. To be able to use our type system on legacy code without deep modifications, we introduce two dynamic cast operators: (Raw) and (Init). The instruction y = (Init)x; allows to dynamically check that x points to a fully initialized object: if the object is fully initialized, then this is a simple assignment to y, otherwise it throws an exception. As explained in Section 7.3, the invariant needed is often weaker and the correctness of a method may only need a Raw(c) reference. y = (Raw(C))x; dynamically checks that x points to an object which is initialized up to the constructor of class C. Cast instructions (as SetInit) have not been encoded as special instructions of the JVM, which would have made the class files incompatible with the standard JVM. Instead, we encoded those instructions as method calls to a very minimalistic library of

7.7. EXPERIMENTAL RESULTS

1 2 3

93

ExArray { ExArray ( @Init ExArray [] v ){ this . m ( v );} @Pre ( @Raw ) m ( @Raw ExArray [] v ){ v [0] = this ;}}

Figure 7.10: Typing Arrays our own. y = (Init)x; is written in Java as y = ChangeType.AssertVirtualInit(x); and y = (Raw(C))x; as ChangeType.AssertStaticInit(x,C.class);. Only the static part of those cast operators has been implemented: currently, a cast operator never fails at run time. To implement the dynamic part, we could instrument the JVM to track the initialization status of each object but this would need a quite deep modification. Another solution would be to add explicit calls to the ChangeType.SetInit() method at the end of every constructor (which are implicit in our type system) and to modify the ChangeType.SetInit() method so it takes the method receiver and the current class as argument and it registers its argument in some collection. It would then be possible to implement the cast methods by checking that the argument is in the collection and with a class object sufficiently low in the class hierarchy.

7.6.2

Handling Arrays

The type system currently forbids storing partially initialized values in arrays. As we shall see in the next section, this limitation has prevented us from handling one class in the whole set of studied classes but it seems to be a restriction we could alleviate. Arrays cannot be simply handled by considering that the type of the reference is the type of its content. The example in Figure 7.10 shows a simple class with a constructor that takes as argument an array which is annotated as initialized. In the constructor, v is typed as @Init, so the call to m seems well typed (@Init is a subtype of @Raw). When the method returns, the array v, still typed as @Init, contains a reference to this, which is not yet fully initialized. The issue is that the sub-typing order considered up to now is for reading: when a @Init reference is read, it may be considered as @Raw. When writing in a array, the order is reverse: we may write an @Init reference in a array for which its content is annotated as @Raw. We propose to annotate arrays with 2 initialization types (W, R), with W subtype of R. Reads from an array annotated with (W, R) will be guaranteed to be of a subtype of R and writes to the same array must be of a subtype of W . (W1 , R1 ) is a subtype of (W2 , R2 ) if W2 is a subtype of W1 and R1 is a subtype of R2 , i.e., if (W1 , R1 ) offers stronger guarantees than (W2 , R2 ). This extension has not been validated yet, neither formally nor experimentally.

7.7 7.7.1

Experimental Results Implementation

The type system proposed in the previous sections is modular and allows the type-checker to be integrated into the BCV, to statically verify at load time that the whole program is correct. For its integration, the type checker must be modular in the sense that it must not need to load other classes to check the current method. To avoid loading other classes to check the current method C.m, the policy of the methods called from C.m must be included in class C.

94

CHAPTER 7. SECURE OBJECT INITIALIZATION

Then, when the method is resolved for the first time, the checker must check that the copy of the initialization policy included in the caller matches the initialization policy of the resolved method. Fields impose the same requirements. This is the approach taken by the BCV to ensure modularity. To avoid having to add the policy to each method call in every class, and to ease the evaluation of our type system, we first have implemented a standalone prototype. When an invoke instruction is found, the method is resolved and the security policy is found as an attribute of the method (see Sect 4.6 and 4.7 of [LY99] or Section 4.7 and 4.8 of [Buc06]). While the type-system has been formalized on a small language without arithmetic, arrays, multi-threading, etc. our type-checker has been implemented for the full Java bytecode (with the exception of subroutine imbrication and the invokedynamic instruction which is not generated by the Java compiler but it is used for scripting languages).

Limitations Although the proposed language has some limitations compared to the Java (bytecode) language, our prototype is targeted to the full bytecode. Static fields and arithmetic have not been introduced in the language but are handled by our implementation and do not add particular difficulties. Arrays have also been left out of the language but our implementation conservatively handles arrays by allowing only writes of Init references in arrays. Although this approach is very likely to be correct it has not been proved (and as we shall see in Section 7.7.2, it is not flexible enough). Multi-threading has also been left out of the current formalization but we conjecture the soundness result still holds with respect to the Java Memory Model because of the flow insensitive abstraction made on the heap. Like for the Java type system and the BCV, native methods may break our type system. It is their responsibility to respect the policy expressed in the program.

7.7.2

A Case Study: Oracle’s JRE

In order to show that our type system allows verifying legacy code with only a few annotations, we tested our prototype on all classes of packages java.lang, java.security and javax.security of the JRE1.6.0 20. This evaluation has essentially been performed by Vincent Monfort. 348 classes out of 381 were proven safe with respect to the default policy without any modification. By either specifying the actual policy when the default policy was too strict, or by adding cast instructions (see below) when the type system was not precise enough, we were able to verify 377 classes, that is to say 99% of classes. We discuss below the 4 remaining classes that are not yet proven correct by our analysis. The modifications represent only 55 source lines of code out of 131,486 for the three packages studied. Moreover most code modifications are to express the actual initialization policy, which means existing code can be proven safe. Figure 7.11 details the annotations and SetInit instructions added to specify the security policy. Note that only 45 annotations have been added to methods out of 3,859 methods (less than 1.1%) and 2 fields out of 1,524 were annotated. Only 4 cast instructions were added when we were sure they would succeed. Last but not least, the execution of the type checker takes less than 20 seconds for the packages studied.

7.7. EXPERIMENTAL RESULTS

95

Figure 7.11: Distribution of the 47 annotations and 6 instructions added to successfully type the three packages of the JRE Adapting the Security Policy Figure 7.11 details the annotations and the SetInit added to specify the security policy. In the runtime library, a usual pattern consists in calling methods that initialize fields during construction of the object. In that case, a simple annotation @Pre(@Raw(super(C))) on methods of class C is necessary. These cases represent the majority of the 37 annotations on method receivers. 6 annotations on method arguments are used, notably for some methods of java.lang.SecurityManager that check permissions on an object during its initialization. The instruction SetInit is used when a constructor initializes all the fields of the receiver and then calls methods on the receiver that are not part of the initialization. In that case the method called needs at least a Raw(C) level of initialization and the SetInit instruction allows to express that the constructor finished the minimum initialization of the receiver. Only 6 SetInit instructions are necessary. Cast Instructions Only 4 cast instructions are necessary. They are needed in two particular cases. First, when a field must be annotated; but annotations on fields were only necessary on two fields: they implied the use of the 3 (Init) cast instructions. The second case is on a receiver in a finalize() method that checks that some fields are initialized, thereby checking that the object was Raw(C) but the type system could not infer this information. The latter case implies to use the unique Raw(C) instruction added. Remaining Classes Finally, only 4 classes are not well typed after the previous modifications. Indeed the compiler generates some code to compile inner classes and part of this code needs annotations in 3 classes. These cases could be handled by doing significant changes on the code, by adding new annotations dedicated to inner classes or by annotating directly the bytecode. The one class remaining is not typable because of the limited precision of our analysis on arrays: one can only store @Init values in arrays. To check this latter class, our type system would need to be extended to handle arrays more precisely, e.g., such as describe in Section 7.6.2. Special Case of Finalize Methods As previously exposed, finalize() methods may be invoked on a completely uninitialized receiver. Therefore, we study the case of finalize() methods in the packages java.* and javax.*. In the classes of those packages there are 28 finalize() methods and only 12

96

CHAPTER 7. SECURE OBJECT INITIALIZATION

succeed to be well typed with our default annotation values. These are either empty or do not use their receiver at all. For the remaining 16 classes, the necessary modifications are either the use of cast instructions when the logic of the code guarantees the success of the cast, or the addition of @Pre(@Raw) annotations on methods called on the receiver. In that case, it is important to verify that the code of any called method is defensive enough. Therefore, the type system forced us to pay attention to the cases that could lead to security breaches or crashes at run time for finalize() methods. After a meticulous checking of the code, we added the necessary annotations and cast instructions that allowed verifying the 28 classes.

7.8

Conclusion and Future Work

We have proposed herein a solution to enforce a secure initialization of objects in Java. The solution is composed of a modular type system that enforces making explicit where uninitialized objects may be accessed, and of a modular type checker which can be integrated into the BCV to statically check a program at load time. The type system has been formalized and proved sound, and the type-checker prototype has been experimentally validated on more than 380 classes of the Java run time library. The experimental results point out that our default annotations minimize the user intervention needed to type a program and allow focusing on the few classes where the security policy needs to be stated explicitly. The possible adaptation of the security policy on critical cases allows to easily prevent security breaches and can, in addition, ensure some finer initialization properties whose violation could lead the program to crash. On one hand, results show that such a static and modular type checking allows proving in an efficient way the absence of bugs. On the other hand, only rare cases necessitate the introduction of dynamic features or to authorize arrays with partially initialized objects. This work has been accomplished during a collaboration with the ANSSI (French Network and Information Security Agency). This collaboration had the purpose of proposing solutions to improve security in Java. We integrated our type-checker in JamVM [Lou], a small but almost complete Java Virtual Machine. This code has been delivered to the ANSSI. The standalone prototype and the Coq formalization and proofs can be downloaded from http://www.irisa.fr/celtique/ext/rawtypes/.

Chapter 8

Conclusion This thesis presents a work ranging from the formalization of new analyses and the formalization of an inference analysis for an existing type-system, to the implementation of these analyses on the full Java bytecode and their experimental evaluation. F¨ahndrich and Leino propose a null pointer analysis that finely tracks object initialization by tagging objects under initialization with Raw types. A contribution of this thesis is to give semantic foundations to this idea by giving semantics to the language and to these annotations. This allows us proving both our inference analysis and their type system sound. Another contribution of this thesis is to identify Raw types as a solution for secure object initialization. This analysis may be used to improve the security provided by the Java ByteCode Verifier (BCV). The semantic foundations of our analyses are an important motivation of our work, but we do not only provide sound analyses, we also provide implementations. From our formal specifications, which abstract many details of the Java bytecode, we produce several successful implementations. • Nit is the implementation of our null-pointer analysis. It is available under the GPL and has been downloaded more than 930 times (as of October 2010). Making this implementation efficient has been an important part of the work. As a complete program analyzer, it needed an abstraction of the control flow, but even the simple CHA [DGC95] algorithm is not that easy to implement on the bytecode as there are 5 different kinds of method calls. Nit has been an important source of improvement for Sawja. • Nit/Eclipse, the plug-in to use Nit under Eclipse, have been presented at JavaOne. It has received a positive feedback and it revealed itself to be very handy to demonstrate a static analyzer. The engineer now working on Sawja in our team, Vincent Monfort, is making the plug-in more independent of Nit so it could be used it with other SA tools. It will then be integrated into Sawja. • Sawja is our library to develop static analyses for Java bytecode. With Javalib, its predecessor and now sub-component, they are available under the LGPL and have been downloaded more than a thousand times as of October 2010. Given that it is a library to handle Java bytecode from OCaml, this result seems quite encouraging. Sawja is now used to develop other static analyzers in our lab, but also by Julien Signoles and Philippe Hermann at the CEA (French Atomic Energy Commission) and by Afshin Amighi and Dilian Gurov at the Royal Institute of Technology, in Stockholm. 97

98

CHAPTER 8. CONCLUSION • Our secure object initialization analysis led to a prototype for the ANSSI (French Network and Information Security Agency) also available as a web demonstrator (http://www.irisa.fr/celtique/ext/rawtypes/). It has been integrated in a branch of the JamVM [Lou] virtual machine.

By targeting the Java bytecode for our implementations, we discovered how complicated this language can be. E.g., we discovered the complexity of class initialization during the development of Nit. At first we thought that a class initializer was a kind of constructor and that the null-pointer analysis should be easy to adapt to static fields. It was definitely wrong. Another surprise was the size of the programs. Our null-pointer and class initialization analyses are complete program analyses. Complete program analyses on real languages, like Java bytecode, offer unexpected challenges. A simple hello world program in Java uses thousands of methods in the runtime. Analyzing even a very simple program requires efficient tools. We addressed this issue when developing Nit and Sawja. Nevertheless, we mastered these difficulties and were able to produce successful software despite these difficulties. The main purpose of static analysis (SA) is to improve software quality, but it cannot improve quality if it is not actually used. Currently, SA is rarely used except for basic type systems and unsound SA. Indeed, FindBugs [HSP06], which relies on unsound SAs in order to lower the number of false positives, seems be quite welcomed by developers. Unsound analyses could probably ease the introduction of SA tools in the development process. Because of their relatively low (false) positive rate, the cost of fixing all issues reported by the tool is quite low. This can make those tools quite efficient, and should help to convince managers that SA tools can have a sufficient ROI (Return On Investment) to be more widely adopted. Another issue is probably that SA tools seem to be run quite late in the process development. SAs have difficulties to handle some patterns, which depend on the analysis. E.g., our null-pointer analysis works better if fields are initialized in their constructors, but some developers tend to initialize the fields of an object just after the end of the constructor. Another example is the manipulation of fields. If a method wants to test that a field is non-null and then use this field, it is easier for any analysis if the field is previously copied to a local variable: it avoids proving that the field may not be accessed concurrently by another thread. If SA tools were used from the beginning of the development, those hard-to-prove patterns would more naturally be avoided by developers. Once the easy bugs are fixed, once developers know how to use theses tools and know the patterns that they should use to ease the work of the SA tools, once managers see the positive ROI of SA tools, “sounder analyses” (having less false negatives) will be required. Another approach is probably to study sound analyses from now on, but combined with ways to turn on some unsound assumptions and to associate a priority to each alarm. E.g., we have introduced in Nit options to assume that arrays contain only non-null values. Another solution could have been to differentiate MayBeNull values coming from arrays from MayBeNull values coming from potentially uninitialized fields. Then, dereferencing an array-MayBeNull variable would show an alarm with a lower priority than dereferencing a field-MayBeNull variable. Another direction which should be investigated is how the potential bugs are reported. Indeed, when an alarm is shown, the developers need some feedback to investigate the cause of the alarm. E.g., by showing the annotations Nit found for each field, method argument and return value, it is easier to see where the warnings come from, but there is still work to

8.1. OUTLOOK

99

do. E.g., when a method argument is annotated as MayBeNull, one needs to check all call points to find where the method is called with a MayBeNull argument. We have developed an HTML output for Sawja which allows navigating backward in the CFG but it is not yet sufficient.

8.1

Outlook

Arrays. We have not studied the initialization of arrays. Arrays in Java present different properties than classes and are harder to track. Indeed, objects are differentiated by their class. If C defines f and is a subclass of C 0 , then, an object o of class C may be manipulated as an object of C 0 but to access f a cast or a similar operation is needed. f may only be accessed on objects of type C. Arrays in Java are also references and they may be seen as objects with a field content. But, unlike C and f , the content of an array may be manipulated without knowing the type of the array. It is like if the field content was declared in the top-most array type. Usual field-based analyses, as used in this thesis, are not well adapted to analyze arrays. The other issue is to analyze the content of an array. Fields are accessed by their name, and it is the only way to access a field in Java (bytecode). Array cells are accessed by their index, which is usually a variable, and the size of an array is usually unknown at compile time. Arrays need some numerical analysis with special abstractions of their content. Arrays have been studied by others and several analyses have been proposed by Reps et al. [GRS05] and Blanchet et al. [BCC+ 02]. Null-pointer Analysis. Our null-pointer inference analysis has been conceived as an improvement over the type system of F¨ahndrich and Leino. Because they choose the type system approach, they use relatively simple annotations to keep the annotation burden low. Our analysis infers the annotations, thus, more sophisticated annotations could be used to improve the precision of the analysis. A disadvantage of improving the precision is that the analysis is then more expensive. However, since our current implementation is very efficient, even faster than compilation, it would probably not be an issue for the user if the analysis were a bit more expensive. The following improvements could be considered. • Only the receiver is tracked precisely through the TVal] abstract domain. We could use the same abstraction for all raw objects. This could even remove the need for Raw annotations: a raw annotation would be an object which has some fields uninitialized. • A post condition could be given for each argument, like for the receiver. In particular, this would allow using a static method to initialise the fields of an object. • At the end of each constructor, we “commit” the initialized fields, i.e., the fields that are not yet initialized are considered as MayBeNull. We could try to commit those fields only when the object is stored in the heap. A pattern we sometimes found was for a method to create an object and then to initialize its fields. In such patterns, the constructor does not initialize the fields and they appear as MayBeNull to our analysis. Tracking precisely objects until they are stored into the heap would improve the precision on these cases.

100

CHAPTER 8. CONCLUSION • Spoto [Spo08] proposed to use a relational null-pointer analysis based on BDDs, but his analysis ignores raw objects. It would be interesting to see how both approaches could be combined to infer more precise method signatures.

In our analysis, preconditions of methods are computed as the least upper bounds of the conditions verified at call points. It corresponds more to the actual behavior of the program than to the intention of the developer and in some cases, weaker preconditions would still be safe. Another approach would be to infer the weakest preconditions that prevent null-pointer exceptions. It would be specially useful to annotate libraries, but it is not as precise as our analysis. Class Initialization. An obvious extension of our work on class initialization is to implement the analysis. Note that the abstraction of the initialization state of the classes, IS, does not depend on the abstraction of the initialized fields, SF . Thus, they should not be computed in parallel but in sequence. This avoids updating SF each time IS changes without being stable. The context sensitivity is problematic to implement within our current implementation framework because it is not possible to generate constraints during fixpoint iteration. Therefore, the context needs to be part of the domain, which implies that the constraints depending on a updated program point need to be reapplied for all contexts even if most of them have not changed. Another framework should be studied to implement this context-sensitive analysis. If the analysis can be expressed as a Datalog program [BJ03], then Bddbddb [WACL05] and DOOP [BS09] could be considered. Several ways to scale the analysis have been proposed in Chapter 7. However, if this is not sufficient, new simplifications should be found. The complexity of the analysis is directly related to the number of contexts a method may have. We already proposed ways to reduce the number of contexts. Another optimization could be to compute, for each class initializer, the set of class C for which an initialize(C) instruction is reachable. It seems that only those classes need to be differentiated in the context of the corresponding class initializer. If all other classes are abstracted as > = {α; β; γ}, then this optimization will not change the soundness of the analysis. Secure Object Initialization. This work could be extended in several directions. On the formal part, the extensions for the dynamic features and arrays should be formalized and proved sound. The soundness in presence of multi-threading should also be formally proved. On the implementation part, our solution for arrays should be experimentally validated. We should also propose a way to automatically infer annotations for synthetic methods, which are generated by the compiler. Indeed, synthetic methods, by definition, are not in the source and developers are probably unaware of their introduction by the compiler. Asking the developer to annotate these methods may therefore not be the most pragmatic solution, and so the analysis needs to infer them. Dynamic features would also need the necessary support to be added to a virtual machine or in a separate library if the code can by dynamically instrumented. This work could also be continued by looking for other guidelines which could be checked with type systems or other static analyses. Sources of such guidelines may by Oracle [Sun10],

8.1. OUTLOOK

101

the CERT [CER10], or Bloch [Blo01]. Indeed, they often identify some patterns that should or should not be implemented and static analysis is suited to check patterns. Nit and the Eclipse Plug-in. The Plug-in has been presented at JavaOne and has received a positive feedback. Obviously, all improvements that may be brought to the null-pointer analysis should also be implemented in Nit to improve its precision, but there are some other ways to improve the tool. It is not possible yet to hide or mark a warning as a false positive, it is therefore displayed every time the tool is run, which is not convenient for the developer. To be able to use it as a bug finder, it should also need to be able to use trusted user annotations. It is needed for native methods, but not only. Indeed, Nit displays false positives that, once a developer has reviewed them, should not only be hidden, but the value that was erroneously inferred as MayBeNull should now be considered as NotNull. Sawja. Displaying static analysis results is a first challenge that we would like to tackle. We would like to facilitate the transfer of annotations from Java source to Java bytecode and then to our intermediate representation, and the transfer of analysis results in the opposite direction. We already provide HTML outputs but ideally the result at source level would be integrated in an IDE such as Eclipse. This manipulation has already been experimented with the Eclipse plug-in for Nit and we plan to integrate it as a new generic Sawja component. To ensure correctness, we would like to replace some components of Sawja by certified extracted code from Coq [Coq] formalizations. A challenging candidate would be the IR generation that relies on optimized algorithms to transform in at most three passes each bytecode method. We would build such a work on top of the Bicolano [Bic] JVM formalization that has been mainly developed by David Pichardie during the European Mobius project.

102

CHAPTER 8. CONCLUSION

Bibliography [Abr96]

J.-R. Abrial. The B Book: Assigning Programs to Meanings. Cambridge University Press, 1996.

[AH04]

C. Artho and K. Havelund. Applying Jlint to space exploration software. In Proceedings of the conference on Verification, Model Checking and Abstract Interpretation (VMCAI), volume 2937 of LNCS, pages 297–308. Springer, 2004.

[BCC+ 02]

B. Blanchet, P. Cousot, R. Cousot, J. Feret, L. Mauborgne, A. Min´e, D. Monniaux, and X. Rival. Design and implementation of a special-purpose static program analyzer for safety-critical real-time embedded software. In The Essence of Computation: Complexity, Analysis, Transformation, volume 2566 of LNCS, pages 85–108. Springer, 2002.

[BCC+ 03]

B. Blanchet, P. Cousot, R. Cousot, J. Feret, L. Mauborgne, A. Min´e, D. Monniaux, and X. Rival. A static analyzer for large safety-critical software. In Proceedings of the conference on Programming Language Design and Implementation (PLDI), pages 196–207. ACM, 2003.

[BCF+ 99]

M G. Burke, J. Choi, S. Fink, D. Grove, M. Hind, V. Sarkar, M J. Serrano, V. C. Sreedhar, H. Srinivasan, and J. Whaley. The Jalape˜ no dynamic optimizing compiler for Java. In Proceedings of Java Grande, pages 129–141. ACM, 1999.

[BGS00]

R. Bodik, R. Gupta, and V. Sarkar. ABCD: eliminating array bounds checks on demand. In Proceedings of the conference on Programming Language Design and Implementation (PLDI), pages 321–333, 2000.

[Bic]

Bicolano - home page. http://mobius.inria.fr/bicolano.

[BJ03]

F. Besson and T. Jensen. Modular class analysis with Datalog. In Proceeding of the Symposium on Static Analysis (SAS), volume 2694 of LNCS. Springer, 2003.

[Blo01]

J. Bloch. Effective Java: Programming Language Guide. The Java Series. Addison-Wesley, 2001.

[Bry92]

R. E. Bryant. Symbolic Boolean manipulation with ordered binary-decision diagrams. ACM Computing Survey, 24(3):293–318, 1992.

[BS96]

D. F. Bacon and P. F. Sweeney. Fast static analysis of C++ virtual function calls. In Proceedings of the conference on Object-Oriented Programming, Systems, Languages & Applications (OOPSLA), pages 324–341. ACM, 1996. 103

104

BIBLIOGRAPHY

[BS99]

E. B¨ orger and W. Schulte. A programmer friendly modular definition of the semantics of Java. In Formal Syntax and Semantics of Java, LNCS, pages 353– 404. Springer, 1999.

[BS00]

E. B¨ orger and W. Schulte. Initialization problems for Java. Software - Concepts and Tools, 19(4):175–178, 2000.

[BS09]

M. Bravenboer and Y. Smaragdakis. Strictly declarative specification of sophisticated points-to analyses. SIGPLAN Not., 44(10):243–262, 2009.

[Buc06]

A. Buckley. JSR 202: JavaTM class file specification update, December 2006. http://jcp.org/en/jsr/detail?id=202.

[CBK09]

V. Chandola, A. Banerjee, and V. Kumar. Anomaly detection: A survey. ACM Computing Survey, 41(3), 2009.

[CC77]

P. Cousot and R. Cousot. Abstract interpretation: A unified lattice model for static analysis of programs by construction or approximation of fixpoints. In Proceedings of the symposium on Principles of Programming Languages (POPL), pages 238–252. ACM, 1977.

[CER]

The CERT. http://www.cert.org.

[CER10]

The CERT Sun Microsystems secure coding standard for Java, February 2010. https://www.securecoding.cert.org/confluence/display/java/.

[CFJJ06]

M. Cielecki, J. Fulara, K. Jakubczyk, and L. Jancewicz. Propagation of JML non-null annotations in Java programs. In Proceedings of the symposium on Principles and Practice of Programming in Java (PPPJ), pages 135–140. ACM, 2006.

[CJ07]

P. Chalin and P. R. James. Non-null references by default in Java: Alleviating the nullity annotation burden. In Proceedings of European Conference on Object-Oriented Programming (ECOOP), volume 4609 of LNCS, pages 227–247. Springer, 2007.

[CK]

D. R. Cok and J. R. Kiniry. ESC/Java2. http://secure.ucd.ie/products/ opensource/ESCJava2/.

[Cle]

X. Clerc. Barista. http://barista.x9c.fr/.

[Coq]

The Coq Proof Assistant. http://coq.inria.fr/.

[CWZ90]

D. R. Chase, M. Wegman, and F. K. Zadeck. Analysis of pointers and structures. In Proceedings of the conference on Programming Language Design and Implementation (PLDI), pages 296–310. ACM, 1990.

[DFW96]

D. Dean, E.W. Felten, and D.S. Wallach. Java security: From HotJava to Netscape and beyond. IEEE Symposium on Security and Privacy, pages 190– 200, 1996.

BIBLIOGRAPHY

105

[DGC95]

J. Dean, D. Grove, and C. Chambers. Optimization of object-oriented programs using static class hierarchy analysis. In Proceedings of the European Conference on Object-Oriented Programming (ECOOP), volume 952 of LNCS, pages 77–101. Springer, 1995.

[DJP09]

D. Demange, T. Jensen, and D. Pichardie. A provably correct stackless intermediate representation for Java bytecode. Research Report RR-7021, INRIA, 2009. http://hal.inria.fr/inria-00414099/en/.

[DLS02]

M. Das, S. Lerner, and M. Seigle. ESP: Path-sensitive program verification in polynomial time. In Proceedings of the conference on Programming Language Design and Implementation (PLDI), pages 57–68, 2002.

[DTH02]

M. Debbabi, N. Tawbi, and H.Yahyaoui. A formal dynamic semantics of Java: An essential ingredient of Java security. Journal of Telecommunications and Information Technology, 4:81–119, 2002.

[Ecl]

Eclipse 3.3 (Europa). www.eclipse.org.

[EH06]

T. Ekman and G. Hedin. Pluggable non-null types for Java. In Extensible Compiler Construction [Ekm06], chapter V.

[EH07]

T. Ekman and G. Hedin. Pluggable checking and inferencing of non-null types for Java. Journal of Object Technology, 6(9):455–475, 2007. Special Issue: TOOLS EUROPE 2007.

[Ekm06]

T. Ekman. Extensible Compiler Construction. PhD thesis, Lund University, June 2006.

[Ers58]

A. P. Ershov. On programming of arithmetic operations. Commun. ACM, 1(8):3–6, 1958.

[FL01]

C. Flanagan and K. R. M. Leino. Houdini, an annotation assistant for ESC/Java. In Proceedings of symposium of Formal Methods Europe (FME), volume 2021 of LNCS, pages 500–517. Springer, 2001.

[FL03]

M. F¨ ahndrich and K. R. M. Leino. Declaring and checking non-null types in an object-oriented language. ACM SIGPLAN Notices, 38(11):302–312, 2003.

[FM03]

S. N. Freund and J. C. Mitchell. A type system for the Java bytecode language and verifier. J. Autom. Reasoning, 30(3-4):271–321, 2003.

[FX07]

M. F¨ ahndrich and S. Xia. Establishing object invariants with delayed types. In Proceedings of the conference on Object-Oriented Programming, Systems, Languages & Applications (OOPSLA), pages 337–350. ACM, 2007.

[FYD+ 08]

S. J. Fink, E. Yahav, N. Dor, G. Ramalingam, and E. Geay. Effective typestate verification in the presence of aliasing. ACM Trans. Softw. Eng. Methodol, 17(2), 2008.

[GC01]

D. Grove and C. Chambers. A framework for call graph construction algorithms. ACM Transactions on Programming Languages and Systems (TOPLAS), 23(6):685–746, 2001.

106

BIBLIOGRAPHY

[gcj07]

The GNU compiler for the Java programming language, 2007. gcc.gnu.org/ java.

[GJSB05]

J. Gosling, B. Joy, G. Steele, and G. Bracha. The JavaTM Language Specification (3rd Edition). The Java Series. Addison Wesley, 3rd edition edition, 2005.

[GRS05]

D. Gopan, T. W. Reps, and S. Sagiv. A framework for numeric analysis of array operations. In Proceedings of the symposium on Principles of Programming Languages (POPL), pages 338–350. ACM, 2005.

[HBB+ 10]

L. Hubert, N. Barr´e, F. Besson, D. Demange, T. Jensen, V. Monfort, D. Pichardie, and T. Turpin. Sawja: Static Analysis Workshop for Java. In Proceedings of the International Conference on Formal Verification of ObjectOriented Software (FoVeOOS), LNCS, 2010. To appear.

[HDH04]

M. Hirzel, A. Diwan, and M. Hind. Pointer analysis in the presence of dynamic class loading. In Proceedings of the European Conference on Object-Oriented Programming (ECOOP), pages 96–122, 2004.

[HJMP10]

L. Hubert, T. Jensen, V. Monfort, and D. Pichardie. Enforcing secure object initialization in Java. In Proceedings of the European Symposium on Research in Computer Security (ESORICS), volume 6345 of LNCS, pages 101–115, 2010.

[HJP08a]

L. Hubert, T. Jensen, and D. Pichardie. Semantic foundations and inference of non-null annotations. In Proceedings of the conference on Formal Methods for Open Object-based Distributed Systems (FMOODS), volume 5051 of LNCS, pages 132–149. Springer, 2008.

[HJP08b]

L. Hubert, T. Jensen, and D. Pichardie. Semantic foundations and inference of non-null annotations. Research Report 6482, INRIA, March 2008. https: //hal.inria.fr/inria-00266171.

[HP07]

D. Hovemeyer and W. Pugh. Finding more null pointer bugs, but not too many. In Proceedings of the workshop on Program Analysis for Software Tools and Engineering (PASTE), pages 9–14. ACM, 2007.

[HP09]

L. Hubert and D. Pichardie. Soundly handling static fields: Issues, semantics and analysis. In Proceedings of ByteCode, 2009. ENTCS, 253(5):15–30.

[HS94]

M. J. Harrold and M. L. Soffa. Efficient computation of interprocedural definition-use chains. ACM Transactions on Programming Languages and Systems (TOPLAS), 16(2):175–204, 1994.

[HSP06]

D. Hovemeyer, J. Spacco, and W. Pugh. Evaluating and tuning a static analysis to find null pointer bugs. SIGSOFT Softw. Eng. Notes, 31(1):13–19, 2006.

[HT01]

N. Heintze and O. Tardieu. Ultra-fast aliasing analysis using CLA: A million lines of C code in a second. In Proceedings of the conference on Programming Language Design and Implementation (PLDI), pages 254–263. ACM, 2001.

BIBLIOGRAPHY

107

[Hub08]

L. Hubert. A Non-Null annotation inferencer for Java bytecode. In Proceedings of the workshop on Program Analysis for Software Tools and Engineering (PASTE), pages 36–42. ACM, 2008.

[IBM]

IBM. The T.J. Watson Libraries for Analysis (Wala). sourceforge.net.

[jav]

Javacc. http://javacc.dev.java.net.

[JCS07]

C. Jaspan, I.-C. Chen, and A. Sharma. Understanding the value of program analysis tools. In Proceedings of the conference on Object-Oriented Programming, Systems, Languages & Applications (OOPSLA), pages 963–970. ACM, 2007.

[jes]

Jess. http://herzberg.ca.sandia.gov.

[JP10]

T. Jensen and D. Pichardie. Secure the clones: Static enforcement of policies for secure object copying. Technical report, INRIA, June 2010. Presented at OWASP 2010.

[jul]

Julia. http://profs.sci.univr.it/$\sim$spoto/julia/.

[KKN00]

M. Kawahito, H. Komatsu, and T. Nakatani. Effective null pointer check elimination utilizing hardware trap. ACM SIGPLAN Notices, 35(11):139–149, 2000.

[KS02]

D. Kozen and M. Stillerman. Eager class initialization for Java. In Proceedings of the symposium on Formal Techniques in Real-Time and Fault-Tolerant Systems (FTRTFT), pages 71–80. Springer, 2002.

[LDG+ 07]

X. Leroy, D. Doligez, J. Garrigue, D. R´emy, and J. Vouillon. The Objective Caml system. Inria, May 2007. http://caml.inria.fr/ocaml/.

[LH03]

O. Lhot´ ak and L. Hendren. Scaling Java points-to analysis using Spark. In Compiler Construction, 12th International Conference, volume 2622 of LNCS, pages 153–169. Springer, 2003.

[LH08]

O. Lhot´ ak and L. J. Hendren. Evaluating the benefits of context-sensitive points-to analysis using a BDD-based implementation. ACM Trans. Softw. Eng. Methodol., 18(1), 2008.

[Lou]

R. Lougher. JamVM - home page. http://jamvm.sourceforge.net.

[LSS00]

K. R. M. Leino, J. B. Saxe, and R. Stata. ESC/Java user’s manual. Compaq Systems Research Center, technical note 2000-002 edition, October 2000.

[LWL05]

B. Livshits, J. Whaley, and M. S. Lam. Reflection analysis for Java. In Proceedings of the Asian Symposium on Programming Languages and Systems (APLAS), pages 139–160. Springer, 2005.

[LY99]

T. Lindholm and F. Yellin. The JavaTM Virtual Machine Specification, Second Edition. Prentice Hall PTR, 1999.

[MD97]

J. Meyer and T. Downing. Java Virtual Machine. O’Reilly Associates, 1997. http://jasmin.sourceforge.net.

http://wala.

108

BIBLIOGRAPHY

[Mor68]

D. R. Morrison. PATRICIA — Practical algorithm to retrieve information coded in alphanumeric. J. ACM, 15(4), 1968.

[MPPD08]

C. Male, D J. Pearce, A. Potanin, and C. Dymnikov. Java bytecode verification for @NonNull types. In Proceedings of the conference on Compiler Construction (CC’08). Springer, 2008.

[NB07]

M. Debbabi N. Belblidia. A dynamic operational semantics for JVML. Journal of Object Technology, 6(3):71–100, 2007.

[NNH99]

F. Nielson, H. Nielson, and C. Hankin. Principles of Program Analysis. Springer, 1999.

[NPW02]

T. Nipkow, L. C. Paulson, and M. Wenzel. Isabelle/HOL — A Proof Assistant for Higher-Order Logic, volume 2283 of LNCS. Springer, 2002.

[NS09]

M. G. Nanda and S. Sinha. Accurate interprocedural null-dereference analysis for Java. In Proceedings of the International Conference on Software Engineering (ICSE), pages 133–143. IEEE, 2009.

[PAL+ 08]

M. Papi, M. Ali, T. Luis, J. Perkins, and M. Ernst. Practical pluggable types for Java. In Proceedings of the 2008 International Symposium on Software Testing and Analysis (ISSTA), 2008.

[PAM+ 09]

B. Pagano, O. Andrieu, T. Moniot, B. Canou, E. Chailloux, P. Wang, P. Manoury, and J.L. Cola¸co. Experience report: using Objective Caml to develop safety-critical embedded tools in a certification framework. In Proceedings of the International Conference on Functional Programming (ICFP), pages 215–220. ACM, 2009.

[QM09]

X. Qi and A. Myers. Masked types for sound object initialization. In Proceedings of the symposium on Principles of Programming Languages (POPL), pages 53– 65. ACM, 2009.

[SA98]

R. Stata and M. Abadi. A type system for Java bytecode subroutines. In Proceedings of the symposium on Principles of Programming Languages (POPL), pages 149–160. ACM, 1998.

[Sec03]

Secunia Advisory SA10056: Sun JRE and SDK untrusted applet privilege escalation vulnerability. Web, October 2003. http://secunia.com/advisories/ 10056/.

[Shi88]

O. Shivers. Control flow analysis in Scheme. In Proceedings of the conference on Programming Language Design and Implementation (PLDI), pages 164–174. ACM, 1988.

[SPE]

SPEC JVM98 benchmarks. http://www.spec.org/jvm98.

[Spo05]

F. Spoto. Julia: A generic static analyser for the Java bytecode. In Proceedings of the workshop Formal Techniques for Java-like Programs (FTfJP), 2005.

BIBLIOGRAPHY

109

[Spo08]

F. Spoto. Nullness analysis in Boolean form. In Proceedings of the conference on Software Engineering and Formal Methods (SEFM), pages 21–30. IEEE, 2008.

[Sun10]

Sun. Secure coding guidelines for the Java programming language, version 3.0. Technical report, Oracle, 2010. http://java.sun.com/security/ seccodeguide.html.

[SY86]

R. Strom and S. A. Yemini. Typestate: A programming language concept for enhancing software reliability. IEEE Transactions on Software Engineering, 12(1):157–171, 1986.

[Tig]

TightVNC. www.tightvnc.com.

[TP00]

F. Tip and J. Palsberg. Scalable propagation-based call graph construction algorithms. In Proceedings of the conference on Object-Oriented Programming, Systems, Languages & Applications (OOPSLA), pages 281–293. ACM, 2000.

[UL08]

C. Unkel and M. S. Lam. Automatic inference of stationary fields: a generalization of Java’s final fields. In Proceedings of the symposium on Principles of Programming Languages (POPL), pages 183–195. ACM, 2008.

[VRCG+ 99] R. Vall´ee-Rai, P. Co, E. Gagnon, L. Hendren, P. Lam, and V. Sundaresan. Soot - A Java bytecode optimization framework. In Proceedings of the Conference of the Center for Advanced Studies on Collaborative Research (CASCON). IBM Press, 1999. [WACL05]

J. Whaley, D. Avots, M. Carbin, and M. S. Lam. Using Datalog with binary decision diagrams for program analysis. In Proceedings of the Asian Symposium on Programming Languages and Systems (APLAS), volume 3780 of LNCS, pages 97–118, 2005.

[Wha99]

J. Whaley. Dynamic optimization through the use of automatic runtime specialization. Master’s thesis, Massachusetts Institute of Technology, May 1999.