| Issue |
202393
|
| Summary |
[libc][clang] escape analysis does not capture stack variable passed via ABI layout in thread launcher
|
| Labels |
|
| Assignees |
|
| Reporter |
SchrodingerZhu
|
The bug is detected during a continuous run of libc cnd_test. We introduced a loop to ensure conditional variable waiter is signaled, however, that loop's condition check is optimized out. The problem is that SROA thinks the local atomic variable does not escape. I am uncertain if this is a libc bug or a clang bug due the "hacky" nature of the code, but gcc does work in this case.
**Reproducer minimized by AI, verified by me**.
## The Reproducer (`repro.cpp`)
```cpp
#include <stdio.h>
struct StartArgs {
void (*func)(void*);
void *arg;
};
// Mock start_thread that reads arguments from the stack frame.
//
// Why this is needed on Linux:
// In a raw SYS_clone syscall, the child thread starts executing on a new stack
// immediately following the syscall instruction. Since RSP has switched to the
// new stack, local variables on the parent thread's stack are inaccessible.
// To pass arguments, the parent packs them into the top of the new stack.
// The child thread must then "sniff out" these arguments using frame-address
// offsets (__builtin_frame_address) since it cannot receive them as standard
// function parameters.
__attribute__((noinline)) void start_thread() {
void *caller_frame = __builtin_frame_address(1);
// We assume StartArgs is at caller_frame - 32
StartArgs *args = (StartArgs*)((char*)caller_frame - 32);
args->func(args->arg);
}
// Mock thread creator that packs arguments into the stack frame
__attribute__((noinline)) void run_thread(void (*func)(void*), void *arg) {
StartArgs args;
args.func = func;
args.arg = arg;
start_thread();
}
// Mock atomic structure using compiler builtins (as in LLVM libc)
struct MockAtomic {
bool val;
bool load() {
bool res;
__atomic_load(&val, &res, __ATOMIC_SEQ_CST);
return res;
}
void store(bool v) {
__atomic_store(&val, &v, __ATOMIC_SEQ_CST);
}
};
void waiter_func(void *arg) {
MockAtomic *f = (MockAtomic*)arg;
f->store(true);
}
int main() {
MockAtomic flag;
flag.store(false);
run_thread(waiter_func, &flag);
// Under optimization (-O3), the compiler optimizes away this check loop because it
// thinks 'flag' never escapes (the write in run_thread was eliminated
// as dead code).
while (!flag.load()) {
}
printf("Success!\n");
return 0;
}
```
## Compilation Command
To reproduce the bug (the loop optimization) on this single-file TU, compile the code using standard `clang++` with `-O3`:
```bash
clang++ -O3 -fno-omit-frame-pointer repro.cpp -o repro
```
## LLVM IR Analysis
The optimizer bug can be traced to the **SROA (Scalar Replacement of Aggregates)** pass. SROA eliminates the stack-allocated structures because it fails to detect accesses through `__builtin_frame_address`.
### 1. `run_thread` Optimization (SROA)
SROA on `run_thread` determines that the `StartArgs args` struct is write-only in the function IR because the read in `start_thread` is opaque (via `__builtin_frame_address(1)`). Therefore, SROA optimizes away the allocation and initialization of `args` entirely:
**Before SROA:**
```llvm
define dso_local void @_Z10run_threadPFvPvES_(ptr noundef %0, ptr noundef %1) #0 {
%3 = alloca ptr, align 8
%4 = alloca ptr, align 8
%5 = alloca %struct.StartArgs, align 8
store ptr %0, ptr %3, align 8
store ptr %1, ptr %4, align 8
call void @llvm.lifetime.start.p0(ptr %5)
%6 = load ptr, ptr %3, align 8
%7 = getelementptr inbounds nuw %struct.StartArgs, ptr %5, i32 0, i32 0
store ptr %6, ptr %7, align 8
%8 = load ptr, ptr %4, align 8
%9 = getelementptr inbounds nuw %struct.StartArgs, ptr %5, i32 0, i32 1
store ptr %8, ptr %9, align 8
call void @_Z12start_threadv()
call void @llvm.lifetime.end.p0(ptr %5)
ret void
}
```
**After SROA:**
```llvm
define dso_local void @_Z10run_threadPFvPvES_(ptr noundef %0, ptr noundef %1) #0 {
call void @_Z12start_threadv()
ret void
}
```
*(Notice that `%0` and `%1` are no longer saved to the stack, meaning `flag`'s address never escapes.)*
---
### 2. `main` Optimization (SROA)
Later, SROA is run on `main`. Because `flag`'s address never escaped, the compiler treats `flag` (type `MockAtomic`) as a thread-local, unmodified variable. SROA replaces the atomic load inside the loop with the constant `false` (`0`):
**Before SROA (Inlined `MockAtomic::load`):**
```llvm
define dso_local noundef i32 @main() local_unnamed_addr #5 {
%1 = alloca %struct.MockAtomic, align 1
call void @llvm.lifetime.start.p0(ptr nonnull %1)
store atomic i8 0, ptr %1 seq_cst, align 1
call void @_Z10run_threadPFvPvES_(ptr noundef nonnull @_Z11waiter_funcPv, ptr noundef nonnull %1)
br label %2
2: ; preds = %2, %0
%3 = load atomic i8, ptr %1 seq_cst, align 1
%4 = trunc nuw i8 %3 to i1
br i1 %4, label %5, label %2
5:
...
}
```
**After SROA:**
```llvm
define dso_local noundef i32 @main() local_unnamed_addr #4 {
call void @_Z10run_threadPFvPvES_(ptr nonnull poison, ptr nonnull poison)
br label %1
1: ; preds = %1, %0
%2 = trunc nuw i8 0 to i1
br i1 %2, label %3, label %1
3:
...
}
```
*(The `load atomic` instruction has been replaced with `%2 = trunc nuw i8 0 to i1` (constant false). A subsequent `InstCombinePass` further simplifies this to `br i1 false` which becomes an unconditional infinite jump loop.)*
---
## Explanation
1. In `run_thread`, the `StartArgs` structure (which holds the pointer to `flag`) is created on the local stack.
2. In `start_thread`, `__builtin_frame_address(1)` is used to retrieve the caller's frame pointer, casting it to read the `StartArgs` structure.
3. Because the compiler's alias analysis cannot trace stack layouts across `__builtin_frame_address` calls, it does not connect the write of `args` in `run_thread` to the read in `start_thread`.
4. As a result, the **SROA** pass on `run_thread` decides that `args` is never read in `run_thread`, marking it as dead code and eliminating the initialization of `args` entirely.
5. Since the initialization is eliminated, the address of the local variable `flag` never escapes.
6. Consequently, the compiler concludes that `flag` is never modified by any other code, and **SROA on `main`** replaces the `load atomic` inside the loop with constant `0` (`false`), which `InstCombine` later transforms into an infinite `jmp` loop.
_______________________________________________
llvm-bugs mailing list
[email protected]
https://lists.llvm.org/cgi-bin/mailman/listinfo/llvm-bugs