Repository navigation
Every kernel stack sits above unmapped memory, and an overflow names its stack - #1211
Conversation
Review r1 of #1211 at
|
Review r1 of #1211, to the owner's design. A thread's stack as a hole in the direct map could not give its memory back, needed a wide shootdown to make each new guard real, and split direct-map leaves on the spawn path with a table allocation that panicked. mm::stack::Stack is now frames its job holds (pmm::alloc_held), mapped at the top of a 256 KiB slot of root slot 510, which the direct map never reaches. The rest of every slot is never mapped in any incarnation, so the guard is true on every CPU with nothing to flush and no direct-map leaf is split at run time. A dropped stack's frames go back at once and its slot's address waits: a spawn that finds no clean slot and 64 retired ones issues one kernel-half shootdown and takes them, otherwise it takes a slot never used; one that can do neither is refused. Until that flush a stale translation reaches only what a kernel use-after-free of the dead stack would, the exposure every frame has through the direct map. On AArch64 the unmap broadcasts each page's invalidation itself. The range's tables are the kernel's own and are never freed, since a paging-structure cache may still name one: one for each 8 slots ever in use at once, a directory for each 4096 (issues/the-kernel-stacks-range-keeps-every-table-it-ever-made.md). A spawn that cannot get a frame for one is refused. No lock is held across an allocation or a zeroing. A CPU's own stacks stay holes in the direct map, carved at bring-up from the kernel's own superframes: an x86-64 AP runs on its idle stack, and loads the IDT that names its IST stacks, under the loader's map, which holds no other range. stack-overflow-report runs the #DF report itself off IST1: the second #DF restarts the handler, which ends the report on one raw line. The record of a pool that never returned memory goes with the pool. The AArch64 record loses its stack-probe sentence: the pinned target probes inline, as a disassembled frame shows. Two unrelated findings are filed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Round two is at
Suggestions. Taken: an actuator that overflows inside the report ( Not done. No negative control for the new range; the PR body says why. |
Review r2 of #1211 at
|
|
Orchestrator, recording the design decision behind the rework (review r2's blocker C asks whose it is). It is mine, taken after review r1 and reported to the owner; it is not an owner ruling, and the record, commit message and PR body should say "the orchestrator's decision".
The tradeoff accepted is one new mechanism, a 4 KiB kernel mapper and unmap on both architectures, kept to what stacks need. |
…ed spawn leaks nothing A spawn refused for want of a table frame built a Stack holding its job's Arc and then forgot it, so the job's count stayed one too high for good. The Stack is now made only once the map has succeeded. The slot lists were two heap Vecs grown under the slot lock, whose doubling reaches the allocator's size assert at 262144 slots. Which slot a stack is handed, and when a retired one is clean, is now kernel::stackslots: three fixed bitmaps over 262144 slots, so nothing allocates, a take past that is refused, and the range's bound is what the bookkeeping holds. Its host tests hold the one silent-failure path: no slot retired since the last flush is handed out, a slot retired during a flush waits for the next, and no more than the batch beyond the peak is ever used. A stack leaf is written accessed and dirty on x86-64, the range's walk asserts it meets tables, and an unmap looks its table up without making one. The range and its never-freed tables are the orchestrator's decision, recorded on the pull request; the record is a question for the owner and no longer claims an acceptance that was not given. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Round three is at A (the leaked B (the slot lists). Replaced by C (whose decision). The record is Suggestions. All taken: the pure host-tested reuse decision, accessed and dirty on x86-64 stack leaves, the table asserts on both architectures, the read-only walk in The host test needed a second pass. A control that makes retired slots clean before the flush is done reds three of the five tests. My first property test ran each flush straight through and stayed green under it; Rows. The ones you listed plus |
Review r3 of #1211 at
|
Review r1 of #1211, to the owner's design. A thread's stack as a hole in the direct map could not give its memory back, needed a wide shootdown to make each new guard real, and split direct-map leaves on the spawn path with a table allocation that panicked. mm::stack::Stack is now frames its job holds (pmm::alloc_held), mapped at the top of a 256 KiB slot of root slot 510, which the direct map never reaches. The rest of every slot is never mapped in any incarnation, so the guard is true on every CPU with nothing to flush and no direct-map leaf is split at run time. A dropped stack's frames go back at once and its slot's address waits: a spawn that finds no clean slot and 64 retired ones issues one kernel-half shootdown and takes them, otherwise it takes a slot never used; one that can do neither is refused. Until that flush a stale translation reaches only what a kernel use-after-free of the dead stack would, the exposure every frame has through the direct map. On AArch64 the unmap broadcasts each page's invalidation itself. The range's tables are the kernel's own and are never freed, since a paging-structure cache may still name one: one for each 8 slots ever in use at once, a directory for each 4096 (issues/the-kernel-stacks-range-keeps-every-table-it-ever-made.md). A spawn that cannot get a frame for one is refused. No lock is held across an allocation or a zeroing. A CPU's own stacks stay holes in the direct map, carved at bring-up from the kernel's own superframes: an x86-64 AP runs on its idle stack, and loads the IDT that names its IST stacks, under the loader's map, which holds no other range. stack-overflow-report runs the #DF report itself off IST1: the second The record of a pool that never returned memory goes with the pool. The AArch64 record loses its stack-probe sentence: the pinned target probes inline, as a disassembled frame shows. Two unrelated findings are filed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
6dc2630 to
bf0d25e
Compare
…ed spawn leaks nothing A spawn refused for want of a table frame built a Stack holding its job's Arc and then forgot it, so the job's count stayed one too high for good. The Stack is now made only once the map has succeeded. The slot lists were two heap Vecs grown under the slot lock, whose doubling reaches the allocator's size assert at 262144 slots. Which slot a stack is handed, and when a retired one is clean, is now kernel::stackslots: three fixed bitmaps over 262144 slots, so nothing allocates, a take past that is refused, and the range's bound is what the bookkeeping holds. Its host tests hold the one silent-failure path: no slot retired since the last flush is handed out, a slot retired during a flush waits for the next, and no more than the batch beyond the peak is ever used. A stack leaf is written accessed and dirty on x86-64, the range's walk asserts it meets tables, and an unmap looks its table up without making one. The range and its never-freed tables are the orchestrator's decision, recorded on the pull request; the record is a question for the owner and no longer claims an acceptance that was not given. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A take that landed inside a flush drew a never-used slot, and the count of slots ever used never falls, so every raced flush grew the range's never-freed tables for good: two threads spawning and joining in a loop could walk an uncharged kernel allocation toward the range's limit while holding two stacks. A take that finds no clean slot while a flush is under way, or with a batch of retired slots waiting, now answers Flush and changes nothing. The kernel passes such a taker through a gate one at a time: the first flushes, the rest find the slots it cleaned. The wait at the gate is the flusher's shootdown, which ends or panics on a CPU that does not answer. A never-used slot is drawn only where fewer than the batch are retired and no flush is under way, so the slots ever used are at most the most alive at once and 64 more, which the model's test now asserts with no term for raced takes. The tables' record states that bound and is a finding again, not a question for the owner. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review r1 of #1211, to the owner's design. A thread's stack as a hole in the direct map could not give its memory back, needed a wide shootdown to make each new guard real, and split direct-map leaves on the spawn path with a table allocation that panicked. mm::stack::Stack is now frames its job holds (pmm::alloc_held), mapped at the top of a 256 KiB slot of root slot 510, which the direct map never reaches. The rest of every slot is never mapped in any incarnation, so the guard is true on every CPU with nothing to flush and no direct-map leaf is split at run time. A dropped stack's frames go back at once and its slot's address waits: a spawn that finds no clean slot and 64 retired ones issues one kernel-half shootdown and takes them, otherwise it takes a slot never used; one that can do neither is refused. Until that flush a stale translation reaches only what a kernel use-after-free of the dead stack would, the exposure every frame has through the direct map. On AArch64 the unmap broadcasts each page's invalidation itself. The range's tables are the kernel's own and are never freed, since a paging-structure cache may still name one: one for each 8 slots ever in use at once, a directory for each 4096 (issues/the-kernel-stacks-range-keeps-every-table-it-ever-made.md). A spawn that cannot get a frame for one is refused. No lock is held across an allocation or a zeroing. A CPU's own stacks stay holes in the direct map, carved at bring-up from the kernel's own superframes: an x86-64 AP runs on its idle stack, and loads the IDT that names its IST stacks, under the loader's map, which holds no other range. stack-overflow-report runs the #DF report itself off IST1: the second The record of a pool that never returned memory goes with the pool. The AArch64 record loses its stack-probe sentence: the pinned target probes inline, as a disassembled frame shows. Two unrelated findings are filed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
bf0d25e to
6d7e355
Compare
…ed spawn leaks nothing A spawn refused for want of a table frame built a Stack holding its job's Arc and then forgot it, so the job's count stayed one too high for good. The Stack is now made only once the map has succeeded. The slot lists were two heap Vecs grown under the slot lock, whose doubling reaches the allocator's size assert at 262144 slots. Which slot a stack is handed, and when a retired one is clean, is now kernel::stackslots: three fixed bitmaps over 262144 slots, so nothing allocates, a take past that is refused, and the range's bound is what the bookkeeping holds. Its host tests hold the one silent-failure path: no slot retired since the last flush is handed out, a slot retired during a flush waits for the next, and no more than the batch beyond the peak is ever used. A stack leaf is written accessed and dirty on x86-64, the range's walk asserts it meets tables, and an unmap looks its table up without making one. The range and its never-freed tables are the orchestrator's decision, recorded on the pull request; the record is a question for the owner and no longer claims an acceptance that was not given. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A take that landed inside a flush drew a never-used slot, and the count of slots ever used never falls, so every raced flush grew the range's never-freed tables for good: two threads spawning and joining in a loop could walk an uncharged kernel allocation toward the range's limit while holding two stacks. A take that finds no clean slot while a flush is under way, or with a batch of retired slots waiting, now answers Flush and changes nothing. The kernel passes such a taker through a gate one at a time: the first flushes, the rest find the slots it cleaned. The wait at the gate is the flusher's shootdown, which ends or panics on a CPU that does not answer. A never-used slot is drawn only where fewer than the batch are retired and no flush is under way, so the slots ever used are at most the most alive at once and 64 more, which the model's test now asserts with no term for raced takes. The tables' record states that bound and is a finding again, not a question for the owner. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
|
Round four is at D. Fixed in code, by the orchestrator's decision. The record. Suggestions. All four taken: the batch constant's doc no longer claims "at most once in this many" for a spent range, Rebase. Twice: onto Checks at the head. The guest rows you listed are green (run 43013777372), the full host run with lint has its one recorded red, and both benches are staged once with the baseline at Not exercised: the gate with two real takers. The model holds the decision; no row puts two CPUs' spawns inside one flush. |
Review r4 of #1211 at
|
…nd are, and the model's test refuses an unowed flush The gate's wait is one shootdown for each taker ahead, not one; the header's table bound pointed at slots in use at once and now points at kernel::stackslots, which bounds the slots ever used. The property test's lone taker asserts begin_flush instead of looping where it answers false. Comment and test text only: both comments keep their line counts, so no panic location moves, and the x86-64 kernel builds to the same bytes before and after. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A thread's kernel stack was a heap allocation with live heap on both sides and a canary word a scheduler pass read after the fact; an IST stack's guard was a filled page; an AP ran its bring-up on a leaked heap block; the boot stack began on the page after the image. Only the idle stacks had an unmapped page under them. mm::stack::Stack is now the one representation: whole pages carved out of a superframe taken as the heap takes its own (so a thread's Charge still covers it), above a page guard_4k takes out of the direct map, followed by a kernel-half shootdown so no CPU keeps the superframe's 2 MiB translation. A thread's stack, each CPU's idle stack and each IST stack are Stacks. An AP runs on its idle stack from its entry, so the bring-up stack is gone. The loader leaves a page between the image and the boot stack and the kernel's tables leave it unmapped. The canary and the heap bands' read of a live stack go with what they stood in for. On x86-64 a fault on a guard page cannot push its frame and is a #DF on IST1, whose report now names the stack (KERNEL STACK OVERFLOW: ... under the kernel stack of pid= tid= / cpuN's idle stack / cpuN's ISTn stack / the boot stack); a kernel #PF that still has stack says the same. A #DF loads IST1's top whatever it interrupted, so an IST stack's overflow, IST1's included, reaches the handler; the handler reports once and a second #DF on its own CPU ends the report with one raw line instead of restarting it. a_kernel_stack_overflow_names_its_stack runs each kind of stack off its bottom with a stack-overflow-* actuator, one boot each. AArch64 gets the same guard pages and still reports nothing, since its vectors run on the stack that overflowed: issues/a-kernel-stack-overflow-on-aarch64-reports-nothing.md. A dropped stack's memory stays in the pool: issues/a-dropped-kernel-stack-never-goes-back-to-the-frame-allocator.md. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A guard page's flush names its CPU (`tlb::flush` records it against `percpu::cpu_id()` once the tables are sized), and `init_bsp` carved the idle and IST stacks before the `wrmsr` that makes `gs:` valid: every x86-64 guest reset there, with no IDT to report the fault (guest-rows run 42939714086). The stacks are carved after the control registers are declared and before the IDT whose gates name them. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Review r1 of #1211, to the owner's design. A thread's stack as a hole in the direct map could not give its memory back, needed a wide shootdown to make each new guard real, and split direct-map leaves on the spawn path with a table allocation that panicked. mm::stack::Stack is now frames its job holds (pmm::alloc_held), mapped at the top of a 256 KiB slot of root slot 510, which the direct map never reaches. The rest of every slot is never mapped in any incarnation, so the guard is true on every CPU with nothing to flush and no direct-map leaf is split at run time. A dropped stack's frames go back at once and its slot's address waits: a spawn that finds no clean slot and 64 retired ones issues one kernel-half shootdown and takes them, otherwise it takes a slot never used; one that can do neither is refused. Until that flush a stale translation reaches only what a kernel use-after-free of the dead stack would, the exposure every frame has through the direct map. On AArch64 the unmap broadcasts each page's invalidation itself. The range's tables are the kernel's own and are never freed, since a paging-structure cache may still name one: one for each 8 slots ever in use at once, a directory for each 4096 (issues/the-kernel-stacks-range-keeps-every-table-it-ever-made.md). A spawn that cannot get a frame for one is refused. No lock is held across an allocation or a zeroing. A CPU's own stacks stay holes in the direct map, carved at bring-up from the kernel's own superframes: an x86-64 AP runs on its idle stack, and loads the IDT that names its IST stacks, under the loader's map, which holds no other range. stack-overflow-report runs the #DF report itself off IST1: the second The record of a pool that never returned memory goes with the pool. The AArch64 record loses its stack-probe sentence: the pinned target probes inline, as a disassembled frame shows. Two unrelated findings are filed. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…rains after the raw line Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…ed spawn leaks nothing A spawn refused for want of a table frame built a Stack holding its job's Arc and then forgot it, so the job's count stayed one too high for good. The Stack is now made only once the map has succeeded. The slot lists were two heap Vecs grown under the slot lock, whose doubling reaches the allocator's size assert at 262144 slots. Which slot a stack is handed, and when a retired one is clean, is now kernel::stackslots: three fixed bitmaps over 262144 slots, so nothing allocates, a take past that is refused, and the range's bound is what the bookkeeping holds. Its host tests hold the one silent-failure path: no slot retired since the last flush is handed out, a slot retired during a flush waits for the next, and no more than the batch beyond the peak is ever used. A stack leaf is written accessed and dirty on x86-64, the range's walk asserts it meets tables, and an unmap looks its table up without making one. The range and its never-freed tables are the orchestrator's decision, recorded on the pull request; the record is a question for the owner and no longer claims an acceptance that was not given. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Run straight through, a flush that made retired slots clean before it ended was not seen: nothing took a slot while it was under way. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
A take that landed inside a flush drew a never-used slot, and the count of slots ever used never falls, so every raced flush grew the range's never-freed tables for good: two threads spawning and joining in a loop could walk an uncharged kernel allocation toward the range's limit while holding two stacks. A take that finds no clean slot while a flush is under way, or with a batch of retired slots waiting, now answers Flush and changes nothing. The kernel passes such a taker through a gate one at a time: the first flushes, the rest find the slots it cleaned. The wait at the gate is the flusher's shootdown, which ends or panics on a CPU that does not answer. A never-used slot is drawn only where fewer than the batch are retired and no flush is under way, so the slots ever used are at most the most alive at once and 64 more, which the model's test now asserts with no term for raced takes. The tables' record states that bound and is a finding again, not a question for the owner. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
…nd are, and the model's test refuses an unowed flush The gate's wait is one shootdown for each taker ahead, not one; the header's table bound pointed at slots in use at once and now points at kernel::stackslots, which bounds the slots ever used. The property test's lone taker asserts begin_flush instead of looping where it answers false. Comment and test text only: both comments keep their line counts, so no panic location moves, and the x86-64 kernel builds to the same bytes before and after. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
22a577a to
384db88
Compare
|
Orchestrator: checked before queueing. |
…ct map holds Since #1211 the direct map leaves out the bring-up guard pages, so it no longer maps every frame; it maps every frame a user page can hold, which is what the read relies on. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Problem
A thread's kernel stack was 128 KiB of kernel heap with live heap on both sides. Running off it wrote into a neighbour, and the only check was a canary word a scheduler pass read afterwards (
issues/a-threads-kernel-stack-has-no-guard-page.md). An IST stack's guard was a filled page read after a crash, an AP ran its bring-up on a leaked heap block, and the boot stack began on the page after the image. Only the idle stacks had an unmapped page under them.What it does now
On x86-64 a fault on a guard cannot push its frame, so it is a
#DFon IST1. Its report, and a kernel#PFthat still has stack, name the stack:The canary, the heap bands' read of a live stack,
OwnedAlloc,sched/idle_stack.rsandsmp::bringup_stackare deleted.The design (the orchestrator's decision after review r1, recorded on this PR; not an owner ruling)
Thread stacks do not live as holes in the direct map. They are mapped in root slot 510 (
mm::stack::RANGE), which the direct map never reaches, in 256 KiB slots: the stack at the top, nothing below it.pmm::alloc_heldtakes them and they go back when the stack drops; a job over its allowance is refused the spawn. The heapChargeno longer covers the stack.FLUSH_BATCH) issues one kernel-half shootdown and takes them; with fewer waiting it takes a slot never used. A spawn that finds a flush already under way waits at a gate for the slots it cleans and draws no new slot; the wait is that shootdown, which ends or panics on a CPU that does not answer. So while never-used slots remain a flush follows at least 64 drops, an exit pays none, and nothing is flushed from the reaper on the idle path.kernel::stackslots, besidePcidPool): three bitmaps over 262144 slots, 96 KiB of the kernel's image. Nothing allocates under the slot lock. The range holds 262144 stacks at once (64 processes at their thread limit); a spawn past that is refused.issues/the-kernel-stacks-range-keeps-every-table-it-ever-made.mdand the manual's memory page.Review history
Stack, and the slot lists were heapVecs grown under the slot lock whose doubling reached the allocator's size assert at 262144 slots. Both are fixed: theStackis built only once mapped, and the slots are the fixed-size pure module. Also taken: accessed and dirty preset on x86-64 stack leaves, the walk's table asserts, and an unmap that looks its table up without making one.What the double-fault path needs
#DFwhatever it interrupted (SDM Vol. 3A §6.14.5), so an overflow of IST2, IST3 or IST1 itself still reaches the handler.#DFon the same CPU writes one raw line and halts the machine.gs:is valid, before the IDT is loaded.Memory cost per thread (observation, not measured under load)
128 KiB of stack plus the machine-state area rounded to pages, in frames: 33 frames where the area fits one page, against a 128 KiB-plus-area heap chunk before. The guard costs no memory. An eighth of a 4 KiB table per slot ever in use is the kernel's.
Files other open PRs touch
#1200, #1204 and #1212 have landed and the branch is rebased over all three; #1202 is closed. Of the PRs open now, #1223 also edits
kernel/src/scheduler.rsand #1131tests/toyos.rs.syscall/dispatch.rsandsdk/abiare untouched.Checks
Head
384db8897: the eight commits rebased onto34cb4c918, where GitHub showed a conflict. The rebase changed one line of the branch, the first commit'scrate::processimport inkernel/src/loader/start.rs, after #1204 madeENDOW_ENTRY_LENprivate (r6/range-diff.txt). #1204's other edits toloader/start.rsandprocess.rsare the spawn endowment's wire format and rights and leave the stack's allocation alone. Main has moved since by three merges that change onlyissues/, none a file of this branch; GitHub reports the PR mergeable. Logs: evidencepr-1211atcfe3fdaa.At the head, chosen for what the rebase can change, the kernel's code compiling and the pure logic beneath it:
cargo run -- --ci hostat384db8897stackslotstests among them; clippy's 26 invocations clean, the kernel for x86-64 and AArch64 in every feature set the stage builds; harness, workspace host members, toyos-crypto and licences passed; the build system's one red is the macOS-onlyllvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key(issues/the-llvm-environment-test-breaks-git-on-macos.md). The run was ended by a SIGTERM from outside it in the models stage, which this change does not owe: no loom model includes a file the branch changes, and the scheduler simulator coverskernel/pure/sched/, which it does not changer6/ci-host.logAt
6d7e3557a, reused: the branch's code is the head's but for the import above and review r4's two comments and one test assert, after which the x86-64 kernel built to the same bytes (r5/kernel-hashes.txt); what the rebase brought in under it is #1204's, which touches no stack code. Not rerun after the rebase for that reason: the guest rows, the host controls and the disassembly.a_kernel_stack_overflow_names_its_stack(five boots),nested_nmi_is_loud,std_threads_count_the_cpus_and_give_back_ended_threads,libc_children,kernel_wx_leaf_is_refused(x86-64 q35 under KVM);virt_smp,virt_user_mode,virt_kernel_wx_leaf_is_refused(AArch64)r4/guest-rows-43013777372.logr4/stackslots-tests.logdiff,logr3/control-no-wait.logcargo run -- --ci host, clippy includedllvm::tests::the_caller_s_environment_reaches_neither_the_build_nor_the_key(issues/the-llvm-environment-test-breaks-git-on-macos.md); every other failure list in the log is a mutation control'sr4/ci-host.log130d5e74fsub sp, sp, #0x1, lsl #12/str xzr, [sp]r2/aarch64-probe-excerpt.txtThe five overflow boots of guest run 43013777372:
What is not shown.
#DFwhosecr2lies in the slot's computed guard). The reuse logic, whose failure would be silent, has the host controls above instead.stack=count has been seen.Metal
Four boots, run by the orchestrator: on the T14 and on the AMD bench, the base
422915e2cwithspawn_costandsyscall_cost, and the image built at6d7e3557awithkernel_wx,smp_roster_and_tsc_trail,topology_readback,pmm_accounting,spawn_cost,syscall_costand the shared memberabuse_thread_table. Each image's revision is read from its ownsupervisor: buildrecord. The head image's x86-64 kernel is22a577abe's byte for byte; it predates the rebase, which adds #1204's endowment changes and no stack code.Judged by the head's own row judges, under a local patch that is published and not committed (
r6/judge-local.diff). The bench driver that ran these boots predates #1213 and wrote noback_unix_secs, and the judge refuses such a readback before it reads a row. OnlyReadback::ran_withinreads the field, and onlywall_clock_is_firmwarescalls that, which is not among these rows. The patch also points the judge at a copy of the benches' records, so judging these readbacks adds nothing to the shared ones. The row judges are the same at6d7e3557a, at422915e2cfor the two cost rows, and at the head.supervisor: buildrecord422915e2c6d7e3557a422915e2c6d7e3557asyscall_cost, cycles a syscallspawn_costmin / median, µs over 50Boot: completems / back in s, against the record's 1152 / 50 (T14) and 1312 / 36 (AMD)judge-t14-basejudge-t14-headjudge-amd-basejudge-amd-headspawn_costandsyscall_costare printed for an A/B and have no record on either bench, so the comparison is the table's, one boot a side, which is not a distribution: the T14's syscall is 4 cycles dearer and its spawn 2 µs dearer at the minimum, 7 µs cheaper at the median; the AMD bench's syscall is unchanged and its spawn 21 µs dearer at the minimum (10%), 15 µs at the median (6%).smp_roster_and_tsc_trailon the head images is the first metal boot, on both benches, of APs that enter on their own guarded idle stacks under x86-64's boot map holds no page writable and executable #1212's W^X boot map (8 CPUs, every AP's TSC within 181 cycles; 16 CPUs, within 288). The base boots ran that boot map with the bring-up stack this branch deletes.abuse_thread_tablepassed on both: 4095 threads exited unjoined, the next was refused, and a join made room for one. It spawns one thread at a time and each exits as soon as it has run; a thread's kernel stack is released at the next scheduler pass after its exit, not at its join (by reading). So its stacks retired one by one, with few alive at once, and by the slots' logic a spawn flushed each time 64 had retired, about 64 flushes over the row. That is inference: the censustlb:line, whosestack=counts them, is in no readback, because the kernel writes it where the machine stops, after logkeeper has made the log file whole.LOCK CONTENTION,DEADLOCK,DOUBLE FAULTorKERNEL STACK OVERFLOW, and none namingmm/stack.rs, in any of the four kernel logs.clock: TSC STEP; its base boot has the same, 1.69 ms. That isissues/chipset-counters-that-stall-together-read-as-a-tsc-step.md, not this change.#DFhalts every CPU before logkeeper can make the log whole, so no metal row can judge one), the flush path and the gate, and AArch64, which has no bench.🤖 Generated with Claude Code