Skip to content

Stack unwinding memory leak. #152898

@ThePatrickCrab

Description

@ThePatrickCrab

Problem Statement

Improper destruction order during stack unwinding can cause a constructed object to never be destructed. If the object manages resources, these resources will never be free'd resulting in a memory leak (see memory leak example). The platform and version that I found this issue on is listed at the bottom of this issue, however the compiler explorer example linked shows the same memory leak on x86-64 clang (trunk).

C++ Standard Reference

Consider the following snippet from C++ Standard Working Draft (generated 2025-08-03) Section 14.3 [except.ctor] Stack unwinding.

struct A { };

struct Y { ~Y() noexcept(false) { throw 0; } };

A f() {
  try {
    A a;
    Y y;
    A b;
    return {};      // #1
  } catch (...) {
  }
  return {};        // #2
}

"At #1, the returned object of type A is constructed. Then, the local variable b is destroyed ([stmt.jump]). Next, the local variable y is destroyed, causing stack unwinding, resulting in the destruction of the returned object, followed by the destruction of the local variable a. Finally, the returned object is constructed again at #2. — end example]"

Stack Unwinding Behavior

With a modified version of the example above, I observed unexpected behavior:

#include <iostream>

struct A {
    A(char c_) : c(c_) { std::cout << "A::ctor(" << this << "): " << this->c << std::endl; }
    ~A() { std::cout << "A::dtor(" << this << "): " << this->c << std::endl; }
    char c;
};

struct Y
{
    Y() { std::cout << "Y::ctor(" << this << ")" << std::endl; }
    ~Y() noexcept(false)
    {
        std::cout << "Y::dtor(" << this << ")" << std::endl;
        throw 0;
    }
};

A foo()
{
    try {
        A a('a');
        Y y;
        A b('b');
        return { 'c' };   // #1
    } catch (...) {
    }
    return { 'd' };       // #2
}

int main()
{
    foo();
}

Destruction According to standard:

  • object b dtor call
  • object y dtor call (throws, causing stack unwinding),
    • unnamed object created on marked line // #1 dtor call
    • object a dtor call

Behavior observed using clang (18.1.3):

  • object b dtor call
  • object y dtor call (throws, causing stack unwinding),
    • object a dtor call

Behavior observed using gcc (13.3.0):

  • object b dtor call
  • object y dtor call (throws, causing stack unwinding),
    • object a dtor call
    • unnamed object created on marked line // #1 dtor call

Neither clang nor gcc behaved as described in the standard, but clang (and apparently msvc? I haven't checked this myself) appears to be missing a destructor call for the unnamed object created on marked line #. IMO this isn't a huge deal for the examples provided so far, but what happens if our object manages resources and this destructor really isn't being called? From my testing, a memory leak is being exposed here due to the missing destructor call.

Memory Leak

Updated snippet adding resources to the object (open in compiler explorer):

#include <array>
#include <memory>

struct A {
    using data_type = std::array<char, 4096>;

    A() : c(), some_data(std::make_unique<data_type>()) {}
    A(char c_) : c(c_), some_data(std::make_unique<data_type>()) {}
    ~A() = default;

    char c;
    std::unique_ptr<data_type> some_data;
};

struct Y { ~Y() noexcept(false) { throw 0; } };

static A foo() {
  try {
    A a('a');
    Y y;
    A b('b');
    return { 'c' };   // #1
  } catch (...) {
  }
  return { 'd' };     // #2
}

int main()
{
    foo();
}

By adding dynamic memory allocation to struct A we now have a function foo() that leaks memory every time it is called. This memory leak is a direct result of the divergence between the behavior described in the standard, and the behavior observed in the clang-compiled binary. Adding a loop to infinitely call foo() causes virtual memory usage to increase until an eventual core dump. The same code compiled with gcc produces a binary that runs forever at a constant virtual memory usage because it actually deletes the object being returned before the exception is thrown.

Compiling with -fsanitize=address,undefined does produce a binary that detects the memory leak, but this memory leak should not exist in this program. If stack unwinding was performed as described in the standard, then there would be no memory leak in this code.

Local Environment Information

General

Running on WSL ubuntu:

$ lsb_release -a
No LSB modules are available.
Distributor ID: Ubuntu
Description:    Ubuntu 24.04.2 LTS
Release:        24.04
Codename:       noble

Local Clang Version

clang++ --version
Ubuntu clang version 18.1.3 (1ubuntu1)
Target: x86_64-pc-linux-gnu
Thread model: posix
InstalledDir: /usr/bin

Installed using:

sudo apt install clang

Local GCC Version

$ g++ --version
g++ (Ubuntu 13.3.0-6ubuntu2~24.04) 13.3.0
Copyright (C) 2023 Free Software Foundation, Inc.
This is free software; see the source for copying conditions.  There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions