task(slice3c): a CPU pin a production thread can carry (inert scaffolding for the per-CPU ksoftirqd and pinned-wake slices) - #811
Merged
Conversation
Ported per hunk from the parked aarch64 testing-profile branch onto main, as the representation the per-CPU ksoftirqd work needs. Nothing in this change sets a pin, so nothing in it moves a thread. * `Thread::cpu_affinity: Option<CpuPin>` and the `CpuPin` type itself (4f6988c's `Option<usize>` field, widened by 5cefa78 to a pin that records WHY it exists -- a per-CPU worker whose bitmap lives on its home CPU, versus a hold pen that is placement only). * `Scheduler::add_thread_inner` resolves the pin to its CPU, bounds it by the online range, and falls through to `least_loaded_cpu()` when there is no pin (4f6988c, with 5cefa78's `.map(|pin| pin.cpu)`). * `scheduler::spawn_on_cpu` (4f6988c, stamping 5cefa78's `per_cpu_worker` pin) and `kthread::kthread_run_on_cpu` (4f6988c). * Every `Thread` struct literal names the new field: 13 sites set `None`, the fork copy in `process/manager.rs` inherits the parent's (ef97d86, whose point was exactly this literal), and the `Clone` impl carries it. Inert by construction, and that is the reason the gates are expected unchanged: `spawn_on_cpu` has 1 call site (`kthread_run_on_cpu`), which has 0, so `cpu_affinity` is the empty state on every thread this kernel builds and `add_thread_inner` takes the `least_loaded_cpu()` fall-through it took before. The pinned-wake liveness filter, the park protocol, the steal and reclaim dispositions and the per-CPU ksoftirqd topology are NOT in this change. Not ported from the branch: f7303d1's testing-cfg mutable binding, which exists only for the `TEST_BINARY_STAGING` hold pen that is not on main -- so `add_thread_inner` keeps its immutable `thread` parameter and the warning that commit fixed cannot arise here. Builds: aarch64 no-features, `boot_tests` and `testing` each exit 0 with 0 `^(warning|error)` lines beyond the toolchain's `core v0.0.0` future-incompat notice; `scripts/check-kernel-no-neon.sh` reports 0 FP/SIMD load/store instructions after each. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
Two rules in tests/loopback_pump_structure.rs, both censused by shape rather than pinned to a line or to a name list. `validate_thread_placement_honours_the_cpu_pin` DERIVES its subject: a scheduler function that resolves a ready-queue target through `least_loaded_cpu()` is a placement function, which is 1 of 1 today (`add_thread_inner`). Each one must read `cpu_affinity` BEFORE the least-loaded choice, resolve it with `.map(|pin| pin.cpu)`, bound it by `online_cpu_count()`, and fall through to `least_loaded_cpu()` for an unpinned thread. An empty census is itself an error, so deleting the placement path does not silence the rule. `validate_cpu_pin_records_its_reason` censuses the constructors of `CpuPin`: each must name the kind it builds, both kinds must be constructible, and `Thread::cpu_affinity` must carry the pin rather than a bare CPU index -- the distinction the later reclaim disposition reads. Mutation, applied singly to kernel/src/task/scheduler.rs and then reverted -- the pin arm in `add_thread_inner` deleted, restoring the unconditional `let target = self.least_loaded_cpu();` main carried before this branch: bash scripts/run-structure-tests.sh loopback_pump_structure -> exit 101 test result: FAILED. 66 passed; 3 failed; 0 ignored; 0 measured thread 'thread_placement_honours_the_cpu_pin' panicked at tests/loopback_pump_structure.rs:1844:10: a pinned thread is queued to the CPU its pin names: "add_thread_inner does not resolve the thread's pin to the CPU it names" The other 2 reds in that run are the mutation-fixture tests themselves (`thread_placement_validator_rejects_a_deleted_pin_arm` and `..._rejects_an_unbounded_pin`): each re-applies the mutation to a source that already carries it, so its "fixture mutation must apply" assertion fires. They are not independent evidence. Unmutated, at the committed bytes: bash scripts/run-structure-tests.sh loopback_pump_structure -> exit 0 test result: ok. 69 passed; 0 failed; 0 ignored; 0 measured claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…claimed docs/planning/green-program/aarch64-testing/SLICE3C-2026-09-05.md records the round: the per-hunk provenance of every ported line (4f6988c, 5cefa78, ef97d86), the hunks deliberately left on the parked branch for slices 3d, 3e and 3f, the inertness census (`spawn_on_cpu` has 1 call site, `kthread_run_on_cpu` has 0, so 15 of 15 `Thread` build sites hold the empty pin), the ratchet and its mutation verbatim, the builds, the boots with their `pgrep` readings, the x86 legs on beast, and a section 9 of things this branch does NOT claim. Two disclosures the doc carries rather than hides: the `pub(crate)` -> `pub` visibility change made while porting, and its dead-code reason; and the 2 build artifacts this worktree symlinked from the primary checkout instead of rebuilding (`userspace/programs/aarch64` and `target/ext2-aarch64.img`), both userspace outputs this branch changes 0 bytes of. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
`poll_tcp_gate_wiring_structure`'s census reads each file under `docker/qemu/` and `scripts/` and `unwrap`ped `read_to_string`, so a single non-UTF-8 file under either root aborted the whole ratchet: thread 'every_oracle_fail_gate_also_requires_the_kernel_ready_lost_marker' panicked at tests/poll_tcp_gate_wiring_structure.rs:102:37: read script <repo>/scripts/__pycache__/claim-lint.cpython-314.pyc The 2 commands the claim-discipline rule asks a round to run are in conflict because of it: `python3 scripts/test_claim_lint.py` imports `scripts/claim-lint.py` and leaves that `.pyc` behind, and the suite battery run afterwards then scores a red on a tree whose 8 of 8 censused gate scripts are correctly wired. This round hit it at its own committed head, which is why it is repaired here rather than reported. A file that is not valid UTF-8 cannot contain the FAIL literal, so it is not a member of the census and failing to read it is not an error. The rule itself is untouched, and its anti-vacuity leg still fires -- mutation, applied singly to this file and then reverted, forcing the census empty with `.map(|text| false && text.contains(ORACLE_FAIL_LITERAL))`: found 0 scripts under docker/qemu/ or scripts/ asserting "POLL_TCP_ORACLE:FAIL" -- this ratchet would be vacuously true, which is exactly the failure mode it exists to prevent Unmutated, with the `.pyc` still on disk: 3 passed, 0 failed; the whole battery is 30 of 30 files, 550 cases. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
Section 5 of the slice doc now carries the one red the second full battery scored -- `poll_tcp_gate_wiring_structure` aborting on the `.pyc` the claim-lint self-test leaves under `scripts/` -- the repair, its anti-vacuity mutation verbatim, and the 30-of-30 re-run afterwards. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
The gate runs happened before the branch's later commits, so section 7 now states the provenance directly: the kernel sources reached their final state in 7257e09 and the 3 commits after it touch 0 lines under kernel/, so the 6 aarch64 boots and the 2 x86 boots ran the kernel this branch lands. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
origin/main moved from 814bc65 to 6f36e1a (PR #809, the ASID-tag census) while this round ran, so the branch carries a merge commit and each gate was run a second time against what would actually land: * 3 aarch64 builds, each with its no-NEON guard: same reading as before. * strict x3: PASS 3/3, 36 s. * production x3: 3 x PASS, pgrep 0 before each. * the structure battery: 30 of 30 files, 559 cases -- 9 more than before, which are PR #809's own ratchet. * x86 on beast at 3898de8: both builds clean, prod-profile boot exit 0, boot_tests exit 0 with `x86 userspace gate: PASS - exited=110 expected>=105 nonzero=0 allowlist=0` and `x86 frame-custody gate run 1: PASS`. Sections 5, 6, 7 and 8 now carry both rows, pre-merge and merged, rather than implying one run covered both trees. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…ssion text Review findings S3C-M1 and S3C-M2, both majors, both about the same defect class: the rules landed in 862fa29 matched exact expression literals, so a change that STRENGTHENED the code they police reddened them, with a message asserting the opposite of what the code did. S3C-M1. `validate_thread_placement_honours_the_cpu_pin` required the compact literals `cpu_affinity.map(|pin|pin.cpu)`, `.filter(|cpu|*cpu<self.online_cpu_count())` and `.unwrap_or_else(||self.least_loaded_cpu())`. The first forbade a pin-kind pre-filter, and the second forbade adding the scheduling-liveness conjunct -- the repair slice 3d owes this very arm -- while printing "does not bound the pinned CPU by the online range", which is false about a strictly stronger bound. The 4 requirements are now read by shape from the statement that runs from the pin read through the fall-through: a `.cpu` field read off a non-self receiver, a `filter` adapter whose predicate names `online_cpu_count` (so the bound REJECTS rather than rewrites), and an `unwrap_or_else` whose argument calls `least_loaded_cpu`. 3 new helpers do the parsing: `compact_call_arguments` (balanced argument text of each `.method(` call), `compact_field_access`, and `statement_region`. S3C-M2. `validate_cpu_pin_records_its_reason` walked each `fn` inside `impl CpuPin` and demanded `per_cpu_worker: true` or `false` of it, so adding an accessor -- the shape slice 3e's reclaim disposition reads the kind through -- reddened it with "builds a pin without naming the reason it exists" about a function that builds no pin. The census is now over the functions whose body builds a pin, which is 2 of the 2 the impl holds today. 1 fixture was re-shaped for the same reason: `thread_placement_validator_rejects_an_unbounded_pin` deleted the filter by matching its exact text, which collapses the moment the predicate is strengthened. `without_call_matching` now removes the `filter` adapter whose predicate names `online_cpu_count`, so the leg survives both repairs. 3 tests added, 3 of 3 GREEN legs -- the guard against the brittleness returning: `thread_placement_validator_accepts_a_stronger_bound`, `thread_placement_validator_accepts_a_pin_kind_prefilter`, and `cpu_pin_reason_validator_accepts_a_non_constructor_method`. 5 mutations, each applied singly to the live kernel file and reverted. Legs 3-5 are the review's own reproductions, RED before this commit and GREEN after: 1 pin arm deleted, restoring `let target = self.least_loaded_cpu();` -> RED "add_thread_inner does not resolve the thread's pin to the CPU it names"; test result: FAILED. 67 passed; 5 failed 2 the bounding filter deleted alone -> RED "add_thread_inner does not reject a pinned CPU outside the online range"; test result: FAILED. 68 passed; 4 failed 3 bound strengthened with `&& self.cpu_accepts_wakeups(*cpu)` -> GREEN, test result: ok. 72 passed; 0 failed 4 `.filter(|pin| pin.per_cpu_worker)` inserted ahead of `.map(|pin| pin.cpu)` -> GREEN, test result: ok. 72 passed; 0 failed 5 `pub const fn is_per_cpu_worker(&self) -> bool` added to `impl CpuPin` -> GREEN, test result: ok. 72 passed; 0 failed The extra reds in legs 1 and 2 are the mutation fixtures themselves, whose surgery target the mutation removed; they are not independent evidence. Unmutated, at the committed bytes: bash scripts/run-structure-tests.sh loopback_pump_structure -> exit 0 test result: ok. 72 passed; 0 failed; 0 ignored; 0 measured (each tests/*_structure.rs) green/total: 30 / 30 cases: 562 python3 scripts/test_claim_lint.py -> exit 0 0 lines under kernel/ change in this commit. claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
…nresolved, and a false provenance sentence The 6 minors from the round-1 review. 5 of them are disclosures the round-1 document owed and did not make; 1 is a false sentence. S3C-m6 (FIXED). Section 7 said "this branch's own kernel sources reached their final state in 7257e09 ... and the 4 commits after it touch 0 lines under kernel/". That count was taken in 71f2e7f, the commit that added the sentence, and was not re-derived when the merge and 1 further commit landed. At 8f5e6c9 there are 7 first-parent commits after 7257e09, and 1 of them -- the merge 3898de8 -- changes 8 files and 237 added / 16 removed lines under kernel/. Section 7 now gives the 3 commands that measure it, and states the claim that is actually true and checkable: 6 of the 7 are this branch's own and touch 0 lines under kernel/, the 7th brings origin/main's lines rather than this branch's, and the branch's own contribution is unmoved -- `git diff origin/main...HEAD -- kernel/` is byte-identical to `git diff 814bc65 7257e09 -- kernel/`. S3C-m1 (DISCLOSED). A production CpuPin is enforced at 1 site, add_thread_inner, and is invisible to 5 of 5 sites that could move a thread off the queue its pin named: reclaim_unschedulable_cpu_queues and the 4 work-stealing sites all go through retain_cpu_affine_test_thread, which is keyed on BOOT_TEST_CPU_AFFINITY rather than Thread::cpu_affinity and sits behind cfg(aarch64 + boot_tests). Section 2 gains the grep and the consequence, section 9 gains a bullet, and the plan's 3e gains a binding item: guard 5 of 5, not the reclaim site alone. S3C-m2, m3, m4, m5 (DISCLOSED). A new section 10 enumerates 4 states the representation makes expressible that this branch does not resolve, each with its file:line and the slice that must rule on it: the out-of-range pin discarded while the field keeps naming the CPU with 0 counters; the pin arm's bound, which tests the online range where the fall-through also tests cpu_accepts_wakeups and whose repair is the liveness test AND the park, not the conjunct alone; the disagreement between the CoW-fork literal that copies the pin and sys_clone that does not; and the 2 independent copies of the field -- the process-table row and the scheduler's publication clone -- with no single authority. The plan's 3d carries 4 of the 4 as binding items. 0 of the 4 is live: 0 threads carry a pin. Section 10 also records the round: the disposition of 8 of 8 findings, the mutation table now in section 4, and the fact that this round changes 0 lines under kernel/ (`git diff --stat 8f5e6c9..HEAD -- kernel/` prints nothing), which is why sections 6, 7 and 8 are not re-run. By-catch, fixed in the same round: the plan named the parked branch's park helper `park_pinned_worker_without_home` at 4 places. The name on 5cefa78 is `park_pinned_thread_without_home` -- `git show 5cefa78:kernel/src/task/scheduler.rs | grep -n park_pinned` prints 7 lines, 6 call sites and the definition at 4066. 4 of 4 corrected. python3 scripts/claim-lint.py -> exit 0 python3 scripts/claim-lint.py --files <SLICE3C doc> -> exit 0 python3 scripts/claim-lint.py --files <SLICE3 plan> -> exit 0 python3 scripts/test_claim_lint.py -> exit 0 (each tests/*_structure.rs) green/total: 30 / 30 cases: 562 claim-lint: scripts/claim-lint.py -> exit 0 Co-authored-by: Ryan Breen <ryan.breen@gmail.com> Co-authored-by: Claude Code <noreply@anthropic.com>
Merged origin/main (0ac28b2, PR #810) into the branch via a fresh worktree, git merge --no-ff, 0 conflicts; git diff --stat origin/main..HEAD lists exactly the 9 paths this slice touches, so the merge dropped no main content. Re-ran the full gate battery at the merged head: 30/30 structure suites (562 cases), 3 aarch64 builds clean with the no-NEON guard passing all 3, aarch64 strict boot 2/2 and production boot 1/1, both x86 builds clean on beast, and the x86 production boot 1/1 (run twice). 0 reds. Appended the results as section 11 of SLICE3C-2026-09-05.md. claim-lint: scripts/claim-lint.py -> exit 0
ryanbreen
added a commit
that referenced
this pull request
Sep 5, 2026
Both branches independently patched the same latent panic in tests/poll_tcp_gate_wiring_structure.rs (a non-UTF8 scripts/__pycache__/*.pyc reaching scripts_asserting_oracle_fail's read closure): origin/main (#811 by-catch) inlined a read_to_string(...).map(...).unwrap_or(false) guard; this branch (6bf0b64) added a shared script_text() helper used at both read sites. Conflict resolution: scripts_asserting_oracle_fail keeps origin/main's already-landed inline form verbatim. That leaves script_text() with a single remaining caller (missing_ready_lost_wiring), so the helper is removed and its non-panicking read is inlined at that one call site instead, preserving the branch's R157 fix there with the smaller diff against origin/main. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What this is
Slice 3c of the aarch64 testing-profile line: it gives a production thread a
representation for the CPU pin its work needs.
Thread::cpu_affinitywidensfrom a bare
boolidea toOption<CpuPin>,add_thread_innerreads it andbounds it by
online_cpu_count()before falling through toleast_loaded_cpu(), andspawn_on_cpu/kthread_run_on_cpuexist as theCPU-targeted counterparts of
spawn/kthread_run. Content is ported perhunk from the parked branch
origin/fix/562-761-aarch64-testing-profile(not merged) — see
docs/planning/green-program/aarch64-testing/SLICE3C-2026-09-05.mdsection 1 for the per-hunk provenance table.
Related work: #562, #786.
Why it is a ratchet, not a fix
0 of the issues this slice sits among is moved by this branch, and it should
not be read as moving one. The single mutating write to
cpu_affinitylivesinside
spawn_on_cpu, which has 1 call site (kthread_run_on_cpu), whichitself has 0 callers — no boot path, syscall, or gate in this branch reaches
either.
add_thread_innertherefore still reads the empty state on the 15 of15
Thread-construction sites the doc's section 3 census covers, and itsunwrap_or_elsestill callsleast_loaded_cpu(), the identical expressionmainevaluated before thisbranch.
tests/loopback_pump_structure.rsgains 2 census-shaped validatorsand 9 tests that police the placement rule the pin would drive once something
calls
spawn_on_cpu; neither validator pins a line or a closed name list.The doc's section 3 gives the full grep-backed inertness argument and section
9 lists what is deliberately not claimed (the wake filter, the reclaim
custody guard, and 4 representable-but-unresolved dispositions section 10
names individually).
The per-CPU ksoftirqd that will actually call
spawn_on_cpuis slice 3f, notthis branch.
Gates run
Round 1 and round 2 (R157) ran the full battery at the pre-merge head and
again after a first
origin/mainmerge (814bc65 -> 6f36e1a, PR #809) — seethe doc's sections 5-8 for those tables. This PR additionally merges a second
origin/mainmove (6f36e1a -> 0ac28b2, PR #810) and re-runs everything atthat merged head (section 11, "Landing re-smoke"):
tests/*_structure.rs)boot_tests,testing)PASS: 2/2 boots succeededPASS: production profile reached bsshd with the futex oracle seam absenttesting,external_test_bins, zero-feature)0 reds across both the round-2 and landing re-smoke passes, so 0 signatures
required classification against the pre-adjudicated gate set.