Skip to content

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

Merged
Japabu merged 7 commits into
mainfrom
gsentry-swapgs
Nov 12, 2026
Merged

Japabu merged 7 commits into
mainfrom
gsentry-swapgs

Conversation

@Japabu

@Japabu Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator

Problem

The x86-64 kernel ran no swapgs. GS held the per-CPU block from boot on, and every entry's first stores went through gs:, SYSCALL's while still on the user's stack. Ring 3 replaces the GS base with a segment load: mov gs, 0x1b loads the user data descriptor, base 0 (SDM Vol. 2B, MOV). Its next syscall then stored near address 0 at CPL 0. SYSCALL switches no stack and #PF has no IST, so the fault's frame was pushed wherever the program had pointed rsp, a kernel address if it chose one. That is a kernel-from-userland write. The record is issues/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_BASE while the kernel runs, and in IA32_KERNEL_GS_BASE while Ring 3 runs.

  • SYSCALL comes only from Ring 3. It swaps first, before cld, and swaps back after pop rsp, just before sysretq. The image gate on the shipping syscall_entry requires the opening sequence swapgs, cld, save, switch.
  • Every other non-IST gate (arch::entry::ring3_naked_asm, which takes the frame's CS offset) swaps when the frame's CS is Ring 3's, and swaps back before any iretq to Ring 3. Deciding by CS is sound for these gates 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/IRETQ) have two properties:
    • IF is clear.
    • No instruction there can fault. SYSRET's rcx is canonical by the user half's placement, and every iretq frame is either the CPU's own or comes from a validated entry.
  • NMI, #MC, #DF and #DB can still land in those spans, and their frame does not show it. They install the kernel's base from GDTR, whose base is the block's own gdt, writing it to IA32_GS_BASE whatever that held:
    • The NMI and #DB write back the base they found.
    • #DF and #MC never return and go through fatal_entry.
  • #DB takes IST4, by the orchestrator's decision. A MOV SS holds a #DB over 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). So TF; mov ss; int3 puts a Ring 0 #DB frame on the #BP gate, with Ring 3's base live. #DB relies on neither vendor's text:
    • From Ring 3: its five words move to the top of the thread's kernel stack, read through GDTR rather than GS, and it goes on through common_entry, ending the process as a trap as before.
    • At CPL 0: it resumes where rip is SYSCALL's entry or a gate's target, which is the held trap. The exception is the #DB entry's own first instruction. There, TF; mov ss; INT1 has had the held trap's delivery overwrite INT1's Ring 3 frame on IST4, and resuming would loop on the same stack with IF clear. So the frame is made a Ring 3 one (USER_CS/USER_DS, IF set, rip/rsp unknown and written 0), and that thread ends as a trap. Anywhere else it ends the machine, since nothing in the kernel sets TF or arms a debug register.
  • DR7 (0x400) and IA32_SYSENTER_CS (0) join the CPU declaration, written and checked on every CPU. This disarms firmware's breakpoints, and closes Intel's SYSENTER by construction (#GP with selector 0; AMD's is #UD in long mode).
  • Context switch: unchanged. Every GDT descriptor Ring 3 may load into GS has base 0, which a compile-time assert checks. A null load gives 0 or keeps the base it found: AMD without NullSelectClearsBase (Fn8000_0021_EAX[6]) keeps it, and that base is itself 0. CR4.FSGSBASE stays 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.
  • AArch64 is unaffected, apart from an assert that refuses the x86-only actuator. No AArch64 row is owed.

Why the IST entries don't decide

The brief had them decide whether to swapgs by the sign of IA32_GS_BASE. That test holds only while no thread can write its own GS base, and WRGSBASE writes kernel-half bases too. Installing the base needs no branch, so no load through GS is speculated with another base. The nmi-user-gs arm stages a kernel-half decoy base, so it reds both rejected forms; both are control branches.

Speculation (CVE-2019-1125)

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_gs runs it under QEMU.
  • nmi-user-gs (idt/nmi_user_gs.rs, outside the NMI-silent nmi.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 the shared-debug boot (nmi_finds_the_kernels_gs), and nmi_under_a_user_gs_base runs it under QEMU.
  • debug_trap: TF across a MOV SS into SYSCALL, INT3 and INT1, and TF into INT1 alone. Each must end as a trap with the machine alive. ring3_debug_traps_end_their_process runs 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-diff shows every kernel and record commit unchanged; only the tests commit and round 2's changed, and only in tests/toyos.rs, which carries the rows into #1254's MACHINE_TESTS form and declares nmi_finds_the_kernels_gs On::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) and toyos-checks (51) exited 0. Evidence, with commands, exit codes and revisions in each README:

Host checks at d614e1b:

  • Kernel builds, kernel clippy (x86-64 ×6, AArch64), toyos-checks, cargo run -- --build-only, and the guest binaries through the harness's builder: exit 0.
  • Whole toyos-build library suite: 385 passed, nmi_does_not_log among them. The one failure is the recorded macOS llvm test, which fails on main too.

Inspected: the #DB entry's object code.

Guest runs and the rows each covers (guest runner, x86-64 under KVM):

run head rows result
43081109599 2fc799b 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_cpu queued; must be green
43093456092 d614e1b ring3_debug_traps_end_their_process (all eight debug_trap arms) queued; must be green
43068462126 24c9bc0, the fix reverted nmi_under_a_user_gs_base, a_ring3_gs_load_leaves_the_kernel_its_gs red, as required: nmi-user-gs: FAIL, and a stall at the first entry after a GS load
43081114529 17abd6b, NMI decides from CS alone nmi_under_a_user_gs_base red on nmi-user-gs: FAIL — … count 0 -> 0, 1 decoy words written
43081121891 e4a7eb5, NMI decides by the sign test nmi_under_a_user_gs_base red on the FAIL verdict

Round 3 changed only the #DB entry's CPL-0 path and debug_trap, which no other row in 43081109599 reaches, so those rows carry over to d614e1b. Its own ring3_debug_traps_end_their_process is 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 as Fault::Trap with the machine up.
  • For tf-mov-ss-int1 and tf-int1, the kernel's FAULT rip=… rsp=… line, recorded. rip and rsp 0 appear only when the #DB entry'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

🤖 Generated with Claude Code

@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

Review: #1262 at b768cef (gsentry-swapgs)

Architecture

This matches the owner's decision:

  • SYSCALL swaps unconditionally.
  • Every other non-IST gate swaps by the frame's CS, with one LFENCE after the join.
  • Exits swap back and read nothing through GS afterwards.
  • NMI, #MC and #DF install the kernel's base from GDTR, whatever GS holds.

The GDTR install departs from the brief's sign test, and it should. It is the form that survives WRGSBASE, and both rejected forms are staged as controls. Routing #DF/#MC through a non-returning fatal_entry and putting spurious/unclaimed on ring3_naked_asm remove special cases.

Net change:

  • kernel +254/−63
  • tests +285
  • build +22/−14
  • issues +48/−13

The tests' growth is the two guest probes, the actuator and their rows.

I verified these against the source, entries.dis and the vendor manuals:

  • No GS access before the swap on any path. This covers syscall_entry, the macro gates, the exception and claim stubs, NMI, fatal_entry, halt-all and first_entry.
  • CS offsets and NMI frame offsets match the disassembly.
    • CS offsets: 24 in common_entry; 8 in timer, tlb, spurious, unclaimed and the plain device entry; 16 in the numbered device entry.
    • NMI frame: nested rip/rsp at +0x20/+0x38, rflags at +0x68.
  • The CPL-0 spans with a user base live cannot fault.
    • On a return to an outer ring, IRETQ checks only that RIP is canonical and loads RSP unchecked (SDM 325462-093US Vol. 2A IRET; AMD 24594 r3.39 IRET, RSP.s = temp_RSP). So a thread's unvalidated stack pointer cannot fault first_entry, and its RIP is a toyos_userbound::Entry.
    • SYSRET's RCX is protected by mm::space::TOP.
    • Every IRETQ runs with NT clear: the gates clear it, and first_entry's flags are 0x002. So the long-mode "IRET with NT=1" #GP cannot arise.
  • NMI nesting, #MC inside an NMI, NMI inside #MC/#DF. Each entry installs the kernel base before its first GS access.
  • GDTR is stable. load_gdt loads it once per CPU. The only other lgdt is the AP trampoline's, inside the window x86-64: FSGSBASE is taken, and the switch carries each thread's FS and GS bases #1264's record covers.
  • Intel speculation. One LFENCE after the join is ID 758371's placement for both a skipped and an extra swapgs. The unconditional swaps and the exits need none.

Required

  1. kernel/src/arch/x86_64/idt/nmi.rs:162,187 breaks a host gate.

    • What: user_gs_selftest calls crate::log! in a file that build/sourcegate.rs:88-89 lists in NMI_SILENT.
    • Why it matters: nmi_does_not_log (build/sourcegate.rs:484) matches log!( there and fails, so the merge queue's host checks will red.
    • Evidence gap: the evidence ran cargo test -p toyos-build --lib -- entry with 388 tests filtered out.
    • Fix: the gate's doc names the pattern: staging that runs in ordinary context lives in its own file (hardlockup/probe.rs). Move the selftest out, and point tests/checks.rs's source-words check at the new file. Then run the whole toyos-build lib suite.
  2. kernel/src/arch/x86_64/entry.rs:13-19 and tests/guest/rust-tests/src/arch/mov_ss.rs:9-12: the #DB premise is Intel's alone.

    • Intel: the manual suppresses the TF trap after MOV SS (SDM Vol. 3A §7.8.3, verified).
    • AMD: the manual says the opposite. APM 24593 r3.46 §8.1.4, "Masking During Stack Switches": "the processor does not allow external interrupts or debug exceptions to occur until the instruction immediately following the MOV SS or POP SS instruction completes execution." That is a delay, with no single-step exception. (Read from a public mirror; amd.com refused this host as well.)
    • What tf-mov-ss-syscall misses: it probes the SYSCALL variant, where a deferred #DB would push to the user's RSP. It does not probe the variant this PR's CS decision depends on.
    • The unprobed variant: after TF; mov ss, ax; int3, a deferred #DB lands at stub_bp's first instruction at CPL 0, on the kernel stack, with the user's GS live and a Ring-0 CS in its frame. common_entry then does not swap, and its add dword ptr gs:[preempt_count], 1 goes through the user's base. Here that base is 0, which crashes the kernel through a #PF loop into #DF. Under x86-64: FSGSBASE is taken, and the switch carries each thread's FS and GS bases #1264 the base is a user-chosen kernel address.
    • Fix: scope both comments to what Intel documents and what the benches measure. Add a tf-mov-ss-int3 arm to debug_trap.
    • Read the AMD testcases result before landing. If either MOV SS arm kills that boot, that is a pre-existing kernel-from-userland defect on AMD. Record it, and take a paranoid #DB entry (IST plus install_kernel_gs and restore, like the NMI's) to the owner. It also blocks x86-64: FSGSBASE is taken, and the switch carries each thread's FS and GS bases #1264.
  3. entry.rs:17, "nothing arms a debug register": nothing establishes it.

    • What: nothing under kernel/src writes DR7, and the BSP inherits firmware's.
    • Why it matters: with any breakpoint armed, Intel's documented MOV SS data-breakpoint deferral (§7.8.3: "inhibited until the instruction boundary following the next instruction") lands a #DB at a gate's first instruction with the user's GS. That is this PR's hole on both vendors.
    • Fix: write DR7 = 0x400 (its reset value) on every CPU in the CPU declaration (control_regs::init).
  4. entry.rs:102-103: the "WRMSR to IA32_GS_BASE serializes (SDM Vol. 3A §11.3)" clause is false on AMD.

  5. entry.rs:45-46: the iretq contract is false for the timer.

    • What: "every iretq in it that can return to Ring 3 is [gate_iretq]'s" does not hold for idt/timer.rs:34-35, whose Ring-3 branch ends in its own swapgs; iretq.
    • Fix: state the real contract: swap back before any iretq to Ring 3, with gate_iretq where the frame decides.

Suggestions

  • SYSENTER. In 64-bit mode on Intel, SYSENTER is the other Ring 3 → 0 transition. Today it is closed only because IA32_SYSENTER_CS resets to 0, so it #GPs. Writing 0 there in the same declaration would close it by construction.
  • PR body. "AMD's null-selector load keeps the base it found" holds only without NullSelectClearsBase (Fn8000_0021_EAX[6]). The zero-base conclusion stands either way.

Evidence and landing

Both guest runs are still queued. The head differs from the tested e47fdba only by a doc comment and an issue record (verified). Once the fixes are in, landing needs:

  • Guest 43068455938: all eight rows green.
  • Control 43068462126: both rows red for the right reason.
    • nmi_under_a_user_gs_base reds on an nmi-user-gs: FAIL verdict: own count unchanged, decoy words written. A boot death does not count.
    • a_ring3_gs_load_leaves_the_kernel_its_gs reds by machine death at the first entry after a GS load.
  • Mutations 053d7ac and 6b8cfaa: nmi_under_a_user_gs_base reds on the FAIL verdict.
  • Metal, T14 and AMD, on the fixed head. The DR7 write and the new arm change what boots, so the queued run on b768cef cannot answer them; it stays valid for the rest.
    • testcases completes. gs_segment_load reports all 12 selectors. Every debug_trap arm, both MOV SS arms included, ends as a trap, with the machine still alive.
    • shared-debug shows nmi-user-gs: PASS (nmi_finds_the_kernels_gs), with the other self-tests unchanged.
    • syscall_cost is reported within its bound.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…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>
Japabu added a commit that referenced this pull request Nov 11, 2026
… 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>
@Japabu

Japabu commented Nov 11, 2026 •

Copy link
Copy Markdown
Collaborator Author

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.

Architecture

The paranoid #DB is the NMI's pattern applied to vector 1. That is the right shape. The one hole is that the entry can be told to resume onto itself (blocker below).

Checked against the source and r2-p1-debug.dis:

  • Ring 3 path. It reads GDTR and takes OFF_GDT = 0x2098 off its base. It reads kernel_rsp at +0x10, which set_kernel_stack always writes together with tss.rsp0. It copies the five words from IST4 to kernel_rsp − 40, where the CPU puts every other Ring 3 frame. Then xchg/pop rsp, push 0, push 1, and on to common_entry, which swaps by the moved frame's CS. Nothing on this path addresses GS.
  • CPL-0 path. Eleven words are pushed, so rip is at +0x58. It saves the base it found, installs the kernel's from GDTR, runs deferred, puts the found base back and iretqs.
  • No other #DB reaches the stack while it is in use. The gate is 0x8E, so IF is clear for all of the IST4 use, and deferred neither blocks nor opens interrupts. The kernel itself never raises #DB: no TF, no armed breakpoint, and AMD's bus-lock trap only at CPL > 0 (APM §8.2.2).

Round 1's findings

  1. Resolved. nmi-user-gs now lives in idt/nmi_user_gs.rs. r2-p1-checks.log:725 shows nmi_does_not_log ... ok, and line 854 shows 385 passed. The one failure reproduces on main e558cc1 (main-llvm-env.log) and is already recorded.
  2. Resolved by the #DB decision, apart from blocker 1. The tf-mov-ss-int3 arm and the ring3_debug_traps_end_their_process row are in.
  3. Resolved. control_regs::init writes DR7 = 0x400 and IA32_SYSENTER_CS = 0 on the BSP (percpu.rs:567) and on every AP (:617). self_check asserts both, and declaration asserts SEP.
  4. Resolved.
  5. Resolved. The contract now names a bare swapgs on a path only a Ring 3 frame takes, which is the timer's case.

The points you asked about

  • "Resume with the base it found" at a gate's first instruction. Correct for every gate except #DB's own:
    • SYSCALL entry. IRETQ to CPL 0 pops SS:RSP unconditionally (SDM Vol. 3A §7.14.3). It restores the user's RSP and STAR's SS, and the entry's first instruction is its swapgs.
    • ring3_naked_asm gates. Each decides the swap by its own frame's CS. A gate frame from Ring 3 holds a NULL SS, which IRET accepts for a 64-bit CPL-0 target (§7.14.3).
    • NMI, #DF and #MC. They install the kernel's base whatever they find.
    • stub_halt_all. It reads no GS.
  • An NMI during #DB.
    • The NMI takes IST2, installs from GDTR and puts back what it found, so it is correct at any point in either path.
    • An NMI pending when the held #DB is delivered comes second: debug traps outrank NMI (SDM Table 7-2, class 4 against 5; APM Table 8-9, 2 against 3). It is then taken before debug_entry's first instruction (APM §13.1.4), which the NMI entry handles.
    • Nothing raises #DB inside the NMI.
  • "Ends the machine anywhere else." Userland cannot reach it:
    • IA32_FMASK clears TF at SYSCALL, and every gate clears it on delivery.
    • A TF set by IRET, POPF or SYSRET traps in Ring 3 (APM §13.1.4).
    • No ToyOS call arms a debug register.
    • The GDT has no 32-bit code segment, so there is no CSTAR entry.
    • SYSENTER is #GP on Intel and #UD on AMD, both of which reach a gate.
  • CVE-2018-8897.
    • Data-breakpoint form: closed on both vendors by construction. DR7 is declared, written and checked as 0x400, and nothing lets Ring 3 arm a breakpoint.
    • TF form: Intel suppresses it ("Any single-step trap that would be delivered following the MOV to SS ... is suppressed", §7.8.3). AMD delays it (APM §8.1.4), and the IST4 entry handles that.
    • Gap: both forms are covered except for the case where the instruction after MOV SS is INT1 (blocker 1).

Required

  1. kernel/src/arch/x86_64/idt/debug.rs:108, with idt/mod.rs:305: deferred resumes onto debug_entry itself, which locks the CPU.
    • Trigger: popfq with TF; mov ss, ax; INT1 (0xF1).
    • Step 1: INT1 reaches vector 1 from Ring 3 with no DPL check (SDM Vol. 3A §7.12.1.2 footnote; debug_trap's own int1 arm). Its #DB pushes the Ring 3 frame on IST4.
    • Step 2: the CPU may then deliver the held single-step at the handler's first instruction. That is this PR's own model (debug.rs:9-10: "It lands on the first instruction of the gate ... that instruction reached").
    • Step 3: if it does, the second #DB reloads RSP from IST4 (§7.14.5: "This mechanism unconditionally switches stacks"). It overwrites the five words with a Ring 0 frame: rip = debug_entry, rsp = IST4 top − 40.
    • Step 4: is_gate_target(debug_entry) is true, so the entry iretqs to debug_entry with rsp at that same frame. Its cs is 0x08 and its rip is debug_entry, so the entry takes the CPL-0 path and resumes again, forever, with IF clear.
    • Consequence: the Ring 3 frame is gone, and a userland program has locked up a CPU, and with it the machine (through the hard-lockup detector or the next IPI wait). The kernel also writes nothing through the user's base, here or on x86-64: FSGSBASE is taken, and the switch carries each thread's FS and GS bases #1264.
    • Reachability:
      • Intel cannot reach it: the single-step is suppressed and no breakpoint is armed.
      • AMD documents only the delay (APM §8.1.4), not whether INT1's own #DB drops the held trap.
      • TF; INT1 without MOV SS is unprobed too. Intel's FRED text guarantees that "a single-step trap will never be pending following FRED event delivery (even if INT n, INT1, ... is executed while RFLAGS.TF = 1)" (§8.3.3). IDT delivery has no such guarantee.
    • What this means for the PR body: its claim that "#DB relies on neither vendor's text" holds for every entry except vector 1's own.
    • Fix:
      • Never resume onto debug_entry. A CPL-0 #DB on its first instruction has overwritten a Ring 3 #DB's frame, so that thread is the one to end. For example: put back the base it found, rewrite the IST4 frame as a Ring 3 one (USER_CS/USER_DS; rip/rsp cannot be recovered), and take label 2:, which ends the process as a trap.
      • Add tf-mov-ss-int1 and tf-int1 arms to debug_trap. The AMD testcases boot answers AMD, and the guest row answers the runner's Intel part.
      • This refines "resumes at a gate's target" by one exception and keeps "from Ring 3 the process ends". Whether that needs the owner is the orchestrator's call.

Evidence and landing

Measured so far:

  • Control 43068462126: red, as required.
    • nmi_under_a_user_gs_base reds on nmi-user-gs: FAIL — ... count 0 -> 0, 1 decoy words written.
    • a_ring3_gs_load_leaves_the_kernel_its_gs reds as STALLED: the UART is silent after the console hand-off, and the guest said nothing for 91 s. That is consistent with main's machine death under base 0, whose fatal path also reads GS.
  • Mutation e4a7eb544 (43081121891): red for the right reason. The FAIL verdict shows 1 decoy word written.
  • Still queued: 43081109599 and 43081114529. The guest runner is an Intel CPU under KVM, so these rows exercise only the Ring 3 path. Only AMD metal exercises the CPL-0 path.

What landing needs:

  • 43081109599: all nine rows green. The fix touches only debug.rs and debug_trap, so the other eight stay valid from this run.
  • 43081114529 (17abd6bec): red on the FAIL verdict, not a boot death.
  • Fixed head: ring3_debug_traps_end_their_process green, with the new arms. That run also meets the boot-before-metal rule.
  • AMD boot of b768cef: its tf-mov-ss-syscall result goes in the record. A death there means AMD holds the single-step trap, and so the IST4 path is the one tested on 2fc799b and later heads.
  • Metal, T14 and AMD, on the fixed head:
    • testcases completes. Every debug_trap arm ends as a trap with the machine alive, including both MOV SS arms and the two new ones. gs_segment_load reports 12 selectors.
    • shared-debug shows nmi-user-gs: PASS.
    • syscall_cost is within its bound.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…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>
Japabu added a commit that referenced this pull request Nov 11, 2026
… 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>
Japabu added a commit that referenced this pull request Nov 11, 2026
…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>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

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.

Architecture

The fix implements the decision as made. A CPL-0 #DB on debug_entry no longer resumes. Instead, the entry rewrites the frame as the Ring 3 #DB frame that the IST4 reload overwrote, and leaves through the existing Ring 3 path. The change adds no new state and no new exit path, only one comparison in deferred and one ring test before iretq. That is the right size for the fix.

Round 2's blocker: resolved

I traced TF; mov ss; INT1 from Ring 3, with the CPU doing what APM §8.1.4 permits:

  1. INT1's #DB pushes the Ring 3 frame at the top of IST4. At this point IA32_GS_BASE holds the user's base and IA32_KERNEL_GS_BASE holds the kernel's.
  2. The held trap reloads RSP from IST4, which "unconditionally switches stacks" (§7.14.5). It overwrites those five words with rip = debug_entry, cs = 0x08, rflags with IF and TF clear, rsp = IST4 top − 40, and ss = 0.
  3. CPL-0 path. The entry makes eleven pushes, so the frame's rip sits at +0x58; r3-p1-debug.dis shows lea rdi, [rsp+0x58].
    • It saves the base it found, which is the user's, and installs the kernel's base through GDTR.
    • deferred checks for debug_entry before it calls is_gate_target, which would also match. It then writes rip 0, cs 0x23 (USER_CS), rflags 0x202, rsp 0 and ss 0x1B (USER_DS).
  4. The entry restores the found base and the GPRs. The test [rsp+8], 3 then sees RPL 3 and branches to 2:.
  5. 2: copies the five words to kernel_rsp − 40, switches to that stack and pushes 0 and 1. In common_entry, the frame's RPL 3 selects swapgs, so IA32_GS_BASE holds the kernel's base and IA32_KERNEL_GS_BASE the user's. This is the same state every Ring 3 entry leaves.
  6. trap_dispatch goes on to exception_handler and then fatal_exception. Ring::of_cs(0x23) is user, so it writes the Ring 3 report and calls fault_process(Fault::Trap), which is -> !.
    • The report shows the user's GPRs, because neither delivery touches them.
    • rip 0 and rsp 0 are values a user can already produce; read_user_u64 and record_user_frame handle them.

On the points you asked about:

  • CS, SS and RFLAGS.
    • The kernel never passes this frame to IRETQ, so it only has to satisfy the Ring 3 path and the report.
    • Its selectors are still exactly what the CPU pushes for Ring 3.
    • 0x202 is the flags value every Ring 3 thread runs with: no process is given IOPL, so neither CLI nor POPF can clear IF. It is also what windows_entered assumes of a Ring 3 frame under mask-windows.
  • The base and swapgs state at each step.
    • The found base is always the user's, because the overwritten frame belonged to a Ring 3 #DB whose entry never executed an instruction.
    • Nothing between the restoring wrmsr and common_entry's swapgs/lfence addresses GS.
    • If the new jnz is mispredicted, the wrong path ends at iretq, which serialises, so no GS load can follow speculatively.
  • No fault, recursion, or kernel base left for user code.
    • Every access is to IST4, to GDTR's block, or to the top of the thread's kernel stack.
    • TF is clear and DR7 is 0x400, so no #DB can arise. IF is clear, so no IRQ can arrive.
    • The path never returns to Ring 3.
  • An NMI at any instruction of the path.
    • The NMI runs on IST2, installs the kernel's base from GDTR whatever GS holds, restores the base it found, and never reschedules.
    • That is correct before and after the CPL-0 path's install, between the restore and 2:, during the copy, after pop rsp, and before common_entry's swapgs.
    • An NMI that is pending at the same time as the held trap is taken after it (settled in round 2), and so lands on debug_entry's first instruction.
  • Attribution. IF stays clear from INT1's delivery until fault_process, and the NMI never switches threads. So kernel_rsp, current_tid and the process that ends all belong to the thread that ran INT1, and vector 1 maps to Fault::Trap.

Checks I ran

  • Kernel clippy: the six x86-64 kernel shapes from build/clippy.rs, with ADOPTED and -D warnings, on a530d5d's tree. All six exited 0. I touched debug.rs first so that it was re-linted.
  • Host tests: not run, because no host test reaches this change. On this arm64 host the kernel's host tests do not compile x86-64 arch code, and rust-tests is a guest crate.

Evidence

  • 43081114529 (17abd6b, NMI decides from CS alone) is red, as required, on nmi-user-gs: FAIL — ... count 0 -> 0, 1 decoy words written. The run did not die at boot.
  • 43068462126 and 43081121891 are red, as read in round 2.
  • Carry-over holds.
    • 2fc799b..d614e1b touches only debug.rs, mov_ss.rs (which only debug_trap includes), debug_trap.rs and the record.
    • None of the other eight rows in 43081109599 raises #DB or runs debug_trap.
    • ring3_debug_traps_end_their_process is superseded by 43093456092.
  • Still queued: 43081109599 (all rows must be green) and 43093456092 (all eight arms must be green).

Required

  1. issues/fsgsbase-is-taken.md:37-47 — the owed list cannot show the behaviour it is owed for, and it states unverified hardware behaviour as fact.
    • What is missing. Round 2 required an AMD boot of b768cef to learn whether AMD holds the single-step trap, and that boot is not in the list.
    • The PR body's claim is false. It says "the metal owed above will show which". As recorded, it cannot: each debug_trap criterion (Fault::Trap, machine up) is met the same way whether or not the CPL-0 path ran. The guest row cannot tell the two apart either.
    • The record's hardware claim is wrong. It says "On the AMD bench these are the CPL-0 path ... which a single-step trap held over a MOV SS reaches".
      • That is false for tf-int1, which has no MOV SS.
      • For tf-mov-ss-int1, APM §8.1.4 documents only the delay. It does not say whether INT1's own #DB absorbs the held trap.
      • "Never takes" is also unmeasured for tf-int1 on the guest runner: SDM §20.3.1.4 names INT n, INT3 and INTO, not INT1, as clearing TF.
      • The same unverified claim appears in debug_trap.rs:30 ("whose trap AMD holds onto the #DB entry itself") and in mov_ss.rs:60.
    • Consequence: when a bench returns, the owed run can go green and close the debt without the new CPL-0 code ever having executed on any machine.
    • Fix (records and wording only, no rerun):
      • Name the check main can show. For these arms, the rewrite is the only way to get a report with rip 0 and rsp 0. So: in the AMD bench's testcases log, record whether the kernel's FAULT rip=… rsp=… line for tf-mov-ss-int1 and for tf-int1 shows zeros (the CPL-0 path ran) or the user address after INT1 (it did not).
      • For tf-mov-ss-syscall and tf-mov-ss-int3, main has no such check: both paths end the same way, and the resume logs nothing. Either keep round 2's b768cef boot as owed, or say plainly that the resume path remains unobserved. Whether a boot that is not main fits the benches-down rule is the orchestrator's decision.
      • Write "may" where only the delay is documented, and drop the guest runner's CPU from the record.

Note for the orchestrator (PR body):

  • The guest-runs heading names the guest runner's CPU model.
  • The owed section says "the runner's … part never takes".
  • The Unresolved claim "the metal owed above will show which" stays false until finding 1 is fixed.

Suggestions

  • debug.rs:121-125. deferred's doc repeats the module header, and its last clause is hard to parse ("the rip and rsp the overwritten frame held unknown"). A possible rewrite: "On debug_entry itself the frame becomes a Ring 3 one, its lost rip and rsp zero, so its thread ends as a trap."

What landing needs

  • Finding 1 fixed (records only).
  • 43081109599 and 43093456092 green.

SEND BACK

Japabu added a commit that referenced this pull request Nov 11, 2026
…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>
Japabu added a commit that referenced this pull request Nov 11, 2026
… 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>
Japabu added a commit that referenced this pull request Nov 11, 2026
…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>
@Japabu

Japabu commented Nov 11, 2026

Copy link
Copy Markdown
Collaborator Author

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 git diff). The owed list now asks main for the discriminating FAULT rip=0 rsp=0 reading on the two INT1 arms and records the syscall/INT3 resume path as unobserved with its exit; the non-main AMD boot is dropped per my ruling. No rerun owed. #1262 queues when 43081109599 and 43093456092 are green.

Japabu and others added 7 commits November 12, 2026 00:30
…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>
Japabu added a commit that referenced this pull request Nov 11, 2026
… 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>
Japabu added a commit that referenced this pull request Nov 11, 2026
…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>
@Japabu

Japabu commented Nov 12, 2026

Copy link
Copy Markdown
Collaborator Author

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 (ring3_debug_traps_end_their_process, all arms with the fixed #DB entry): success; controls and mutations red as required. Owed on metal recorded.

@Japabu
Japabu marked this pull request as ready for review November 12, 2026 01:51
@Japabu
Japabu added this pull request to the merge queue Nov 12, 2026
Merged via the queue into main with commit ce202b2 Nov 12, 2026
10 checks passed
@Japabu
Japabu deleted the gsentry-swapgs branch November 12, 2026 02:06
Japabu added a commit that referenced this pull request Nov 12, 2026
… 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>
Japabu added a commit that referenced this pull request Nov 12, 2026
…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>
Japabu added a commit that referenced this pull request Nov 12, 2026
… 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>
Japabu added a commit that referenced this pull request Nov 12, 2026
… 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>
Japabu added a commit that referenced this pull request Nov 12, 2026
…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>
Japabu added a commit that referenced this pull request Nov 12, 2026
… 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>
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