CETS: Compiler-Enforced Temporal Safety for C Santosh Nagarakatte, Jianzhou Zhao, Milo M. K. Martin, Steve Zdancewic Lara Khamisy, Kevin Matthews, and Chris Pratt
Introduction Description: Compiler-enforced temporal safety (CERT) for C programs. Background: Temporal memory safety errors are a prevalent source of software bugs in unmanaged languages such as C. Existing schemes that attempt to retrofit temporal safety for such languages have high runtime overheads and/or are incomplete, thereby limiting their effectiveness as debugging aid. Solution: CETS is a pass that will instrument IR to detect all temporal safety violations. Background: Temporal memory safety errors, such as dangling pointer dereferences and double frees, are a prevalent source of software bugs in unmanaged languages such as C. Existing schemes that attempt to retrofit temporal safety for such languages have high runtime overheads and/or are incomplete, thereby limiting their effectiveness as debugging aids
Overview of Memory Violations Spatial - the pointer refers to the wrong place in the address space Buffer overflow Dereference uninitialized pointer Temporal - the place in the address space is no longer valid Dangling pointer dereference Double free Both types of memory errors can result in crashes, silent data corruption, and severe security vulnerabilities.
Examples of Temporal Safety Violations Heap-based int *a, *b; a = malloc(8); … b = a; ... free(a); … = *b; Stack-based int *a; void foo() { int b; a = &b; } int main() { foo(); … = *a; Both examples are dereferencing a deallocated object Explanation in figure 1 of paper
Motivation for Detecting Temporal Safety Violations C/C++ provide low-level control/management of memory in OS, embedded software, etc Lack of bounds checking and manual memory management leads to temporal safety violations Temporal safety violations lead to crashes, silent data corruption, and severe security vulnerabilities Temporal safety violations include: Dangling pointer references (dereferencing a deallocated object that hasn’t been set to nullptr) Double frees (calling free on a dangling pointer) Invalid frees (calling free with a non-heap address)
Issues with Other Methods of Detecting Temporal Violations (e. g Issues with Other Methods of Detecting Temporal Violations (e.g. Valgrind Memcheck) High runtime overhead High memory overhead Failure to detect all temporal errors To the stack Reallocated heap addresses Arbitrary casts Requiring annotations inserted by the programmer
Program Instrumentation for Detecting Temporal Safety Violation Binary Instrumentation Hardware-assisted Instrumentation Source-level Instrumentation Compiler-based Instrumentation Binary instrumentation operates on an executable and produces a new one - operates on already linked libraries and even when source code is not available, but can lead to high runtime overhead Hardware-assisted would have the same benefits as binary level instrumentation without the runtime overhead, but major drawback is that it requires entirely new hardware Source-level happens before compilation and thus is independent of any specific compiler or instruction set, but makes it harder for compiler to optimize the additional memory operations Compiler-based allows compiler to make its standard optimizations before inserting checks and then again after the checks have been inserted
CETS Approach Compiler-based Instrumentation Apply Optimizations Apply pass to insert checks Apply Optimizations Again C Program Optimized IR with Checks IR of Program IR with Checks Binary instrumentation operates on an executable and produces a new one - operates on already linked libraries and even when source code is not available, but can lead to high runtime overhead Hardware-assisted would have the same benefits as binary level instrumentation without the runtime overhead, but major drawback is that it requires entirely new hardware Source-level happens before compilation and thus is independent of any specific compiler or instruction set, but makes it harder for compiler to optimize the additional memory operations Compiler-based allows compiler to make its standard optimizations before inserting checks and then again after the checks have been inserted
Quick Look at CETS Functionality and Limitations CETS uses compiler based instrumentation. Identifier based scheme, which assigns a unique key for each allocation region to identify dangling pointers Pointers are tracked using disjoint shadowspace (in order not to affect the program memory layout) Limitations The method does not detect spatial violations Must be combined with existing spatial safety mechanisms for 100% memory safety. When you compile your program and you allocate malloc and so on, your program occupies memory and allocate memory There's a layout in which addresses occupy your memory if you instrument your program and the instrumentation you added needs to allocate memory to your array for heap address. if your program allocate memory in heap and instrumentation code allocate memory on the heap, both of them are using the memory, you want to keep the layout exactly the same for every pointer they will create an array instead they do it in a different memory space so they don’t corrupt the memory space you can tell OS occupy memory from x to y. for the instrument action code allocate memory in a different space they're completely disjoint.
Lock-and-Key Identifier Based Approach CETS augments each pointer with two additional word-sized fields: (1) a unique allocation key and (2) a lock address that points to a lock location data Per Pointer Metadata Heap Allocation ptr1 = malloc(size); ptr1 ptr1_key ptr1_key lock address ptr1_key = counter++; ptr1_lock_addr = allocate_lock(); *(ptr1_lock_addr) = ptr1_key; freeable_ptrs_map.insert(ptr1_key, ptr1); lock ptr1_key The shaded area shows the instrumentation code added by the compiler pass. Each pointer object is associated with two fields - a key field and lock-address field. We keep a 64 bit counter which is incremented with each allocation so that each pointer has a unique key. When memory is allocated with a malloc call a unique key is assigned, and a lock location is allocated. The lock location is initialized with the ptr_key. CETS maintains mappings of keys that are freeable. On allocation requests malloc instructions are inserted to the list. Memory
Lock-and-Key Identifier Based Approach - Cont. CETS propagates metadata to newly allocated pointer to the same memory location data Per Pointer Metadata Pointer metadata propagation ptr2 = ptr1 + offset; ptr1 ptr1_key ptr1_key lock address ptr2_key = ptr1_key ptr2_lock_addr = ptr1_lock_addr; ptr1_key lock address ptr2 ptr1_key lock Key1 Shaded area shows instrumentation code added by the compiler pass Spatial check insures ptr1+offset is checked not to exceed bounds Ptr2 points to the same memory allocated previously and assigned to ptr1 Memory
Lock-and-Key Identifier Based Approach - Cont. CETS performs dangling pointer check data Per Pointer Metadata Dangling pointer check ptr1_key lock address if (ptr1_key != *(ptr1_lock_addr) { abort(); } ptr1 ptr1_key ptr1_key lock address ptr2 ptr1_key value = *ptr1; // original load lock ptr1_key Shaded area shows instrumentation code added by the compiler pass When memory is referenced we check if the key associated with the pointer is equal to the key stored in the lock location Memory
Lock-and-Key Identifier Based Approach - Cont. Heap deallocation actions data Per Pointer Metadata Heap deallocation ptr1_key lock address if (freeable_ptrs_map.lookup(ptr1_key) != ptr1) { abort(); } freeable_ptrs_map.remove(ptr1_key); ptr1 ptr1_key ptr1_key lock address ptr2 ptr1_key free(ptr1); *(ptr1_lock_addr) = INVALID_KEY; deallocate_lock(ptr1_lock_addr); lock ptr1_key Shaded area shows instrumentation code added by the compiler pass Check if ptr1 being freed is in freeable pointer map, and if so remove it from the map Set the lock location associated with the pointer being freed to invalid Memory
Call Stack Based Allocation / Deallocation void func() { // Function prologue To detect dangling pointers to the call stack, a key and corresponding lock address is also associated with each stack frame This key and lock address pair is given to any pointer derived from the stack pointer (and thus points to an object on the stack). local_key = next_key++; local_lock_addr++; // allocate lock address *(local_lock_addr) = local_key; int var; // local variable ptr = &var; // ptr defined in main ptr_key = local_key; ptr_lock_addr = local_lock_addr; // Function epilogue *(local_lock_addr) = INVALID_KEY; local_lock_addr--; // deallocate lock address }
Benchmarks The key advantage of compiler based is being able to perform optimizations before and after we insert instrumentation code No temporal checks are required for any pointer that is directly derived from the stack pointer within the corresponding function call, because the stack frame is guaranteed to live until the function exits Functional correctness: CETS successfully detected all temporal errors without false violations. Performance: Overall runtime overhead of 48% Compared to 77% with alternate implementation 116% when checking for spatial and temporal 122% overhead from other checker, such as Valgrind Memcheck
CETS Optimizations Unnecessary checks Redundant checks Pointers to globals Stack pointers within a function call Redundant checks
Conclusion CETS is a compiler based temporal safety detection method It allows for optimizations pre and post instrumentation code addition It doesn’t change the memory layout of the original program (compared with source code instrumentation methods) It was run correctly on NIST-SAMATE benchmark and was able to find all temporal errors By doing post IR instrumentation optimizations passes it was shown to produce 48% overhead, compared with existing methods of 77%.
Thank You Questions?