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

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

이 글의 핵심

Understand use-after-free in C++: dangling pointers, container invalidation, ASan and Valgrind, smart pointers, weak_ptr, and a gdb + ASan debugging workflow.

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

다음은 cpp를 활용한 상세한 구현 코드입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

// 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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 조건문으로 분기 처리를 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

다음은 cpp를 활용한 상세한 구현 코드입니다. 필요한 모듈을 import하고, 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

아래 코드는 cpp를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 cpp를 사용한 구현 예제입니다. 필요한 모듈을 import하고. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

#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

다음은 cpp를 활용한 상세한 구현 코드입니다. 클래스를 정의하여 데이터와 기능을 캡슐화하며. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

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

아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

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)

아래 코드는 cpp를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

#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

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.

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3