Aussie AI Blog
Use-After-Free Memory Safety in C++
-
November 2nd, 2024
-
by David Spuler, Ph.D.
Use-After-Free Memory Safety in C++
Use-after-free errors arise when heap memory is de-allocated, but there is still
a pointer to that address.
This becomes a "dangling pointer" (or "dangling reference") and any use of that memory via the pointer
is a "use-after-free" error.
Note that the word "free" means any memory deallocation primitive,
such as the free
function or the delete
operator.
There are several problems with use-after-free errors:
- Crashes
- Insidious program errors
- Portability issues
- Security exploits
Programs with use-after-free errors often exhibit unpredictable behavior with intermittent failures. They may also work fine on one platform, but crash when ported to a different platform, or when the optimizer level is turned up.
Use-After-Free Security Vulnerabilities
Surprisingly, use-after-free errors in heap memory are a very common security vulnerability, second only to buffer overflow attacks on stack memory. The attack involves these steps:
(a) Intentionally triggering a problematic free to gain a dangling pointer,
(b) Waiting for something important to get allocated into the previously-freed memory, and
(c) Accessing or modifying the important data (e.g., Unix suid bits) via the dangling pointer.
This sounds very complicated and unwieldy, but it's been a very successful method of targeting vulnerabilities in C++ software.
Detecting Use-After-Free
The methods to detect use-after-free errors include:
- Memory sanitizer runtime tools
- Memory tagging
- Memory poisoning (magic bytes)
- Hardware-assisted memory block exceptions
The main way to detect these sorts of errors is to use memory sanitizers, such as Valgrind or AddressSanitizer. These tools are very good at this stuff, and you should be running them in your nightly builds with a full regression test suite. It's also useful to run these tools when using "fuzzing" (testing with many large random inputs), as a way to detect these memory errors on unexpected inputs.
Some of the ways to reduce these errors, or to mitigate them as a security attack vector, include:
- Never-free policies (where possible).
- Delayed-free policies (with various configurations).
- Random delayed free (less predictable delayed-free sequences).
Note that if you change the memory deallocation policy, you need to do it at a low level,
such as in your own custom memory allocators, or in debug wrappers for allocation functions.
You can't just comment out all the delete
statements in destructors,
becauses it's sometimes important that the destructors for these sub-objects can still run.
The idea of never deallocating any memory is horror-inspiring for most programmers. However, it's a plausible idea for short batch programs that aren't hanging around long enough for the leaks to matter.
Also, one particular case is that you can disable memory deallocation whenever the program is shutting down, whether it's a batch program or a long-running service. Program termination commonly triggers a huge volume of deallocation requests in destructors for stack, heap, and global objects, making it a fertile field for memory deallocation errors, not to mention that it also causes slow program exits! Plenty of inadequately tested programs will crash on exit due to earlier heap corruptions. And yet, these deallocations don't actually matter because the operating system will reclaim all the memory once the program shuts down.
Double Deallocation Errors
One special case of the use-after-free error is a double-free
or a double-delete
.
A program crash is likely from this:
char *s = (char*)malloc(100); free(s); free(s); // Boom!
One minor mitigation is to clear the pointer to null whenever using any deallocation:
free(s); s = NULL; // safety
Hence, the second call will do free(NULL)
, which is not a crash,
and supposedly harmless according to the standards.
You can do self-referential macro tricks with the comma operator:
#define free(s) ( free((s)), (s) = NULL )
Another way is that you can define wrapper functions with reference paramters:
#define free free_wrapper inline void free_wrapper(void *&v) { free(v); v = NULL; // change reference parameter }
However, it's harder to do these types of tricks
for the delete
operator
because its syntax is not function-like.
If only C++ had a more powerful preprocessor mode!
Related Memory Safety Blog Articles
See also these articles:
- Canary Values & Redzones for Memory-Safe C++
- DIY Preventive C++ Memory Safety
- Array Bounds Violations and Memory Safe C++
- Poisoning Memory Blocks for Safer C++
- Uninitialized Memory Safety in C++
- DIY Memory Safety in C++
- CUDA C++ Floating Point Exceptions
- Memory Safe C++ Library Functions
- Smart Stack Buffers for Memory Safe C++
- Safe C++ Text Buffers with snprintf
Safe C++ Book
The new Safe C++ coding book by David Spuler:
Get your copy from Amazon: Safe C++: Fixing Memory Safety Issues |
More AI Research Topics
Read more about: