Memory safety bugs in C++ code

C++ lets developers work with raw pointers, allowing some performance tricks not available in higher-level languages. By allowing developers to decide when objects are allocated and deallocated, developers have the flexibility to choose between (or mix and match) reference counting, various forms of tracing garbage collection, and ad-hoc calls to "new" and "delete". Developers also have the ability to allocate some objects on the stack rather than the heap, improving performance. While C++ is not the only language that allows stack allocation, it is one of the few that lets you maintain linked lists of stack-allocated objects.

C++ also allows developers to do manual pointer arithmetic, making it possible to steal bits from pointers or implement XOR linked lists. Arrays use implicit pointer arithmetic without bounds-checking, which is nice for performance when bounds-checking would be redundant.

Unfortunately, most C++ compilers do not include theorem provers, so they cannot require you to declare invariants and provide enough proof hints to explain why your use of raw pointers is safe. As a result, it is easy to have bugs that lead to severe security holes.

Common types of memory safety bugs

These memory safety bugs usually manifest themselves as crashes. They're also usually exploitable to run arbitrary code.

  • Using a dangling pointer. A simple read from a dangling pointer usually won't cause too much damage, except perhaps to privacy. But writing to a dangling pointer can corrupt another data structure, and freeing a dangling pointer can leave another data structure open to future corruption. Worst, calling a virtual member function on a dangling pointer will jump to a memory location based on a vtable pointer that is likely to have been overwritten, easily leading to arbitrary code execution. Most of the memory safety bugs I have found in Gecko involve dangling pointers.
  • Buffer overflows, also known as "writing past the end of a string or array". These are the best-known memory safety bugs, and among the first to be exploited to run arbitrary code. They're dangerous whether the array is on the heap or the stack, and whether the overflow is as long as the attacker wants or a single byte.
  • Integer overflows, bugs due to forgetting that what C++ calls "int" is really "integer mod 232" (or 264). If int computation is used to decide how much memory to allocate, overflow can lead to a buffer-overflow situation. If reference counting is implemented using an int counter, overflow can lead to an object being freed prematurely, creating a dangling-pointer situation.

Safe crashes

Several common types of crashes that are not security holes:

  • Dereferencing NULL. Most operating systems never allocate page 0, so userland programs can assume that dereferencing null is a safe crash. This is good because null dereferences are significantly harder to prevent than uses of dangling pointers.
  • Too much recursion. Most operating systems have a guard page at the stack limit to prevent your stack and heap from colliding. This is good because preventing too-much-recursion bugs is hard and has historically not been necessary.

Note that some operating systems have bugs or design flaws that turn these "safe" crashes into security holes. Until recently, Windows had a bug that turned null dereferences in some programs into security holes. And at least as of 2005, some operating systems do not guarantee that null dereferences and too-much-recursion are crashes. IMO, those operating systems need to be fixed, so developers can continue treating null-dereference crashes as having the same severity across operating systems.

Comments are closed.