Introduction

Significant production software is written in languages which provide powerful memory management primitives (like C/C++). Software is written in collaborative fashion and on tight deadlines and bugs do get introduced. A subset of these bugs introduce issues commonly termed as "memory safety" issues. The root causes of memory safety issues are numerous but eventually such issues surface as invalid memory accesses with regards to the language specification.

Programs use pointers to access memory. We refer to the parts of the programs in which accessing an object is valid as its "lifetime". An object’s lifetime depends on the region of memory it is stored on:

  • Heap: A memory allocator uses operating system facilities to create large memory mappings and expose APIs so that software can manage (allocate and free) memory at their convenience. As an example malloc allocates a specified number of bytes and returns a pointer to the allocated memory; free frees the memory so that allocator can recycle that memory. Depending on the operating system, there might be many similar APIs (e.g. calloc, memalign, etc.), but they are always paired with a corresponding free call. The lifetime of an object on the heap is between malloc (or similar) and free.

  • Stack: The operating system allocates a region to be used as the stack of the program. When a function is called, a stack frame is created at the top of the stack. This is used to hold local variables, which are accessed relative to the stack pointer, which designates the top of the stack. Upon return from the function, the frame is popped from the stack by adjusting the stack pointer. Within a stack frame, the compiler can reuse the same memory for local variables that have non-overlapping lifetimes (known as stack coloring, implemented e.g. in LLVM StackColoring cite:[LLVM_StackColoring]). The lifetime of an object on the stack is governed by the programming language’s scoping rules (e.g. C++ scoping rules cite:[CppScope]).

  • Globals: Storage for global variables is allocated by the operating system on process startup (or when the dynamic object is loaded). Generally, the storage for globals is destroyed on process shutdown (depending on the OS, dlclose might cause it to be destoyed earlier).

In addition, an object has a size that represents the amount of memory allocated for it, and except for reassignment, a pointer must always point to memory of the object it was previously assigned to point to.

Two prominent patterns of invalid memory accesses are:

  • Temporal: A pointer is dereferenced past the lifetime of the object it refers to. With respect to heap memory, such issues are commonly called "use after free" (UAF). In case of stack, such issues are termed as "stack use after return", "stack use after scope".

  • Spatial: A pointer outside of the bounds of the object it refers to be created, leading to invalid memory access when dereferenced. This is commonly referred to as a "buffer overflow" (sometimes also "buffer underflow"), or "out of bounds".

To catch these issues, we can check memory accesses for validity at runtime. We do this by employing a lock and key scheme. Memory is assigned a memory tag (lock). Each pointer is assigned a pointer tag (key) in its unused top bits. On access, the key needs to match the lock, or the program is terminated.

When the lifetime of an object starts (e.g. malloc is called), we assign the same tag as a memory tag (lock) for the size bytes representing the object, and pointer tag (key). Subsequent in-bounds accesses using that pointer are valid. When the lifetime of the object ends (e.g. free is called), we change the memory tag (lock). Subsequent accesses through the pointer (which remains unchanged) will be invalid and terminate the program.

Existing software mechanisms using compiler instrumentation and runtime support exist:

  • HWAddressSanitizer (HWAsan) cite:[HWASAN] which uses pointer-masking to implement the idea explained above.

  • AddressSanitizer (ASan) cite:[ASAN] which is fully implemented in software, but in a different way to the idea above.

However software approach has significant cost from performance perspective and are mostly limited to unit test, developer test or special test harness (like fuzzing) environments. Hardware assistance in management and checking of tags on memory accesses helps in more scalable testing of software in variety of deployment scenarios including production.

Leveraging this software usage model, the Zimt extension defines a mechanism for tagged pointer construction, tag management instructions to store tags and tag checks on memory accesses (pointer dereferences). Svatag and Smvatag extensions defines tag storage mechanism in virtual memory along with protection mechanisms for tag storage.