5. Propuesta didáctica
5.2. Planificación y competencias
The two noninterference results that we have proved, Theorems 4.12 and 4.13, reveal an important difference between the security guarantees of the idealized memory-safe abstract machine and those of the micro-policy. Noninterference for the abstract machine guarantees that changing unreachable memory in arbitrary ways has no significant effect on program execution. On the symbolic machine, however, this result only holds for successful executions: if one of the executions leads to an error,
there is nothing interesting we can say about the other. The reason for that is that modifying the amount of allocated memory at the initial execution state can cause a program to run out of memory and terminate execution.
We have seen a similar situation in the information-flow world: the termination-insensitive non- interference property that we proved in Chapter 3 only prevents information leaks if both execu- tions terminate successfully. By analogy, we could call the variant of noninterference satisfied by the memory-safety policyerror-insensitive. Many of the problems discussed in the context of information-flow control carry over to this setting [5]. Our version of error-insensitive noninter- ference can only leak one bit of secret information. However, this could change if outputs can be produced during execution, or in systems like Java, where memory exhaustion does not trigger a fatal error, but rather an exception that can be caught and treated. In any case, we could hope to show that the only information that can be leaked in this way is the total memory consumption of the process, and likely not anything about the contents of memory regions.
4.8 Discussion and Related Work
Memory safety has been one of the main applications for micro-policies and the PUMP. Earlier work presented policies for spatial and temporal protection of the heap [11, 33, 34] (inspired by an approach due to Clause et al. [19]), and other designs for stack protection are underway [91].
There is an extensive literature on hardware and software mechanisms for enforcing memory safety; we overview just a few recent ones that are particularly relevant for the micro-policy pre- sented here. The SAFE platform [25], where the PUMP has its roots, provided memory safety with dedicated hardware fat pointers that compress base, bounds, and offset in 64 bits [73]. The mech- anism obviates the need for marking memory locations and pointers with region identifiers, but it requires metadata to distinguish pointers from other kinds of values and to preserve the integrity of their base and bounds—only the highly privileged memory manager is capable of manipulat- ing this information, similarly to our system services. HardBound [30] provides hardware support for fat pointers by storing base and bounds information in a separate address space. This metadata can be manipulated with special instructions provided by the architecture. These instructions can be executed by regular user code, and pointer integrity is guaranteed by having the compiler use them only at the right places. SoftBound [84] is a subsequent software implementation of a sim-
ilar scheme. CHERI [113] is a RISC architecture with first-class support for capabilities. These capabilities are fat pointers with additional permission metadata that track which operations can be performed through that pointer—for example, read, write, or execute. They are manipulated by spe- cial instructions inserted by the compiler, and stored in conventional memory. To preserve capability integrity, the memory tracks where capabilities are stored, and declares them as invalid as soon as they are modified by conventional instructions. The micro-policy presented here sits somewhere in between hardware and software approaches: it takes advantage of specialized hardware support, but the hardware is much more generic than other mechanisms, and still needs to be customized to enforce memory safety.
Motivated by the critical security implications, many other works have proposed formal charac- terizations of memory safety, and used them to evaluate the security guarantees of mechanisms for enforcing it. These prior proposals mostly focus on guaranteeing that a particular list of erroneous operations never arise during execution. For example, SoftBound [84] guarantees that no memory access occurs out of bounds, while Cyclone [47], a language for safe manual memory management, prevents type errors and references to dangling pointers. In contrast, the characterization presented here enables reasoning about high-level data-isolation properties in programs. Related principles had already been recognized in other works. We have closely analyzed the case of separation logic [90], whose frame rule is similar to our results. However, as argued, separation logic was designed to apply to radically different settings from ours, where safety is established through proof, and where we can rely on little support from the language. By contrast, our characterization leverages the guarantees that are provided in memory-safe settings unconditionally.
There is also an extensive literature on reasoning principles for rich languages featuring isola- tion guarantees similar to the frame rule that apply unconditionally to all programs, such asL3[2] or
LNLu�[70], phrased as systems of logical relations for reasoning about typed programs. However,
these other systems tend to rely on much more expressive linguistic mechanisms than memory safety alone, such as linear type systems, or encapsulation features that control the operations that compo- nents of a program are allowed to perform on objects allocated in memory. Our characterization, on the other hand, is more directly applicable to the lower-level systems where the lack of memory safety is a concern.
We believe our characterization could serve as a criterion to guide possible strengthenings and variations of the micro-policy described here. A clear shortcoming of the current micro-policy is the
isolation guarantees are too strong: once a region of memory becomes unreachable, there is no way the program can ever interact with it again. In practice, we want additional mechanisms for telling when and where a program is allowed to manipulate certain memory regions. For example, in the imperative language of Section 4.5, reachability is relative to the set of local variables mentioned by a program. If some part of the program does not mention some local variable𝑥, then we know that
that part of the program does not interfere with memory regions that can only be reached through𝑥.
Other parts of the program, however, are free to use this otherwise unreachable region. Concretely, we could consider strengthening the memory-safety micro-policy by including a protected stack [91], intra-object bounds checking (as done by SoftBound [84], for example), a basic distinction between code and data, or pointer narrowing for preventing code that receives a pointer from gaining access to the whole memory region it corresponds to. Though these additions would make the policy more complex, we believe that they may not have a huge impact on the proof on noninterference. On a different direction, we could replace the current memory allocator, which initializes every location in a new region to0@D(𝑖,UNINIT), where𝑖is a fresh identifier, to a different allocator that only
sets the region tags toD(𝑖,UNINIT), whereUNINITis a new data tag for uninitialized values. Given
that many policies tag memory sparsely, it is conceivable that there could be architectural support for tagging large regions of memory at once, which could lead to a faster policy [91]. Furthermore, thanks to theUNINITtag, it would still be possible to detect illegal uses of uninitialized memory,
Chapter 5
Concrete Machine
The symbolic machine is a convenient tool for describing security policies for low-level code. Though we can deploy these policies by emulating the symbolic machine in software, the machine was de- signed to be backed by efficient hardware. We now discuss how this can be achieved through a low- levelconcrete machinethat checks and propagates metadata tags by combining a hardware cache with software rules programmed in machine code. We prove that the concrete machine can refine the symbolic machine for an arbitrary choice of its parameters by instantiating it with correct imple- mentations of the transfer function and system services and using the tagging mechanism itself to protect the monitor’s integrity, isolating it from user code.
5.1 Machine State
To user code, there are barely any differences between the concrete machine and its symbolic coun- terpart: both are simple RISC processors with metadata tags accompanying every word. What dis- tinguishes them is how tags are checked and propagated. While the symbolic machine is tailored for specification and reasoning, the concrete machine is closer to a practical processor with pro- grammable tags. Its tags are not structured objects, but bare machine words. Instead of monitoring execution by feeding tags into an abstract transfer function, the concrete machine uses an imple- mentation of the transfer function in machine code to decide whether an instruction is valid or not. To achieve good performance, the transfer function is not actually run on every instruction, but memoized using a hardware cache: after computing the result of the function for some input, this
information is stored on the cache, so that it can be retrieved later without running the function again. This combination allows the concrete machine to retain the flexibility of the symbolic one without giving up on much performance.
Definition 5.1. A state of the concrete machine is a tuple of the form(𝑚, 𝑟𝑠, 𝑝𝑐, 𝑝𝑐u�, 𝑐𝑎𝑐ℎ𝑒), where
• 𝑚 ∈Word⇀Word×Wordis thememory;
• 𝑟𝑠 ∈Reg⇀Word×Wordis theregister bank;
• 𝑝𝑐 ∈Word×Wordis thePC;
• 𝑝𝑐u� ∈Word×Wordis thefaulting PC; and
• 𝑐𝑎𝑐ℎ𝑒 ∈IVec⇀OVecis therule cache.
The setsIVecandOVeccontaininput and output tag vectors. Aninput vectoris a tuple of the form
(𝑜, 𝑡u�u�, 𝑡u�, 𝑡u�1, 𝑡u�2, 𝑡u�), where𝑜is an opcode, and the other elements are words. Anoutput vector
is a pair of words(𝑡u�u�, 𝑡u�u�u�).
Since tags are full-sized words, there is a wide range of meanings that we can ascribe to them, from records of smaller bit vectors to pointers to complex data structures stored in memory. There is no arbitrary limit on how many input-output lines can be stored in the rule cache. This contrasts with the typical size constraints faced by real hardware, which might implement the rule cache with a much smaller (for instance, 512-line) near-associative hash table [32]. Since transfer functions are pure, we can use the concrete machine to implement most micro-policies without the need for evicting lines from the cache. Note that this may no longer hold if policies need to change the meaning of tags during execution for performance reasons—for example, to rearrange the memory data structures they point to.
In addition to the usual machine registers, there is a special-purpose register𝑝𝑐u�that saves the
value of the PC on a cache miss, so that control can resume at the faulting point later. The concrete machine omits the extra state component of the symbolic machine, which must now be interpreted as data structures in memory manipulated by regular machine code.
Format Description
AddRule Add line to rule cache
JumpEpc Transfer control to faulting instruction
GetTag𝑟u�𝑟u� Extract tag from word
PutTag𝑟u�𝑟u�u�u�𝑟u� Pack payload and tag into an atom
Figure 5.1: New instructions for concrete machine. From now on, any mentions to the setsInstrand
Opcodeimplicitly include these new instructions (including Definition 5.1).