Repository navigation
x86-64: every entry from Ring 3 swaps in the kernel's GS base, and NMI, #MC and #DF install it whatever GS holds - #1262
Conversation
Review: #1262 at b768cef (
|
…DR7 and SYSENTER_CS are declared Review round 1 of #1262. `#DB` relied on a premise that holds on Intel alone: the gates decide `swapgs` by their frame's `CS`, and a `#DB` that a `MOV SS` holds over the next instruction lands on the first instruction of whatever entry that instruction reaches, at CPL 0 with Ring 3's GS base live. Intel holds only data breakpoints that way (SDM Vol. 3A §7.8.3); AMD holds single-step too (APM Vol. 2 §8.1.4), so `TF; mov ss; int3` puts a Ring 0 `#DB` frame on the `#BP` gate's first instruction, whose `common_entry` would then address the user's base. By the orchestrator's decision, `#DB` is now an entry like the NMI's: - It takes IST4. From Ring 3 its five words move to the top of the thread's kernel stack, where the CPU puts every other Ring 3 frame (read through `GDTR`, not GS), and it goes on through `common_entry` as vector 1 and ends its process as a trap as before. - At CPL 0 it installs the kernel's GS base from `GDTR`, and resumes with the base it found where `rip` is `SYSCALL`'s entry or a gate's target, whose held trap it is. Anywhere else it ends the machine: nothing in the kernel sets `TF` or arms a debug register. - Neither branch addresses GS before making it the kernel's. `DR7` (its reset value, 0x400) and `IA32_SYSENTER_CS` (0, under which Intel's `SYSENTER` is `#GP`) join the CPU declaration, written and checked on every CPU: firmware's breakpoints are disarmed, and `SYSCALL` is Ring 3's one fast way in by construction. `debug_trap` gains `tf-mov-ss-int3`, and the QEMU row `ring3_debug_traps_end_their_process` runs it there. Also: - `nmi-user-gs` moves out of `nmi.rs`, which `build/sourcegate.rs`'s `nmi_does_not_log` holds silent, into `idt/nmi_user_gs.rs`; the gate's doc names it in place of a deleted file. - The `install_kernel_gs` doc no longer says `WRMSR` to `IA32_GS_BASE` serializes, which AMD's FsGsBaseWriteNotSerializing parts deny; no branch precedes it, which is the argument. - The gate macro's contract covers the timer's own `swapgs; iretq`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an AP takes its CPU declaration before its TSS Review round 1 of #1264, on #1262's round. The rebase onto #1262 also gave its new `#DB` entry `RDGSBASE` and `WRGSBASE`, as the NMI's has. - The switch reads the outgoing FS and GS bases only for a thread of a process; the idle context's were never read back, and the read sat on the wake path. - An AP applies its `control_regs` declaration before `load_gdt`, so from the `ltr` that gives the IST entries their stacks they also have `CR4.FSGSBASE` for their instructions; the record of the earlier span, before `ltr`, says only what is left in it. - `fsgs_bases_per_thread` starts its rounds once both threads are pinned, so each thread running across the other's is the test's premise and not a race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review: #1262 at 2fc799b (round 2)Scope: whether round 1's required findings are resolved, and whether the code that fixes them is correct. Hardware text was read from SDM 325384-093US and APM 24593 r3.45. ArchitectureThe paranoid Checked against the source and
Round 1's findings
The points you asked about
Required
Evidence and landingMeasured so far:
What landing needs:
SEND BACK |
…d, and the metal #1262 owes is recorded Review round 2 of #1262. `deferred` resumed any CPL-0 `#DB` whose `rip` was a gate's target, and `debug_entry` is one. `TF; mov ss; INT1` from Ring 3 makes `INT1`'s `#DB` push a Ring 3 frame on IST4, and on AMD the single-step the `MOV SS` held can then land on `debug_entry`'s first instruction. That delivery reloads IST4 over the Ring 3 frame, and resuming its Ring 0 frame re-enters the entry on the same stack, again and again with `IF` clear: a userland program locking up a CPU. By the orchestrator's decision, that `#DB` never resumes. `deferred` takes the frame and, on `debug_entry`'s own first instruction, makes it a Ring 3 one: `USER_CS` and `USER_DS`, `IF` set as every Ring 3 thread runs, and `rip` and `rsp` 0, since the overwritten frame's are gone. After the base it found is back, the entry's own `CS` test sends it down the Ring 3 path, and the thread that ran the `INT1` ends as a trap through `common_entry`. `debug_trap` gains `tf-mov-ss-int1` and `tf-int1`, which `TF; INT1` without a `MOV SS` reaches if a single-step is ever pending after `INT1`'s delivery through the IDT. The metal rows #1262 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an AP takes its CPU declaration before its TSS Review round 1 of #1264, on #1262's round. The rebase onto #1262 also gave its new `#DB` entry `RDGSBASE` and `WRGSBASE`, as the NMI's has. - The switch reads the outgoing FS and GS bases only for a thread of a process; the idle context's were never read back, and the read sat on the wake path. - An AP applies its `control_regs` declaration before `load_gdt`, so from the `ltr` that gives the IST entries their stacks they also have `CR4.FSGSBASE` for their instructions; the record of the earlier span, before `ltr`, says only what is left in it. - `fsgs_bases_per_thread` starts its rounds once both threads are pinned, so each thread running across the other's is the test's premise and not a race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and the metal #1264 owes is recorded Review round 2 of #1264. The earlier rebase resolved `arch::entry`'s header and the gate macro's contract with this branch's text from before #1262's round 2, which said no `#DB` lands in the swapped spans and that every `iretq` to Ring 3 is `gate_iretq`'s. Both are #1262's again; this branch keeps only its `install_kernel_gs` in `WRGSBASE` form. The rebase onto #1262's round 3 gave the `#DB` entry's thread-ending path the `WRGSBASE` restore. The metal rows #1264 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review: #1262 at a530d5d (round 3; code d614e1b)Scope: this review covers round 2's blocker, the orchestrator's decision, the code that fixes it, and the owed list. I checked the hardware behaviour against SDM 325384-093US §7.8.3, §7.14.5 and §20.3.1.4, and APM 24593 Vol. 2 §8.1.4. ArchitectureThe fix implements the decision as made. A CPL-0 Round 2's blocker: resolvedI traced
On the points you asked about:
Checks I ran
Evidence
Required
Note for the orchestrator (PR body):
Suggestions
What landing needs
SEND BACK |
…part, and the resume into a gate stays an open item Review round 3 of #1262: records and comments only. Each `debug_trap` arm's criterion, `Fault::Trap` with the machine up, is met the same way whether or not the `#DB` entry's CPL-0 path ran, so the owed run could close the debt without that code having executed anywhere. For `tf-mov-ss-int1` and `tf-int1` the kernel's `FAULT rip=… rsp=…` line tells the two apart (0 and 0 only from the rewrite), and the record now owes that reading. For `tf-mov-ss-syscall` and `tf-mov-ss-int3` nothing main reports tells, so the resume into a gate is recorded as unobserved with its exit. The non-main AMD boot that would have answered it is dropped, by the orchestrator's ruling. `tf-int1` has no `MOV SS`, and for `tf-mov-ss-int1` the APM documents only the delay, so the record, `debug_trap`'s description and `mov_ss`'s comment say "may"; the record no longer names the guest runner's CPU. `deferred`'s doc is shorter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an AP takes its CPU declaration before its TSS Review round 1 of #1264, on #1262's round. The rebase onto #1262 also gave its new `#DB` entry `RDGSBASE` and `WRGSBASE`, as the NMI's has. - The switch reads the outgoing FS and GS bases only for a thread of a process; the idle context's were never read back, and the read sat on the wake path. - An AP applies its `control_regs` declaration before `load_gdt`, so from the `ltr` that gives the IST entries their stacks they also have `CR4.FSGSBASE` for their instructions; the record of the earlier span, before `ltr`, says only what is left in it. - `fsgs_bases_per_thread` starts its rounds once both threads are pinned, so each thread running across the other's is the test's premise and not a race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and the metal #1264 owes is recorded Review round 2 of #1264. The earlier rebase resolved `arch::entry`'s header and the gate macro's contract with this branch's text from before #1262's round 2, which said no `#DB` lands in the swapped spans and that every `iretq` to Ring 3 is `gate_iretq`'s. Both are #1262's again; this branch keeps only its `install_kernel_gs` in `WRGSBASE` form. The rebase onto restore. The metal rows #1264 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator verification of the round-3 record fix at 6f85150: against code head d614e1b only the record, comments and one test arm's description string changed (read with |
…dged where the kernel enters Two checks of the kernel's GS handling at its entries, landed ahead of the entry they judge, so that this commit on its own is the kernel without it. `gs_segment_load` loads GS with every selector CPL 3 may load from this GDT (null, user data, user code, each at RPL 0 to 3) in a child, and enters the kernel after each by a syscall, a Ring 3 page fault on an unpopulated page, and a park, reading a marker through GS after each. A segment load writes the descriptor's base with the selector (SDM Vol. 2B, MOV), so a kernel that addresses its per-CPU block through whatever GS holds stores near address 0 on the next entry, on the user's stack. It is a shared-boot member on each bench; `a_ring3_gs_load_leaves_the_kernel_its_gs` runs it under QEMU. The `nmi-user-gs` actuator enters vector 2 at CPL 0 once with a decoy kernel-heap block as the user's GS base (`IA32_KERNEL_GS_BASE`, `swapgs`, `int 2`, `swapgs`), the state between an entry and its `swapgs` and after an exit's. The handler must count itself in this CPU's own census, write nothing through the decoy, and return to the decoy. The decoy is a kernel-half address, which an entry deciding by the sign of `IA32_GS_BASE` would take for the kernel's, as it would a `WRGSBASE` Ring 3 made once FSGSBASE is on. It rides the T14's `shared-debug` boot with the other in-kernel self-tests (`nmi_finds_the_kernels_gs`), and `nmi_under_a_user_gs_base` runs it under QEMU. `debug_trap` gains `tf-mov-ss-syscall`: `TF` across a `MOV SS` into a syscall. Intel's SDM (Vol. 3A §7.8.3) suppresses the single-step trap a `MOV SS` would take rather than deferring it past the next instruction, so no `#DB` lands on the syscall entry's first instruction at CPL 0 on the user's stack; the arm has each bench answer that for its own CPU, the AMD bench's included, whose manual was not read for it. The assembly the binaries need is in `tests/guest/rust-tests/src/arch/`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#MC and #DF install it whatever GS holds The kernel ran no `swapgs`: GS held this CPU's `PerCpu` from boot on, and every entry's first stores went through `gs:`. Ring 3 replaces the GS base with a segment load (`mov gs, 0x1b`: the user data descriptor, base 0), so its next syscall stored near address 0 at CPL 0 on its own stack. `SYSCALL` switches no stack and `#PF` has no IST, so the fault's frame was pushed to the stack pointer the program chose, a kernel address if it chose one, and every handler after it addressed the same base. Found by reading in #1251; the previous commit's two rows red on that kernel. Now `IA32_GS_BASE` is Ring 3's own while it runs and the kernel's while the kernel runs, with `IA32_KERNEL_GS_BASE` the other: - `SYSCALL` comes only from Ring 3 and swaps first and on its way out. The image gate on the shipping `syscall_entry` now requires it to open `swapgs`, `cld`, the save and the switch. - Every other gate swaps when its frame's `CS` is Ring 3's, and back before an `iretq` to Ring 3. That decision is sound because the kernel's CPL-0 spans with a user's base live (before `SYSCALL`'s `swapgs`, and from an exit's `swapgs` to its `SYSRET` or `IRETQ`) run with `IF` clear and no instruction that can fault: `SYSRET`'s `rcx` is canonical by the user half's placement, and every `iretq` frame is the CPU's or a validated entry's. No `#DB` lands there: nothing arms a debug register, `IA32_FMASK` clears `TF`, and a single-step trap after `MOV SS` is suppressed, not deferred (SDM Vol. 3A §7.8.3). - NMI, `#MC` and `#DF` can land in those spans and their frame does not say so. They take the kernel's base from `GDTR`, whose base is the block's own `gdt`, and write it to `IA32_GS_BASE` whatever it held; the NMI puts back the base it found before its `iretq`. They decide nothing: a test of the found base's sign would hold only while no thread can write its own GS base, and `WRGSBASE` writes any canonical one, the kernel half's included (`issues/fsgsbase-is-taken.md`). `#DF` and `#MC` never return, so any dispatched vector on an IST stack goes through the new `fatal_entry`. Speculation (CVE-2019-1125): the gate's `swapgs` is conditional, so an `LFENCE` follows the join, before any load through GS, which is Intel's placement for both a skipped and an extra `swapgs` ("Speculative Behavior of SWAPGS and Segment Registers", ID 758371). The unconditional `SYSCALL` swap needs none, the exits load nothing through GS after their `swapgs` and end in a serializing `IRETQ` or in `SYSRET`, and the IST entries have no branch before their serializing `WRMSR` (SDM Vol. 3A §11.3). On AMD the fence orders dispatch only where `LFENCE` is dispatch-serializing (AMD 70586 G-2, MSR C001_1029[1]), which #1243 sets; AMD's own statement on SWAPGS was not reachable to read. The switch moves no GS base: every descriptor Ring 3 may load into GS has base 0 (asserted at compile time beside the GDT), AMD's null load keeps the base it found, and `CR4.FSGSBASE` stays forbidden, so each CPU's `IA32_KERNEL_GS_BASE` starts at 0 and every thread's user GS base is 0. The switch's save and restore of it belongs with taking FSGSBASE. AArch64 has no GS and is unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… record that a thread's selectors are its CPU's `cld` no longer opens `syscall_entry`; it still runs before the entry calls anything, which is what the copies need. Building the swapgs entry showed that the switch carries a thread's FS base and none of its DS, ES, FS or GS selectors, so a thread reads the selector the previous thread on its CPU loaded. No base is involved, so nothing reaches the kernel or another thread's memory; recorded as a finding with its exit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…DR7 and SYSENTER_CS are declared Review round 1 of #1262. `#DB` relied on a premise that holds on Intel alone: the gates decide `swapgs` by their frame's `CS`, and a `#DB` that a `MOV SS` holds over the next instruction lands on the first instruction of whatever entry that instruction reaches, at CPL 0 with Ring 3's GS base live. Intel holds only data breakpoints that way (SDM Vol. 3A §7.8.3); AMD holds single-step too (APM Vol. 2 §8.1.4), so `TF; mov ss; int3` puts a Ring 0 `#DB` frame on the `#BP` gate's first instruction, whose `common_entry` would then address the user's base. By the orchestrator's decision, `#DB` is now an entry like the NMI's: - It takes IST4. From Ring 3 its five words move to the top of the thread's kernel stack, where the CPU puts every other Ring 3 frame (read through `GDTR`, not GS), and it goes on through `common_entry` as vector 1 and ends its process as a trap as before. - At CPL 0 it installs the kernel's GS base from `GDTR`, and resumes with the base it found where `rip` is `SYSCALL`'s entry or a gate's target, whose held trap it is. Anywhere else it ends the machine: nothing in the kernel sets `TF` or arms a debug register. - Neither branch addresses GS before making it the kernel's. `DR7` (its reset value, 0x400) and `IA32_SYSENTER_CS` (0, under which Intel's `SYSENTER` is `#GP`) join the CPU declaration, written and checked on every CPU: firmware's breakpoints are disarmed, and `SYSCALL` is Ring 3's one fast way in by construction. `debug_trap` gains `tf-mov-ss-int3`, and the QEMU row `ring3_debug_traps_end_their_process` runs it there. Also: - `nmi-user-gs` moves out of `nmi.rs`, which `build/sourcegate.rs`'s `nmi_does_not_log` holds silent, into `idt/nmi_user_gs.rs`; the gate's doc names it in place of a deleted file. - The `install_kernel_gs` doc no longer says `WRMSR` to `IA32_GS_BASE` serializes, which AMD's FsGsBaseWriteNotSerializing parts deny; no branch precedes it, which is the argument. - The gate macro's contract covers the timer's own `swapgs; iretq`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d, and the metal #1262 owes is recorded Review round 2 of #1262. `deferred` resumed any CPL-0 `#DB` whose `rip` was a gate's target, and `debug_entry` is one. `TF; mov ss; INT1` from Ring 3 makes `INT1`'s `#DB` push a Ring 3 frame on IST4, and on AMD the single-step the `MOV SS` held can then land on `debug_entry`'s first instruction. That delivery reloads IST4 over the Ring 3 frame, and resuming its Ring 0 frame re-enters the entry on the same stack, again and again with `IF` clear: a userland program locking up a CPU. By the orchestrator's decision, that `#DB` never resumes. `deferred` takes the frame and, on `debug_entry`'s own first instruction, makes it a Ring 3 one: `USER_CS` and `USER_DS`, `IF` set as every Ring 3 thread runs, and `rip` and `rsp` 0, since the overwritten frame's are gone. After the base it found is back, the entry's own `CS` test sends it down the Ring 3 path, and the thread that ran the `INT1` ends as a trap through `common_entry`. `debug_trap` gains `tf-mov-ss-int1` and `tf-int1`, which `TF; INT1` without a `MOV SS` reaches if a single-step is ever pending after `INT1`'s delivery through the IDT. The metal rows #1262 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…he kernel's GS base Round 2 made `#DB` such an entry; the track's "Built" still named NMI, `#MC` and `#DF` alone. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…part, and the resume into a gate stays an open item Review round 3 of #1262: records and comments only. Each `debug_trap` arm's criterion, `Fault::Trap` with the machine up, is met the same way whether or not the `#DB` entry's CPL-0 path ran, so the owed run could close the debt without that code having executed anywhere. For `tf-mov-ss-int1` and `tf-int1` the kernel's `FAULT rip=… rsp=…` line tells the two apart (0 and 0 only from the rewrite), and the record now owes that reading. For `tf-mov-ss-syscall` and `tf-mov-ss-int3` nothing main reports tells, so the resume into a gate is recorded as unobserved with its exit. The non-main AMD boot that would have answered it is dropped, by the orchestrator's ruling. `tf-int1` has no `MOV SS`, and for `tf-mov-ss-int1` the APM documents only the delay, so the record, `debug_trap`'s description and `mov_ss`'s comment say "may"; the record no longer names the guest runner's CPU. `deferred`'s doc is shorter. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
6f85150 to
6fd6739
Compare
… an AP takes its CPU declaration before its TSS Review round 1 of #1264, on #1262's round. The rebase onto #1262 also gave its new `#DB` entry `RDGSBASE` and `WRGSBASE`, as the NMI's has. - The switch reads the outgoing FS and GS bases only for a thread of a process; the idle context's were never read back, and the read sat on the wake path. - An AP applies its `control_regs` declaration before `load_gdt`, so from the `ltr` that gives the IST entries their stacks they also have `CR4.FSGSBASE` for their instructions; the record of the earlier span, before `ltr`, says only what is left in it. - `fsgs_bases_per_thread` starts its rounds once both threads are pinned, so each thread running across the other's is the test's premise and not a race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and the metal #1264 owes is recorded Review round 2 of #1264. The earlier rebase resolved `arch::entry`'s header and the gate macro's contract with this branch's text from before #1262's round 2, which said no `#DB` lands in the swapped spans and that every `iretq` to Ring 3 is `gate_iretq`'s. Both are #1262's again; this branch keeps only its `install_kernel_gs` in `WRGSBASE` form. The rebase onto restore. The metal rows #1264 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator: landing under the benches-down rule. Review r3 code correct; its record blocker resolved at 6f85150 (verified); rebased onto main as 6fd6739 (tests/toyos.rs registry only; kernel commits unchanged per range-diff). Guest runs: 43081109599 at 2fc799b (the eight rows untouched by the #DB fix): success; 43093456092 at d614e1b ( |
… an AP takes its CPU declaration before its TSS Review round 1 of #1264, on #1262's round. The rebase onto #1262 also gave its new `#DB` entry `RDGSBASE` and `WRGSBASE`, as the NMI's has. - The switch reads the outgoing FS and GS bases only for a thread of a process; the idle context's were never read back, and the read sat on the wake path. - An AP applies its `control_regs` declaration before `load_gdt`, so from the `ltr` that gives the IST entries their stacks they also have `CR4.FSGSBASE` for their instructions; the record of the earlier span, before `ltr`, says only what is left in it. - `fsgs_bases_per_thread` starts its rounds once both threads are pinned, so each thread running across the other's is the test's premise and not a race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and the metal #1264 owes is recorded Review round 2 of #1264. The earlier rebase resolved `arch::entry`'s header and the gate macro's contract with this branch's text from before #1262's round 2, which said no `#DB` lands in the swapped spans and that every `iretq` to Ring 3 is `gate_iretq`'s. Both are #1262's again; this branch keeps only its `install_kernel_gs` in `WRGSBASE` form. The rebase onto restore. The metal rows #1264 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… owes on metal #1251 brought the record to main, and #1262 then fixed the defect without touching it, so main's record still read as an open, unfixed hole. It now says what #1262 built, how the exit's guest test and its negative control came out under QEMU, and what the benches still owe before it closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… an AP takes its CPU declaration before its TSS Review round 1 of #1264, on #1262's round. The rebase onto #1262 also gave its new `#DB` entry `RDGSBASE` and `WRGSBASE`, as the NMI's has. - The switch reads the outgoing FS and GS bases only for a thread of a process; the idle context's were never read back, and the read sat on the wake path. - An AP applies its `control_regs` declaration before `load_gdt`, so from the `ltr` that gives the IST entries their stacks they also have `CR4.FSGSBASE` for their instructions; the record of the earlier span, before `ltr`, says only what is left in it. - `fsgs_bases_per_thread` starts its rounds once both threads are pinned, so each thread running across the other's is the test's premise and not a race. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t, and the metal #1264 owes is recorded Review round 2 of #1264. The earlier rebase resolved `arch::entry`'s header and the gate macro's contract with this branch's text from before #1262's round 2, which said no `#DB` lands in the swapped spans and that every `iretq` to Ring 3 is `gate_iretq`'s. Both are #1262's again; this branch keeps only its `install_kernel_gs` in `WRGSBASE` form. The rebase onto restore. The metal rows #1264 owes, with the benches down, are recorded in `issues/fsgsbase-is-taken.md`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… owes on metal #1251 brought the record to main, and #1262 then fixed the defect without touching it, so main's record still read as an open, unfixed hole. It now says what #1262 built, how the exit's guest test and its negative control came out under QEMU, and what the benches still owe before it closes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
The x86-64 kernel ran no
swapgs. GS held the per-CPU block from boot on, and every entry's first stores went throughgs:,SYSCALL's while still on the user's stack. Ring 3 replaces the GS base with a segment load:mov gs, 0x1bloads the user data descriptor, base 0 (SDM Vol. 2B, MOV). Its next syscall then stored near address 0 at CPL 0.SYSCALLswitches no stack and#PFhas no IST, so the fault's frame was pushed wherever the program had pointedrsp, a kernel address if it chose one. That is a kernel-from-userland write. The record isissues/ring-3-replaces-the-kernels-gs-base-with-a-segment-load.md, opened on #1251's branch.What changes
The kernel's GS base lives in
IA32_GS_BASEwhile the kernel runs, and inIA32_KERNEL_GS_BASEwhile Ring 3 runs.cld, and swaps back afterpop rsp, just beforesysretq. The image gate on the shippingsyscall_entryrequires the opening sequenceswapgs,cld, save, switch.arch::entry::ring3_naked_asm, which takes the frame'sCSoffset) swaps when the frame'sCSis Ring 3's, and swaps back before anyiretqto Ring 3. Deciding byCSis sound for these gates because the kernel's CPL-0 spans with a user's base live (beforeSYSCALL'sswapgs, and from an exit'sswapgsto itsSYSRET/IRETQ) have two properties:IFis clear.SYSRET'srcxis canonical by the user half's placement, and everyiretqframe is either the CPU's own or comes from a validated entry.#MC,#DFand#DBcan still land in those spans, and their frame does not show it. They install the kernel's base fromGDTR, whose base is the block's owngdt, writing it toIA32_GS_BASEwhatever that held:#DBwrite back the base they found.#DFand#MCnever return and go throughfatal_entry.#DBtakes IST4, by the orchestrator's decision. AMOV SSholds a#DBover the next instruction onto the first instruction of whatever entry that instruction reaches. Intel does this for a data breakpoint (SDM Vol. 3A §7.8.3), AMD for single-step too (APM Vol. 2 §8.1.4). SoTF; mov ss; int3puts a Ring 0#DBframe on the#BPgate, with Ring 3's base live.#DBrelies on neither vendor's text:GDTRrather than GS, and it goes on throughcommon_entry, ending the process as a trap as before.ripisSYSCALL's entry or a gate's target, which is the held trap. The exception is the#DBentry's own first instruction. There,TF; mov ss; INT1has had the held trap's delivery overwriteINT1's Ring 3 frame on IST4, and resuming would loop on the same stack withIFclear. So the frame is made a Ring 3 one (USER_CS/USER_DS,IFset,rip/rspunknown and written 0), and that thread ends as a trap. Anywhere else it ends the machine, since nothing in the kernel setsTFor arms a debug register.DR7(0x400) andIA32_SYSENTER_CS(0) join the CPU declaration, written and checked on every CPU. This disarms firmware's breakpoints, and closes Intel'sSYSENTERby construction (#GPwith selector 0; AMD's is#UDin long mode).CR4.FSGSBASEstays forbidden. So every thread's user GS base is 0, and x86-64: FSGSBASE is taken, and the switch carries each thread's FS and GS bases #1264 takes it from there.Why the IST entries don't decide
The brief had them decide whether to
swapgsby the sign ofIA32_GS_BASE. That test holds only while no thread can write its own GS base, andWRGSBASEwrites kernel-half bases too. Installing the base needs no branch, so no load through GS is speculated with another base. Thenmi-user-gsarm stages a kernel-half decoy base, so it reds both rejected forms; both are control branches.Speculation (CVE-2019-1125)
LFENCEfollows the join before any load through GS. That is Intel's placement for both a skipped and an extraswapgs(ID 758371).SYSCALLswap is unconditional.swapgs.#DB's ring test leads to no GS access on either path before that path's own protection.LFENCEis dispatch-serializing (AMD 70586 G-2, MSR C001_1029[1]), which A pure function decides a CPU's speculation mitigations, and each boot says its verdict #1243 sets. x86-64: FSGSBASE is taken, and the switch carries each thread's FS and GS bases #1264 must land after A pure function decides a CPU's speculation mitigations, and each boot says its verdict #1243.issues/user-pointer-checks-have-no-spectre-v1-fence.md.issues/each-boot-prints-the-vulnerabilities-lines-linux-prints.mdno longer excuses the swapgs clause.Tests
The first commit lands them ahead of the fix, so reverting the fix leaves them in place.
gs_segment_load: a child loads GS with each selector CPL 3 may load (null, user data, user code, at RPL 0–3). It then enters the kernel by a syscall, a page fault and a park, reading a marker through GS after each. It is a shared-boot member on each bench;a_ring3_gs_load_leaves_the_kernel_its_gsruns it under QEMU.nmi-user-gs(idt/nmi_user_gs.rs, outside the NMI-silentnmi.rs): vector 2 entered at CPL 0 under a decoy kernel-heap base. It must count in this CPU's own block, write nothing through the decoy, and return to the decoy. It runs on theshared-debugboot (nmi_finds_the_kernels_gs), andnmi_under_a_user_gs_baseruns it under QEMU.debug_trap:TFacross aMOV SSintoSYSCALL,INT3andINT1, andTFintoINT1alone. Each must end as a trap with the machine alive.ring3_debug_traps_end_their_processruns it under QEMU, which exercises the IST4 entry's move of a Ring 3 frame.Checks and evidence
Head 6fd6739, rebased onto main 8188095.
git range-diffshows every kernel and record commit unchanged; only the tests commit and round 2's changed, and only intests/toyos.rs, which carries the rows into #1254'sMACHINE_TESTSform and declaresnmi_finds_the_kernels_gsOn::Every. So its kernel code is d614e1b's on the new base. The runs below stand for it. At 6fd6739, kernel builds, every kernel clippy shape (x86-64 ×6, AArch64 ×4) andtoyos-checks(51) exited 0. Evidence, with commands, exit codes and revisions in each README:Host checks at d614e1b:
toyos-checks,cargo run -- --build-only, and the guest binaries through the harness's builder: exit 0.toyos-buildlibrary suite: 385 passed,nmi_does_not_logamong them. The one failure is the recorded macOSllvmtest, which fails on main too.Inspected: the
#DBentry's object code.Guest runs and the rows each covers (guest runner, x86-64 under KVM):
a_ring3_gs_load_leaves_the_kernel_its_gs,nmi_under_a_user_gs_base,nested_nmi_is_loud,a_kernel_stack_overflow_names_its_stack,a_user_copy_faults_in_mid_copy_or_ends_as_bad_address,vector_state_survives_a_switch,std_threads_count_the_cpus_and_give_back_ended_threads,affinity_pins_hold_on_every_cpuring3_debug_traps_end_their_process(all eightdebug_traparms)nmi_under_a_user_gs_base,a_ring3_gs_load_leaves_the_kernel_its_gsnmi-user-gs: FAIL, and a stall at the first entry after a GS loadCSalonenmi_under_a_user_gs_basenmi-user-gs: FAIL — … count 0 -> 0, 1 decoy words writtennmi_under_a_user_gs_baseFAILverdictRound 3 changed only the
#DBentry's CPL-0 path anddebug_trap, which no other row in 43081109599 reaches, so those rows carry over to d614e1b. Its ownring3_debug_traps_end_their_processis superseded by 43093456092.Owed on metal. The benches are down, so metal is not a landing condition. Each item below is recorded in
issues/fsgsbase-is-taken.md, and on the T14 and the AMD bench main must show:debug_trap: every arm ends asFault::Trapwith the machine up.tf-mov-ss-int1andtf-int1, the kernel'sFAULT rip=… rsp=…line, recorded.ripandrsp0 appear only when the#DBentry's CPL-0 path ran. Either reading is a pass.gs_segment_load: all 12 selectors reported.nmi_finds_the_kernels_gs:nmi-user-gs: PASS.syscall_cost: within its bound.Unresolved
#DBentry resuming at the first instruction ofSYSCALL's entry or the#BPgate, after a single-step held overMOV SS(tf-mov-ss-syscall,tf-mov-ss-int3). It has run on no machine that could show it. Those arms end the same way whether or not it ran, and no row of main tells, so it is recorded as unobserved inissues/fsgsbase-is-taken.md, with its exit. The non-main AMD boot that might have answered it is dropped, by the orchestrator's ruling.percpu.rs, in separate hunks.issues/each-boot-prints-the-vulnerabilities-lines-linux-prints.mdchanges here and in A pure function decides a CPU's speculation mitigations, and each boot says its verdict #1243, and A pure function decides a CPU's speculation mitigations, and each boot says its verdict #1243's decision function does not yet name the swapgs barriers.issues/a-threads-segment-selectors-are-its-cpus.md.🤖 Generated with Claude Code