Repository navigation
A user change asks each CPU running the space for its range, and owes itself nothing - #1267
Conversation
… itself nothing A change to a user space's leaves reached the CPUs running it through the machine-wide generation. So every CPU, targeted or not, owed it a flush of every user translation. The initiator paid one too, in its own wait, though it had already flushed its pages one by one. And each CPU reached flushed every user translation for what was often one page. The 2026-11-11 stage probes put about 900 of the T14's 3,341 ns for `mem.munmap_touched_remote.4k` in the shootdown, 200 of it the initiator's own full flush and 254 the target's. Now each CPU has a mailbox (`shootdown::Asks`). The initiator writes its range there, sets its bit in each target's asking set, sends the IPIs, and flushes its own pages while they fly. A target takes every bit asking it. Asked once, it flushes that range by page up to 64 pages; asked by more than one initiator, or for a longer range, it flushes every user translation. It then sets its bit in each asker's answered set. The initiator waits for its own answers only, so a CPU not asked owes nothing, and the initiator never asks itself. Why the answers are per pair, and not the generation the design proposed: a generation read as a count would let a target that flushed one initiator's range answer another initiator's earlier generation, whose range it never flushed. A bit per asking CPU is consumed exactly once. An initiator clears its answers before it asks, and asks again only once every target has answered. So no answer stands for a later ask, and no target reads a mailbox while its owner rewrites it. Machine-wide rounds keep the generation: kernel-half changes (wide), PCID reclaim, MMIO, counters, SMI_CMD and the clock handover. The IPI handler and every spin answer both, and only what they owe. A flush of a range does not stamp `tlb_tags`. Only a flush of every user translation does. kernel-loom's `tlb_shootdown` models the ask: an answered ask flushed its range after the write; two asks of one CPU at once; a mailbox reused; two CPUs asking each other from their waits. Four new negative controls red it: the take `Relaxed`, the range written after asking, answers left standing across asks (mailbox reuse), and two asks answered with one range. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review of #1267 at cf96bb9 (base main 45935f4)Architecture. The split is right. The change of mechanism is a real soundness fix. Suppose a One/Many mark carries the machine-wide generation. B marks the target first, and the target flushes only B's range and publishes generation 6. A's wait for generation 5 is then satisfied before A's mark even lands, and A's range was never flushed. Per-pair asking and answered bits remove this:
So no answer outlives its ask, and no mailbox is rewritten while a target reads it. All six approved outcomes hold in the code ( Size: net production is +175 lines ( Ask/answer correctness, traced.
Memory ordering.
#1251. The composition is sound as stated, with one obligation.
Kernel half (#1211).
Loom. The models are adequate for the protocol's safety. Each of these reds for its stated reason in the logs:
Gaps:
RequiredB1.
B2.
B3. B4. For the orchestrator, in the PR description. The reason given for running no guest rows is false:
My judgment is that no guest row is owed. The queued metal boots exercise the ask on a PCID x2APIC machine and on a no-PCID machine, with all CPUs churning. That is stronger liveness evidence than a two-CPU q35 guest, and the description should give that reason instead. S10 names a cheap optional dispatch. Suggestions
Evidence and the A/B ruleChecks at cf96bb9. The local logs match the published evidence;
The fixes asked above touch only comments and test names, so the queued metal at cf96bb9 and 1466c03 stays valid. Re-run
What metal must show for LAND:
SEND BACK. B1-B3 are small fixes to comments and test names; B4 is the orchestrator's correction to the description. With those done, LAND pending the metal results above. |
…what keeps the IPI's read fresh - `an_initiator_answers_while_it_waits` goes. With `Shootdown::wait_turn` gone, it ran the serve itself and could not red for the deadlock it cited; what it still asserted, a serve answering a later generation, the other models cover. The order in which a wait answers before it asks is `arch::tlb::wait_for`'s, which loom does not compile, and its doc states it. The ask's counterpart is renamed to what it asserts: `each_asker_is_answered_once_with_its_own_range`. - `Shootdown::owes` said a stale `false` costs nothing. The IPI handler now serves only what is owed, so a stale read there would lose the IPI. The comment now says what keeps the handler's read fresh: the send is ordered after the issue. `Asks::serve`'s gate says the same. - `UserTables::shoot_down`: every reached CPU's user translations go only where tables went; otherwise a short range goes page by page. - A negative control on the answer edge, `shootdown-answer-relaxed`: the answer's Release made `Relaxed`, which reds `an_answered_ask_flushed_its_range_after_the_write`. The generation path already has `shootdown-served-relaxed`. - `Flush::All` is `Flush::User`, so it is not read as the flush that also drops global translations. - `Asks::ask` states as its caller's precondition that the last ask was answered. The no-PCID flush cites APM Vol. 2 §5.5 beside the SDM. The lock spin's comment names `Asks::serve` as surviving a nested IPI too. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Re-review of a5ca5faScope: whether round 1's findings are resolved, and whether the changed code is correct. What I read:
Round 1
Changed code
FindingsBLOCKER
SUGGESTION
Guest runsBoth are queued:
RuleAmendment 2 of For the orchestrator: its label "AMENDMENT 2, … ~21:20" is later than the commit that published it, at 21:01:48 +0200. The file was last written at 20:58. Landing conditions
SEND BACK, for the one line. |
…wed on metal The answer's `Release` comment repeated the one on `ANSWER` itself. With no bench booting, the record names what metal still owes the asks: - the T14's `test_rs_tlb_shootdown_waits`; - an A/B on both benches of the unmap, protect and churn rows, from before and after; - each bench's cost of a per-page flush against a full one, which decides `BY_PAGE`. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
Orchestrator: landing under the benches-down rule. Review r2 SEND BACK was one comment deletion, verified at e11ea5e. Guest run 43092270183 at e11ea5e (incl. |
…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>
Problem
A change to a user space's leaves (
arch::tlb::user_change) reached the CPUs running the space through the machine-wide shootdown generation. On the T14 the 2026-11-11 stage probes put about 900 of the 3,341 ns ofmem.munmap_touched_remote.4kin the shootdown (evidence):Change
The design approved in
design-shootdown.md, with one change of mechanism, explained below.shootdown::Asks).user_changewrites its range there, sets its bit in each target's asking set, sends the IPIs, and then flushes its own pages while they fly.INVPCIDby address with PCID,INVLPGwithout.tlb_tags, as before; a range flush does not.SMI_CMDand the clock handover. The IPI handler, every lock spin and every initiator's wait answer both kinds, and only what they owe. So a CPU no longer flushes on an IPI that only asked it for a range.The mechanism differs from the design. The design kept the global generation and put it in a per-CPU One/Many mark. With a range flush that is unsound. A target that served one initiator's range publishes a generation. That number also counts as an answer to an earlier generation of a second initiator that had not yet marked the target, whose range it never flushed.
So answers here are per pair. Each asking bit is consumed exactly once. An initiator clears its answers before it asks, and asks again only once every target has answered. So no answer stands for a later ask, and no target reads a mailbox while its owner rewrites it: the design's invariant that the outbox is not reused until the wait ends.
The approved outcomes all hold:
tlb_tags.With #1251 (switch-drops-unneeded-work, in review; read at a99cd8c):
Tags::changed(tag, tables, ..)). That composes as is: the ask goes only to the CPUschangednames.answerloads the kernel's root on an idle CPU after its flush (leave_idle_space). This PR's ask answer (answer_ask) must do the same. Whichever of A switch loads only what the incoming thread does not already have #1251 and A user change asks each CPU running the space for its range, and owes itself nothing #1267 lands second putsleave_idle_spaceintoanswer_ask. Forgetting it fails loudly:retire'sassert!(!held(tag))fires at the first exit that has an idle holder.retireasks for[0, u64::MAX), which is past 64 pages, so it is answered with a flush of every user translation, as it is today.invalidation.rs.INVLPGB stays out of scope. It is already recorded in
issues/features-no-proving-machine-is-known-to-offer.md.Checks
At a5ca5fa (review r1; the head e11ea5e only deletes one comment and adds the record's owed list), each exit 0 unless the note says red (logs). The checks at cf96bb9 are here.
kernel-loom
tlb_shootdown, 11 models. Four are new, on the ask:counters_roundandtlb_tagsalso pass.Not modelled: the order in which a waiting CPU answers before it reads its own answer lives in
arch::tlb::wait_for, which loom does not compile. Its doc states it, and inspection holds it. Round 1 had two models that claimed it and could not red for it:an_initiator_answers_while_it_waitsis deleted, and its ask counterpart is renamed to what it asserts.Negative controls, each red with its listed verdicts. Five are new and are in
build/ci.rs'sCONTROLS:shootdown-take-relaxed: the target takes its asksRelaxed.shootdown-answer-relaxed: the target answersRelaxed. This is the answer edge, the counterpart ofshootdown-served-relaxed.shootdown-ask-range-late: the range is written after asking.shootdown-ask-stale-answers: answers are left standing across asks. This is the mailbox reuse.shootdown-ask-many-as-one: two asks are answered with one range.shootdown-serve-relaxedandshootdown-served-relaxedstill red.Kernel library:
cargo test -p kernel --lib --features sched-check, 459 passed. It compiles neithershootdown.rsnortlb.rs.Clippy in the kernel's CI shapes, with the adopted lints and
-D warnings:x86_64-unknown-noneplain, with the actuators, and with every instrument;aarch64-unknown-none-softfloat;kernel-loom, all targets, with and without the shootdown controls.toyos-build'severy_model_control_is_run.The full
cargo run -- --ci hostwas not run, under the owner's testing policy.Guest run 43092270183 at the head e11ea5e, x86-64 and AArch64 together. It is queued. It shows the kernel boots and that shootdowns complete, under KVM with
-cpu host:virt_smp;std_threads_count_the_cpus_and_give_back_ended_threads: thread stacks are unmapped while a sibling runs;affinity_pins_hold_on_every_cpu;a_user_copy_faults_in_mid_copy_or_ends_as_bad_address;desktop_without_x2apic: four CPUs in xAPIC mode, the only row whose IPIs use the two-half ICR.The runs at the measurement branches (43081320048, cancelled; 43082013102, green) were for flashing and are not code-identical.
Owed on metal, in
issues/a-tlb-shootdown-reaches-only-the-cpus-that-ran-the-space.md. The benches are down, so none of it is a landing condition:shared-debugboot. Main must passtest_rs_tlb_shootdown_waits: a change still waits for every CPU running the space.osbench_systemandosbench_mem, main from before and after. Main after must readmem.munmap_touched_remote.4kfaster, andmem.munmap_part.4k,mem.mprotect.4kand the multi-threadmem.malloc_churn.*rows no slower.INVPCIDon the T14,INVLPGon the AMD bench) against a full user flush, which decides whetherBY_PAGE(64) is lowered. The branchesmemperf-sd-ab-baseandmemperf-sd-ab-headcarry a probe that logs both.The speed-up this PR exists for is not yet measured on metal. The 900 ns of shootdown it addresses is the diagnosis probes' attribution, not a measured gain.
Not taken, unmeasured: aligning each
Mailboxto 128 bytes, so that a spinning initiator's lines are not a neighbour's. It is the first thing to try if the A/B shows the remote row gaining less than expected.Risks
tlb_tagsstill sees a change since its last full flush and flushes everything at that entry, as before. Whether that moves cost rather than removing it is for the A/B and later rows to say.INVLPGon the current space. It is correct because, without PCID, aCR3load drops the previous space's translations (SDM Vol. 3A §4.10.4.1, APM Vol. 2 §5.5).🤖 Generated with Claude Code