[GH-ISSUE #71] QEMU TCG: x87 FSAVE/FRSTOR across a page boundary writes mantissas into the next physical page (kernel pcb workaround done; user programs still affected) #66

Closed
opened 2026-11-03 22:03:38 -07:00 by gitea-mirror · 1 comment
Owner

Originally created by @raynorpat on GitHub (Oct 20, 2026).
Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/71

Symptom

Under QEMU's software CPU emulation (TCG), x87 FSAVE/FNSAVE (and FRSTOR) to a 108-byte save area that crosses a 4 KB page boundary, with fewer than 8 bytes of the area on the first page, writes each 8-byte register mantissa into the next physical page instead of the next virtual page. That page belongs to someone else. The exponents and the environment block go to the right place.

This silently corrupts unrelated memory. In this project it showed up as:

  • an ext2 mmap sync read-back with 8 zeroed bytes, 2 intact bytes, then 8 zeroed bytes, at off=0x10036 (FP register 3's slot, 0x18 + 30);
  • corrupted anonymous memory in checker processes;
  • probably other one-off stress failures that never reproduced.

Cause

  • In QEMU: target/i386/tcg/access.c access_ptr(). The bounds check offset <= ac->size1 - len underflows when fewer than len bytes remain on the first page, so the 8-byte store is taken as fitting in the first page's mapping and runs into the following physical page.
  • What the kernel did to trigger it: fpstate sat at offset 124 in struct pcb (machdep/i386/thread.h:157) and pcbs are 16-byte aligned. A pcb at page offset 0xf80 put fpstate at 0xffc. Every fnsave/frstor for that thread (fp_support.c :290, :345, :383) then sprayed 8 mantissas at offsets 0x18 + 10*i of an unrelated physical page.
  • Live capture: pcb 0x20317f80's second page held only the exponent; physical 0x1881c018 held the thread's mantissas.

Kernel workaround (done)

c54056db9: fpstate is __attribute__((aligned(16))), so a 108-byte save area never leaves fewer than 8 bytes on a page. Merged into ffs-perf in 4fd6d07b6.

With an FPU burner keeping a signature in all 8 x87 registers while 150 processes churn (vm/work/ext2-sync/fpburn.c, guest RAM scan hz.py):

  • Before: misdirected writes in 3 of 3 scans; 8 anon-memory corruptions in 2 rounds.
  • After: 0 in 7 scans; 32 checker runs with 0 corruptions.

Still open

  • User programs: any user program that runs FSAVE/FRSTOR on a save area of its own placed this way is still corrupted under QEMU TCG. vm/work/ext2-sync/fsavetest.c shows it in 1000 of 1000 attempts. The kernel can't prevent that.
  • Real hardware: not affected.
  • Upstream: the real fix belongs in QEMU (an upstream bug report, not yet filed). Until then, stress results under TCG that show isolated 8-byte corruption should be read with this in mind.

Related: #65, #67, #68 (one-off stress failures worth re-running on a kernel with c54056db9).

Originally created by @raynorpat on GitHub (Oct 20, 2026). Original GitHub issue: https://github.com/RhapsodiOS/RhapsodiOS/issues/71 ## Symptom Under QEMU's software CPU emulation (TCG), x87 `FSAVE`/`FNSAVE` (and `FRSTOR`) to a 108-byte save area that crosses a 4 KB page boundary, with fewer than 8 bytes of the area on the first page, writes each 8-byte register mantissa into the **next physical page** instead of the next virtual page. That page belongs to someone else. The exponents and the environment block go to the right place. This silently corrupts unrelated memory. In this project it showed up as: - an ext2 `mmap sync` read-back with 8 zeroed bytes, 2 intact bytes, then 8 zeroed bytes, at `off=0x10036` (FP register 3's slot, 0x18 + 30); - corrupted anonymous memory in checker processes; - probably other one-off stress failures that never reproduced. ## Cause - **In QEMU:** `target/i386/tcg/access.c` `access_ptr()`. The bounds check `offset <= ac->size1 - len` underflows when fewer than `len` bytes remain on the first page, so the 8-byte store is taken as fitting in the first page's mapping and runs into the following physical page. - **What the kernel did to trigger it:** `fpstate` sat at offset 124 in `struct pcb` (`machdep/i386/thread.h:157`) and pcbs are 16-byte aligned. A pcb at page offset 0xf80 put `fpstate` at 0xffc. Every `fnsave`/`frstor` for that thread (`fp_support.c` :290, :345, :383) then sprayed 8 mantissas at offsets 0x18 + 10*i of an unrelated physical page. - **Live capture:** pcb 0x20317f80's second page held only the exponent; physical 0x1881c018 held the thread's mantissas. ## Kernel workaround (done) c54056db9: `fpstate` is `__attribute__((aligned(16)))`, so a 108-byte save area never leaves fewer than 8 bytes on a page. Merged into ffs-perf in 4fd6d07b6. With an FPU burner keeping a signature in all 8 x87 registers while 150 processes churn (`vm/work/ext2-sync/fpburn.c`, guest RAM scan `hz.py`): - **Before:** misdirected writes in 3 of 3 scans; 8 anon-memory corruptions in 2 rounds. - **After:** 0 in 7 scans; 32 checker runs with 0 corruptions. ## Still open - **User programs:** any user program that runs `FSAVE`/`FRSTOR` on a save area of its own placed this way is still corrupted under QEMU TCG. `vm/work/ext2-sync/fsavetest.c` shows it in 1000 of 1000 attempts. The kernel can't prevent that. - **Real hardware:** not affected. - **Upstream:** the real fix belongs in QEMU (an upstream bug report, not yet filed). Until then, stress results under TCG that show isolated 8-byte corruption should be read with this in mind. Related: #65, #67, #68 (one-off stress failures worth re-running on a kernel with c54056db9).
gitea-mirror 2026-11-03 22:03:38 -07:00
Author
Owner

@raynorpat commented on GitHub (Oct 30, 2026):

Going to close this as a wontfix

<!-- gh-comment-id:6533305056 --> @raynorpat commented on GitHub (Oct 30, 2026): Going to close this as a wontfix
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
github-starred/RhapsodiOS#66
No description provided.