MemSafe: ensuring the spatial and temporal memory safety of C at runtime

Authors


  • An earlier version [1] of this paper was presented at the 10th IEEE International Working Conference on Source Code Analysis and Manipulation (SCAM), Timişoara, Romania, September 12–13, 2010.

Matthew S. Simpson, 1431 A. V. Williams Building, Department of Electrical & Computer Engineering, University of Maryland, College Park, MD 20742-3256, U.S.A.

E-mail: simpsom@umd.edu

SUMMARY

Memory access violations are a leading source of unreliability in C programs. As evidence of this problem, a variety of methods exist that retrofit C with software checks to detect memory errors at runtime. However, these methods generally suffer from one or more drawbacks including the inability to detect all errors, the use of incompatible metadata, the need for manual code modifications, and high runtime overheads. This paper presents a compiler analysis and transformation for ensuring the memory safety of C called MemSafe. MemSafe makes several novel contributions that improve upon previous work and lower the cost of safety. These include (i) a method for modeling temporal errors as spatial errors, (ii) a metadata representation that combines features of both object-based and pointer-based approaches, and (iii) a dataflow representation that simplifies optimizations for removing unneeded checks. MemSafe is capable of detecting real errors with lower overheads than previous efforts. Experimental results show that MemSafe detects all memory errors in six programs with known violations as well as two large and widely used open source applications. Finally, MemSafe ensures complete safety with an average overhead of 88% on 30 programs commonly used for evaluating the performance of error detection tools. Copyright © 2012 John Wiley & Sons, Ltd.

1 INTRODUCTION

Use of the C programming language remains common despite the well-known memory errors it allows. The features that make C a desirable language for many system-level programming tasks—namely its weak typing, low-level access to computer memory and pointers—are the same features whose misuse cause the variety of difficult-to-detect memory access violations common among C programs. Although these violations often cause a program to crash immediately, their symptoms can frequently go undetected long after they occur, resulting in data corruption and incorrect results while making software testing and debugging a particularly onerous task.

A commonly cited memory error is the buffer overflow, where data is stored to a memory location outside the bounds of the buffer allocated to hold it. Although buffer overflow errors have been understood as early as 1972 [2], they and other memory access violations still plague modern software and are a major source of recently reported security vulnerabilities. For example, according to the United States Computer Emergency Readiness Team, 67 (29%) of the 228 vulnerability notes released in 2008–2009 were due to buffer overflow errors alone [3].

Several safety methods [4-7] have characterized memory access violations as either spatial or temporal errors. A spatial error is a violation caused by dereferencing a pointer that refers to an address outside the bounds of its ‘referent’. Examples include indexing beyond the bounds of an array, dereferencing pointers obtained from invalid pointer arithmetic and dereferencing uninitialized, NULL or ‘manufactured’ pointers. § A temporal error is a violation caused by using a pointer whose referent has been deallocated (e.g., by calling the free standard library function) and is no longer a valid memory object. The most well-known temporal violations include dereferencing ‘dangling’ pointers to dynamically allocated memory and attempting to deallocate a pointer more than once. However, dereferencing pointers to automatically allocated memory (i.e., stack variables) is also a concern if the address of the referent ‘escapes’ and is made available outside the function in which it was defined. A program is memory safe if it does not commit any spatial or temporal errors.

Safe languages, such as Java, ensure memory safety through a combination of syntax restrictions and runtime checks and are widely used when security is a major concern. Others, such as Cyclone [8] and Deputy [9], preserve many of the low-level features of C but require additional programmer annotations to assist in ensuring safety. Although the use of these languages may be ideal for safety-critical environments, the reality is that many of today's applications—including operating systems, web browsers, and database management systems—are still typically implemented in C or C++ because of its efficiency, predictability, and access to low-level features. This trend will likely continue into the future.

As an alternative to safe languages, sophisticated static analysis methods for C [10-14] can be used alone, or in conjunction with other systems, to ensure the partial absence of spatial and temporal errors statically. Although these techniques are invaluable for software verification and debugging, they can rarely prove the absence of all memory errors and often require a significant amount of verification time because of the precision of their analyses.

A growing number of methods rely primarily on inserted runtime checks to detect memory access violations dynamically. However, the methods capable of detecting both spatial and temporal memory safety violations [4-6, 15-23] generally suffer from one or more practical drawbacks that have thus far limited their widespread adoption. These drawbacks can be summarized by the following qualities.

  • Completeness. Methods that associate metadata (the base and bound information required for runtime checks) with objects [15-21]—rather than the pointers to these objects—generally do not detect two kinds of memory errors. First, because C supports the allocation of nested objects (e.g., an array of structures), spatial errors involving sub-object overflows are not detected because inner objects share metadata with the outer object. Second, if the system allocates an object to a previously deallocated location, temporal errors are not detected because dangling pointers to the deallocated object may still refer to a location within bounds of the newly allocated object.
  • Compatibility. The use of alternate pointer representations, such as multiword ‘fat-pointers’ [4, 22] to store metadata raises compatibility concerns. Inline metadata breaks many legacy programs—and requires implicit language restrictions for new ones—because it changes the memory layout of pointers. For example, because their data types are the same size, programmers can cast pointers to integers to compute certain addresses. However, because fat-pointers alter memory layout, casting a single-word integer to a multiword pointer, or casting a fat-pointer of one type to a pointer of another type, is no longer valid and can result in data corruption. Inline metadata also breaks the calling convention of external libraries whose parameters or return types involve pointers.
  • Code Modifications. Some methods [22] require non-trivial source code modifications to avoid the aforementioned compatibility issues or to prevent an explosion in runtime. A common example is for a programmer to write ‘wrapper functions’ that remove inline metadata to interface with external libraries.
  • Cost. Methods capable of detecting both spatial and temporal errors often suffer from high performance overheads [4-6, 23]. This is commonly due to the cost of maintaining the metadata required for ensuring spatial safety and the use of conservative garbage collection for ensuring temporal safety. High runtime overhead can make a method prohibitively expensive for deployment and can slow the development process when it is used for testing, especially if a program is to be executed many times to increase coverage.

This paper introduces MemSafe [1], a method for ensuring both the spatial and temporal memory safety of C programs at runtime. MemSafe is a whole-program compiler analysis and transformation that, like other runtime methods, utilizes a limited amount of static analysis to prove memory safety whenever possible and then inserts checks to ensure the safety of the remaining memory accesses at runtime. MemSafe is complete, compatible, requires no code modifications, and generally has lower runtime cost than other complete and automatic methods achieving the same level of safety. MemSafe makes the following contributions for lowering the runtime cost of dynamically ensuring memory safety:

  • MemSafe uniformly handles all memory violations by modeling temporal errors as spatial errors. Therefore, the use of separate mechanisms for detecting temporal errors (e.g., garbage collection or explicit checks for temporal safety [4-6, 21]) is no longer required.
  • MemSafe captures the most salient features of object and pointer metadata in a hybrid spatial metadata representation. MemSafe's handling of pointer metadata is similar to that of SoftBound [7], a previous technique for detecting spatial errors and ensures MemSafe's completeness and compatibility. However, MemSafe's additional use of object metadata creates a novel synergy with pointer metadata that allows the detection of temporal errors as well.
  • MemSafe uniformly handles pointer dataflow in a representation that simplifies several performance-enhancing optimizations. Unlike previous methods that require checks for all dereferences and the expensive propagation of metadata at every pointer assignment [4-6, 23], MemSafe eliminates redundant checks and the propagation of unused metadata. This capability is further enhanced with whole-program analysis.

To achieve the aforementioned contributions, MemSafe exploits several key insights related to the flow of pointer values in a program. The following program behavior models form the foundation of MemSafe's approach.

  1. Memory deallocation can be modeled as an assignment. For example, the statement free(p) can be represented by the statement p=invalid, where invalid is a special untyped pointer to a temporally ‘invalid’ range of memory.

    This insight is useful because it enables spatial safety mechanisms to be reused to ensure temporal safety. To detect spatial safety violations, existing methods insert before pointer dereferences runtime checks that determine whether the pointers refer to a location within the base and bound addresses of their referents. If a dereferenced pointer refers to a location outside the region of memory occupied by its referent, a spatial safety violation is signaled. By assigning pointers to deallocated memory to be equal to the invalid pointer, they inherit the base and bound addresses of the ‘invalid’ region of memory. If the base and bound addresses of this region are defined such that they represent some impossible address range (e.g., a block with a negative size), any legal pointer must refer to a location outside this range. Thus, dereferences of dangling pointers and multiple deallocation attempts can then be detected with the inserted checks for spatial safety.

  2. Indirect pointer assignments can be modeled as explicit assignments. Statements of the form ptr1=*p, where both ptr1 and p are pointers, make low-cost memory safety difficult to achieve because ptr1's set of potential referents is not known statically. Alias analysis can be used to narrow this set, and MemSafe makes the results of this analysis explicit in the program's static single assignment (SSA) [24] form by using a new ϕ-like construct called the ϱ-function.

    For example, assume that the statement s0:*p=ptr0 is the only direct reaching definition of a pointer defined as s1:ptr1=*p. The statement s2:*q=ptr2 may indirectly redefine ptr1 if p and q may alias and control flow may reach statement s1 from s2. Therefore, MemSafe models the statement ptr1=*pas ptr1= ϱ(ptr0,ptr2), meaning the value of ptr1 may equal that of ptr0 or ptr2 but only these two values.

    This insight is useful because it enables MemSafe to construct a convenient dataflow graph that codifies both direct and indirect pointer assignments—in addition to memory deallocation with the insight mentioned previously—as simple definition and use relationships. Thus, this representation greatly simplifies optimizations for reducing the cost of achieving memory safety.

A prototype implementation of MemSafe has been evaluated in terms of its completeness and runtime cost. MemSafe was able to successfully detect known memory violations in multiple versions of the Apache HTTP server [25] and the GNU Core Utilities [26] software package. Additionally, MemSafe detected all previously reported memory errors in six programs from the BugBench [27] benchmark suite. In terms of cost, MemSafe's average overhead was 88% on 30 large programs widely used in evaluating error detection tools. Finally, as evidence of its compatibility, MemSafe compiled each of the aforementioned programs without requiring any code modifications or programmer intervention.

Table 1 summarizes previous software approaches for ensuring both spatial and temporal safety. Each method is evaluated on its completeness, compatibility, lack of code modifications, use of whole-program analysis, and runtime cost. For consistency, slowdown is reported for the Olden benchmarks [28] where results are available. MemSafe compares favorably in each category and has the lowest overhead among all existing complete and automatic methods. This result is primarily due to MemSafe's novel contributions based on the aforementioned insights.

Table 1. Related work.
ApproachCompleteCompatibleNo code modificationsWhole programSlowdown
  • A comparison of methods providing both spatial and temporal memory safety is given. Slowdown is computed as the ratio of the execution time of the instrumented program to that of the original program. Slowdown is reported for the Olden benchmarks [28] unless otherwise noted.

  • *

    Checks are only inserted for heap objects.

  • Slowdown is the average of all results reported by the authors.

  • Checks are only inserted for store operations.

Purify [20]NoYesYesYes148.44*
Patil, Fischer [5]YesYesYesNo6.38
Safe C [4]YesNoYesNo4.88
Fail-Safe C [23]YesYesYesNo4.64
MSCC [6]YesYesYesNo2.33
Yong, Horwitz [21]NoYesYesNo1.37
CCured [22]YesNoNoYes1.30
 
MemSafeYesYesYesYes1.29

Because MemSafe's performance overheads cannot necessarily be considered ‘low’, MemSafe is deployable in systems whose primary concern is memory safety. In practice, it has been observed that many runtime checks can be avoided with MemSafe's simple optimizations and that for safety-critical applications, MemSafe's moderate runtime overheads can be an acceptable trade-off compared with redesigning systems in a safe language. However, for performance-critical applications, MemSafe is primarily useful as a dynamic bug detection tool.

2 BACKGROUND

Memory safety violations can be divided into two categories: violations of spatial safety and violations of temporal safety. A spatial safety violation is an error in which a pointer is used to access the data at a location in memory that is outside the bounds of an allocated object. The error is ‘spatial’ in the sense that the dereferenced pointer refers to an incorrect location in memory. A temporal safety violation is an error in which a pointer is used in an attempt to access or deallocate an object that has already been deallocated. The violation is ‘temporal’ in the sense that the pointer use occurs at an invalid instance during the execution of the program (i.e., after the object to which it refers has been deallocated). Table 2 lists spatial and temporal memory safety violations that MemSafe detects and gives examples of each.

Table 2. Memory safety violations.Thumbnail image of
  • Example code fragments demonstrating memory safety violations are presented and grouped by whether they affect aspects of spatial or temporal safety.

  • As evidence of the significance of these memory safety violations, the instrumentation of C programs to ensure memory safety remains an actively researched topic. The remainder of this section reviews previous approaches for detecting some or all spatial and temporal safety violations, primarily focusing on the prior works’ use of metadata. In the context of enforcing memory safety, metadata refers to the creation of additional data for describing the spatial or temporal properties of an object or pointer. For example, metadata often consists of the base and bound addresses that indicate the valid address range to which a pointer may refer.

    2.1 Spatial safety

    The goal of spatial safety is to ensure that every memory access occurs within the bounds of a known object. Spatial safety is typically enforced by inserting runtime checks before pointer dereferences. Alternatively, checking for bounds violations after pointer arithmetic is also possible [15-17, 33] but requires care because pointers in C are allowed to be out of bounds so long as they are not dereferenced. The metadata required for spatial safety checks can be associated either with objects or pointers, and there are strengths and weaknesses of each approach.

    Object metadata

    Methods that utilize object metadata usually record the base and bound addresses of objects, as they are allocated, in a global database that relates every address in an allocated region to the metadata of its corresponding object. Advantages of this approach include efficiency, because it avoids the propagation of metadata at every pointer assignment (see the discussion of pointer metadata discussed in the succeeding texts), and compatibility, because it does not change the layout of objects in memory or prohibit the use of pre-compiled libraries. Prominent methods employing this strategy include the work by Jones and Kelly [15], Ruwase and Lam [16], Dhurjati and Adve [17], Akritidis et al. [33], SafeCode [32], and SVA [19].

    However, the use of object metadata as means of enforcing spatial safety results in several drawbacks. First, this approach prevents complete spatial safety. Because nested objects (e.g., an array of structures) are assigned base and bound addresses that span the entire allocated region, it is impossible to detect sub-object overflows if an out-of-bounds pointer to an inner object remains within bounds of the outer object. Second, this approach requires a runtime lookup operation for retrieving metadata from the object database. Dhurjati and Adve [17] improve the runtime cost associated with this lookup operation by partitioning the object database using automatic pool allocation [34], and Akritidis et al. [33] improve runtime by constraining the size and alignment of allocated objects. However, these methods do not detect sub-object overflows or temporal errors.

    Figure 1 depicts the utility of object metadata (shown in dark gray) in enforcing memory safety. To ensure memory safety, complete spatial and temporal safety must be enforced. Because all pointer dereferences are either object-level references or sub-object references, it follows that all object and sub-object references must be both spatially and temporally safe for a program to be memory safe. However, object-level base and bound information is only useful in enforcing object-level spatial safety because sub-objects must share metadata with their corresponding outer objects. || Figure 1 will be referenced again when describing the remaining prior enforcement strategies.

    Figure 1.

    Prior enforcement methods. The use of spatial metadata, garbage collection, and temporal capabilities is shown for previous methods of enforcing memory safety.

    Pointer metadata

    An alternative to using object metadata for enforcing spatial safety is to associate metadata with individual pointers. When a new pointer is created (i.e., with malloc or the address-of operator), its metadata is initialized to be the base and bound addresses of its referent; when a pointer definition uses the value of another pointer (e.g., pointer arithmetic), its metadata is inherited from the original pointer. Advantages of this approach include avoiding costly database lookups and the ability to ensure complete safety because sub-object overflows can be detected by assigning each pointer a unique base and bound addresses.

    Pointer metadata is commonly implemented using multiword blocks of memory, called ‘fat-pointers’, that record the required base and bound information inline with pointers. Each pointer in a program essentially becomes a struct containing three fields: the original pointer value and the base and bound addresses of its referent. Prominent methods employing this strategy include Safe C [4], Fail-Safe C [23], and CCured [22]. However, the use of inline metadata is not always compatible and breaks many programs. Because a pointer's size is no longer equal to the word size of the target architecture, many programming idioms no longer work as expected. Additionally, interfacing with external libraries becomes difficult and requires wrapper functions to pack and unpack fat-pointers at boundaries with uninstrumented code.

    Several pointer-based methods have developed approaches that avoid some of the compatibility issues of fat-pointers. CCured [22], MSCC [6], and Patil and Fischer [5] record metadata in disjoint structures that mirror the shape of the underlying data, but maintaining this representation increases runtime. Fail-Safe C [23] combines fat-pointers with fat integers and virtual structure offsets, but this too increases cost. Finally, Softbound [7] maintains metadata for in-memory pointers in an efficient global lookup table, but this method only detects spatial violations.

    Another disadvantage of the use of pointer metadata as a means of enforcing spatial safety is its runtime cost. Although it avoids the need for expensive database lookups operations, metadata must instead be propagated at every pointer assignment. CCured [22] reduces metadata propagation by using a type system to infer pointer usage. CCured classifies pointers as SAFE, SEQ, and WILD and optimizes the inserted checks and code for propagating metadata for each pointer kind. However, CCured requires manual code modifications to avoid the expensive bookkeeping of WILD pointers and to correct the compatibility issues of fat-pointers.

    Figure 1 depicts the utility of pointer metadata (shown in medium gray) in enforcing memory safety. Because individual pointers can be associated with unique base and bound addresses, pointer metadata can be used to enforce complete object-level and sub-object spatial safety. However, prior methods are not capable of utilizing pointer metadata in enforcing temporal safety.

    MemSafe's approach

    MemSafe's use of metadata as a means for ensuring spatial safety avoids the drawbacks of the aforementioned approaches. MemSafe captures the most salient features of object and pointer metadata in a hybrid representation. To ensure complete and compatible spatial safety, MemSafe maintains disjoint pointer-based metadata in an approach similar to that of SoftBound. However, to lower runtime cost, MemSafe models temporal errors as spatial errors and propagates pointer-based metadata only when it is needed for performing runtime checks. Additionally, MemSafe maintains some object-based metadata in a global database but performs lookup operations only when MemSafe's pointer-based metadata is insufficient for ensuring temporal safety.

    2.2 Temporal safety

    The goal of enforcing temporal safety is to ensure that every memory access refers to an object that has not been deallocated. As summarized in Table 2, temporal safety violations occur when dereferencing pointers to stack objects, if the function in which they were defined has exited, and when dereferencing pointers to heap objects, if the object to which they refer has been deallocated with free. Temporal safety is typically enforced with garbage collection or by software checks. Like the methods for ensuring spatial safety, there are strengths and weaknesses of each approach.

    Garbage collection

    Methods using garbage collection to prevent dangling pointers to heap objects commonly ignore calls to the free function and replace calls to malloc with the Boehm– Demers–Weiser conservative garbage collector [35]. To prevent dangling pointers to stack objects, local variables can be ‘heapified’ and replaced with dynamically allocated objects that are managed by the garbage collector. This is the approach taken by CCured [22] and Fail-Safe C [23].

    However, garbage collection negates several of C's primary benefits, including its predictability and low-level access to memory. Garbage collection voids real-time guarantees [36], increases address space requirements, reduces reference locality, and increases page fault and cache miss rates [37]. Moreover, because the collector must be conservative, some memory may never be reclaimed by the system, resulting in memory leaks. Finally, heapifying stack objects increases the runtime overhead of enforcing temporal safety because dynamic allocation is slower than automatic allocation.

    Despite these drawbacks, conservative garbage collection is capable of enforcing complete temporal safety. This capability is depicted in Figure 1, where the use of garbage collection is shown in light gray.

    Temporal checks

    An alternative to using garbage collection for enforcing temporal safety is to insert explicit software checks that test the temporal validity of referenced objects. To achieve this, a ‘capability store’ is commonly used to record the temporal capability of objects as they are created and destroyed. Additional temporal metadata that is created and propagated with spatial metadata links a pointer to the temporal capability of its referent. Methods employing this strategy include Safe C [4], MSCC [6], and the work by Patil and Fischer [5] and Yong and Horwitz [21].

    There are advantages and disadvantages of using explicit temporal checks for enforcing temporal safety. The primary strength of this approach is that it retains C's memory allocation model and avoids the drawbacks associated with garbage collection. However, the inclusion of additional runtime checks and metadata significantly increases the runtime overhead beyond that of enforcing spatial safety alone. Figure 1 indicates that temporal capabilities (shown in light gray), like garbage collection, can be used to enforce complete temporal safety.

    MemSafe's approach

    One of MemSafe's main contributions is the modeling of temporal errors as spatial errors. Therefore, MemSafe does not require conservative garbage collection or explicit temporal checks, and it avoids the drawbacks of both approaches. Instead, MemSafe relies on spatial safety checks and the hybrid metadata representation mentioned previously for ensuring temporal safety.

    3 MEMSAFE

    This section describes MemSafe's unoptimized approach for ensuring the memory safety of C programs. MemSafe is a compiler analysis and transformation that inserts software checks before memory accesses to detect spatial and temporal violations. It requires a limited amount of static analysis (a flow-insensitive and context-insensitive alias analysis) to avoid unnecessary checks and metadata propagation for memory accesses that it can statically verify to be safe.

    3.1 Language extensions and assumptions

    Because the C programming language, in its entirety, is both large and complex, the language defined in Figure 2 will be used for describing MemSafe's source code translations and the rules for runtime check insertion and metadata propagation. Figure 2 defines a small SSA [24] intermediate language that captures all the relevant pointer-related portions of C. Features of the language include, among others, syntax for pointer types, manual memory management, type-casting of pointer values, pointer arithmetic, and complex control flow.

    Figure 2.

    Syntax for a simple static single assignment [24] language with procedures, pointers, control flow, and manual memory management.

    Without loss of generality, the following assumptions are made of the language presented in Figure 2. First, it is assumed that memory is only accessed with explicit load (e.g., x=*ptr) and store (e.g., *ptr=x) operations involving pointers. Second, it is assumed that pointer values are only created with the address-of operator (&) or by calling the malloc function. Recall that in C, a variable declared as an array of some particular type can act as a pointer to that type and, when used by itself, is a pointer that points to the first element of the array. To enforce the notion that pointers are only created through the two mechanisms mentioned previously, all array accesses are represented as an indexing operation applied to the address of the first element of the array. For example, for the allocation of an array a of 10 elements, an access of the fifth element a[4] is represented as (&a[0])[4]. That is, a pointer is created to the first element of the array, and then this pointer is used to compute the address of the fifth element. In this way, all new pointer values may only be created with the address-of operator and by calling the malloc system function.

    Furthermore, MemSafe assumes that all global variable definitions define a symbol that provides the address of an object instead of the actual object ‘contents’. Because assignments of global variables must be conservatively accounted for in SSA, compiler intermediate representations (e.g., LLVM, [38]) often represent global variables as pointers to statically allocated regions of memory. The advantage of this approach is that within a procedure, a global variable can be loaded from memory, renamed according to the SSA conversion algorithm, and then stored back to memory before control flow reaches another procedure. Therefore, MemSafe identifies statically allocated objects by their location in memory. Note that MemSafe's representation of global variables is analogous to the discussion of arrays mentioned previously in that the declaration of an object implicitly creates a pointer to that object.

    MemSafe models both memory deallocation and pointer store operations as explicit assignments using syntax extensions to this C-like SSA language. The advantage of this approach is that it enables MemSafe to ensure complete memory safety by reasoning solely about pointer definitions, which eliminates the need for separate mechanisms for detecting spatial and temporal errors and reveals optimization opportunities. The remainder of this section describes these abstractions.

    3.2 Memory deallocation

    Memory deallocation can implicitly change the object to which a pointer refers. If the region of memory that was occupied by a deallocated object is ever reallocated, the contents of the region may change, and any remaining pointers to the original object implicitly become invalid. This implicit redefinition of pointers can be made apparent by modeling both automatic and dynamic memory deallocation as an explicit pointer assignment. For example, MemSafe models the statement free(p) as p=invalid, where invalid is a special untyped pointer constant that points to an ‘invalid’ region of memory. The base and bound addresses associated with this abstract memory region are defined by the impossible address range [1,0]. Thus, if the spatial metadata of p is located at address addrp in memory, then p could be associated with the base and bound of the invalid pointer by the statements addr_p->base=1 and addr_p->bound=0. Because the size of this block is −1, spatial safety checks involving the base and bound of the invalid pointer are guaranteed to always report a safety violation. Therefore, temporal safety violations can be detected with runtime checks inserted for enforcing spatial safety. The rules for inserting assignments of the invalid pointer are given next.

    Automatic memory deallocation

    If the address of a stack-allocated object is taken with the address-of operator (&), the pointer to this object may ‘escape’ and be made available outside the function in which the object is allocated. Such an occurrence is possible, for example, if a local variable's address is stored in a global or heap variable. Although this is a legal operation in the C programming language, a common consequence of escaping pointers is the program committing a temporal safety violation. When a function exits, its local variables are automatically deallocated, and any escaping pointers to these deallocated objects become dangling. To make the implicit redefinition of these pointers explicit, MemSafe inserts assignments of the invalid pointer at the end of a procedure for each of its local variables whose address is taken, as shown in the following example.

    image

    In this example, the nested structure s is allocated automatically on the stack as a local variable of function f. In line 4, the address of s is taken and stored in pointer p. It is assumed that p may escape to another procedure and result in a dangling pointer when function f exits. Therefore, MemSafe assigns p the value of the invalid pointer before the function exits, indicating that the pointer now refers to a temporally ‘invalid’ region of memory. After this assignment, the base and bound of p would be updated to be equal to that of the invalid pointer, and any pointer derived from or aliased with p would inherit this metadata as well (see Section 3.5).

    Dynamic memory deallocation

    If a pointer's referent is deallocated dynamically by a program calling the free function, all pointers that refer to this object become dangling pointers. The subsequent dereference of a dangling pointer results in a temporal safety violation. To make the redefinition of these pointers explicit, MemSafe inserts assignments of the invalid pointer after calls to free for the pointer used in deallocating the object, as shown in the following example.

    image

    In this example, an object of size bytes is dynamically allocated by a program with the malloc function, and the base address of this object is assigned to pointer p. In line 5, the object to which p refers is deallocated by the program with the free function. Therefore, MemSafe assigns p the value of the invalid pointer to indicate that the pointer now refers to a temporally ‘invalid’ region of memory.

    3.3 Pointer stores

    Having inserted assignments of the invalid pointer to make the redefinition of pointers to deallocated memory explicit, MemSafe then transforms the program to also make indirect pointer store operations explicit assignments.

    Indirect assignments are problematic in SSA form and make the representation of pointer stores nonintuitive. A key property of SSA is that each assignment is given a unique name (hence, the ‘single assignment’ condition of SSA). However, this property does not hold for in-memory assignments of the form *p=x. In this case, *p is not given a unique name when it is assigned the value of x, and it is unclear what values loaded from memory can be equal to x.

    To address this problem for the indirect assignment of pointer values, MemSafe models in-memory pointer assignments (including those induced for the invalid pointer) as explicit assignments using alias analysis and a ϕ-like SSA extension called the ϱ-function. In the same way that the ϕ-function of SSA is used to resolve control flow uncertainty, thereby giving a unique name to conditional assignments at the point where control-flow paths merge, MemSafe uses the ϱ-function to resolve the dataflow uncertainty of pointer values, thereby giving a unique name to indirect pointer assignments at the point where pointers are loaded from memory.

    For example, assume that the statement s0:*p=ptr0 is the only direct reaching definition of a pointer defined as s1:ptr1=*p. The statement s2:*q=ptr2 may indirectly redefine ptr1 if p and q may alias and control flow may reach statement s1 from s2. Therefore, MemSafe models ptr1=*pas ptr1= ϱ(ptr0,ptr2), meaning the value of ptr1 may equal that of ptr0 or ptr2 but only these two values. In this way, all indirect pointer assignments and object deallocations are represented as direct assignments of the pointers that are potentially modified.

    The following code fragment provides a more concrete example of the ϱ-function and demonstrates how this syntax extension creates a synergy with MemSafe's representation of memory deallocation that results in the propagation of the dataflow associated with the invalid pointer.

    image

    In this example, all pointer values exist in memory, and pointer assignments are made possible by pointer store and load operations. After the call to the free function in line 7, an in-memory assignment of the invalid pointer is inserted to indicate that the referent of *p has been deallocated. Because p and q are assumed to potentially alias, this store operation and the ones in lines 3 and 5 may define the pointer loaded and assigned to the variable c0 in line 9. Therefore, MemSafe resolves this uncertainty in pointer dataflow and gives a unique name to these assignments by inserting the ϱ-function after line 9 and assigning it to the variable c1. Pointers a, b, and invalid are added to the ϱ-function assigned to pointer c1, meaning c1 may be equal to any of these three values but only these values. All subsequent uses of pointer c0 are replaced with uses of c1.

    By default, MemSafe utilizes flow-insensitive and context-insensitive pointer alias information to determine the arguments of ϱ-functions. However, MemSafe is capable of using more precise alias analyses; in general, their use results in ϱ-functions with smaller arity. In the case of the former, MemSafe performs a simple reachability analysis to improve the results of alias analysis. For example, consider the pointer store operation *ptr1=p0 and the pointer load operation p1=*ptr2. If alias analysis indicates that the pointers ptr1 and ptr2 may alias, p0 would be added to the ϱ-function inserted for p1. However, if there is no control-flow path from the store operation to the load operation, this is unnecessary because there is no program execution in which the stored value can modify the loaded value. MemSafe does not include stored pointers in the ϱ-functions inserted for loaded pointers if the store cannot reach the load. Note that MemSafe's reachability analysis does not result in a flow-sensitive alias analysis.

    3.4 The required checks and metadata

    After inserting code for modeling memory deallocation and pointer stores as explicit assignments, MemSafe then inserts the runtime checks and metadata necessary for enforcing memory safety. This section describes the pointer-based and object-based checks and metadata that MemSafe requires.

    Figure 3 depicts MemSafe's unique combination of object-based and pointer-based metadata. In contrast to the enforcement methods shown in Figure 1, MemSafe utilizes this hybrid metadata representation for ensuring both the spatial and temporal memory safety of C programs.

    Figure 3.

    Hybrid metadata representation. The use of object and pointer spatial metadata is shown for MemSafe's method of enforcing memory safety.

    The differences between Figures 1 and 3 can be explained as follows. First, because MemSafe models temporal errors as spatial errors, MemSafe avoids the drawbacks associated with the use of conservative garbage collection and the use of additional checks and metadata for enforcing temporal safety (shown in light gray in Figure 1). Second, because MemSafe's hybrid metadata representation captures the most salient features of object and pointer metadata, MemSafe avoids the drawbacks associated with the use of each in enforcing spatial safety and gains the ability to reuse this metadata for enforcing temporal safety as well. As shown in Figure 3, MemSafe utilizes pointer-based metadata for enforcing complete spatial and partial temporal safety, and MemSafe utilizes object-based metadata for enforcing complete temporal and partial spatial safety. Pointer bounds check (PBC) and object bounds check (OBC) are MemSafe's runtime checks that utilize this metadata. These checks, in addition to MemSafe's object-based and pointer-based metadata, are discussed next.

    Pointer metadata

    For the definition of a new pointer p (i.e., a pointer created with malloc or the address-of operator), MemSafe creates pointer metadata in the form of a 3-tuple 〈base,bound,idp of intermediate values. Together, basep and boundp indicate the range [base,bound) of memory p is permitted to access. idp is a unique key that is assigned to p's referent object, and it is used to associate p with the metadata of its referent (discussed in Section 3.4). MemSafe maintains pointer metadata in memory and allocates at runtime an address addrp from a set of unused addresses A for storing 〈base,bound,idp. These values are stored to memory with an explicit dereference operation, represented by M[addrp] ← 〈base,bound,idp, where M[addrp] holds the value at address addrp in memory.

    In addition to the pointer metadata described previously, MemSafe also creates a tuple 〈addr,idp of intermediate values. These values are created for the definition of each pointer p in a program (i.e., not just those pointers created with malloc or the address-of operator) and are statically named such that there is a known compile-time association with p. Unlike the metadata described previously, no dereference is required at runtime for retrieving these values. As previously described, addrp is the location in memory containing the base and bound addresses that indicate the range of memory p is permitted to access. Finally, to allow the reuse of location addrp (discussed later), a copy of the id associated with p's referent is also maintained with this statically associated tuple.

    Pointer bounds check

    MemSafe utilizes pointer metadata for performing a PBC. MemSafe inserts a PBC before each pointer dereference that cannot be verified to be safe statically (see Section 4.2 for optimizations that reason about dereferences that must be safe). PBC is the forcibly inlined procedure defined by

    image

    In this check, ptr, baseptr, and boundptr are all pointers to the type unsigned char, and size is the size in bytes (as indicated by the sizeof operator) of ptr's referent. ** MemSafe utilizes the pointer metadata of a pointer ptr to ensure the safety of its dereference at runtime:

    image

    In this example, MemSafe will abort the program and report a violation of memory safety (by calling signal_safety_violation) if the dereference *ptr will access a location outside the range specified by [baseptr,boundptr). Because the PBC only utilizes pointer metadata, no costly database lookup is required to retrieve baseptr and boundptr, as 〈addr,idptr are uniquely named symbols in the inserted code.

    As depicted in Figure 3, the PBC and MemSafe's pointer-based metadata are capable of ensuring not only complete spatial safety but also temporal safety with a single check. Whenever a pointer p is assigned the value of the invalid pointer, its pointer metadata is updated as M[addrp] ← 〈base,bound,idinvalid, which will always cause the PBC to signal a safety violation because the invalid pointer refers to an impossible address range.

    As mentioned previously, because addresses in A can be reused, a copy of the id associated with a pointer p must be included with p's pointer metadata. Whenever the metadata associated with the invalid pointer is stored to a particular address, this address is marked for potential reuse. Thus, addrp may be reused for storing the pointer metadata of another pointer if p's referent is deallocated. To ensure that addrp has not been reused, the PBC checks whether the id associated with the dereferenced pointer p is equal to the id located at addrp. If it is not, addrp has been reused for the pointer metadata of another pointer, and p's referent is temporally invalid.

    However, the PBC is insufficient for ensuring complete temporal safety. Because a nested object (e.g., an array of structures or a structure containing and array field) is deallocated using a pointer to its base address, only pointers that refer to the outer object are assigned the value of the invalid pointer upon the object's deallocation. The pointer metadata of any potential sub-object reference is not updated in this way (see the rules for metadata propagation in Section 3.5). Thus, object metadata is required to associate pointers to inner objects with the base and bound addresses of their corresponding outer object. Object metadata is introduced next.

    Object metadata

    For every object allocation, MemSafe creates and assigns a unique id to the object and records a tuple 〈base,bound〉for the allocated region in a global object metadata facility. MemSafe removes entries for objects from the metadata facility when they are deallocated. The object metadata facility maps an object's id to its base and bound addresses and is formally defined by the partial function:

    display math

    where I is the set of ids and O is the set of object metadata. For notational convenience, the function omd can also be represented more generally as the relation math formula, where math formula if the object associated with id is a valid memory object that has yet to be deallocated. A discussion of the implementation of the object metadata facility is deferred until Section 5.2 to separate the presentation of MemSafe's method from its prototype implementation.

    Object bounds check

    MemSafe utilizes object metadata for performing an OBC. MemSafe inserts an OBC, in addition to the PBC described earlier, before each pointer dereference that may access a sub-object if the pointer cannot be statically verified to be temporally safe. †† OBC is the forcibly inlined procedure defined by

    image

    In this check, ptr is a pointer to type unsigned char, id is a component of ptr's pointer metadata, and size is the size in bytes of ptr's referent. MemSafe utilizes the object metadata of pointer ptr's referent, denoted 〈base,boundid, to ensure the safety of its dereference at runtime:

    image

    In this example, MemSafe will abort the program and report a violation of memory safety if the dereference *ptr will access a location outside the range specified by [baseid,boundid). The OBC uses the id field of ptr's pointer metadata to retrieve the object metadata of its referent from the object metadata facility. Assuming pointer ptr refers to a sub-object, the temporal safety of ptr's dereference is ensured because, had ptr's referent been previously deallocated, its entry would have been unmapped in the object metadata facility math formula, causing omd(id) to fail and MemSafe to signal a safety violation.

    As depicted in Figure 3, the OBC and MemSafe's object-based metadata is capable of ensuring not only complete temporal safety but also partial spatial safety with a single check. Thus, if the detection of sub-object overflows is not a requirement, the PBC in the aforementioned example can be eliminated because the OBC also verifies ptr is within bounds of its outer object.

    3.5 Propagation of the required metadata

    Having presented the runtime checks that MemSafe requires for ensuring memory safety, this section describes MemSafe's translations for creating and propagating the required metadata. In doing so, it is assumed that the program has already been transformed such that it includes the syntax extensions for modeling memory deallocation and pointer stores as explicit pointer assignments (see Section 3.1). In the next discussion, the rules for propagating the required metadata are addressed according to the way in which pointers are defined.

    Memory allocation

    As described previously, MemSafe creates entries in the global object metadata facility as objects are allocated. For automatic memory allocation (i.e., the allocation of stack variables), MemSafe generates a new id for the allocated object and maps it to the object's base and bound addresses in math formula, as shown in the following.

    display math(1)

    In this example, a structure containing an array field is allocated on the procedure stack. Therefore, MemSafe obtains a new id for the allocated object and maps it to the base and bound addresses of the allocated region in math formula (1).

    For dynamic memory allocation (i.e., the allocation of objects on the heap), MemSafe updates math formula as it does for automatic memory allocation, but it also creates pointer metadata for the pointer returned by malloc because the malloc function is responsible for creating a new object as well as a new pointer to the allocated object. If the pointer returned by malloc is equal to the NULL pointer, the pointer inherits the metadata of the invalid pointer. MemSafe creates the required object and pointer metadata for heap-allocated objects as shown in the following.

    display math(2)
    display math(3)
    display math(4)
    display math(5)

    In this example, an object of size bytes is allocated dynamically by calling malloc, and the address returned by malloc is assigned to the pointer p. After the program allocates the object, MemSafe obtains an address for holding the pointer metadata of p and obtains a new unique id for the allocated object (2). If the value returned by malloc is equal to NULL, the object metadata associated with idp is set to the base and bound addresses of the ‘invalid’ region of memory. Otherwise, the metadata associated with the object is defined such that it refers to the space occupied by the allocated region of memory (3). Finally, MemSafe associates the object's metadata with idp in math formula (4) and stores the metadata of p at its associated address (5).

    For static memory allocation (i.e., the allocation of global variables), MemSafe initializes the object metadata facility to include entries for the base and bound addresses of each allocated region because the number and size of global variables is known at compile time.

    Memory deallocation

    Whenever an object is deallocated, MemSafe removes its entry from math formula and sets the pointer metadata of the pointer that refers to the object to be equal to that of the invalid pointer. Stack-allocated objects are deallocated when the function in which they are defined exits. Therefore, MemSafe removes their entries from math formula before the end of the procedure, as shown in the following.

    display math(6)
    display math(7)
    display math

    In this example, structure s is an automatic variable of function f and contains an array sub-object that is nested within it. In line 5, pointer p is assigned the address of an element of the structure's array field, and it is assumed that p may escape to another procedure. Before the procedure exits, MemSafe removes the entry for s from math formula (6) using the unique id associated with s (‘ ∖’ denotes set difference). Because p may escape, it is assigned the value of the invalid pointer in line 7, and its pointer metadata is updated to refer to the metadata associated with invalid (7).

    Heap-allocated objects are deallocated dynamically with the free function. Similar to the aforementioned rule for automatic memory deallocation, MemSafe updates object and pointer metadata for dynamic memory deallocation, as shown in the following.

    display math(8)
    display math(9)
    display math

    In this example, a pointer p to a heap-allocated object is used to deallocate its referent dynamically by the program calling the free function. Before the call to free in line 3, MemSafe removes the entry for the deallocated object from math formula with idp (8) and sets the pointer metadata of p to be equal to that of the invalid pointer (9). Pointer p is assigned the value of invalid in line 4.

    If idp had been previously unmapped in the object metadata facility (indicating that p's referent was already deallocated before the call to free), the lookup operation represented by omd (idp) would fail. In this case, MemSafe would signal a temporal safety violation to indicate the multiple deallocation attempt.

    Address-of operator

    Like dynamic memory allocation, the address-of operator (&) creates a pointer to a new location. Therefore, having already updated math formula for an object's allocation, MemSafe creates pointer metadata for pointers to the object, as shown in the following.

    display math(10)
    display math(11)

    In this example, as in previous examples, a pointer p is assigned the address of an element of the array field of structure s. Because the program creates a new pointer, MemSafe obtains a new address for storing the pointer metadata of p (10). MemSafe then creates and stores pointer metadata for p to indicate that it refers to the base and bound addresses of the array field of s (11).

    This example also demonstrates MemSafe's ability to detect sub-object overflows. Although p refers to a location within object s (indeed, p inherits the id of s), p's base and bound addresses are associated with the array field of s.

    Pointer copies and arithmetic

    Pointers defined as simple pointer copies or in terms of pointer arithmetic (e.g., array and structure indexing) inherit the pointer metadata of the original pointer, as shown in the following. ‡‡

    display math(12)

    In this example, because pointer p1 is defined in terms of pointer arithmetic, it simply inherits the pointer metadata associated with pointer p0 (12).

    ϱ-functions

    Because the value produced by a ϱ-function is not known statically, MemSafe must ‘disambiguate’ it for the returned pointer to inherit the correct metadata. Thus, MemSafe requires an additional metadata facility. Like the object metadata facility, the pointer metadata facility maps the address of an in-memory pointer to its pointer metadata and is defined by the partial function:

    display math

    where A is the set of addresses and P is the set of pointer metadata. For convenience, the function pmd can also be represented more generally as the relation math formula, where math formula.

    For pointer loads, MemSafe creates a new definition for the loaded value and assigns it the result of a ϱ-function, which indicates the set of values to which the loaded value may potentially be equal. For a pointer ptr whose pointed-to location is loaded in defining another pointer p, MemSafe retrieves from the pointer metadata facility the required pointer metadata for p with the lookup operation pmd(ptr), as shown in the following.

    display math(13)

    In this example, an in-memory pointer is loaded and assigned to pointer p0. MemSafe then creates a new pointer p1 and assigns it the result of a ϱ-function indicating the values the in-memory pointer may potentially equal. The pointer metadata for p1 is retrieved from the pointer metadata facility with the pmd(ptr1) lookup operation (13), and all uses of p0 are replaced with uses of p1.

    For each argument of the ϱ-function (including the invalid pointer), MemSafe saves their pointer metadata in math formula at the locations each pointer is stored to memory, as shown in the following.

    display math(14)

    In this example, pointer ptr2 is assumed to potentially alias with pointer ptr1 from the previous example. Thus, pointer a0 appears in the defined ϱ-function for pointer p1 because of the pointer store in line 3. Here, MemSafe maps pointer ptr2 to the pointer metadata of a0 in math formula (14). If ptr1 equals ptr2, the pointer metadata of a0 would be retrieved in the previous example.

    NULL and manufactured pointers

    Pointer type-casts and unions do not require any additional metadata propagation. The new pointer simply inherits the pointer metadata of the original pointer, as in the rule for pointer copies and arithmetic. However, pointers defined as NULL or as a cast from a non-pointer type must inherit the base and bound of the invalid pointer, as shown in the following. §§

    display math(15)

    In this example, pointer p is defined as a type-cast from the integer 42. Thus, MemSafe defines the pointer metadata for p to be equal to that of the invalid pointer (15). The result would have been the same if p had been assigned the value of NULL.

    Function arguments and return values

    MemSafe requires an additional metadata facility to propagate pointer metadata for pointers passed as arguments to functions or returned from functions. Let callee values refer to formal pointer arguments and pointer values that are returned from functions. Similarly, let caller values refer to actual pointer arguments and local pointer values to be returned from functions. The function metadata facility maps a callee value to the pointer metadata of its corresponding caller value and is defined by the partial function:

    display math

    where C is the set of caller values, P is the set of pointer metadata, and callee is a tuple 〈&f,i〉 indicating the ith pointer associated with function f. Pointers are statically assigned an index i on the basis of their usage: the return value of a function is assigned index zero, and the pointer arguments of a function are assigned an index equal to their offset in the function's argument list, beginning at one. For notational convenience, the function fmd can also be represented more generally as the relation math formula, where math formula.

    For function calls, MemSafe creates an entry in the function metadata facility for pointer arguments passed to the function. Similarly, MemSafe defines the pointer metadata of a pointer returned from the function call by performing a lookup operation of math formula. MemSafe updates and defines pointer metadata for function calls as shown in the following.

    display math(16)
    display math(17)

    In this example, a pointer p0 is passed as an argument to function f and pointer p1 is assigned the returned value. The return value of f is statically associated with the index ‘0’, and its single pointer argument is given an index of ‘1’. Thus, before the function call, the pointer metadata of p0 is associated with the tuple 〈&f,1〉in math formula (16). That is, a key represented by the address of f and the integer ‘1’ is mapped to the pointer metadata of p0. Similarly, after the call returns, the pointer metadata for p1 is retrieved from math formula with the tuple 〈&f,0〉 (17).

    For the declaration of a function with pointer arguments, MemSafe retrieves the pointer metadata for each incoming pointer by performing a lookup operation of math formula. Similarly, if a function returns a pointer value, MemSafe creates an entry in math formula for its pointer metadata just before the function returns. MemSafe updates and defines pointer metadata for function declarations as shown in the following.

    display math(18)
    display math(19)
    display math

    In this example, pointer q is a formal argument of function f, and pointer r is returned at the end of the procedure. Because q is declared to be the first pointer in the function's argument list, MemSafe retrieves the pointer metadata for q from math formula with the tuple 〈&f,1〉at the beginning of the procedure (18). Similarly, because MemSafe statically assigns pointer return values the index ‘0’, the pointer metadata of r is associated with the tuple 〈&f,0〉in math formula before the procedure exits (19).

    MemSafe's approach for propagating metadata for pointer arguments and return values is quite robust. It is sufficient for interfacing with pre-compiled libraries, handling variable-argument functions, and passing metadata through poorly typed function pointers. For complete safety, pre-compiled libraries must have been compiled with MemSafe's safety checks, but a safe application is capable of interfacing with unsafe libraries as well.

    Memory copying functions

    The memory copying functions (e.g., memcpy and memmove) defined in the string.h standard library generally copy a specified number of bytes from a location indicated by a source pointer src to the location referred to by a destination pointer dest. Although these procedures result in multiple read and write operations, MemSafe only needs to perform bound checks for the source and destination buffers once before the operation begins.

    However, any in-memory pointer values that are located in the source buffer and copied to the destination buffer must also have their associated pointer metadata copied. Recall that pointer metadata that is associated with in-memory pointers is maintained in the pointer metadata facility math formula. Thus, any mapped address in math formula that is within the range [base,bound) of the source buffer must have its metadata copied and associated with a new address that is located at a distance of destsrc bytes from the original address. MemSafe updates and defines pointer metadata for the memory copying functions of string.h as shown in the following.

    display math(20)
    display math(21)
    display math(22)

    In this example, the definition of S selects the pointer metadata associated with the source buffer that must be copied (20), and the definition of D associates the metadata with a new address within bounds of the destination buffer (21). Finally, math formula is updated to contain the copied metadata (22). To avoid the runtime overhead of performing the metadata copy, MemSafe attempts to infer if the source buffer contains any in-memory pointer values by reasoning about its type and usage. Although this may lead to the pointer metadata facility not being properly updated, instances of in-memory pointers being copied with the string.h functions have been observed to be rare in practice.

    4 REDUCING THE RUNTIME COST OF ENFORCING MEMORY SAFETY

    Because the runtime overhead of MemSafe's basic approach can be prohibitively expensive for use in real systems, this section develops several tools and optimizations for reducing the cost associated with the inserted checks for memory safety and the code required for propagating metadata. First, a novel pointer dataflow representation is presented that is made possible by the modeling of both memory deallocation and pointer store operations as explicit pointer assignments. Then, this dataflow representation is used as the foundation of several optimizations that identify and eliminate unneeded runtime checks and code for propagating unused metadata.

    4.1 A dataflow graph for pointers

    By utilizing the abstractions developed in Section 3.1 for memory deallocation and pointer stores, MemSafe creates a whole-program dataflow of pointers graph (DFPG). The DFPG is a definition-use graph for the flow of all pointer metadata in a program. Because the invalid pointer creates a direct pointer assignment for each deallocated object, and because the ϱ-function creates a pointer assignment for indirect pointer stores, every pointer assignment, whether it is an explicit or implicit assignment, is given a unique name in the SSA representation of the program. Therefore, if the pointer metadata associated with a pointer p is copied to that of another pointer q, there is a directed edge from p to q in the DFPG. ¶¶ Similarly, if the pointer metadata of q is loaded from memory, a ϱ-function is inserted that indicates the pointers whose metadata may equal that of q, and these pointers are represented in the DFPG as predecessors of q. In general, because the dataflow of pointers may flow recursively, cycles are possible in the DFPG. Recall that because the dataflow associated with pointer loads and stores is represented by the ϱ-function, the definitions and uses associated with these operations are not included in the DFPG.

    Figure 4 shows (a) an example code fragment and (b) its associated DFPG. In this code fragment, which was introduced during the discussion of the invalid pointer and ϱ-function (see Section 3.3), the only pointer definition that is not a pointer load or store operation is that of c1, which occurs after line 9. Because the definition of c1 uses the value of three other pointers (a, b, and invalid), there is an edge in the DFPG from each of these pointers to c1.

    Figure 4.

    Dataflow of pointers graph construction (DFPG). A code fragment with MemSafe's syntax extensions (a) and its corresponding DFPG (b). Numbered lines indicate original code.

    4.2 Optimizations of the basic approach

    MemSafe utilizes the DFPG to perform several optimizations that reduce the cost of memory safety. Because the DFPG blurs the distinction between spatial and temporal errors, MemSafe's optimizations (described in the following texts) affect aspects of both.

    Dominated dereferences

    Multiple dereferences of the same pointer require safety checks only for the dereference that dominates the others. Dominated dereferences do not require checking.

    Temporally safe dereferences

    If a pointer p is not reachable from invalid in the DFPG, then it must refer to a temporally valid object. Therefore, a dereference of p does not require an OBC. Recall that because MemSafe models temporal errors as spatial errors, the PBC ensures spatial and temporal safety for object-level references. However, if p may refer to a sub-object, its dereference requires the OBC in addition to the PBC to ensure temporal safety. p's potential referents are represented by the set of nodes in DFPGT that are reachable from p and have no children. DFPGT is the transpose of the DFPG (i.e., the DFPG with its edges reversed).

    Non-incremental dereferences

    If a pointer p must refer to a temporally valid object and is not reachable from a path in the DFPG representing pointer arithmetic, then p must refer to the base of a valid object or sub-object. If p is physically sub-typed [40] with each of its potential referents (i.e., their types are compatible for assignment), p's dereference does not require a PBC. If p is reachable from only constant increments (e.g., structure field accesses), MemSafe performs compile-time checks to eliminate the PBC.

    Monotonically addressed ranges

    A pointer whose value is a monotonic function of a loop induction variable refers to a monotonically addressed range of memory. For the dereference of such a pointer, MemSafe removes the pointer's PBC from within the loop body and inserts a monotonically addressed range check (MARC) in the loop pre-header. If the pointer's dereference also requires an OBC, this check is placed in the pre-header as well. MARC is the forcibly inlined procedure defined by

    image

    In the aforementioned check, ptr, baseptr, and boundptr are all pointers to type unsigned char, and size is the size on bytes of ptr's referent. MemSafe inserts the following code to ensure the safety of a pointer dereference within a loop:

    image

    In this example, MemSafe signals a safety violation if the dereference *(ptr+i) will access a location outside the range specified by 〈base,bound,idptr on any iteration of the loop and eliminates ptr's PBC within the loop. If ptr may refer to a sub-object, MemSafe hoists the OBC within the loop to the location before the MARC in the loop pre-header. ||||

    Unused metadata

    Connected components in the DFPG represent disjoint alias sets. Therefore, if MemSafe eliminates checks for all pointers in a particular connected component, then their metadata is unused, and MemSafe eliminates it as well. This is more aggressive than dead code elimination because MemSafe removes not only unused metadata but also code that updates the metadata facilities.

    5 IMPLEMENTATION

    Having described MemSafe's approach for inserting and optimizing the runtime checks needed for ensuring memory safety, this section describes the implementation of the MemSafe compiler and the global data structures it requires for maintaining metadata. Additionally, implementation issues related to the C language and the typical C programming development process are also considered.

    5.1 MemSafe's analysis and transformation

    MemSafe is implemented within the LLVM [38] compiler infrastructure. LLVM's intermediate representation is a low-level, typed SSA [24] form that is language independent and also independent of any instruction set architecture. Thus, the implementation of MemSafe's transformation for ensuring memory safety is not specific to a particular computer architecture and, in theory, could also be used to enforce safety for languages other than C. However, MemSafe has not been tested for this purpose. By default, MemSafe uses Andersen's analysis [42] for interprocedural flow-insensitive and context-insensitive points to information, but MemSafe is compatible with any alias analysis implementation that operates within the LLVM infrastructure.

    MemSafe consists of a collection of analyses and transformations that each contribute a portion of the overall approach described in Section 3. These include compilation passes for (i) inserting assignments of the invalid pointer at deallocation sites, (ii) inserting ϱ-functions with the aid of alias analysis, (iii) constructing the DFPG, (iv) inserting optimized safety checks and code for metadata propagation, and (v) aggressively removing all previously inserted assignments of invalid (but not its associated metadata) and all ϱ-functions because these are only used for MemSafe's analysis. We run the aforementioned passes after the program has been linked and optimized with LLVM's standard set of optimizations. Applying MemSafe's transformation after LLVM's optimizations improves the results of alias analysis and ensures that MemSafe avoids inserting unnecessary checks. We run LLVM's standard optimizations once more after MemSafe has completed its transformation to further optimize the inserted checks and to eliminate any dead code that MemSafe may have introduced during its analysis.

    5.2 Metadata facilities

    The global facilities (math formula, math formula, and math formula) MemSafe requires for maintaining object and pointer metadata can be implemented using any data structure that supports efficient insertion, deletion, and retrieval operations. For simplicity and ease of implementation, MemSafe uses dynamically resized hash tables for all three metadata facilities. Collisions are resolved using separate chaining. Hash functions are a modulo of the key with the size of the table, which becomes an efficient bitwise and operation by restricting table sizes to powers of two.

    The prototype implementation of MemSafe makes two simplifications in the process described for creating pointer and object metadata. Fist, MemSafe acquires addresses addr ∈ A for storing a pointer's pointer metadata using malloc, and this storage is released back to the system using free when a pointer's metadata is updated to be that of the invalid pointer. Second, MemSafe acquires a unique id ∈ I, which is used as a key for the object metadata facility by incrementing a global counter. Although this ensures that each generated id is unique, this places a finite limitation on the number of objects that can be allocated by a program. However, as reasoned by Nagarakatte et al. [7], note that a 4 GHz computer would take 136 years to overflow a 64-bit counter allocating a new object on every clock cycle.

    5.3 Limitations

    Although MemSafe's method of ensuring the memory safety of C is complete and compatible with most programs, given C's weak typing guarantees and the typical application development process, in practice MemSafe is not free from limitations. For example, the implementation of MemSafe currently does not support inline assembly instructions and does not allow self-modifying code. For programs requiring assembly, MemSafe could be extended with the appropriate rules for handling these instructions, but this would likely limit the effectiveness of MemSafe's optimizations.

    MemSafe's most significant limitation is its use of whole-program analysis to limit the number of required checks and to avoid unnecessary metadata propagation. Although analyzing the entire program is essential for reducing the cost of software-provided memory safety, it negates the advantages of separate compilation and can be problematic for use in common build environments. However, MemSafe's whole-program analysis, which is based on the construction of the DFPG, is not required for enforcing safety. The checks and metadata propagation described in Sections 3.4–3.5 are fully compatible with separate compilation, and MemSafe's optimizations can be turned off for programs where whole-program analysis is infeasible. Section 6 presents performance overheads with and without using whole-program analysis.

    Another limitation is the requirement that external libraries be compiled with MemSafe to achieve complete safety. This limitation is necessary not only for ensuring the safety of the libraries but also for correctly propagating metadata to the applications. However, for various compatibility issues, it is not uncommon for systems to maintain multiple versions of pre-compiled libraries, and in such cases, it may be reasonable to accommodate safe and unsafe library versions as well. As mentioned in Section 3.5, the design of MemSafe's metadata facilities enables both safe and unsafe libraries to link with a safe application. When linking a safe application with unsafe libraries, MemSafe ensures that externally defined pointers inherit the metadata of the invalid pointer. Although this may result in some false positives, we have observed this to be uncommon in practice.

    6 RESULTS

    Having discussed the prototype implementation of the MemSafe compiler, this section presents a thorough evaluation of MemSafe's approach for ensuring the memory safety of C programs at runtime. Specifically, this section evaluates (i) MemSafe's completeness by demonstrating that it is capable of detecting known memory safety violations in several large programs, (ii) MemSafe's cost by measuring its runtime overhead on a variety of programs and comparing this slowdown with that of prior methods, and (iii) the effectiveness of MemSafe's static analysis by measuring quantities related to MemSafe's dataflow representation and the number of required checks and performed optimizations.

    In performing the aforementioned evaluation, it will be demonstrated that MemSafe is compatible with a variety of C programs and that it does not require any source code modifications or programmer intervention. Additionally, it will be shown that MemSafe's key contributions—namely, the modeling of temporal violations as spatial violations, the use of a hybrid metadata representation, and MemSafe's dataflow representation—are effective tools for reducing the runtime cost of dynamically ensuring memory safety.

    6.1 Effectiveness in detecting errors

    To provide evidence of its completeness and ability to detect real errors, MemSafe was evaluated on programs containing known memory errors from the BugBench [27] suite of programs. BugBench is a collection of programs containing various documented software bugs that was expressly created to evaluate the effectiveness of error detection tools. Table 3 shows that MemSafe is capable of detecting all known memory errors in six programs from BugBench. BugBench programs that were excluded from Table 3 include programs that only contain errors that are not related to spatial or temporal safety (e.g., memory leaks and race conditions). Thus, the programs in Table 3 are representative of all memory safety violations in BugBench. The size of each program is given in lines of code and the number of static dereferences.

    Table 3. MemSafe's ability to detect all memory violations in the BugBench [27] programs.
    BenchmarkSizeDetected all
    SuiteProgramLOCDerefs
    1. Program size is measured in lines of code (LOC) and the number of static dereferences.

    BugBench099.go29,24616,632Yes
     129.compress1934232Yes
     bc-1.0614,2882474Yes
     gzip-1.2.490761722Yes
     ncompress-4.2.41922838Yes
     polymorph-0.4.071665Yes

    MemSafe's ability to detect real-world memory violations was further validated by it compiling two large applications and successfully detecting the known memory errors. Table 4 summarizes the memory safety violations detected by MemSafe in various versions of the Apache HTTP server [25] and the GNU Core Utilities [26] software package. The Apache HTTP server is a widely used open source web server, and the GNU Core Utilities is a GNU software package that provides the basic text, shell, and file utilities (e.g., cat, expr, and cp) common among virtually all Unix-like operating systems. To reproduce the known errors in these programs, the online development archive and bug database of each was consulted to identify particular versions of the software that contain memory safety violations and the runtime conditions necessary for producing them. Having discovered the known violations, MemSafe was then used to compile each version of the software, and the programs were executed to verify that the inserted runtime checks successfully detected the violations. The size of each program in Table 4 is given in lines of code.

    Table 4. Known real-world memory violations from the Apache HTTP Server [25] and GNU Core Utilities [26] that MemSafe successfully detects.
    ApplicationVersionLOCComponentDetected violation
    Apache HTTP Server*2.0.39262,487mod_ext_filterNull dereference
    2.0.40266,741mod_envNull dereference
    2.0.46282,682mod_sslDangling pointer
    2.0.48284,627mod_sslNull dereference
    2.0.50262,266mod_rewriteBuffer overflow
    2.0.52263,513mod_auth_ldapNull dereference
    2.0.54265,243mod_auth_ldapNull dereference
    2.0.59267,783mod_rewriteUninitialized pointer
    2.2.0310,283mod_proxyDouble free
    2.2.2311,235mod_dbdDouble free
    2.2.6314,531mod_proxy_balancerBuffer overflow
    2.2.8316,713mod_log_configNull dereference
    2.2.9332,867mod_ldapNull dereference
    2.3.4206,590mod_proxyNull dereference
    GNU Core Utilities5.2.1103659ftsDouble free
    5.2.1103,659copyBuffer overflow
    5.2.1103,659whoBuffer overflow
    5.3.0107,147cutDouble free
    5.9.0112,781regexecBuffer overflow
    6.1069,491mkfifoNull dereference
    6.1069,491mknodNull dereference
    6.1069,491ptxBuffer overflow

    6.2 Runtime performance

    MemSafe's increase in runtime and memory consumption was measured on a total of 30 programs from the Olden [28], PtrDist [4], and SPEC [43] benchmark suites. Programs from the Olden and PtrDist suites are known for being memory allocation intensive, whereas those from SPEC are larger and generally more computationally intensive. The programs were executed on a system running the Ubuntu 8.04 LTS Desktop operating system with Linux kernel version 2.6.24. The system contains a single 3 GHz Pentium 4 processor and 2 GB of main memory. Program execution times were determined by taking the lowest of three times obtained using the GNU/Linux time command, and memory usage was measured by instrumenting all allocation and deallocation instructions to record the number of allocated objects and their sizes. Due in part to LLVM's research-quality implementation of Andersen's analysis, the current implementation of MemSafe is not yet robust enough to compile the entire set of SPEC benchmarks. The results presented in this section pertain to the subset that MemSafe correctly compiles.

    Increase in runtime

    Table 5 summarizes the runtime and memory consumption overheads of MemSafe's fully optimized approach. Although this section discusses the increase in runtime of programs compiled with MemSafe's safety checks, the discussion of their increase in memory consumption is deferred until Section 6.2.

    Table 5. Dynamic results with whole-program analysis.
    BenchmarkSizeRuntime (s)Memory (MB)Slowdown
    SuiteProgramLOCDerefsBaseMemSafeBaseMemSafeMemSafeCCuredMSCC
    1. Program size is measured in lines of code and the number of static dereferences, runtime is measured in seconds, and memory consumption is measured in megabytes. Slowdown is computed as the ratio of the execution time of the instrumented program to that of the original program. Slowdown for MemSafe with all optimizations is shown in comparison with CCured [22] and MSCC [6] where results are available.

    Oldenbh20732844.645.3414.6717.701.151.442.82
    bisort350761.311.59214.73275.121.211.451.76
    em3d6881875.116.9554.8455.151.361.871.79
    health5022360.470.7036.6353.611.481.292.72
    mst428570.310.3624.9024.921.171.061.76
    perimeter4842580.360.4837.3380.121.341.093.37
    power6222854.094.702.915.141.151.071.22
    treeadd245260.380.5948.00182.921.551.103.23
    tsp5821943.834.44144.00278.651.161.152.28
     Average7161782.282.7964.22108.151.291.302.33
    PtrDistanagram6501131.562.960.240.251.901.43
    bc729739271.343.150.720.722.359.91
    ft17662462.043.613.249.201.771.03
    ks7822391.542.970.020.091.931.11
    yacr2398610001.965.9829.8830.173.051.56
     Average289611051.693.736.828.092.203.01
    SPEC'95099.go29,24616,6320.621.260.000.012.031.222.60
    129.compress19342320.010.020.000.002.201.171.85
    130.li759749050.060.1219.9119.911.931.70
    147.vortex67,20225,1350.000.0096.4196.41
     Average26,49511,7260.170.3529.0829.082.051.36
    SPEC'00164.gzip86051,49920.7243.10187.93187.942.081.46
    175.vpr17,72953868.3416.2644.3544.371.953.53
    181.mcf2,41253411.3421.8999.8699.461.932.85
    186.crafty24,975757914.9334.947.097.142.34
    255.vortex67,21325,1343.968.2896.4696.462.09
    256.bzip24649125422.3345.55191.95191.962.04
    300.twolf20,45911,7417.5215.346.3912.742.04
     Average20,863759012.7326.4890.5891.492.07
    SPEC'06401.bzip2829340136.2017.55855.79855.792.83
    445.gobmk197,21527,6140.290.6628.5028.662.27
    456.hmmr35,99275827.8217.5259.8259.892.24
    458.sjeng13,847583210.1222.26179.63179.642.20
    473.astar584218730.000.00313.15313.15
     Average52,23893834.8911.60287.38287.432.39
    Average18,39451364.779.6293.31106.921.88

    The ‘Runtime’ and ‘Slowdown’ columns of Table 5 show that MemSafe ensured complete spatial and temporal safety for all 30 programs with an average overhead of 88%. In general, MemSafe's overhead was observed to be comparable with that of CCured [22]: on the allocation intensive Olden benchmarks, MemSafe's overhead was 29% versus CCured's 30%; on CCured's entire set of reported benchmarks, MemSafe overhead was 69% versus CCured's 80%. Not including bc (on which CCured's overhead was particularly high) reduces these to 65% and 30%, respectively. Although the runtime cost of MemSafe is similar to that of CCured, MemSafe does not incur the drawbacks associated with the use of CCured—the need for manual modifications and the compatibility issues arising from the use of ‘fat-pointers’. Because of CCured's need for manual code modifications, results for CCured on additional programs were not obtained.

    Additionally, MemSafe demonstrated a significant and consistent improvement over the reported performance of MSCC [6], the tool with the lowest overhead among all existing complete and automatic methods that detect both spatial and temporal errors. On the Olden benchmarks, MemSafe's average overhead (29%) was roughly 1/4 that of MSCC (133%); on the entire set of MSCC's reported benchmarks, MemSafe's overhead (44%) was roughly 1/3 that of MSCC (137%).

    To provide a direct comparison with MSCC (instead of relying on published results) on the same computer hardware, an attempt was made to compile our entire set of benchmarks with MSCC. However, perhaps because of MSCC having not been actively maintained since its publication, it was found to be difficult to compile MemSafe's entire set of 30 benchmarks with MSCC. Figure 5 compares the slowdown of MemSafe's fully optimized approach for spatial and temporal safety with that of MSCC on the set of benchmarks MSCC compiled correctly. MemSafe's average overhead for these benchmarks (74%) was roughly 1/6 that of MSCC (486%). Although these results show a dramatic increase in runtime overhead for MSCC, the overall trend is similar to the reported results for MSCC shown in Table 5. Comparisons with additional methods on the Olden benchmarks is presented in Section 1.

    Figure 5.

    Runtime comparison with MSCC. Slowdown for MemSafe and MSCC [6] is shown for spatial and temporal and spatial-only safety.

    MemSafe's optimized approach improves the runtime cost required for memory safety in comparison with that of prior work for the following reasons: (i) MemSafe's dataflow representation enables performance-enhancing optimizations that reduce overhead from 253% to 88% (explained later) and (ii) MemSafe's modeling of temporal errors as spatial errors, combined with a hybrid metadata representation, enables MemSafe to ensure temporal safety with only a 10% increase in the overhead of spatial safety alone (also explained later).

    In particular, MemSafe's large improvement versus MSCC on the Olden benchmarks is because these programs deallocate all dynamically allocated memory at once before terminating. Thus, by determining that there is no control-flow path from the deallocation to other points in the program, MemSafe is able to eliminate the propagation of the metadata associated with the invalid pointer and remove all object bounds checks. Deallocated memory at the end of a program is a common programming style when objects are required to have an unlimited lifespan or when memory reallocation is not needed.

    Increase in memory consumption

    The ‘Memory’ column of Table 5 reports the memory consumption of each program when compiled with the base LLVM compiler and MemSafe. MemSafe ensured complete spatial and temporal safety for all 30 programs with an average increase in memory of 13.61 MB, which is equal to 48.60% of the programs’ original memory requirements. MemSafe's average memory consumption overhead is significantly higher for the Olden ( 73.52%) and PtrDist ( 130.32%) benchmark suites compared with the three SPEC ( 8.46%) benchmark suites. Because MemSafe requires the metadata of each allocated object to be mapped in the object metadata facility, allocation intensive programs, such as those in the Olden and PtrDist suites, require more memory for maintaining metadata than computationally intensive programs. The memory required for MemSafe's metadata is determined by the number of allocated objects rather than their total size.

    Effectiveness of optimizations

    Figure 6(a) shows that MemSafe's optimizations and whole-program analysis are effective tools for reducing the runtime overhead required for ensuring memory safety. Shown in the ‘Average’ histogram, MemSafe's optimizations reduced its average runtime overhead from 253% to 88%. Because the optimization for dominated dereferences is minimally effective, it is presented in Figure 6(a) as the baseline. The optimization for temporally safe dereferences (TDO) reduced overhead by 102%, and the optimization for non-incremental dereferences (NDO) reduced overhead by 37%. Combined with the optimization for unused metadata, which is included with both, NDO and TDO accounted for the greatest reduction in overhead. The optimization for monotonically addressed ranges (MRO) was marginally effective and reduced overhead by only 1%.

    Figure 6.

    Optimization effectiveness. Slowdown with whole-program analysis (a) and without whole-program analysis (b) is shown for spatial and temporal and spatial-only safety. Optimizations include dominated dereferences (DDO), temporally safe dereferences (TDO), non-incremental dereferences (NDO), and monotonically addressed ranges (MRO).

    Figure 6(b) shows the effectiveness of MemSafe's optimizations without utilizing whole-program analysis. When restricted to not use interprocedural information, MemSafe's optimizations reduced the overhead from 253% to 209%. TDO reduced overhead by 11%, NDO reduced overhead by 7%, and MRO reduced overhead by an additional 2%. Hence, MemSafe's average overhead with separate compilation was 209% versus 88% with whole-program analysis. MemSafe's seemingly large improvement in runtime overhead when given the ability to perform interprocedural optimizations is not by chance: By representing memory deallocation and pointer stores as direct assignments, MemSafe makes whole-program optimizations much more effective. Thus, MemSafe's overheads are lower than those of existing methods that cannot benefit in this way.

    Additional cost of temporal safety

    Figure 6(a) also quantifies the additional cost required for MemSafe to ensure temporal safety. The last bar in the ‘Average’ histogram shows that MemSafe's overhead for both spatial and temporal safety (88%) is comparable with the runtime overhead that MemSafe requires for ensuring spatial safety (80%). Thus, for the 30 programs tested, MemSafe ensured complete temporal safety with a modest 10% increase in the average overhead for achieving spatial safety.

    Finally, the additional cost of ensuring temporal safety with MemSafe is a significant reduction in the cost of achieving temporal safety with MSCC. On MSCC's set of reported benchmarks, the additional cost of ensuring temporal safety with MemSafe (1%) is a reduction in the additional runtime cost required for MSCC to ensure temporal safety (62%) by a factor of 62. For the set of programs that were successfully compiled with MSCC on the same platform as MemSafe (shown in Figure 5), the additional cost of ensuring temporal safety with MemSafe (13%) is a reduction in the additional runtime cost required for MSCC to ensure temporal safety (114%) by a factor of nearly 9. Thus, the difference in the additional overhead required for MemSafe to achieve temporal safety is further evidence that by modeling temporal errors as spatial errors, MemSafe's optimizations are effective tools for reducing the cost of temporal safety.

    6.3 Static analysis

    The ‘Checks’, ‘Opts.’, and ‘DFPG’ columns of Table 6 describe results related to MemSafe's whole-program analysis. First, the ‘Checks’ column shows the static number of required checks, organized by check type, as a percentage of the static number of total pointer dereferences. Second, the ‘Opts.’ column shows the static number of checks (i.e., PBC and OBC) that were eliminated by MemSafe's optimizations, organized by optimization type. Finally, the ‘DFPG’ column summarizes the DFPG with the percentage of nodes reachable from the node representing the invalid pointer and with the ϱ/store quantity. The former indicates the portion of pointers that may refer to temporally invalid objects, and the latter indicates the average number of loaded memory locations that each pointer store may potentially modify. Thus, these two quantities are a static estimate of the uncertainty in pointer dataflow. Figure 7 demonstrates that Andersen's alias analysis [42] is often capable of reducing ϱ/store by several orders of magnitude.

    Table 6. Static results with whole-program analysis.
    BenchmarkSizeChecks (%)Opts. (%)DFPG
    SuiteProgramLOCDerefsPBCOBCMARCDDOTDONDOInvalid (%)ϱ/Store
    1. Program size is measured in lines of code (LOC) and the number of static dereferences. The static number of required checks and optimizations is measured as a percentage of dereferences. The DFPG is measured by the percentage of nodes reachable from invalid and the average number of ϱ-nodes modifiable by each pointer store. PBC, pointer bounds check; OBC, object bounds check; MARC, monotonically addressed range check; DFPG, dataflow of pointers graph; DDO, dominated dereferences optimization; TDO, temporally safe dereferences optimization; NDO, non-incremental dereferences optimization.

    Oldenbh207328419.010.001.7627.1170.0752.110.007.19
    bisort350769.210.000.0035.5364.4755.260.009.93
    em3d68818731.550.003.217.4982.3557.750.001.84
    health50223622.460.000.0015.2584.3262.290.003.42
    mst4285719.300.005.2612.2877.1963.160.000.24
    perimeter48425819.770.000.000.00100.0080.230.00142.61
    power62228537.890.000.0022.4675.7939.650.000.00
    treeadd2452639.470.000.000.00100.0060.530.005.33
    tsp58219415.980.000.0031.9668.0452.060.0031.07
     Average71617823.850.001.1416.9080.2558.120.0022.40
    PtrDistanagram65011333.630.000.0021.2452.2145.130.000.29
    bc7297392715.763.791.1232.8557.7050.278.9943.62
    ft176624630.490.000.0024.8070.3344.723.92155.43
    ks78223928.030.000.0027.6249.3744.350.0013.71
    yacr23986100034.705.003.9034.6051.2026.804.856.15
     Average2896110528.521.761.0028.2256.1642.253.5543.84
    SPEC'95099.go29,24616,63257.540.005.9625.4411.0611.060.00
    129.compress193423216.380.004.7440.9537.9337.930.004.13
    130.li7597490514.920.000.0627.2670.8957.760.00694.18
    147.vortex67,20225,1357.350.250.0434.3664.2558.2513.181511.59
     Average26,49511,72624.050.062.7032.0046.0341.253.30736.63
    SPEC'00164.gzip8605149921.350.474.3444.7030.0229.620.963.79
    175.vpr17,729538621.590.322.3222.0871.6754.017.0714.52
    181.mcf24125347.300.191.3123.6069.1067.799.6125.74
    186.crafty24,975757938.4911.110.0115.6446.1545.8623.7729.08
    255.vortex67,21325,1347.350.250.0434.3764.2458.2413.181511.61
    256.bzip24649125443.460.403.0341.2314.8312.281.12316.95
    300.twolf20,45911,74116.730.880.2520.8272.2462.219.393.71
     Average20,863759022.321.951.6128.9252.6147.149.3054.72
    SPEC'06401.bzip28293401317.298.071.2012.0975.1869.4227.83440.48
    445.gobmk197,21527,61438.518.521.9619.4941.8040.0512.70209.98
    456.hmmr35,992758227.138.761.5818.4065.6352.8914.21108.66
    458.sjeng13,847583226.292.540.2228.2148.4745.2818.3780.29
    473.astar584218737.902.620.3219.3875.7172.4018.9139.38
     Average52,238938323.426.101.0619.5161.3656.0118.40133.89
    Average18,394513624.231.771.4224.0462.0750.316.48177.68
    Figure 7.

    Effect of aliasing. The average number of ϱ-nodes modifiable by each pointer store is shown with Andersen's alias analysis versus no alias analysis.

    Finally, Figure 8 shows MemSafe's compile-time slowdown for each program. Slowdown is computed as the ratio of the compilation time required by MemSafe to that of the base LLVM compiler using default optimizations. In general, the compile-time requirements of MemSafe are modest. For 28 of the benchmarked programs, MemSafe was able to ensure memory safety with an increase in compile time by less than a factor of two. The compilation time required by ft and 130.li surpassed this threshold because of the time required for inserting ϱ-functions. Despite these two programs, MemSafe's average increase in compile time over all 30 benchmarks was 62%.

    Figure 8.

    Compile-time slowdown. For each program, slowdown is computed as the ratio of the compilation time required by MemSafe to that of the base low-level virtual machine compiler using the default set of optimizations.

    7 RELATED WORK

    Most prior techniques related to the enforcement of memory safety were presented in Section 2 after having described the various types of spatial and temporal memory errors. Therefore, this section will not repeat that content, but it will discuss additional details for methods capable of detecting both spatial and temporal violations as well as techniques that can only detect one type of error. This section also presents a discussion of previous work related to MemSafe's dataflow analysis.

    7.1 Spatial and temporal safety

    Although generally not enforcing complete memory safety, several methods are capable of detecting both spatial and temporal errors. Purify [20] operates on binaries but only ensures the safety of heap-allocated objects. Yong and Horwitz [21] present a similar approach and improve its cost with static analysis, but this method only checks store operations. Safe C [4] ensures complete safety but is incompatible because of its use of fat-pointers. Patil and Fischer [5] address these issues by maintaining disjoint metadata and performing checks in a separate ‘shadow process’, but this requires an additional CPU. CCured [22] utilizes a type system to eliminate checks for safe pointers and reduce metadata bookkeeping. However, CCured's use of fat-pointers causes compatibility issues, and some programs require code modifications to lower cost. MSCC [6] is highly compatible and complete but is unable to handle some downcasts. Fail-Safe C [23] maintains complete compatibility with ANSI C but incurs significant runtime overhead. Finally, Clause et al. [44] describe an efficient technique for detecting memory errors, but it requires custom hardware.

    7.2 Spatial safety

    Methods that primarily detect bounds violations are numerous. Notable is the work by Jones and Kelly [15] because it maintains compatibility with pre-compiled libraries. However, this method has high overhead and results in false positives. Ruwase and Lam [16] extend this method to track out-of-bounds pointers to avoid false positives. Additionally, Dhurjati and Adve [17] utilize Automatic Pool Allocation [34] to improve cost, and Akritidis et al. [33] constrain the size and alignment of allocated regions to further improve cost. However, these methods do not detect temporal violations and are unable to detect sub-object overflows.

    HardBound [45] is a hardware-assisted approach for ensuring spatial safety with low overhead. This method encodes fat-pointers in a special ‘shadow space’ and provides architectural support for checking and propagating metadata. SoftBound [7] is a related technique that records pointer metadata in disjoint data structures similar to MemSafe's representation. However, although these methods ensure complete spatial safety, they do not ensure temporal safety, and HardBound requires custom hardware to achieve low overhead.

    7.3 Temporal safety

    Few methods are designed primarily for detecting temporal violations. Dhurjati and Adve [46] describe a technique based on the Electric Fence [47] malloc debugger: Their system assigns a unique virtual page to every dynamically allocated object and relies on hardware page protection to detect dangling pointer dereferences. This approach is improved with Automatic Pool Allocation [34] and a customized address mapping. However, this method does not detect spatial violations and only detects temporal violations of heap objects. CETS [48] inserts temporal safety checks before pointer dereferences and utilizes an efficient lock-and-key mechanism, instead of hash tables, for accessing the required temporal metadata. However, this method also does not detect spatial violations and must be combined with an existing spatial safety mechanism to guarantee complete temporal safety.

    7.4 Software debugging tools

    Although not intended for deployment in production-quality applications, automated debugging tools can be used to detect memory errors during software development and testing. Valgrind [49] is a heavyweight dynamic binary instrumentation framework providing the Memcheck [50] tool for debugging memory accesses and leaks, and Mudflap [18] is a compiler approach for debugging memory accesses implemented in the GNU Compiler Collection [51] compiler infrastructure. However, these tools are incapable of ensuring complete spatial and temporal memory safety and incur significant runtime overheads. For example, both Memcheck and Mudflap are unable to detect spatial safety errors where an out-of-bounds pointer to one object happens to fall within bounds of another object. They are also unable to detect temporal safety errors when the runtime system reallocates memory to a previously deallocated location. Moreover, Memcheck does not aim to ensure spatial or temporal safety for stack-allocated objects and increases runtime by a factor of 10–30.

    7.5 Other methods of memory protection

    Several methods utilize software checks to enforce various security-related policies. Abadi et al. [29] describe a technique to prevent software attacks by enforcing control-flow integrity. Similarly, Castro et al. [30] enforce dataflow integrity with an analysis based on reaching definitions, and WIT [31] enforces write-integrity by ensuring each write operation accesses an object from a static set of legally modifiable objects. Although these techniques are capable of preventing many memory access violations, they do not ensure complete spatial and temporal safety.

    DieHard [52] is a dynamic memory allocator capable of preventing many heap-related errors. It uses random object placement within a larger-than-normal heap to prevent invalid deallocations and probabilistically avoid heap buffer overflows. However, this method is incapable of ensuring complete spatial and temporal memory safety.

    Other methods seek to provide minimal memory protection guarantees to programs executed on systems lacking hardware virtual memory. Simpson et al. [53] developed a low-overhead method for achieving memory segmentation using compiler-inserted runtime checks. Like paging, segmented virtual memory is a common approach for providing coarse-grained memory protection. In another method, Biswas et al. [54] developed a technique for avoiding out-of-memory errors with compiler-inserted runtime checks, memory reuse, and the compression of unused data. Finally, Middha et al. [55] developed a similar method for avoiding out-of-memory errors in embedded systems by sharing stack space among the executing tasks of multitasking workloads.

    7.6 Static single assignment extensions

    Various methods have extended SSA [24] to incorporate alias information. IPSSA [56] is an interprocedural, gated SSA [57] that uses alias analysis to replace indirect stores with ϕ-like functions whose semantics are similar to the ϱ-function. However, IPSSA represents all indirect stores as direct assignments, whereas MemSafe's ϱ-function is only used for pointer stores. Other extensions include the χ and μ extensions [58], which model may-def and may-use information but, unlike the ϱ-function, do not keep track of the defining values. Finally, Cytron and Gershbein [59] describe a demand-driven algorithm for incrementally incorporating alias information with SSA to avoid a large increase in program size.

    8 CONCLUSION

    MemSafe is a compiler analysis and transformation for ensuring the spatial and temporal memory safety of C at runtime. MemSafe builds upon previous work to enable its completeness and compatibility, capturing the most salient features of object and pointer metadata in a new hybrid representation. To improve cost, MemSafe exploits a novel mechanism for modeling temporal errors as spatial errors and a new dataflow representation that simplifies optimizations for removing unneeded checks and metadata.

    We verified MemSafe's ability to detect real errors with lower overhead than previous methods. MemSafe detected all documented memory errors in six programs with known bugs as well as two large open source applications. Additionally, it ensured complete safety with an average overhead of 88% on 30 programs commonly used for evaluating error detection tools. Finally, MemSafe's average runtime overhead on the Olden benchmarks was 1/4 that of the tool with the lowest overhead among all existing complete and automatic methods that detect both spatial and temporal errors.

    • §

      A manufactured pointer is a pointer created by means other than explicit memory allocation (e.g., by calling the malloc standard library function) or taking the address of a variable using the address-of operator (&). Type-casting an integral type to a pointer type is a common example. The various memory safety violations are discussed in detail in Section 2.

    • Other methods (e.g., CIT [29], DFI [30], WIT [31], SoftBound [7], SafeCode [32], ‘baggy’ bounds checking [33], etc.) are excluded from Table 1 because they either are (i) not software-only mechanisms for detecting memory errors or (ii) do not aim to ensure complete spatial and temporal safety. However, these methods are discussed in detail in Section 7.

    • ||

      Some methods [15-17, 19, 32, 33] are capable of using object metadata to detect some, but not all, temporal safety violations. However, if an object is deallocated and its space is reallocated for use by another object, dangling pointer dereferences to the original object will not be detected because they are within bounds of the new object. Thus, these methods are incapable of enforcing either complete object-level or sub-object temporal safety.

    • **

      These pointers are implicitly type-cast to be pointers to type unsigned char because sizeof(unsigned char) is defined to always equal 1 byte [39]. This is required for the pointer arithmetic performed in the body of the bounds check to be valid.

    • ††

      MemSafe determines the set of potential sub-object references by traversing its dataflow graph, which is described in Section 4.1.

    • ‡‡

      Static single assignment ϕ-functions that involve pointer values are no different than ordinary pointer copies. For example, p2= ϕ(p0,p1) copies p0 to p2 at the end of the basic block defining p0 and copies p1 to p2 at the end of the basic block defining p1.

    • §§

      Although this may result in false positives, they have been observed to be rare occurrences in practice. For reading and writing to memory-mapped input/output locations, MemSafe requires a target's backend to specify the base and bound addresses of all valid address ranges.

    • ¶¶

      For simplicity, nodes in the dataflow of pointers graph are identified by named pointer values. However, in actuality, nodes represent the pointer metadata associated with pointers and not the pointers themselves.

    • ||||

      Although MemSafe uses the monotonically addressed range check to essentially hoist checks out of loops, array bounds check elimination [41] can be used with MemSafe to completely eliminate some of these checks.

    Ancillary