llvm / llvm/llvm-project

[x86_64 ABI] va_arg incorrectly extracts stdfix types from overflow_arg_area instead of gp_offset

Open
#196,279 2 comments 0 reactions 0 assignees View on GitHub
ABI backend:X86 clang:codegen
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

Description:
When passing _Fract or _Accum fixed-point types (enabled via -ffixed-point) through variadic arguments on the
x86_64-unknown-linux-gnu target, Clang correctly pushes the argument via general-purpose registers (as i16 or i32). However, the
va_arg(ap, _Fract) expansion inside the callee incorrectly tries to read the value from the stack (overflow_arg_area) instead of
the register save area (gp_offset). This results in reading garbage/zero memory.

Reproduction:

// test.c
#include
void take_fract(int count, ...) {
va_list ap;
va_start(ap, count);
_Fract f = va_arg(ap, _Fract);
va_end(ap);
}
int main() {
take_fract(1, 0.5r);
}

Compile with: clang -ffixed-point -O1 -S -emit-llvm test.c

Analysis of LLVM IR:
The caller passes the _Fract 0.5r correctly as a 16-bit integer via general-purpose register ABI:

1 tail call void (i32, ...) @take_fract(i32 poison, i16 noundef 16384)

Inside the callee, va_arg(ap, _Fract) completely skips the gp_offset check and blindly accesses index 2 of __va_list_tag, which is
the overflow_arg_area (stack):

1 %3 = getelementptr inbounds nuw i8, ptr %2, i64 8
2 %4 = load ptr, ptr %3, align 8
3 %5 = getelementptr i8, ptr %4, i64 8
4 store ptr %5, ptr %3, align 8
This fails to retrieve the value that was passed via the GPR.

Environment:
- Target: ubuntu wsl
- Flags: -ffixed-point
- Tested on Clang version 21.1.7 and 18.1.3

You can also check the following program, the provides a workaround for the bug in current clang version:

```
#include
#include
void take_fract(int count, ...) {
va_list ap;
va_start(ap, count);
_Fract f = va_arg(ap, _Fract);
printf("%f\n", (float)f); // random number
va_end(ap);
}
void take_fract_works(int count, ...) {
va_list ap;
va_start(ap, count);
// WORKAROUND: Extract as 'int' to force Clang to read from the
// general-purpose register save area, then bit-cast to _Fract.
int f_int = va_arg(ap, int);
_Fract f = *(_Fract*)&f_int;
printf("f=%f\n", (double)f);
va_end(ap);
}
int main() {
take_fract(1, 0.5r);
take_fract_works(1, 0.5r);
}
```

Outputs:

```
$ clang -ffixed-point -O1 test.c && ./a.out
0.397461
f=0.500000
```

Contributor guide

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.