FXSAVE under TCG leaves reserved bytes (10-15) of x87/MMX register slots and the FOP field unwritten
Host environment
- Operating system: Linux
- OS/kernel version: Linux 6.8.0-107-generic #107 (closed)-Ubuntu SMP PREEMPT_DYNAMIC x86_64 GNU/Linux
- Architecture: x86_64
- QEMU flavor: qemu-x86_64 (linux-user) and qemu-system-x86_64 — both TCG
- QEMU version: 11.0.0 (distro), and reproduced on git master built from source
v11.0.0-1572-g81cc5f39aa(commit 81cc5f39) - QEMU command line:
qemu-x86_64 ./fxsave64
Emulated/Virtualized environment
- none — a static x86-64 Linux binary; no guest OS
Description of problem
When emulating FXSAVE / FXSAVE64 under TCG, the 6 reserved bytes
(offsets 10–15) of each of the 8 x87/MMX register slots, and the 2-byte FOP
field (legacy offset 6), are left unwritten — they retain whatever was
previously in the destination memory.
Real x86 silicon — and KVM, which uses the host CPU's FXSAVE — writes those
bytes (the reserved ones come back zero). The TCG store instead leaks stale
memory into the FXSAVE/XSAVE legacy area, diverging from hardware. It is
mode-independent and affects both qemu-user and qemu-system, since the store is
shared code.
Root cause
target/i386/tcg/fpu_helper.c @ 81cc5f39aa — do_fstt() stores 8 + 2 = 10
bytes per register and do_xsave_fpu() strides 16, so bytes 10–15 are never
written; the FOP field at offset 6 is also skipped:
static void do_fstt(X86Access *ac, target_ulong ptr, floatx80 f)
{
CPU_LDoubleU temp;
temp.d = f;
access_stq(ac, ptr, temp.l.lower); /* bytes 0..7 */
access_stw(ac, ptr + 8, temp.l.upper); /* bytes 8..9 -> 10..15 never written */
}
...
access_stw(ac, ptr + XO(legacy.fcw), env->fpuc);
access_stw(ac, ptr + XO(legacy.fsw), fpus);
access_stw(ac, ptr + XO(legacy.ftw), fptag ^ 0xff);
/* legacy.fop at offset 6 skipped */
access_stq(ac, ptr + XO(legacy.fpip), 0);
access_stq(ac, ptr + XO(legacy.fpdp), 0);
addr = ptr + XO(legacy.fpregs);
for (i = 0; i < 8; i++) { do_fstt(ac, addr, ST(i)); addr += 16; }Steps to reproduce
Fill a 16-aligned 512-byte buffer with the sentinel 0xCC, fninit,
fxsave64, then check bytes 10–15 of all 8 register slots (legacy offset 32,
stride 16):
/* fxsave64.c -- gcc -static -O0 fxsave64.c -o fxsave64 */
#include <stdio.h>
#include <string.h>
int main(void) {
unsigned char buf[512] __attribute__((aligned(16)));
memset(buf, 0xCC, sizeof buf);
__asm__ volatile ("fninit");
__asm__ volatile ("fxsave64 %0" : "=m"(buf));
int bug = 0;
for (int s = 0; s < 8; s++)
for (int b = 10; b < 16; b++)
if (buf[32 + s*16 + b] != 0) bug = 1;
printf("slot0 [off 32], bytes 0..15:");
for (int i = 0; i < 16; i++) printf(" %02x", buf[32 + i]);
printf("\nRESULT: %s\n", bug ? "BUG - reserved bytes 10..15 NOT written"
: "OK - reserved bytes 10..15 written (zeroed)");
return bug ? 33 : 0;
}$ gcc -static -O0 fxsave64.c -o fxsave64
$ ./fxsave64 # native (host CPU)
slot0 [off 32], bytes 0..15: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
RESULT: OK - reserved bytes 10..15 written (zeroed)
$ qemu-x86_64 ./fxsave64 # TCG
slot0 [off 32], bytes 0..15: 00 00 00 00 00 00 00 00 00 00 cc cc cc cc cc cc
RESULT: BUG - reserved bytes 10..15 NOT writtenIt reproduces identically under full-system TCG (the boot-sector version below);
KVM -cpu host is correct in every case.
Self-contained qemu-system reproducer (real-mode boot sector, nasm)
Same check, in real mode; reports via the serial port and isa-debug-exit.
; nasm -f bin fxsave_repro.asm -o fxsave_repro.img
; TCG : qemu-system-x86_64 -accel tcg -drive format=raw,file=fxsave_repro.img \
; -display none -serial stdio -device isa-debug-exit,iobase=0xf4,iosize=0x01 -> BUG, exit 33
; KVM : same with -accel kvm -cpu host -> OK, exit 1
BITS 16
ORG 0x7C00
%define COM1 0x3F8
%define BUF 0x9000
start:
cli
xor ax, ax
mov ds, ax
mov es, ax
mov ss, ax
mov sp, 0x7C00
cld
; init COM1 115200 8N1
mov dx, COM1+1
xor al, al
out dx, al
mov dx, COM1+3
mov al, 0x80
out dx, al
mov dx, COM1+0
mov al, 1
out dx, al
mov dx, COM1+1
xor al, al
out dx, al
mov dx, COM1+3
mov al, 0x03
out dx, al
mov dx, COM1+2
mov al, 0xC7
out dx, al
; ensure x87/SSE usable
mov eax, cr0
and eax, ~((1<<2)|(1<<3))
or eax, (1<<1)
mov cr0, eax
mov eax, cr4
or eax, (1<<9)|(1<<10)
mov cr4, eax
fninit
; fill 0xCC, then fxsave
mov di, BUF
mov cx, 512
mov al, 0xCC
rep stosb
fxsave [BUF]
; dump slot 0
mov si, msg_hdr
call puts
mov si, BUF+32
mov cx, 16
.dump:
lodsb
call printhex8
mov al, ' '
call putc
loop .dump
call crlf
; verdict: bytes 10..15 of 8 slots still 0xCC?
xor bx, bx
mov si, BUF+32
mov cx, 8
.slot:
push cx
mov di, si
add di, 10
mov cx, 6
.byte:
cmp byte [di], 0
je .next
mov bx, 1
.next:
inc di
loop .byte
pop cx
add si, 16
loop .slot
test bx, bx
jnz .bug
mov si, msg_ok
call puts
xor al, al
jmp .done
.bug:
mov si, msg_bug
call puts
mov al, 0x10
.done:
mov dx, 0xF4
out dx, al
.hang:
hlt
jmp .hang
putc:
push dx
push ax
mov dx, COM1+5
.w: in al, dx
test al, 0x20
jz .w
pop ax
push ax
mov dx, COM1
out dx, al
pop ax
pop dx
ret
puts:
push ax
.l: lodsb
test al, al
jz .d
call putc
jmp .l
.d: pop ax
ret
printhex8:
push ax
push bx
mov bl, al
shr al, 4
call .nib
mov al, bl
and al, 0x0F
call .nib
pop bx
pop ax
ret
.nib:
and al, 0x0F
cmp al, 10
jb .d
add al, 'A'-10
jmp .e
.d: add al, '0'
.e: call putc
ret
crlf:
push ax
mov al, 13
call putc
mov al, 10
call putc
pop ax
ret
msg_hdr: db "FXSAVE slot0 [off32], bytes 0..15:", 13, 10, 0
msg_ok: db "RESULT: OK - reserved bytes 10..15 written (zeroed).", 13, 10, 0
msg_bug: db "RESULT: BUG - reserved bytes 10..15 keep 0xCC.", 13, 10, 0
times 510-($-$$) db 0
dw 0xAA55