【问题标题】:std::bind and stack-use-after-scopestd::bind 和 stack-use-after-scope
【发布时间】:2018-02-12 14:58:34
【问题描述】:

所以,今天我在运行一些使用 Address Sanitizer 构建的代码时,偶然发现了一个奇怪的 stack-use-after-scope 错误。 我有这个简化的例子:

#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 构建它,它们都产生类似的汇编器输出。

【问题讨论】:

  • 我无法重现此内容,但可能是 UB。您是否尝试过不将n 作为参考,看看您是否仍然获得stack-use-after-scope
  • 这有什么奇怪的?您的bind(...)() 表达式返回对int 的常量引用。 int 应该住在哪里?

标签: c++ c++11


【解决方案1】:

std::bind 本质上会生成一个实现函数对象,该对象调用具有所需参数的绑定函数。在你的情况下,这个实现函数对象大约相当于

struct Impl
{
    const int &operator()() const
    {
        int tmp = k_;
        return n(tmp);
    }

private:
    k k_;

    Impl(/*unspecified*/);
};

由于n 将其参数作为常量引用返回,Impl 的调用运算符将​​返回对局部变量的引用,这是一个悬空引用,然后从main 中读取。因此在范围错误后使用堆栈。

您的困惑可能源于没有bindreturn n(l); 预计在这里可以正常工作。然而,在后一种情况下,临时的int 是在main 的堆栈帧中创建的,在构成return 参数的完整表达式的持续时间内存在,它被评估为int

换句话说,虽然临时对象会一直存在到创建它的完整表达式的末尾,但在该完整表达式中调用的函数内部生成的临时对象并非如此。这些被视为不同完整表达式的一部分,并在该表达式被求值时被销毁。

PS:出于这个原因,将签名R(Args...) 的任何函数(对象)绑定到std::function&lt;const R&amp;(Args...)&gt; 会导致在调用时保证返回悬空引用——IMO 库应在编译时拒绝该构造。

【讨论】:

  • 你的解释比我的更容易理解。 +1
  • 实际上,我的困惑源于 cppreference 关于 bind Calling this wrapper is equivalent to invoking f with some of its arguments bound to args. 的引用,我将其解释为 calling bindtemp() must be equivalent to calling n(l)。查看标准,似乎并非如此。看起来bindtemp()实现 必须是as-if,它调用std::invoke(n, l)。有了这种解释,现在就有意义了,实现被定义为与您的示例中的实现等价,并且我们确实可以保证获得一个悬空引用。
【解决方案2】:

好吧,如果您不了解有关 std::bind 的详细信息,这将是一个棘手的问题。

当使用std::bind 将参数绑定到可调用对象时,参数的副本 是 maid (source):

bind 的参数被复制或移动,并且永远不会通过引用传递,除非包装在 std::ref 或 std::cref 中。

std::bind(n, l) 返回一个未指定类型的可调用对象,该对象具有类型为k 的成员对象构建为l 的副本。请注意这个可调用对象是一个临时的(一个rvalue)我会给它一个名字:bindtmp

当被调用时,bindtmp() 创建一个临时 (inttemp) 整数 (5) 以便将 bindtmp::lcopy 应用于 bindtmp::ncopy(这些是从 main::l 和 @ 构造的成员对象987654332@)。 ::n 在 return 语句中返回对 inttemp 的 const 引用bindtmp() 的范围内

这就是事情变得棘手的地方 (source):

每当引用绑定到临时对象或其子对象时,临时对象的生命周期都会延长以匹配引用的生命周期,但以下情况除外:
- 在 return 语句中绑定到函数的返回值的临时变量没有被扩展:它在 return 表达式的末尾被立即销毁。这样的函数总是返回一个悬空引用。
- ...

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

从这一点开始,一切都崩溃了。 bindtmp() 返回对生命周期已结束的对象的引用,main 尝试将其转换为 lvalue,这就是未定义的行为(odr-use of an object from the stack after its使用)发生。

【讨论】:

  • 感谢您的回答,现在一切都清楚了。一件事让我很烦恼:你提到的很多事情都是bind() 实现的细节。它的合同中不应该有禁止这种方式使用它的东西吗?我的意思是,看起来在其他一些实现中bindtmp 可以将bindtmp::lcopy() 的结果存储在其成员中,而不是创建临时inttemp。不能指望bind() 的用户知道这一点。
  • @SergeyStepanov 该错误来自::n 返回对临时对象的(const)引用,而不是来自std::bind 的临时对象不存储副本的事实。事实上,它确实将其参数的副本存储为成员对象(这不是实现细节,这是指定的)。
  • inttemp 不是::n 范围内的临时对象。我的意思是,::n 收到一个引用并将其返回。唯一知道它是临时的是bindtmp(),它创建了它。我知道bind() 存储其参数的副本,我的意思是它可以存储k::operator int() 的结果,而不是使其成为临时的。
猜你喜欢
  • 2014-12-17
  • 1970-01-01
  • 2013-04-16
  • 1970-01-01
  • 1970-01-01
  • 2023-03-16
  • 1970-01-01
  • 2021-12-30
  • 1970-01-01
相关资源
最近更新 更多