Repository navigation
A switch loads only what the incoming thread does not already have - #1251
Conversation
…134d3e9199de6: x86 guest rows, reused checks, metal staging; masked
Review of #1251, round 1, at e63e985 (base b566931)What I read: the three commits, the whole diff, and every changed file. I also read the callers and boundaries the change relies on:
I also read the evidence at bcf4626a3 r1 and the brief, #1211 as merged, and #1241 at 954b57d. I checked the SDM text in the implementer's own ArchitectureEach piece has the right owner:
The idle context keeping the root follows the established lazy-TLB design. A change to leaves only passes an idle holder by. A change that frees a table reaches it. A retire reaches every holder before the space goes. The FPU skip is the standard test: a per-CPU holder plus the CPU the thread last left from. Saves stay eager, so nothing reintroduces LazyFP. Size: production +377/−60, tests +223/−12, issues +45/−41. Most of the growth is the idle-holder targeting, the No manual change is needed: Required
Suggestions
The brief's questionsCR3: correct.
FPU: correct across migration, thread death and slot reuse.
FS base: correct.
SS: correct.
The dying-thread fix: complete on both architectures.
Loom: the model and its controls are sound.
Overlaps: none that make the coming work harder.
Evidence checked.
What the readbacks must show (after items 1–4)
SEND BACK. Items 1–4 are small: three comment fixes and a rebase. Once they are done, this can land pending the readbacks above. |
…#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>
…ge that frees no table is what reaches no idle CPU
- The SS comment cites SDM Vol. 3A §7.14.4, "Stack Switching in IA-32e
Mode", where an entry from Ring 3 through an IDT gate forces SS null.
- The cached FS base says what is true: exact at every switch-in, since
every switch away from a user thread reads the register back, and stale
while a thread runs, which moves it by a segment load or
SYS_SET_THREAD_POINTER. It is read from the register at each CPU's
bring-up instead of starting at a sentinel a later WRFSBASE could
write.
- `user_change` states what letting frames go past an idle holder rests
on, against SDM Vol. 3A §5.10.4.4 ("Delayed Invalidation"): SMAP faults
every other Ring 0 access to a user page, a freed user frame is only
ever mapped write-back again (§14.12.4), and a user mapping that is not
write-back is UC (§14.3) or the WC scanout. A non-write-back mapping of
pool frames breaks a stated condition rather than a silent one.
- `arch::tlb`'s header keeps only what it owns, an idle holder's answer
and the retire; `tlb_tags` says which changes reach an idle holder.
- `fpu`'s header no longer describes kernel threads, which do not exist.
- `tlb_shootdown_waits`' third part claimed that a change in a space no
other CPU runs reaches none. A change that frees a table now rightly
reaches a CPU idle on the space's root, which the main thread's parks
in its joins can leave behind, so the part releases the mapping's
middle page, whose table the rest of the mapping keeps, and says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
e63e985 to
f06faad
Compare
…d70c42057c3f7: targeted host checks, loom controls, metal staging, the APM fetch; masked
|
Response to review r1 (head now f06faad: the three commits rebased onto main 6d4b7e4, past #1211, #1238 and #1131, plus f06faad "Review r1 of #1251"). Required:
Suggestions taken:
Not taken: the optional metal row for the resume flush. Whether the T14 keeps translations across its idle states is worth knowing, but it needs a new actuator row; it is recorded as a follow-up in the handoff. #1241: if it lands first, the rebase checks that a retire can't target a CPU the stop has taken out of answering. |
Review of #1251, round 2, at f06faad (base 6d4b7e4)Scope: whether round 1's four required findings are resolved, whether the code changed for them is correct, and whether the rebase's conflict resolutions are correct. I read f06faad in full and ran
I checked the SDM citations against the copy under ArchitectureNothing structural changed since round 1, and the rebase did not change any ownership. #1211's The FPU skip still keys on the context plus the per-thread The branch still merges cleanly with current main (6d0cb3a, #1234). The only file both sides touch is Round 1's required findings
Suggestions that were taken
RequiredBLOCKER:
Suggestions
What the pending runs must show
SEND BACK. One blocker remains: move or replace the FS-base bring-up read. After that, with a compile check, this can land, provided the runs above show what they must. |
…t's segment load leaves `init_bsp` and `init_ap` read `IA32_FS_BASE` before `load_gdt`, whose `mov fs, KERNEL_DS` loads the descriptor's zero base and clears the upper half (SDM Vol. 3A §3.4.4). The boot CPU's cache could then hold the firmware's leftover base while the register held 0, and a first thread whose thread pointer matched it would run with FS.base 0. The cache now starts at 0 for that reason, and neither function reads the register. `arch::tlb`'s SDM citations are numbered as one edition, order 325384-093US, and the idle holder's SMAP condition names the accesses it makes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…4b824282d0707: x86 clippy shapes; masked
|
Response to review r2. Head is now a99cd8c, "Review r2 of #1251".
|
Review of #1251, round 3, at a99cd8c (from f06faad)Scope: round 2's single blocker (the FS-base cache read before ArchitectureNothing structural changed. The fix removes the bring-up Round 2's blocker: resolvedThe register holds 0 after
BSP. APs. The trampoline loads FS with No FS write between
The cache is exact at the first switch-in and afterwards.
Round 2's evidence still applies.
Comments and citations
Evidence
Suggestions
Still pending, as round 2 listed
LAND, pending the guest runs and metal runs above. |
Both benches are down, so the switch's skips land on review, host checks and guest rows. What only metal can show is recorded where its subject lives: the idle holder's `tlb_shootdown_waits` and retire asserts in the TLB record, and in the hardware-speed record `vector_state`'s trips and `sysret_ss_reload` on both benches, the restore skip's two negative controls on the T14, and the osbench A/B of main before and after. The speed-up is unmeasured on metal, and the record says so. 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>
…ge that frees no table is what reaches no idle CPU
- The SS comment cites SDM Vol. 3A §7.14.4, "Stack Switching in IA-32e
Mode", where an entry from Ring 3 through an IDT gate forces SS null.
- The cached FS base says what is true: exact at every switch-in, since
every switch away from a user thread reads the register back, and stale
while a thread runs, which moves it by a segment load or
SYS_SET_THREAD_POINTER. It is read from the register at each CPU's
bring-up instead of starting at a sentinel a later WRFSBASE could
write.
- `user_change` states what letting frames go past an idle holder rests
on, against SDM Vol. 3A §5.10.4.4 ("Delayed Invalidation"): SMAP faults
every other Ring 0 access to a user page, a freed user frame is only
ever mapped write-back again (§14.12.4), and a user mapping that is not
write-back is UC (§14.3) or the WC scanout. A non-write-back mapping of
pool frames breaks a stated condition rather than a silent one.
- `arch::tlb`'s header keeps only what it owns, an idle holder's answer
and the retire; `tlb_tags` says which changes reach an idle holder.
- `fpu`'s header no longer describes kernel threads, which do not exist.
- `tlb_shootdown_waits`' third part claimed that a change in a space no
other CPU runs reaches none. A change that frees a table now rightly
reaches a CPU idle on the space's root, which the main thread's parks
in its joins can leave behind, so the part releases the mapping's
middle page, whose table the rest of the mapping keeps, and says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t's segment load leaves `init_bsp` and `init_ap` read `IA32_FS_BASE` before `load_gdt`, whose `mov fs, KERNEL_DS` loads the descriptor's zero base and clears the upper half (SDM Vol. 3A §3.4.4). The boot CPU's cache could then hold the firmware's leftover base while the register held 0, and a first thread whose thread pointer matched it would run with FS.base 0. The cache now starts at 0 for that reason, and neither function reads the register. `arch::tlb`'s SDM citations are numbered as one edition, order 325384-093US, and the idle holder's SMAP condition names the accesses it makes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both benches are down, so the switch's skips land on review, host checks and guest rows. What only metal can show is recorded where its subject lives: the idle holder's `tlb_shootdown_waits` and retire asserts in the TLB record, and in the hardware-speed record `vector_state`'s trips and `sysret_ss_reload` on both benches, the restore skip's two negative controls on the T14, and the osbench A/B of main before and after. The speed-up is unmeasured on metal, and the record says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
15a11eb to
fa9f2e5
Compare
…0e820558248d5: rebase checks, models; masked
|
Rebased onto main 8188095. The head is now fa9f2e5; its six commits are the old ones carried over. Conflicts:
One break produced no textual conflict. #1246's #1241 landed first. A retire cannot wait on a silenced CPU: under the stop every CPU keeps taking interrupts and scheduler passes ( Checks at fa9f2e5 (evidence
Guest run 43112810732 at fa9f2e5 carries the four x86 rows and the four AArch64 rows; it is queued. Each name was checked against |
Review of #1251, round 4: the rebase onto main 8188095 (head fa9f2e5)Scope: the rebase from a99cd8c (LAND at r3) onto 8188095, as the brief asks. I did not reopen settled points. What I read:
ArchitectureThe rebase changes no ownership or mechanism.
Range-diff: nothing else changed
Resolutions
The retire on a clone (
|
…ross its shootdown, and the clippy count is 10 kernel shapes and 2 for kernel-loom
…ge that frees no table is what reaches no idle CPU
- The SS comment cites SDM Vol. 3A §7.14.4, "Stack Switching in IA-32e
Mode", where an entry from Ring 3 through an IDT gate forces SS null.
- The cached FS base says what is true: exact at every switch-in, since
every switch away from a user thread reads the register back, and stale
while a thread runs, which moves it by a segment load or
SYS_SET_THREAD_POINTER. It is read from the register at each CPU's
bring-up instead of starting at a sentinel a later WRFSBASE could
write.
- `user_change` states what letting frames go past an idle holder rests
on, against SDM Vol. 3A §5.10.4.4 ("Delayed Invalidation"): SMAP faults
every other Ring 0 access to a user page, a freed user frame is only
ever mapped write-back again (§14.12.4), and a user mapping that is not
write-back is UC (§14.3) or the WC scanout. A non-write-back mapping of
pool frames breaks a stated condition rather than a silent one.
- `arch::tlb`'s header keeps only what it owns, an idle holder's answer
and the retire; `tlb_tags` says which changes reach an idle holder.
- `fpu`'s header no longer describes kernel threads, which do not exist.
- `tlb_shootdown_waits`' third part claimed that a change in a space no
other CPU runs reaches none. A change that frees a table now rightly
reaches a CPU idle on the space's root, which the main thread's parks
in its joins can leave behind, so the part releases the mapping's
middle page, whose table the rest of the mapping keeps, and says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t's segment load leaves `init_bsp` and `init_ap` read `IA32_FS_BASE` before `load_gdt`, whose `mov fs, KERNEL_DS` loads the descriptor's zero base and clears the upper half (SDM Vol. 3A §3.4.4). The boot CPU's cache could then hold the firmware's leftover base while the register held 0, and a first thread whose thread pointer matched it would run with FS.base 0. The cache now starts at 0 for that reason, and neither function reads the register. `arch::tlb`'s SDM citations are numbered as one edition, order 325384-093US, and the idle holder's SMAP condition names the accesses it makes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
fa9f2e5 to
2184217
Compare
Both benches are down, so the switch's skips land on review, host checks and guest rows. What only metal can show is recorded where its subject lives: the idle holder's `tlb_shootdown_waits` and retire asserts in the TLB record, and in the hardware-speed record `vector_state`'s trips and `sysret_ss_reload` on both benches, the restore skip's two negative controls on the T14, and the osbench A/B of main before and after. The speed-up is unmeasured on metal, and the record says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The switch in a same-CPU futex round trip cost 403 of 3,998 ns on the T14 (wakeperf's segment probes): a CR3 write though both threads share one space, an FPU restore, an FS-base MSR read and write, and an SS reload, and a park with nothing runnable switched to idle reloading CR3 and back. x86-64: - The root. `Cr3::activate` records what it loads in a per-CPU word, and the switch loads a thread's root only when another is loaded. The idle context keeps whatever root it finds: its code reaches only the kernel half, which every root shares (a space copies every kernel root slot in when it is made, and `seal_kernel_half` gives each a table first). A CPU idle on a root still runs that space for the shootdown targeting, with an idle mark: a change that frees no table passes it by and it flushes when a thread of the space resumes there (`Tags::resume`, paired with `Tags::changed` by the same fence pattern as `enter`); a change that frees a table reaches it, and the answer of a CPU in its idle context loads the kernel's root, so it is reached once. A process's last thread out retires its space (`Space::retire`): every CPU still idle on its root loads the kernel's before the teardown goes on, so the space's drop waits on no CPU, and `UserTables`' drop asserts none holds it. `activate` runs masked, so no answer finds a CPU between two roots. - The FPU. Every switch away from a thread still saves its state (nothing is deferred to a #NM trap, so Ring 3 only ever holds its own state); the restore is skipped where this CPU's registers still hold it: the per-CPU holder names the thread's context and the thread last left on this CPU. `load_initial` clears the holder. This is the design issues/a-thread-resuming-on-the-cpu-that-holds-its-state-restores-it-again.md laid out, which this closes. - The FS base. The switch still reads it back at a switch-out, since Ring 3 moves it with a segment load (SDM Vol. 2B, MOV); it writes it at a switch-in only when the CPU's last-known value differs. - SS. Reloaded only on a part whose SYSRET keeps SS's hidden descriptor, which is every part but Intel's (SDM Vol. 2B SYSRET sets it whole; AMD's APM Vol. 3 SYSRET sets the selector alone), and only when SS is not already the kernel's data selector (an IDT entry from Ring 3 leaves it null, SDM Vol. 3A §7.14.4). The probe says so on Intel, and the metal judge reads which part a boot ran on from its PCI host bridge's vendor, not from the kernel's own decision. AArch64: the switch skips a TTBR0 write identical to what TTBR0 holds; its idle context still loads the kernel's empty root, since nothing there could take a dropping space's root off a CPU idle on it. Tests: kernel-loom's `tlb_tags` gains the idle holder's model and its negative control `tlb-tags-resume-relaxed`; `vector_state` gains a thread that leaves its registers on an idle CPU, changes them on another and comes back. Found on the way and filed, not fixed: issues/ring-3-replaces-the-kernels-gs-base-with-a-segment-load.md. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…it held Every state loaded onto a CPU, a restore or a new thread's initial image, belongs to a thread that leaves through `fpu::leave` before another thread resumes there, so the holder that leave records is the whole of the skip's record: the writes in `resume` and `load_initial` recorded nothing a later reader could see. `tlb::retire` asserts its post-condition, that no CPU still holds the space's root, in the teardown that called it rather than only at the drop, which may run long after the process's exit is published. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…k from idle checks its tags A dying thread can be switched out and back in between its leave of the process and its exit pass: a killed thread keeps running through its quantum, and any preempt-enable on the way may reschedule it. Its context still named the space's root, so a switch back loaded that root again after the last thread out had retired the space; a CPU could then idle on a root `retire` had taken off every CPU, and the space's drop would assert. `driver::leave_address_space` gives the running context the kernel's root before loading it, so every later switch into the thread loads the kernel's, on both architectures. `Tags::resume` paid its fence on every switch between two threads of one space, and flushed every user translation wherever the CPU's own by-page flush had left its stamp behind the space's last change. Only a CPU that idled on the root can have been passed by; one that ran a thread of the space until the switch was reached by every change since, so it returns at once, as a switch between two such threads did before. `tlb::retire` no longer leaves an idle space on its own CPU: it runs on the last thread out, which has left the space already. A change reads the idle mark with acquire, pairing with the release that sets it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ge that frees no table is what reaches no idle CPU
- The SS comment cites SDM Vol. 3A §7.14.4, "Stack Switching in IA-32e
Mode", where an entry from Ring 3 through an IDT gate forces SS null.
- The cached FS base says what is true: exact at every switch-in, since
every switch away from a user thread reads the register back, and stale
while a thread runs, which moves it by a segment load or
SYS_SET_THREAD_POINTER. It is read from the register at each CPU's
bring-up instead of starting at a sentinel a later WRFSBASE could
write.
- `user_change` states what letting frames go past an idle holder rests
on, against SDM Vol. 3A §5.10.4.4 ("Delayed Invalidation"): SMAP faults
every other Ring 0 access to a user page, a freed user frame is only
ever mapped write-back again (§14.12.4), and a user mapping that is not
write-back is UC (§14.3) or the WC scanout. A non-write-back mapping of
pool frames breaks a stated condition rather than a silent one.
- `arch::tlb`'s header keeps only what it owns, an idle holder's answer
and the retire; `tlb_tags` says which changes reach an idle holder.
- `fpu`'s header no longer describes kernel threads, which do not exist.
- `tlb_shootdown_waits`' third part claimed that a change in a space no
other CPU runs reaches none. A change that frees a table now rightly
reaches a CPU idle on the space's root, which the main thread's parks
in its joins can leave behind, so the part releases the mapping's
middle page, whose table the rest of the mapping keeps, and says so.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…t's segment load leaves `init_bsp` and `init_ap` read `IA32_FS_BASE` before `load_gdt`, whose `mov fs, KERNEL_DS` loads the descriptor's zero base and clears the upper half (SDM Vol. 3A §3.4.4). The boot CPU's cache could then hold the firmware's leftover base while the register held 0, and a first thread whose thread pointer matched it would run with FS.base 0. The cache now starts at 0 for that reason, and neither function reads the register. `arch::tlb`'s SDM citations are numbered as one edition, order 325384-093US, and the idle holder's SMAP condition names the accesses it makes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Both benches are down, so the switch's skips land on review, host checks and guest rows. What only metal can show is recorded where its subject lives: the idle holder's `tlb_shootdown_waits` and retire asserts in the TLB record, and in the hardware-speed record `vector_state`'s trips and `sysret_ss_reload` on both benches, the restore skip's two negative controls on the T14, and the osbench A/B of main before and after. The speed-up is unmeasured on metal, and the record says so. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2184217 to
9b7d6d9
Compare
…32a805e0d3e35: rebase past #1266 and #1267, checks, models; masked
|
I rebased onto main 36ed75b. That takes in #1266, #1259, #1243, #1239, #1263 and #1267. The head is now 9b7d6d9; the six commits are the old ones carried over. Conflicts:
I resolved #1267 first against main plus #1267's queued head (e11ea5e). Once it merged, Checks at 9b7d6d9 (evidence
Guest run 43125192691 is at 9b7d6d9 (the run list shows that SHA). It carries the four x86 rows and |
Review of #1251, round 5: the rebase onto main 36ed75b (head 9b7d6d9)Scope: the rebase from fa9f2e5 (LAND in round 4) onto 36ed75b, which brings in #1267's per-CPU asks. I did not reopen settled points. What I read:
ArchitectureThe rebase changes no ownership and adds no mechanism.
The checks the brief asks for1. The composed shootdown asks every CPU it must, and none answers before its flush.
2.
3. The whole-range ask for a freed table is sufficient.
4.
5. The range-diff shows nothing else.
FindingsBLOCKER:
SUGGESTION (optional): two lists in
EvidenceMine, at 9b7d6d9: a detached worktree with a clean tree (
The author's Guest run 43125192691 is at 9b7d6d9 and carries the rows the description names. It was still running at 00:31 UTC, so I read no result.
Metal: not a landing condition while the benches are down. SEND BACK for the resume comments. Once they are fixed, I see nothing else between this head and LAND, provided guest run 43125192691 is green. |
…e CPU's last full flush `Tags::resume` and `tlb::resume` said a resuming CPU flushes where a change passed it by while it idled. The code flushes where the space changed after the CPU's last flush of every user translation, and since #1267 a by-page answer does not count as one, so a CPU reached before it idled can owe the flush too. Both comments now say so, as `enter`'s does. The TLB record keeps one "Owed on metal" list: a single `shared-debug` boot on the T14 settles both the asks' and the idle holder's `tlb_shootdown_waits`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…fdb412f88f006: comments and record only
…fdb412f88f006: comments, record, the red virt_mask_windows run; masked
|
Review r5: the head is now f183499, "Review r5 of #1251". The commit touches comments and records only.
Guest run 43125192691 at 9b7d6d9: seven rows passed and
Census: no Evidence: https://github.com/ToyOSOrg/evidence/tree/b554bd2b082f99741c6fdc06a49df74a2a65ab00/r6 |
|
Orchestrator: landing under the benches-down rule. Rebase review r5 LAND (composition with #1266/#1267 correct); its comment blocker fixed at f183499 (comments only, verified). Guest run 43125192691 at 9b7d6d9: 7 of 8 green; |
… 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>
… 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
On the T14, the switch inside a same-CPU futex round trip cost 403 of 3,998 ns (wakeperf's segment probes on main 422915e; Linux's whole round trip is 2,298 ns): a
CR3write of ~120 ns although both threads share one address space, an FPU restore (~100 ns with the save), anIA32_FS_BASEread and write (91 ns) and an SS reload (44 ns). A park with nothing runnable switched to the idle context reloadingCR3, and back.What the switch does now
x86-64
Cr3::activaterecords the value it loads in a per-CPU word; the switch loads a thread's root only when another is loaded. The idle context keeps whatever root it finds: idle code reaches only the kernel half, which every root shares.tlb_tags). A change that frees no table passes it by, and the CPU flushes when a thread of the space resumes there (Tags::resume, paired withTags::changedby the sameSeqCst-fence pattern asenter; only a CPU back from idle pays it). A change that frees a table reaches it, since its walker could reach the freed table, and a CPU answering any shootdown in its idle context loads the kernel's root.user_changestates what makes that sound: SMAP faults every other Ring 0 access to a user page, a freed user frame is only ever mapped write-back again (§14.12.4), and a user mapping that is not write-back is a device window, UC (never read speculatively, §14.3) or the WC scanout. A non-write-back mapping of pool frames would break a stated condition.process::leave_as) gives its own context the kernel's root before loading it (sched::driver::leave_address_space), so a dying thread switched out and back in before its exit pass never loads the space again. Once every thread has left, the last one out retires the space (Space::retire→tlb::retire): every CPU still idle on its root is reached and loads the kernel's before the teardown goes on, so the space's drop waits on no CPU.tlb::retireasserts its post-condition andUserTables' drop asserts that no CPU holds the tag.activateruns masked, so no answer finds a CPU between two roots.#NMtrap, so Ring 3 only ever holds its own state (LazyFP, CVE-2018-3665). The restore is skipped when this CPU's registers still hold the thread's state: the per-CPU holder names the thread's context and the thread's own last switch-out was on this CPU. This keepsarch::fpu'sXSAVEOPT/XSAVESinvariant: a resume without a restore follows a save on the same CPU, so no save elsewhere wrote the area in between (SDM Vol. 1 §13.6). Closesissues/a-thread-resuming-on-the-cpu-that-holds-its-state-restores-it-again.md.SYS_SET_THREAD_POINTERwrites it; written at a switch-in only when the CPU's cached value differs. The cache starts at 0, the baseload_gdt'smov fs, KERNEL_DSleaves (SDM Vol. 3A §3.4.4), and is exact at every switch-in.CR4.FSGSBASEstays forbidden.SYSRETkeeps SS's hidden descriptor, which is every part but Intel's (SDM Vol. 2B SYSRET writes it whole; AMD APM Vol. 3 SYSRET sets the selector alone), and only when SS is not already the kernel's data selector (an IDT entry from Ring 3 leaves it null, SDM Vol. 3A §7.14.4). Thesysret_ss_reloadmetal row reads which part the boot ran on from the PCI host bridge's vendor, independently of the kernel's CPUID reading. The AMD APM itself was not fetched: amd.com ends the stream for the build's user agent (r2/apm-fetch.log); the reviewer's reading of APM Vol. 3 (24594), SYSRET, agrees, and the fact only decides that non-Intel parts keep the reload.AArch64 (shared code only, no arm-only work): the switch skips a
TTBR0_EL1write identical to what it holds; its idle context still loads the kernel's empty root, since nothing there takes a dropping space's root off an idle CPU.leave_address_spaceapplies there too.Test.
tlb_shootdown_waits' third part claimed that a change in a space no other CPU runs reaches none. A change that frees a table now rightly reaches a CPU idle on the space's root, which the main thread's parks in its joins can leave behind, so the part releases the mapping's middle page, whose table the rest of the mapping keeps.Decisions and their alternatives
StackandGuard) makes an idle CPU drop its root as it answers.Found on the way, filed, not fixed
issues/ring-3-replaces-the-kernels-gs-base-with-a-segment-load.md: Ring 3 can load GS with a DPL-3 selector, replacing the per-CPU GS base the no-swapgsentry addresses first.Checks and evidence
Head 421f84d: 9b7d6d9's code, then review r5's comment fixes and two records-only commits. 9b7d6d9 is rebased onto main 36ed75b (past #1266, #1259, #1243, #1239, #1263 and #1267). Round 5 there: the 10 kernel clippy shapes and the 2 loom-crate shapes exit 0,
cargo test -p kernel --lib485 passed,cargo check --test toyos-buildexit 0, loomtlb_tagsand its controls as below, andcargo run -- --ci host modelsexit 0 (89 steps, all green, #1266's share tests and #1267's shootdown controls among them): https://github.com/ToyOSOrg/evidence/tree/358f169f8661121911a2e20aed96a31f7c7e7901/r5.Round 5's conflicts:
kernel/pure/sched/task.rs(#1266'soutrun_requestbesidectx_mut;cpu.rsauto-merged withrunning_ctx),kernel/src/arch/x86_64/tlb.rs(#1267's asks beside the idle holder:user_changekeeps itstablesargument and asks each reached CPU, so a table-freeing change asks an idle holder for the whole user range, which its answer flushes whole;answer_askleaves an idle CPU's space asanswerdoes) and the TLB issue record (both accounts kept).Round 4 (fa9f2e5, onto 8188095) resolved
kernel/src/arch/x86_64/boot.rs(#1244'scopies_go_downarm kept beside the vendor read once),kernel/src/invalidation.rs(#1241 removedStop;Retirestays beside #1211'sGuardandStack) andkernel/src/sched/driver.rs(#1246'scurrent_handlesbesideleave_address_space). One break had no textual conflict: #1246'steardown_resourcesno longer holds the process data's guard where the retire used it, so the retire now runs on a clone of the space'sArcand holds no process-data lock across its shootdown, only the space's own, as an unmap does (folded into the first commit).Round 3 (review r2's FS-base fix, a99cd8c): x86 kernel clippy shapes, https://github.com/ToyOSOrg/evidence/tree/3224156f92a772d52fc9559a5002d3bead4800d9/r3.
Round 2, at f06faad on main 6d4b7e4 (rebased past #1211, #1238, #1131). Commands, exit codes and logs: https://github.com/ToyOSOrg/evidence/tree/1a08e6bb6978c796427b929bb6c9bb764db0a851/r2 (round 1:
r1/there).Host (targeted, per the owner's testing policy):
cargo clippy -p kernelfor x86_64 (default;boot-actuators,test-actuators;boot-actuators; the instrument set) and AArch64 (the same four), the kernel library (sched-check; with the kernel's controls) andkernel-loom(default; bothtlb_tagscontrols), all with the adopted lints and-D warnings: exit 0 each.cargo test -p kernel --lib sched::: 197 passed, exit 0.cargo test --manifest-path kernel/loom/Cargo.toml --test tlb_tags: exit 0, both models. Negative controls:--features tlb-tags-resume-relaxedexit 101 with only the idle holder's model red;--features tlb-tags-enter-relaxedexit 101 with only the entering model red.Guest at 9b7d6d9 (the head since adds comments and records only): run 43125192691. Seven rows green: the x86 rows
vector_state_survives_a_switch(16 away-and-back trips, the FPU skip's guest check),std_threads_count_the_cpus_and_give_back_ended_threadsandlibc_children(thread and process ends, soleave_address_spaceand the retire),affinity_pins_hold_on_every_cpu(migration), and the AArch64 rowsvirt_fp_isolation virt_smp virt_jobs_at_el2.virt_mask_windowsred with the signature ofissues/virt-mask-windows-finds-a-mask-with-no-hook-in-a-lower-el-data-abort.md, red on main before this branch; recorded there as another sighting.Metal: both benches are down, so this lands on review, host checks and guest rows. The speed-up is not yet measured on metal. Owed on metal, recorded in
issues/toyos-uses-what-modern-hardware-offers-for-speed.mdandissues/a-tlb-shootdown-reaches-only-the-cpus-that-ran-the-space.md:testcasesboot green,vector_statereporting its 16 trips; main'sshared-debugboot withsysret_ss_reloadjudging "not needed" on the T14 (host bridge 8086) and "reloaded" on the AMD bench (1022).tlb_shootdown_waitsinshared-debuggreen, its third part counting zero CPUs reached. On both benches, notestcases,shared-debugortestcases-watchdogboot ending on either retire assert.vector_statefor its own reason: ignoring where the thread last left its state, "held the file it had left there"; ignoring the CPU's holder (the deleted issue's own mutant), "changed while it was switched out".osbenchof main before this PR against main after it, four boots a side in a counterbalanced order, on both benches. The switch-bound rows (pingpong.futex.same_cpu,pingpong.pipe.same_cpu,ipc.reqrep.*,wake.onto_busy,time.yield) measure the four skips together and stand for the deleted issue's "cost of a switch with and without the skip". No row may be slower beyond its spread.The images staged for these at f06faad are deleted; the request that listed them is in
r2/stage/on the evidence branch.Not tested by a row: the dying thread switched back in after its process's teardown. No test forces that interleaving; after
leave_address_spacethe thread's context names the kernel's root, and the switch loads nothing butincoming.root.Risks and coordination
quiesce's "tasks stop; CPUs do not"), and nothing clearssmp::answeringonce the APs are released.swapgsentry) will add the user GS base read-back beside FS's in the switch-out; the FS cache no longer depends on a sentinel aWRFSBASEcould write.leave_idle_spaceinto itsanswer_askas well asanswer: an idle holder leaves its root on every answer, a generation's or an ask's.🤖 Generated with Claude Code