C++ Use-After-Free (UAF): Causes, ASan, and Ownership Rules

C++ Use-After-Free (UAF): Causes, ASan, and Ownership Rules

이 글의 핵심

UAF causes, AddressSanitizer, ownership with smart pointers, and practical gdb workflows.

What is use-after-free?

Using memory after it has been freed or after the object’s lifetime ended.

int* ptr = new int(10);
delete ptr;
*ptr = 42;  // undefined behavior

Common causes

// 1. Access after delete
int* ptr = new int(10);
delete ptr;
std::cout << *ptr << std::endl;

// 2. Dangling pointer return
int* getPointer() {
    int* ptr = new int(10);
    delete ptr;
    return ptr;
}

// 3. Double free then use
int* ptr = new int(10);
delete ptr;
delete ptr;
*ptr = 42;

// 4. Container invalidation
std::vector<int> vec = {1, 2, 3};
int* ptr = &vec[0];
vec.clear();
*ptr = 42;

Examples

Example 1: Basic UAF

#include <iostream>

void useAfterFree() {
    int* ptr = new int(10);
    std::cout << *ptr << std::endl;
    
    delete ptr;
    std::cout << *ptr << std::endl;  // UAF
}

void safeUse() {
    int* ptr = new int(10);
    std::cout << *ptr << std::endl;
    
    delete ptr;
    ptr = nullptr;
    
    if (ptr) {
        std::cout << *ptr << std::endl;
    }
}

void smartPointer() {
    auto ptr = std::make_unique<int>(10);
    std::cout << *ptr << std::endl;
}

Example 2: Dangling reference

#include <string>

const std::string& getNameBad() {
    std::string name = "Alice";
    return name;
}

std::string getNameGood() {
    std::string name = "Alice";
    return name;
}

Example 3: Iterator / pointer invalidation

#include <vector>

void iteratorInvalidation() {
    std::vector<int> vec = {1, 2, 3};
    int* ptr = &vec[0];
    
    vec.push_back(4);  // may reallocate
    std::cout << *ptr << std::endl;  // possibly UAF
}

void useIndex() {
    std::vector<int> vec = {1, 2, 3};
    size_t index = 0;
    vec.push_back(4);
    std::cout << vec[index] << std::endl;
}

Example 4: Object lifetime

#include <memory>

class Widget {
public:
    int value = 42;
    ~Widget() { }
};

void ownershipTransfer() {
    auto ptr = std::make_unique<Widget>();
    Widget* raw = ptr.get();
    
    ptr.reset();
    std::cout << raw->value << std::endl;  // UAF
}

void keepOwnership() {
    auto ptr = std::make_unique<Widget>();
    std::cout << ptr->value << std::endl;
    ptr.reset();
}

void sharedOwnership() {
    auto ptr1 = std::make_shared<Widget>();
    auto ptr2 = ptr1;
    ptr1.reset();
    std::cout << ptr2->value << std::endl;
}

Common pitfalls

Pitfall 1: Double free

void doubleFree() {
    int* ptr = new int(10);
    delete ptr;
    delete ptr;
    *ptr = 42;
}

void safeFree() {
    int* ptr = new int(10);
    delete ptr;
    ptr = nullptr;
    delete ptr;
}

Pitfall 2: Returning freed pointers

std::unique_ptr<int> createSafe() {
    return std::make_unique<int>(10);
}

Pitfall 3: Lambda capture

#include <functional>

std::function<int()> createGetterGood() {
    int value = 10;
    return [value]() { return value; };
}

std::function<int()> createGetterShared() {
    auto ptr = std::make_shared<int>(10);
    return [ptr]() { return *ptr; };
}

Pitfall 4: Member pointers past container lifetime

class Container {
    int* data;
public:
    Container() : data(new int(10)) {}
    ~Container() { delete data; }
    int* getData() { return data; }
};

void useContainer() {
    int* ptr;
    {
        Container c;
        ptr = c.getData();
    }
    *ptr = 42;  // UAF
}

Detection

g++ -fsanitize=address -g program.cpp
./a.out

valgrind --tool=memcheck ./program
clang-tidy program.cpp
cppcheck --enable=all program.cpp

Mitigation patterns

auto ptr = std::make_unique<int>(10);
delete ptr;
ptr = nullptr;

Debugging tips (MSVC-style example)

#ifdef _DEBUG
void* operator new(size_t size) {
    void* ptr = malloc(size);
    memset(ptr, 0xCD, size);
    return ptr;
}
#endif

Typical UAF root causes

  1. Raw pointer after explicit free: Dereferencing after delete/free; setting nullptr only blocks some mistakes—reuse of the same address can still hide logic bugs.
  2. Ownership vs borrow confusion: Returning interior pointers that outlive the owning container or object.
  3. Iterator/pointer invalidation: Reallocation in vector, mutating string, etc.
  4. Async callbacks: Lambdas capturing stack addresses or raw pointers to destroyed objects.
  5. Double free: Corrupts heap metadata; later operations become UAF or crashes.

Catching UAF early with AddressSanitizer

With ASan, most UAFs are reported immediately with allocation stack, free stack, and access site.

g++ -std=c++17 -O1 -g -fsanitize=address -fno-omit-frame-pointer uaf.cpp -o uaf
ASAN_OPTIONS=abort_on_error=1:detect_stack_use_after_return=1 ./uaf
  • detect_stack_use_after_return=1 catches more stack lifetime bugs (higher overhead).
  • Reports include shadow memory details—compare the nearest delete/free with the failing access.

Valgrind Memcheck also catches UAF but is slower; CI often uses ASan builds.

Smart pointers and ownership

  • std::unique_ptr: Single owner—do not use the old raw pointer after transfer; consider std::exchange to make intent obvious.
  • std::shared_ptr / std::weak_ptr: Break cycles with weak_ptr for caches and graphs.
  • Observers: For non-owning pointers, adopt a consistent convention (e.g. non-owning T* with documented lifetime) and follow C++ Core Guidelines on ownership.

Rule of thumb: If only one place calls delete, UAF from forgotten pairing drops sharply—keep new/delete at library boundaries and use RAII internally.

Real-world patterns

  1. Event loops / timers: If the object is destroyed before the callback, guard with weak_ptr or unregister in the destructor.
  2. C APIs: One place owns free; copying or double-closing handles causes UAF—wrap in RAII and control move/copy.
  3. string_view lifetime: A view must not outlive the owning std::string; store a string if you need persistence.

Workflow: gdb + ASan

  1. Read the ASan ERROR line and source location first.
  2. In gdb, bt full; if lines look wrong due to inlining, try -O0 or disable optimization for that TU.
  3. Trace pointer lifetime: search for alloc/free pairs for the same logical object.
  4. Minimize the repro and add a regression test.

Without ASan, platform debug heaps or allocation hooks help but are costly—prefer an ASan configuration in daily dev.

FAQ

Q1: When does UAF happen?

A: After free, dangling pointers, invalidation.

Q2: Detection?

A: ASan, Valgrind, static analysis.

Q3: Prevention?

A: Smart pointers, clear ownership, RAII.

Q4: Symptoms?

A: Crashes, flaky behavior, corruption.

Q5: Is nullptr check enough?

A: Partially—smart pointers and lifetime discipline are stronger.

Q6: Resources?

A: Effective C++, ASan docs, CWE-416.


See also

  • C++ heap corruption
  • C++ memory leak
  • C++ buffer overflow

Practical tips

Debugging

  • Warnings first
  • Minimal repro

Performance

  • Profile before optimizing

Code review

  • Team conventions

Checklist

Before coding

  • Right fix?
  • Maintainable?
  • Performance OK?

While coding

  • Warnings?
  • Edge cases?
  • Errors?

At review

  • Clear?
  • Tests?
  • Docs?

Keywords

C++, use-after-free, memory, safety, AddressSanitizer, debugging.


  • C++ buffer overflow
  • C++ heap corruption
  • C++ memory leak
  • C++ sanitizers
  • C++ Valgrind