Двойное освобождение в стандартной библиотеке C++ с использованием только std::function и std::shared_pointer
Недавно я столкнулся со странной ошибкой 9X_g++ double-free в программе при захвате shared_ptr
в лямбда-выражении. Я 9X_g++ смог уменьшить это следующим минимальным 9X_libstdc++ примером:
#include #include struct foo { std::function fun; }; foo& get() { auto f = std::make_shared(); // Create a circular reference by capturing the shared pointer by value f->fun = [f]() {}; return *f; } int main(void) { get().fun = nullptr; return 0; }
Компиляция этого с помощью GCC 9X_cpp 12.2.0 и средства очистки адресов и его 9X_libstdc++ запуск дает двойное освобождение в 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::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::~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*) /usr/include/c++/12.2.0/bits/stl_construct.h:151 #8 0x556a0086830d in void std::allocator_traits >::destroy(std::allocator&, foo*) /usr/include/c++/12.2.0/bits/alloc_traits.h:648 #9 0x556a008680fa in std::_Sp_counted_ptr_inplace, (__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::~__shared_ptr() /usr/include/c++/12.2.0/bits/shared_ptr_base.h:1524 #13 0x556a00866251 in std::shared_ptr::~shared_ptr() /usr/include/c++/12.2.0/bits/shared_ptr.h:175 #14 0x556a008652ad in ~ /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::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 > /usr/include/c++/12.2.0/bits/std_function.h:161 #2 0x556a008657e3 in _M_init_functor > /usr/include/c++/12.2.0/bits/std_function.h:215 #3 0x556a00865719 in function > /usr/include/c++/12.2.0/bits/std_function.h:449 #4 0x556a00865578 in operator= > /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
Как 9X_libstdc++ только функция get
возвращает значение, std::function
внутри 9X_cpp структуры foo
владеет единственным shared_ptr
, которому 9X_undefined-behaviour принадлежит объемлющий объект foo
. Это означает, что 9X_cpp присвоение ему nullptr
должно уничтожить shared_ptr
, что, в 9X_undefined-behavior свою очередь, должно освободить объект foo
.
Кажется, что 9X_compiler-bug здесь происходит то, что вызов delete
в std_function.h:175
сначала 9X_cxx запускает деструктор лямбды, который уничтожает 9X_undefined-behaviour shared_ptr
, объект foo
и заключенный в него объект std::function
перед 9X_undefined-behavior освобождением памяти. Однако уничтожение 9X_libstdc++ объекта std::function
уже освободило эту ячейку памяти, что 9X_cxx привело к двойному освобождению.
Сейчас я 9X_undefined-behavior пытаюсь выяснить, является ли это ошибкой 9X_compiler-bug в реализации стандартной библиотеки (libstdc++
) или 9X_c++ программа где-то вызывает неопределённое 9X_undefined-behaviour поведение.
Индикатором того, что это может 9X_undefined-behavior быть ошибка libstdc++
, является то, что с clang++
и libc++
14.0.6 9X_cxx нет двойного освобождения (или, по крайней 9X_c++ мере, не обнаружено), но clang++
с libstdc++
также есть 9X_cxx проблема с двойным бесплатным выпуском.
Нарушает 9X_compiler-bug ли эта программа какие-либо правила/вызывает 9X_libstdc++ неопределенное поведение в соответствии 9X_cpp с каким-либо стандартом C++?
Все это я воспроизвел 9X_g++ на x86-64 linux машине.
Ответ #1
Ответ на вопрос: Двойное освобождение в стандартной библиотеке C++ с использованием только std::function и std::shared_pointer
Я считаю, что соответствующей частью стандарта 9X_libstdc++ является [res.on.objects], в котором говорится
Если обращение 9X_compiler-bug к объекту стандартного библиотечного типа 9X_c++ и начало жизни объекта не происходит до 9X_undefined-behaviour доступа или доступ не происходит до конца 9X_c++ жизни объекта, поведение не определено, если 9X_g++ не указано иное.
В вашем примере вы получаете 9X_undefined-behaviour доступ к std::function
, назначая ему. Во время этого 9X_compiler-bug доступа вызывается деструктор std::function
, заканчивая 9X_undefined-behaviour время жизни std::function
. Но доступ не завершен, так 9X_undefined-behaviour что это не тот случай, когда доступ происходит 9X_cxx до окончания срока жизни объекта.
Следовательно, код 9X_undefined-behavior имеет неопределенное поведение.
-
3
-
3
-
2
-
4
-
5
-
3
-
4
-
5
-
1
-
8
-
1
-
3
-
1
-
2
-
5
-
1
-
3
-
3
-
3
-
4
-
1
-
2
-
1
-
3
-
2
-
4
-
1
-
10
-
11
-
10
-
9
-
9
-
7
-
6
-
10
-
7
-
4
-
8
-
3
-
4
-
2
-
3
-
2
-
5
-
3
-
2
-
11
-
5
-
4
-
2