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:

Safe C++ Book



Safe C++: Fixing Memory Safety Issues The new Safe C++ coding book by David Spuler:
  • Memory Safety
  • Rust versus C++
  • The Safe C++ Standard
  • Pragmatic Memory Safety

Get your copy from Amazon: Safe C++: Fixing Memory Safety Issues

More AI Research Topics

Read more about: