Repository: llvm/llvm-project

Issue:
C++20 coroutine with stateful allocator results in ASAN stack-use-after-scope starting with clang 19
When trying to upgrade to clang 19, we ran into an issue where an ASAN violation is reported in code using a stateful allocator on a promise type. I've tried to reduce the repro in https://godbolt.org/z/soPP6x9rx which shows 18 and 19 side-by-side. It looks like the `stack-use-after-scope` occurs during the construction of the coroutine frame.

```
==1==ERROR: AddressSanitizer: stack-use-after-scope on address 0x723e446091a0 at pc 0x5842f8f8642e bp 0x7fff8c00cc10 sp 0x7fff8c00cc08
READ of size 16 at 0x723e446091a0 thread T0
    #0 0x5842f8f8642d in callback_converter<my_coro_allocator<std::byte>> wrap<main::$_0, my_coro_allocator<std::byte>>(my_coro_allocator<std::byte>, main::$_0) /app/example.cpp:200:31
    #1 0x5842f8f85e04 in std::__n4861::coroutine_handle<void> callback_to_coroutine_handle<main::$_0, my_coro_allocator<std::byte>>(main::$_0, my_coro_allocator<std::byte> const&) /app/example.cpp:209:12
    #2 0x5842f8f85e04 in main /app/example.cpp:215:14
    #3 0x723e46429d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 490fef8403240c91833978d494d39e537409b92e)
    #4 0x723e46429e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 490fef8403240c91833978d494d39e537409b92e)
    #5 0x5842f8ea5374 in _start (/app/output.s+0x2c374)

Address 0x723e446091a0 is located in stack of thread T0 at offset 160 in frame
    #0 0x5842f8f85fbf in callback_converter<my_coro_allocator<std::byte>> wrap<main::$_0, my_coro_allocator<std::byte>>(my_coro_allocator<std::byte>, main::$_0) /app/example.cpp:201

  This frame has 3 object(s):
    [32, 64) 'agg.tmp.i.i' (line 78)
    [96, 128) 'agg.tmp.i' (line 157)
    [160, 192) 'agg.tmp1.i' (line 157) <== Memory access at offset 160 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope /app/example.cpp:200:31 in callback_converter<my_coro_allocator<std::byte>> wrap<main::$_0, my_coro_allocator<std::byte>>(my_coro_allocator<std::byte>, main::$_0)
```


Discussion:

@llvm/issue-subscribers-coroutines

Author: Bart J.F. De Smet (bartdesmet)

<details>
When trying to upgrade to clang 19, we ran into an issue where an ASAN violation is reported in code using a stateful allocator on a promise type. I've tried to reduce the repro in https://godbolt.org/z/soPP6x9rx which shows 18 and 19 side-by-side. It looks like the `stack-use-after-scope` occurs during the construction of the coroutine frame.
</details>

Cleaned reproducer: https://godbolt.org/z/4Y7r7dadr

@llvm/issue-subscribers-clang-codegen

Author: Bart J.F. De Smet (bartdesmet)

<details>
When trying to upgrade to clang 19, we ran into an issue where an ASAN violation is reported in code using a stateful allocator on a promise type. I've tried to reduce the repro in https://godbolt.org/z/soPP6x9rx which shows 18 and 19 side-by-side. It looks like the `stack-use-after-scope` occurs during the construction of the coroutine frame.

```
==1==ERROR: AddressSanitizer: stack-use-after-scope on address 0x723e446091a0 at pc 0x5842f8f8642e bp 0x7fff8c00cc10 sp 0x7fff8c00cc08
READ of size 16 at 0x723e446091a0 thread T0
    #<!-- -->0 0x5842f8f8642d in callback_converter&lt;my_coro_allocator&lt;std::byte&gt;&gt; wrap&lt;main::$_0, my_coro_allocator&lt;std::byte&gt;&gt;(my_coro_allocator&lt;std::byte&gt;, main::$_0) /app/example.cpp:200:31
    #<!-- -->1 0x5842f8f85e04 in std::__n4861::coroutine_handle&lt;void&gt; callback_to_coroutine_handle&lt;main::$_0, my_coro_allocator&lt;std::byte&gt;&gt;(main::$_0, my_coro_allocator&lt;std::byte&gt; const&amp;) /app/example.cpp:209:12
    #<!-- -->2 0x5842f8f85e04 in main /app/example.cpp:215:14
    #<!-- -->3 0x723e46429d8f  (/lib/x86_64-linux-gnu/libc.so.6+0x29d8f) (BuildId: 490fef8403240c91833978d494d39e537409b92e)
    #<!-- -->4 0x723e46429e3f in __libc_start_main (/lib/x86_64-linux-gnu/libc.so.6+0x29e3f) (BuildId: 490fef8403240c91833978d494d39e537409b92e)
    #<!-- -->5 0x5842f8ea5374 in _start (/app/output.s+0x2c374)

Address 0x723e446091a0 is located in stack of thread T0 at offset 160 in frame
    #<!-- -->0 0x5842f8f85fbf in callback_converter&lt;my_coro_allocator&lt;std::byte&gt;&gt; wrap&lt;main::$_0, my_coro_allocator&lt;std::byte&gt;&gt;(my_coro_allocator&lt;std::byte&gt;, main::$_0) /app/example.cpp:201

  This frame has 3 object(s):
    [32, 64) 'agg.tmp.i.i' (line 78)
    [96, 128) 'agg.tmp.i' (line 157)
    [160, 192) 'agg.tmp1.i' (line 157) &lt;== Memory access at offset 160 is inside this variable
HINT: this may be a false positive if your program uses some custom stack unwind mechanism, swapcontext or vfork
      (longjmp and C++ exceptions *are* supported)
SUMMARY: AddressSanitizer: stack-use-after-scope /app/example.cpp:200:31 in callback_converter&lt;my_coro_allocator&lt;std::byte&gt;&gt; wrap&lt;main::$_0, my_coro_allocator&lt;std::byte&gt;&gt;(my_coro_allocator&lt;std::byte&gt;, main::$_0)
```
</details>

@EugeneZelenko can this issue be assigned to someone familiar with coroutine compilation? This issue still reproduces in clang 20 and trunk, so we're holding off on a toolchain update on our end (but I may attempt some `noinline` or `optnone` workarounds to unblock us).
@bartdesmet: You try to look on coroutines code changes history and ping developers here.
@zmodem any thoughts on this issue?
It's on my radar, but I haven't had time to look at it yet, sorry. @ChuanqiXu9 too.
Gently ping to see if this could be looked at and get fixed for 21?
It seems exception related allocas are collected into the frame even though the frontend has emitted `lifetime.end` for them. This leads to increased frame size. Moreover, a stack-use-after-scope error occurs because we attempt to save these allocas to the frame after lifetime.end.

Upon closer inspection, `std::function` is a complex type. Several identical exception handling blocks are inlined into the coroutine body, which makes control flow analysis between `lifetime.start` and `lifetime.end` rather difficult. CFG analyzer just gives up and concludes that lifetime.start is reachable to suspend point. As a result, these allocas are included in the frame.

Personally, I think we'd merge these identical basic blocks. Do you have any ideas? @zmodem @ChuanqiXu9 

> It seems exception related allocas are collected into the frame even though the frontend has emitted `lifetime.end` for them. This leads to increased frame size. Moreover, a stack-use-after-scope error occurs because we attempt to save these allocas to the frame after lifetime.end.
> 
> Upon closer inspection, `std::function` is a complex type. Several identical exception handling blocks are inlined into the coroutine body, which makes control flow analysis between `lifetime.start` and `lifetime.end` rather difficult. CFG analyzer just gives up and concludes that lifetime.start is reachable to suspend point. As a result, these allocas are included in the frame.
> 
> Personally, I think we'd merge these identical basic blocks. Do you have any ideas? [@zmodem](https://github.com/zmodem) [@ChuanqiXu9](https://github.com/ChuanqiXu9)

Thanks for looking into this. But for the higher level and somewhat abstract topic, I don't have any concrete idea yet.

Patch:
diff --git a/llvm/lib/Transforms/Coroutines/CoroFrame.cpp b/llvm/lib/Transforms/Coroutines/CoroFrame.cpp
index 2f5f1089067bf..53d78edda2e9f 100644
--- a/llvm/lib/Transforms/Coroutines/CoroFrame.cpp
+++ b/llvm/lib/Transforms/Coroutines/CoroFrame.cpp
@@ -1215,11 +1215,17 @@ static void insertSpills(const FrameDataInfo &FrameData, coro::Shape &Shape) {
   for (const auto &A : FrameData.Allocas) {
     AllocaInst *Alloca = A.Alloca;
     UsersToUpdate.clear();
-    for (User *U : Alloca->users()) {
+    for (User *U : make_early_inc_range(Alloca->users())) {
       auto *I = cast<Instruction>(U);
-      if (DT.dominates(Shape.CoroBegin, I))
+      // It is meaningless to retain the lifetime intrinsics refer for the
+      // member of coroutine frames and the meaningless lifetime intrinsics
+      // are possible to block further optimizations.
+      if (I->isLifetimeStartOrEnd())
+        I->eraseFromParent();
+      else if (DT.dominates(Shape.CoroBegin, I))
         UsersToUpdate.push_back(I);
     }
+
     if (UsersToUpdate.empty())
       continue;
     auto *G = GetFramePointer(Alloca);
@@ -1233,17 +1239,8 @@ static void insertSpills(const FrameDataInfo &FrameData, coro::Shape &Shape) {
     for (auto *DVR : DbgVariableRecords)
       DVR->replaceVariableLocationOp(Alloca, G);
 
-    for (Instruction *I : UsersToUpdate) {
-      // It is meaningless to retain the lifetime intrinsics refer for the
-      // member of coroutine frames and the meaningless lifetime intrinsics
-      // are possible to block further optimizations.
-      if (I->isLifetimeStartOrEnd()) {
-        I->eraseFromParent();
-        continue;
-      }
-
+    for (Instruction *I : UsersToUpdate)
       I->replaceUsesOfWith(Alloca, G);
-    }
   }
   Builder.SetInsertPoint(&*Shape.getInsertPtAfterFramePtr());
   for (const auto &A : FrameData.Allocas) {