std::bind and stack-use-after-scope

std::bind and stack-use-after-scope

本文关键字:stack-use-after-scope and bind std      更新时间:2023-10-16

所以,今天,我正在运行一些使用地址消毒剂构建的代码,并偶然发现了一个奇怪的堆栈使用后的错误。我有一个简化的例子:

#include <functional>
class k
{
public: operator int(){return 5;}
};
const int& n(const int& a)
{
  return a;
}
int main()
{
  k l;
  return std::bind(n, l)();
}

Asan抱怨最后一个代码行:

==27575==ERROR: AddressSanitizer: stack-use-after-scope on address 0x7ffeab375210 at pc 0x000000400a01 bp 0x7ffeab3750e0 sp 0x7ffeab3750d8
READ of size 4 at 0x7ffeab375210 thread T0
    #0 0x400a00  (/root/tstb.exe+0x400a00)
    #1 0x7f97ce699730 in __libc_start_main (/lib64/libc.so.6+0x20730)
    #2 0x400a99  (/root/tstb.exe+0x400a99)
Address 0x7ffeab375210 is located in stack of thread T0 at offset 288 in frame
    #0 0x40080f  (/root/tstb.exe+0x40080f)
  This frame has 6 object(s):
    [32, 33) '<unknown>'
    [96, 97) '<unknown>'
    [160, 161) '<unknown>'
    [224, 225) '<unknown>'
    [288, 292) '<unknown>' <== Memory access at offset 288 is inside this variable
    [352, 368) '<unknown>'
HINT: this may be a false positive if your program uses some custom stack unwind mechanism or swapcontext
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope (/root/tstb.exe+0x400a00)
Shadow bytes around the buggy address:
  0x1000556669f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100055666a00: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100055666a10: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 f1 f1
  0x100055666a20: f1 f1 f8 f2 f2 f2 f2 f2 f2 f2 f8 f2 f2 f2 f2 f2
  0x100055666a30: f2 f2 f8 f2 f2 f2 f2 f2 f2 f2 f8 f2 f2 f2 f2 f2
=>0x100055666a40: f2 f2[f8]f2 f2 f2 f2 f2 f2 f2 00 00 f2 f2 f3 f3
  0x100055666a50: f3 f3 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100055666a60: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100055666a70: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100055666a80: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
  0x100055666a90: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07
  Heap left redzone:       fa
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
  Left alloca redzone:     ca
  Right alloca redzone:    cb
==27575==ABORTING

如果我正确理解,它说我们正在访问堆栈变量后,它已经消失了范围。查看未爆炸且不优化的拆卸,我确实看到它发生在实例化的__invoke_impl中:

Dump of assembler code for function std::__invoke_impl<int const&, int const& (*&)(int const&), k&>(std::__invoke_other, int const& (*&)(int const&), k&):
   0x0000000000400847 <+0>:     push   %rbp
   0x0000000000400848 <+1>:     mov    %rsp,%rbp
   0x000000000040084b <+4>:     push   %rbx
   0x000000000040084c <+5>:     sub    $0x28,%rsp
   0x0000000000400850 <+9>:     mov    %rdi,-0x28(%rbp)
   0x0000000000400854 <+13>:    mov    %rsi,-0x30(%rbp)
   0x0000000000400858 <+17>:    mov    -0x28(%rbp),%rax
   0x000000000040085c <+21>:    mov    %rax,%rdi
   0x000000000040085f <+24>:    callq  0x4007a2 <std::forward<int const& (*&)(int const&)>(std::remove_reference<int const& (*&)(int const&)>::type&)>
   0x0000000000400864 <+29>:    mov    (%rax),%rbx
   0x0000000000400867 <+32>:    mov    -0x30(%rbp),%rax
   0x000000000040086b <+36>:    mov    %rax,%rdi
   0x000000000040086e <+39>:    callq  0x4005c4 <std::forward<k&>(std::remove_reference<k&>::type&)>
   0x0000000000400873 <+44>:    mov    %rax,%rdi
   0x0000000000400876 <+47>:    callq  0x40056a <k::operator int()>
   0x000000000040087b <+52>:    mov    %eax,-0x14(%rbp)
   0x000000000040087e <+55>:    lea    -0x14(%rbp),%rax
   0x0000000000400882 <+59>:    mov    %rax,%rdi
   0x0000000000400885 <+62>:    callq  *%rbx
=> 0x0000000000400887 <+64>:    add    $0x28,%rsp
   0x000000000040088b <+68>:    pop    %rbx
   0x000000000040088c <+69>:    pop    %rbp
   0x000000000040088d <+70>:    retq
End of assembler dump.

调用k::operator int()后,它将返回的值放在堆栈上并将其地址传递给n(),该值立即返回它,然后从__invoke_impl本身返回(并一直返回到Main的返回(。

所以,它看起来像是Asan在这里,我们确实有一个堆栈使用后的访问。

问题是:我的代码怎么了?

我尝试使用GCC,Clang和ICC构建它,它们都产生相似的汇编输出。

std::bind基本上生成了一个用所需参数调用绑定函数的实现函数对象。在您的情况下,此实现函数对象大约等于

struct Impl
{
    const int &operator()() const
    {
        int tmp = k_;
        return n(tmp);
    }
private:
    k k_;
    Impl(/*unspecified*/);
};

由于n将其参数返回作为const引用,因此Impl的呼叫操作员将返回对本地变量的引用,该变量是悬挂的参考,然后在main中读取。因此,示波器错误后的堆栈使用。

您的混乱可能源于没有bindreturn n(l);在这里工作正常。但是,在后一种情况下,临时int是在main的堆栈框架中创建的,该列为完整表达式的持续时间,构成了return的参数,该参数已评估为int

换句话说,虽然暂时生存直到创建其创建的完整表达结束,但对于在该完整表达式中调用的函数中生成的临时性并非如此。这些被认为是a 不同的全部表达的一部分,并在评估该表达时被破坏。

ps:因此,将签名R(Args...)的任何功能(对象(绑定到std::function<const R&(Args...)>的任何函数(对象(会导致悬挂参考的保证返回时,称为IMO库在编译时库应拒绝。

<</p> <</p> <</p>

好吧,如果您不知道std::bind的细节。

使用std::bind将参数绑定到可呼叫时,参数的A copy 是女仆(来源(:

绑定的论点是复制或移动的,除非包裹在std :: ref或std :: cref。

std::bind(n, l)返回一个可呼叫的对象的未指定类型的可可对象,该对象具有k类型的成员对象为l的副本。请注意,此可呼叫对象是一个临时的( rvalue (,我给它一个名称: bindtmp

调用后,bindtmp()会创建一个临时( inttemp (整数(5(以将bindtmp::lcopy应用于bindtmp::ncopy(这些是由main::l::n构建的成员对象(。::n在返回语句中的bindtmp() 范围内返回inttemp 的const引用。

这是事物变得棘手的地方(来源(:

每当参考与临时或子对象绑定时,临时的寿命会延长以匹配参考的寿命,但以下例外:
-A 临时绑定到返回语句中函数的返回值的临时绑定:在返回表达式结束时,它立即被销毁。这样的功能总是返回悬而未决的参考。
- ...

这意味着,::n返回后临时inttemp被销毁。

从这一点开始,一切都崩溃了。bindtmp()返回对其寿命已经结束的对象的引用,main尝试并将其转换为 lvalue ,在不确定的行为(使用后从堆栈中使用ODR使用(发生的对象。