[libc++] Heap corruption (double-free) when capturing and rethrowing std::runtime_error via std::exception_ptr on Windows (MSVC ABI)
- Dominant language
- LLVM
- Stars
- 40.5k
- Forks
- 18.7k
- PR merge metrics
- PR metrics pending
Description
### Description
When using Clang with LLVM's `libc++` on Windows targeting the MSVC ABI (`x86_64-pc-windows-msvc`), capturing a `std::runtime_error` with `std::current_exception()` into a `std::exception_ptr` and later rethrowing it with `std::rethrow_exception()` results in a fatal heap corruption crash (`STATUS_HEAP_CORRUPTION`, code `0xC0000374`).
This is caused by a shallow bitwise copy (`std::memcpy`) of `std::runtime_error` inside `libcxx/src/support/runtime/exception_pointer_msvc.ipp`, resulting in a double-free of the error message buffer managed by `__std_exception_data`.
### Environment
- **OS**: Windows 10/11 / Windows Server (x86_64)
- **Compiler**: Clang / Clang++ (LLVM 20.x / 21.x)
- **Target**: `x86_64-pc-windows-msvc`
- **Standard Library**: `libc++` (`c++_shared` / `-stdlib=libc++`) with `_LIBCPP_ABI_VCRUNTIME`
### Minimal Reproducing Example (MCVE)
```cpp
#include
#include
#include
int main() {
std::exception_ptr exceptionPointer;
// 1. Throw and capture into std::exception_ptr
try {
throw std::runtime_error{"Test exception message"};
} catch (...) {
exceptionPointer = std::current_exception();
}
// When exiting the catch block above, the original stack exception is destroyed.
// 2. Rethrow and catch
try {
if (exceptionPointer) {
std::rethrow_exception(exceptionPointer);
}
} catch (const std::exception& exception) {
std::cout << "Caught: " << exception.what() << std::endl;
}
// When exiting this catch block, the rethrown exception copy is destroyed -> CRASH!
return 0;
}
```
### Build & Execution Command
```bash
# Must be built with LLVM's libc++ on Windows (not MSVC STL)
clang++ -std=c++20 -stdlib=libc++ main.cpp -o test.exe
./test.exe
```
*(Note: This issue cannot be reproduced on Compiler Explorer / Godbolt as its Windows runners currently only provide the Microsoft MSVC STL, where this bug is absent.)*
### Actual Behavior / Crash Output
The process crashes with `STATUS_HEAP_CORRUPTION` (`0xC0000374`):
```text
==========================================
CRASH DETECTED: Windows Structured Exception
Description: STATUS_HEAP_CORRUPTION
Code: 0xC0000374
Fault address: 00007FFE6CDD7A45
Stack frames:
[00] ntdll.dll!RtlpFreeHeap
[01] ntdll.dll!RtlFreeHeap
[02] ucrtbase.dll!free
[03] VCRUNTIME140.dll!__std_exception_destroy
[04] CppUtils-UnitTests.exe!std::exception::~exception()
[05] VCRUNTIME140.dll!__CxxFrameHandler4
[06] ntdll.dll!RtlDispatchException
[07] ...
==========================================
```
### Root Cause Analysis
1. **`__std_exception_data` on MSVC ABI**:
Under `_LIBCPP_ABI_VCRUNTIME`, `std::exception` (and by inheritance `std::runtime_error`) uses the MSVC runtime structure defined in ``:
```cpp
struct __std_exception_data {
char const* _What;
bool _DoFree;
};
```
When `std::runtime_error{"..."}` is constructed, `_What` is dynamically allocated via `malloc` and `_DoFree` is set to `true`.
2. **`libc++` fallback to `memcpy` in `exception_pointer_msvc.ipp`**:
In `libcxx/src/support/runtime/exception_pointer_msvc.ipp`, cloning the exception object into the heap-allocated control block relies on `__copy_exception_object`:
```cpp
void __copy_exception_object(void* __dest, const void* __src, const CatchableType* const __type) {
if ((__type->properties & CT_IsSimpleType) || __type->copyFunction == 0) {
std::memcpy(__dest, __src, __type->sizeOrOffset);
return;
}
// ... invoke copyFunction closure ...
}
```
For `std::runtime_error`, Clang does not emit a `copyFunction` entry in the `CatchableType` table (`copyFunction == 0`), causing `libc++` to perform a bitwise `std::memcpy`.
3. **Double Free**:
- The bitwise `std::memcpy` duplicates the `_What` pointer with `_DoFree = true`.
- When the first `catch (...)` block exits, the original stack-based exception is destroyed, invoking `__std_exception_destroy` which calls `free(_What)`.
- When the second `catch` block exits, the rethrown exception copy is destroyed, invoking `__std_exception_destroy` which calls `free(_What)` a second time on the exact same pointer.
- The Windows heap allocator detects the double-free and immediately aborts the process with `STATUS_HEAP_CORRUPTION` (`0xC0000374`).
### Expected Behavior
`std::current_exception()` and `std::rethrow_exception()` should handle `std::runtime_error` without shallow copying pointers that own heap memory, avoiding double-free and memory corruption.
Contributor guide
Research direction
Start by reproducing the Windows MSVC ABI case with the supplied C++20 command, then read libcxx/src/support/runtime/exception_pointer_msvc.ipp, especially __copy_exception_object and its handling of missing copyFunction entries. Trace std::runtime_error through std::current_exception() and std::rethrow_exception(); done means the exception can be rethrown without STATUS_HEAP_CORRUPTION or a double-free.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- cpp
- Domain
- compilers, operating-systems
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 48/100