Stack Inspection : Theory and Variants Cédric Fournet

Stack inspection is a security mechanism implemented in runtimes such as the JVM and the CLR to accommodate components with diverse levels of trust. Although stack inspection enables the fine-grained expression of access control policies, it has rather a complex and subtle semantics. We present a formal semantics and an equational theory to explain how stack inspection affects program behaviour and code optimisations. We discuss the security properties enforced by stack inspection, and also consider variants with stronger, simpler properties. 1. SECURITY BY STACK INSPECTION? Stack inspection is a software-based access control mechanism. Its purpose is to allow components with diverse origins to share the same runtime and access its resources in a controlled manner, according to their respective levels of trust. It is a key security mechanism in typed runtime environments such as the JVM [19, 11] and the CLR [8] that support distributed computation based on mobile code. It enables the fine-grained expression of access control policies, and hence is more liberal and flexible than a strict sandboxing mechanism. It has received much attention in the literature [5, 7, 9, 16, 17, 27, 29, 30]. Now, stack inspection is often marketed as a feature that: (1) allows security-conscious developers, such as the authors of trusted libraries, to express their security requirements easily and precisely, and (2) can safely be ignored by everyone else. We began this work with the realisation that these two claims are problematic and need careful qualification: (1) The first problem is that stack inspection, as its name suggests, is usually thought of in specific, low level terms. It seems to be remarkably hard to give a general account of what actually is guaranteed by stack inspection. Hence, it can be difficult to assess whether it Permission to make digital or hard copies of all or part of this work for personal or classroom use is granted without fee provided that copies are not made or distributed for profit or commercial advantage and that copies bear this notice and the full citation on the first page. To copy otherwise, to republish, to post on servers or to redistribute to lists, requires prior specific permission and/or a fee. POPL ’02, Jan. 16-18, 2002 Portland, OR USA c © 2002 ACM ISBN 1-58113-450-9/02/01. $5.00 is correctly implementing a higher level security policy. Besides, certain higher-order features, such as threads and method delegation, need careful treatment. (2) The second problem is that stack inspection profoundly affects the semantics of all programs. In particular, it invalidates a wide variety of program transformations, such as inlining and tail call optimisations. We address these two problems in the setting of a λcalculus model [27, 29] of stack inspection. We formally state some of the guarantees given by stack inspection and suggest variations of stack inspection with stronger, simpler properties. We develop an equational theory of stack inspection that helps to highlight its subtle effects and also justifies certain transformations. Having outlined our motivations, we next review the ideas of stack inspection. Then we elaborate on the difficulties it raises. We close this introductory section by describing our contributions in more detail. An Outline of Stack Inspection. The situation addressed by stack inspection mechanisms is as follows. Applications are collections of components, possibly compiled from different languages, that share the same runtime. Components have a variety of origins, more or less trusted. Some mechanism—such as scoping or typing rules—prevents direct access from untrusted components to resources protected by trusted components. Still, untrusted code may call trusted code, and the other way round. We express access to different kinds of protected resources in terms of permissions, such as “may perform screen I/O” or “may perform file I/O”. A configurable policy determines the access rights available to each component given evidence of its origin, that is, where it came from and who wrote it. The access rights are simply a set of allowed permissions. Here we abstract from the details of policy and evidence, and simply refer to this set of permissions itself as the principal that owns the code. For example, a System principal might consist of all permissions, whereas an Applet principal might consist of a very limited set of rights, including “may perform screen I/O,” but not including “may perform file I/O. ” During compilation and loading, but before execution, each function or method body securely receives an annotation (here called a frame) specifying the principal owning it. During execution, when trusted code is about to access some protected resource, it invokes the stack inspection primitive (here called test) to determine whether the appropriate permission is present. A first requirement is that its immediate caller be statically annotated with the permission. In fact, the basic algorithm is to inspect the whole call stack to ensure that indirect callers as well as the immediate caller are all statically annotated with the permission. The purpose of inspecting the whole stack is to prevent the possibility that untrusted code lacking the permission could somehow cause an indirect call to a trusted function that itself accesses the resource—an instance of the Confused Deputy attack [14, 30]. Abstractly, this basic algorithm computes a compound principal whose access rights are the intersection of the access rights of all the principals on the stack, and then checks whether this compound principal has the appropriate permission. The full algorithm allows trusted code to invoke a primitive (here called grant) to override the inspection of its callers for some permissions and hence to assert responsibility for use of those permissions in every context. For example, suppose some System-owned function implementing screen I/O needs to write into a log-file, for performance debugging purposes, and hence needs the “may perform file I/O” permission. If this function is called by an Applet-owned function, access to the log-file is denied because Applet does not have the “may perform file I/O” permission. The function would override inspection of its callers for the “may perform file I/O” permission so that the file write is allowed even if its caller is Applet-owned. Limitations of Stack Inspection. The permissions authorised by stack inspection (the test primitive) are determined by a clever algorithm, outlined above, that scans control stacks on demand. The authorisation decision depends solely on the current series of nested calls. Therefore, it does not depend on other kinds of interaction between software components. Such interactions include, for instance, the use of results returned by untrusted code, mutable state, inheritance, side effects, concurrency, and dynamic loading. These interactions are commonplace, and their impact on security must be addressed independently. In the formalism of this paper, the problem appears when trusted and untrusted code exchange functions as values. As a result, a careful analysis of any code that explicitly manipulates permissions may not in fact yield any strong guarantee (although it may reveal security problems). This significantly restricts the scope of stack inspection, in isolation. On the other hand, stricter mechanisms, based for instance on systematic flow analyses, yield stronger guarantees, but may be harder and more costly to implement and to use. Living in Harmony with Stack Inspection. Assuming the target platform features stack inspection, the programmer faces two conflicting problems. Some untrusted component may take advantage of the programmer’s code to breach security—this is potentially quite bad, but it is hard to characterize. A more immediate concern is that some permission may be missing in the middle of a computation involving this code (even if the code statically has those permissions); typically, an unexpected security exception is raised—this complies with the policy, but remains undesirable. In addition, the compiler writer must deal with a specific problem: stack inspection makes the control stack observable, hence the actual runtime stack must agree with the stack as it appears to the source program. This correctness issue hinders any program transformation that changes the structure of the stack. (A prerequisite to using stack inspection is to make the control stack apparent in the source language. This may be troublesome in declarative languages like, for instance, Haskell or Mercury.) There are two further problems. Programmers and compiler writers may be concerned about the runtime costs incurred by stack inspection and by other operations on permissions. Besides, they have little control of the security policy that will be applied to their code, and must program without knowing exactly which static permissions their code will receive. Contributions of the Paper. We discuss stack inspection in the precise and abstract setting of λsec, a call-by-value λ-calculus [26] with notions of permissions, principals, and stack inspection, introduced by Pottier, Skalka, and Smith [27, 29]. Previous studies of λsec focus on type systems for checking information about permissions. Here, we use the untyped λsec-calculus as a minimal formalism for investigating the runtime behaviour of stack inspection. • We present the first equational theory for a calculus of stack inspection. We prove soundness of a primitive set of equations with respect to Morris-style contextual equivalence (Theorem 1), and completeness with respect to the reduction semantics (Theorem 2). • To obtain a co-inductive proof technique to justify our equational theory, we recast Abramsky’s applicative bisimilarity for the λsec-calculus. We show that bisimilarity is a congruence by Howe’s method (Theorem 3). Hence, we prove that bisimilarity in