The technology relates to improving reliability of software programs. More particularly, the field relates to achieving improved robustness of software through execution of the software in randomized execution contexts.
Applications written in unsafe languages like C and C++ are vulnerable to many types of errors. In particular, these programs are vulnerable to memory management errors, such as buffer overflows, dangling pointers, and reads of uninitialized data. Such errors can lead to program crashes, security vulnerabilities, and unpredictable behavior. While many safe languages are now in wide use, a good number of installed software applications in use today are written in unsafe languages, such as C and C++. These languages allow programmers to maximize performance by providing greater control over such operations as memory allocation, but they are also error-prone.
The existing paradigm for improved software robustness generally seeks to pinpoint the location of the errors in a program by extensive testing and then fixing the identified errors. The effectiveness of this paradigm, however, is subject to the effectiveness of the testing regime and requires changes to the code. Moreover, even extensively tested programs can fail in the field once they are deployed.
For instance, memory management errors at runtime are especially troublesome. Dynamic memory allocation is the allocation of memory storage for use in a computer program during the runtime of that program. It is a way of distributing ownership of limited memory resources among many pieces of data and code. A dynamically allocated object remains allocated until it is deallocated, either explicitly by the programmer or automatically by a garbage collector. In heap-based dynamic memory allocation, memory is allocated from a large pool of unused memory area called the heap. The size of the memory allocation can be determined at runtime, and the lifetime of the allocation is not dependent on the current procedure or stack frame. The region of allocated memory is accessed indirectly, usually via a reference.
The basic functions of heap memory management by a memory allocator in the C language in a runtime system includes Malloc( ) for allocating an address on the heap to an object and Free( ) for freeing the object (or in other words, de-allocating). Although this appears to be simple, programming errors related to runtime memory management generate several well-known errors, which can be categorized as follows:
Dangling pointers: If a live object is freed prematurely, the memory allocator may overwrite its contents on the heap with a new object or heap metadata.
Buffer overflows: Out-of-bounds writes to heap can overwrite live objects on the heap, thus corrupting their contents.
Heap metadata overwrites: If heap metadata is stored too near heap objects, it can also be corrupted by buffer overflows.
Uninitialized reads: Reading values from newly-allocated memory leads to undefined behavior.
Invalid frees: Passing illegal addresses to Free( ) can corrupt the heap or lead to undefined behaviour.
Double frees: Repeated calls to Free( ) of objects that have already been freed undermine the integrity of freelist-based allocators.
Tools like Purify by IBM Rational and Valgrind, an open source debugger for Linux, allow programmers to pinpoint the exact location of some of these memory errors. However, they result in a significant increase in running time and are generally restricted in their use to the testing phase. Thus, deployed programs remain vulnerable to crashes or attack. Moreover, these tools may require changes to the code. Conservative garbage collectors can, at the cost of increased runtime and additional memory, disable calls to free( ) and so eliminate three of the above errors (e.g., invalid frees, double frees, and dangling pointers). Furthermore, assuming that the source code is available, a programmer can also compile the code with a safe C compiler that inserts dynamic checks for the remaining errors. This solution also results in further increasing the running time and requires changes to the program code. Furthermore, as soon as an error is detected, the inserted code aborts the execution of the program. Aborting a computation is often undesirable. Recognizing this need, some runtime systems sacrifice soundness in order to prolong execution, even in the face of memory errors. For example, some failure-oblivious computing solutions build on a safe C compiler, but drop illegal writes and manufactures values for invalid reads. Unfortunately, these systems cannot provide a probabilistic assurance to programmers that their programs are executing correctly.
Thus, it is desirable to improve robustness of a software program without the need to change the program's code. It is further desirable to determine to a given probabilistic level of certainty, the robustness of a software program.
Described herein are methods and systems for improving robustness of software by executing the software within randomized execution contexts. The execution context comprises randomized configurations of stack layouts and randomized configurations of runtime heap layouts, which are error tolerant. In one aspect, plurality of replicas of an executing program is executed, each within a randomly different execution context. As such, it is likely that at least some of the replicas will be executing within execution contexts that are error tolerant. Outputs of the replicas executing within error tolerant execution contexts are accepted as corresponding to the output of a correctly executing instance of the software program.
In one aspect, the error tolerant execution contexts are identified by determining at least two replicas and their corresponding randomized execution contexts that yield outputs that agree.
In one aspect, execution context comprises an error tolerant runtime heap layout which is made error tolerant by configuring it to approximate the semantics of an infinite heap. For instance, the infinite heap semantics can be approximated to known degrees of probability by expanding, (e.g., by an order of a multiple), the heap size required for a program assuming perfect packing of its objects. The expansion factor multiple can be any number and the approximated infinite heap can be a multiple of the perfectly packed heap itself or some approximation thereof.
In a further aspect, memory errors can be avoided by randomly allocating memory on the approximation of the infinite heap. For instance, upon receiving a request to allocate memory, a random number is generated. Then, some address on the approximated infinite heap that is based on the generated random number is identified and, if unallocated, it is allocated to the object associated with the current request. In another aspect, the allocation on the approximated infinite heap is based at least in part on the size of the memory requested. For instance, the approximated infinite heap is subdivided into different page sets with each set dedicated to objects of a specific size. The requests as such are then first mapped to a page based at least in part on the size of their object.
Chances of generating an ideal runtime execution environment that masks errors can be improved by executing a plurality of replicas of a program. For instance, plurality of replicas of a program are executed with different randomization seeds correspondingly associated therewith for randomly allocating memory on their respective approximations of the infinite heap. To detect uninitialized memory reads, each of the infinite heap approximations are initialized with randomly generated values, wherein the random values for each different heap are generated by use of its associated randomization seed. In a further aspect, outputs of those replicas that agree are accepted as the output of a correctly executing program instance. In one aspect, the replicas whose outputs do not agree with the others are terminated and another replica may be used to replace the terminated replica a copy of a non-terminated replica with a different randomization seed.
Additional features and advantages will become apparent from the following detailed description of illustrated embodiments, which proceeds with reference to accompanying drawings.
Execution contexts in which a software program executes (e.g., the runtime system, program stack, etc.) can vary significantly and yet maintain identical execution semantics with respect to a correct execution of the program. If there is an error in a program, some of these execution contexts will result in incorrect execution of the program. For instance, if there is a buffer overrun, then writing to the memory beyond the array may overwrite other important data. Other execution contexts, however, are tolerant of such errors. For instance, if it so happens that the locations at the end of the array are not being used for other purposes by the program, no data overwrites are caused. Thus, some error tolerant execution contexts will allow programs with errors, such as buffer overruns, to execute correctly to completion despite the errors, while others do not. Based on this observation, for a given program, one exemplary method of improving software robustness is to seek an execution context in which, despite any errors, the program will terminate correctly.
In a further step described with reference to an exemplary randomized runtime system 200 of
Exemplary execution contexts of a program include specific implementation details that map abstract program constructs (such as variables, heap objects, stack frames, etc.) into the concrete implementation of the program on a computer (e.g., the memory locations of the program abstractions). Thus, in this sense, both the compiler and the runtime define the execution context of a program. The compiler determines stack layouts, locations of static data, code, and field offsets, etc., whereas the runtime system determines the heap layout and location of objects on the heap, mappings of threads to processors, etc. Many of the exemplary embodiments described herein consider the part of the execution context defined by the program runtime system without the loss of generality. In particular, many of the exemplary embodiments herein describe in concrete terms how the runtime memory allocator can be used for randomizing execution contexts for achieving improved software robustness. The principles described with respect to the memory allocator can also be applied to other aspects of the execution context (e.g., things defined by the compiler, such as stack layout) and other parts of the runtime system (e.g., the thread implementation).
One of the key components of a runtime system is a memory manager that, among other things, allocates and de-allocates memory with respect to various objects of the program during its execution. Different memory allocation schemes can be implemented by a memory manager for the same program without a change in the semantic behavior of the program's execution. Thus, there are many equivalent execution contexts (or memory allocation schemes) that will result in a correct execution. This is because, objects allocated on the heap can be allocated at any address and the program should still execute correctly. However, some allocation schemes are more robust than others. For instance, some incorrect programs write past the end of an array. For such programs, the implementation of the memory allocator can have a significant effect on correct program execution. One implementation of the memory allocator may place an important object right next to the array, so that overwriting the array corrupts the data in the important object. Another implementation may place an empty space after the array, such that writing past the end of the array does not result in any program data being overwritten. Thus, this latter implementation can effectively hide the program error and allow the program to run correctly to completion. Such buffer overruns are just one type of a memory safety error that breaks type-safety in programs written in weakly-typed languages, such as C and C++. The other examples include duplicate frees (where an object that has been freed is given back to the memory allocator to be reallocated, but is accidentally freed again by the programmer) and dangling pointers (where addresses of objects exist, but the object pointing to that address has been de-allocated).
An ideal runtime system, however, could address these memory safety errors by effectively masking them. One such exemplary ideal runtime environment would have the semantics of an infinite heap with the following exemplary properties:
While such an ideal runtime system is not practical, if in fact it can be realized, programs with memory safety errors would be more likely to complete correctly than they would with any conventional runtime system implementation. In much of the existing paradigm, programs that have any memory safety violation are considered to be incorrect a priori and, as a result, they are not concerned with understanding whether the complete execution of such programs results in a desired output. For the methods described herein, an ideal runtime execution is defined as a program executing to normal termination and generating results that is equivalent to that which would be produced by the program if it were run with the ideal runtime system.
In any event, one practical approach to providing robustness to memory safety errors equivalent to that provided by the ideal runtime system is by approximating the behavior of an ideal runtime environment with infinite heap semantics.
A memory manager having access to a truly infinite heap is impractical. However, an approximation of an infinite heap memory manager, with one or more of the characteristics described above can be made practical. For instance, in one exemplary implementation, the address space of an approximated infinite heap is defined to be an exemplary expansion factor M times the total amount of address space required by the program assuming it has perfect packing. Intuitively, the larger the expansion factor (e.g., AM), the more unallocated space exists between allocated objects and, thus, the less likely that a buffer overwrite of one object by another will occur. As a result, a larger value for the expansion factor increases the probability that a particular random execution will generate results that agree with results of an execution in an ideal runtime environment. Of course, a larger value for the exemplary expansion factor also increases the memory requirements of running the program. However, users can analytically reason about the trade-offs between increased robustness versus increased demand for memory use.
The memory manager for allocating and de-allocating objects on an approximated infinite heap, such as the one at 600, is randomized in order to improve the chances of hiding memory safety errors.
The method 700 of
A further alternative implementation illustrated with reference to
Such classification based on size of requests and allocating to different regions of memory that are size specific makes the algorithm more practical by reducing the fragmentation that is likely to result if small objects are scattered across the entire heap.
A key aspect of both strategies is the ability to characterize a runtime system configuration concisely. Thus, it is advantageous to be able to control the runtime system configuration purely through specifying a collection of parameters so that no recompilation and/or relinking are necessary to run an application with a different configuration.
When an object is allocated in a randomized runtime, the goal is to distribute the addresses of the allocations across the address space as uniformly as possible with high efficiency. While two possible methods are illustrated in detail, there are many ways to accomplish the same. For instance, either approach above could be written such that, if the random address generated already contains an object, then another random address is generated, continuing until an empty location is identified.
In the context of method 700 of
As noted above with respect to
The corrupted replica can be determined by comparing the outputs of the various replicas R1-R4 (901-904) at the voter 930. For instance, when the output of one replica (e.g., R2 at 902) disagrees with the rest, that replica is more likely to be corrupted than not, therefore, it is discarded while the execution of the rest is continued. Thus, when one or more replicas have outputs that agree, the confidence that one has that the results of these executions agree with the results generated in an ideal runtime environment can be determined analytically and controlled by choosing the number of replicas N, for instance. Generally, the greater, the number of replicas, the greater the chance, that one of them is executing in an ideal runtime environment. Furthermore, the greater the number of replicas whose outputs agree, the greater the chances that they are executing correctly.
However, choosing a high number of replicas also has drawbacks. For instance, running a number of replicas, particularly simultaneously, adds to the cost of computing by putting a greater burden on the processor. Therefore, it would be better in some cases to implement the execution of the replicas on a system with multiple processors to reduce the negative impact on processing speeds. Another factor that impacts the probability of identifying an execution context that is ideal is the expansion factor M associated with generating infinite heap approximation (e.g., 420 of
In one exemplary implementation, a programmer could explicitly choose the number of replicas N and the expansion factor M, and pass it on as parameters to a runtime system. In another implementation, the runtime system could choose the number of replicas N and/or the expansion factor M based on previously measured executions of the program and recorded outcomes. In another implementation, a standard value of N and M (e.g., N=3, M=2) could be used in all cases. In one exemplary implementation, the expansion factor M associated with each of the replicas for approximating their associated infinite heap need not be the same.
The voter 930 of
Having several replicas running with errors in deployed applications can also be addressed. Randomizing execution contexts by generating randomized infinite heaps improves the chances that at least some of the replicas are executing in an ideal runtime environment. As noted above, the drawback to this approach is the cost of running multiple replicas. Therefore, in one implementation, the randomized replicas could be run during a testing phase and when an ideal runtime environment is identified, the variables, such as the expansion factor M, associated with successful execution and the outputs of the successful execution can be used by memory managers associated with the deployed applications. Even here, the successful execution is still probabilistic. In one exemplary implementation of this approach, the application would be run in phases. In phase one the application would be run over a suite of test inputs with a large heap expansion factor M and two replicas, for instance, so that the likelihood of an ideal runtime environment execution is high. This process is repeated until the outputs of the two replicas agree. The outputs of the two replicas would be compared as above. The resulting output is highly likely to be the same as the ideal runtime environment output (and this likelihood can be determined analytically). This output is stored for use in phase two.
In phase two, smaller values of the expansion factor could be used and the resulting output can be compared against the output of the ideal runtime environment obtained in phase one. Generating multiple random instances for a given value of the expansion factor and comparing the results against the ideal runtime environment output allows us to determine the probability of an ideal runtime environment execution for a given value of the expansion factor M (over the specific test inputs used). In phase three, the value of the expansion factor, for which the desired probability of correct execution has been measured, is used for subsequent executions of the program. At this stage, with no replicas and an empirical model for the likelihood of an ideal runtime environment execution, a program implementation with predictable level of robustness at a specific level of memory overhead can be deployed.
Double frees and invalid frees are addressed simply by ignoring attempts to Free( ) already free objects which are identified in a free list 625 of
In one exemplary method 1100 of
In cases where uninitialized reads are not of much concern the allocated memory space may be initialized with zeros. This will not detect uninitialized memory reads by the above method but the programs are more likely to terminate.
One exemplary embodiment of a randomized memory manager for managing an approximation of an infinite heap is described herein with reference to
The initialization operation 1200 first obtains free memory from the system using mmap 1210. The heap size 1220 is a parameter to the allocator function DieHardMalloc at 1200, corresponding to the expansion factor M described above. For the replicated version, randomized memory manager (e.g., 200 in
Another aspect of the algorithm is the total separation of heap metadata from heap objects. Many allocators store heap metadata in areas immediately adjacent to allocated objects (e.g., as “boundary tags”). A buffer overflow of just one byte past an allocated space can corrupt the heap, leading to program crashes, unpredictable behavior, or security vulnerabilities. Other allocators place such metadata at the beginning of a page, reducing but not eliminating the likelihood of corruption. Keeping all of the heap metadata separate from the heap protects it from buffer overflows. The heap metadata includes a bitmap for each heap region, where one bit always stands for one object. All bits are initially set to zero, indicating that every object is free. Additionally, the number of objects allocated to each region is tracked by (inUse) at 1215. This number is used to ensure that the number of objects does not exceed the threshold factor of 1/M in the partition.
When an application requests memory from randomized memory manager via a call to DieHardMalloc 1300 in
To defend against erroneous programs, the de-allocator DieHardFree 1400 of
With reference to
The storage 1540 may be removable or non-removable, and includes magnetic disks, magnetic tapes or cassettes, CD-ROMs, CD-RWs, DVDs, or any other medium which can be used to store information and which can be accessed within the computing environment 1500. The storage 1540 stores instructions for the software 1580 implementing methods described herein for improving software robustness through randomized execution contexts.
The input device(s) 1550 may be a touch input device, such as a keyboard, mouse, pen or trackball, a voice input device, a scanning device, or another device, that provides input to the computing environment 1500. For audio, the input device(s) 1550 may be a sound card or similar device that accepts audio input in analog or digital form, or a CD-ROM reader that provides audio samples to the computing environment 1500. The output device(s) 1560 may be a display, printer, speaker, CD-writer, or another device that provides output from the computing environment 1500.
The communication connection(s) 1570 enable communication over a communication medium to another computing entity. The communication medium conveys information, such as computer-executable instructions or other data in a modulated data signal, for instance.
Computer-readable media are any available media that can be accessed within a computing environment 1500. By way of example, and not limitation, with the computing environment 1500, computer-readable media include memory 1520, storage 1540, communication media (not shown), and combinations of any of the above.
In view of the many possible embodiments to which the principles of the disclosed technology may be applied, it should be recognized that the illustrated embodiments are only preferred examples of the technology and should not be taken as limiting the scope of the following claims. We therefore claim all that comes within the scope and spirit of these claims. The disclosed technology is directed toward novel and unobvious features and aspects of the embodiments of the system and methods described herein. The disclosed features and aspects of the embodiments can be used alone or in various novel and unobvious combinations and sub-combinations with one another.
For instance, although the operations of the disclosed methods are described in a particular, sequential order for convenient presentation, it should be understood that this manner of description encompasses rearrangements, unless a particular ordering is required by specific language set forth below. For example, operations described sequentially may in some cases be rearranged or performed concurrently. Some of the steps may be eliminated or other steps added. Moreover, for the sake of simplicity, the disclosed flow charts and block diagrams typically do not show the various ways in which particular methods can be used in conjunction with other methods. Additionally, the detailed description sometimes uses terms like “determine” to describe the disclosed methods. Such terms are high-level abstractions of the actual operations that are performed. The actual operations that correspond to these terms will vary depending on the particular implementation and are readily discernible by one of ordinary skill in the art.
Having described and illustrated the principles of our technology with reference to the illustrated embodiments, it will be recognized that the illustrated embodiments can be modified in arrangement and detail without departing from such principles.