llvm / llvm/llvm-project

Incorrect stack variable address with musttail

Open
#190,429 0 comments 0 reactions 0 assignees View on GitHub
llvm:optimizations
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

I ran into an issue with stack allocated arguments in conjunction with a musttail call and reproduced it with this test case only at `-O2` or higher ([https://godbolt.org/z/9caYvnr7v](https://godbolt.org/z/9caYvnr7v)):

```c
#include
#include
#include

#define TAIL __attribute__((musttail))
#define NOINLINE __attribute__((noinline))

typedef uint64_t u64;

typedef struct {
u64 arr[4];
} quad;

quad quads[] = {
{10, 10, 10, 10},
{20, 20, 20, 20},
};

#define PARAMS u64 a, quad v
#define ARGS a, v

u64 NOINLINE f2(PARAMS) {
return v.arr[1];
}

u64 NOINLINE f1_good(PARAMS) {
v.arr[0] += 1;
TAIL return f2(ARGS);
}

u64 NOINLINE f1_good_2(PARAMS) {
v = quads[0];
TAIL return f2(ARGS);
}

u64 NOINLINE f1_bad(PARAMS) {
v = quads[a];
TAIL return f2(ARGS);
}

int main(int argc, char **argv) {
u64 a = argc;
u64 x, expected;

quad v = (quad){.arr={0, -1, 0, 0}};

x = f1_good(a, v);
expected = v.arr[1];
printf("x=%lu (%lx) should be %lu\n", x, x, expected);
assert(x == expected);

x = f1_good_2(a, v);
expected = quads[0].arr[1];
printf("x=%lu (%lx) should be %lu\n", x, x, expected);
assert(x == expected);

x = f1_bad(a, v);
expected = quads[a].arr[1];
printf("x=%lu (%lx) should be %lu\n", x, x, expected);
assert(x == expected);
}
```

```
$ clang --version
clang version 22.1.2 (Fedora 22.1.2-1.fc45)
Target: x86_64-redhat-linux-gnu
Thread model: posix
InstalledDir: /usr/bin
Configuration file: /etc/clang/x86_64-redhat-linux-gnu-clang.cfg
$ clang -O2 bugredux.c && ./a.out
x=18446744073709551615 (ffffffffffffffff) should be 18446744073709551615
x=10 (a) should be 10
x=18446744073709551615 (ffffffffffffffff) should be 20
a.out: bugredux.c:62: int main(int, char **): Assertion `x == expected' failed.
Aborted (core dumped) ./a.out
````

Looking at the assembly, we see that `f2`, `f1_good`, and `f1_good_2` expect the argument `v` at `rsp + 0x8`, but `f1_bad` thinks it is at `rsp - 0x28`.

```asm
0000000000400480 :
400480: mov rax, qword ptr [rsp + 0x10]
400485: ret
400486: nop word ptr cs:[rax + rax]

0000000000400490 :
400490: inc qword ptr [rsp + 0x8]
400495: jmp rsi
400497: nop word ptr [rax + rax]

00000000004004a0 :
4004a0: movaps xmm0, xmmword ptr [rip + 0x2b89] # 0x403030
4004a7: movaps xmmword ptr [rsp + 0x18], xmm0
4004ac: movaps xmm0, xmmword ptr [rip + 0x2b6d] # 0x403020
4004b3: movaps xmmword ptr [rsp + 0x8], xmm0
4004b8: jmp rsi
4004ba: nop word ptr [rax + rax]

00000000004004c0 :
4004c0: mov rax, rdi
4004c3: shl rax, 0x5
4004c7: movups xmm0, xmmword ptr [rax + 0x403030]
4004ce: movaps xmmword ptr [rsp - 0x18], xmm0
4004d3: movups xmm0, xmmword ptr [rax + 0x403020]
4004da: movaps xmmword ptr [rsp - 0x28], xmm0
4004df: jmp rsi
4004e1: nop word ptr cs:[rax + rax]
```

The output of `clang -O2 -mllvm -print-after-all bugredux.c` shows:

```
# *** IR Dump After Shrink Wrapping analysis (shrink-wrap) ***:
# Machine code for function f1_bad: NoPHIs, TracksLiveness, NoVRegs, TiedOpsRewritten, TracksDebugUserValues
Frame Objects:
fi#-3: size=8, align=16, fixed, at location [SP+8]
fi#-2: size=8, align=16, fixed, at location [SP+8]
fi#-1: size=32, align=16, fixed, at location [SP+8]
fi#0: size=32, align=16, at location [SP+8]
save/restore points:
save points are empty
restore points are empty
Function Live Ins: $rdi, $rsi

bb.0 (%ir-block.3):
liveins: $rdi, $rsi
renamable $rax = COPY renamable $rdi
renamable $rax = nuw nsw SHL64ri killed renamable $rax(tied-def 0), 5, implicit-def dead $eflags
renamable $xmm0 = MOVUPSrm renamable $rax, 1, $noreg, @quads + 16, $noreg :: (load (s128) from unknown-address + 16, align 8)
MOVAPSmr %stack.0, 1, $noreg, 16, $noreg, killed renamable $xmm0 :: (store (s128) into %stack.0 + 16)
renamable $xmm0 = MOVUPSrm killed renamable $rax, 1, $noreg, @quads, $noreg :: (load (s128), align 8)
MOVAPSmr %stack.0, 1, $noreg, 0, $noreg, killed renamable $xmm0 :: (store (s128) into %stack.0)
TCRETURNri64 killed renamable $rsi, 0, , implicit $rsp, implicit $ssp, implicit $rdi, implicit $rsi

# End machine code for function f1_bad.

# *** IR Dump After Prologue/Epilogue Insertion & Frame Finalization (prologepilog) ***:
# Machine code for function f1_bad: NoPHIs, TracksLiveness, NoVRegs, TiedOpsRewritten, TracksDebugUserValues
Frame Objects:
fi#-3: size=8, align=16, fixed, at location [SP+8]
fi#-2: size=8, align=16, fixed, at location [SP+8]
fi#-1: size=32, align=16, fixed, at location [SP+8]
fi#0: size=32, align=16, at location [SP-40]
save/restore points:
save points are empty
restore points are empty
Function Live Ins: $rdi, $rsi

bb.0 (%ir-block.3):
liveins: $rdi, $rsi
renamable $rax = COPY renamable $rdi
renamable $rax = nuw nsw SHL64ri killed renamable $rax(tied-def 0), 5, implicit-def dead $eflags
renamable $xmm0 = MOVUPSrm renamable $rax, 1, $noreg, @quads + 16, $noreg :: (load (s128) from unknown-address + 16, align 8)
MOVAPSmr $rsp, 1, $noreg, -24, $noreg, killed renamable $xmm0 :: (store (s128) into %stack.0 + 16)
renamable $xmm0 = MOVUPSrm killed renamable $rax, 1, $noreg, @quads, $noreg :: (load (s128), align 8)
MOVAPSmr $rsp, 1, $noreg, -40, $noreg, killed renamable $xmm0 :: (store (s128) into %stack.0)
TCRETURNri64 killed renamable $rsi, 0, , implicit $rsp, implicit $ssp, implicit $rdi, implicit $rsi

# End machine code for function f1_bad.
```

where I think that shows `fi#0` switching from location `SP+8` to `SP-40`.

Interestingly on previous llvm versions (not exhaustive but at least 14) this code segfaults at the `ret` in `f2` because `f1_good` writes directly over `rsp` (and does some extra copies that I don't quite understand):

```asm
0000000000401150 :
401150: inc qword ptr [rsp + 0x8]
401155: movaps xmm0, xmmword ptr [rsp + 0x18]
40115a: movups xmmword ptr [rsp + 0x10], xmm0
40115f: movaps xmm1, xmmword ptr [rsp + 0x8]
401164: movups xmmword ptr [rsp], xmm1
401168: movaps xmmword ptr [rsp + 0x8], xmm1
40116d: movaps xmmword ptr [rsp + 0x18], xmm0
401172: jmp rsi
401174: nop word ptr cs:[rax + rax]
```

Double checked that the same behavior is seen with `clang -fsanitize=undefined,address -g -O2 bugredux.c && ./a.out`

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue from bugredux.c with clang -O2 and inspect f1_bad alongside the shrink-wrap and Prologue/Epilogue Insertion dumps. Compare the stack location used for fi#0 with the locations expected by f2, f1_good, and f1_good_2. Done means the program's final assertion passes without the musttail call using an incorrect argument address.

Written by the indexing model from the issue text.

Assessment

Tech stack
c
Domain
compilers
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Quiet
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.