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 @ 81cc5f39aado_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 written

It 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
Edited by Kreijstal