当前位置: 首页 > 知识库问答 >
问题:

在C标准库中,仅使用std::function和std::shared_pointer进行双重释放

周健
2023-03-14

最近,我在一个程序中捕获lambda中的shared_ptr时,遇到了一个奇怪的双重免费bug。我可以用以下最小的例子来减少它:

#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;
}

使用GCC 12.2.0和地址清理器编译并运行它,会在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

一旦get函数返回,foo结构中的std::function拥有唯一拥有封闭的foo对象的shared_ptr。这意味着,将nullptr分配给它应该会破坏shared_ptr,这反过来会释放foo对象。

这里发生的似乎是std_function中的delete调用。h: 175首先运行lambda的析构函数,它在释放内存之前销毁shared_ptrfooobject及其附带的 std::function对象现在已经释放了该内存位置,从而导致双空闲。

我现在试图弄清楚这是标准库实现(libstdc)中的一个bug,还是程序在某处触发了未定义的行为。

这可能是< code>libstdc 错误的一个标志是,在< code>clang 和< code>libc 14.0.6中,没有双释放(或者至少没有检测到),但是< code > clang with < code > libstdc 也有双释放问题。

这个程序是否违反了任何规则/根据任何C标准触发未定义的行为?

我在x86-64 linux机器上复制了所有这些。

共有1个答案

郭和硕
2023-03-14

我认为该标准的相关部分是[物体上的物体],其中规定

如果访问了标准库类型的对象,并且对象生存期的开始在访问之前不会发生,或者访问未在对象的生存期结束之前发生,则除非另有指定,否则行为未定义。

在您的示例中,您可以通过分配给 std:: 函数来访问它。在此访问期间,将调用 std::函数的析构函数,结束 std::函数的生存期。但是访问尚未完成,因此访问不会在对象的生存期结束之前发生。

因此,代码具有未定义的行为。

 类似资料:
  • 标准库函数bind()和function()定义于头文件<functional>中(该头文件还包括许多其他函数对象),用于处理函数及函数参数。bind()接受一个函数(或者函数对象,或者任何你可以通过”(…)”符号调用的事物),生成一个其有某一个或多个函数参数被“绑定”或重新组织的函数对象。(译注:顾名思义,bind()函数的意义就像它的函数名一样,是用来绑定函数调用的某些参数的。)例如: int

  • 在设计回调函数的时候,无可避免地会接触到可回调对象。在C++11中,提供了std::function和std::bind两个方法来对可回调对象进行统一和封装。 可调用对象 C++中有如下几种可调用对象:函数、函数指针、lambda表达式、bind对象、函数对象。其中,lambda表达式和bind对象是C++11标准中提出的(bind机制并不是新标准中首次提出,而是对旧版本中bind1st和bind

  • 所有报价均来自N3797。 4/3[conv] 表达式e可以隐式转换为类型T,当且仅当声明T=e;对于一些发明的临时变量t来说 这意味着任何表达式都不能隐式转换为,因为对于所有表达式都是非法的。即使是类型的表达式,例如,也是如此。 因此,类型的表达式不能隐式转换为。 这让我们想到: 20.9.2/2要求[func.require] 将INVOKE(f, t1, t2,..., tN, R)定义为隐

  • 在标准库中,tuple(一个N元组:N-tuple)被定义为N个值的有序序列。在这里,N可以是从0到文件中所定义的最大值中的任何一个常数。你可以认为tuple是一个未命名的结构体,该结构体包含了特定的tuple元素类型的数据成员。特别需要指出的是,tuple中元素是被紧密地存储的(位于连续的内存区域),而不是链式结构。 可以显式地声明tuple的元素类型,也可以通过make_tuple()来推断出

  • 我在理解条件变量及其在互斥体中的使用时遇到了一些困难,我希望社区能帮助我。请注意,我来自win32背景,因此与CRITICAL_SECTION、HANDLE、SetEvent、WaitForMultipleObject等一起使用。 这是我第一次尝试使用C++11标准库进行并发操作,它是在这里找到的一个程序示例的修改版本。 关于这个的几个问题。 我读过“任何要等待std::condition_var

  • 在我的Fedora 34环境(g)中,定义为: 如果表达式已经是右值,那么