[x86_64 ABI] va_arg incorrectly extracts stdfix types from overflow_arg_area instead of gp_offset
- 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
Assessment
This issue has not been assessed yet.