Verified Bytecode Verifiers

Tobias Nipkow

Fakultät für Informatik, Technische Universität München

                      http://www.in.tum.de/~nipkow/

Abstract. Using the theorem prover Isabelle/HOL we have formalized and proved correct an executable bytecode verifier in the style of Kildall’s algorithm for a significant subset of the Java Virtual Machine. First an abstract framework for proving correctness of data flow based type inference algorithms for assembly languages is formalized. It is shown that under certain conditions Kildall’s algorithm yields a correct bytecode verifier. Then the framework is instantiated with a model of the JVM.

1 Introduction

Over the past few years there has been considerable interest in formal models for Java and the JVM, and in particular its bytecode verifier (BCV ). So far most of the work has concentrated on abstract models of particularly tricky aspects of the JVM, in particular the idiosyncratic notion of “subroutines”. This paper complements those studies by focussing on machine-checked proofs of executable models. Its distinctive features are:

– The first machine-checked proof of correctness of a BCV implementation for a nontrivial subset of the JVM.

– The BCV is an almost directly executable functional program (which can be generated automatically from its Isabelle/HOL formalization). The few non-executable constructs (some choice functions) are easily implemented.

– The work is modular: the BCV (and its correctness proof!) becomes a simple instance of a general framework for data flow analysis.

– Almost all details of the model are presented. The complete formalization, including proofs, is available via the author’s home page.

Thus the novelty is not in the actual mathematics but in setting up a framework that matches the needs of the JVM, instantiating it with a description of the JVM, and doing this in complete detail, down to an executable program, and including all the proofs. Moreover this is not an isolated development but is based firmly on existing formalizations of Java and the JVM in Isabelle/HOL [11–13]. From them it also inherits the absence of subroutines, object initialization, and exception handling. Although our only application is the JVM, our modular framework is in principle applicable to other “typed assembly languages” as well; hence the plural in the title.

What does the BCV do and what does it guarantee? The literature already contains a number of (partial) answers to this question. We follow the type system approach of Stata, Abadi and others [16, 4, 5, 14, 7]. The type systems check a program w.r.t. additional type annotations that provide the missing typing of storage locations for each instruction. These type systems are then shown to guarantee type soundness, i.e. absence of type errors during execution (which was verified formally by Pusch [13] for a subset of the system by Qian [14]). Only Qian [15] proves the correctness of an algorithm for turning his type checking rules into a data flow analyzer. However, his algorithm is still quite abstract.

Closely related is the work by Goldberg [6] who rephrases and generalizes the overly concrete description of the BCV given in [9] as an instance of a generic data flow framework. Work towards a verified implementation in the Specware system is sketched by Coglio et al. [3]. Although we share the latticetheoretic foundations with this work and it appears to consider roughly the same instruction set, it is otherwise quite different: whereas we solve the data flow problem directly, they generate constraints to be solved separately, which is not described. Furthermore, they state desired properties axiomatically but do not prove them. Casset and Lanet [2] also sketch work using the B method to model the JVM. However, their subset of the JVM is extremely simple (it does not even include classes), and it remains unclear what exactly they have proved. Based on the work of Freund and Mitchell [4] Bertot [1] has recently used the Coq system to prove the correctness of a bytecode verifier that handles object initialization (but again without classes).

The rest of the paper is structured as follows. The basic datatypes and semilattices required for data flow analysis are introduced in §2. Our abstract framework relating type systems, data flow analysis, and bytecode verification is set up in §3. It is proved that under certain conditions bytecode verification determines welltypedness. In §4, Kildall’s algorithm, an iterative data flow analysis, is shown to be a bytecode verifier. Finally, in §5, the results of the previous sections are used to derive a bytecode verifier for a subset of the JVM.

2 Types, orders, and semilattices

This section introduces the basic mathematical concepts and their formalization in Isabelle/HOL. Note that HOL distinguishes types and sets: types are part of the meta-language and of limited expressiveness, whereas sets are part of the object language and very expressive.

2.1 Basic types

Isabelle’s type system is similar to ML’s. There are the basic types bool, nat, and int, and the polymorphic types α set and α list, and a conversion function set from lists to sets. The “cons” operator on lists is the infix #, concatenation the infix @. The length of a list is denoted by size. The i-th element (starting with 0!) of list xs is denoted by xs!i. Overwriting the i-th element of a list xs with a new value x is written xs[i := x]. Recursive datatypes are introduced with the datatype keyword. The remainder of this section introduces the HOL-formal-ization of the basic lattice-theoretic concepts required for data flow analysis and its application to the JVM.

2.2 Partial orders

Partial orders are formalized as binary predicates. Based on the type synonym α ord = α → α → bool and the notations x ≤_{r} y = r x y and x <_{r} y = (x ≤_{r} y ∧ x ≠ y) we say that r :: α ord is a partial order iff the predicate order :: α ord → bool holds for r:

\begin{aligned}order r & = & (∀x. x ≤_{r} x) ∧ (∀xy. x ≤_{r} y ∧ y ≤_{r} x ⟶ x = y) ∧ \\ & & (∀xyz. x ≤_{r} y ∧ y ≤_{r} z ⟶ x ≤_{r} z) \end{aligned}

We say that r satisfies the ascending chain condition if there is no infinite ascending chain x_{0} <_{r} x_{1} <_{r} ⋯ and call T a top element if x ≤_{r} T for all x.

2.3 Semilattices

Based on the type synonyms α binop = α → α → α and α sl = α set × α ord × α binop and the supremum notation x +_{f} y = f x y we say that (A, r, f) :: α sl is a semilattice iff the predicate semilat :: α sl → bool holds:

semilat(A, r, f) = order r ∧ closed A f ∧ (∀xy ∈ A. x ≤_{r} x +_{f} y) ∧

(∀xy ∈ A. y ≤_{r} x +_{f} y) ∧ (∀xyz ∈ A. x ≤_{r} z ∧ y ≤_{r} z ⟶ x +_{f} y ≤_{r} z)

where closed A f = ∀xy ∈ A. x +_{f} y ∈ A

Usually data flow analysis is phrased in terms of infimum semilattices. We have chosen a supremum semilattice because it fits better with our intended application, where the ordering is the subtype relation and the join of two types is the least common supertype (if it exists).

We will now look at a few datatypes and the corresponding semilattices which are required for the construction of the JVM bytecode verifier. In order to avoid name clashes, Isabelle provides separate names spaces for each theory, where a theory is like a module in a programming language. Qualified names are of the form Theoryname.localname.

2.4 The error type and err-semilattices

Theory Err introduces an error element to model the situation where the supremum of two elements does not exist. We introduce both a datatype and an equivalent construction on sets:

datatype α err = Err | OK α err A = {Err} ∪ {OK a | a ∈ A}

Orderings r on α can be lifted to α err by making Err the top element:

le r (OK x) (OK y) = x ≤_{r} y
le r Err = True
le r Err  (OK y) = False

We proved that le preserves the ascending chain condition.

The following lifting functional is frequently useful:

lift2 f (OK x) (OK y) = f x y
lift2 f = Err

This brings us to the genuinely new notion of an err-semilattice. It is a variation of a semilattice with top element. Because the behaviour of the ordering and the supremum on the top element are fixed, it suffices to say how they behave on non-top elements. Thus we can represent a semilattice with top element Err compactly by a triple of type esl:

α ebinop = α → α → α err α esl = α set × α ord × α ebinop

Conversion between the types sl and esl is easy:

esl :: α sl → α esl  sl :: α esl → α err sl
esl(A, r, f) = (A, r, λxy. OK(f x y))  sl(A, r, f) = (err A, le r, lift2 f)

Now we define L :: α esl to be an err-semilattice iff sl L is a semilattice. It follows easily that esl L is an err-semilattice if L is a semilattice. The supremum operation of sl(esl L) is useful on its own:

sup f = lift2(λxy. OK(x +_{f} y))

In a strongly typed environment like HOL we found err-semilattices easier to work with than semilattices with top element.

2.5 The option type

Theory Opt introduces type option and set opt as duals to type err and set err,

datatype α option = None | Some α opt A = {None} ∪ {Some a | a ∈ A}

an ordering that makes None the bottom element, and a corresponding supremum operation:

le r (Some x) (Some y) = x ≤_{r} y sup f (Some x) (Some y) = Some(f x y)
le r None  = True sup f None  z  = z
le r (Some x) None  = False sup f z  None  = z

We proved that function sl(A, r, f) = (opt A, le r, sup f) maps semilattices to semilattices and that le preserves the ascending chain condition.

2.6 Products

Theory Product provides what is known as the coalesced product, where the top elements of both components are identified. In terms of err-semilattices, this is

esl :: α esl → β esl → (α × β) esl
esl (A, r_{A}, f_{A}) (B, r_{B}, f_{B}) = (A × B, le r_{A} r_{B}, sup f_{A} f_{B})

le :: α ord → β ord → (α × β) ord

le r_{A} r_{B} = λ(a_{1}, b_{1})(a_{2}, b_{2}). a_{1} ≤_{r_{A}} a_{2} ∧ b_{1} ≤_{r_{B}} b_{2}

sup :: α ebinop → β ebinop → (α × β)ebinop

sup f g = λ(a_{1}, b_{1})(a_{2}, b_{2}). Err.sup (λxy.(x, y)) (a_{1} +_{f} a_{2}) (b_{1} +_{g} b_{2})

Note that we use × both on the type and set level.

We have shown that if both L_{1} and L_{2} are err-semilattices, so is esl L_{1} L_{2}, and that if both r_{A} and r_{B} satisfy the ascending chain condition, so does le r_{A} r_{B}.

2.7 Lists of fixed length

Theory Listn provides the concept of lists of a given length over a given set. In HOL, this is formalized as a set rather than a type:

list n A = {xs | size xs = n ∧ set xs ⊆ A}

This set can be turned into a semilattice in a componentwise manner, essentially viewing it as an n-fold cartesian product:

sl :: nat → α sl → α list sl  le :: α ord → α list ord
sl n (A, r, f) = (list n A, le r, map2 f)  le r = list all2 (λxy. x ≤_{r} y)

where map2 :: (α → β → γ) → α list → β list → γ list and list all2 :: (α → β → bool) → α list → β list → bool are the obvious functions. We introduce the notation xs ≤_{[r]} ys = xs ≤_{(le r)} ys. We have shown (by induction on n) that if L is a semilattice, so is sl n L, and that if r is a partial order and satisfies the ascending chain condition, so does le r.

In case we want to combine lists of different lengths, or if the supremum on the elements of the list may return Err, we use the following function:

sup :: (α → β → γ err) → α list → β list → γ list err

sup f xs ys = if size xs = size ys then coalesce(map2 f xs ys) else Err coalesce [] = OK [] coalesce (e#es) = Err.sup (λx xs. x#xs) e (coalesce es)

This corresponds to the coalesced product. Below we also need the structure of all lists up to a specific length:

upto esl :: nat → α esl → α list esl

upto esl n (A, r, f) = (⋃_{i≤n} list i A, le r, sup f)

We have shown that if L is an err-semilattice, so is upto esl n L.

3 Relating type checking and data flow analysis

The purpose of this section is to set up an abstract framework for relating type checking and data flow analysis of machine code, i.e. lists of instructions. We assume that instructions may be typed (e.g. distinguishing integer from floating point addition) but storage locations are not necessarily typed and may also change their type during execution. Thus it is necessary to infer the type of each storage location at each instruction to see if the instruction will manipulate values of the required type. To keep things abstract, we do not fix the type system or the machine architecture. We simply assume that our model contains a type of states that characterizes the state of the machine. This state type is a parameter of our setup and will be represented by the type variable σ. Note that σ is intended not to represent values but their abstraction, types. For example, in a register machine σ would be a list of types, one for each register (roughly speaking). We can now define a program type, i.e. the type of a program, simply as a list of state types: each element in the list characterizes the state of the machine before execution of the corresponding instruction.

In order to lessen the confusion between types in the programming language under consideration and types in our modeling language, the latter are sometimes referred to as HOL types. For example, σ is a HOL type that represents part of the programming language type system.

In this abstract setting, we do not yet have to talk about the instruction sequences themselves. They will be hidden inside functions that characterize their behaviour. These functions form the parameters of our model, namely the type system and the data flow analyzer. In the Isabelle formalization, these functions are parameters of everything. In this article, we pretend they are global constants, thus increasing readability.

3.1 Welltyped instructions

The type system is characterized by a function and an order with top element:

wti :: σ list → nat → bool stands for well typed instruction: wti ss p is true iff instruction number p is welltyped w.r.t. the program type ss. Thus wti characterizes welltypedness of the instructions of a particular program, which remains implicit. When instantiating the framework, wti will be instantiated by the partial application of some type system to a program.

r :: (σ × σ)set, an ordering relation on σ representing the subtype relation lifted to program states. Thus our framework can deal with programming languages with subtypes.

⊤ :: σ is the top element w.r.t. r and should be thought of as the inconsistent state, indicating a type error.

The program embodied by wti is welltyped w.r.t. some program type ss iff all instructions are welltyped with non-⊤ states:

welltyping ss = ∀p < size(ss). wti ss p ∧ ss!p ≠ ⊤

3.2 Abstract semantics

The semantics of a program is characterized by the functions

step :: nat → σ → σ is the abstract execution function: step p s is the result of executing instruction p starting in state s. In the literature step p is called the transfer function or flow function associated with instruction p.

succs :: nat → nat list computes the possible successor instructions: succs p = [q_{1}, …, q_{k}] means that execution of instruction p may transfer control to any of the instructions q_{1}, …, q_{k}. We use lists instead of sets for reasons of executability.

We say that succs is bounded by n if for all p < n the elements of succs p are less than n, i.e. control never leaves the list of instructions below n.

3.3 A specification of data flow analysis and bytecode verification

Data flow analysis is concerned with solving data flow equations, i.e. systems of equations involving the flow functions over a semilattice. In our case step is the flow function and σ the semilattice. Instead of an explicit formalization of the data flow equation it suffices to consider certain prefixed points. To that end we define what it means that a program type ss is stable at p and stable:

stable ss p = ∀q ∈ set(succs p). step p (ss!p) ≤r ss!q
stables ss = ∀p < size(ss). stable ss p

We call a function dfa :: σ list → σ list a data flow analyzer (w.r.t. n :: nat and A :: σ set) iff for all ss ∈ list n A

1. dfa preserves A: dfa ss ∈ list n A,

2. dfa produces stable program types: stables (dfa ss)

3. dfa is increasing: ss ≤_{[r]} dfa ss

4. dfa is bounded by stable program types:

∀ts ∈ list n A. ss ≤_{[r]} ts ∧ stables ts ⟶ dfa ss ≤_{[r]} ts

In case you are not initimately familiar with data flow analysis: the correctness of this specification is not an issue because it is merely a stepping stone that does not occur in our main result.

We need to introduce the subset A of σ to make distinctions beyond HOL’s type system: for example, when representing a list of registers, σ is likely to be a HOL list type; but the fact that in any particular program the number of registers is fixed cannot be expressed as a HOL type, because it requires dependent types to formalize lists of a fixed length. We use sets to express such fine grained distinctions.

A bytecode verifier is defined as a function between program types, where the absence of ⊤ in the result indicates welltypedness. Formally, a function bcv :: σ list → σ list is a bytecode verifier (w.r.t. n :: nat and A :: σ set) iff

∀ss ∈ list n A. ⊤ ∉ set(bcv ss) = (∃ts. ss ≤_{[r]} ts ∧ welltyping ts)

3.4 Relating data flow analysis and bytecode verification

Since the data flow analyzer is specified in terms of stability and the bytecode verifier in terms of welltyping, the two notions need to be related. Naively one may assume that the stable program types are exactly the welltypings. This is almost true, but for lists containing ⊤: they may be stable but will not be welltypings. We say that wti and stable agree for ⊤-free program types (w.r.t. n :: nat and A :: σ set) iff for all ss ∈ list n A and p < n

⊤ ∉ set(ss) ⟶ (wti ss p = stable ss p)

Note that because stable is defined in terms of step, r and succs, this property relates wti to those three constants.

Theorem 1. Let r be a partial order with top element ⊤. If wti and stable agree for ⊤-free program types and dfa is a data flow analyzer then dfa is a bytecode verifier (all w.r.t. some n and A).

This theorem follows easily from the definitions. The hard part is to discharge the premise that dfa is a data flow analyzer, i.e. to provide a correct implementation. This is the topic of §4.

Note that instead of having both wti and step as parameters of the framework and requiring that they agree, one could define wti in terms of stable. We have not done so because much of the work on Java’s bytecode verifier is based on type systems rather than flow functions. In particular, in §5.5 we build on the verification by Pusch which is phrased in terms of a type system.

4 Kildall’s algorithm

This section defines and verifies a functional version of Kildall’s algorithm [8, 10], a standard data flow analysis tool. In fact, the description of bytecode verification in the official JVM specification [9, pages 129–130] is essentially Kildall’s algorithm. We define the algorithm in the context of semilattice (A, f, r) and the functions step and succs. Its core is the iteration on a state type ss and a worklist w :: nat set that holds the set of all indices of ss whose contents has changed and still needs to be propagated. Once w becomes empty, ss is returned:

iter(ss, w) = if w = {} then ss else let p = εp. p ∈ w; t = step p (ss!p) in iter(propa (succs p) t ss (w − {p}))

The choice of which position to consider next is made by Hilbert’s ε-operator: εx.P (x) is some arbitrary but fixed x such that P (x) holds; if there is no such x, then the value of εx.P (x) is arbitrary but still defined. Since the choice in iter is guarded by w ≠ {}, we know that p ∈ w. An implementation is free to chose whichever element it wants.

The body of the iteration is hidden in function propa which propagates the new value at ss!p to all its successors, thus reflecting the execution of a single instruction p. This means, the result of step p (ss!p) has to be “merged” [9, page 130] with the state ss!q of all successor instructions q of p. “Merging” means the computation of the supremum w.r.t. (A, f, r). The worklist is updated in case this merging results in a change.

propa []  t ss w = (ss, w)
propa (q#qs) t ss w = let u = t +f ss!q
w′ = if u = ss!q then w else {q} ∪ w
in propa qs t (ss[q := u]) w′

Kildall’s algorithm is simply a call to iter where the worklist is initialized with the set of indices that need propagating:

kildall ss = iter(ss, {p. p < size ss∧∃q ∈ set(succs p).step p (ss!p)+f ss!q ≠ ss!q})

Function iter is partial in general. Thus its above definition is illegal in HOL, a logic of total functions. The actual definition of iter in HOL requires that the ordering r of our semilattice satisfies the ascending chain condition. For reasons discussed already towards the end of §3.3 we also require that the functions step and succs do not lead outside the semilattice carrier A:

1. succs is bounded by size ss

2. step preserves A: ∀s ∈ A, p < n. step p s ∈ A

Finally, ss and w must also be initialized appropriately: set ss ⊆ A and w is bounded by size ss: ∀p ∈ w. p < size ss.

4.1 Kildall’s algorithm is a bytecode verifier

We will now sketch the proof that, under suitable assumptions, Kildall’s algorithm is indeed a bytecode verifier. For that purpose we introduce some more terminology. We call step monotone (w.r.t. A and n) iff for all s ∈ A, p < n, s ≤r t implies step p s ≤r step p t. The main theorem states that iter satisfies the properties of a data flow analyzer as defined in §3.3:

Theorem 2. If r satisfies the ascending chain condition, step preserves A and is monotone w.r.t. A and n, succs and w are bounded by n, ss ∈ list n A, and ss is stable at p < n for all p ∉ w, then

iter(ss, w) ∈ list n A ∧ stables (iter(ss, w)) ∧ ss ≤_{[r]} iter(ss, w) ∧
∀ts ∈ list n A. ss ≤_{[r]} ts ∧ stables ts ⟶ iter(ss, w) ≤_{[r]} ts

The following two corollaries follow easily from the definition of a data flow analyzer and kildall and from Theorem 1:

Corollary 3. If r satisfies the ascending chain condition, step preserves A and is monotone w.r.t. A and n, and succs is bounded by n, then kildall is a data flow analyzer.

Corollary 4. If r satisfies the ascending chain condition, ⊤ is its top element, step preserves A and is monotone w.r.t. A and n, succs is bounded by n, and wti and stable agree for ⊤-free program types, then kildall is a bytecode verifier.

The proof of Theorem 2 is by induction along the recursion of iter. In an imperative language, iter would be a while-loop and the proof would proceed by the following invariant:

ss ∈ list n A ∧ (∀p < n. p ∉ w ⟶ stable ss p) ∧ cs ≤_{[r]} ss ∧
∀ts ∈ list n A. cs ≤_{[r]} ts ∧ stables ts ⟶ ss ≤_{[r]} ts

where cs is a logical variable that refers to the initial value of ss. Of course, the proof of Theorem 2 follows exactly the same line. As a first step we show that propa can be split into two independent computations: if w is bounded by size ss then

propa qs t ss w = (merges t qs ss, {q. q ∈ set qs ∧ t +f ss!q ≠ ss!q} ∪ w)

where merges is defined recursively:

merges t []  ss = ss
merges t (p#ps) ss = merges t ps (ss[p := t +f ss!p])

Thus the verification can be phrased in terms of the simpler merges instead of propa. Preservation of the invariant can be shown with a few suitable lemmas about merges. When w = {} it is easy to see and prove that the invariant implies the corresponding conditions in Theorem 2.

5 Application to JVM

In this section we apply the generic framework to a subset of the JVM. We do not consider the details of how information, e.g. the subclass relationship, is encoded in the compiled class files. Instead, we represent this information abstractly, e.g. as a binary relation between class names. To minimize explicit parameterization, such class file derived information is dealt with via implicit parameters, just as explained in the beginning of §3.

5.1 Types

Theory JType describes the types of our JVM. The machine only supports the void type, integers, null references and class types (based on a type cname of class names):

datatype ty = Void | Integer | NullT | Class cname

Each class file records the direct superclass. In our formalization this gives rise to an implicit parameter S of HOL type (cname × cname)set, where (D, C) ∈ S means that D is a direct subclass of C. Subclasses induce a subtype relation:

subtype τ_{1} τ_{2}  = (τ_{1} = τ_{2} ∨ τ_{1} = NullT ∧ is Class τ_{2} ∨
∃C D. τ_{1} = Class C ∧ τ_{2} = Class D ∧ (C, D) ∈ S^{∗})
is Class (Class C) = True
is Class  = False
is ref τ  = (τ = NullT ∨ is Class τ)

Corresponding to it we have a supremum operation on types:

sup NullT (ClassD) = OK(Class D)
sup (Class C) NullT = OK(Class C)
sup (Class C) (Class D) = OK(Class(some lub C D))
sup τ_{1}  τ_{2}  = if τ_{1} = τ_{2} then OK τ_{1} else Err

The auxiliary function some lub used in the computation of the supremum of two classes is defined non-constructively (as some least upper bound, using Hilbert’s ε-operator). Of course we also prove that (under suitable conditions) least upper bounds are uniquely determined and exist. Thus our work is independent of the particular algorithm used for this calculation.

The type cname is assumed to have a distinguished element Object. Predicate is type :: ty → bool is true for all basic types and for all class types below Class Object (w.r.t. the given subclass hierarchy S). As abbreviations we introduce types = {τ | is type τ} and τ_{1} ⊑ τ_{2} = τ_{1} ≤subtype τ_{2}.

Theorem 5. The triple esl = (types, subtype, sup) is an err-semilattice provided S is univalent (each subclass has at most one direct superclass, i.e. S represents a single inheritance hierarchy) and S is acyclic.

Univalence and acyclicity together imply that S is a set of trees, and is type focusses on the subtree below Object.

Because any infinite subtype chain would induce an infinite subclass chain we also obtain

Lemma 6. If S^{−1} is wellfounded (there is no infinite ascending subclass chain (C_{i}, C_{i+1}) ∈ S) then subtype satisfies the ascending chain condition.

Note that by incorporating a fixed class hierarchy into our model we assume that all required classes have been loaded, i.e. we model an eager class loader. Although Sun’s JVM is a bit lazier, the JVM specification [9, p. 127] does permit eager loading.

5.2 Instructions

For our subset of the JVM we have selected the following representative instruction set

datatype instr = Load nat | Store nat | AConst Null | IConst int | IAdd
| Getfield ty cname | Putfield ty cname | New cname
| Invoke cname mname ty ty | CmpEq nat | Return

where mname is the type of method names. The main difference to Sun’s JVM is that Load, Store, CmpEq, and Return are polymorphic (the real JVM has one such instruction for each base type), Invoke only supports methods with a single parameter (the first ty argument), Getfield and Putfield carry the field type but not its name, and CmpEq uses absolute instead of relative addressing.

The JVM is a stack machine where each activation record consists of a stack for expression evaluation and a list of local variables (which we call registers). The abstract semantics, which operates on the type level, records the type of each stack element and each register. At a specific program point, a register may hold either of two incompatible types, e.g. an integer or a reference, depending on the computation path that lead to that point. This facilitates reuse of registers and is modeled by the HOL type ty err, where OK τ represents type τ itself and Err represents the unusable/inconsistent type. In contrast, the stack should only hold values that can actually be used. Thus the configurations of our abstract JVM are pairs of an expression stack and a list of registers:

config = ty list × ty err list

The execution of a single instruction is modeled by function exec :: instr → config → config err where the result Err indicates a run time error, either because of stack over or underflow or because of type mismatch. In Sun’s JVM the maximal stack size is recorded in the class file as the code attribute max stack for each method. To simplify the presentation, we concentrate on the verification of a single method and introduce two further implicit parameters maxs :: nat and rT :: ty, the method’s maximal stack size and its return type. The definition of exec below is quite straightforward.

Note that access to a register beyond the size of reg is not checked during execution because it is prevented easily by a single pass over the instruction sequence (see wfi below). A dynamic check would merely add clutter.

We now start to instantiate the parameters of our abstract framework in §3: σ = config err option where None indicates a program point that has not been reached yet, Some(OK c) is a normal configuration c and Some Err an error. The introduction of the extra option layer means that our data flow analyzer will also determine reachability of instructions and enforce type correctness for reachable instructions only. Function step is merely a lifted version of exec

step p = option map (lift exec (bs!p))

lift f e = case e of Err ⇒ Err | OK x ⇒ f x option map f None = None option map f (Some x) = Some(f x)

where bs is yet another implicit parameter, the list of instructions of the method under examination. As dictated by the general framework in §3.2, we also need a successor function. Its definition is straightforward:

succs p = case bs!p of Return ⇒ [p] | CmpEq q ⇒ [p+1, q] | ⇒ [p+1]

exec instr (st, reg) = case instr of Load n ⇒ if size st < maxs then case reg!n of Err ⇒ Err | OK τ ⇒ OK(τ#st, reg) else Err

| Store n ⇒ (case st of [] ⇒ Err | τ#st_{1} ⇒ OK(st_{1}, reg[n := OK τ]))
| AConst Null ⇒ if size st < maxs then OK(NullT#st, reg) else Err
| IConst i ⇒ if size st < maxs then OK(Integer#st, reg) else Err
| IAdd ⇒ (case st of Integer#Integer#st_{2} ⇒ OK(Integer#st_{2}, reg) | ⇒ Err)
| Getfield τ_{f} C ⇒ (case st of [] ⇒ Err | τ_{o}#st_{1} ⇒ if τ_{o} ⊑ Class C then OK(τ_{f} #st_{1}, reg) else Err)
| Putfield τ_{f} C ⇒ (case st of τ_{v}#τ_{o}#st_{2} ⇒ if τ_{v} ⊑ τ_{f} ∧ τ_{o} ⊑ Class C then OK(st_{2}, reg) else Err | ⇒ Err)

| New C ⇒ if size st < maxs then OK((Class C)#st, reg) else Err
| Invoke C m τ_{p} τ_{r} ⇒ (case st of τ_{a}#τ_{o}#st_{2} ⇒ if τ_{o} ⊑ Class C ∧ τ_{a} ⊑ τ_{p} then OK(τ_{r}#st_{2}, reg) else Err | ⇒ Err)

| CmpEq q ⇒ (case st of τ_{1}#τ_{2}#st_{2} ⇒ if τ_{1} = τ_{2} ∨ is ref τ_{1} ∧ is ref τ_{2} then OK(st_{2}, reg) else Err | ⇒ Err)

| Return ⇒ (case st of [] ⇒ Err | τ# ⇒ if τ ⊑ rT then OK(st, reg) else Err)

Modelling the Return instruction by a self loop may seem odd, but it needs a successor instruction because otherwise data flow analysis would never execute it. And a loop is simpler than introducing a fictitious successor.

The main omissions in our model of the JVM are exceptions, object initialization, and jsr/ret instructions because our work builds on the type safety proof by Pusch (see §5.5) which does not cover these features either. Including object initialization is straightforward, but exceptions require a modification of our abstract framework: flow functions (step) need to be associated with edges rather than nodes of the program graph because exceptional behaviour can differ from normal behaviour. The difficulty of including jsr/ret is unclear.

5.3 Type system

We start with those context conditions that can be checked separately for each instruction. This check uses a method dictionary of HOL type

mdict = cname → (mname × ty → ty option)

A method dictionary is a functional abstraction of the information about the types of all methods defined in the current collection of class files. It maps a class name C, method name m, and a type τ to the result type of the method that is selected when m is invoked with an object of class C and a parameter of type τ (remember that Invoke only supports single argument methods). In case there is no applicable method, None is returned. The argument type is necessary because Java allows overloading of method names, which are disambiguated via their argument types. The formal details need not concern us here and can be found elsewhere [11].

The wellformedness of an instruction is checked in the context of two further implicit parameters: a method dictionary md :: mdict and a limit maxr :: nat for the maximal register index in the current method (the max locals code attribute in Sun’s JVM).

wfi(Load n)  = n < maxr
wfi(Store n)  = n < maxr
wfi(Getfield τ C)  = is type τ ∧ is type (Class C)
wfi(Putfield τ C)  = is type τ ∧ is type (Class C)
wfi(New C)  = is type (Class C)
wfi(Invoke C m τ_{p} τ_{r}) = md C (m, τ_{p}) = Some(τ_{r})
wfi( )  = True

The instruction sequence bs is wellformed iff all of its elements are: ∀instr ∈ set bs. wfi instr.

The method dictionary also comes with a wellformedness condition reflecting the type soundness requirement that a method redefined in a subclass must have a more specialized result type: md is wellformed iff

md C mT = Some τ ⟶
is type τ ∧ (∀D. (D, C) ∈ S^{∗} ⟶ (∃τ′. md D mT = Some τ′ ∧ τ′ ⊑ τ))

Note that the official JVM specification does not mention this wellformedness property but imposes a stronger one implicitly: one method can override another only if their result types are identical, a restriction we have relaxed.

Now we come to the core of the type system, namely the semilattice structure on σ and the predicate wti. Turning σ into a semilattice is easy, because all of its constituent types are (err-)semilattices. The expression stacks form an errsemilattice because the supremum of stacks of different size is Err; the list of registers forms a semilattice because the number of registers is fixed:

stk esl :: ty list esl  reg sl :: ty err list sl
stk esl = upto esl maxs JType.esl  reg sl = Listn.sl maxr (Err.sl JType.esl)

See Theorem 5 for JType.esl. Since any error on the stack must be propagated, the stack and registers are combined in a coalesced product via Product.esl and then embedded into err and option to form the final semilattice sl :: σ sl:

sl = Opt.sl(Err.sl(Product.esl stk esl (Err.esl reg sl)))  (1)

As a shorthand, the three components of sl are named states (the carrier set), le (the ordering), and sup (the supremum). Furthermore we introduce an infix abbreviation for the ordering le: s ≪ t = s ≤le t

Combining the theorems about the various (err-)semilattice constructions involved in the definition of sl (starting from Theorem 5), it is easy to prove Corollary 7. If S is univalent and acyclic then sl is a semilattice. It is trivial to show that ⊤ = Some Err is the top element of this semilattice.

Below you find the definition of wti, the only remaining parameter of our abstract framework. If you expect type systems to come as a set of inference rules, note that each case in the definition of wti could be turned into an equivalent inference rule for a judgment ss, p ⊢ instr.

wti ss p = case ss!p of None ⇒ True | Some e ⇒ case e of Err ⇒ False | OK(st, reg) ⇒ case bs!p of Load n ⇒ size st < maxs ∧ ∃τ. reg!n = OK τ ∧ Some(OK(τ#st, reg)) ≪ ss!(p+1) | Store n ⇒ ∃τ st_{1}. st = τ#st_{1} ∧ Some(OK(st_{1}, reg[n := OK τ])) ≪ ss!(p+1) | AConst Null ⇒ size st < maxs ∧ Some(OK(NullT#st, reg)) ≪ ss!(p+1) | IConst i ⇒ size st < maxs ∧ Some(OK(Integer#st, reg)) ≪ ss!(p+1) | IAdd ⇒ ∃st_{2}. st = Integer#Integer#st_{2} ∧ Some(OK(Integer#st_{2}, reg)) ≪ ss!(p+1) | Getfield τ_{f} C ⇒ ∃τ st_{1}. st = τ#st_{1} ∧ τ ⊑ Class C ∧ Some(OK(τ_{f}#st_{1}, reg)) ≪ ss!(p+1)

| Putfield τ_{f} C ⇒ ∃τ_{v} τ_{o} st_{2}. st = τ_{v}#τ_{o}#st_{2} ∧ τ_{v} ⊑ τ_{f} ∧ τ_{o} ⊑ Class C ∧ Some(OK(st_{2}, reg)) ≪ ss!(p+1)

| NewC ⇒ size st < maxs ∧ Some(OK((Class C)#st, reg)) ≪ ss!(p+1) | Invoke C m τ_{p} τ_{r} ⇒ ∃τ_{a} τ_{o} st_{2}. st = τ_{a}#τ_{o}#st_{2} ∧ τ_{o} ⊑ Class C ∧ τ_{a} ⊑ τ_{p} ∧ Some(OK(τ_{r}#st_{2}, reg)) ≪ ss!(p+1)

| CmpEq q ⇒ ∃τ_{1} τ_{2} st_{2}. st = τ_{1}#τ_{2}#st_{2} ∧ (τ_{1} = τ_{2} ∨ is ref τ_{1} ∧ is ref τ_{2}) ∧ Some(OK(st_{2}, reg)) ≪ ss!(p+1) ∧ Some(OK(st_{2}, reg)) ≪ ss!q

| Return ⇒ ∃τ st_{1}. st = τ#st_{1} ∧ τ ⊑ rT

5.4 A verified bytecode verifier

We can now instantiate Kildall’s algorithm with its remaining (implicit) parameter, namely the semilattice. Clearly (A, f, r) must be sl as defined in (1) and hence A = states, f = sup and r = le. We call the resulting specialized algorithm kiljvm :: σ list → σ list. Before we can use Corollary 4 to prove that kiljvm is a bytecode verifier, we need three properties of step: Lemma 8. If bs and md are wellformed, step preserves states and is monotone. If succs is bounded by size bs then wti and stable agree for ⊤-free program types. Each property is proved by a case distinction over the different instructions. Corollary 9. If S is univalent and S^{−1} wellfounded (see Lemma 6), bs and md are wellformed, and succs is bounded by size bs, then kiljvm is a bytecode verifier. It follows directly from Corollary 4: some of its preconditions are covered by Lemma 8; the ascending chain property of le follows from Lemma 6 because the semilattice constructions involved preserve the property. This corollary clearly shows that the verification of class files can be split up into separate tasks: a first phase where static wellformedness conditions of class files are checked (the class hierarchy, the method result types, and the individual instructions), and the actual data flow analysis. The same separation is also present in the official JVM specification.

5.5 The global picture

So far we have only considered an abstraction of the JVM that works on types rather than values. It remains to show that this abstraction is faithful, i.e. that welltypedness on this abstract level actually guarantees the absence of type errors in the concrete JVM operating on values. To this end we connect our work with that of Pusch [13] who gave a specification of a bytecode verifier and showed that it implies type soundness of the concrete JVM. Roughly speaking, she proved that during execution of welltyped code, all values conform to the types given in the welltyping. That is, she defined a type jvmstate of concrete machine states (which include the pc), a function exec_{val} :: jvmstate → jvmstate option for executing a single instruction, and a conformance relation corrstate s ss between a concrete machine state s and a program type ss (all in the context of a set of class files). And she proved

welltyping ss ∧ corrstate s ss ∧ exec_{val} s = Some t ⟶ corrstate t ss

i.e. a welltyping guarantees type safe execution. By our definition of “bytecode verifier” in terms of welltyping (§3.3) and the fact that kiljvm is a bytecode verifier (Corollary 9) it follows that if ⊤ ∉ kiljvm ss_{0} then execution is type safe from any concrete state s conforming to ss_{0} (because corrstate is monotone: corrstate s ss_{0} and ss_{0} ≤_{[le]} ss imply corrstate s ss).

The program type ss_{0} that iteration starts from is initialized as follows. When checking a method in class C with parameter types τ_{1}, …, τ_{n} we set

ss_{0} = [Some(OK(init)), None, …, None]
init = ([], [Some(Class C), p_{1}, …, p_{n}, None, …, None])

such that size ss_{0} = size bs; init models the state upon entry into the method: the stack is empty, the first register contains the this-pointer, the next n registers contain the parameters p_{i} = Some(τ_{i}), and the remaining registers, which hold the actual local variables, are uninitialized, i.e. do not contain a usable value.

6 Conclusion

By verifying an executable BCV, this work closes a significant gap in the effort to provide a machine-checked formalization of the Java/JVM architecture. Despite its relative compactness (500 lines of specifications and programs, and 2000 lines of proofs), the amount of work to construct such a detailed model should not be underestimated. But when it comes to security, there is no substitute for complete formality. And since both the theory and the tools are available, we intend to verify implementations of more realistic BCVs (incl. object initialization and exceptions) in the not too distant future.

Acknowledgments. I thank Zhenyu Qian, Gilad Bracha, Gerwin Klein and David von Oheimb for many helpful discussions and for reading drafts of this paper.

References

1. Y. Bertot. A Coq formalization of a type checker for object initialization in the Java Virtula Machine. Technical Report RR-4047, INRIA, Nov. 2000.

2. L. Casset and J. L. Lanet. A formal specification of the Java bytecode semantics using the B method. In ECOOP’99 Workshop Formal Techniques for Java Programs, 1999.

3. A. Coglio, A. Goldberg, and Z. Qian. Toward a provably-correct implementation of the JVM bytecode verifier. In Proc. DARPA Information Survivability Conference and Exposition (DISCEX’00), Vol. 2, pages 403–410. IEEE Computer Society Press, 2000.

4. S. N. Freund and J. C. Mitchell. A type system for object initialization in the Java bytecode language. In ACM Conf. Object-Oriented Programming: Systems, Languages and Applications, 1998.

5. S. N. Freund and J. C. Mitchell. A formal framework for the java bytecode language and verifier. In ACM Conf. Object-Oriented Programming: Systems, Languages and Applications, 1999.

6. A. Goldberg. A specification of Java loading and bytecode verification. In Proc. 5th ACM Conf. Computer and Communications Security, 1998.

7. M. Hagiya and A. Tozawa. On a new method for dataflow analysis of Java virtual machine subroutines. In G. Levi, editor, Static Analysis (SAS’98), volume 1503 of Lect. Notes in Comp. Sci., pages 17–32. Springer-Verlag, 1998.

8. G. A. Kildall. A unified approach to global program optimization. In Proc. ACM Symp. Principles of Programming Languages, pages 194–206, 1973.

9. T. Lindholm and F. Yellin. The Java Virtual Machine Specification. Addison-Wesley, 1996.

10. S. S. Muchnick. Advanced Compiler Design and Implementation. Morgan Kaufmann, 1997.

11. T. Nipkow and D. v. Oheimb. Java_{ℓight} is type-safe — definitely. In Proc. 25th ACM Symp. Principles of Programming Languages, pages 161–170, 1998.

12. T. Nipkow, D. v. Oheimb, and C. Pusch. μJava: Embedding a programming language in a theorem prover. In F. Bauer and R. Steinbrüggen, editors, Foundations of Secure Computation, pages 117–144. IOS Press, 2000.

13. C. Pusch. Proving the soundness of a Java bytecode verifier specification in Isabelle/HOL. In W. Cleaveland, editor, Tools and Algorithms for the Construction and Analysis of Systems (TACAS’99), volume 1579 of Lect. Notes in Comp. Sci., pages 89–103. Springer-Verlag, 1999.

14. Z. Qian. A formal specification of Java Virtual Machine instructions for objects, methods and subroutines. In J. Alves-Foss, editor, Formal Syntax and Semantics of Java, volume 1523 of Lect. Notes in Comp. Sci., pages 271–311. Springer-Verlag, 1999.

15. Z. Qian. Standard fixpoint iteration for Java bytecode verification. ACM Trans. Programming Languages and Systems, ?:?–?, 200? Accepted for publication.

16. R. Stata and M. Abadi. A type system for Java bytecode subroutines. In Proc. 25th ACM Symp. Principles of Programming Languages, pages 149–161. ACM Press, 1998.