I recently came across a weird double-free bug in a program when capturing a shared_ptr
in a lambda. I was able to reduce it this the following minimal example:
#include <memory>
#include <functional>
struct foo {
std::function<void(void)> fun;
};
foo& get() {
auto f = std::make_shared<foo>();
// Create a circular reference by capturing the shared pointer by value
f->fun = [f]() {};
return *f;
}
int main(void) {
get().fun = nullptr;
return 0;
}
Compiling this with GCC 12.2.0 and the address sanitizer and running it, produces a double-free in std::function
:
$ g -fsanitize=address -g -Wall -Wextra -o main main.cpp && ./main
=================================================================
==2401674==ERROR: AddressSanitizer: attempting double-free on 0x602000000010 in thread T0:
#0 0x7f7064ac178a in operator delete(void*, unsigned long) /usr/src/debug/gcc/libsanitizer/asan/asan_new_delete.cpp:164
#1 0x556a00865b9d in _M_destroy /usr/include/c /12.2.0/bits/std_function.h:175
#2 0x556a00865abe in _M_manager /usr/include/c /12.2.0/bits/std_function.h:203
#3 0x556a008658b9 in _M_manager /usr/include/c /12.2.0/bits/std_function.h:282
#4 0x556a00866623 in std::function<void ()>::operator=(decltype(nullptr)) /usr/include/c /12.2.0/bits/std_function.h:505
#5 0x556a008654b5 in main /tmp/cpp/main.cpp:16
#6 0x7f706443c28f (/usr/lib/libc.so.6 0x2328f)
#7 0x7f706443c349 in __libc_start_main (/usr/lib/libc.so.6 0x23349)
#8 0x556a008651b4 in _start ../sysdeps/x86_64/start.S:115
0x602000000010 is located 0 bytes inside of 16-byte region [0x602000000010,0x602000000020)
freed by thread T0 here:
#0 0x7f7064ac178a in operator delete(void*, unsigned long) /usr/src/debug/gcc/libsanitizer/asan/asan_new_delete.cpp:164
#1 0x556a00865b9d in _M_destroy /usr/include/c /12.2.0/bits/std_function.h:175
#2 0x556a00865abe in _M_manager /usr/include/c /12.2.0/bits/std_function.h:203
#3 0x556a008658b9 in _M_manager /usr/include/c /12.2.0/bits/std_function.h:282
#4 0x556a00866215 in std::_Function_base::~_Function_base() /usr/include/c /12.2.0/bits/std_function.h:244
#5 0x556a00866579 in std::function<void ()>::~function() /usr/include/c /12.2.0/bits/std_function.h:334
#6 0x556a00868337 in foo::~foo() /tmp/cpp/main.cpp:4
#7 0x556a00868352 in void std::_Destroy<foo>(foo*) /usr/include/c /12.2.0/bits/stl_construct.h:151
#8 0x556a0086830d in void std::allocator_traits<std::allocator<void> >::destroy<foo>(std::allocator<void>&, foo*) /usr/include/c /12.2.0/bits/alloc_traits.h:648
#9 0x556a008680fa in std::_Sp_counted_ptr_inplace<foo, std::allocator<void>, (__gnu_cxx::_Lock_policy)2>::_M_dispose() /usr/include/c /12.2.0/bits/shared_ptr_base.h:613
#10 0x556a00866005 in std::_Sp_counted_base<(__gnu_cxx::_Lock_policy)2>::_M_release() /usr/include/c /12.2.0/bits/shared_ptr_base.h:346
#11 0x556a008664c5 in std::__shared_count<(__gnu_cxx::_Lock_policy)2>::~__shared_count() /usr/include/c /12.2.0/bits/shared_ptr_base.h:1071
#12 0x556a00866235 in std::__shared_ptr<foo, (__gnu_cxx::_Lock_policy)2>::~__shared_ptr() /usr/include/c /12.2.0/bits/shared_ptr_base.h:1524
#13 0x556a00866251 in std::shared_ptr<foo>::~shared_ptr() /usr/include/c /12.2.0/bits/shared_ptr.h:175
#14 0x556a008652ad in ~<lambda> /tmp/cpp/main.cpp:10
#15 0x556a00865b90 in _M_destroy /usr/include/c /12.2.0/bits/std_function.h:175
#16 0x556a00865abe in _M_manager /usr/include/c /12.2.0/bits/std_function.h:203
#17 0x556a008658b9 in _M_manager /usr/include/c /12.2.0/bits/std_function.h:282
#18 0x556a00866623 in std::function<void ()>::operator=(decltype(nullptr)) /usr/include/c /12.2.0/bits/std_function.h:505
#19 0x556a008654b5 in main /tmp/cpp/main.cpp:16
#20 0x7f706443c28f (/usr/lib/libc.so.6 0x2328f)
previously allocated by thread T0 here:
#0 0x7f7064ac0672 in operator new(unsigned long) /usr/src/debug/gcc/libsanitizer/asan/asan_new_delete.cpp:95
#1 0x556a00865906 in _M_create<get()::<lambda()> > /usr/include/c /12.2.0/bits/std_function.h:161
#2 0x556a008657e3 in _M_init_functor<get()::<lambda()> > /usr/include/c /12.2.0/bits/std_function.h:215
#3 0x556a00865719 in function<get()::<lambda()> > /usr/include/c /12.2.0/bits/std_function.h:449
#4 0x556a00865578 in operator=<get()::<lambda()> > /usr/include/c /12.2.0/bits/std_function.h:534
#5 0x556a008653aa in get() /tmp/cpp/main.cpp:10
#6 0x556a008654a8 in main /tmp/cpp/main.cpp:16
#7 0x7f706443c28f (/usr/lib/libc.so.6 0x2328f)
SUMMARY: AddressSanitizer: double-free /usr/src/debug/gcc/libsanitizer/asan/asan_new_delete.cpp:164 in operator delete(void*, unsigned long)
==2401674==ABORTING
Once the get
function returns, the std::function
inside the foo
struct owns the only shared_ptr
that owns the enclosing foo
object.
This means, assigning nullptr
to it should destroy the shared_ptr
which in turn should free the foo
object.
What seems to be happening here is that the delete
call in std_function.h:175
first runs the destructor of the lambda, which destroys the shared_ptr
, the foo
object, and its enclosed std::function
object before freeing memory.
However, the destruction of the std::function
object has now already freed that memory location, leading to the double-free.
I'm now trying to figure out whether this is a bug in the standard library implementation (libstdc
) or whether the program is triggering undefined behavior somewhere.
An indicator that this might be a libstdc
bug is that with clang
and libc
14.0.6, there is no double-free (or at least none is detected), but clang
with libstdc
does have the double-free issue as well.
Is this program breaking any rules/triggering undefined behavior according to any C standard?
I reproduced all this on an x86-64 linux machine.
CodePudding user response:
I believe the relevant part of the standard is [res.on.objects], which states
If an object of a standard library type is accessed, and the beginning of the object's lifetime does not happen before the access, or the access does not happen before the end of the object's lifetime, the behavior is undefined unless otherwise specified.
In your example, you access the std::function
by assigning to it. During this access, the destructor of the std::function
is called, ending the lifetime of the std::function
. But the access has not finished, so it is not the case that the access happens before the end of the object's lifetime.
Therefore, the code has undefined behavior.