Skip to content

x86-64: a float load's base address no longer clobbers the cached rax - #202

Merged
teo1747 merged 2 commits into
mainfrom
x86-addr-reg
Nov 9, 2026
Merged

teo1747 merged 2 commits into
mainfrom
x86-addr-reg

Conversation

@teo1747

@teo1747 teo1747 commented Nov 9, 2026

Copy link
Copy Markdown
Member

A wrong-code bug on main, found by the PR fuzz job on #200 (seed 1038932645: x86-64 at -O2, -O3 and -Os; every other board right). It must land before 0.1.0 is tagged.

The bug. The x86-64 backend keeps a cache of which value rax holds. addr_reg, which puts a stack-held address in rcx for a float load, did it as mov slot,%rax; mov %rax,%rcx without telling the cache. The next instruction that wanted the value the cache still named used rax as it was:

lea    -0x30(%rbp),%rax      # &la, cached in rax
mov    %rax,-0x68(%rbp)
mov    -0x38(%rbp),%rax      # addr_reg: the double's base, through rax
mov    %rax,%rcx
movsd  0x8(%rcx),%xmm7
movswl 0x2(%rax),%eax        # la[1]... read from the double's base

The fix. The address is loaded straight from its slot into rcx; rax is untouched. The other places that write rax directly all run after a cache reset (calls, memcpy, memzero, va_start, 128-bit and x87 arithmetic) or in the prologue; this was the only one in the middle of an operation that trusts the cache.

Test. tests/exec/x86-addr-reg-cache.c is the fuzz program, reduced. The reduction was checked against UBSan, ASan and clang's return-type and uninitialised-variable warnings: a first reduction had dropped return statements, and the guard rejected it. Against the old compiler it fails at -O2 and -Os; with the fix it passes at every level. Hand-written versions did not trigger it: whether it happens depends on the allocator spilling both the pointer and the array's address.

Checked.

addr_reg put a slot-held address in rcx by loading it into rax and
copying it, behind the back of the rax residency cache, which still
named the value rax held before. The next instruction that wanted that
value used rax as it was: in fuzz seed 1038932645 (x86-64 at -O2, -O3
and -Os) a local array's address was cached in rax, a double was loaded
through a pointer kept in a slot, and the short read from the array
after it came from the double's base. The address now goes straight
from its slot into rcx; rax is not touched.

Every other place that writes rax directly runs in an operation that
resets the cache first (calls, memcpy, memzero, va_start, 128-bit and
x87 arithmetic) or in the prologue; addr_reg was the one that did so in
the middle of an operation that trusts it.

tests/exec/x86-addr-reg-cache.c is the fuzz program, reduced under
UBSan, ASan and clang's return-type and uninitialised warnings; it
fails with the old compiler at -O2 and -Os and passes at every level.
A fuzz program whose checksum mixes doubles: RX's double is binary32,
so the checksum is another number and the program returns 1 at every
level, -O0 included (CI's run of #202).
@teo1747
teo1747 merged commit 9380979 into main Nov 9, 2026
21 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant