llvm / llvm/llvm-project

[CFGuard] setjmp call site missing from longjmp table when IAT load is hoisted out of a loop

Open
#213,548 4 comments 0 reactions 0 assignees View on GitHub
backend:AArch64 backend:X86 miscompilation platform:windows
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

When a `setjmp` call sits inside a loop and the callee is `dllimport` (mingw ucrt), MachineLICM hoists the IAT load out of the loop and the call becomes register-indirect. The CFGuardLongjmp pass then fails to record the call site, so the binary's Guard longjmp table is missing the entry and `longjmp` back to it is killed by the OS with `FAST_FAIL_INVALID_LONGJUMP_TARGET` (0xC0000409).

## Reproducer

```c
// test.c
#include
#include

static jmp_buf buf;

__attribute__((noinline)) static void work(int i) {
if (i == 1)
longjmp(buf, 1);
}

__attribute__((noinline)) static int loop_setjmp(int n) {
for (int i = 0; i < n; i++) {
if (setjmp(buf) == 0)
work(i);
}
return 0;
}

__attribute__((noinline)) static int single_setjmp(void) {
if (setjmp(buf) == 0)
longjmp(buf, 1);
return 1;
}

int main(int argc, char **argv) {
printf("single_setjmp: %d\n", single_setjmp());
fflush(stdout);
printf("loop_setjmp: entering\n");
fflush(stdout);
loop_setjmp(argc + 1);
printf("loop_setjmp: ok\n");
return 0;
}
```

```console
clang -O2 -mguard=cf test.c -o cfguard-fail.exe
./cfguard-fail.exe
```

clang 22.1.7, x86_64-w64-windows-gnu (msys2 clang64, ucrt), Windows 11.

## Expected

Both functions survive the longjmp.

## Actual

```text
single_setjmp: 1
loop_setjmp: entering
(exit 0xC0000409, FAST_FAIL_INVALID_LONGJUMP_TARGET)
```

`clang -O2 -mguard=cf -S`:

```asm
single_setjmp:
callq *__imp__setjmp(%rip)
$cfgsj_single_setjmp0:

loop_setjmp:
movq __imp__setjmp(%rip), %r15 # hoisted out of the loop
...
callq *%r15 # no $cfgsj_ symbol after this call
```

```asm
.section .gljmp$y,"dr"
.symidx $cfgsj_single_setjmp0 # only entry
```

---

I searched for similar issues, there is #66063, but it's different. There the setjmp TU is compiled *without* guard, while here the TU is compiled with `-mguard=cf` and the entry is lost by optimization passes (notably `-O0` works, both `.gljmp` entries are emitted).

---

For reference, this is seen in freetype2 on Windows, see https://github.com/mpv-player/mpv/issues/18332 for real life issue.

---

Thanks,
Kacper

Contributor guide

Open the contributing guide

Research direction

Start with the test.c reproducer and the clang -O2 -mguard=cf command, then trace how MachineLICM changes the setjmp call before CFGuardLongjmp processes it. Inspect the CFGuardLongjmp handling of register-indirect calls and verify that the generated .gljmp$y section contains entries for both setjmp call sites and that the program survives longjmp.

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
48/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.