Skip to content

The primary's fork checkout, and its nested submodules, are moved to the commit its tree pins - #718

Merged
Japabu merged 11 commits into
mainfrom
wt/toyos-forkpin
Oct 4, 2026
Merged

Japabu merged 11 commits into
mainfrom
wt/toyos-forkpin

Conversation

@Japabu

@Japabu Japabu commented Oct 4, 2026 •

Copy link
Copy Markdown
Collaborator

Before anything builds from it, the primary's fork checkout is moved to the commit its tree pins. Its initialised nested submodules move with it.

Why

The primary built std, and decided whether to bootstrap, from whatever commit its rust/ held. sysroot::fork_checkout returned the primary's rust/ as it stood (Owner::Us => return fork, since 8603559). toolchain::ensure asked compiler::primary_is_current before anything checked the pin. git pull moves the gitlink and leaves the submodule where it was. So after #702, the primary's rust/ at 95960d6c compiled against a toyos-abi that had moved on, and the owner's build of main broke. The licence gate (licence.rs's std_library, through fork_checkout) read the same stale library/.

What changed, per decision

  • The primary moves to the pin whenever its HEAD differs, ahead or behind (src/sysroot.rs fork_checkout). The primary checkout is no workspace, so nothing ahead is kept. A linked worktree still keeps a checkout ahead of its pin, because that is where an agent edits the fork.
  • Every initialised nested submodule moves with the checkout, in the primary and in a linked worktree alike. The move reads every gitlink the new pin records (git ls-tree -r <pin>, 58 ms on the real fork). Each nested submodule that has a .git and sits at another commit is checked out at the recorded one, with git -C <it> checkout --detach. A nested commit the submodule does not hold is refused by name before anything moves, naming git -C <it> fetch origin <commit>. Nothing is fetched.
    • Not git submodule update, which is what x.py runs. In a linked fork checkout, that rewrites core.worktree in the primary's rust/.git/modules/<it>/config to point at the linked checkout, and git in the primary's nested submodule then dies. This was measured in a scratch fixture, and mutation m3 below shows it in the test.
    • The nested submodules move first, and the checkout's own HEAD moves last. A move killed partway through therefore leaves HEAD off the pin, and the next build asks for the move again and finishes it. Done the other way round, a killed move would leave nested submodules behind a HEAD already at the pin. No build re-checks that state, and in a linked worktree, x.py's own sync (update_existing_submodules, run on every x.py) would run git submodule update there.
    • A nested submodule at exactly the commit the pin records there is not uncommitted work. That is the state a move killed after its nested checkouts leaves. In git status --porcelain=v2 --ignore-submodules=none, an entry 1 .M SC.. (the gitlink moved, nothing else) is let through only when the nested HEAD equals the pin's record. A nested HEAD at any other commit is refused, naming that commit and the recorded one: it may be a commit an agent made in the nested submodule that no gitlink records yet, and moving off it would leave it only in that submodule's reflog, which a worktree's removal deletes. That holds whether the commit sits on the checkout's own record (round 2's BLOCKER) or on the pin's (round 3's). A nested HEAD strictly behind the pin's record is refused too (round 3's NOTE): no move leaves that state, and the refusal fails loudly. Edits, untracked files or staged changes inside a nested submodule are refused as well. This check runs after the pin's and the nested commits' held checks, because it reads the commit the pin records.
  • It refuses by name instead of moving when the checkout has local changes (both commits named), or when it does not hold the pinned commit. That refusal names git -C <rust> fetch origin <pin>.
  • The primary's move holds the worktree lock exclusively, then the global lock exclusively (new buildlock::global_exclusive). That is buildlock.rs's declared order (worktree → global). The lock keeps the move out of the primary's own Scope::Global phases in toolchain::ensure: the bootstrap, the hosted rustc and complete. Those build from rust/ with the worktree lock put down (Held::act_if → without_shared). Without the global lock, a second primary process could move the sources under a running x.py. No other worktree's reader is protected: a compiler_shared holder reads only rust/build/, which a checkout does not touch.
  • An uninitialised primary rust/ (a CI runner's, before ensure_shallow_fork) is returned as it is. Git run there would walk up into the superproject. Whatever initialises it later does so at the pin.
  • toolchain::ensure's primary rust_dir is fork_checkout's return. The bootstrap decision therefore cannot run before the move: the ordering is a data dependency, not a comment.
  • compiler::tests::estate gains the x.py that a real primary has, so toolchain::owner reads its primary as Us and not Installed.
  • two_pins (the fixture) now records different library/backtrace commits at C1 and C2. Before, no test reached a nested move.

Round 1 also carried a pin refusal in ensure_shallow_fork, a call to it from release::bootstrap, and an issue file. All three were removed in round 2. The refusal guarded a state toolchain.yml cannot reach, because the step before initialises rust/ at the pin. The issue file was wt/toyos-counterstall's to carry.

Tests (host, src/sysroot.rs)

  • the_primary_s_fork_checkout_is_moved_to_its_pin. Its third case is the state a move killed after its nested checkouts leaves:
    • behind → moved, and the nested submodule is at C2's commit;
    • ahead → moved, and the nested submodule is at C1's commit;
    • only the nested gitlink moved, to the commit the pin records → no refusal, moved;
    • dirty → refused, naming both commits, and not moved;
    • pin not held → refused, naming the fetch command, and not moved;
    • nested commit not held → refused, naming git -C <rust>/library/backtrace fetch origin <commit>, and neither the checkout nor the nested submodule moved;
    • uninitialised → returned, and the superproject's HEAD untouched.
  • a_worktree_pinning_another_fork_commit_gets_its_own_checkout (existing, extended): a linked checkout behind its pin, with its nested submodule behind too, is moved with it. The primary's rust/ git status still runs and is unchanged. Then, with linked/rust at C1 and the tree pinning C2, two nested commits are refused. Each refusal names that commit and C2's record, and neither the nested HEAD nor rust/'s HEAD moves:
    • a commit in linked/rust/library/backtrace on top of C1's record (round 3);
    • a commit there on top of C2's record, the pin's (round 4). This is the common shape: an agent commits in the nested submodule while rust/ is at its pin, then main moves the pin for a change that leaves library/backtrace where it was.
  • twelve_builds_at_once_make_and_move_one_fork_checkout (existing): now that the fixture's nested gitlink differs, its move of a checkout reset to C1 passes through a moved nested gitlink.
  • the_primary_s_fork_checkout_moves_only_while_no_toolchain_phase_builds_from_it: another process holds a Scope::Global phase, as a bootstrap does, with the worktree lock put down. The move queues on the global lock's intent (the test waits on that event with a 20 s deadline) and has not moved. Once the other process lets go, the move lands at the pin.
  • the_primary_s_bootstrap_decides_from_the_pinned_compiler: in the estate fixture, the primary's rust/ is left at a commit whose compiler/ differs. primary_is_current is false there and true once fork_checkout has run.

All of this is git state and flock state on the host, so host tests reach it. No guest test is added.

Checks (build system, concurrency)

Negative control, round 1. The whole production change was reverted onto the base, keeping only the first two tests. cargo test --lib -- sysroot::tests::the_primary_s → EXIT=101. The patch is in the round-1 PR comment.

Mutations. Each was applied as a checked patch, built, run as cargo test --lib -- sysroot::tests, and reverted in the same script, leaving the tree clean.

mutation build exit red tests
m1: a moved nested gitlink counts as work (round 1's check) 0 101 ..._is_moved_to_its_pin, a_worktree_pinning_..., twelve_builds_...
m2: nested submodules not moved 0 101 ..._is_moved_to_its_pin, a_worktree_pinning_...
m3: nested moved by git submodule update --recursive --no-fetch after the top-level checkout 0 101 a_worktree_pinning_... (git in the primary's nested submodule dies: cannot chdir to '…/linked/rust/library/backtrace')
m4: no nested-held check before the move 0 101 ..._is_moved_to_its_pin
m5: no global lock in the primary's move 0 101 ..._moves_only_while_no_toolchain_phase_builds_from_it
m6: any 1 .M SC.. entry is no work (a9cf973d6's filter) 0 101 a_worktree_pinning_... (nothing was refused: () at the nested-commit case, after the move's checked it out line)
m7: the review's swapped arguments, --is-ancestor <record> <nested HEAD> 0 101 a_worktree_pinning_... (nothing was refused: () at round 4's case, after the two earlier refusals)
m8: after the cut, a nested HEAD at or on top of the pin's record passes 0 101 a_worktree_pinning_... (the same case)

The ordering itself (nested first, HEAD last) has no mutation: no host test kills a move between two git commands. A reader can check it at the site.

Independent oracle: none that is not this code. The decisions read git's own status --porcelain=v2, ls-tree, merge-base --is-ancestor (ahead of the pin) and cat-file -e.

Gates

  • cargo test --lib -- sysroot::tests compiler::tests buildlock::tests at bf7c87b0d → EXIT=0
  • cargo run -- --ci host at bf7c87b0d (the head; origin/main is still 1c1c70f77, merged at 85a3f0f14) → EXIT=0. Earlier rounds: at 85a3f0f14, a9cf973d6 and 02ddebbe6 → EXIT=0.
  • Round 1, at 72621e9cb: cargo run -- --build-only → EXIT=0.
  • Round 1, at 72621e9cb: cargo test (guest) → EXIT=1 (virt_el1_smp stalled under host load). Not re-run since; this branch changes no guest byte.

What these gates do not reach. Every gate ran in a linked worktree, where fork_checkout takes the Owner::Elsewhere path. --build-only, --ci host and the guest run exercise none of the changed primary path. That path's only measurements are the git and flock fixtures above. The owner's reproduction is the measurement on a real primary.

The owner's reproduction (to run in the primary before landing)

These are review round 3's steps, with the head and a scratch directory filled in. H is this branch's head as pushed. If the branch moves again, set H to its new head.

Run from the primary checkout, with no build of your own running there:

H=bf7c87b0d36c7ddbb405fa11cec7427f3cb387b4; P=$(git ls-tree $H rust | awk '{print $3}')
S=$PWD/../toyos-forkpin-repro; mkdir -p $S
git fetch origin && test "$P" = "$(git ls-tree origin/main rust | awk '{print $3}')" && echo SAME-PIN
git checkout --detach $H
git -C rust checkout --detach 95960d6c214338e9375b1c7c6c0d4e5ece4c4ba3
git -C rust rev-parse HEAD; git ls-files -s rust
cargo run -- --build-only > $S/repro.log 2>&1; echo EXIT=$? >> $S/repro.log
git -C rust rev-parse HEAD; git status --porcelain --ignore-submodules=none
git checkout main
  • If SAME-PIN does not print, stop. Going back to main would move rust/ as well. At the time of writing, H and origin/main (1c1c70f77) both pin rust at f293615ad7aa162200b40426c159c505c449e182.
  • If the build is not EXIT=0, run git -C rust checkout --detach $P before git checkout main. Otherwise the primary is left at 95960d6c, which is the owner's break.
  • git checkout --detach $H is needed because git checkout wt/toyos-forkpin is refused while that branch is checked out in its worktree.
  • The first git -C rust rev-parse HEAD prints 95960d6c…, and git ls-files -s rust prints f293615ad7….
  • The move holds the global lock exclusively, so it waits on every linked worktree's compiler_shared holder (each one's sysroot build and hosted-rustc read). Its [build-lock] lines name them.

The fix shows as:

  • in $S/repro.log, the line <primary>/rust was at 95960d6c214338e9375b1c7c6c0d4e5ece4c4ba3, and this tree pins f293615ad7aa162200b40426c159c505c449e182: checked it out, ahead of any Building full toolchain or std build line;
  • then EXIT=0 as the log's last line;
  • after it, git -C rust rev-parse HEAD prints f293615ad7…;
  • and git status --porcelain --ignore-submodules=none prints nothing for rust.

Why no bootstrap is expected. This was read read-only (GIT_OPTIONAL_LOCKS=0). The primary's rust/ holds 95960d6c, an ancestor of f293615ad7. The two record the same gitlinks, and they differ in four library/std files and no compiler/ file.

What main does with the same rust/. The steps on main were not run: they would leave the owner's primary broken. Expected: main builds std from 95960d6c's library/ and fails. What was measured is that compile itself. 95960d6c's library/ and f293615ad7's, each with its library/backtrace at f8a3e681, were type-checked against this branch's toyos-abi and toyos, which equal origin/main's (git diff origin/main HEAD -- toyos-abi toyos is empty). The command was cargo +toyos check -Z build-std=std,panic_abort --target x86_64-unknown-toyos --offline on an empty binary, following src/CLAUDE.md's recipe. Results:

  • f293615ad7 → EXIT=0.
  • 95960d6c → EXIT=101: NetError::NetdNotFound not found (sys/net/connection/toyos.rs:26), missing fields path_len and path_ptr in initializer of SpawnArgs (sys/process/toyos.rs:369), launch::Parent::Init not found (:508).

These two pins record the same library/backtrace and src/llvm-project, so this reproduction exercises the top-level move only. The nested move is measured by the fixtures.

Unsure

  • When a commit ahead of the pin sits on no branch, moving the primary off it orphans that commit. The printed line names the old HEAD, so it can be recovered.

Net lines (git diff --shortstat origin/main...HEAD): 4 files changed, 315 insertions(+), 75 deletions(-). Production is +140 −74. Tests are +175 −1.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8

The orchestrator's run in the primary (2026-10-04)

Run in /Users/jan/Dev/jan/toyos with nothing else building there: git checkout --detach bf7c87b0d…, git -C rust checkout --detach 95960d6c214338e9375b1c7c6c0d4e5ece4c4ba3 (the owner's broken state), then cargo run -- --build-only.

before: rust 95960d6c214338e9375b1c7c6c0d4e5ece4c4ba3
160000 f293615ad7aa162200b40426c159c505c449e182 0	rust
11:21:21 /Users/jan/Dev/jan/toyos/rust was at 95960d6c214338e9375b1c7c6c0d4e5ece4c4ba3, and this tree pins f293615ad7aa162200b40426c159c505c449e182: checked it out
EXIT=0
after: rust f293615ad7aa162200b40426c159c505c449e182

EXIT=0; the move came before any toolchain or std build line; afterwards rust/ is at the pin and the tree is clean. The primary was returned with git checkout main (1c1c70f77), rust/ at f293615ad7a, status empty.

Japabu and others added 3 commits October 4, 2026 10:22
The primary built std, and decided whether to bootstrap, from whatever
commit its `rust/` held: `fork_checkout` returned the primary's checkout
as it stood (since 8603559), and `toolchain::ensure` decided the
bootstrap before asking it. `git pull` moves the gitlink and never the
submodule, so after #702 the primary's `rust/` at 95960d6c compiled
against a `toyos-abi` that had moved on, and the owner's build of `main`
broke. The licence gate read the same stale `library/`.

Now the primary's checkout is moved to the pin whenever its `HEAD`
differs, ahead or behind: it is no workspace, so nothing ahead is kept.
It is refused by name when it has local changes, or does not hold the
pinned commit (the refusal names the `git fetch` that fetches it); no
fetch is made for it. The move holds the worktree lock and then the
global lock exclusively, in `buildlock.rs`'s declared order, because
every worktree's compiler and LLVM are built from that checkout. An
uninitialised `rust/` (a runner's) is left to whoever initialises it:
git there would walk up into the superproject. `toolchain::ensure` moves
the checkout before it decides the bootstrap.

Two latent gaps the same defect left in CI: `ensure_shallow_fork`
accepted any `rust/` with an `x.py`, at whatever commit, and
`release::bootstrap` keyed its layers from `rust/` as it stood. The
first now refuses a checkout off the pin, and the second runs it first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
… refused

Two tests the first commit's mutations showed it lacked: removing the
global lock from the primary's move, and accepting a shallow fork at any
commit, each stayed green.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
Seen in this branch's whole-suite run at 72621e9, which changes no
guest byte; the host's load is recorded with it, as root CLAUDE.md asks
of a red seen under load.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation and negative-control patches for the runs the body reports. Each was applied with git apply --check then git apply, run, and reverted with git apply -R in the same script.

negative-control.patch

diff --git a/src/buildlock.rs b/src/buildlock.rs
index 9968ef74a..d4a95e7af 100644
--- a/src/buildlock.rs
+++ b/src/buildlock.rs
@@ -208,13 +208,6 @@ pub fn compiler_shared(root: &Path, what: &str) -> Guard {
     acquire(&git_lock_dir(root), LOCK_SH, what, BUILD)
 }
 
-/// The global lock exclusively: what the primary holds, inside its worktree
-/// lock held exclusively, while it moves its fork checkout, which every
-/// worktree's compiler is built from.
-pub fn global_exclusive(root: &Path, what: &str) -> Guard {
-    acquire(&git_lock_dir(root), LOCK_EX, what, BUILD)
-}
-
 /// Exclusive lock over the shared cargo artifact paths.
 ///
 /// Cargo keys an artifact path on (crate, target, profile) and nothing else, so
diff --git a/src/lib.rs b/src/lib.rs
index 3b94b39f6..83d302956 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -120,18 +120,12 @@ pub fn ensure_submodules(repo_dir: &Path) {
 
 /// `rust/` at the commit this tree pins and with no history, where a CI
 /// runner's checkout has none: what reading the fork's sources and building
-/// the toolchain from them need. One already there at another commit is
-/// refused. Refused in a linked worktree, whose `rust/` is
+/// the toolchain from them need. Refused in a linked worktree, whose `rust/` is
 /// `sysroot::fork_checkout`'s to make: `git submodule` there rewrites the
 /// `core.worktree` of the primary's fork.
 pub fn ensure_shallow_fork(root: &Path) -> Result<(), String> {
-    let fork = root.join("rust");
-    if fork.join("x.py").exists() {
-        let head = sysroot::git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string();
-        let pinned = sysroot::pinned_fork(root);
-        return (head == pinned)
-            .then_some(())
-            .ok_or_else(|| format!("{} is at {head}, and this tree pins the fork at {pinned}", fork.display()));
+    if root.join("rust/x.py").exists() {
+        return Ok(());
     }
     if let toolchain::Owner::Elsewhere(_) = toolchain::owner(root) {
         return Err(format!(
diff --git a/src/release.rs b/src/release.rs
index 1ff7c20e2..2ba36b343 100644
--- a/src/release.rs
+++ b/src/release.rs
@@ -196,7 +196,6 @@ fn outputs(layers: &[Layer]) -> String {
 /// than its build does, and so is one the build left not whole under its key.
 pub fn bootstrap(root: &Path) -> Result<String, String> {
     let file = step_outputs()?;
-    crate::ensure_shallow_fork(root)?;
     let layers = layers(root);
     let restored: Vec<bool> = layers.iter().map(|layer| root.join(&layer.paths[0]).exists()).collect();
     whole(&layers, &restored, |layer| defect(root, layer)).map_err(|why| format!("restored, {why}"))?;
diff --git a/src/sysroot.rs b/src/sysroot.rs
index 720a8289c..59cce0351 100644
--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -19,8 +19,7 @@
 //! it, and a crate built against a sysroot learns which targets' libraries
 //! moved ([`Identity`]). Each build refuses dep-info that says otherwise.
 //!
-//! **Each worktree builds std in its own fork checkout.** The primary builds in its `rust/`,
-//! moved to the commit its tree pins;
+//! **Each worktree builds std in its own fork checkout.** The primary builds in its `rust/`;
 //! a linked worktree in its own `rust/`, made on first need as a git worktree of
 //! the primary's fork repository at the commit this tree pins ([`fork_checkout`]).
 //! `library/std` names `toyos-abi` and `toyos` as `../../../`, so each
@@ -486,7 +485,7 @@ fn key_of(root: &Path, freestanding: &Key, build: &str) -> Key {
 
 /// The commit this checkout's tree pins the std fork at: the index's, so a
 /// staged gitlink counts as the tree's.
-pub(crate) fn pinned_fork(root: &Path) -> String {
+fn pinned_fork(root: &Path) -> String {
     let entry = git_out(root, &["ls-files", "-s", "--", "rust"]);
     let mut words = entry.split_whitespace();
     match (words.next(), words.next()) {
@@ -495,23 +494,21 @@ pub(crate) fn pinned_fork(root: &Path) -> String {
     }
 }
 
-/// The fork checkout `root`'s std is built in, at the commit `root`'s tree pins.
+/// The fork checkout `root`'s std is built in.
 ///
-/// The primary's is its own `rust/`, used where it is not initialised yet, which
-/// whoever initialises it does at the pin. It is no workspace, and every
-/// worktree's compiler and LLVM are built from it, so one whose `HEAD` is not the
-/// pin is moved there, under the global lock held exclusively; refused by name
-/// if it has local changes, or does not hold the pinned commit.
+/// The primary's is its own `rust/`. A linked worktree's `rust/` starts as the
+/// empty stub `git worktree add` leaves; it is made here, the first time it is
+/// needed, as a git worktree of the primary's fork repository at the commit
+/// this tree pins, sharing its objects — and `library/backtrace` the same way
+/// from the primary's, or by git's own clone where the primary does not hold
+/// that commit.
 ///
-/// A linked worktree's `rust/` starts as the empty stub `git worktree add`
-/// leaves; it is made here, the first time it is needed, as a git worktree of
-/// the primary's fork repository at the pin, sharing its objects — and
-/// `library/backtrace` the same way from the primary's, or by git's own clone
-/// where the primary does not hold that commit. It is where an agent edits the
-/// fork, so one ahead of the pin is used as it stands; one neither at the pin
-/// nor ahead of it is moved there, fetching the commit from the primary's
-/// repository first if it does not hold it, and refused by name if it has local
-/// changes rather than moved out from under whoever made them.
+/// A checkout that exists is used as it stands, which is where an agent edits
+/// the fork; one whose `HEAD` is neither the pinned commit nor ahead of it is
+/// moved there itself, fetching the commit from the primary's repository first
+/// if the checkout does not already hold it, unless the checkout has local
+/// changes, in which case it is refused by name rather than moved out from
+/// under whoever made them.
 ///
 /// Every build in a worktree asks this at once, so the making and the move are
 /// each decided and done under the worktree's lock held exclusively
@@ -520,68 +517,63 @@ pub(crate) fn pinned_fork(root: &Path) -> String {
 pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf {
     let fork = root.join("rust");
     let primary = match toolchain::owner(root) {
-        Owner::Us if !fork.join(".git").exists() => return fork,
-        Owner::Us => None,
+        Owner::Us => return fork,
         Owner::Installed => panic!("an installed toolchain has no fork checkout to build std in"),
-        Owner::Elsewhere(primary) => Some(primary),
+        Owner::Elsewhere(primary) => primary,
     };
     let pinned = pinned_fork(root);
-    if let Some(primary) = &primary {
-        lock.act_if(
-            Scope::Worktree,
-            "make the fork checkout",
-            || (!fork.join(".git").exists()).then_some(()),
-            |()| {
-                let stub = fs::read_dir(&fork).map_or(0, |d| d.count());
-                assert!(
-                    stub == 0,
-                    "{} is neither a fork checkout nor the empty stub a worktree starts with",
-                    fork.display()
-                );
-                let _ = fs::remove_dir(&fork);
-                eprintln!("Making {} a fork checkout at {pinned} (a git worktree of the primary's)", fork.display());
-                git_run(&primary.join("rust"), &["worktree", "add", "--detach", path_str(&fork), &pinned]);
-                let backtrace = git_out(&fork, &["ls-tree", "HEAD", "library/backtrace"]);
-                let commit = backtrace.split_whitespace().nth(2).unwrap_or_else(|| {
-                    panic!("{} pins no library/backtrace: {backtrace:?}", fork.display())
-                });
-                let theirs = primary.join("rust/library/backtrace");
-                let held = Command::new("git")
-                    .args(["cat-file", "-e", &format!("{commit}^{{commit}}")])
-                    .current_dir(&theirs)
-                    .status()
-                    .is_ok_and(|s| s.success());
-                let at = fork.join("library/backtrace");
-                if held {
-                    let _ = fs::remove_dir(&at);
-                    git_run(&theirs, &["worktree", "add", "--detach", path_str(&at), commit]);
-                } else {
-                    git_run(&fork, &["submodule", "update", "--init", "library/backtrace"]);
-                }
-            },
-        );
-    }
+    lock.act_if(
+        Scope::Worktree,
+        "make the fork checkout",
+        || (!fork.join(".git").exists()).then_some(()),
+        |()| {
+            let stub = fs::read_dir(&fork).map_or(0, |d| d.count());
+            assert!(
+                stub == 0,
+                "{} is neither a fork checkout nor the empty stub a worktree starts with",
+                fork.display()
+            );
+            let _ = fs::remove_dir(&fork);
+            eprintln!("Making {} a fork checkout at {pinned} (a git worktree of the primary's)", fork.display());
+            git_run(&primary.join("rust"), &["worktree", "add", "--detach", path_str(&fork), &pinned]);
+            let backtrace = git_out(&fork, &["ls-tree", "HEAD", "library/backtrace"]);
+            let commit = backtrace.split_whitespace().nth(2).unwrap_or_else(|| {
+                panic!("{} pins no library/backtrace: {backtrace:?}", fork.display())
+            });
+            let theirs = primary.join("rust/library/backtrace");
+            let held = Command::new("git")
+                .args(["cat-file", "-e", &format!("{commit}^{{commit}}")])
+                .current_dir(&theirs)
+                .status()
+                .is_ok_and(|s| s.success());
+            let at = fork.join("library/backtrace");
+            if held {
+                let _ = fs::remove_dir(&at);
+                git_run(&theirs, &["worktree", "add", "--detach", path_str(&at), commit]);
+            } else {
+                git_run(&fork, &["submodule", "update", "--init", "library/backtrace"]);
+            }
+        },
+    );
     lock.act_if(
         Scope::Worktree,
         "move the fork checkout to its pin",
         || {
             let head = git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string();
-            let ahead = primary.is_some()
-                && Command::new("git")
-                    .args(["merge-base", "--is-ancestor", &pinned, &head])
-                    .current_dir(&fork)
-                    .status()
-                    .is_ok_and(|s| s.success());
+            let ahead = Command::new("git")
+                .args(["merge-base", "--is-ancestor", &pinned, &head])
+                .current_dir(&fork)
+                .status()
+                .is_ok_and(|s| s.success());
             (head != pinned && !ahead).then_some(head)
         },
         |head| {
-            let _global = primary.is_none().then(|| buildlock::global_exclusive(root, "move the fork checkout to its pin"));
             let dirty = git_out(&fork, &["status", "--porcelain", "--ignore-submodules=none"]);
             assert!(
                 dirty.is_empty(),
-                "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \
-                 here would compile a std this tree does not name, and moving the checkout would lose \
-                 that work.\n{dirty}",
+                "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}, which \
+                 that is not ahead of: a build here would compile a std this tree does not name, and \
+                 moving the checkout would lose that work.\n{dirty}",
                 fork.display(),
             );
             let held = Command::new("git")
@@ -590,18 +582,10 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf {
                 .status()
                 .is_ok_and(|s| s.success());
             if !held {
-                match &primary {
-                    Some(primary) => git_run(&fork, &["fetch", path_str(&primary.join("rust")), &pinned]),
-                    None => panic!(
-                        "{} is at {head}, and this tree pins the fork at {pinned}, which it does not hold: \
-                         `git -C {} fetch origin {pinned}` fetches it",
-                        fork.display(),
-                        fork.display(),
-                    ),
-                }
+                git_run(&fork, &["fetch", path_str(&primary.join("rust")), &pinned]);
             }
             git_run(&fork, &["checkout", "--detach", "-q", &pinned]);
-            eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display());
+            eprintln!("{} was at {head}, behind this tree's pin {pinned}: checked it out", fork.display());
         },
     );
     fork
diff --git a/src/toolchain.rs b/src/toolchain.rs
index 736ad0f7d..b7a00883c 100644
--- a/src/toolchain.rs
+++ b/src/toolchain.rs
@@ -560,8 +560,6 @@ pub fn ensure(root: &Path, lock: &mut buildlock::Held, hosted_rustc: bool) -> Sy
         Owner::Us => {}
     }
 
-    // The bootstrap decides from the `compiler/` this tree pins.
-    sysroot::fork_checkout(root, lock);
     let hosted_stamp = stamps_dir.join("hosted-rustc.stamp");
     lock.act_if(
         Scope::Global,

mutation-no-global.patch

diff --git a/src/sysroot.rs b/src/sysroot.rs
index 5cd9cd431..748d03a25 100644
--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -575,7 +575,6 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf {
             (head != pinned && !ahead).then_some(head)
         },
         |head| {
-            let _global = primary.is_none().then(|| buildlock::global_exclusive(root, "move the fork checkout to its pin"));
             let dirty = git_out(&fork, &["status", "--porcelain", "--ignore-submodules=none"]);
             assert!(
                 dirty.is_empty(),

mutation-no-git-guard.patch

diff --git a/src/sysroot.rs b/src/sysroot.rs
index 5cd9cd431..11ff4bf58 100644
--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -520,7 +520,6 @@ pub(crate) fn pinned_fork(root: &Path) -> String {
 pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf {
     let fork = root.join("rust");
     let primary = match toolchain::owner(root) {
-        Owner::Us if !fork.join(".git").exists() => return fork,
         Owner::Us => None,
         Owner::Installed => panic!("an installed toolchain has no fork checkout to build std in"),
         Owner::Elsewhere(primary) => Some(primary),

mutation-primary-keeps-ahead.patch

diff --git a/src/sysroot.rs b/src/sysroot.rs
index 5cd9cd431..15c9e382f 100644
--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -566,8 +566,7 @@ pub fn fork_checkout(root: &Path, lock: &mut Held) -> PathBuf {
         "move the fork checkout to its pin",
         || {
             let head = git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string();
-            let ahead = primary.is_some()
-                && Command::new("git")
+            let ahead = Command::new("git")
                     .args(["merge-base", "--is-ancestor", &pinned, &head])
                     .current_dir(&fork)
                     .status()

mutation-shallow-any-commit.patch

diff --git a/src/lib.rs b/src/lib.rs
index 3b94b39f6..28e5d536e 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -129,7 +129,7 @@ pub fn ensure_shallow_fork(root: &Path) -> Result<(), String> {
     if fork.join("x.py").exists() {
         let head = sysroot::git_out(&fork, &["rev-parse", "HEAD"]).trim().to_string();
         let pinned = sysroot::pinned_fork(root);
-        return (head == pinned)
+        return (head == head)
             .then_some(())
             .ok_or_else(|| format!("{} is at {head}, and this tree pins the fork at {pinned}", fork.display()));
     }

first-commit-tests.patch

(This was applied under mutation-no-global and mutation-shallow-any-commit to run them against the first commit's tests alone. Each exited 0.)

diff --git a/src/buildlock.rs b/src/buildlock.rs
index 5d36317eb..9968ef74a 100644
--- a/src/buildlock.rs
+++ b/src/buildlock.rs
@@ -636,16 +636,6 @@ pub(crate) mod tests {
         Elsewhere::hold("buildlock::tests::child_role", &env)
     }
 
-    /// The primary's compiler of `root`, read by a process of its own.
-    pub(crate) fn compiler_read_elsewhere(root: &Path) -> Elsewhere {
-        held_elsewhere(root, "read-compiler")
-    }
-
-    /// Whether an exclusive acquirer of `root`'s global lock is queued for it.
-    pub(crate) fn global_queued(root: &Path) -> bool {
-        !try_lock(&open_lock_file(&git_lock_dir(root).join("intent")), LOCK_SH)
-    }
-
     /// What `role` of [`child_role`] takes in `root`, held by a process of its own.
     fn held_elsewhere(root: &Path, role: &str) -> Elsewhere {
         Elsewhere::hold("buildlock::tests::child_role", &[(ROLE, OsStr::new(role)), (ROOT, root.as_os_str())])
@@ -784,10 +774,6 @@ pub(crate) mod tests {
                 let _using = keyed_using(&root, Keyed::Sysroot, &Key::parse(&std::env::var(KEY).unwrap()).unwrap());
                 hold_until_released();
             }
-            "read-compiler" => {
-                let _reading = compiler_shared(&root, "child");
-                hold_until_released();
-            }
             "clean" | "clean-unlocked" => {
                 touch(&root.join("cleaner-ready"));
                 assert!(appeared(&root.join("builder-mid"), Duration::from_secs(20)));
diff --git a/src/sysroot.rs b/src/sysroot.rs
index 5cd9cd431..720a8289c 100644
--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -1591,45 +1591,6 @@ mod tests {
         assert_eq!(git(&primary, &["rev-parse", "HEAD"]), superproject, "git ran in the superproject");
     }
 
-    /// **The primary's fork checkout moves only while nobody reads its
-    /// compiler**: a sysroot being made in another worktree holds the global
-    /// lock shared, and the move queues for it and moves nothing until it is
-    /// let go.
-    #[test]
-    fn the_primary_s_fork_checkout_moves_only_while_nobody_reads_its_compiler() {
-        let base = TempDir::new("fork-primary-global");
-        let (primary, _linked, c1, c2) = two_pins(&base);
-        let fork = primary.join("rust");
-        git(&primary, &["update-index", "--cacheinfo", &format!("160000,{c2},rust")]);
-        git(&primary, &["commit", "-qm", "pins C2"]);
-        let reader = buildlock::tests::compiler_read_elsewhere(&primary);
-        std::thread::scope(|scope| {
-            let mover = scope.spawn(|| drop(fork_checkout(&primary, &mut buildlock::shared(&primary, "a build"))));
-            let deadline = std::time::Instant::now() + std::time::Duration::from_secs(20);
-            while !buildlock::tests::global_queued(&primary) {
-                assert!(!mover.is_finished(), "the move took no global lock");
-                assert!(std::time::Instant::now() < deadline, "the move never queued for the global lock");
-                std::thread::sleep(std::time::Duration::from_millis(5));
-            }
-            assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c1, "the checkout moved while a sysroot build read its compiler");
-            reader.release();
-            mover.join().unwrap();
-        });
-        assert_eq!(git(&fork, &["rev-parse", "HEAD"]), c2);
-    }
-
-    /// **A shallow fork is the commit its tree pins**: one already there is
-    /// accepted at the pin and refused, naming both commits, anywhere else.
-    #[test]
-    fn ensure_shallow_fork_refuses_a_fork_off_its_pin() {
-        let base = TempDir::new("fork-shallow-pin");
-        let (primary, _linked, c1, c2) = two_pins(&base);
-        assert_eq!(crate::ensure_shallow_fork(&primary), Ok(()));
-        git(&primary.join("rust"), &["checkout", "-q", &c2]);
-        let refused = crate::ensure_shallow_fork(&primary);
-        assert!(refused.as_ref().is_err_and(|why| why.contains(&format!("is at {c2}")) && why.contains(&c1)), "{refused:?}");
-    }
-
     /// **The primary's bootstrap decides from the `compiler/` its tree pins**:
     /// a fork checkout left at a commit naming another `compiler/` reads as a
     /// stale compiler, and once it is moved to the pin the compiler the primary

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 1, head a34c2b7d3. Net: 7 files, +248 −63. The body counts production at +95 −63, tests +120 and the issue +24.

BLOCKER

  • src/lib.rs:129-135, src/release.rs:199, src/sysroot.rs:1621-1631 — "latent CI gaps" 1 and 2 guard a state that cannot be reached. toolchain.yml runs --ci toolchain (ensure_shallow_fork, from an empty rust/, depth 1 at the pin) and then --ci bootstrap in the same job. Between them, only actions/cache/restore of rust/build/... paths runs, and that cannot move rust/'s HEAD. licence::std_library calls it only when library/Cargo.toml is absent. step_outputs refuses both release entry points on a dev host. Per Growth, "a new check … that guards what a reader can check" goes back. Delete the refusal, the release::bootstrap line, the pinned_fork widening to pub(crate) if nothing else needs it (lib.rs:131 is its only new caller), and ensure_shallow_fork_refuses_a_fork_off_its_pin. The body's claim that release::bootstrap "keyed its layers from rust/ as it stood" also goes: the step before it established the pin.
  • src/sysroot.rs:578-604 — this compromise is named in the body's "Unsure" and recorded nowhere. git checkout --detach leaves nested submodules (src/llvm-project, library/backtrace, src/tools/cargo, the books) at the old pin's commits. Only an x.py run (update_existing_submodules) syncs them, and the primary often runs none after a pull: a sysroot or compiler key a linked worktree already built is reused. The next pin move is then refused by the --ignore-submodules=none check, and that refusal says "with uncommitted work … moving the checkout would lose that work". That is false: a nested gitlink at another commit is no uncommitted work. Remove it (move the initialised nested submodules with the checkout, or make the dirty check tell a moved gitlink from work in it), or record it in issues/build/ with an owner, the evidence and an exit. Either way, add a fixture pin pair in two_pins whose library/backtrace differs between C1 and C2: today none does, so no test reaches this.
  • issues/kernel/virt-el1-smp-stalled-on-counters-read-under-load.md — this file is false of the record and outside the brief. status: open says nobody holds the defect, but wt/toyos-counterstall holds it (762a524f0 "Counters: only the answer that completes a round wakes its readers", and bc5f36c7b on the harness's second job wait). That branch does not delete this file, so after its fix lands, an open defect would stay on main. Its exit also asks for a capture to diagnose a cause that branch has already named. Delete it in a new commit on this branch. If the stall needs a record, it is counterstall's to carry.

NOTE

  • src/sysroot.rs:578, src/sysroot.rs:1595-1617, src/buildlock.rs:211-213 — the stated reason for the global lock names a reader the move cannot disturb. A compiler_shared holder elsewhere reads rust/build/<host>/stage2 and rust/build/toyos-compiler, and git checkout in rust/ touches neither. The reader the lock does protect is the primary's own Scope::Global phases in toolchain::ensure (bootstrap, hosted rustc, complete). Those drop the worktree lock (Held::act_if → without_shared), so without the global lock, a second primary process could move the sources under a running x.py. Make the test hold the global lock exclusively, as a bootstrap does, and state that reason in the body.
  • src/toolchain.rs:563-564 — the ordering is kept by a comment and by nothing that can fail. Deleting the call turns no test red, and the defect it then lets through (the bootstrap judging the old compiler/ current, then std built from the new library/ with the old compiler) is the one this PR fixes. Make the order a data dependency: on the Owner::Us path, take rust_dir from sysroot::fork_checkout's return. Then the comment goes.
  • PR body — every gate was run in a linked worktree, where fork_checkout's Owner::Elsewhere path is behaviourally unchanged. --build-only, --ci host and the guest run therefore exercise none of the changed primary path, and its only measurements are the git and flock fixtures. The body should say this. The red guest run is evidence of nothing here, and it does not block. The owner's reproduction (the primary at a pin behind main, after git pull) is the orchestrator's to run before landing.

REMOVE

  • src/sysroot.rs:502-503 — "and every worktree's compiler and LLVM are built from it". This is false: a linked worktree whose compiler/ differs builds its compiler and LLVM from its own fork checkout.
  • src/buildlock.rs:212-213 — "which every worktree's compiler is built from". It is false for the same reason.
  • src/lib.rs:123-124 — "One already there at another commit is refused." It goes with the first BLOCKER.

SEND BACK

… and the unreachable CI refusals go

- The move checks out every initialised nested submodule at the commit the
  new pin records, with `git checkout` in it and never `git submodule`, which
  run in a linked fork checkout rewrites the primary's nested `core.worktree`.
  It refuses by name, before moving anything, a nested commit not held. A
  nested gitlink moved and nothing more is no longer read as uncommitted work.
- `two_pins`' C1 and C2 now record different `library/backtrace` commits, so
  the primary and linked tests reach the nested move.
- The global-lock test holds a `Scope::Global` phase in another process, as a
  bootstrap does; that is what the lock keeps the move out of.
- `toolchain::ensure` takes the primary's `rust_dir` from `fork_checkout`.
- `ensure_shallow_fork`'s pin refusal, `release::bootstrap`'s call to it and
  their test go: `toolchain.yml` initialises `rust/` at the pin in the step
  before, and nothing between moves it.
- The virt_el1_smp stall issue goes: wt/toyos-counterstall carries it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 mutation patches, at dd27e9f32. Each was applied with git apply --check then git apply, built (cargo build --lib --tests, EXIT=0 each), run as cargo test --lib -- sysroot::tests, and reverted with git apply -R in the same script (run.sh below). The tree was clean after each.

m1-moved-gitlink-is-work build EXIT=0
m1-moved-gitlink-is-work test EXIT=101
m1-moved-gitlink-is-work tree after restore: []
m2-nested-not-moved build EXIT=0
m2-nested-not-moved test EXIT=101
m2-nested-not-moved tree after restore: []
m3-nested-by-git-submodule build EXIT=0
m3-nested-by-git-submodule test EXIT=101
m3-nested-by-git-submodule tree after restore: []
m4-no-nested-held-precheck build EXIT=0
m4-no-nested-held-precheck test EXIT=101
m4-no-nested-held-precheck tree after restore: []
$SCRATCH/mut/m5-no-global-lock.patch:8: trailing whitespace.
            
warning: 1 line adds whitespace errors.
m5-no-global-lock build EXIT=0
m5-no-global-lock test EXIT=101
m5-no-global-lock tree after restore: []
DONE

m1-moved-gitlink-is-work.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -577,7 +577,7 @@
             let _global = primary.is_none().then(|| buildlock::global_exclusive(root, "move the fork checkout to its pin"));
             // A nested gitlink checked out at another commit, and nothing more, is no work: it moves with the checkout.
             let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]);
-            let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect();
+            let work: Vec<&str> = status.lines().collect();
             assert!(
                 work.is_empty(),
                 "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \

m2-nested-not-moved.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -618,9 +618,6 @@
                 );
             }
             git_run(&fork, &["checkout", "--detach", "-q", &pinned]);
-            for (at, commit) in &nested {
-                git_run(at, &["checkout", "--detach", "-q", commit]);
-            }
             eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display());
         },
     );

m3-nested-by-git-submodule.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -618,9 +618,7 @@
                 );
             }
             git_run(&fork, &["checkout", "--detach", "-q", &pinned]);
-            for (at, commit) in &nested {
-                git_run(at, &["checkout", "--detach", "-q", commit]);
-            }
+            git_run(&fork, &["submodule", "update", "--recursive", "--no-fetch"]);
             eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display());
         },
     );

m4-no-nested-held-precheck.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -608,7 +608,7 @@
                 })
                 .filter(|(at, commit)| at.join(".git").exists() && git_out(at, &["rev-parse", "HEAD"]).trim() != commit)
                 .collect();
-            for (at, commit) in &nested {
+            for (at, commit) in &nested[..0] {
                 assert!(
                     holds(at, commit),
                     "{} does not hold {commit}, which the fork's {pinned} records there: \

m5-no-global-lock.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -574,7 +574,7 @@
             (head != pinned && !ahead).then_some(head)
         },
         |head| {
-            let _global = primary.is_none().then(|| buildlock::global_exclusive(root, "move the fork checkout to its pin"));
+            
             // A nested gitlink checked out at another commit, and nothing more, is no work: it moves with the checkout.
             let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]);
             let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect();

run.sh

#!/bin/sh
D=$SCRATCH
W=$WORKTREE
cd $W || exit 1
for p in $D/mut/*.patch; do
  n=$(basename $p .patch)
  git apply --check -p1 $p || { echo "$n: patch does not apply"; exit 1; }
  git apply -p1 $p
  cargo build --lib --tests > $D/mut/$n.build.log 2>&1; echo "$n build EXIT=$?"
  cargo test --lib -- sysroot::tests > $D/mut/$n.log 2>&1; echo "$n test EXIT=$?"
  git apply -R -p1 $p
  echo "$n tree after restore: [$(git status --porcelain --ignore-submodules=none)]"
done
echo DONE

A move killed between the nested checkouts and the top-level one then leaves
`HEAD` off the pin, so the next build asks for the move again and finishes
it. Moved the other way round, a killed move left nested submodules behind a
`HEAD` already at the pin, which no build re-checks. In a linked worktree,
x.py's own sync of those submodules would then run `git submodule update`
there, which rewrites the primary's nested `core.worktree`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Round 2 mutation patches, at a9cf973d6. Each was applied with git apply --check then git apply, built (cargo build --lib --tests), run as cargo test --lib -- sysroot::tests, and reverted with git apply -R in the same script (run.sh below). The tree was clean after each. m3 was regenerated after the first pass, to run git submodule after the top-level checkout as x.py would, and was re-run alone through the same script.

m1-moved-gitlink-is-work build EXIT=0
m1-moved-gitlink-is-work test EXIT=101
m1-moved-gitlink-is-work tree after restore: []
m2-nested-not-moved build EXIT=0
m2-nested-not-moved test EXIT=101
m2-nested-not-moved tree after restore: []
m4-no-nested-held-precheck build EXIT=0
m4-no-nested-held-precheck test EXIT=101
m4-no-nested-held-precheck tree after restore: []
m5-no-global-lock build EXIT=0
m5-no-global-lock test EXIT=101
m5-no-global-lock tree after restore: []
m3-nested-by-git-submodule build EXIT=0
m3-nested-by-git-submodule test EXIT=101
m3-nested-by-git-submodule tree after restore: []

m1-moved-gitlink-is-work.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -577,7 +577,7 @@
             let _global = primary.is_none().then(|| buildlock::global_exclusive(root, "move the fork checkout to its pin"));
             // A nested gitlink checked out at another commit, and nothing more, is no work: it moves with the checkout.
             let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]);
-            let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect();
+            let work: Vec<&str> = status.lines().collect();
             assert!(
                 work.is_empty(),
                 "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \

m2-nested-not-moved.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -617,9 +617,6 @@
                     at.display(),
                 );
             }
-            for (at, commit) in &nested {
-                git_run(at, &["checkout", "--detach", "-q", commit]);
-            }
             // Last, so a move killed before it is asked for again: `HEAD` alone decides.
             git_run(&fork, &["checkout", "--detach", "-q", &pinned]);
             eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display());

m3-nested-by-git-submodule.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -616,12 +616,10 @@
                     at.display(),
                     at.display(),
                 );
-            }
-            for (at, commit) in &nested {
-                git_run(at, &["checkout", "--detach", "-q", commit]);
             }
             // Last, so a move killed before it is asked for again: `HEAD` alone decides.
             git_run(&fork, &["checkout", "--detach", "-q", &pinned]);
+            git_run(&fork, &["submodule", "update", "--recursive", "--no-fetch"]);
             eprintln!("{} was at {head}, and this tree pins {pinned}: checked it out", fork.display());
         },
     );

m4-no-nested-held-precheck.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -608,7 +608,7 @@
                 })
                 .filter(|(at, commit)| at.join(".git").exists() && git_out(at, &["rev-parse", "HEAD"]).trim() != commit)
                 .collect();
-            for (at, commit) in &nested {
+            for (at, commit) in &nested[..0] {
                 assert!(
                     holds(at, commit),
                     "{} does not hold {commit}, which the fork's {pinned} records there: \

m5-no-global-lock.patch

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -574,7 +574,6 @@
             (head != pinned && !ahead).then_some(head)
         },
         |head| {
-            let _global = primary.is_none().then(|| buildlock::global_exclusive(root, "move the fork checkout to its pin"));
             // A nested gitlink checked out at another commit, and nothing more, is no work: it moves with the checkout.
             let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]);
             let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect();

run.sh

#!/bin/sh
D=$SCRATCH
W=$WORKTREE
cd $W || exit 1
for p in $D/mut/*.patch; do
  n=$(basename $p .patch)
  git apply --check -p1 $p || { echo "$n: patch does not apply"; exit 1; }
  git apply -p1 $p
  cargo build --lib --tests > $D/mut/$n.build.log 2>&1; echo "$n build EXIT=$?"
  cargo test --lib -- sysroot::tests > $D/mut/$n.log 2>&1; echo "$n test EXIT=$?"
  git apply -R -p1 $p
  echo "$n tree after restore: [$(git status --porcelain --ignore-submodules=none)]"
done
echo DONE

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu Japabu changed the title The primary's fork checkout is moved to the commit its tree pins The primary's fork checkout, and its nested submodules, are moved to the commit its tree pins Oct 4, 2026
@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 2, head 02ddebbe6. Net: 4 files, +273 −75 (the body counts production at +121 −74 and tests at +152 −1). The merge of 8b4f88446 touches none of the four files, so the mutations measured at a9cf973d6 stand at this head.

Round 1 BLOCKERs

  • CLOSED: the unreachable CI refusals. src/lib.rs and src/release.rs are out of the diff, along with pinned_fork's widening and ensure_shallow_fork_refuses_a_fork_off_its_pin. cargo run -- --ci host at 02ddebbe6 exits 0 (ci-host3.exit), and its log shows every new and changed sysroot test ok.
  • CLOSED: nested submodules left behind by the move. They now move with the checkout, and two_pins records different library/backtrace commits at C1 and C2. m1, m2 and m4 each exit 101, and m3 as regenerated exits 101 (only/m3-nested-by-git-submodule.log: cannot chdir to '…/linked/rust/library/backtrace'). A new defect in the filter this fix brought in is below.
  • CLOSED: issues/kernel/virt-el1-smp-stalled-on-counters-read-under-load.md is out of the diff.

The round 1 NOTEs are answered: the global-lock test now holds a Scope::Global phase, and m5 exits 101; rust_dir is fork_checkout's return; the body says which path the gates do not reach. The round 1 REMOVEs are gone.

BLOCKER

  • src/sysroot.rs:579-580 — the move now moves a nested commit that nothing records, and says nothing about it. 1 .M SC.. is let through for any nested HEAD other than the gitlink, including a commit an agent made inside the nested submodule. Lines 620-622 then check that submodule out at the pin's commit. The old commit is left reachable only from that submodule's HEAD reflog, and the eprintln! names only the top-level HEAD. This happens in a linked worktree, which the doc at :510-514 calls where an agent edits the fork. Take an agent who commits in linked/rust/library/backtrace, has not yet committed the gitlink in rust/, and merges an origin/main that moves the pin: rust/'s HEAD is now behind its pin, and the next build detaches that commit. Round 1's check refused this case, and orchestrator.md's worktree removal deletes the reflog. That is data loss, and it contradicts the body's "Edits … inside a nested submodule are still refused". Fix: let an SC.. entry through only when the nested HEAD is the commit the pin records there, or an ancestor of it (git -C <at> merge-base --is-ancestor <nested HEAD> <recorded>). Refuse any other, naming both commits. Case 3 of the_primary_s_fork_checkout_is_moved_to_its_pin still passes under that rule. Test: in a_worktree_pinning_another_fork_commit_gets_its_own_checkout, with rust/ at C1 and the tree pinning C2, commit on top of C1's record in linked/rust/library/backtrace. Assert fork_checkout is refused, and that the nested HEAD and rust/'s HEAD are both unchanged. Then run it against a9cf973d6's filter, where it has to be red.

NOTE

  • PR body, "The owner's reproduction": "with this branch checked out in the primary" cannot be done as git checkout wt/toyos-forkpin. Git refuses it, because the branch is checked out at /Users/jan/Dev/jan/toyos-forkpin. Name the step exactly: git -C <primary> checkout --detach <head>, and afterwards git -C <primary> checkout main. Both 02ddebbe6 and main pin 724238524f…, so going back moves nothing. If the head or main's pin changes before the run, check that again.
  • PR body, same section: say how to recover if step 3 does not end at EXIT=0: git -C <primary>/rust checkout --detach <pin>. Otherwise the primary is left at 95960d6c, which is the owner's break. Also say that step 3's move takes the global lock exclusively, so it waits for every linked worktree's sysroot build that holds compiler_shared.
  • PR body, "On main, the same three steps … fail against the current toyos-abi": no command run shows this. Measure it, or mark it as expected.
  • Checked, so the orchestrator need not repeat it: step 1 is safe in the primary as it stands. In rust/, git status --porcelain=v2 --ignore-submodules=none prints nothing (run read-only with GIT_OPTIONAL_LOCKS=0). It holds 95960d6c, which is an ancestor of 724238524f. The two commits record the same 11 gitlinks, and only library/backtrace and src/llvm-project are initialised. They differ in four library/std files and no compiler/ file, so step 3 starts no bootstrap.

REMOVE

  • src/sysroot.rs:517-518 — "and the next move would read it as work": the filter at :580 makes this false.

SEND BACK

Japabu and others added 2 commits October 4, 2026 12:48
A nested submodule's gitlink at another commit was let through as no
work whatever that commit was, so a commit an agent made inside
linked/rust/library/backtrace, not yet recorded by a gitlink, was
detached by the next move and left reachable only from that
submodule's reflog. An `SC..` entry now goes through only where the
nested HEAD is the commit the pin records there or an ancestor of it
(`git merge-base --is-ancestor`); any other is refused, naming both.
The check moves after the pin's and the nested commits' held checks,
since it reads the commit the pin records.

The doc's "the next move would read it as work" goes: the filter made
it false.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Mutation m6, round 3, at 4546e0cd7: a9cf973d6's filter (any 1 .M SC.. entry is no work) put back in place of the ancestor-of-recorded check. Applied with git apply --check then git apply, built (cargo test --lib --no-run BUILD_EXIT=0), run as cargo test --lib -- sysroot::tests → TEST_EXIT=101, red test a_worktree_pinning_another_fork_commit_gets_its_own_checkout (nothing was refused: () at the new nested-commit case, after linked/rust was at <C1> … checked it out), reverted with git apply -R; git status --porcelain --ignore-submodules=none printed nothing after.

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -613,21 +613,7 @@
             // A nested submodule checked out at the commit the pin records there, or behind it, and nothing more, is
             // no work: it moves with the checkout. One at any other commit holds a commit nothing else records.
             let status = git_out(&fork, &["status", "--porcelain=v2", "--ignore-submodules=none"]);
-            let work: Vec<String> = status
-                .lines()
-                .filter_map(|entry| {
-                    let path = entry.strip_prefix("1 .M SC.. ").and_then(|rest| rest.splitn(6, ' ').nth(5));
-                    let Some((path, commit)) = path.and_then(|path| recorded.iter().find(|(p, _)| p == path)) else {
-                        return Some(entry.to_string());
-                    };
-                    let at = fork.join(path);
-                    let at_head = git_out(&at, &["rev-parse", "HEAD"]).trim().to_string();
-                    let behind = git_try(&at, &["merge-base", "--is-ancestor", &at_head, commit]).is_ok();
-                    (!behind).then(|| {
-                        format!("{} is at {at_head}, which is neither {commit}, what {pinned} records there, nor behind it", at.display())
-                    })
-                })
-                .collect();
+            let work: Vec<&str> = status.lines().filter(|entry| !entry.starts_with("1 .M SC.. ")).collect();
             assert!(
                 work.is_empty(),
                 "{} is at {head} with uncommitted work, and this tree pins the fork at {pinned}: a build \

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 3, head 85a3f0f14. Net: 4 files, +305 −75 (git diff --shortstat origin/main...85a3f0f14). The body counts production at +140 −74 and tests at +165 −1. Since 02ddebbe6, the branch changed only src/sysroot.rs. The two merges bring in main's src/build.rs and src/licence.rs, which are not this branch's.

Round 2 BLOCKER

  • CLOSED: a nested commit that nothing records was moved off silently. sysroot.rs:613-638 now lets a 1 .M SC.. entry through only when merge-base --is-ancestor <nested HEAD> <pin's record> holds. m6 (a9cf973d6's filter put back, at 4546e0cd7) exits 101. Its log (forkpin-r3/m6.log) shows nothing was refused: () coming from the new nested-commit case in a_worktree_pinning_..., after the uncommitted-work case refused as it should. cargo run -- --ci host at 85a3f0f14 → EXIT=0 (ci-host.exit, ci-host.head = 85a3f0f14…), with all six sysroot fork tests ok (log lines 534-557). Round 2's NOTEs on the reproduction are answered. Main's failure is now a measurement: stdcheck-old → 101, with the three errors the body quotes, and stdcheck-new → 0.

Reproduction (checked read-only at this head, GIT_OPTIONAL_LOCKS=0)

The steps still start from a state that can be reproduced:

  • origin/main = 1c1c70f77, and the primary is on main at it.
  • git ls-tree 85a3f0f14 rust = git ls-tree origin/main rust = git ls-files -s rust = f293615ad7….
  • rust/ HEAD is f293615ad7…, and git -C rust status --porcelain=v2 --ignore-submodules=none and git status --porcelain --ignore-submodules=none both print nothing.
  • rust/ holds 95960d6c…, which is an ancestor of f293615ad7….
  • Both commits record the same gitlinks, and they differ in four library/std files only.
  • 724238524f..f293615ad7 touches no compiler, src/tools, src/stage0 or Cargo.lock. No commit since d47b383cf touches src/compiler.rs, src/clang.rs or src/llvm.rs. rust/build/toyos-compiler was written today, so the "no bootstrap" claim stands as far as reading can tell.
  • src/main.rs:205's ensure_submodules initialises only empty submodules. Nothing therefore moves rust/ before fork_checkout, and the run exercises the fix.

Run it on the head this branch lands at, not on 85a3f0f14, and re-check the pins first. From /Users/jan/Dev/jan/toyos, with no build of your own running there:

H=<final head>; P=$(git ls-tree $H rust | awk '{print $3}')
git fetch origin && test "$P" = "$(git ls-tree origin/main rust | awk '{print $3}')" && echo SAME-PIN
git checkout --detach $H
git -C rust checkout --detach 95960d6c214338e9375b1c7c6c0d4e5ece4c4ba3
git -C rust rev-parse HEAD; git ls-files -s rust
cargo run -- --build-only > <scratch>/repro.log 2>&1; echo EXIT=$? >> <scratch>/repro.log
git -C rust rev-parse HEAD; git status --porcelain --ignore-submodules=none
git checkout main
  • If SAME-PIN does not print, stop: going back to main would move rust/ as well.
  • If the build is not EXIT=0, run git -C rust checkout --detach $P before git checkout main.
  • In repro.log, look for the line /Users/jan/Dev/jan/toyos/rust was at 95960d6c…, and this tree pins $P: checked it out, ahead of any toolchain or std build line.
  • The move waits on every linked worktree's compiler_shared holder; its [build-lock] lines name them.

BLOCKER

  • src/sysroot.rs:625 — no test fails if the arguments to --is-ancestor are swapped, and the swap lets through exactly the data loss round 2 closed. Mutation: -["merge-base", "--is-ancestor", &at_head, commit] / +["merge-base", "--is-ancestor", commit, &at_head].
    • Every case still passes under it. In case 3 and twelve_builds_..., the nested HEAD equals the recorded commit, so the check holds either way. The new case commits on top of C1's record b1, which has diverged from C2's b2, so it is refused either way.
    • What the swapped check then accepts is a nested commit on top of the pin's record. That is the common shape of the round 2 case. An agent commits in linked/rust/library/backtrace while rust/ is at its pin. Main then moves the rust pin for a std change that leaves library/backtrace where it was. rust/ is now behind, the pin's record is the agent's parent, and the agent's commit is detached.
    • Add a case to a_worktree_pinning_another_fork_commit_gets_its_own_checkout: rust/ at C1, nested checked out at C2:library/backtrace, a commit on top of it, then assert that fork_checkout is refused, naming that commit and C2's record, and that neither HEAD moves.
    • Run the patch above against it. It must exit 101 with that case red, and the result goes in the body's mutation table.

NOTE

  • src/sysroot.rs:625 — the strictly-behind arm (a nested HEAD that is an ancestor of the pin's record, and neither that record nor the current HEAD's) is reached by no test: behind = at_head == *commit passes every case. Wrongly it would cost only a refusal, which fails loudly, so this is not a BLOCKER. Either add a case that reaches the arm, or cut the arm to equality, which also drops a git call per entry. The body describes whichever the code does.

SEND BACK

Japabu and others added 2 commits October 4, 2026 13:08
Review round 3's BLOCKER: swapping the arguments to `merge-base
--is-ancestor` passed every case. The new case commits in the linked
checkout's nested submodule on top of what C2 records there, with `rust/`
at C1, and asserts the refusal names that commit and the record and that
neither HEAD moves.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
Review round 3's NOTE: the arm that let through a nested HEAD strictly
behind the pin's record was reached by no case. The only state the move
itself leaves there is a nested submodule at the record, after a move
killed before its last checkout; anything else is refused by name, which
fails loudly. Equality also drops a `merge-base` call per entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WcU2Dsw6mDYtwYfzVHPzM8
@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Round 4 mutations. Each applied with /Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/forkpin-r4/mutate.sh <name> <patch> (below): git apply --check, apply, cargo test --lib --no-run, cargo test --lib -- sysroot::tests, git apply -R, then git status --porcelain --ignore-submodules=none empty.

m7 at bb5eeceab (the new case, before the arm was cut): the review's swapped --is-ancestor arguments. BUILD=0, EXIT=101, red: a_worktree_pinning_another_fork_commit_gets_its_own_checkout (nothing was refused: () at the new case, after the two earlier refusals).

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -622,7 +622,7 @@
                     };
                     let at = fork.join(path);
                     let at_head = git_out(&at, &["rev-parse", "HEAD"]).trim().to_string();
-                    let behind = git_try(&at, &["merge-base", "--is-ancestor", &at_head, commit]).is_ok();
+                    let behind = git_try(&at, &["merge-base", "--is-ancestor", commit, &at_head]).is_ok();
                     (!behind).then(|| {
                         format!("{} is at {at_head}, which is neither {commit}, what {pinned} records there, nor behind it", at.display())
                     })

m8 at bf7c87b0d (the arm cut to equality): a nested HEAD at or on top of the pin's record passes. BUILD=0, EXIT=101, red: the same test, at the same case.

--- a/src/sysroot.rs
+++ b/src/sysroot.rs
@@ -623,5 +623,5 @@
                     let at = fork.join(path);
                     let at_head = git_out(&at, &["rev-parse", "HEAD"]).trim().to_string();
-                    (at_head != *commit).then(|| {
+                    git_try(&at, &["merge-base", "--is-ancestor", commit, &at_head]).is_err().then(|| {
                         format!("{} is at {at_head}, not at {commit}, what {pinned} records there", at.display())
                     })

Script:

#!/bin/sh
# usage: mutate.sh <name> <patch>; applies, builds, runs sysroot tests, restores.
set -u
D=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/forkpin-r4; W=/Users/jan/Dev/jan/toyos-forkpin
cd $W || exit 2
git apply --check "$2" || exit 3
git apply "$2" || exit 3
git rev-parse HEAD > $D/$1.head
cargo test --lib --no-run > $D/$1.build.log 2>&1; echo BUILD=$? > $D/$1.exit
cargo test --lib -- sysroot::tests > $D/$1.log 2>&1; echo EXIT=$? >> $D/$1.exit
git apply -R "$2" || exit 4
git status --porcelain --ignore-submodules=none > $D/$1.status-after

@Japabu

Japabu commented Oct 4, 2026

Copy link
Copy Markdown
Collaborator Author

Review round 4, head bf7c87b0d. Net lines: 4 files, +315 −75 (git diff --shortstat origin/main...bf7c87b0d). The body counts production at +140 −74 and tests at +175 −1. Since round 3's 85a3f0f14, only src/sysroot.rs changed, +16 −6: one test case, and the nested arm cut to equality. origin/main is still 1c1c70f77.

Round 3 BLOCKER

  • CLOSED: sysroot.rs:625, the swapped --is-ancestor arguments.
    • The new case is at sysroot.rs:1571-1579: a commit on top of C2's record in linked/rust/library/backtrace, with rust/ at C1. It asserts the refusal names that commit and the record, and that neither HEAD moves.
    • m7 is the review's exact patch, at bb5eeceab: BUILD=0, EXIT=101 (forkpin-r4/m7.exit). In m7.log, a_worktree_pinning_... is red with nothing was refused: () at the third refusal, after the two earlier refusals fire.
    • m8 is the acceptance put back over the cut arm, at bf7c87b0d: BUILD=0, EXIT=101. The same case is red at sysroot.rs:1768 (refusal), after the uncommitted-work refusal and the 26cb665…, not at 393a748… refusal print (m8.log:41-66).
    • Both status-after files are empty.

Round 3 NOTE

  • CLOSED: the strictly-behind arm. It is cut to at_head != *commit (sysroot.rs:626), so every state but the pin's record is refused loudly. The doc comment (:108-111), the site comment (:614-615) and the body describe the equality.

Gates at this head

  • cargo run -- --ci host at bf7c87b0d → EXIT=0 (ci-host.exit, ci-host.head = bf7c87b0d…). Its log ends Host: 73 step(s), all green, and all six sysroot fork tests are ok (log lines 524-549). The FAILED lines in that log are the loom and model controls' expected reds.
  • cargo test --lib -- sysroot::tests compiler::tests buildlock::tests → EXIT=0 (unit.exit).

Reproduction, checked read-only in the primary (GIT_OPTIONAL_LOCKS=0)

  • The primary is on main at 1c1c70f77 = origin/main.
  • git ls-tree bf7c87b0d rust = git ls-tree origin/main rust = git ls-files -s rust = f293615ad7…, and rust/ HEAD is f293615ad7….
  • Both git -C rust status --porcelain=v2 --ignore-submodules=none and git status --porcelain --ignore-submodules=none print nothing.
  • bf7c87b0d resolves in the primary before any fetch, so the shared object store lets P be computed on the first line.
  • rust/ holds 95960d6c…, which is an ancestor of f293615ad7…. Their gitlinks are identical (12 each). The diff between them is four library/std files.
  • submodule.recurse is unset in the primary and in rust/. git checkout --detach $H and git checkout main therefore leave rust/ where it is, and the recovery works on either side of git checkout main.
  • $PWD/../toyos-forkpin-repro resolves to /Users/jan/Dev/jan/toyos-forkpin-repro, which does not exist.
  • No step runs git submodule or git clone, or touches main's ref.
  • The expected line matches sysroot.rs:288's format. fork_checkout is reached through toolchain::ensure (toolchain.rs:559) from build.rs:1790's shipped_parts, before Building full toolchain (toolchain.rs:567).

The steps are right in paths, the pin check, the expected output and the recovery, with the two hazards under NOTE.

BLOCKER

None.

NOTE

  • PR body, reproduction block — pasted whole, the block does not stop when SAME-PIN fails to print. It also runs git checkout main before anyone reads EXIT=, which contradicts "run git -C rust checkout --detach $P before git checkout main". Split it in three:
    • the H/P/S lines and the pin check;
    • from git checkout --detach $H through the second rev-parse/status;
    • git checkout main.
      Change the recovery's "before" to "after the build and before or after git checkout main (submodule.recurse is unset)", or leave it as is once the block is split.
  • PR body, S=$PWD/../toyos-forkpin-repro — this puts a toyos-* directory beside the worktrees, and no step removes it. Use the orchestrator's scratch instead, S=/Users/jan/.claude/jobs/2280e09e/tmp/scratchpad/orch/718-repro. End with rm -r $S once the log is quoted into the body.
  • PR body, "The owner's reproduction" — the primary path is measured only by git and flock fixtures (stated under "What these gates do not reach"). Before landing, record the run in the body: the command, EXIT=, the checked it out line, and the two post-run reads. If any of them is not as the body predicts, this branch goes back.

REMOVE

None.

LAND AFTER NAMED CHANGES

@Japabu
Japabu marked this pull request as ready for review October 4, 2026 11:25
@Japabu
Japabu enabled auto-merge October 4, 2026 11:25
@Japabu
Japabu added this pull request to the merge queue Oct 4, 2026
Merged via the queue into main with commit ce50411 Oct 4, 2026
6 checks passed
@Japabu
Japabu deleted the wt/toyos-forkpin branch October 4, 2026 12:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant