A Type System for Data-Flow Integrity on Windows Vista

A Type System for Data-Flow Integrity on Windows Vista

Abstract

The Windows Vista operating system implements an interesting model of multi-level integrity. We observe that in this model, trusted code can be blamed for any information-flow attack; thus, it is possible to eliminate such attacks by static analysis of trusted code. We formalize this model by designing a type system that can efficiently enforce data-flow integrity on Windows Vista. Typechecking guarantees that objects whose contents are statically trusted never contain untrusted values, regardless of what untrusted code runs in the environment. Some of Windows Vista’s runtime access checks are necessary for soundness; others are redundant and can be optimized away.

d
\authorinfo

Avik Chaudhuri University of California at Santa Cruz avik@cs.ucsc.edu \authorinfoPrasad Naldurg and Sriram Rajamani Microsoft Research India {prasadn,sriram}@microsoft.com \conferenceinfoPLAS’08, June 8, 2008, Tucson, Arizona, USA. \CopyrightYear2008 \copyrightdata978-1-59593-936-4/08/06

\category

D.4.6Operating SystemsSecurity and Protection[Access controls, Information flow controls, Verification] \categoryD.2.4Software EngineeringProgram Verification[Correctness proofs] \categoryF.3.1Logics and Meanings of ProgramsSpecifying and Verifying and Reasoning about Programs[Specification techniques, Invariants, Mechanical verification]

\terms

Security, Verification, Languages, Theory

ynamic access control, data-flow integrity, hybrid type system, explicit substitution

1 Introduction

Commercial operating systems are seldom designed to prevent information-flow attacks. Not surprisingly, such attacks are the source of many serious security problems in these systems [44]. Microsoft’s Windows Vista operating system implements an integrity model that can potentially prevent such attacks. In some ways, this model resembles other, classical models of multi-level integrity [9]—every process and object111In this context, an object may be a file, a channel, a memory location, or indeed any reference to data or executable code. is tagged with an integrity label, the labels are ordered by levels of trust, and access control is enforced across trust boundaries. In other ways, it is radically different. While Windows Vista’s access control prevents low-integrity processes from writing to high-integrity objects, it does not prevent high-integrity processes from reading low-integrity objects. Further, Windows Vista’s integrity labels are dynamic—labels of processes and objects can change at runtime. This model allows processes at different trust levels to communicate, and allows dynamic access control. At the same time, it admits various information-flow attacks. Fortunately, it turns out that such attacks require the participation of trusted processes, and can be eliminated by code analysis.

In this paper, we provide a formalization of Windows Vista’s integrity model. In particular, we specify an information-flow property called data-flow integrity (DFI), and present a static type system that can enforce DFI on Windows Vista. Roughly, DFI prevents any flow of data from the environment to objects whose contents are trusted. Our type system relies on Windows Vista’s runtime access checks for soundness. The key idea in the type system is to maintain a static lower-bound label  for each object. While the dynamic label of an object can change at runtime, the type system ensures that it never goes below , and the object never contains a value that flows from a label lower than . The label is declared by the programmer. Typechecking requires no other annotations, and can be mechanized by an efficient algorithm.

By design, DFI does not prevent implicit flows [18]. Thus DFI is weaker than noninterference [23]. Unfortunately, it is difficult to enforce noninterference on a commercial operating system such as Windows Vista. Implicit flows abound in such systems. Such flows arise out of frequent, necessary interactions between trusted code and the environment. They also arise out of covert control channels which, given the scope of such systems, are impossible to model sufficiently. Instead, DFI focuses on explicit flows [18]. This focus buys a reasonable compromise—DFI prevents a definite class of attacks, and can be enforced efficiently on Windows Vista. Several successful tools for malware detection follow this approach [12, 52, 47, 49, 16, 37], and a similar approach guides the design of some recent operating systems [19, 57].

Our definition of DFI is dual to standard definitions of secrecy based on explicit flows—while secrecy prevents sensitive values from flowing to the environment, DFI prevents the flow of values from the environment to sensitive objects. Since there is a rich literature on type-based and logic-based analysis for such definitions of secrecy [11, 3, 48, 13], it makes sense to adapt this analysis for DFI. Such an adaptation works, but requires some care. Unlike secrecy, DFI cannot be enforced without runtime checks. In particular, access checks play a crucial role by restricting untrusted processes that may run in the environment. Further, while secrecy prevents any flow of high-security information to the environment, DFI allows certain flows of low-security information from the environment. We need to introduce new technical devices for this purpose, including a technique based on explicit substitution [4] to track precise sources of values. This device is required not only to specify DFI precisely but also to prove that our type system enforces DFI.

We design a simple higher-order process calculus that simulates Windows Vista’s security environment [31, 17, 43]. In this language, processes can fork new processes, create new objects, change the labels of processes and objects, and read, write, and execute objects in exactly the same ways as Windows Vista allows. Our type system exploits Windows Vista’s runtime access checks to enforce DFI, and can recognize many correct programs. At the same time, our type system subsumes Windows Vista’s execution controls, allowing them to be optimized away.

1.1 Summary of contributions

To sum up, we make the following main contributions in this paper:

  • We propose DFI as a practical multi-level integrity property in the setting of Windows Vista, and formalize DFI using a semantic technique based on explicit substitution.

  • We present a type system that can efficiently enforce DFI on Windows Vista. Typechecking guarantees DFI regardless of what untrusted code runs in the environment.

  • We show that while most of Windows Vista’s runtime access checks are required to enforce DFI, Windows Vista’s execution controls are redundant and can be optimized away.

1.2 Outline

The rest of this paper is organized as follows. In Section 2, we introduce Windows Vista’s security environment, and show how DFI may be violated in that environment. In Section 3, we design a calculus that simulates Windows Vista’s security environment, equip the calculus with a semantics based on explicit substitution, and formalize DFI in the calculus. In Section 4, we present a system of integrity types and effects for this calculus. In Section 5, we prove soundness and other properties of typing. Finally, in Section 6, we discuss limitations and contributions with respect to related work and conclude. Supplementary material, including proof details and an efficient typechecking algorithm, appear in the appendix.

2 Windows Vista’s integrity model

In this section, we provide a brief overview of Windows Vista’s integrity model.222Windows Vista further implements a discretionary access control model, which we ignore in this paper. In particular, we introduce Windows Vista’s security environment, and show how DFI may be violated in that environment. We observe that such attacks require the participation of trusted processes.

2.1 Windows Vista’s security environment

In Windows Vista, every process and object is tagged with a dynamic integrity label. We indicate such labels in brackets below. Labels are related by a total order , meaning “at most as trusted as”. Let range over processes, over objects, and over labels. Processes can fork new processes, create new objects, change the labels of processes and objects, and read, write, and execute objects. In particular, a process with label can:

  • fork a new process ;

  • create a new object ;

  • lower its own label;

  • change the label of an object to iff ;

  • read an object ;

  • write an object iff ;

  • execute an object by lowering its own label to .

Rules (i) and (ii) are straightforward. Rule (iii) is guided by the principle of least privilege [34], and is used in Windows Vista to implement a feature called user access control (UAC) [43]. This feature lets users execute commands with lower privileges when appropriate. For example, when a system administrator opens a new shell (typically with label ), a new process is forked with label ; the shell is then run by the new process. When an Internet browser is opened, it is always run by a new process whose label is lowered to ; thus any code that gets run by the browser gets the label —by Rule (i)—and any file that is downloaded by the browser gets the label —by Rule (ii).

Rules (iv) and (v) are useful in various ways, but can be dangerous if not used carefully. (We show some attacks to illustrate this point below.) In particular, Rule (iv) allows unprotected objects to be protected by trusted processes by raising their labels, and Rule (v) allows processes to read objects at lower trust levels. At the same time, Rule (iv) facilitates dynamic access control, and Rule (v) facilitates communication across trust boundaries.

Rule (vi) protects objects from being written by processes at lower trust levels. Thus, for example, untrusted code forked by a browser cannot affect local user files. User code cannot modify registry keys protected by a system administrator. Rule (vii) is part of UAC; it prevents users from accidentally launching less trusted executables with higher privileges. For example, a virus downloaded from the Internet cannot run in a trusted user shell. Neither can system code dynamically link user libraries.

2.2 Some attacks

We now show some attacks that remain possible in this environment. Basically, these attacks exploit Rules (iv) and (v) to bypass Rules (vi) and (vii).

(Write and copy)

By Rule (vi), cannot modify if . However, can modify some object , and then some process can copy ’s content to . Thus, Rule (iv) can be exploited to bypass Rule (vi).

(Copy and execute)

By Rule (vii), cannot execute at if . However, can copy ’s content to some object and then execute . Thus, Rule (iv) can be exploited to bypass Rule (vii).

(Unprotect, write, and protect)

By Rule (vi), cannot modify if . However, some process can unprotect to , then can modify , and then can protect back to . Thus, Rule (v) can be exploited to bypass Rule (vi).

(Copy, protect, and execute)

By Rule (vii), cannot execute at if . However, some process can copy ’s content to an object , and then can protect to and execute . Thus, Rules (iv) and (v) can be exploited to bypass Rule (vii).

Next, we show that all of these attacks can violate DFI. At the same time, we observe that access control forces the participation of a trusted process (one with the higher label) in any such attack.

  • In (Write and copy) or (Unprotect, write, and protect), suppose that the contents of are trusted, and is the label of untrusted code, with . Then data can flow from to , violating DFI, as above. Fortunately, some process can be blamed here.

  • In (Copy and execute) or (Copy, protect, and execute), suppose that the contents of some object are trusted, and is the label of untrusted code, with . Then data can flow from some process to , violating DFI, as follows: packs code to modify and writes the code to , and unpacks and executes that code, as above. Fortunately, can be blamed here.

Our type system can eliminate such attacks by restricting trusted processes (Section 4). (Obviously, the type system cannot restrict untrusted code running in the environment.) Conceptually, this guarantee can be cast as Wadler and Findler’s “well-typed programs can’t be blamed” [51]. We rely on the fact that a trusted process can be blamed for any violation of DFI; it follows that if all trusted processes are well-typed, there cannot be any violation of DFI.

3 A calculus for analyzing DFI on Windows Vista

To formalize our approach, we now design a simple higher-order process calculus that simulates Windows Vista’s security environment. We first introduce the syntax and informal semantics, and present some examples of programs and attacks in the language. We then present a formal semantics, guided by a precise characterization of explicit flows.

3.1 Syntax and informal semantics

Several simplifications appear in the syntax of the language. We describe processes by their code. We use variables as object names, and let objects contain packed code or names of other objects. We enforce a mild syntactic restriction on nested packing, which makes typechecking significantly more efficient (Appendix B; also see below). Finally, we elide conditionals—for our purposes, the code

can be conservatively analyzed by composing and in parallel. (DFI is a safety property in the sense of [7], and the safety of the latter code implies that of the former. We discuss this point in more detail in Section 3.3.)

Values include variables, , and packed expressions. Expressions include those for forking new processes, creating new objects, changing the labels of processes and objects, and reading, writing, and executing objects. They also include standard expressions for evaluation and returning results (see Gordon and Hankin’s concurrent object calculus [24]). expression fork action evaluation result action create object change process label change object label read object write object execute object result variable unit process fork action evaluation value value result packed expression Syntactically, we distinguish between processes and expressions: while every expression is a process, not every process is an expression. For example, the process is not an expression, although the process is. Expressions can be packed, but processes in general cannot. In particular, a process cannot be of the form . (Such a process can, however, be written as .) The benefits of this distinction become clear in Section 5, where we discuss mechanical typechecking. However, for the bulk of the paper, the reader may ignore this distinction; indeed, neither the semantics nor the type system are affected by this distinction.

Processes have the following informal meanings.

  • forks a new process with the current process label and continues as (see Rule (i)).

  • creates a new object with the current process label, initializes with , and returns (see Rule (ii)); the annotation is used by the type system (Section 4) and has no runtime significance.

  • changes the current process label to and continues as ; it blocks if the current process label is lower than (see Rule (iii)).

  • changes ’s label to and returns ; it blocks if is not bound to an object at runtime, or the current process label is lower than ’s label or (see Rule (iv)).

  • returns the value stored in ; it blocks if is not bound to an object at runtime (see Rule (v)).

  • writes the value to and returns ; it blocks if is not bound to an object at runtime, or if the current process label is lower than ’s label (see Rule (vi)).

  • unpacks the value stored in to a process , lowers the current process label with ’s label, and executes ; it blocks if is not bound to an object at runtime or if the value stored in is not a packed expression (see Rule (vii)).

  • executes , binds the value returned by to , and continues as with bound.

  • returns itself.

3.2 Programming examples

We now consider some programming examples in the language. We assume that , , , and are labels, ordered in the obvious way. We assume that the top-level process always runs with , which is the most trusted label.

Example 3.1.

Suppose that a user opens an Internet browser with privileges (recall UAC), and clicks on a that contains ; the virus contains code to overwrite the command shell executable , which has label .

This code may eventually reduce to

However, at this point the write to blocks due to access control. (Recall that a process with label cannot write to an object with label .)

Example 3.2.

Next, consider the following attack, based on the (Copy, protect, and execute) attack in Section 2.2. A user downloads a virus from the Internet that contains code to erase the user’s home directory (), and saves it by default in . A administrator protects and executes .

This code may eventually reduce to

The user’s home directory may be erased at this point. (Recall that access control does not prevent a process with label from writing to an object with label .)

3.3 An overview of DFI

Informally, DFI requires that objects whose contents are trusted at some label never contain values that flow from labels lower than . In Example 3.1, we trust the contents of at label , as declared by the static annotation . DFI is not violated in this example, since access control prevents the flow of data from to . On the other hand, in Example 3.2, we trust the contents of at label . DFI is violated in this example, since the value flows from to .

By design, DFI is a safety property [7]—roughly, it can be defined as a set of behaviors such that for any behavior that not in that set, there is some finite prefix of that behavior that is not in that set. To that end, DFI considers only explicit flows of data. Denning and Denning characterizes explicit flows [18] roughly as follows: a flow of is explicit if and only if the flow depends abstractly on (that is, it depends on the existence of , but not on the value ). Thus, for example, the violation of DFI in Example 3.2 does not depend on the value any other value causes the same violation. Conversely, is not dangerous in itself. Consider the reduced process in Example 3.2. Without any knowledge of execution history, we cannot conclude that DFI is violated in . Indeed, it is perfectly legitimate for a -process to execute the code

intentionally, say as part of administration. However, in Example 3.2, we know that this code is executed by unpacking some code designed by a -process. The violation of DFI is due to this history.

It follows that in order to detect violations of DFI, we must distinguish between various instances of a value, and track the sources of those instances during execution. We maintain this execution history in the operational semantics (Section 3.4), by a technique based on explicit substitution [4].

Before we move on, let us ease the tension between DFI and conditionals. In general, conditionals can cause implicit flows [18]; a flow of can depend on the value if appears in the condition of some code that causes that flow. For example, the code

causes an implicit flow of to that depends on the value . We can abstract away this dependency by interpreting the code as the code . Recall that DFI is a safety property. Following [33], the safety of can be expressed by the logical formula , where is the formula that expresses the safety of , and is the formula that expresses the safety of . Likewise, the safety of can be expressed by the formula . Clearly, we have , so that the code is a refinement of the code . It is well-known that safety is preserved under refinement [33].

But implicit flows are of serious concern in many applications; one may wonder whether focusing on explicit flows is even desirable. Consider the code above; the implicit flow from to violates noninterference, if is an untrusted value and the contents of are trusted. In contrast, DFI is not violated in the interpreted code

if and are trusted values. Clearly, DFI ignores the implicit flow from to . But this may be fine—DFI can be used to prove an invariant such as “the contents of are always boolean values”. Note that the code

does not maintain this invariant, since may be an arbitrary value. Thankfully, DFI is violated in this code.

3.4 An operational semantics that tracks explicit flows

Local reduction

(Reduct evaluate)

(Reduct new)

(Reduct read)

(Reduct write)

(Reduct execute)

(Reduct un/protect)

Structural equivalence

(Struct bind)

(Struct substitution)

(Struct fork)

(Struct store)

(Struct equiv)

Global reduction

(Reduct context)

(Reduct congruence)

We now present a chemical-style operational semantics for the language, that tracks explicit flows.333This presentation is particularly convenient for defining and proving DFI; of course, a concrete implementation of the language may rely on a lighter semantics that does not track explicit flows. We begin by extending the syntax with some auxiliary forms. process source process store explicit substitution substituted value value object initialization The process asserts that the object contains and is protected with label . A key feature of the semantics is that objects store values “by instance”—only variables may appear in stores. We use explicit substitution to track and distinguish between the sources of various instances of a substituted value. Specifically, the process creates a fresh variable , records that is bound to by a process with label , and continues as with bound. Here is an instance of and is the source of . If is a value, then this process is behaviorally equivalent to with substituted by . For example, in Example 3.2 the source of the instance of in is ; this fact is described by rewriting the process as

DFI prevents this particular instance () of from being written to ; but it allows other instances whose sources are at least as trusted as . The rewriting follows a structural equivalence rule (Struct bind), explained later in the section.

While explicit substitution has been previously used in language implementations, we seem to be the first to adapt this device to track data flow in a concurrent language. In particular, we use explicit substitution both to specify DFI (in Definitions 3.3 and 3.4) and to verify it statically (in proofs of Theorems 5.4 and 5.7). We defer a more detailed discussion on this technique to Section 6.

We call sets of the form substitution environments.

Definition 3.3 (Explicit flows).

A variable flows from a label or lower in a substitution environment , written , if for some and such that either , or is a variable and (inductively) .

In other words, flows from a label or lower if is an instance of a value substituted at or lower. In Definition 3.4 below, we formalize DFI as a property of objects, as follows: an object is protected from label if it never contains instances that flow from or lower. We define to be the set of values in that is an instance of: , and if (inductively) and for some and , then . The operational semantics ensures that substitution environments accurately associate instances of values with their runtime sources.

We now present rules for local reduction, structural equivalence, and global reduction. Reductions are of the form , meaning that “process may reduce to process with label in substitution environment ”. Structural equivalences are of the form , meaning that “process may be rewritten as process ”. The notions of free and bound variables ( and ) are standard. We write if , that is, there is a value that both and are instances of.

We first look at the local reduction rules. In (Reduct evaluate), a substitution binds to the intermediate value and associates with its runtime source . (Reduct new) creates a new store denoted by a fresh variable , initializes the store, and returns ; a substitution binds to the initialization of the new object and associates with its runtime source . The value and the trust annotation in the initialization are used by the type system (Section 4). The remaining local reduction rules describe reactions with a store, following the informal semantics.

Next, we define evaluation contexts [20]. An evaluation context is of the form , and contains a hole of the form ; the context yields a process that executes with label in substitution environment , if the hole is plugged by a process that executes with label in substitution environment . evaluation context hole sequential evaluation fork left fork right explicit substitution lowering of process label Evaluation can proceed sequentially inside processes, and in parallel under forks [24]; it can also proceed under explicit substitutions and lowering of process labels. In particular, note how evaluation contexts build substitution environments from explicit substitutions, and labels from changes of process labels. We denote by the process obtained by plugging the hole in with .

Next, we look at the structural equivalence and global reduction rules. In (Struct bind), is the process obtained from by the usual capture-avoiding substitution of by . The rule states that explicit substitution may invert usual substitution to create instances as required. In particular, variables that appear in packed code can be associated with the label of the process that packs that code, even though those variables may be bound later—by (Reduct evaluate)—when that code is eventually unpacked at some other label. For example, the instance of in may be correctly associated with (the label at which it is packed) instead of (the label at which it is unpacked). Thus, in combination, the rules (Reduct evaluate) and (Struct bind) track precise sources of values by explicit substitution.

By (Struct substitution), substitutions can float across contexts under standard scoping restrictions. By (Struct fork), forked processes can float across contexts [24], but must remain under the same process label. By (Struct store), stores can be shared across further contexts.

Reduction is extended with contexts and structural equivalence in the natural way.

Finally, we formalize DFI in our language, as promised.

Definition 3.4 (Dfi).

The object is protected from label by process if there is no process , substitution environment , and instance such that and .

4 A type system to enforce DFI

We now show a type system to enforce DFI in the language. (The formal protection guarantee for well-typed code appears in Section 5.) We begin by introducing types and typing judgments. We then present typing rules and informally explain their properties. Finally, we consider some examples of typechecking. An efficient algorithm for typechecking is outlined in Appendix B.

4.1 Types and effects

Core typing judgments

(Typ unit)

(Typ variable)

(Typ fork)

(Typ limit)

(Typ evaluate)

(Typ substitute)

(Typ store)

(Typ new)

(Typ pack)

(Typ un/protect)

(Typ write)

(Typ read)

(Typ execute)

The core grammar of types is shown below. Here effects are simply labels; these labels belong to the same ordering as in the operational semantics. type object packed code unit static approximation type and effect

  • The type is given to an object that contains values of type . Such contents may not flow from labels lower than ; in other words, indicates the trust on the contents of this object. DFI follows from the soundness of object types.

  • The type is given to packed code that can be run with label . Values returned by the code must be of type and may not flow from labels lower than . In fact, our type system admits a subtyping rule that allows such code to be run in a typesafe manner with any label that is at most .

  • The effect is given to a value that does not flow from labels lower than .

When creating an object, the programmer declares the trust on the contents of that object. Roughly, an object returned by gets a type . For example, in Examples 3.1 and 3.2, we declare the trust on the contents of and the trust on the contents of .

A typing environment contains typing hypotheses of the form . We assume that any variable has at most one typing hypothesis in , and define as the set of variables that have typing hypotheses in . A typing judgment is of the form , where is the label of the process , is the type and effect of values returned by , and .

4.2 Core typing rules

In the previous page, we present typing rules that enforce the core static discipline required for our protection guarantee. Some of these rules have side conditions that involve a predicate on labels. These conditions, which are marked in \textshade[.91]sharpcornersshaded boxes, are ignored in our first reading of these rules. (The predicate is true everywhere in the absence of a special label , introduced later in the section.) One of the rules has a condition that involves a predicate on expressions; we introduce that predicate in the discussion below. The typing rules preserve several invariants.

  • Code that runs with a label cannot return values that have effects higher than .

  • The contents of an object of type cannot have effects lower than .

  • The dynamic label that protects an object of type cannot be lower than .

  • An object of type cannot be created at a label lower than .

  • Packed code of type must remain well-typed when unpacked at any label lower than .

Invariant (1) follows from our interpretation of effects. To preserve this invariant in (Typ variable), for example, the effect of at is obtained by lowering ’s effect in the typing environment with .

In (Typ store), typechecking is independent of the process label, that is, a store is well-typed if and only if it is so at any process label; recall that by (Struct store) stores can float across contexts, and typing must be preserved by structural equivalence. Further, (Typ store) introduces Invariants (2) and (3). Invariant (2) follows from our interpretation of static trust annotations. To preserve this invariant we require Invariant (3), which ensures that access control prevents code running with labels less trusted than from writing to objects whose contents are trusted at .

By (Typ new), the effect of the initial content of a new object cannot be lower than . Recall that by (Reduct new), the new object is protected with the process label ; since by Invariant (1), we have , so that both Invariants (2) and (3) are preserved. Conversely, if then the process does not typecheck; Invariant (4) follows.

Let us now look carefully at the other rules relevant to Invariants (2) and (3); these rules—combined with access control—are the crux of enforcing DFI. (Typ write) preserves Invariant (2), restricting trusted code from writing values to that may flow from labels lower than . (Such code may not be restricted by access control.) Conversely, access control prevents code with labels lower than from writing to , since by Invariant (3), ’s label is at least as trusted as . (Typ un/protect) preserves Invariant (3), allowing ’s label to be either raised or lowered without falling below . In (Typ read), the effect of a value read from at  is approximated by —the least trusted label from which ’s contents may flow—and further lowered with to preserve Invariant (1).

In (Typ pack), packing code requires work akin to proof-carrying code [39]. Type safety for the code is proved and “carried” in its type , independently of the current process label. Specifically, it is proved that when the packed code is unpacked by a process with label , the value of executing that code has type and effect . In Section 5 we show that such a proof in fact allows the packed code to be unpacked by any process with label , and the type and effect of the value of executing that code can be related to (Invariant (5)). This invariant is key to decidable and efficient typechecking (Appendix B). Of course, code may be packed to run only at specific process labels, by requiring the appropriate label changes.

Preserving Invariant (5) entails, in particular, preserving Invariant (4) at all labels . Since a expression that is not guarded by a change of the process label may be run with any label , that expression must place the least possible trust on the contents of the object it creates. This condition is enforced by predicate :

(Typ execute) relies on Invariant (5); further, it checks that the label at which the code is unpacked () is at most as trusted as the label at which the code may have been packed (approximated by ). This check prevents privilege escalation—code that would perhaps block if run with a lower label cannot be packed to run with a higher label. For example, recall that in Example 3.2, the code is packed at and then copied into . While a -process can legitimately execute (so that the code is typed and is not blocked by access control), it should not run that code by unpacking from . The type system prevents this violation. Let be of type . Then (Typ store) requires that , and (Typ execute) requires that (contradiction).

Because we do not maintain an upper bound on the dynamic label of an executable, we cannot rely on the lowering of the process label in (Reduct execute) to prevent privilege escalation. (While it is possible to extend our type system to maintain such upper bounds, such an extension does not let us typecheck any more correct programs than we already do.) In Section 5, we show that the lowering of the process label can in fact be safely eliminated.

In (Typ evaluate), typing proceeds sequentially, propagating the type and effect of the intermediate process to the continuation. (Typ substitution) is similar, except that the substituted value is typed under the process label recorded in the substitution, rather than under the current process label. In (Typ limit), the continuation is typed under the changed process label. In (Typ fork), the forked process is typed under the current process label.

4.3 Typing rules for stuck code

Stuck typing judgments

(Typ escalate stuck)

(Typ write stuck)

(Typ un/protect stuck)

(Typ subsumption stuck-I)

(Typ subsumption stuck-II)

While the rules above rely on access control for soundness, they do not exploit runtime protection provided by access control to typecheck more programs. For example, the reduced process in Example 3.1 cannot yet be typed, although we have checked that DFI is not violated in . Below we introduce stuck typing to identify processes that provably block by access control at runtime. Stuck typing allows us to soundly type more programs by composition. (The general principle that is followed here is that narrowing the set of possible execution paths improves the precision of the analysis.) This powerful technique of combining static typing and dynamic access control for runtime protection is quite close to hybrid typechecking [21]. We defer a more detailed discussion of this technique to Section 6.

We introduce the static approximation for processes that do not return values, but may have side effects. static approximation code stuck process We now present rules for stuck-typing. As before, in our first reading of these rules we ignore the side conditions in \textshade[.91]sharpcornersshaded boxes (which involve the predicate ). (Typ write stuck) identifies code that tries to write to an object whose static trust annotation is higher than the current process label . By Invariant (3), the label that protects the object must be at least as high as ; thus and the code must block at runtime due to access control. For example, let be of type in Example 3.1. By (Typ write stuck), the code is well-typed since . (Typ un/protect stuck) is similar to (Typ write stuck); it further identifies code that tries to raise the label of an object beyond the current process label. (Typ escalate stuck) identifies code that tries to raise the current process label. All such processes block at runtime due to access control.

By (Typ subsumption stuck-I), processes that are typed under stuck hypotheses are considered stuck as well. For example, this rule combines with (Typ evaluate) to trivially type a continuation if the intermediate process is identified as stuck. Finally, by (Typ subsumption stuck-II), stuck processes can have any type and effect, since they cannot return values.

4.4 Typing rules for untrusted code

Typing must guarantee protection in arbitrary environments. Since the protection guarantee is derived via a type preservation theorem, arbitrary untrusted code needs to be accommodated by the type system. We assume that untrusted code runs with a special label , introduced into the total order by assuming for all . We now present rules that allow arbitrary interpretation of types at .

Typing rules for untrusted code

(Typ subsumption -I)

(Typ subsumption -II)

By (Typ subsumption -I), placing the static trust on the contents of an object amounts to assuming any type for those contents as required. By (Typ subsumption -II), a value that has effect may be assumed to have any type as required. These rules provide the necessary flexibility for typing any untrusted code using the other typing rules. On the other hand, arbitrary subtyping with objects can in general be unsound—we now need to be careful when typing trusted code. For example, consider the code

A -process reads the name of an object () from a