diff --git a/adoption/platforms/linux-wsl2-new-distro.md b/adoption/platforms/linux-wsl2-new-distro.md index 94f431396..70b3c4a95 100644 --- a/adoption/platforms/linux-wsl2-new-distro.md +++ b/adoption/platforms/linux-wsl2-new-distro.md @@ -2,34 +2,46 @@ Added after `v2026.09.26.2`, with its templates under [`adoption/templates/wsl/`](../templates/wsl/) and the record [`docs/decisions/2026-10-01-new-wsl-distro-recipe.md`](../../docs/decisions/2026-10-01-new-wsl-distro-recipe.md), which -holds the decisions, their alternatives, the command table and every source. Status: documented, not run. No host has -executed these steps; the first one that does starts with the rehearsal of R1 and records the stage-1 receipt described -at the end. - -This page creates a second WSL 2 distribution on the existing Windows host from Canonical's published -`ubuntu-24.04.5-wsl-amd64.wsl`, gives it its default user without a prompt through cloud-init, and proves systemd, linger +holds the decisions, their alternatives, the command table and every source. After the WSL 2.7.13 rehearsal stopped +at F1 on shared cgroups, run 2 on WSL 3.0.1.0 passed stage 1 and W5's path-A proofs, showed distinct cgroup namespaces, +and stopped at W5's paired rule on a shared-console collision. Run 3 (2026-10-02T10:54Z to 11:01Z) then completed P1 to +F5 with the template's getty mask, the paired proof and a rootless container; the uv and Node probes and the +rollback arm are still owed. Start with R1 and record the stage-1 receipt described at the end. + +This page creates a second WSL 2 distribution on the existing Windows host from the selected Canonical image, +`ubuntu--wsl-amd64.wsl`, gives it its default user without a prompt through cloud-init, and proves systemd, linger and the user bus before stage 2, the repository's bootstrap. The workstation's distribution keeps running, stays the default and is never shut down. -The page runs twice on a host: first as a rehearsal on a throwaway name, which R1 then removes, and then for the real +The page rehearses both image arms on separate throwaway names, which R1 then removes, before running for the real `` ([Rehearsal first](#rehearsal-first)). Each run starts with P1 to P3, `sh` checks in the workstation's distribution that change nothing on the host; it shares the kernel and already has Ubuntu's keyring and cloud-init. Stage 1 (W1 to W7) runs on the Windows host in PowerShell. A session inside the workstation's WSL distribution runs each -PowerShell block as a `.ps1` file with `powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(wslpath -w step.ps1)"` +PowerShell block as a `.ps1` file with +`/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile -ExecutionPolicy Bypass -File "$(wslpath -w step.ps1)"` ([Windows-side commands from WSL](linux-wsl2.md#windows-side-commands-from-wsl)). Open each block's file with `Start-Transcript -LiteralPath 'Z:\WSL\downloads\-stage1.log' -Append`, placed in W1 right after its first line (which creates that folder), and end the file with `Stop-Transcript`. That private transcript is the raw install log the receipt is cut from. The first boot (F1 to F11) runs inside the new -distribution as ``, from an interactive `wsl.exe -d ` or, from a WSL session, -`wsl.exe -d -- bash -s < steps.sh`. F2's idle observation and F10 run from the Windows side and F11 needs a login -shell instead, as their sections say. +distribution as ``, from an interactive `/mnt/c/Windows/System32/wsl.exe -d ` or, from a WSL session, +`/mnt/c/Windows/System32/wsl.exe -d -- bash -s < steps.sh`. F2's idle observation and F10 run from the Windows +side and F11 needs a login shell instead, as their sections say. ## Names and inputs +Canonical's [26.04 release notes](https://documentation.ubuntu.com/release-notes/26.04/) and Microsoft's +[creation commands](https://learn.microsoft.com/en-us/windows/wsl/basic-commands) support the distribution and creation +route. Microsoft's command documentation is pinned in the decision record at `MicrosoftDocs/WSL` +`7b28cc1ee9b8ff672ada5e1c6c326d3573d703e5`, `WSL/basic-commands.md` and `WSL/build-custom-distro.md`. +These sources do not establish full-stack compatibility for either arm. + | Placeholder | Meaning | Rule | | --- | --- | --- | +| `` | `26.04.1` default or `24.04.5` rollback | the clean install uses 26.04.1; rollback applies only to a release-caused 26.04 failure with no in-release remedy; use the same release in W2, W4 and W6 | | `` | the new distribution's name (`--name`) and the user-data file's name | letters, digits, `.`, `_`, `-`; not already registered (W1); a throwaway name for the rehearsal (R1) | +| `` | the already-running workstation distribution's name | W1's starred distribution; every removal checks interop there | +| `` | a system service unit active in both distributions | include `.service`; for example, `cron.service` when both run it; W5 verifies it before reading its cgroup | | `` | the Linux user cloud-init creates | `^[a-z_][a-z0-9_-]*$`, the rule of the image's `/usr/lib/wsl/wsl-setup` | | `Z:\WSL\` | the install location (`--location`); WSL puts `ext4.vhdx` there | must not exist yet (W1) | | `Z:\WSL\downloads` | the image, its checksum list and the private logs | outside the Windows profile, so its path names no user | @@ -37,6 +49,18 @@ shell instead, as their sections say. | `` | the host value file `adoption/hosts/.json` | `^[A-Za-z0-9][A-Za-z0-9_.-]*$`, the bootstrap's `--host` rule | | `` | the adoption profile stage 2 installs | a `profiles[].id` of `adoption/manifest.json` | +The controlled release pins below come from Canonical's signed sums and Microsoft's catalog at +`8bc98bc33b246fe66710eec9eaa1b24c323da987`. The merged +[definitive manifest](../../evidence/artifacts/new-wsl-definitive-defaults-20261001/definitive-manifest.json) names +Ubuntu 26.04.1 LTS (Canonical WSL image) as the single default. Ubuntu 24.04.5 is the named rollback, used only for a +release-caused 26.04 failure with no in-release remedy. The rehearsal's host cgroup failure is not such a release +failure, and neither image has full new-host acceptance. + +| Release | Image file | Catalog entry | SHA-256 | Observed download bytes | +| --- | --- | --- | --- | --- | +| 26.04.1 | `ubuntu-26.04.1-wsl-amd64.wsl` | `Ubuntu-26.04` | `48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104` | 418,495,746; rehearsal run 2, 2026-10-02 | +| 24.04.5 | `ubuntu-24.04.5-wsl-amd64.wsl` | `Ubuntu-24.04` | `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e` | 388,975,696; historical artifact check | + ## Host-wide rules - No `.wslconfig` change, no `wsl --update` and never `wsl --shutdown`, which stops every distribution, the workstation's @@ -44,17 +68,43 @@ shell instead, as their sections say. `Select-String`; no command here writes, copies or edits it. - The default distribution stays the workstation's: nothing here sets a default, W1 records the starred line of `wsl.exe --list --verbose` and W4 and W5 prove it unchanged. -- No WSL update is needed to install: `wsl --install --from-file` needs WSL 2.4.4 or later (Microsoft), 2.4.8 (Ubuntu's - announcement) or 2.4.10 (Ubuntu's install guide), and the host runs 2.7.13. Later releases do change the first launch: +- This recipe adopts stable WSL 3.0.1 or later for a second systemd distribution, and requires it before W4 or any W6 + import. WSL 2.7.13, the release of the 2026-10-02 rehearsal, has a shared cgroup tree: equal user uids collide, and a + system-unit stop or restart can act on another distribution's processes. + Microsoft's [2.9.8 pre-release notes](https://github.com/microsoft/WSL/releases/tag/2.9.8) name + [PR #40519](https://github.com/microsoft/WSL/pull/40519), which isolates distro cgroups; the + [2.9.13 pre-release notes](https://github.com/microsoft/WSL/releases/tag/2.9.13) name + [PR #41512](https://github.com/microsoft/WSL/pull/41512), which creates their namespaces. Neither pre-release is + adopted: [3.0.1](https://github.com/microsoft/WSL/releases/tag/3.0.1) is the first stable release with both. The host + was updated to WSL 3.0.1.0 on 2026-10-02 and passed its check (rootless container, interop, user manager); + run 2 observed distinct cgroup namespaces, local process ids and two active user managers while both distributions + ran. W1 records the version; W5 must repeat the isolation proof before F1 changes anything. +- An install with a single systemd distribution, with no second systemd distribution running, can still use WSL + 2.4.10 or later. That is the conservative install-only minimum: Microsoft gives 2.4.4, Ubuntu's announcement + 2.4.8 and Ubuntu's install guide 2.4.10. This page keeps the workstation running and therefore requires 3.0.1. + Later releases also change the first launch: microsoft/WSL#40941 (after the first-run setup, files created from Windows are owned by 0:0) is fixed by PR #40977, - which ships first in 2.9.8, a pre-release, and in 3.0.1; tags 2.7.13 and 2.7.14 lack it. On 2.7.13, W7 terminates - `` once after the first launch and probes a file's owner before anything else is written from Windows. Updating - WSL is the keys lane's decision, not this page's. + which ships first in 2.9.8, a pre-release, and in 3.0.1; tags 2.7.13 and 2.7.14 lack it. On 2.7.13, W7 terminated + `` once after the first launch and probed a file's owner before anything else was written from Windows. Updating + WSL is the keys lane's action, and the cgroup prerequisite must hold before this page proceeds. - `.wslconfig` is global, so the new distribution inherits the workstation's settings, among them `networkingMode=mirrored`, `swap` and the two idle keys that W1 reads and F2 observes. - WSL 2 distributions share one network namespace ([Listeners and ports](linux-wsl2.md#listeners-and-ports)): every listener of the new distribution competes with the workstation's for the same ports. F8 picks the host file's ports outside the workstation's set. +- The VM console is shared: both distributions expose `/dev/tty1` as character device 4,1 of the same kernel. + Their enabled `getty@tty1.service` units use `TTYPath=/dev/tty1`, `TTYVHangup=yes`, `Restart=always` and + `RestartSec=0`, so competing gettys receive SIGHUP and restart until both hit the start limit. + Source review at microsoft/WSL tag `3.0.1`: + [`src/linux/init/init.cpp:363-365`](https://github.com/microsoft/WSL/blob/3.0.1/src/linux/init/init.cpp#L363-L365) + masks `console-getty.service` because the tty devices are shared + ([PR #14490](https://github.com/microsoft/WSL/pull/14490), merged 2026-04-09, "Fixes #13595"); + [`validate-modern.py:27-40`](https://github.com/microsoft/WSL/blob/3.0.1/distributions/validate-modern.py#L27-L40) + discourages that unit but does not list `getty@tty1.service`; a maintainer in + [issue #13595](https://github.com/microsoft/WSL/issues/13595) recommends disabling getty and adjacent units in WSL. + The user-data masks `getty@tty1.service` in the new distribution before systemd starts it (W5); the workstation's + unit is never touched. A host whose other systemd distributions keep an unmasked getty will see both fail whenever + two of them run; masking it there is the host owner's decision, outside this page. - The official first launch writes to Windows by design. WSL adds a Start-menu shortcut and a Windows Terminal profile for the distribution, and the image's `wsl-setup` copies the Ubuntu Sans Mono font into `%LOCALAPPDATA%\Microsoft\Windows\Fonts` and registers it under `HKCU` when that file is missing. The generated @@ -62,19 +112,68 @@ shell instead, as their sections say. ## Rehearsal first +Run 2 on WSL 3.0.1 passed stage 1 and W5's path-A proofs and showed distinct cgroup namespaces, then stopped at W5's +paired rule on the console collision. Probe E1 tested the template's getty mask once outside the recipe, and +run 3 then completed P1 to F5 with it: the unit masked and inactive, the workstation equal to its baseline, the file +owner `1000:1000` after one terminate, the distribution still listed in all twelve idle polls, a user-owned bus and +`Hello from Docker!` from a rootless container. The uv and Node probes need stage 2 and are owed, as is the rollback +arm. + ### R1. Rehearse on a throwaway name, then remove it Run the page once on a throwaway distribution before the real one. For that run `` is a throwaway name that no other distribution uses, never the real `` and never the workstation's, and `Z:\WSL\` is its own new folder. -Run it through F3: P1 to P3, W1 to W7 (W6 only after the W5 marker) and F1 to F3, with the observations no host has -made yet: P3's baseline and W5's second count of storage errors, W5's `cloud-init schema --system` (path A), W7's -file-ownership probe and F2's idle observation. The first real reading of the storage rule thus comes from the throwaway -distribution. Record the rehearsal's name, result, creation path and those observations in the receipt's `rehearsal` -block. Every -host-wide rule holds: the rehearsal names only its own distribution, and R1 terminates and unregisters only that literal -name. A rehearsal that stopped before W4 installed nothing, and R1 has nothing to remove. - -After the rehearsal's F3, read the list first and remove the throwaway distribution. When every proof held, no export is +Run it through F3: P1 to P3, W1 to W7 (W6 only after the W5 marker) and F1 to F3, recording P3's baseline and W5's +second count of storage errors, W5's `cloud-init schema --system` (path A), W7's file-ownership probe and F2's idle +observation. Run 3 observed all of them on 3.0.1 with the corrected template. Record the rehearsal's name, result, creation path and those observations in the receipt's +`rehearsal` block. Every host-wide rule holds: the rehearsal names only its own distribution, and R1 terminates and +unregisters only that literal name. A rehearsal that stopped before W4 installed nothing, and R1 has nothing to remove. + +Rehearse both `26.04.1` default and `24.04.5` rollback on separate throwaway names, with the same checkout, user-data, +profile, Windows driver and host-wide settings. 26.04.1 is the single default for the clean install; 24.04.5 returns +only on a release-caused 26.04 failure with no in-release remedy. Comparison arms stay on throwaway distributions. +Before removal, extend each successful run through F9 using the existing bootstrap and its native recipes, then run +the comparison probes below from the workstation's second-instance session (the PowerShell block is launched through +the same Windows-side route as W1). Each `wsl.exe -d` targets that arm's literal throwaway `` and its default user. +F1 to F3 and the probes test first boot and the user manager from a second instance; they do not assume the 24.04.5 +historical receipts are acceptance on this new host. + +The following criteria are the preregistered ones for both arms with the amendments of 2026-10-02 made before any +comparison ran, recorded in the [decision record](../../docs/decisions/2026-10-01-new-wsl-distro-recipe.md). +Preserve a failed or skipped criterion with its returned output; neither a missing comparison nor a host version +check qualifies an arm. Both arms' full new-host acceptance is owed; run 2 stopped at W5's paired rule. + +| Criterion | Same required observation for each arm | Primary source or accepted recipe | +| --- | --- | --- | +| `first_boot` | W5 exits 0 without the user/OOBE failure markers; default uid 1000, retained cloud-init results, the selected image's own schema check and F1's pass condition | W5, cloud-init tag 26.1; Microsoft creation docs pinned above; each image's inspected `wsl-setup` | +| `systemd_user_from_second_instance` | F2 records linger and idle behavior; F3 and the second-instance probes show a user-owned directory/socket and user manager `running` | F1 to F3; Microsoft systemd guide at the pinned revision; systemd v255 references in the decision record; 259 lifecycle acceptance remains owed | +| `wsl_gpu` | `/dev/dxg` is a character device and native `nvidia-smi` exits 0 with the Windows-provided GPU visible on the same driver; record limited WSL features and any failure | [Microsoft GPU guide](https://learn.microsoft.com/en-us/windows/wsl/tutorials/gpu-compute), [NVIDIA WSL guide](https://docs.nvidia.com/cuda/wsl-user-guide/index.html); no Linux driver installation | +| `uv_cpython_3_13` | The bootstrap's uv runs a separate managed CPython 3.13 and prints `Python 3.13.x`, exit 0; the system Python version is recorded separately | F9 and `adoption/bootstrap.md`'s `uv run --no-project --python 3.13` prerequisite command; installed uv 0.12.17 `run --help` confirms both selectors | +| `node_24` | The bootstrap's selected Node starts and prints `v24.21.0`, exit 0; retain any loader/package failure | F4, F9 and `adoption/pins-linux-x86_64.json`'s official Node 24.21.0 tarball | + +```powershell +wsl.exe -d '' --exec bash -lc 'stat -c %U,%F /run/user/$(id -u) /run/user/$(id -u)/bus' +wsl.exe -d '' --exec systemctl --user is-system-running --wait +wsl.exe -d '' --exec test -c /dev/dxg +wsl.exe -d '' --exec /usr/lib/wsl/lib/nvidia-smi +wsl.exe -d '' --exec bash -lc 'python3 --version' +wsl.exe -d '' --exec bash -lc 'uv run --no-project --python 3.13 python --version' +wsl.exe -d '' --exec bash -lc 'node --version' +``` + +The first probe prints `,directory` and `,socket`. No command that a PowerShell block hands to +`wsl.exe` carries a double quote inside its single-quoted argument: Windows PowerShell 5.1 does not escape it for a +native program, and the argument breaks apart (run 3, 2026-10-02: the earlier `stat -c "%U %F"` form returned +`stat: missing operand`, and a quoted `for` loop returned `unexpected EOF`). + +Record these per-arm observations in `comparison_arms` alongside each rehearsal's schema, ownership, storage and idle +observations. The inspected 26.04.1 system interpreter is Python 3.14.4-1ubuntu0.1 (its `python3` metapackage is +3.14.3-0ubuntu2); it is not the toolkit's CPython 3.13 migration acceptance. uv selects 3.13 separately. +GPU visibility and interpreter/version startup are bounded criteria; the full stack, GPU workloads and model behavior +still require their own native acceptance. Complete every criterion for the default before accepting the clean install; +the same criteria apply if a release-caused failure makes the rollback necessary. + +After F3 and the preregistered comparisons, read the list first and remove the throwaway distribution. When every proof held, no export is needed: ```powershell @@ -82,6 +181,8 @@ $env:WSL_UTF8 = '1' wsl.exe --list --quiet wsl.exe --terminate '' wsl.exe --unregister '' +wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver' +if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' } wsl.exe --list --verbose ``` @@ -99,14 +200,40 @@ if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' } (Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\-rehearsal.tar').Hash.ToLowerInvariant() (Get-Item -LiteralPath 'Z:\WSL\downloads\-rehearsal.tar').Length wsl.exe --unregister '' +wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver' +if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' } wsl.exe --list --verbose ``` +After every `--unregister`, the check runs in ``: `/proc/sys/fs/binfmt_misc/WSLInterop` must exist and the +Windows executable `/mnt/c/Windows/System32/cmd.exe /d /c ver` must launch with exit 0. Linux shells on this page call Windows executables by full path, because a distribution may set `appendWindowsPath=false` (the workstation does), and then a bare name is not found even when interop works. On WSL 2.7.13 the rehearsal's unregister removed this VM-wide +registration on the surviving distribution, and `sudo systemctl restart systemd-binfmt` there restored it. That restart +is no remedy on the adopted release: on 3.0.1 the registration survived both unregisters of 2026-10-02, in run 2 and +probe E1. WSL mounts `/proc/sys/fs/binfmt_misc/status` read-only, so that one distribution +cannot flush the VM-wide registrations ([PR #40621](https://github.com/microsoft/WSL/pull/40621)), and that restart +itself exits 1 (F1 gives the reason). The guard stops the block when either observation fails. A failed check on 3.0.1 +is a finding for review, not a fault to repair: stop, do not import or provision another distribution, and record these +observations from a native shell in the surviving distribution, after the new distribution has been removed: + +```sh +test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver +ls /proc/sys/fs/binfmt_misc +systemctl status systemd-binfmt.service --no-pager +``` + +Proof: nothing here repairs anything. The first line repeats the check from a native shell, the second lists the +registered formats, which shows whether `WSLInterop` is among them, and the third prints the unit's state and log +lines. Record the returned output of all three, and of the failed check, in the receipt (`interop_after_unregister`), +then stop for review: never restart, stop or start a unit, and never import or provision another distribution. W5's +failed-proof removal and W6's removal use this same check and recovery. If W5 already removed a failed rehearsal, +record its export and do not remove that name again in R1. + Proof: the first `--list --quiet` names the throwaway distribution; the last `--list --verbose` no longer lists it, and its starred line is still W1's. A failed rehearsal's export goes into the `rehearsal` block with its SHA-256 and size, and the real run waits until the failure is understood. The rehearsal's private transcript and its user-data file under `%USERPROFILE%\.cloud-init` stay; record whether WSL's Start-menu entry and terminal profile for the throwaway name -outlive `--unregister`. Then run the page again from P1 for the real ``, without R1. +outlive `--unregister`. After the required comparison records, run the page again from P1 for the default real +``, or the qualified rollback, without R1. ## Pre-checks in the workstation distribution @@ -124,25 +251,35 @@ the workstation's Ubuntu installs, from the `ubuntu-keyring` package, and `gpgv` an empty temporary directory, which is also `gpgv`'s home, so no GnuPG home of the user is read or written. ```sh +for RELEASE in 26.04.1 24.04.5; do SUMS_DIR="$(mktemp -d)" -curl -fsSL -o "$SUMS_DIR/SHA256SUMS" https://releases.ubuntu.com/24.04.5/SHA256SUMS -curl -fsSL -o "$SUMS_DIR/SHA256SUMS.gpg" https://releases.ubuntu.com/24.04.5/SHA256SUMS.gpg +curl -fsSL -o "$SUMS_DIR/SHA256SUMS" "https://releases.ubuntu.com/$RELEASE/SHA256SUMS" || exit 1 +curl -fsSL -o "$SUMS_DIR/SHA256SUMS.gpg" "https://releases.ubuntu.com/$RELEASE/SHA256SUMS.gpg" || exit 1 gpgv --homedir "$SUMS_DIR" --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg "$SUMS_DIR/SHA256SUMS.gpg" "$SUMS_DIR/SHA256SUMS" -grep ' \*ubuntu-24\.04\.5-wsl-amd64\.wsl$' "$SUMS_DIR/SHA256SUMS" +if [ "$?" -ne 0 ]; then exit 1; fi +grep -F " *ubuntu-$RELEASE-wsl-amd64.wsl" "$SUMS_DIR/SHA256SUMS" || exit 1 +done ``` Proof: both downloads exit 0; `gpgv` exits 0 and prints `using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092` and -`Good signature from "Ubuntu CD Image Automatic Signing Key (2012) "`; `grep` prints -`bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e *ubuntu-24.04.5-wsl-amd64.wsl`, the hash W2 pins. A +`Good signature from "Ubuntu CD Image Automatic Signing Key (2012) "` for each release; `grep` prints +each image's signed line with its exact hash from the release table. Both signed sums must pass, including the arm not +selected for this run. The coordinator's preserved checks already passed for both (decision record, Evidence classes); +reuse matching evidence for this authoring unit. A `BAD signature`, a missing key or any other nonzero exit stops the run with nothing installed. ### P2. The user-data schema Added after `v2026.09.26.2`: this renders `adoption/templates/wsl/cloud-init.user-data.template` with `` before any boot and validates it with cloud-init's own schema check. The render is F8's `string.Template` program, which -writes the same bytes as W3's literal replace (W3 says why). Run it from the root of the checkout that `` names. -The workstation's cloud-init checks against its own version's schema, which `cloud-init --version` records; the image -runs 26.1. +writes the same bytes as W3's literal replace (W3 says why). Run it from the root of the checkout that `` names, +for the selected image's user-data. The workstation's cloud-init checks against its own version's schema, which +`cloud-init --version` records. This host check is not image qualification. The inspected 26.04.1 image packages +cloud-init 26.1-0ubuntu3~26.04.1; the historical 24.04.5 image packages 26.1-0ubuntu1~24.04.1. W5 records the selected +image's version and validates the provisioned user-data with its own `cloud-init schema --system` on path A. Canonical's +[cloud-init WSL guide](https://ubuntu.com/wsl/docs/stable/howto/cloud-init/) assumes 24.04 or 22.04; it does not qualify +26.04. The 26.04.1 rehearsal's own schema check passed on both 2.7.13 and 3.0.1; each run must repeat it, and 24.04.5 +remains unrun. ```sh RENDER_DIR="$(mktemp -d)" @@ -164,13 +301,15 @@ would meet. microsoft/WSL#41482 (https://github.com/microsoft/WSL/issues/41482, (`/sbin/init failed to start within 10000ms`). Its workaround, `swap=0` in the global WSL configuration and a WSL shutdown, is outside this page. Without `sudo` the user cannot read the kernel journal, and `dmesg` holds only the recent ring buffer, so this reads the journal of the current boot. It searches for the driver's name, not a device id, -and leaves out the driver's registration line, `hv_vmbus: registering driver hv_storvsc`, which every boot logs. A count +and leaves out the driver's registration line, `hv_vmbus: registering driver hv_storvsc`, which every boot logs, and +the kernel's command-line echo. Run 2's baseline of `2` contained only `Command line:` and `Kernel command line:` +echoes naming `hv_storvsc.storvsc_max_hw_queues=4`; neither is a storage error. A count over the whole boot cannot tell errors that are happening now from an old burst, so the count is a baseline, not a verdict: the newest error line's age decides here, and W5 counts again after the first launch. ```sh -sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc' -sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | grep -v 'registering driver hv_storvsc' | tail -n 1 +sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -Evc 'registering driver hv_storvsc|[Cc]ommand line:' +sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | grep -Ev 'registering driver hv_storvsc|[Cc]ommand line:' | tail -n 1 cat /proc/uptime swapon --show ``` @@ -185,6 +324,11 @@ Proof: installed: record the count, the line's time and its shape without device ids, name microsoft/WSL#41482 and leave the next step to the user, because the workaround changes the global WSL configuration and shuts WSL down. An older line, or none, lets the run continue. The one-hour threshold is this recipe's choice, not an upstream figure. +- One exception, added on 2026-10-02: a newest line whose kernel time an earlier run of this page already recorded as + its own attachment-window line (W5, `storage_errors.new_lines`) is known and does not stop the run; record that it + is the earlier run's line. Run 2 and probe E1 each left exactly one such line in their install windows (7118.5 s and + 7678.6 s), so a second arm rehearsed within the hour would otherwise stop on its predecessor's line. A line that no + earlier receipt explains keeps the rule. - The kernel time decides, not the journal's wall-clock stamp: journald stamps a kernel line when it reads it, and a restart of the workstation distribution reads the kernel's ring buffer again and stamps old lines anew. - Record `swapon --show`: active swap is the issue's condition, not a failure by itself. @@ -210,9 +354,48 @@ Select-String -LiteralPath (Join-Path $env:USERPROFILE '.wslconfig') -Pattern '^ Get-PSDrive -Name Z | Select-Object -Property Name, Used, Free ``` +Then record this baseline natively in the already-running workstation distribution, as its default user and without +`sudo`. It holds the same five observations that W5 reads from both distributions, taken before anything is imported: + +```sh +id -u +systemctl is-system-running +systemctl --failed --no-legend --plain +systemctl is-active "user@$(id -u).service" +readlink /proc/self/ns/cgroup +``` + +Record each command's exit separately. `systemctl is-system-running` exits 1 for `degraded`, which is expected and +recorded; the proof reads the printed values. The failed-unit value is the first column of each printed row. + Proof, recorded in the receipt's `host` block: -- `wsl.exe --version` starts with `WSL version:` and a version of 2.4.10 or later. +- `wsl.exe --version` starts with `WSL version:` and a version of 3.0.1 or later for this second systemd distribution. + An older version stops the run before W4 or W6 imports anything. The reason is the shared cgroup tree on 2.7.x; + PR #40519 isolated distro cgroups and PR #41512 created their namespaces, first together in stable 3.0.1 + (the release notes linked in Host-wide rules). +- Record the five lines as the workstation's baseline (`workstation_baseline`): the uid, the system state, the names of + the failed units, the user manager's state and the cgroup namespace. `cgroup:[4026531835]` is the kernel's initial + cgroup namespace, `PROC_CGROUP_INIT_INO` in Linux's + [`include/linux/proc_ns.h`](https://github.com/torvalds/linux/blob/v6.18/include/linux/proc_ns.h). + A noninitial namespace is necessary but does not prove isolation. The shell's own + `/proc/self/ns/cgroup` needs no `sudo` and equals `/proc/1/ns/cgroup` on the updated host. W1's version + precondition and W5's paired check while both distributions run are required; record what the updated host actually + prints. +- The workstation's baseline passes only when all four hold: its uid equals the new distribution's planned default + uid `1000`; its system state is `running` with no failed unit, or `degraded` with its failed set contained in + {`systemd-binfmt.service`, `getty@tty1.service`}; its user manager is `active`; and its cgroup namespace is not + `cgroup:[4026531835]`. Anything else stops the run before W4 and before any W6 import, because W5 compares the + workstation with this baseline and its proof could not hold. +- `getty@tty1.service` is allowed in that failed set only as the leftover of an earlier collision on the shared + console. When it is there, also run the following read-only command and record it as `getty_tty1_result`; + `Result=start-limit-hit` is required, and another result stops the run. The workstation's unit messages are not + proved again here: it is compared with itself, and F1 proves the message for the new distribution. + +```sh +systemctl show getty@tty1.service -p Result -p NRestarts +``` + - The starred line of `--list --verbose` names the workstation's distribution (`default_distribution_before`). - `` is not a line of `--list --quiet`, and the first `Test-Path` prints `False`. WSL refuses a name or an install folder another registration already uses and creates the folder itself. @@ -230,14 +413,16 @@ Proof, recorded in the receipt's `host` block: default 60000 ms) is the VM's. Without `instanceIdleTimeout=-1`, `` and its user services stop about `instanceIdleTimeout` after the last client exits (15 seconds by default), and issue reports say linger does not prevent it; F2 observes it. Changing either key is the user's decision, outside this page. -- `Free` on `Z:` is recorded. The image unpacks to about 1.3 GB before stage 2 adds its tools. +- `Free` on `Z:` is recorded. The historical 24.04.5 image unpacked to about 1.3 GB; the 26.04.1 unpacked size remains + unmeasured. Stage 2 adds its tools. Stop on any other result. ### W2. Download and verify the image The expected sha256 is published twice, by Canonical next to the image and by Microsoft's WSL distribution catalog. -This block reads the catalog at the commit of 2026-09-14 (#41465), whose `Ubuntu-24.04` entry is this image. It is the +This block reads the catalog at the commit of 2026-09-14 (#41465), choosing `Ubuntu-26.04` or `Ubuntu-24.04` for the selected +release. It is the only hash check before W4 installs the file: at WSL 2.7.13, `wsl --install --from-file` checks no hash (`WslClient.cpp:500-537`). The online `wsl --install Ubuntu-24.04` does compare its download with the catalog entry's `Sha256` (`WslInstall.cpp:36-50`, `:315`), but it reads the catalog from WSL's `master` branch at install time; this @@ -245,18 +430,30 @@ page installs a pinned file whose hash the operator sees, and P1 has tied that h ```powershell $ProgressPreference = 'SilentlyContinue' -Invoke-WebRequest -UseBasicParsing -Uri 'https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl' -OutFile 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl' -Invoke-WebRequest -UseBasicParsing -Uri 'https://releases.ubuntu.com/24.04.5/SHA256SUMS' -OutFile 'Z:\WSL\downloads\SHA256SUMS' -$Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e' -$Published = (Select-String -LiteralPath 'Z:\WSL\downloads\SHA256SUMS' -Pattern ' \*ubuntu-24\.04\.5-wsl-amd64\.wsl$').Line.Split(' ')[0] -$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content | ConvertFrom-Json).ModernDistributions.Ubuntu | Where-Object Name -eq 'Ubuntu-24.04' -$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl').Hash.ToLowerInvariant() +$Release = '' +switch ($Release) { + '26.04.1' { $Expected = '48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104'; $CatalogName = 'Ubuntu-26.04' } + '24.04.5' { $Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e'; $CatalogName = 'Ubuntu-24.04' } + default { throw 'unsupported release: do not download or install' } +} +$Image = "ubuntu-$Release-wsl-amd64.wsl" +$ImagePath = "Z:\WSL\downloads\$Image" +$ImageUrl = "https://releases.ubuntu.com/$Release/$Image" +$SumsPath = "Z:\WSL\downloads\$Release-SHA256SUMS" +Invoke-WebRequest -UseBasicParsing -Uri $ImageUrl -OutFile $ImagePath +Invoke-WebRequest -UseBasicParsing -Uri "https://releases.ubuntu.com/$Release/SHA256SUMS" -OutFile $SumsPath +$Published = (Select-String -LiteralPath $SumsPath -Pattern (' \*' + [regex]::Escape($Image) + '$')).Line.Split(' ')[0] +$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content | ConvertFrom-Json).ModernDistributions.Ubuntu | Where-Object Name -eq $CatalogName +$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $ImagePath).Hash.ToLowerInvariant() "computed $Actual published $Published listed $($Listed.Amd64Url.Sha256) url $($Listed.Amd64Url.Url)" -if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected) { throw 'sha256 mismatch: do not install' } +if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected -or $Listed.Amd64Url.Url -ne $ImageUrl) { throw 'sha256 mismatch: do not install' } +(Get-Item -LiteralPath $ImagePath).Length ``` -Proof: the printed line shows one hash three times, `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e`, and -the URL `https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl`; the file is 388,975,696 bytes. A `throw` +Proof: the printed line shows the selected release's exact hash three times and its actual release URL from the pinned +catalog, as listed in the release table. `Get-FileHash` hashes the entire downloaded image. Record `Length` as this run's +actual image size; run 2 measured 418,495,746 bytes for 26.04.1, while 24.04.5's historical stream measured +388,975,696 bytes. A `throw` stops stage 1 with nothing installed. Record `.Hash` only: `Get-FileHash` also prints the file's path. ### W3. Render the user-data before any boot @@ -285,12 +482,12 @@ the last command prints nothing. The file holds no secret: the user's password s ```powershell $env:WSL_UTF8 = '1' -wsl.exe --install --from-file 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl' --name '' --location 'Z:\WSL\' --no-launch +wsl.exe --install --from-file 'Z:\WSL\downloads\ubuntu--wsl-amd64.wsl' --name '' --location 'Z:\WSL\' --no-launch $LASTEXITCODE wsl.exe --list --verbose ``` -Proof: the output is `Installing: Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl`, then +Proof: the output is `Installing: Z:\WSL\downloads\ubuntu--wsl-amd64.wsl` with the selected release filled in, then `Distribution successfully installed. It can be launched via 'wsl.exe -d '`; `$LASTEXITCODE` prints `0`; `--list --verbose` shows `` as `Stopped` at version 2 with the starred line unchanged. `--install --from-file` registers the distribution with its first-run setup enabled and `--no-launch` boots nothing, so cloud-init has not run yet. @@ -311,6 +508,9 @@ cmd.exe /d /c "wsl.exe -d < NUL" $LASTEXITCODE wsl.exe -d '' --exec id -un wsl.exe -d '' --exec id -u +wsl.exe -d '' --exec systemctl is-enabled getty@tty1.service +wsl.exe -d '' --exec systemctl show getty@tty1.service -p LoadState -p ActiveState -p NRestarts +wsl.exe -d '' -u root --exec cloud-init --version wsl.exe -d '' -u root --exec cloud-init status --long wsl.exe -d '' -u root --exec cat /var/lib/cloud/data/result.json wsl.exe -d '' -u root --exec cat /var/lib/cloud/data/status.json @@ -323,10 +523,28 @@ wsl.exe --list --verbose Proof (path A, cloud-init provisioned the instance): +- `wsl: Failed to start the systemd user session` stops the run even when the launch exits 0. Record the warning + and use the failed-proof recovery below; do not continue to F1 or try W6 as a remedy for that warning. - The launch prints `Provisioning the new WSL instance ` and `This might take a while...`, and `$LASTEXITCODE` - prints `0`. The image's `wsl-setup` prints both lines, not WSL: lines 117-118 at its version 0.5.10~24.04.2, Launchpad + prints `0`. The image's `wsl-setup` prints both lines, not WSL: for the historical 24.04.5 image, lines 117-118 at its version 0.5.10~24.04.2, Launchpad tag `import/0.5.10_24.04.2`, commit `74bfc89113bc7d46a4d9feb1e69cd6951fbc6908`. +- For 26.04.1, the inspected image packages `wsl-setup` 0.6.3ubuntu~26.04.1, matched to upstream tag `0.6.3`, commit + `73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8` (`wsl-setup:122-123,155-156` and `ubuntu-insights.sh:15-19,114-142`). + Its Insights script preserves existing native Linux/Windows consent; it prompts only when stdin is a terminal + (`-t 0`). W5's NUL input takes its native noninteractive path. No command here invents consent or writes a global + consent registry value. This source review is not first-boot acceptance. - `id -un` prints `` and `id -u` prints `1000`. +- `systemctl is-enabled getty@tty1.service` prints `masked`; exit 1 is that command's normal exit for a masked unit. + `systemctl show` prints `LoadState=masked`, `ActiveState=inactive` and `NRestarts=0`. The user-data's `bootcmd` + masks the unit in cloud-init's network stage, before systemd starts it: the 26.04.1 image's + `cloud-init-network.service` has `Before=systemd-user-sessions.service`, `getty@.service` has + `After=systemd-user-sessions.service`, and `/etc/cloud/cloud.cfg` lists `bootcmd` in `cloud_init_modules`. + These are source reviews of the pinned official image's own files, read for probe E1 on 2026-10-02, not a complete + recipe run. In that probe the unit was masked and inactive with no restarts, cloud-init finished without errors, + and the only failed unit was `systemd-binfmt.service`. The journal's `Failed to start getty@tty1.service.` line + was the refused job of a masked unit, which did not appear in the failed-unit list. +- `cloud-init --version` records the selected image's packaged version before its own schema check; P2 recorded only + the workstation's version. - `cloud-init status --long` prints `status: disabled` and `boot_status_code: disabled-by-marker-file`. This line proves only that the marker is in place, because cloud-init 26.1 reports `disabled` once `/etc/cloud/cloud-init.disabled` exists, whatever its run did (`cloudinit/cmd/status.py:284-286` and `:385-386`). @@ -335,6 +553,14 @@ Proof (path A, cloud-init provisioned the instance): - Exit 2 means recoverable errors (warnings) on that boot while the line still says `disabled` (`:158-163`, `:254-255`). Record them, as `status.json` below does. - `status: error` or exit 1 means that boot's run failed: stop. +- With the template's `bootcmd`, one recoverable error is expected and `cloud-init status --long` exits 2: `Failed to + wait for network`, from `systemctl start systemd-networkd-wait-online.service`, with `Unit + systemd-networkd-wait-online.service is masked.` cloud-init waits for the network when the user-data holds a + `bootcmd` (26.1, `cloudinit/cmd/main.py:351-402` and `:522-528`; `cloudinit/net/activators.py:312-314`, read in + the 26.04.1 image), and WSL masks that unit because it configures the network itself (microsoft/WSL `3.0.1`, + `src/linux/init/init.cpp:356-358`). Run 3 recorded it in the `init` and `modules-config` stages with empty + `errors`, no delay (the stage took 0.54 s) and `result.json` without errors. Any other recoverable error is + recorded and reviewed as before. - `/var/lib/cloud/data/result.json` holds `"datasource": "DataSourceWSL"` and `"errors": []`. cloud-init writes this file only when its final stage ends, and it collects the errors of every stage (`cloudinit/cmd/main.py:1017-1030`). Its presence proves the run completed; its empty list proves the run had no errors. @@ -348,27 +574,111 @@ Proof (path A, cloud-init provisioned the instance): (cloud-init 26.1, `cloudinit/config/schema.py:1388-1393`). With more than one data part a `Found cloud-config data types:` header comes first and the line is indented (`:1443-1458`, `:1492`); an invalid schema exits 1 (`:1493-1498`). The `schema` subcommand never reads the marker (`cloudinit/cmd/main.py:1240-1241`, - `:1286-1293`), but no new instance has run it after the marker yet. Record the output (`schema_system`). + `:1286-1293`). The 26.04.1 rehearsal returned `Valid schema user-data` after the marker on both host releases; + each run must repeat that observation. Record the output (`schema_system`). - `/etc/wsl.conf` holds `[boot]`, `systemd=true`, `[user]` and `default=`, each once. - The marker file exists, and `sudo -l` lists `(ALL) NOPASSWD: ALL`. - The starred line of `--list --verbose` is still W1's (`default_distribution_after`). +After the first launch and before F1, prove cgroup isolation from the already-running workstation distribution, +while both distributions run. Keep a client of `` open during the check. Choose `` active in both +distributions, for example `cron.service` when both run it. Run this block natively in the workstation's shell; +the `wsl.exe -d` commands alone target the new distribution: + +```sh +systemctl is-active '' +/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-active '' +cat '/sys/fs/cgroup/system.slice//cgroup.procs' +/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active "user@$(id -u).service"' +``` + +Proof: both system-unit checks print `active` and exit 0; `cgroup.procs` contains local process ids and no `0` entry; +the new distribution's `user@.service` prints `active` and exits 0, for its default user's uid 1000. A `0` entry +is a process from another distribution's process namespace. An empty or unreadable file or a unit not active in both +cannot prove isolation and stops the run. On path B, W6's manual-user relaunch must repeat this proof and the paired +record below before W7 or F1; the initial OOBE prompt failure alone does not establish a default user's manager. +Nothing on the updated host is assumed from its version or namespace value. + +Then record the same five observations from both running distributions at once, because a reading of one side does not +show the same uid, distinct namespaces and two healthy managers. Run this block natively in the workstation's shell, +after the cgroup block above and before F1. Its first five lines repeat W1's baseline commands, and its next five run +the same commands inside ``. Record each command's output and exit in its own receipt entry: + +```sh +id -u +systemctl is-system-running +systemctl --failed --no-legend --plain +systemctl is-active "user@$(id -u).service" +readlink /proc/self/ns/cgroup +/mnt/c/Windows/System32/wsl.exe -d '' --exec id -u +/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-system-running +/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl --failed --no-legend --plain +/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active "user@$(id -u).service"' +/mnt/c/Windows/System32/wsl.exe -d '' --exec readlink /proc/self/ns/cgroup +``` + +Proof: both distributions print the same uid; both user managers print `active`; the new distribution's system state +is `running` with no failed unit, or `degraded` with `systemd-binfmt.service` as its only failed unit (its message is +proved at F1); the two `cgroup:[...]` values differ from each other and neither is `cgroup:[4026531835]`; and all five +workstation values equal its own values recorded in W1 before the import, so the new distribution did not disturb it. +A `getty@tty1.service` in the new distribution's failed set, or one that newly appears in the workstation's, fails the +proof: the mask did not take effect before the unit started. Record all of it, with the WSL version and +kernel from W1 and the image revision from W2, in the receipt's `paired_isolation`: one object for the workstation and +one for ``, each with `uid`, `system_state`, `failed_units`, `user_manager` and `cgroup_namespace`. + +When any W5 proof fails, or the user-session warning appears, stop the run and never stop or restart a unit in either +distribution. Terminate only the new distribution, export its failed state, and unregister only that name. Do not +take path B for any such failure. Record its cause (`cgroup`, `tty` or a short text); this recovery block serves every +failed W5 proof: + +```powershell +$env:WSL_UTF8 = '1' +wsl.exe --list --quiet +wsl.exe --terminate '' +wsl.exe --export '' 'Z:\WSL\downloads\-w5-failed.tar' +$LASTEXITCODE +if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' } +(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\-w5-failed.tar').Hash.ToLowerInvariant() +(Get-Item -LiteralPath 'Z:\WSL\downloads\-w5-failed.tar').Length +wsl.exe --unregister '' +wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver' +if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' } +wsl.exe --list --verbose +``` + +Proof: the export exits 0 before deletion, its cause, hash and size are recorded in `w5_failure_export`, and `` is +gone with the starred line unchanged. The surviving distribution must retain `WSLInterop` and launch the Windows +executable. If it does not, R1's interop recovery (its records, then a stop for review) applies only after the new +distribution is removed. Keep the failure and stop for review; an upstream version claim does not replace the failed +isolation observation. + On both paths, right after the launch and before W6 or W7, count the storage errors again in the workstation distribution, where P3 ran: ```sh -sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc' +sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -Evc 'registering driver hv_storvsc|[Cc]ommand line:' ``` -Proof: the count equals P3's baseline; record it as `second_count`. A larger count means storage errors during W4 to -W5. Record both counts and the new lines (P3's second line with `tail -n` set to the difference, without device ids), -treat a failed W5 as exposure to microsoft/WSL#41482 rather than a cloud-init failure, and do not continue to stage 2 -(F9) until that is decided: its workaround changes the global WSL configuration and shuts WSL down, both outside this -page. +Proof: record both counts. On both paths, record P3's baseline before the import and `second_count` after the first launch. +An increase confined to the window in which the new disk is attached, before cloud-init starts, is recorded with the +new lines and does not stop the run. Any storage error after that window, or any provisioning step that fails with a +storage cause, stops the run. + +Run 2 observed one real driver error (`cmd 0x2a`, `srb 0x4`, host `0xc00000a1`) at kernel time 7118.5 s, inside W4's +install window: the new disk was unmounted at 7126.9 s and cloud-init's `init-local` started at 7134.15 s. The written +window rule recorded that line without stopping the run; the second count of `3` also included the two command-line +echoes that the corrected filter now drops. + +When the count increases, read the new lines with P3's second command and `tail -n` set to the difference, without +device ids. Compare their kernel times with the attachment window and W5's retained `status.json` `init-local` start. +If the window cannot be established, stop for review. Record the window and its lines in `storage_errors.new_lines`; +apply the same stop rule to later provisioning. A failure with a storage cause is exposure to microsoft/WSL#41482 and +stops the run before stage 2 (F9); its workaround changes the global WSL configuration and shuts WSL down, both +outside this page. A failed W5 without a storage cause keeps its own diagnosis. How to tell that cloud-init did not provision the instance: the launch output contains `Create a default Unix user account:` and `OOBE command "/usr/lib/wsl/wsl-setup" failed, exiting`, and the exit code is -not 0. WSL keeps the first-run setup pending. Go to W6. +not 0. WSL keeps the first-run setup pending. Only that prompt failure, with no other failed W5 proof, permits W6. ### W6. Path B: import and a manual user (only after the W5 marker) @@ -395,19 +705,30 @@ if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' } (Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\-failed.tar').Hash.ToLowerInvariant() (Get-Item -LiteralPath 'Z:\WSL\downloads\-failed.tar').Length wsl.exe --unregister '' -wsl.exe --import '' 'Z:\WSL\' 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl' --version 2 +wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver' +if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' } +wsl.exe --import '' 'Z:\WSL\' 'Z:\WSL\downloads\ubuntu--wsl-amd64.wsl' --version 2 wsl.exe -d '' -u root --exec cloud-init status --wait --long ``` +Before the import, the surviving distribution's `WSLInterop` registration must exist and its Windows executable must +launch. If the guard throws, do not import: take R1's interop recovery there, which records the observations and stops +for review. W1's WSL 3.0.1 precondition still applies to this import. + An imported distribution starts as root and skips the first-run command. Whether cloud-init provisions it is not documented. The `cloud-init status` line therefore only records the import's first boot: - `done` or `error` when cloud-init ran; - `disabled` with `boot_status_code: disabled-by-generator` when it found no datasource (`cloudinit/cmd/status.py:298-300`). The next commands skip what already exists. Run them as root inside ``: in a shell from -`wsl.exe -d -u root`, or from a WSL session through `wsl.exe -d -u root -- bash -s < path-b.sh`. +`/mnt/c/Windows/System32/wsl.exe -d -u root`, or from a WSL session through +`/mnt/c/Windows/System32/wsl.exe -d -u root -- bash -s < path-b.sh`. + +An import's first boot starts the getty before the first line below can run. The paired proof after the relaunch +decides: if the workstation's values differ from its baseline, stop and use W5's recovery block. ```sh +systemctl mask --now getty@tty1.service id -u '' || useradd --create-home --uid 1000 --groups adm,cdrom,sudo,dip,plugdev --shell /bin/bash '' printf '%s ALL=(ALL) NOPASSWD:ALL\n' '' > /etc/sudoers.d/90-wsl-default-user chmod 0440 /etc/sudoers.d/90-wsl-default-user @@ -425,8 +746,9 @@ wsl.exe --list --running wsl.exe -d '' --exec id -un ``` -Proof: `visudo` prints `parsed OK`; `` is absent from `--list --running`; `id -un` prints ``. Then repeat -W5's checks from `id -u` on, with three differences: +Proof: `visudo` prints `parsed OK`; `` is absent from `--list --running`; `id -un` prints ``. Repeat W5's +cgroup-isolation proof and its paired record while both distributions run, before W7 or F1, with their same failure +recovery. Then repeat W5's checks from `id -u` on, with three differences: - `cloud-init status --long` reports `boot_status_code: disabled-by-marker-file` because the root block wrote the marker. - `result.json` and `status.json` exist only if cloud-init ran on the import (open question 1). Record their contents, or their absence, instead of treating absence as a failure. @@ -446,7 +768,7 @@ file created will be 0:0 until the next wsl --shutdown", a command this page nev complete. And this only recovers after a distro termination." It ships first in WSL 2.9.8, a pre-release, and in 3.0.1; tags 2.7.13 and 2.7.14 lack it. So this step terminates `` once, relaunches it for the F steps and reads the owner of an empty file created from Windows under the new user's home. Whether a terminate clears the state has not been -observed anywhere; this probe is the first observation. +observed on WSL 3.0.1 on this host; the 2.7.13 rehearsal returned `1000:1000`, and this run must repeat the probe. ```powershell $env:WSL_UTF8 = '1' @@ -471,12 +793,40 @@ Run F1 to F8 as `` in ``, except F2's idle observation, which ru ```sh systemctl is-system-running --wait -systemctl --failed --no-legend +systemctl --failed --no-legend --plain +journalctl -b 0 -t systemd-binfmt --no-pager -n 4 systemctl list-unit-files --type=service --no-pager ``` -Proof: `running`, no failed unit, and the service list prints. Ubuntu's own setup tests require `running` for a new -instance. On `degraded`, record `systemctl --failed` and stop for review. +Proof: `running` with no failed unit, or `degraded` when `systemctl --failed --no-legend --plain` lists exactly +`systemd-binfmt.service` and that unit's log, from the `journalctl` line, holds +`Failed to flush binfmt_misc rules, ignoring: Read-only file system`; +the service list prints. Ubuntu's own setup tests require `running` for a new instance, so `degraded` passes only in +that one case. Any other failed unit stops the run: record `systemctl --failed` and stop for review. `` reads +that log without `sudo` because it is in `adm` (the user-data's `groups`, and W6's `useradd --groups`), which systemd's +tmpfiles rules give read access to the system journal. On `degraded`, if the `journalctl` line prints only +`Hint: You are currently not seeing messages from other users and the system.` or `-- No entries --`, the user cannot +read the journal: repeat that line once with `sudo`, record both outputs and judge the second. +`getty@tty1.service` must not appear in `systemctl --failed` here. + +```sh +sudo journalctl -b 0 -t systemd-binfmt --no-pager -n 4 +``` + +The log line is selected by identifier (`-t systemd-binfmt`), not by unit: on the updated host +`journalctl -u systemd-binfmt.service` returned only systemd's four lines about the unit, because the unit's own +early-boot message is not attributed to it, while the identifier query returned the message. + +The exception has a reason. On the adopted release WSL mounts `/proc/sys/fs/binfmt_misc/status` read-only, so that one +distribution cannot flush the VM-wide registrations ([PR #40621](https://github.com/microsoft/WSL/pull/40621)). It does +so when the distribution's `[boot] protectBinfmt` setting is on, which is the default, and as a best-effort step whose +failures are only logged (microsoft/WSL `3.0.1`: `src/linux/init/init.cpp` L2433 and L2916-L2923, +`WslDistributionConfig.h` L27 and L62). Under that condition `systemd-binfmt.service` fails with that message and exit +1; that is what every boot of the workstation and of the rehearsal distributions showed on 2026-10-02, not a guarantee +for every configuration. A distribution that turns the setting off, or where the lock did not take, may print +`running`, which F1 accepts as well. Upstream calls the error benign +([issue #41226](https://github.com/microsoft/WSL/issues/41226), a contributor's answer of 2026-08-04: "The systemd error +is benign and won't affect registration of user defined binfmt settings."). ### F2. Linger @@ -516,6 +866,23 @@ Proof: ` directory`, then ` socket` (a symbolic link fails), `systemd --user` bus that the guarded runners require ([`adoption/tools/README.md`](../tools/README.md) and [`adoption/lifecycle.md`](../lifecycle.md)). +Right after the bus proof, check that Docker Engine / Moby starts a rootless container as ``, without sudo. +This check is owed on the first run after the WSL update: [microsoft/WSL#41492](https://github.com/microsoft/WSL/issues/41492) +reports that rootless Docker and Podman cannot start a container after the cgroup hierarchy moved, and remains open. +No new distribution has been observed on 3.0.1. Use Docker's +[supported rootless setup](https://docs.docker.com/engine/security/rootless/) and its `rootless` context. +If the engine is not installed yet, record `owed`, complete F4 and F5 and that setup, then return here before F9 or +accepting R1. An owed check is not a pass. + +```sh +docker --context rootless info --format '{{json .SecurityOptions}}' +docker --context rootless run --rm hello-world +``` + +Proof: the first command exits 0 and includes `name=rootless` before the run command is attempted; the second exits +0 and prints `Hello from Docker!`. Record both outputs. A missing rootless mode, a failed container start or a +cgroup error stops the run; retain the failure with issue 41492 and do not accept the engine from its version alone. + ### F4. Packages Changed after `v2026.09.26.2`: `adoption/bootstrap-linux.sh` installs `ca-certificates curl git tar gzip xz-utils jq` and @@ -530,7 +897,8 @@ sudo apt-get install -y --no-install-recommends ca-certificates curl git tar gzi dpkg-query -W -f='${Package} ${Version}\n' jq libatomic1 uidmap ``` -Proof: both apt commands exit 0, and `dpkg-query` prints a version for each of the three packages, which the image lacks. +Proof: both apt commands exit 0, and `dpkg-query` prints a version for each of the three packages. The historical +24.04.5 image lacked them; do not assume the selected 26.04.1 image has the same package gaps. ### F5. Subordinate ids @@ -540,11 +908,13 @@ one only when the first `grep` prints nothing. ```sh grep "^$(id -un):" /etc/subuid /etc/subgid -sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(id -un)" +grep -q "^$(id -un):" /etc/subuid || sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(id -un)" grep "^$(id -un):" /etc/subuid /etc/subgid ``` Proof: `/etc/subuid::100000:65536` and `/etc/subgid::100000:65536`, or another range of 65,536. +The second line adds a range only when `/etc/subuid` has none for the user, so running the block twice never adds a +second range (run 3: `useradd` had allocated the range, and `usermod` did not run). Record whether the range came from `useradd` or from `usermod`. 65,536 stays the stage-1 value. Docker's @@ -626,15 +996,17 @@ Proof: as in [`adoption/bootstrap.md`](../bootstrap.md); stage 2 records its own After stage 2, install the profile example for `` as step 5 of [Windows Terminal profiles and the login shell](linux-wsl2.md#windows-terminal-profiles-and-the-login-shell) describes: -`` is ``, `` the new user and `` `/home//code/native-agent-stack`. Rename the -three profiles, for example to ` - Shell`, ` - Codex` and ` - Claude`, and save the file as +`` is ``, `` the new user and `` `/home//code/native-agent-stack`. Rename every +profile of the example by replacing its `WSL` prefix with ``, for example ` - Shell`, ` - Codex` and +` - Claude` (and the resume profiles when the example carries them), and save the file as `.json` in the same Fragments folder. Windows Terminal derives a fragment profile's identity from the folder name and the profile name, so the workstation's identically named profiles would collide with these. Then prove the profile -launch shape from Windows: +launch shape from Windows (the loop tests each path inside `[[ ]]`, which needs no quotes to keep a path with a space +whole; see R1 for why these commands carry no double quote): ```powershell wsl.exe -d '' -u '' --exec /bin/bash -lc 'type -P claude codex' -wsl.exe -d '' -u '' --exec /bin/bash -lc 'for p in $(type -P claude codex); do test -f $p && test -x $p && echo executable: $p; done' +wsl.exe -d '' -u '' --exec /bin/bash -lc 'for n in claude codex; do p=$(type -P $n); [[ -f $p && -x $p ]] && echo executable: $p; done' ``` Proof: the first line prints two absolute paths, and the second prints `executable:` followed by each of them. @@ -711,9 +1083,18 @@ transcript and the F outputs: - the default distribution before and after; - `creation_path` (`A` or `B`), with the W5 markers when B was taken; - on path B, `failed_attempt_export`: the file name, SHA-256 and size of W6's export of the failed attempt; + - W1's `workstation_baseline`, W5's common-unit process ids and active user manager and its `paired_isolation`, any + `w5_failure_export` with its `cause`, each removal's interop check and the records of any stop for review, + F1's outcome (`running`, or `degraded` with its two proving outputs) and F3's rootless-container result or `owed`; - the clone's commit, the subordinate-id outcome and F11's `jcodemunch_registration`; - the `rehearsal` block of R1, the `pre_checks` of P1 and P2, the `storage_errors` of P3 and W5, W1's `idle_keys`, W5's `schema_system`, W7's `ownership_probe` and F2's `idle_observation`. + - all five workstation values equal to `host.workstation_baseline`; its optional `getty_tty1_result` is present only + when W1's extra command ran; and `getty_mask` under `first_launch`, with `is_enabled`, `load_state`, `active_state` + and `n_restarts` from W5's two proof commands. + - the selected release and actual image size, with the two fixed `supported_images` pins and both `comparison_arms`. + The checked-in example is a synthetic fixture with unrun comparison arms; fill actual results from the native logs + and replace the synthetic fixture labels in a contributed receipt only with the operations it actually observed. - **Contribute.** On a branch of current `main`, copy it to `evidence/receipts/wsl-new-distro-stage1--.json` and add a `receipts[]` row to `manifests/evidence.json` with the same `id`, `kind`, `component_ids`, `claim` and `limitations` and its `path`. Its claim quotes only that run's output. Follow @@ -728,12 +1109,16 @@ Kept open in the record, each with the observation that would settle it: - what keeps `` running with no client attached: with `instanceIdleTimeout=-1` the setting does, by WSL's source, and issue reports say linger alone does not (F2 records it; whether `wsl.exe --list --running` counts as a client is not verified); -- whether binfmt registrations survive `wsl --terminate` (WSL's `protectBinfmt`); -- whether `useradd` allocated the subordinate ids on 24.04.5 (F5 records it); +- whether a rootless container starts in the new distribution on the updated host (F3; microsoft/WSL#41492 remains + open); run 2 observed cgroup isolation with both distributions running, and each run must repeat W5's paired record; + a binfmt loss after unregister has R1's required check and its stop-for-review recovery; +- whether `useradd` allocated the subordinate ids on the selected release (F5 records it); - whether a terminate clears the 0:0 file owner of microsoft/WSL#40941 (W7 records it); - whether `hv_storvsc` errors break a first launch on this kernel (microsoft/WSL#41482; P3 records a baseline and stops on an error line less than one hour old, and W5 counts again after the first launch); -- the bounded comparison against Ubuntu 26.04.1 LTS, WSL's current default `Ubuntu`. +- completion of the preregistered comparison of Ubuntu 26.04.1 default and 24.04.5 rollback on throwaway names; + the default's run 3 passed P1 to F5 on 3.0.1 with the corrected template; stage 2, its two probes and the rollback + arm are owed. ## Boundaries diff --git a/adoption/templates/wsl/cloud-init.user-data.template b/adoption/templates/wsl/cloud-init.user-data.template index 838c9d366..cf5ac78e7 100644 --- a/adoption/templates/wsl/cloud-init.user-data.template +++ b/adoption/templates/wsl/cloud-init.user-data.template @@ -1,5 +1,5 @@ #cloud-config -# User-data for a new Ubuntu 24.04.5 WSL instance: adoption/platforms/linux-wsl2-new-distro.md, step W3, renders this +# User-data for Ubuntu 26.04.1 default or 24.04.5 rollback WSL image: adoption/platforms/linux-wsl2-new-distro.md, step W3, renders this # file to %USERPROFILE%\.cloud-init\.user-data, where is the --name of the install, before the instance # first boots. The cloud-init WSL datasource reads that file at the first boot; the image's /usr/lib/wsl/wsl-setup then # waits for cloud-init, reuses this user (the first one with uid 1000 or more), keeps the [user] default written below @@ -8,6 +8,7 @@ # Keys follow Canonical's "Automatic setup of Ubuntu on WSL with cloud-init" example and the cloud-init WSL datasource # "Default user" example. The groups are the ones the image's wsl-setup gives a user it creates interactively. uid 1000 # matches defaultUid in the image's /etc/wsl-distribution.conf. The password stays locked; sudo needs none. +# P2 checks the workstation schema; W5 must validate the selected image's own schema. Full new-host acceptance is owed. users: - name: ${WSL_USER} uid: 1000 @@ -22,3 +23,8 @@ write_files: content: | [user] default=${WSL_USER} + +# /dev/tty1 is shared across distributions in the WSL VM. Mask its getty in cloud-init's network stage, +# before systemd starts the unit. The source review, ordering and proof are in the page's W5. +bootcmd: +- [systemctl, mask, --now, getty@tty1.service] diff --git a/adoption/templates/wsl/first-boot-checklist.md b/adoption/templates/wsl/first-boot-checklist.md index 51c161835..22669820e 100644 --- a/adoption/templates/wsl/first-boot-checklist.md +++ b/adoption/templates/wsl/first-boot-checklist.md @@ -1,41 +1,72 @@ # Stage-1 and first-boot checklist Copy this list into the private run log, not into the checkout, and tick a step only when its proof in -[the recipe](../../platforms/linux-wsl2-new-distro.md) holds. The commands live in the recipe only; this list names the -pass condition each step must show. A step that shows anything else stops the run. W6 runs only after the W5 marker. -The ticked list and the transcript feed [the receipt example](stage1-receipt.example.json). Tick a copy twice: once for -the rehearsal on a throwaway name, through F3 and then R1, and once for the real run, which skips R1. +[the recipe](../../platforms/linux-wsl2-new-distro.md) holds. The recipe gives every command; this list repeats the five +baseline and paired observations so each has its own record. A step that shows anything else stops the run. W6 runs only after the W5 marker. +The ticked list and transcript feed the synthetic [receipt example](stage1-receipt.example.json). Tick a separate copy +for each image arm's throwaway rehearsal, through F3 and the R1 comparisons, then for the real run, which skips R1. +Ubuntu 26.04.1 LTS (Canonical WSL image) is the single default for the clean install. Ubuntu 24.04.5 is the named +rollback, used only on a release-caused 26.04 failure with no in-release remedy. The 26.04.1 rehearsal stopped at F1 +on WSL 2.7.13. Run 2 on WSL 3.0.1.0 (2026-10-02) passed stage 1 and W5's path-A proofs and observed distinct cgroup +namespaces, then stopped at the paired rule on the console collision. Probe E1 tested the getty mask outside the +recipe; a complete run with it through F3 and full acceptance remain owed. -## Rehearsal (once, before the real run) +| Release | Selected image | Exact SHA-256 | +| --- | --- | --- | +| 26.04.1 default | `ubuntu-26.04.1-wsl-amd64.wsl` | `48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104` | +| 24.04.5 rollback | `ubuntu-24.04.5-wsl-amd64.wsl` | `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e` | -- [ ] **R1** The page ran on a throwaway name and its own folder through F3, with P3's baseline and W5's second storage count, W5's `cloud-init schema --system`, W7's owner and F2's idle observation recorded in the receipt's `rehearsal` block; then that name alone was terminated and unregistered (a failed rehearsal exported first, with exit 0 and its SHA-256 and size recorded); the last `--list --verbose` no longer lists it and the starred line is unchanged. +## Rehearsals (both image arms, before the real run) + +- [ ] **R1** Each image arm ran on its own throwaway name and its own folder through F3, with P3's baseline and W5's second storage count, W5's `cloud-init schema --system`, W7's owner and F2's idle observation recorded in the receipt's `rehearsal` block; then that name alone was terminated and unregistered (a failed rehearsal exported first, with exit 0 and its SHA-256 and size recorded); after each unregister, the surviving distribution's `WSLInterop` registration exists and a Windows executable launches, and when either check fails R1's records (`ls /proc/sys/fs/binfmt_misc` and `systemctl status systemd-binfmt.service --no-pager`) are kept and the run stops for review, importing or provisioning nothing; the last `--list --verbose` no longer lists it and the starred line is unchanged. A name already removed by W5 is not removed again. Both arms have the preregistered criteria with the amendments of 2026-10-02 made before any comparison ran ([decision record](../../../docs/decisions/2026-10-01-new-wsl-distro-recipe.md)): first boot with F1's pass condition, schema, systemd user from the second instance, WSL GPU visibility, uv-managed CPython 3.13 and Node 24.21.0, recorded in `comparison_arms`; failures and skips stay explicit, and F3's rootless check must pass before acceptance. The real install uses the single default unless the release-caused rollback condition holds. ## Workstation distribution (sh), before stage 1 -- [ ] **P1** `gpgv` exits 0 with `Good signature` by `843938DF228D22F7B3742BC0D94AA3F0EFE21092`, with no key import; the signed `SHA256SUMS` line of the image carries the hash W2 pins. -- [ ] **P2** `cloud-init schema -c` prints `Valid schema` for the render of `` and exits 0; the cloud-init version and the render's SHA-256 are recorded. -- [ ] **P3** The `hv_storvsc` count without the registration line is recorded as the baseline; the newest error line, by its kernel time against `/proc/uptime`, is at least one hour old or absent (younger: errors are current, the run stops; microsoft/WSL#41482); `swapon --show` is recorded. +- [ ] **P1** `gpgv` exits 0 with `Good signature` by `843938DF228D22F7B3742BC0D94AA3F0EFE21092`, with no key import; both 26.04.1 and 24.04.5 signed `SHA256SUMS` lines carry the exact release hashes above. +- [ ] **P2** `cloud-init schema -c` prints `Valid schema` for the render of `` and exits 0; the workstation cloud-init version and render's SHA-256 are recorded for the selected image. A host schema check is not image qualification; W5 must record and validate the selected packaged version on path A. +- [ ] **P3** The `hv_storvsc` count without the registration line or the kernel's command-line echo is recorded as the baseline; the newest error line, by its kernel time against `/proc/uptime`, is at least one hour old or absent (younger: errors are current, the run stops; microsoft/WSL#41482), unless its kernel time is a line an earlier run of the page recorded as its own attachment-window line, which is noted and does not stop the run; `swapon --show` is recorded. ## Windows host (PowerShell) -- [ ] **W1** `Z:\WSL\downloads` exists and the transcript is open; WSL is 2.4.10 or later; the default (starred) distribution is recorded; `` is unregistered; `Z:\WSL\` does not exist; no Landscape user-data exists for ``; no `agent.yaml` exists, or its recorded top-level keys include neither `users` nor `write_files`; the `.wslconfig` read shows `instanceIdleTimeout` and `vmIdleTimeout` or their absence, recorded as `idle_keys`; free space on `Z:` is recorded. -- [ ] **W2** The computed, Canonical-published and DistributionInfo.json sha256 are all `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e`; no `throw`. +- [ ] **W1** `Z:\WSL\downloads` exists and the transcript is open; `wsl.exe --version` records WSL 3.0.1 or later before installing or importing a second systemd distribution (PR #40519 and PR #41512); an older version stops before W4 or W6; the workstation's baseline is recorded from the already-running distribution without `sudo`, with the five commands below each on its own line and with its own receipt entry and exit. Its uid equals the new default uid `1000`; its system state is `running` with no failed unit, or `degraded` with its failed set contained in {`systemd-binfmt.service`, `getty@tty1.service`}; its user manager is `active`; its namespace is not `cgroup:[4026531835]` (`PROC_CGROUP_INIT_INO`, the initial namespace). Anything else stops before W4 and before any W6 import. A failed getty is allowed only as an earlier console collision's leftover: `systemctl show getty@tty1.service -p Result -p NRestarts` is then recorded as optional `getty_tty1_result` and must show `Result=start-limit-hit`, or the run stops. The default (starred) distribution is recorded; `` is unregistered; `Z:\WSL\` does not exist; no Landscape user-data exists for ``; no `agent.yaml` exists, or its recorded top-level keys include neither `users` nor `write_files`; the `.wslconfig` read shows `instanceIdleTimeout` and `vmIdleTimeout` or their absence, recorded as `idle_keys`; free space on `Z:` is recorded. The older 2.4.10 install-only minimum applies to a single systemd distribution with no second systemd distribution running. + +```sh +id -u +systemctl is-system-running +systemctl --failed --no-legend --plain +systemctl is-active "user@$(id -u).service" +readlink /proc/self/ns/cgroup +``` +- [ ] **W2** Explicit `` is one of the two controlled arms; download URL, filename and catalog entry match that release. The actual whole-image, signed Canonical publication and pinned DistributionInfo.json SHA-256 all equal that release's exact hash above; actual file size recorded (run 2 observed 418,495,746 bytes for 26.04.1); no `throw`. - [ ] **W3** `%USERPROFILE%\.cloud-init\.user-data` exists, starts with `#cloud-config`, names `` twice and contains no `${`; its SHA-256 equals P2's. -- [ ] **W4** Install exit 0; `Distribution successfully installed`; `` is `Stopped` at version 2; the starred line is unchanged. -- [ ] **W5** First launch exit 0, without the prompt marker; `id -un` is `` and `id -u` is `1000`; `cloud-init status --long` prints `status: disabled` with `boot_status_code: disabled-by-marker-file`; `/var/lib/cloud/data/result.json` names `DataSourceWSL` with `"errors": []`; `/var/lib/cloud/data/status.json` shows the four stages finished without errors; `cloud-init schema --system` prints `Valid schema user-data` and exits 0; `/etc/wsl.conf` names the user once; `/etc/cloud/cloud-init.disabled` exists; sudo is NOPASSWD; the starred line is unchanged. On both paths, the second storage count equals P3's baseline; a larger count is recorded with the new lines, a failed W5 counts as exposure to microsoft/WSL#41482, and nothing continues to stage 2 until that is decided. -- [ ] **W6** Only after the W5 marker: re-listed before `--unregister`; before `--unregister`, `` terminated and exported to `Z:\WSL\downloads\-failed.tar` with exit 0, and its SHA-256 and size recorded in `failed_attempt_export`; import exit 0; `cloud-init status` recorded; `visudo` `parsed OK`; after `--terminate`, `id -un` is ``; W5's checks pass, with `result.json` and `status.json` checked only if cloud-init ran on the import (otherwise their absence is recorded) and `cloud-init schema --system` skipped; `creation_path` is `B`. +- [ ] **W4** The selected `ubuntu--wsl-amd64.wsl` installs with exit 0; `Distribution successfully installed`; `` is `Stopped` at version 2; the starred line is unchanged. +- [ ] **W5** First launch exit 0, without the prompt marker; `wsl: Failed to start the systemd user session` stops even with exit 0; `id -un` is `` and `id -u` is `1000`; getty `is-enabled` prints `masked` (normal exit 1), and `show` prints `LoadState=masked`, `ActiveState=inactive`, `NRestarts=0`, recorded as `getty_mask`; selected-image `cloud-init --version` recorded; `cloud-init status --long` prints `status: disabled` with `boot_status_code: disabled-by-marker-file` (with the template's `bootcmd` it exits 2 on one expected recoverable error, `Failed to wait for network`, because WSL masks `systemd-networkd-wait-online.service`; any other recoverable error is recorded and reviewed); `/var/lib/cloud/data/result.json` names `DataSourceWSL` with `"errors": []`; `/var/lib/cloud/data/status.json` shows the four stages finished without errors; `cloud-init schema --system` prints `Valid schema user-data` and exits 0; `/etc/wsl.conf` names the user once; `/etc/cloud/cloud-init.disabled` exists; sudo is NOPASSWD; the starred line is unchanged. After the first launch, while both distributions run and before F1, the already-running distribution reads a system unit active in both: its `cgroup.procs` has local process ids and no `0` entry, and the new distribution's `user@.service` is active. An empty or unreadable file is inconclusive and stops. The paired block below records each of the five commands on each side in its own receipt entry with its own exit, in `paired_isolation`: the same uid, both user managers `active`, the new distribution `running` with no failed unit or `degraded` with `systemd-binfmt.service` as its only failed unit (message proved at F1), two `cgroup:[...]` values that differ from each other and are not `cgroup:[4026531835]`, and all five workstation values equal to its W1 baseline. A getty failed in the new distribution or newly failed in the workstation fails the proof. Any failed W5 proof terminates only the new distribution, never stops or restarts a unit in either, exports `-w5-failed.tar` with the exit guard and its cause (`cgroup`, `tty` or a short text) in `w5_failure_export`, unregisters and checks surviving `WSLInterop` and a Windows executable, with R1's records and stop for review. Do not take path B for any such failure. On both paths, record P3's baseline before the import and `second_count` after the first launch. An increase confined to the window in which the new disk is attached, before cloud-init starts, is recorded with the new lines and does not stop the run. Any storage error after that window, or any provisioning step that fails with a storage cause, stops the run. An unknown attachment window stops for review; storage failures retain microsoft/WSL#41482 and their own cause. + +```sh +id -u +systemctl is-system-running +systemctl --failed --no-legend --plain +systemctl is-active "user@$(id -u).service" +readlink /proc/self/ns/cgroup +/mnt/c/Windows/System32/wsl.exe -d '' --exec id -u +/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-system-running +/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl --failed --no-legend --plain +/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active "user@$(id -u).service"' +/mnt/c/Windows/System32/wsl.exe -d '' --exec readlink /proc/self/ns/cgroup +``` +- [ ] **W6** Only after the W5 OOBE marker, never for another failed W5 proof: re-listed before `--unregister`; before `--unregister`, `` terminated and exported to `Z:\WSL\downloads\-failed.tar` with exit 0, and its SHA-256 and size recorded in `failed_attempt_export`; immediately after unregister, the surviving distribution's `WSLInterop` exists and a Windows executable launches before any import, and when either check fails R1's records are kept and the run stops for review with no import; W1's version precondition holds; the same selected image imports with exit 0; `cloud-init status` recorded; the root block starts with `systemctl mask --now getty@tty1.service`, after the import's first boot has already started the getty; `visudo` `parsed OK`; after `--terminate`, `id -un` is ``; W5's cgroup proof, paired record and checks pass before W7 or F1, or a workstation value differing from its baseline stops and uses W5's recovery, with `result.json` and `status.json` checked only if cloud-init ran on the import (otherwise their absence is recorded) and `cloud-init schema --system` skipped; `creation_path` is `B`. - [ ] **W7** Both paths: `` terminated once and absent from the running list, then relaunched with `id -un` printing ``; the file created from Windows under the user's home reads `1000:1000` and is deleted; `0:0` is recorded and stops every write from Windows into the distribution. ## Inside the new distribution (as ``) -- [ ] **F1** `systemctl is-system-running --wait` prints `running`; no failed unit. +- [ ] **F1** `systemctl is-system-running --wait` prints `running` with no failed unit, or `degraded` when `systemctl --failed --no-legend --plain` lists exactly `systemd-binfmt.service` and its log, from `journalctl -b 0 -t systemd-binfmt --no-pager -n 4`, holds `Failed to flush binfmt_misc rules, ignoring: Read-only file system` (microsoft/WSL#40621 and #41226); any other failed unit stops for review. `getty@tty1.service` must not appear there. - [ ] **F2** `Linger` is `yes`; with no client attached, `` is in every list of the two minutes of polls, or the time it left is recorded beside W1's idle keys. -- [ ] **F3** `/run/user/` is a directory and its `bus` a socket, both owned by ``; the user manager is `running`. +- [ ] **F3** `/run/user/` is a directory and its `bus` a socket, both owned by ``; the user manager is `running`. Right afterwards, as that user without sudo, Docker's `rootless` context reports `name=rootless` and `docker --context rootless run --rm hello-world` exits 0 with `Hello from Docker!`; a failed start or missing rootless mode stops with microsoft/WSL#41492. This check is owed on the first run after the WSL update; if the engine is absent, record `owed`, complete F4, F5 and Docker's supported rootless setup, then return here before F9 or accepting R1. - [ ] **F4** apt exits 0; `jq`, `libatomic1` and `uidmap` print versions. - [ ] **F5** `/etc/subuid` and `/etc/subgid` hold 65,536 ids for ``; whether `useradd` or `usermod` wrote them is recorded. - [ ] **F6** `~/.bash_profile` is exactly the hand-off line. - [ ] **F7** The clone's `HEAD` equals `origin` `main`; the commit is recorded. - [ ] **F8** `ss` shows no listener on the four ports; the host file parses with the nine keys and is git-ignored. - [ ] **F9** Stage 2 runs from this clone with `--profile `; its own receipts cover it. -- [ ] **F10** The fragment profiles carry `` in their names; `type -P claude codex` prints two absolute paths, and the per-path `test -f` and `test -x` line prints `executable:` for both. +- [ ] **F10** The fragment profiles carry `` in their names; `type -P claude codex` prints two absolute paths, and the per-path `[[ -f ... && -x ... ]]` line prints `executable:` for both (no double quote inside a command that PowerShell hands to `wsl.exe`). - [ ] **F11** In a login shell, after F9 and F10: when `test -x` finds `jcodemunch-mcp`, `claude mcp add` run from the clone's root prints `Added ...` and `claude mcp get jcodemunch` names the local scope and the command; when it does not, `not installed` is recorded, and a step not run is recorded as `skipped` with the reason. `jcodemunch_registration` holds the outcome. diff --git a/adoption/templates/wsl/stage1-receipt.example.json b/adoption/templates/wsl/stage1-receipt.example.json index 205a294c4..b53911641 100644 --- a/adoption/templates/wsl/stage1-receipt.example.json +++ b/adoption/templates/wsl/stage1-receipt.example.json @@ -1,17 +1,19 @@ { "schema_version": 1, + "artifact_class": "synthetic_fixture", "id": "wsl-new-distro-stage1--", "kind": "native_cli_e2e", "component_ids": [ "systemd" ], - "claim": " (uid 1000) as the default user, W7 read 1000:1000, and F1-F3 printed running, Linger yes and a user-owned bus socket.>", + "claim": " (uid 1000) as the default user, W7 read 1000:1000, W5's paired record showed the same uid, distinct namespaces and two active user managers, and F1 met F1's pass condition, F2 printed Linger yes and a user-owned bus socket.>", "limitations": [ "One run on one Windows host: a new distribution on the existing WSL kernel and Windows installation, not a new physical machine.", "Stage 1 and first boot only: stage 2 (the bootstrap), native sign-in, services and the Windows Terminal PATH proof are not covered.", "Sanitized: Windows and Linux user names, home and profile paths and the checkout path are replaced by placeholders; the raw transcript stays private on the host.", "jcodemunch_registration records F11's state only: a registration is not evidence that a session or subagent uses jCodeMunch.", - "The rehearsal block records a separate run on a throwaway name, removed by R1; its observations sit beside this run's, not in place of them." + "The rehearsal block records a separate run on a throwaway name, removed by R1; its observations sit beside this run's, not in place of them.", + "Synthetic fixture only: placeholders and unrun comparison arms are not upstream tests, new-host acceptance or fresh model/GPU runs." ], "recorded_at_utc": "", "recipe": { @@ -19,6 +21,22 @@ "catalog_revision": "", "creation_path": "" }, + "supported_images": { + "26.04.1": { + "file": "ubuntu-26.04.1-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl", + "sha256": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "catalog_name": "Ubuntu-26.04", + "bytes": 418495746 + }, + "24.04.5": { + "file": "ubuntu-24.04.5-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl", + "sha256": "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e", + "catalog_name": "Ubuntu-24.04", + "bytes": 388975696 + } + }, "rehearsal": { "name": " or the workstation's>", "result": "", @@ -34,15 +52,15 @@ } }, "pre_checks": { - "signed_sums": "", - "user_data_schema": "" + "signed_sums": "", + "user_data_schema": "" }, "storage_errors": { - "baseline": "", + "baseline": "", "newest_line_time": "", "swap": "", - "second_count": "", - "new_lines": "" + "second_count": "", + "new_lines": "" }, "host": { "wsl_version": [ @@ -51,20 +69,55 @@ "default_distribution_before": "", "default_distribution_after": "", "z_free_bytes": "", - "idle_keys": "" + "idle_keys": "", + "workstation_baseline": { + "uid": "", + "system_state": "", + "failed_units": "", + "user_manager": ".service; active>", + "cgroup_namespace": "", + "getty_tty1_result": "" + } }, "image": { - "file": "ubuntu-24.04.5-wsl-amd64.wsl", - "url": "https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl", - "bytes": 388975696, - "sha256_published": "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e", - "sha256_distributioninfo": "", - "sha256_computed": "" + "release": "", + "file": "ubuntu--wsl-amd64.wsl", + "url": "https://releases.ubuntu.com//ubuntu--wsl-amd64.wsl", + "bytes": "", + "sha256_published": "", + "sha256_distributioninfo": "", + "sha256_computed": "" }, "first_launch": { "user_prompt_marker": "", "oobe_failed_marker": "", - "schema_system": "" + "schema_system": "", + "cloud_init_version": "", + "cgroup_isolation": ".service; the paired five-value record is paired_isolation; any failed W5 proof uses the guarded w5_failure_export/removal>", + "systemd_user_session_warning": "", + "getty_mask": { + "is_enabled": "", + "load_state": "", + "active_state": "", + "n_restarts": "" + } + }, + "paired_isolation": { + "recorded_with": "", + "workstation": { + "uid": "", + "system_state": "", + "failed_units": "", + "user_manager": ".service; active; equal to host.workstation_baseline>", + "cgroup_namespace": "" + }, + "new_distribution": { + "uid": " through wsl.exe; equal to the workstation's>", + "system_state": "", + "failed_units": ", or none; getty@tty1.service must not be failed>", + "user_manager": ".service inside ; active>", + "cgroup_namespace": "; differs from the workstation's and is not cgroup:[4026531835]>" + } }, "ownership_probe": "", "idle_observation": " was in every list, or the time of the first list without it>", @@ -76,6 +129,48 @@ "bytes": "" }, "steps": [ + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec bash -lc 'stat -c %U,%F /run/user/$(id -u) /run/user/$(id -u)/bus'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec systemctl --user is-system-running --wait", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec test -c /dev/dxg", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec /usr/lib/wsl/lib/nvidia-smi", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec bash -lc 'python3 --version'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec bash -lc 'uv run --no-project --python 3.13 python --version'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec bash -lc 'node --version'", + "exit": "", + "output_excerpt": "" + }, { "step": "R1", "cmd": "$env:WSL_UTF8 = '1'", @@ -100,6 +195,18 @@ "exit": "", "output_excerpt": "" }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }", + "exit": "", + "output_excerpt": "" + }, { "step": "R1", "cmd": "wsl.exe --list --verbose", @@ -160,12 +267,48 @@ "exit": "", "output_excerpt": "" }, + { + "step": "R1", + "cmd": "wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }", + "exit": "", + "output_excerpt": "" + }, { "step": "R1", "cmd": "wsl.exe --list --verbose", "exit": "", "output_excerpt": "" }, + { + "step": "R1", + "cmd": "test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "ls /proc/sys/fs/binfmt_misc", + "exit": "", + "output_excerpt": "" + }, + { + "step": "R1", + "cmd": "systemctl status systemd-binfmt.service --no-pager", + "exit": "", + "output_excerpt": "" + }, + { + "step": "P1", + "cmd": "for RELEASE in 26.04.1 24.04.5; do", + "exit": "", + "output_excerpt": "" + }, { "step": "P1", "cmd": "SUMS_DIR=\"$(mktemp -d)\"", @@ -174,15 +317,15 @@ }, { "step": "P1", - "cmd": "curl -fsSL -o \"$SUMS_DIR/SHA256SUMS\" https://releases.ubuntu.com/24.04.5/SHA256SUMS", - "exit": "", - "output_excerpt": "" + "cmd": "curl -fsSL -o \"$SUMS_DIR/SHA256SUMS\" \"https://releases.ubuntu.com/$RELEASE/SHA256SUMS\" || exit 1", + "exit": "", + "output_excerpt": "" }, { "step": "P1", - "cmd": "curl -fsSL -o \"$SUMS_DIR/SHA256SUMS.gpg\" https://releases.ubuntu.com/24.04.5/SHA256SUMS.gpg", - "exit": "", - "output_excerpt": "" + "cmd": "curl -fsSL -o \"$SUMS_DIR/SHA256SUMS.gpg\" \"https://releases.ubuntu.com/$RELEASE/SHA256SUMS.gpg\" || exit 1", + "exit": "", + "output_excerpt": "" }, { "step": "P1", @@ -192,9 +335,21 @@ }, { "step": "P1", - "cmd": "grep ' \\*ubuntu-24\\.04\\.5-wsl-amd64\\.wsl$' \"$SUMS_DIR/SHA256SUMS\"", - "exit": "", - "output_excerpt": "" + "cmd": "if [ \"$?\" -ne 0 ]; then exit 1; fi", + "exit": "", + "output_excerpt": "" + }, + { + "step": "P1", + "cmd": "grep -F \" *ubuntu-$RELEASE-wsl-amd64.wsl\" \"$SUMS_DIR/SHA256SUMS\" || exit 1", + "exit": "", + "output_excerpt": "" + }, + { + "step": "P1", + "cmd": "done", + "exit": "", + "output_excerpt": "" }, { "step": "P2", @@ -228,13 +383,13 @@ }, { "step": "P3", - "cmd": "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc'", + "cmd": "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -Evc 'registering driver hv_storvsc|[Cc]ommand line:'", "exit": "<0, or 1 when the count is 0>", "output_excerpt": "" }, { "step": "P3", - "cmd": "sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | grep -v 'registering driver hv_storvsc' | tail -n 1", + "cmd": "sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | grep -Ev 'registering driver hv_storvsc|[Cc]ommand line:' | tail -n 1", "exit": "", "output_excerpt": "" }, @@ -316,6 +471,42 @@ "exit": "", "output_excerpt": "" }, + { + "step": "W1", + "cmd": "id -u", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W1", + "cmd": "systemctl is-system-running", + "exit": "<0 for running; 1 for degraded, expected and recorded>", + "output_excerpt": "" + }, + { + "step": "W1", + "cmd": "systemctl --failed --no-legend --plain", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W1", + "cmd": "systemctl is-active \"user@$(id -u).service\"", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W1", + "cmd": "readlink /proc/self/ns/cgroup", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W1", + "cmd": "systemctl show getty@tty1.service -p Result -p NRestarts", + "exit": "", + "output_excerpt": "" + }, { "step": "W2", "cmd": "$ProgressPreference = 'SilentlyContinue'", @@ -324,51 +515,111 @@ }, { "step": "W2", - "cmd": "Invoke-WebRequest -UseBasicParsing -Uri 'https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl' -OutFile 'Z:\\WSL\\downloads\\ubuntu-24.04.5-wsl-amd64.wsl'", - "exit": "", - "output_excerpt": "" + "cmd": "$Release = ''", + "exit": "", + "output_excerpt": "" }, { "step": "W2", - "cmd": "Invoke-WebRequest -UseBasicParsing -Uri 'https://releases.ubuntu.com/24.04.5/SHA256SUMS' -OutFile 'Z:\\WSL\\downloads\\SHA256SUMS'", - "exit": "", - "output_excerpt": "" + "cmd": "switch ($Release) {", + "exit": "", + "output_excerpt": "" }, { "step": "W2", - "cmd": "$Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e'", - "exit": "", - "output_excerpt": "" + "cmd": "'26.04.1' { $Expected = '48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104'; $CatalogName = 'Ubuntu-26.04' }", + "exit": "", + "output_excerpt": "" }, { "step": "W2", - "cmd": "$Published = (Select-String -LiteralPath 'Z:\\WSL\\downloads\\SHA256SUMS' -Pattern ' \\*ubuntu-24\\.04\\.5-wsl-amd64\\.wsl$').Line.Split(' ')[0]", - "exit": "", - "output_excerpt": "" + "cmd": "'24.04.5' { $Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e'; $CatalogName = 'Ubuntu-24.04' }", + "exit": "", + "output_excerpt": "" }, { "step": "W2", - "cmd": "$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content | ConvertFrom-Json).ModernDistributions.Ubuntu | Where-Object Name -eq 'Ubuntu-24.04'", - "exit": "", - "output_excerpt": "" + "cmd": "default { throw 'unsupported release: do not download or install' }", + "exit": "", + "output_excerpt": "" }, { "step": "W2", - "cmd": "$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\\WSL\\downloads\\ubuntu-24.04.5-wsl-amd64.wsl').Hash.ToLowerInvariant()", - "exit": "", - "output_excerpt": "" + "cmd": "}", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$Image = \"ubuntu-$Release-wsl-amd64.wsl\"", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$ImagePath = \"Z:\\WSL\\downloads\\$Image\"", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$ImageUrl = \"https://releases.ubuntu.com/$Release/$Image\"", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$SumsPath = \"Z:\\WSL\\downloads\\$Release-SHA256SUMS\"", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "Invoke-WebRequest -UseBasicParsing -Uri $ImageUrl -OutFile $ImagePath", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "Invoke-WebRequest -UseBasicParsing -Uri \"https://releases.ubuntu.com/$Release/SHA256SUMS\" -OutFile $SumsPath", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$Published = (Select-String -LiteralPath $SumsPath -Pattern (' \\*' + [regex]::Escape($Image) + '$')).Line.Split(' ')[0]", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content | ConvertFrom-Json).ModernDistributions.Ubuntu | Where-Object Name -eq $CatalogName", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $ImagePath).Hash.ToLowerInvariant()", + "exit": "", + "output_excerpt": "" }, { "step": "W2", "cmd": "\"computed $Actual published $Published listed $($Listed.Amd64Url.Sha256) url $($Listed.Amd64Url.Url)\"", "exit": "", - "output_excerpt": "" + "output_excerpt": "" }, { "step": "W2", - "cmd": "if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected) { throw 'sha256 mismatch: do not install' }", - "exit": "", - "output_excerpt": "" + "cmd": "if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected -or $Listed.Amd64Url.Url -ne $ImageUrl) { throw 'sha256 mismatch: do not install' }", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W2", + "cmd": "(Get-Item -LiteralPath $ImagePath).Length", + "exit": "", + "output_excerpt": "" }, { "step": "W3", @@ -420,9 +671,9 @@ }, { "step": "W4", - "cmd": "wsl.exe --install --from-file 'Z:\\WSL\\downloads\\ubuntu-24.04.5-wsl-amd64.wsl' --name '' --location 'Z:\\WSL\\' --no-launch", - "exit": 0, - "output_excerpt": "" + "cmd": "wsl.exe --install --from-file 'Z:\\WSL\\downloads\\ubuntu--wsl-amd64.wsl' --name '' --location 'Z:\\WSL\\' --no-launch", + "exit": "", + "output_excerpt": "" }, { "step": "W4", @@ -466,6 +717,24 @@ "exit": "", "output_excerpt": "" }, + { + "step": "W5", + "cmd": "wsl.exe -d '' --exec systemctl is-enabled getty@tty1.service", + "exit": "<1 for masked, this command's normal exit>", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe -d '' --exec systemctl show getty@tty1.service -p LoadState -p ActiveState -p NRestarts", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe -d '' -u root --exec cloud-init --version", + "exit": "", + "output_excerpt": "" + }, { "step": "W5", "cmd": "wsl.exe -d '' -u root --exec cloud-init status --long", @@ -516,7 +785,163 @@ }, { "step": "W5", - "cmd": "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc'", + "cmd": "systemctl is-active ''", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-active ''", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "cat '/sys/fs/cgroup/system.slice//cgroup.procs'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active \"user@$(id -u).service\"'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "id -u", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "systemctl is-system-running", + "exit": "<0 for running; 1 for degraded, expected and recorded>", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "systemctl --failed --no-legend --plain", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "systemctl is-active \"user@$(id -u).service\"", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "readlink /proc/self/ns/cgroup", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec id -u", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-system-running", + "exit": "<0 for running; 1 for degraded, expected and recorded>", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl --failed --no-legend --plain", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active \"user@$(id -u).service\"'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "/mnt/c/Windows/System32/wsl.exe -d '' --exec readlink /proc/self/ns/cgroup", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe --list --quiet", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe --terminate ''", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe --export '' 'Z:\\WSL\\downloads\\-w5-failed.tar'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "$LASTEXITCODE", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' }", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\\WSL\\downloads\\-w5-failed.tar').Hash.ToLowerInvariant()", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "(Get-Item -LiteralPath 'Z:\\WSL\\downloads\\-w5-failed.tar').Length", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe --unregister ''", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe --list --verbose", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -Evc 'registering driver hv_storvsc|[Cc]ommand line:'", "exit": "<0, or 1 when the count is 0>", "output_excerpt": "" }, @@ -576,9 +1001,21 @@ }, { "step": "W6", - "cmd": "wsl.exe --import '' 'Z:\\WSL\\' 'Z:\\WSL\\downloads\\ubuntu-24.04.5-wsl-amd64.wsl' --version 2", - "exit": "", - "output_excerpt": "" + "cmd": "wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W6", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }", + "exit": "", + "output_excerpt": "" + }, + { + "step": "W6", + "cmd": "wsl.exe --import '' 'Z:\\WSL\\' 'Z:\\WSL\\downloads\\ubuntu--wsl-amd64.wsl' --version 2", + "exit": "", + "output_excerpt": "" }, { "step": "W6", @@ -586,6 +1023,12 @@ "exit": "", "output_excerpt": "" }, + { + "step": "W6", + "cmd": "systemctl mask --now getty@tty1.service", + "exit": "", + "output_excerpt": "" + }, { "step": "W6", "cmd": "id -u '' || useradd --create-home --uid 1000 --groups adm,cdrom,sudo,dip,plugdev --shell /bin/bash ''", @@ -685,14 +1128,20 @@ { "step": "F1", "cmd": "systemctl is-system-running --wait", - "exit": 0, - "output_excerpt": "running" + "exit": "", + "output_excerpt": "" }, { "step": "F1", - "cmd": "systemctl --failed --no-legend", + "cmd": "systemctl --failed --no-legend --plain", "exit": "", - "output_excerpt": "" + "output_excerpt": "" + }, + { + "step": "F1", + "cmd": "journalctl -b 0 -t systemd-binfmt --no-pager -n 4", + "exit": "", + "output_excerpt": "" }, { "step": "F1", @@ -700,6 +1149,12 @@ "exit": "", "output_excerpt": "" }, + { + "step": "F1", + "cmd": "sudo journalctl -b 0 -t systemd-binfmt --no-pager -n 4", + "exit": "", + "output_excerpt": "" + }, { "step": "F2", "cmd": "sudo loginctl enable-linger \"$(id -un)\"", @@ -736,6 +1191,18 @@ "exit": "", "output_excerpt": "" }, + { + "step": "F3", + "cmd": "docker --context rootless info --format '{{json .SecurityOptions}}'", + "exit": "", + "output_excerpt": "" + }, + { + "step": "F3", + "cmd": "docker --context rootless run --rm hello-world", + "exit": "", + "output_excerpt": "" + }, { "step": "F4", "cmd": "sudo apt-get update", @@ -762,7 +1229,7 @@ }, { "step": "F5", - "cmd": "sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 \"$(id -un)\"", + "cmd": "grep -q \"^$(id -un):\" /etc/subuid || sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 \"$(id -un)\"", "exit": "", "output_excerpt": "" }, @@ -864,7 +1331,7 @@ }, { "step": "F10", - "cmd": "wsl.exe -d '' -u '' --exec /bin/bash -lc 'for p in $(type -P claude codex); do test -f $p && test -x $p && echo executable: $p; done'", + "cmd": "wsl.exe -d '' -u '' --exec /bin/bash -lc 'for n in claude codex; do p=$(type -P $n); [[ -f $p && -x $p ]] && echo executable: $p; done'", "exit": "", "output_excerpt": "" }, @@ -892,5 +1359,33 @@ "exit": "", "output_excerpt": "" } - ] + ], + "comparison_arms": { + "26.04.1": { + "status": "unrun", + "name": "", + "first_boot": "", + "systemd_user_from_second_instance": "", + "wsl_gpu": "", + "uv_cpython_3_13": "", + "node_24": "" + }, + "24.04.5": { + "status": "unrun", + "name": "", + "first_boot": "", + "systemd_user_from_second_instance": "", + "wsl_gpu": "", + "uv_cpython_3_13": "", + "node_24": "" + } + }, + "w5_failure_export": { + "file": "-w5-failed.tar; null otherwise>", + "sha256": "", + "bytes": "", + "cause": "" + }, + "interop_after_unregister": "", + "rootless_container": "" } diff --git a/blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json b/blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json index a3cd02a61..e446b0034 100644 --- a/blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json +++ b/blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json @@ -3,8 +3,12 @@ "kind": "convergence_experiment", "status": "planned", "lane": "native-agent-engineering", - "task_and_failure": "The definitive-WSL goal of 2026-10-01 needs a second WSL 2 distribution on the existing Windows host, created and first-booted unattended by an LLM-native session with upstream commands, while the workstation's distribution keeps running and stays the default. The repository had no such recipe, and the interactive first launch stops at wsl-setup's user prompt, which needs a person. The image and the creation path of adoption/platforms/linux-wsl2-new-distro.md were selected by source review only: no comparison ran, and no host executed stage 1 or first boot.", + "task_and_failure": "Accepted W-IMG dual-image recipe parameterization at base 798ac445 for a recoverable second WSL 2 distribution beside the running default workstation. Ubuntu 26.04.1 trial and 24.04.5 fallback are symmetric provisional arms with no merit precedence. Source/signature/image facts were inspected, but neither new-host import, first boot, systemd-user, GPU or toolkit comparison has run. The preceding native Claude preparation worker hit its actual weekly limit, exit 1, and made no changes.", "primary_sources_and_pins": [ + { + "url": "https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl", + "revision": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104" + }, { "url": "https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl", "revision": "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e" @@ -40,6 +44,18 @@ { "url": "https://github.com/microsoft/WSL/pull/40977", "revision": "70f6890c83bb5e56b8ea17abdbb834b55e92dc80" + }, + { + "url": "https://releases.ubuntu.com/26.04.1/SHA256SUMS", + "revision": "4adde9f7e7760a624db5821ad75679a76b9cde1e0d3e9cb2117cfe7d8043fa43" + }, + { + "url": "https://releases.ubuntu.com/26.04.1/SHA256SUMS.gpg", + "revision": "055292b53dffbc5387c1691e8c2357df0b4db88e7d7fd4c1b36fadf068479c5d" + }, + { + "url": "https://github.com/ubuntu/wsl-setup/blob/73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8/ubuntu-insights.sh", + "revision": "73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8" } ], "discovery_provenance": [ @@ -47,15 +63,16 @@ "Pinned reads reported by the research unit: the 24.04.5 image streamed and hashed against Canonical's SHA256SUMS and DistributionInfo.json at 8bc98bc3; resolute SHA256SUMS of 2026-08-27 for the 26.04.1 image (sha256 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104), which DistributionInfo.json maps to WSL's default Ubuntu; cloud-init at tag 26.1, microsoft/WSL at tag 2.7.13 and ubuntu/wsl-setup at 86a561d5 for the first-run and provisioning behaviour.", "Review-repair unit of 2026-10-01, after the cross-family review of PR #569 asked for this record. It read Microsoft Learn's Basic commands for WSL (ms.date 2025-12-01) and Canonical's cloud-init on WSL page (last updated 2026-09-28) through Context Mode for W6's export before --unregister. It added the export, made the receipt example carry every recipe command, and wrote this record.", "Second follow-up of 2026-10-01, on the coordinator's brief: a completeness critic (11 findings) and a researcher's packet (eight answers with sources and file:line) preceded it. The unit added R1 (a rehearsal on a throwaway name), P1 to P3 (signed checksums with gpgv, cloud-init schema -c on the render, the kernel journal's hv_storvsc count), W5's cloud-init schema --system, W7 (one terminate and a file-ownership probe for microsoft/WSL#40941) and F2's idle observation; corrected three facts (WSL 3.0.1 does change the first launch; the online wsl --install checks the catalog's hash while --from-file checks none; W5's console lines come from the image's wsl-setup at Launchpad 74bfc891); and ran P1 to P3's commands in the workstation distribution as artifact checks.", - "The coordinator then replaced P3's stop on any count, which an old burst would have made permanent on this host: P3's count is a baseline, the newest error line's kernel time against /proc/uptime stops the run only when it is less than one hour old (a threshold of this recipe's choosing, not an upstream figure), and W5 counts again after the first launch on both paths. The unit ran the new P3 in the workstation distribution on 2026-10-01 at 12:19Z: baseline 21, newest error line about 7.4 days old." + "The coordinator then replaced P3's stop on any count, which an old burst would have made permanent on this host: P3's count is a baseline, the newest error line's kernel time against /proc/uptime stops the run only when it is less than one hour old (a threshold of this recipe's choosing, not an upstream figure), and W5 counts again after the first launch on both paths. The unit ran the new P3 in the workstation distribution on 2026-10-01 at 12:19Z: baseline 21, newest error line about 7.4 days old.", + "Accepted W-IMG follow-up at 798ac445 reuses the existing recipe/tests/source experiment and maintained search-first quick-mode and TDD guidance. The coordinator already verified both signed sums with gpgv, exit 0, key 843938DF228D22F7B3742BC0D94AA3F0EFE21092, and streamed the entire 26.04.1 image through curl/tee, sha256sum and tar inspection, exit 0. Preserved per-release native-signed-sums.stdout/stderr and sums/signature bytes, and 26.04.1 image-stream.sha256/native-image-inspection.stdout were inspected again without redownloading or rerunning them. The exact 26.04.1 hash matches the signed line and Microsoft catalog at 8bc98bc33b246fe66710eec9eaa1b24c323da987; 26.04.1 file size is unknown. The prior weekly-limit worker failure stays recorded; it is not an image acceptance run." ], - "license_and_compatibility": "License is not a selection criterion here and was not assessed. Compatibility, as the research unit read it: wsl --install --from-file needs WSL 2.4.4 or later (Microsoft), 2.4.8 (Ubuntu's announcement) or 2.4.10 (Ubuntu's install guide), and the host's wsl.exe --version printed 2.7.13. The bootstrap covers the 24.04 image's python3 3.12; 26.04.1 brings python3 3.14, uutils coreutils and sudo-rs, which no repository evidence covers yet.", + "license_and_compatibility": "Existing Microsoft/Canonical creation commands remain the source; no custom installer or license assessment. The earlier installed WSL 2.7.13 capability read is historical, not a new version check. Canonical 26.04 release notes and Microsoft creation docs support creation, not full-stack compatibility. Canonical cloud-init WSL guidance assumes 24.04/22.04 and does not qualify 26.04. Actual 26.04.1 packages: cloud-init 26.1-0ubuntu3~26.04.1, systemd 259.5-0ubuntu3.4, Python interpreter 3.14.4-1ubuntu0.1 (python3 metapackage 3.14.3-0ubuntu2), sudo-rs 0.2.13-0ubuntu1 and wsl-setup 0.6.3ubuntu~26.04.1. System Python 3.14 is not toolkit CPython 3.13 migration acceptance; uv installs/selects managed 3.13 separately. Neither arm has new-host compatibility acceptance.", "baseline_and_candidate": { - "baseline": "Not selected: WSL's default, wsl --install of the catalog's Ubuntu, which DistributionInfo.json at 8bc98bc3 maps to Ubuntu 26.04.1 LTS, with the interactive first launch whose prompt needs a person. Its image is the queued bounded comparison (status: queued, not run). The decision record's Alternatives 2 keeps the interactive first launch and the in-place repair as alternatives not chosen.", - "candidate": "Selected by source review: Canonical's ubuntu-24.04.5-wsl-amd64.wsl (sha256 bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e), its SHA256SUMS verified against Canonical's signature (P1) and the hash checked against both publications before install, installed with wsl --install --from-file ... --no-launch and given its default user by cloud-init on the first launch (path A), after a rehearsal on a throwaway name (R1). Path B is the fallback (status: documented, not run): W6 exports the failed attempt, then runs --unregister, wsl --import and a manual user." + "baseline": "Provisional 24.04.5 rollback arm, ubuntu-24.04.5-wsl-amd64.wsl, exact SHA-256 bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e. Historical artifact/native receipts keep their original class and scope; they do not qualify this new distribution. Same P1-P3/W1-W7/F1-F9/R1 criteria and paths A/B as the default; it replaces the default only on a release-caused 26.04 failure with no in-release remedy.", + "candidate": "Provisional 26.04.1 default arm, ubuntu-26.04.1-wsl-amd64.wsl, exact SHA-256 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104. Signature, whole-image hash and packaged source facts observed only; size unknown. Same P1-P3/W1-W7/F1-F9/R1 criteria and paths A/B as the rollback. Both acceptance comparisons unrun." }, "frozen_inputs": { - "base_revision": "b72c588f17a66ba4090c23e4ceac4f7e2c667bb4", + "base_revision": "798ac445307e2cd8eba6e74d7722ac0e16da02c7", "tasks": [ "wsl-new-distro-stage1-first-boot" ], @@ -65,11 +82,15 @@ "sources": [ { "path": "adoption/platforms/linux-wsl2-new-distro.md", - "sha256": "144d01671fcd63263f81e4e18415f3e75daa50dd6d356651588ab2281f4b8250" + "sha256": "91e8052e075bff33aa92e2253a8309e21381f8c5ca2aab680996b39e79ae6cb1" }, { "path": "adoption/templates/wsl/cloud-init.user-data.template", - "sha256": "39b2f3806217fe4df5370d7e0861675a754bb8676f159a76d248c863ef090be0" + "sha256": "08e6a3f6b078dfa54d78297596d54729d10528d57147b9b9eae063c37160eeb9" + }, + { + "path": "docs/decisions/2026-10-01-new-wsl-distro-recipe.md", + "sha256": "0fb3f1fd31e1cfea0b49923486eb705d29e51c4da00c9cc24bfed959235fed7a" } ], "inputs": [ @@ -81,36 +102,36 @@ "evaluation": [ { "path": "adoption/templates/wsl/first-boot-checklist.md", - "sha256": "dc8de9e5593be9908a92de798aa2892b9dc428297a968f778c59ac44ecda542e" + "sha256": "2ecc17772416e39390a11d5cf30414bbc34540812114a6dfc44b9dc36eb7f23d" }, { "path": "adoption/templates/wsl/stage1-receipt.example.json", - "sha256": "cf0f90dbfe3e780a8cca30cd7a31f88e430e825d2b479b2199ed0ab0adf20c78" + "sha256": "5dd17f19b447e92e1ecbbec82e5d38d219bd4cbc1f3402e03d9202c0afaa466d" }, { "path": "tests/test_wsl_new_distro_recipe.py", - "sha256": "4274bf85162b71a1dfaa6245c4e02eee729ab6522fc7b7f9d34c5296297caf58" + "sha256": "ba9046d7730bb72f95b52408271c26c13151085a855db00132eb740571d061cb" } ] }, "predeclared_metrics": { - "quality_rule": "The selection qualifies within scope only when one Windows host first passes the rehearsal of R1 on a throwaway name through F3 and removes it, and then runs stage 1 and first boot from the pinned recipe with every proof holding: P1 to P3 (no hv_storvsc error line less than one hour old among them), W1 to W5 on path A with W5's second storage count equal to P3's baseline, or W6 on path B after the export, then W7 with 1000:1000, then F1 to F11. That run is recorded as a sanitized stage-1 receipt in the shape of the pinned receipt example, with every recipe command's exit code and output and the rehearsal's observations. The image switches to 26.04.1 only when its bounded comparison passes the same recipe and stage 2 (the decision record's overturn condition 1).", - "usage_rule": "No usage claim. The research unit's model usage (tokens, attempts and retries) was not recorded and stays unknown; this record neither estimates it nor counts it as zero. The second follow-up's two research passes, as the coordinator's brief reports them: the completeness critic made about 44 HTTP requests to 37 distinct URLs against a budget of about 25, its token usage not recorded here; the researcher used 284,098 tokens and 70 tool uses and made about 85 HTTP requests against a budget of about 35. The scope of that token counter is not stated here, and nothing is summed or compared. A host run executes deterministic commands; the session that runs them records its own provider usage separately. No whole-task savings claim." + "quality_rule": "Both 26.04.1 trial and 24.04.5 fallback are symmetric provisional arms with no merit precedence. Preregister the same separate-throwaway R1 comparisons on one unchanged host/kernel/driver, checkout, user-data and bootstrap profile. Both signed sums pass P1; P2 workstation schema is not image qualification. W2 hashes the whole selected image against its exact signed/catalog release mapping; W4/W6 use that filename. Each arm must record first_boot (W5 default uid 1000, retained results, selected-image cloud-init version/schema and F1 running; amended on 2026-10-02 before any comparison ran: on WSL 3.0.1 the criterion is F1's pass condition, running, or degraded with systemd-binfmt.service as the only failed unit, which fails by design there; amended on 2026-10-02 before any comparison ran, after rehearsal run 2 and the scoped cross-family read: the first amendment's short form means F1's complete pass condition, running with no failed unit, or degraded with exactly systemd-binfmt.service failed and its log holding Failed to flush binfmt_misc rules, ignoring: Read-only file system; W1 stops before W4 or any W6 import unless uid 1000, an acceptable system state and failed set, an active user manager and a noninitial namespace all hold; W5 requires the new getty masked and inactive with no restarts and all five workstation values equal to their baseline; its separate command exits and filtered storage readings are retained); systemd_user_from_second_instance (F2 linger/idle and F3 directory/socket ownership plus manager running from the workstation second instance); wsl_gpu (/dev/dxg and native nvidia-smi visibility on the same Windows driver); uv_cpython_3_13 (separate managed Python 3.13.x through uv); node_24 (bootstrap v24.21.0 startup). Preserve P3 age/baseline, W5 second count, W7 owner 1000:1000 and failed-rehearsal/W6 export-before-unregister guards. Missing, failed or skipped criteria do not qualify an arm or rank its peer. Record sanitized native per-arm output before selecting within the measured host scope. Full-stack, GPU-workload and model acceptance remain separate and unrun.", + "usage_rule": "No usage claim. The research unit's model usage (tokens, attempts and retries) was not recorded and stays unknown; this record neither estimates it nor counts it as zero. The second follow-up's two research passes, as the coordinator's brief reports them: the completeness critic made about 44 HTTP requests to 37 distinct URLs against a budget of about 25, its token usage not recorded here; the researcher used 284,098 tokens and 70 tool uses and made about 85 HTTP requests against a budget of about 35. The scope of that token counter is not stated here, and nothing is summed or compared. A host run executes deterministic commands; the session that runs them records its own provider usage separately. No whole-task savings claim. Prior native Claude W-IMG preparation attempt: actual weekly-limit failure, process exit 1, no changes. Its returned result has num_turns 87; input_tokens 162, cache_creation_input_tokens 390235, cache_read_input_tokens 19973920, output_tokens 110059 and output_tokens_details.thinking_tokens 86281. These are native result fields, not summed totals; thinking is included in output. Native retries, whole-task total and per-image usage remain unknown. No new provider run or usage comparison is claimed by this recipe parameterization." }, "runtime_and_platform": { - "platform": "Planned target, not run: the existing Windows host with WSL 2.7.13 (the research unit's wsl.exe --version read) and a second WSL 2 distribution, , beside the workstation's.", + "platform": "Planned target, not run: the existing Windows host, updated from WSL 2.7.13 to 3.0.1.0 on 2026-10-02 (the adopted release for a second systemd distribution), and a second WSL 2 distribution, , beside the workstation's.", "tools": [ { "name": "WSL", - "version": "2.7.13 as read on the host by the research unit; source read at microsoft/WSL tag 2.7.13 (80697fd42cca3de0c0d5dd1931c36112372a577e); not run for this record" + "version": "3.0.1.0 as read on the host on 2026-10-02 (kernel 6.18.40.1-1); 2.7.13 as read by the research unit before the update; source read at microsoft/WSL tag 2.7.13 (80697fd42cca3de0c0d5dd1931c36112372a577e); not run for this record" }, { "name": "Ubuntu WSL image", - "version": "ubuntu-24.04.5-wsl-amd64.wsl, 388,975,696 bytes, sha256 bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e" + "version": "26.04.1: ubuntu-26.04.1-wsl-amd64.wsl, exact sha256 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104, preserved whole-image stream hash/inspection, size unknown. Historical 24.04.5: ubuntu-24.04.5-wsl-amd64.wsl, 388,975,696 bytes, sha256 bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e. Neither is imported or first-boot qualified." }, { "name": "cloud-init", - "version": "source read at tag 26.1 (8bf3567532b07e2cc15aa4c76c36ebed65ccfaec); the image's packaged version is not recorded here; the workstation distribution's 26.1-0ubuntu1~24.04.1 ran P2's schema check as an artifact check on 2026-10-01" + "version": "source read at tag 26.1 (8bf3567532b07e2cc15aa4c76c36ebed65ccfaec); the image's packaged version is not recorded here; the workstation distribution's 26.1-0ubuntu1~24.04.1 ran P2's schema check as an artifact check on 2026-10-01; actual 26.04.1 packaged version 26.1-0ubuntu3~26.04.1 inspected from the image, not executed" }, { "name": "gpgv", @@ -120,7 +141,7 @@ "isolation": "No stage-1 command ran. For a host run, the recipe's host-wide rules apply: commands act on only, and the rehearsal's R1 terminates and unregisters only its own throwaway name; no .wslconfig change (W1 only reads two keys with Select-String), no wsl --update and never wsl --shutdown; the only stop is wsl --terminate ; the default distribution stays the workstation's; and W6 exports the failed attempt before any --unregister. WSL 2 distributions share the global .wslconfig, one kernel and one network namespace, so the new distribution is not isolated from the workstation's; P1 to P3 run in the workstation distribution and write only into new temporary directories." }, "commands": [ - "Rehearsal first, R1: the whole page on a throwaway name through F3, then R1's PowerShell block removes that name (its second block exports a failed rehearsal before --unregister).", + "R1 preregistration: rehearse both selected releases on separate throwaway names through F3, extend successful rehearsals through F9 via the existing bootstrap, then use the recipe's second-instance systemd-user, native WSL GPU visibility, separate uv CPython 3.13 and Node 24 comparison probes. Preserve failures before removing only the owned name; failed rehearsals export before unregister. No such host run has executed.", "Pre-checks, P1 to P3: the sh blocks of adoption/platforms/linux-wsl2-new-distro.md in the workstation distribution, before W1.", "Stage 1, W1 to W7: the PowerShell blocks of the same page on the Windows host, each run as a .ps1 file inside Start-Transcript; W6 only after the W5 markers; W7 on both paths.", "First boot, F1 to F11: the sh blocks of the same page inside as ; F2's idle observation and F10 from the Windows side; F11 in a login shell after stage 2 (F9) and F10.", @@ -130,21 +151,27 @@ "observations": [], "failures_and_skips": [], "limitations": [ - "Selection by source review only. No comparison ran: the 26.04.1 bounded comparison is queued, not run. No host executed stage 1 or first boot on either image or path.", - "Status planned: this record has no observation. Paths A and B are documented, not run. Whether cloud-init provisions an imported distribution (path B) stays the decision record's open question 1.", + "Recipe parameterization only. Ubuntu 26.04.1 is the single default named by the merged definitive manifest and 24.04.5 the named rollback for a release-caused 26.04 failure; neither new-host acceptance comparison has run.", + "Status planned describes the host comparison, not the preserved artifact inspections or the rehearsals. No qualifying image comparison has run. A 26.04.1 rehearsal on WSL 2.7.13 on 2026-10-02 held stage 1 and stopped at F1 on the shared cgroup tree (evidence/artifacts/new-wsl-rehearsal-20261002/). Run 2 on WSL 3.0.1.0, 2026-10-02T08:32Z to 08:40Z, passed stage 1 and W5's path-A proofs and observed distinct cgroup namespaces, then stopped at W5's paired rule on a shared-console getty collision. Path A has run; path B's first boot and import provisioning remain open. These rehearsals qualify neither comparison arm.", "The research unit read the sources, and the decision record cites them as it reported them. The review-repair unit re-read only Microsoft's Basic commands page and Canonical's cloud-init page on 2026-10-01; neither documents an in-place repair of a failed provisioning.", "Usage unknown: the research unit's model and token usage was not recorded.", "A new distribution on the existing WSL kernel and Windows installation is not a new physical machine, and it inherits the host-wide WSL settings.", "The checker verifies declared consistency, not truth. The pinned test checks repository text and an in-memory template render; a pass is not a host run.", - "P1 to P3's commands ran on 2026-10-01 only as artifact checks in the workstation distribution, not as a run of the recipe: P1's signature and P2's schema passed with their negative controls failing as expected, and P3 counted 21 hv_storvsc lines (one burst of 14 distinct WRITE_10 errors about seven days earlier) in the current boot's kernel journal. Under the coordinator's rule that count is P3's baseline and its newest line, about 7.4 days old, does not stop the run; no second reading exists, because no first launch has run. Nothing in stage 1 has run, the rehearsal included." + "As of the 2026-10-01 artifact-check session, P1 to P3's commands ran only as artifact checks in the workstation distribution, not as a run of the recipe: P1's signature and P2's schema passed with their negative controls failing as expected, and P3 counted 21 hv_storvsc lines (one burst of 14 distinct WRITE_10 errors about seven days earlier) in the current boot's kernel journal. Under the coordinator's rule that count was P3's baseline and its newest line, about 7.4 days old, did not stop the run; no second reading existed, because no first launch had run. At that session nothing in stage 1 had run, the rehearsal included; the 2026-10-02 rehearsals below supersede that status.", + "Both signed sums and the entire 26.04.1 image hash/inspection are reused matching coordinator evidence, not new downloads or runs by this repair. The preserved 2026-10-01 stream did not measure image size; W2 in rehearsal run 2 on 2026-10-02 observed 418495746 bytes. Source/signature/image facts and reported rehearsal observations retain their separate evidence classes.", + "The failed prior native Claude worker hit its actual weekly limit, exit 1, with no changes. Its returned usage fields are retained separately; missing totals/retries and earlier unknown usage stay unknown.", + "Canonical cloud-init WSL guide covers 24.04/22.04, not 26.04 qualification. The workstation's version/schema check is not selected-image qualification. Python 3.14.4 system packages do not establish toolkit CPython 3.13 migration.", + "Completeness critic: no modality among the accepted two image arms was omitted from preregistration, but native execution of both arms and independent acceptance observation remain the decision-changing gap. GPU visibility and runtime startup are bounded checks; no full-stack, GPU workload, provider or model acceptance is claimed.", + "Amendments of 2026-10-02, made before any comparison ran: the comparison still applies the same criteria to both images with no merit precedence between them; the merged definitive manifest names 26.04.1 as the clean-install default and 24.04.5 as the rollback; the original first_boot criterion's 'F1 running' and the first amendment's short form mean F1's complete pass condition, including the read-only flush message when degraded (microsoft/WSL issue 41226). The second amendment adds the getty mask and separate baseline/paired observations after run 2 and the accepted scoped read.", + "Probe E1 at 2026-10-02T08:42Z, outside the recipe, observed the amended template's masked, inactive getty with no restarts and no cloud-init errors; it was removed afterwards. The workstation's getty had already failed in run 2, so a live workstation getty surviving a first boot is inferred from the masked unit never starting, not observed. The 24.04.5 image's own unit files and path B's first boot were not read or observed. Run 3 on 2026-10-02T10:54Z to 11:01Z then completed P1 to F5 with the amended template on a third throwaway name, including the paired proof and a rootless container; it is a rehearsal and qualifies neither comparison arm. The uv and Node probes, stage 2 and the rollback arm remain owed. Earlier planned/unrun and size-unknown fields retain the original record's scope and are superseded by these dated limitations only where stated." ], "decision_and_scope": { "decision": "trial", - "scope": "Ubuntu 24.04.5 from Canonical's .wsl file, path A (cloud-init) with path B as the fallback, for a second WSL 2 distribution beside the workstation's on the existing Windows host. Selected by source review; not a completed adoption until a host run and the queued comparison.", + "scope": "Accepted W-IMG recipe parameterization only, Ubuntu 26.04.1 as the single default and 24.04.5 as the named rollback, both provisional; source/signature/image facts retained. No host-scope adoption before both preregistered throwaway comparisons and independently observed native outputs.", "qualification_run_ids": [] }, "rollback": "Nothing on a host changed for this record. A host run that stops before W4 installs nothing. After W4, W6 keeps the failed attempt as an export, and wsl --unregister on the literal removes the new distribution; R1 removes the rehearsal's throwaway name the same way, after an export when the rehearsal failed. The workstation's distribution, the default and .wslconfig are never touched. Reverting the recipe's commits removes the documentation.", - "next_decision_changing_test": "First the rehearsal: run the pinned recipe on a throwaway name through F3 (P1 to P3, W1 to W7, F1 to F3), record P3's baseline and newest error line time, W5's second storage count, W5's cloud-init schema --system, W7's ownership probe and F2's idle observation in the receipt's rehearsal block, and remove that name with R1. A second count above the baseline holds the run before stage 2 as exposure to microsoft/WSL#41482. Then run stage 1 and first boot on the same Windows host for the real , and record the sanitized stage-1 receipt (path A, or path B with the export of the failed attempt). Then run the queued bounded comparison of Ubuntu 26.04.1 through the same recipe and stage 2: the bootstrap under python3 3.14, uutils coreutils and sudo-rs. A passing first run makes 24.04.5 with path A adoptable within that host's scope; a passing comparison switches the image (the decision record's overturn condition 1).", + "next_decision_changing_test": "On WSL 3.0.1 or later, run both R1 throwaway rehearsals with identical host/kernel/driver, checkout, user-data and profile. For each selected release verify P1-P3, W1-W7, F1-F3 and its own cloud-init version/schema, storage readings and Windows-file owner; extend through F9 via the accepted bootstrap, then compare the preregistered first_boot, systemd_user_from_second_instance, wsl_gpu, uv_cpython_3_13 and node_24 criteria. Record per-arm native results and all failed/skipped criteria, with 26.04.1 as the default and 24.04.5 as the rollback; export each failed rehearsal before unregistering its literal name. Both new-host acceptance runs remain unrun; only complete independently observed comparisons permit a scoped image decision, followed by the real selected distribution and separate full-stack acceptance.", "usage_assessment": { "claim": "none", "coverage": "partial", diff --git a/docs/decisions/2026-10-01-new-wsl-distro-recipe.md b/docs/decisions/2026-10-01-new-wsl-distro-recipe.md index af3694d9c..345456b02 100644 --- a/docs/decisions/2026-10-01-new-wsl-distro-recipe.md +++ b/docs/decisions/2026-10-01-new-wsl-distro-recipe.md @@ -1,4 +1,4 @@ -# Decision: a new WSL distribution from Canonical's Ubuntu 24.04.5 image, provisioned by cloud-init (2026-10-01) +# Decision: Ubuntu 26.04.1 WSL default and 24.04.5 rollback, provisioned by cloud-init (2026-10-01) **Decided by:** coordinator session `native-agent-stack-c5` (decisions 1 to 5 below), on the research unit's findings of 2026-10-01. Unit W2 of that wave wrote the recipe, the templates and the test, and re-read the sources the same day. @@ -6,7 +6,9 @@ A follow-up unit added F11 and the F5 range rule later that day, on the coordina then added the completeness critic's pre-stage-1 checks (R1, P1 to P3, W7, W5's schema check and F2's idle observation) and corrected three facts, on the coordinator's brief and a research packet of the same day. The coordinator then set P3's rule: the count is a baseline, an error line less than one hour old stops the run, and W5 counts again after the -first launch. +first launch. A third follow-up took the updated host's first observations into the recipe on 2026-10-02: it names WSL +3.0.1 as the adopted target, adds W5's paired isolation record and F1's one accepted failed unit, and replaces the +interop restart with a stop for review (Updated-host correction). This record changes nothing on a host. No `wsl.exe` command, import, `.wslconfig` edit or first launch ran for it. The second follow-up's artifact checks ran P1 to P3's commands in the workstation distribution and changed nothing on the host (Evidence classes). @@ -15,14 +17,15 @@ host (Evidence classes). pointer section in `adoption/platforms/linux-wsl2.md`; `adoption/templates/wsl/` (`cloud-init.user-data.template`, `host.new-distro.json.template`, `first-boot-checklist.md`, `stage1-receipt.example.json`); `tests/test_wsl_new_distro_recipe.py`. -**Status:** selected by source review; not a completed adoption until a host run and the queued comparison. The scoped -convergence experiment record -[`blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json`](../../blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json) -states this: `status: planned`, decision `trial`, no observation and no usage claim, with the research unit's usage -unknown and the follow-up's two research passes' usage given as text. Its next test starts with the rehearsal (R1). It -names the 26.04.1 comparison (queued, not run) and path B (documented, not run) as the alternatives, and -`python3 scripts/validate_convergence.py` accepts it (exit 0). It pins the recipe, the templates and the test by -SHA-256, so a change to any of them needs a re-pin. +**Status:** the merged definitive manifest selects Ubuntu 26.04.1 LTS (Canonical WSL image) as the single default. +Ubuntu 24.04.5 is the named rollback, used only on a release-caused 26.04 failure with no in-release remedy. +The 2026-10-02 rehearsal held through stage 1, then stopped at F1 on the host's shared cgroup tree. It does not +trigger the image rollback. The host was updated to WSL 3.0.1.0 on 2026-10-02; full acceptance and the corrected +checks on it, with the two-distribution proof, remain owed. The earlier scoped +[experiment](../../blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json) keeps `status: planned`, +decision `trial`, no qualification runs and no usage comparison. Its next test preregisters both R1 rehearsals. The +reported signature exits and image stream hash match retain their bounded artifact scope; local consistency checks and synthetic receipt examples +do not qualify a distribution. Recipe, templates, this decision and tests are pinned by SHA-256 in that experiment. ## Context @@ -40,15 +43,17 @@ SHA-256, so a change to any of them needs a re-pin. - `--manage --set-default-user `; - `--terminate `. - **Minimum WSL version for `--install --from-file`.** Microsoft gives 2.4.4 (build-custom-distro, updated 2025-09-12), - Ubuntu's announcement 2.4.8 and Ubuntu's install guide 2.4.10 (Method 1), so the host's 2.7.13 needs no update to - install. The research unit read the notes of WSL 2.7.14 (2026-09-11) and 3.0.1 (2026-09-29) and the `microsoft/WSL` + Ubuntu's announcement 2.4.8 and Ubuntu's install guide 2.4.10 (Method 1). That install-only minimum remains valid + for a single systemd distribution with no second systemd distribution running. It is insufficient for this page's + second distribution: W1 requires WSL 3.0.1 or later before W4 or any W6 import, for isolated cgroups and their + namespaces (PR #40519 and PR #41512). The research unit read the notes of WSL 2.7.14 (2026-09-11) and 3.0.1 (2026-09-29) and the `microsoft/WSL` source at the three tags, and wrote that they change nothing about install, import or first run. That was wrong (completeness critic, item 2). The fix for microsoft/WSL#40941, PR #40977 (after the first-run setup, files created from Windows are owned by 0:0), first ships in 2.9.8, a pre-release, and in 3.0.1. Its function, `ConnectionTargetManager::UpdateUid`, is absent from `src/windows/common/Redirector.h` at tags 2.7.13 and 2.7.14 and present at 2.9.8 and 3.0.1 (research packet, answer 3). W7 terminates `` once after the first launch and probes a file's owner; updating WSL stays the keys lane's decision. -- **Selected image.** `ubuntu-24.04.5-wsl-amd64.wsl` (sha256 `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e`) +- **Historical 24.04.5 image artifact check.** `ubuntu-24.04.5-wsl-amd64.wsl` (sha256 `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e`) is 388,975,696 bytes and published twice: - in `https://releases.ubuntu.com/24.04.5/SHA256SUMS` (Last-Modified 2026-09-15 19:11:11 GMT; the same line is in `noble/SHA256SUMS`, with `SHA256SUMS.gpg` beside it); @@ -226,13 +231,158 @@ SHA-256, so a change to any of them needs a re-pin. carries `843938DF228D22F7B3742BC0D94AA3F0EFE21092`. The repository's 2026-09-21 precedent verified Ubuntu's signed sums with the same key (`docs/portable-userspace-install-20260921.md`). +## W-IMG preregistration and preserved primary evidence (2026-10-01) + +The north-star action is a recoverable second WSL distribution for native engineering. The accepted unit extends the +existing upstream-command recipe, templates and tests at `798ac445`; it creates no installer or architecture. The +coordinator applied the maintained ECC `search-first` guidance in quick mode and the TDD skill at the already-agreed +public recipe/template/experiment seams. No new dependency or skill installation is needed. + +| Image role | Actual release URL | Exact image SHA-256 | Catalog entry at `8bc98bc33b246fe66710eec9eaa1b24c323da987` | +| --- | --- | --- | --- | +| 26.04.1 default | https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl | `48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104` | `Ubuntu-26.04` (also `Ubuntu`) | +| 24.04.5 rollback | https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl | `bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e` | `Ubuntu-24.04` | + +Both arms use the same R1 preregistered criteria with the amendments of 2026-10-02 made before any comparison ran +(recorded below), and native probes in the recipe, on separate throwaway names under the same host kernel, driver, +settings, checkout, user-data and bootstrap profile. R1 keeps its initial run +through F3, then extends successful rehearsals through F9 for the existing toolkit and the bounded comparison probes. +The default-user systemd manager and bus are checked from the workstation's second-instance session. GPU visibility +uses Microsoft's/NVIDIA's native WSL guidance; toolkit runtime checks use the accepted bootstrap and official Node +24.21.0 pin. A version/startup probe or visible GPU does not establish full-stack, CUDA workload or model acceptance. +The clean install uses the single 26.04.1 default from +[`definitive-manifest.json`](../../evidence/artifacts/new-wsl-definitive-defaults-20261001/definitive-manifest.json). +The 24.04.5 rollback returns only for a release-caused 26.04 failure with no in-release remedy. Both arms' full +new-host acceptance remains owed; the comparison distributions do not become clean-install alternatives. + +**Preserved facts, reused rather than rerun.** The retained +[integration receipt](../../evidence/artifacts/new-wsl-dual-image-20261001/receipt.json) reports signed-checksum +verification exit 0 for both releases, signer fingerprint `843938DF228D22F7B3742BC0D94AA3F0EFE21092` and a 26.04.1 +image stream hash match. There is no retained verification output for that 26.04.1 signature check; those reported +exits, fingerprint and match do not establish a retained `Good signature` stream. The earlier 24.04.5 P1 output and +tampered-copy control below retain their historical artifact scope. No 26.04.1 image size was observed in that +authoring stream; the later rehearsal records its own download size separately. + +The inspected 26.04.1 packages are cloud-init 26.1-0ubuntu3~26.04.1, systemd 259.5-0ubuntu3.4, Python interpreter +3.14.4-1ubuntu0.1, sudo-rs 0.2.13-0ubuntu1 and wsl-setup 0.6.3ubuntu~26.04.1. Correction: the earlier Python 3.14.3 +statement was the `python3` metapackage (3.14.3-0ubuntu2); the retained dpkg `python3.14`, `python3.14-minimal` and +`libpython3.14-stdlib` stanzas prove interpreter 3.14.4-1ubuntu0.1. System Python 3.14 is not toolkit CPython 3.13 +migration acceptance; uv selects and installs managed 3.13 separately. Installed uv 0.12.17 `run --help` exited 0 +and lists `--no-project` and `--python`; no managed interpreter run was performed for this unit. + +The retained `wsl-setup` and `wait-for-cloud-init` image extracts match tag `0.6.3`, upstream commit +`73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8`. `wsl-setup:155-156` calls `ubuntu-insights.sh`. Its consent path preserves +existing Linux/Windows settings; `ubuntu-insights.sh:15-19` asks only on terminal stdin (`-t 0`). W5 uses NUL stdin. +No recipe command invents consent or writes the Windows-wide consent registry. Neither source inspection nor the +workstation's P2 schema check qualifies a new image; W5 must record the selected packaged version and its own schema. +Canonical's cloud-init WSL guide explicitly assumes 24.04/22.04 and does not qualify 26.04. + +**Prior failed preparation attempt.** The preceding native Claude worker returned the actual weekly-limit message, +process exit 1, and made no changes. Its native returned usage is preserved in the experiment's usage rule, without +summing cache subsets, estimating missing totals or treating unknown retries as zero. This is a recipe-preparation +failure, not a failed first boot of either image. No provider, GPU, import or first-boot run is claimed by this unit. + +**Completeness critic.** Both signed sums, exact selected-image bytes/hash, packaged schema versus host schema, +second-instance systemd user behavior, Windows GPU visibility, separate uv CPython 3.13 and Node 24 startup now have +explicit symmetric criteria. Preserve the old export guard for failed rehearsals and W6. The remaining gap is native +new-host execution of both arms, plus separate full-stack/GPU-workload/model acceptance. Debian, alternate creation +routes and containers remain the earlier sweep candidates; this bounded unit adds none. The lifecycle-task skills +sweep receives only a future execution gap, since the accepted lifecycle/TDD/search guidance covers parameterization. + +## Rehearsal correction (2026-10-02) + +The retained `coordinator-review.md` and `stage1-receipt.json` on +`origin/foundation/new-wsl-rehearsal-20261002` at `76b7c0b4ee45d3a6f0a60fa68d053e0f2039e8e4`, under +`evidence/artifacts/new-wsl-rehearsal-20261002/`, record the real +Ubuntu 26.04.1 rehearsal on WSL 2.7.13. Stage 1 held, including schema and ownership; F1 returned `degraded`, with +`user@1000.service` failing to spawn its executor. From the already-running distribution, the coordinator read +`0 0` in `system.slice/cron.service/cgroup.procs` and the other distribution's user manager reported the +workstation's own `ControlGroup`. The supplied namespace observation, `cgroup:[4026531835]`, names Linux's initial +cgroup namespace, `PROC_CGROUP_INIT_INO` in `include/linux/proc_ns.h`; it corroborates the diagnosis and is not a gate. + +Correction: command support on 2.7.13 did not establish safe concurrent systemd distributions. Microsoft's release +notes name PR #40519 in 2.9.8 (isolate distro cgroups) and PR #41512 in 2.9.13 (create their namespaces), both +pre-releases; 3.0.1 is the first stable release with both. The 2.7 line has neither. W1 now requires 3.0.1 or later +before any install or import. After launch, W5 must observe no `0` entry in a unit active in both distributions, from +the already-running distribution, and an active new `user@.service`, before F1. A failed observation terminates +only the new distribution, exports, unregisters and checks surviving interop; it never stops or restarts a system unit +in either distribution. When this correction was written, nothing had been observed on 3.0.1 on this host; the +Updated-host correction below records what followed. Rootless Docker/Podman issue 41492 remains open; F3's container +start is owed on the first run after the update. + +After removal, the surviving distribution lost the VM-wide `WSLInterop` registration. The coordinator restored it +with `sudo systemctl restart systemd-binfmt`. Every unregister now checks both that registration and one Windows +executable on the survivor and stops if either observation does not return. The Updated-host correction replaces +the restart, which the adopted release does not support, with a stop for review. + +The rehearsal's storage baseline was 7 and second count 13, with six new lines in the disk-attachment window before +cloud-init. Their placement about 15 seconds before `init-local` is derived in the receipt; the launch exited 0. +W5 and the checklist now record an attachment-only increase with its lines and continue; any later storage error or +provisioning failure with a storage cause stops. The older rule requiring equality was not supported by that run. + +The repair extends the existing Python `unittest` consistency checks with independent stage ids, every selected-image +digest field and in-memory controls restoring the old unsafe wording. These checks inspect source only; they do not +rerun the host rehearsal. A completeness check keeps path B, the export guards, all existing stages and the older +single-distribution install boundary; it also checks the new unregister in cgroup recovery. Public upstream refresh +was unavailable because DNS resolution failed, so the retained review and the supplied upstream references are used +without claiming a fresh fetch or an updated-host result. + +## Updated-host correction (2026-10-02) + +The coordinator's brief of 2026-10-02 reports the first observations of the updated host, WSL 3.0.1.0 with kernel +6.18.40.1-1, made from 06:39Z to 06:57Z in the workstation's distribution, as its default user and without `sudo`. +`id -u` printed `1000`. `systemctl is-system-running` printed `degraded` (exit 1), with `systemd-binfmt.service` as the +only failed unit. `systemctl is-active "user@$(id -u).service"` printed `active`. `readlink /proc/self/ns/cgroup` +printed `cgroup:[4026532183]`, equal to `/proc/1/ns/cgroup`; WSL 2.7.13 had printed the initial namespace, +`cgroup:[4026531835]`. The host also passed its check of a rootless container, interop and the user manager. +`systemd-binfmt.service` failed at each observed boot with `Failed to flush binfmt_misc rules, ignoring: Read-only file system` +and exit 1, while `WSLInterop` and `python3.12` are registered and Windows executables launch. (Qualified on 2026-10-02 +after the Codex lane's bounded review of `46acc1e5`: WSL installs that read-only lock only while `[boot] +protectBinfmt` is on, its default, and as a best-effort step, `init.cpp` L2433 and L2916-L2923 and +`WslDistributionConfig.h` L62 at tag `3.0.1`; "every boot" is this host's observation under the default, not an +upstream guarantee.) Nothing was observed with +two distributions running. + +Three decisions follow; Sources lists what each rests on. + +1. **Adopted target.** The recipe adopts stable WSL 3.0.1 or later for a second systemd distribution. The 2.9.8 and + 2.9.13 pre-release history and the 2.7.13 rehearsal stay as history, and the single-distribution note stays. The + host's update and its check are one sentence in Host-wide rules; the two-distribution proof is still owed. +2. **Paired isolation (W5).** W5 records the same five observations from both running distributions, `uid`, + `system_state`, `failed_units`, `user_manager` and `cgroup_namespace`, under `paired_isolation` in the receipt, after + the cgroup block and before F1. The workstation's values must equal its own baseline from W1, the two namespaces must + differ from each other and from `cgroup:[4026531835]`, and both user managers must be `active`. W6's relaunch repeats + the record, as it repeats the cgroup proof, and the failure recovery is the cgroup-failure recovery, unchanged. +3. **The by-design unit failure (F1) and the interop recovery (R1).** F1 accepts `degraded` only when + `systemctl --failed --no-legend --plain` lists exactly `systemd-binfmt.service` and that unit's log holds the + read-only flush message; any other failed unit stops the run as before. `sudo systemctl restart systemd-binfmt` was + the WSL 2.7.x interop remedy. On the adopted release the registration is protected and that restart itself exits 1, + so R1 keeps its check after every unregister and, when it fails on 3.0.1, stops for review with + `ls /proc/sys/fs/binfmt_misc` and `systemctl status systemd-binfmt.service --no-pager` recorded; nothing is imported + or provisioned. + +Alternatives, each rejected against that evidence: + +- **A one-sided W5 reading**, the new distribution's `user@.service` alone: it cannot show the same uid, distinct + namespaces and two healthy managers, so the record pairs the five observations. +- **Namespace values alone**: a value does not prove isolation (W1 already says so), and two values compare only next to + the workstation's own baseline. +- **Stopping at every `degraded`**, the earlier F1: the unit fails at each boot on the adopted release under its default + `protectBinfmt` setting, so no run on this host could pass F1. +- **Accepting any `degraded`**: it would hide a real failed unit. The pass condition names the one unit and its log + message. +- **Keeping the restart as the interop recovery**: it exited 1 on this host on the adopted release, where PR #40621 + protects the registration on purpose while `protectBinfmt` is on (its default). + +The conditions that would overturn these decisions are in Overturn condition, items 3 and 4. + ## Alternatives -1. **Image.** Ubuntu 26.04.1 LTS. `resolute` `SHA256SUMS` (2026-08-27) publishes - `ubuntu-26.04.1-wsl-amd64.wsl` as `48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104`, and - `DistributionInfo.json` maps both `Ubuntu` (WSL's default) and `Ubuntu-26.04` to it. It brings `python3` 3.14.3, uutils - coreutils for `sha256sum`, `realpath` and `mktemp`, `sudo-rs` beside `sudo`, and ships `bubblewrap` and `libatomic1`. - The repository has no evidence for it. Other alternatives: +1. **Images.** Ubuntu 26.04.1 LTS (Canonical WSL image) is the single clean-install default; the parameterized recipe + retains historical 24.04.5 as the named rollback only for a release-caused 26.04 failure with no in-release remedy. + Full new-host compatibility remains owed. The earlier `python3` 3.14.3 statement described its metapackage, not + the 3.14.4 interpreter (correction in W-IMG preregistration). + Other alternatives: - `wsl --install Ubuntu-24.04` from the online catalog installs the same bytes. At 2.7.13 it does check the download against the catalog entry's `Sha256` (`WslInstall.cpp:36-50`, `:315`), while `--install --from-file` checks no hash (`WslClient.cpp:500-537`); this record first had that backwards (completeness critic, item 8). The online path @@ -276,9 +426,11 @@ SHA-256, so a change to any of them needs a re-pin. coordinator's brief: F11 records `not installed`, and the install stays with step 4a. - Leave the clone unregistered. Four carrier blocks would then name tools of a server the session lacks. 4. **Host-wide.** - - Update WSL to 3.0.1 first: it carries the fix for microsoft/WSL#40941, which 2.7.14 lacks. Not chosen: updating - WSL is the keys lane's decision, and W7's terminate and probe cover the bug on 2.7.13. This record first said that - neither release changes the first launch, which was wrong. + - Proceed with the second systemd distribution on WSL 2.7.13: rejected after the rehearsal proved a shared cgroup + tree and a failed uid-1000 user manager. WSL 3.0.1 or later is now a host prerequisite, for PR #40519 and PR + #41512, and also carries the fix for microsoft/WSL#40941. The keys lane performs the update outside this page. + W7's owner probe remains required. This record's earlier install-only version claim did not establish safe + concurrent systemd distributions; the retained rehearsal and upstream release notes correct it. - Apply settings with `wsl --shutdown`: it stops every distribution. - Set `swap=0` for microsoft/WSL#41482, or change an idle key: both change the global WSL configuration, and the first takes effect only after a WSL shutdown; that is the user's decision, not this recipe's. @@ -293,36 +445,51 @@ SHA-256, so a change to any of them needs a re-pin. ## Decision -1. **Image.** Ubuntu 24.04.5 LTS from Canonical's `.wsl` file, pinned by its sha256 and checked against both publications - before install (W2). The repository has evidence for 24.04, and the bootstrap covers its `python3` 3.12. 26.04.1 LTS - is queued as a bounded comparison unit before any switch: the bootstrap under `python3` 3.14, uutils coreutils and - `sudo-rs`. +1. **Images.** The single clean-install default is Ubuntu 26.04.1 LTS (Canonical WSL image). Select 24.04.5 only as + the named rollback for a release-caused 26.04 failure with no in-release remedy. P1 verifies both signed sums; W2 controls each release-to-file-to-SHA-to-catalog + pairing, downloads the selected release URL and hashes the actual whole image. W4 and W6 use the same selected + filename. Historical 24.04 receipts keep their original evidence class and scope; they do not accept either arm + on a new host. 2. **Creation path.** - **Path A.** Render `%USERPROFILE%\.cloud-init\.user-data` from the template (W3). The user is ``, uid 1000, in groups `adm, cdrom, sudo, dip, plugdev`, with `sudo: "ALL=(ALL) NOPASSWD:ALL"`, a locked password and `[user] default` appended to `/etc/wsl.conf`. Then run - `wsl --install --from-file \ubuntu-24.04.5-wsl-amd64.wsl --name --location Z:\WSL\ --no-launch` (W4), + `wsl --install --from-file \ubuntu--wsl-amd64.wsl --name --location Z:\WSL\ --no-launch` (W4), then a first launch with standard input at end of file (W5). - - **Rehearsal first (R1).** The page runs once on a throwaway name and folder through F3, with W5's schema check, W7's - probe and F2's idle observation recorded in the receipt's `rehearsal` block. R1 then terminates and unregisters - only that name, after W6's export rule when the rehearsal failed. Only then does the page run for the real - ``. + - **Rehearsals first (R1).** Each release runs on its own throwaway name and folder through F3, recording W5's schema, + W7's probe and F2's idle observation. Successful rehearsals extend through F9 and the same preregistered comparison + probes; per-arm results enter `comparison_arms`. R1 terminates and unregisters only its own name, after W6's export + rule when a rehearsal failed. Both comparison records precede the real explicitly selected ``. - **Pre-checks (P1 to P3)**, in the workstation distribution before W1. P1 verifies `SHA256SUMS` with `gpgv` - against the installed archive keyring (key `843938DF228D22F7B3742BC0D94AA3F0EFE21092`) and prints the image's - signed line. P2 runs `cloud-init schema -c` on the render and prints its SHA-256, which W3's file must match. P3 + for both releases against the installed archive keyring (key `843938DF228D22F7B3742BC0D94AA3F0EFE21092`) and prints + both signed image lines. P2 runs the workstation's `cloud-init schema -c` on the selected-image render and prints its + SHA-256, which W3's file must match; this does not qualify the selected image's schema, which W5 checks itself. P3 records the current boot's `hv_storvsc` kernel journal count, without the registration line, as a baseline and prints the newest error line's kernel time beside `/proc/uptime`. An error line less than one hour old stops the run (microsoft/WSL#41482); the threshold is this recipe's choice, not an upstream figure. - - **Second storage reading (W5).** On both paths, right after the first launch, the same count again in the - workstation distribution. Equal to the baseline passes. A larger count is recorded with the new lines, a failed - W5 counts as exposure to microsoft/WSL#41482, and nothing continues to stage 2 until that is decided. + - **Second storage reading (W5).** Record the baseline before the import and the second count after the first + launch on both paths. An increase confined to disk attachment before cloud-init starts is recorded with its + lines and does not stop the run. Any storage error after that window, or a provisioning failure with a storage + cause, stops the run. An unknown window stops for review; retain the actual cause with microsoft/WSL#41482. - **Preflight (W1).** Neither Ubuntu Pro file exists, or `agent.yaml`'s top-level keys are recorded and include neither `users` nor `write_files`. W1 also reads `instanceIdleTimeout` and `vmIdleTimeout` of the global WSL - configuration, without writing it. + configuration, without writing it. WSL must be 3.0.1 or later before W4 or any W6 import. Record the + workstation's baseline of five values in the already-running distribution, as its default user and without `sudo`: + the uid, the system state, the failed unit names, the user manager's state and `readlink /proc/self/ns/cgroup`, + with the initial namespace `cgroup:[4026531835]`, Linux's `PROC_CGROUP_INIT_INO`, named as corroboration rather than + the gate. The baseline must already meet W5's rule for one distribution, or the run stops before W4. - **After the first launch (W5).** `cloud-init status --long` must print `status: disabled` with `boot_status_code: disabled-by-marker-file`. Completion and errors come from `/var/lib/cloud/data/result.json` (`DataSourceWSL`, `"errors": []`) and `/var/lib/cloud/data/status.json` (four stages finished without errors). On path A, `cloud-init schema --system` must print a line matching `^\s*Valid schema user-data$`; path B skips it. + `wsl: Failed to start the systemd user session` stops even when the launch exits 0. While both distributions run, + before F1, the already-running distribution must read no `0` entry in a system unit's `cgroup.procs`, for a unit + active in both, and the new distribution's `user@.service` must be active. Then the paired record reads the + same five values from both distributions (`paired_isolation`): the same uid, active user managers, system managers + `running` or `degraded` with only `systemd-binfmt.service` failed, two namespaces that differ from each other and + from the initial one, and the workstation's values equal to its W1 baseline. Failure of either proof terminates + only the new distribution, never stops or restarts a unit in either, guards the export, unregisters and checks + surviving interop. - **Terminate once (W7).** After W5 (path A) or W6 (path B): `wsl --terminate `, a relaunch, and an empty file created from Windows under the user's home must read `1000:1000`. `0:0` stops every write from Windows. - **How to tell cloud-init did not provision.** The launch prints `Create a default Unix user account:` and @@ -330,17 +497,24 @@ SHA-256, so a change to any of them needs a re-pin. - **Fallback, path B (W6).** Re-list, then preserve the failed attempt. `wsl --terminate ` and `wsl --export Z:\WSL\downloads\-failed.tar` keep its cloud-init logs, `/var/lib/cloud/instance` and its configuration. Its SHA-256 and size go to the receipt's `failed_attempt_export`. A nonzero export exit throws - before any deletion. Then `--unregister` the literal new name, `--import ... --version 2`, then a manual user with + before any deletion. Then `--unregister` the literal new name, check surviving interop, `--import ... --version 2`, then a manual user with the same groups and NOPASSWD drop-in, `[user] default`, the marker and `wsl --terminate `. - **Stage-1 receipt.** The private PowerShell transcript, sanitized into the shape of `adoption/templates/wsl/stage1-receipt.example.json`. Its payload keys are those `scripts/validate.py` compares with a `receipts[]` row; `kind` is `native_cli_e2e` and `component_ids` is `systemd` (stack row `255.4-1ubuntu8.17`, the image's version). 3. **First boot**, as the user: - - `systemctl is-system-running --wait` must print `running`; Ubuntu's own setup test asserts that. + - `systemctl is-system-running --wait` must print `running`, as Ubuntu's own setup test asserts, or `degraded` when + `systemctl --failed --no-legend --plain` lists exactly `systemd-binfmt.service` and that unit's log holds the + read-only flush message: the adopted release mounts the binfmt status file read-only (microsoft/WSL#40621) and + upstream calls the unit's error benign (#41226). Any other failed unit stops the run for review. - `loginctl enable-linger`, then `Linger=yes`; then, with no client attached, `wsl.exe --list --running` every 10 seconds for two minutes records whether `` stays up (F2). - - The user bus is a socket owned by the user. + - The user bus is a socket owned by the user. Right after F3, a rootless container must start on Docker's + `rootless` context, as the user without sudo. Check `name=rootless` and run + `docker --context rootless run --rm hello-world`; a failed start stops with microsoft/WSL#41492. This is owed + on the first run after the update; an absent engine requires F4, F5 and Docker's supported rootless setup, then + a return to this check before F9 or accepting R1. - `apt-get install` the bootstrap's list plus `libatomic1` and `uidmap`: - `uidmap` for rootless Docker in the Harbor harness; - `libatomic1` for parity with the workstation profile (`docs/new-workstation-runtime-profile-20260922.md:34`). The @@ -367,15 +541,23 @@ SHA-256, so a change to any of them needs a re-pin. - The generated terminal profile may stay, hidden by the operator if duplicated. - WSL package updates are the keys lane's decision. - The rehearsal (R1) names only its own distribution. + - Every unregister checks `/proc/sys/fs/binfmt_misc/WSLInterop` and launches a Windows executable in the surviving + distribution. If registration or Windows execution does not return, the run stops for review after the new + distribution is removed, with `ls /proc/sys/fs/binfmt_misc` and `systemctl status systemd-binfmt.service + --no-pager` recorded; nothing is imported or provisioned. The WSL 2.7.x remedy, restarting `systemd-binfmt`, is + not used on the adopted release, where that restart exits 1. - W1's `Select-String` read is the only command that names `.wslconfig`; the test rejects any other command naming it. - The test rejects any recipe command that breaks these rules. 5. **Open questions** stay open; see below. ## Overturn condition -1. **Image.** Switch to 26.04.1 when its bounded comparison passes the same recipe and stage 2. Re-pin when Canonical - publishes a newer 24.04 point release and `DistributionInfo.json` maps `Ubuntu-24.04` to it. A sha256 mismatch in W2 - stops every run until the hash is re-verified against both publications. +1. **Images.** Keep 26.04.1 as the single default; 24.04.5 returns only for a release-caused 26.04 failure with no + in-release remedy. Accept the selected image within the measured host scope only after the required, identically scoped R1 + comparison records for first boot, the systemd user manager from a second instance, WSL GPU visibility, + uv-managed CPython 3.13 and the pinned Node 24. Preserve failures and skips; missing evidence is not a win. + Comparisons stay on throwaway names. Re-pin a release only after Canonical's signed sums and the + pinned Microsoft catalog agree. A W2 mismatch stops the run with nothing installed. 2. **Creation path.** Revisit in two cases: - a host run with a correct user-data file still shows the W5 markers; - a WSL release changes when the first-run command runs (`WslCoreInstance.cpp:204`) or what a failure leaves @@ -383,30 +565,45 @@ SHA-256, so a change to any of them needs a re-pin. The coordinator may instead adopt the in-place repair as the first fallback, ahead of path B. 3. **First boot.** Revisit in four cases: - - F1 reports `degraded` on a clean run; + - F1 reports `degraded` with a failed unit other than `systemd-binfmt.service`, or without that unit's read-only + flush message, on a clean run; - the bootstrap starts installing `uidmap` or `libatomic1` itself; - the Harbor lane retires rootless Docker, which removes `uidmap` and F5; - the user-scope MCP template registers jCodeMunch again (the addendum's overturn condition), or a script runs the per-project registration, which turns F11 into a check. Drop `libatomic1` when the coordinator accepts the `readelf` measurement. -4. **Host-wide.** Revisit when the keys lane updates WSL (re-verify install, import and first run at that tag; from - 2.9.8 or 3.0.1 on, the fix for microsoft/WSL#40941 is present and W7 becomes a check) or when a separate decision - makes the new distribution the default. Revisit P3 when microsoft/WSL#41482 names a fixed release, or when a run - shows storage errors that the one-hour threshold, this recipe's choice, misjudges. +4. **Host-wide.** Revisit on a later WSL update after the completed 3.0.1 update (re-verify install, import and first + run at that tag, including W7's check of the microsoft/WSL#40941 fix already present in 3.0.1) or when a separate + decision makes the new distribution the default. Revisit P3 when microsoft/WSL#41482 names a fixed release, or when a run + shows storage errors that the one-hour threshold, this recipe's choice, misjudges. Revisit the adopted target and + W5's paired record when a stable release after 3.0.1 changes how distributions share cgroups, or when a run shows two + equal namespaces or an unhealthy user manager. Revisit F1's one accepted failed unit when a WSL release stops mounting + the binfmt status file read-only or systemd stops failing on it, and R1's stop for review when a 3.0.1 run loses + `WSLInterop` after an unregister and upstream documents a remedy. ## Command table Every command line of the recipe's `powershell` and `sh` blocks, in order. `tests/test_wsl_new_distro_recipe.py` fails -when a row and the recipe disagree. The proofs are what the run must print; none of them has run on a host as part of -the recipe. P1 to P3's commands ran as artifact checks in the workstation distribution (Evidence classes). +when a row and the recipe disagree. The proofs are what the next run must print. The earlier 2.7.13 rehearsal is +retained separately; the corrected run on 3.0.1 remains owed. P1 to P3 also have historical artifact checks in the +workstation distribution (Evidence classes). | Step | Shell | Command | Proof | | --- | --- | --- | --- | +| R1 | powershell | `wsl.exe -d '' --exec bash -lc 'stat -c %U,%F /run/user/$(id -u) /run/user/$(id -u)/bus'` | R1: record the returned exit and output required by the recipe | +| R1 | powershell | `wsl.exe -d '' --exec systemctl --user is-system-running --wait` | R1: record the returned exit and output required by the recipe | +| R1 | powershell | `wsl.exe -d '' --exec test -c /dev/dxg` | R1: record the returned exit and output required by the recipe | +| R1 | powershell | `wsl.exe -d '' --exec /usr/lib/wsl/lib/nvidia-smi` | R1: record the returned exit and output required by the recipe | +| R1 | powershell | `wsl.exe -d '' --exec bash -lc 'python3 --version'` | R1: record the returned exit and output required by the recipe | +| R1 | powershell | `wsl.exe -d '' --exec bash -lc 'uv run --no-project --python 3.13 python --version'` | R1: record the returned exit and output required by the recipe | +| R1 | powershell | `wsl.exe -d '' --exec bash -lc 'node --version'` | R1: record the returned exit and output required by the recipe | | R1 | powershell | `$env:WSL_UTF8 = '1'` | as in W1 | | R1 | powershell | `wsl.exe --list --quiet` | read first: the throwaway name is listed, and it is not the real `` | | R1 | powershell | `wsl.exe --terminate ''` | the throwaway distribution stopped; no other distribution stops | | R1 | powershell | `wsl.exe --unregister ''` | the throwaway distribution removed, nothing else | +| R1 | powershell | `wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'` | on the surviving distribution: WSLInterop exists and /mnt/c/Windows/System32/cmd.exe /d /c ver launches with exit 0 | +| R1 | powershell | `if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }` | a failure stops the block; R1's recovery records the observations and stops for review | | R1 | powershell | `wsl.exe --list --verbose` | the throwaway name absent; the starred line equals W1's | | R1 | powershell | `$env:WSL_UTF8 = '1'` | as in W1 | | R1 | powershell | `wsl.exe --list --quiet` | read first: the throwaway name is listed, and it is not the real `` | @@ -417,24 +614,32 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | R1 | powershell | `(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\-rehearsal.tar').Hash.ToLowerInvariant()` | the export's SHA-256, recorded in `rehearsal` | | R1 | powershell | `(Get-Item -LiteralPath 'Z:\WSL\downloads\-rehearsal.tar').Length` | the export's size in bytes, recorded in `rehearsal` | | R1 | powershell | `wsl.exe --unregister ''` | the throwaway distribution removed, nothing else | +| R1 | powershell | `wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'` | on the surviving distribution: WSLInterop exists and /mnt/c/Windows/System32/cmd.exe /d /c ver launches with exit 0 | +| R1 | powershell | `if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }` | a failure stops the block; R1's recovery records the observations and stops for review | | R1 | powershell | `wsl.exe --list --verbose` | the throwaway name absent; the starred line equals W1's | +| R1 | sh | `test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver` | only after a failed removal check, natively on the survivor: the check again, its output recorded | +| R1 | sh | `ls /proc/sys/fs/binfmt_misc` | the registered formats, recorded: whether `WSLInterop` is among them | +| R1 | sh | `systemctl status systemd-binfmt.service --no-pager` | the unit's state and log lines, recorded; then stop for review and import or provision nothing | +| P1 | sh | `for RELEASE in 26.04.1 24.04.5; do` | both signed-sums arms, each in an empty temporary directory | | P1 | sh | `SUMS_DIR="$(mktemp -d)"` | an empty temporary directory, also `gpgv`'s home | -| P1 | sh | `curl -fsSL -o "$SUMS_DIR/SHA256SUMS" https://releases.ubuntu.com/24.04.5/SHA256SUMS` | exit 0 | -| P1 | sh | `curl -fsSL -o "$SUMS_DIR/SHA256SUMS.gpg" https://releases.ubuntu.com/24.04.5/SHA256SUMS.gpg` | exit 0 | +| P1 | sh | `curl -fsSL -o "$SUMS_DIR/SHA256SUMS" "https://releases.ubuntu.com/$RELEASE/SHA256SUMS" \|\| exit 1` | P1: record the returned exit and output required by the recipe | +| P1 | sh | `curl -fsSL -o "$SUMS_DIR/SHA256SUMS.gpg" "https://releases.ubuntu.com/$RELEASE/SHA256SUMS.gpg" \|\| exit 1` | P1: record the returned exit and output required by the recipe | | P1 | sh | `gpgv --homedir "$SUMS_DIR" --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg "$SUMS_DIR/SHA256SUMS.gpg" "$SUMS_DIR/SHA256SUMS"` | exit 0 and `Good signature` by `843938DF228D22F7B3742BC0D94AA3F0EFE21092`; `BAD signature` or a missing key stops the run | -| P1 | sh | `grep ' \*ubuntu-24\.04\.5-wsl-amd64\.wsl$' "$SUMS_DIR/SHA256SUMS"` | the signed line, carrying the hash W2 pins | +| P1 | sh | `if [ "$?" -ne 0 ]; then exit 1; fi` | nonzero interop verification stops recovery | +| P1 | sh | `grep -F " *ubuntu-$RELEASE-wsl-amd64.wsl" "$SUMS_DIR/SHA256SUMS" \|\| exit 1` | each release's exact signed image line from the controlled pins | +| P1 | sh | `done` | P1: record the returned exit and output required by the recipe | | P2 | sh | `RENDER_DIR="$(mktemp -d)"` | an empty temporary directory | | P2 | sh | `python3 -c 'import string, sys; sys.stdout.write(string.Template(open(sys.argv[1], encoding="utf-8").read()).substitute(WSL_USER=sys.argv[2]))' adoption/templates/wsl/cloud-init.user-data.template '' > "$RENDER_DIR/.user-data"` | the bytes W3 writes, rendered in the workstation | | P2 | sh | `cloud-init --version` | the workstation's cloud-init version, recorded | | P2 | sh | `cloud-init schema -c "$RENDER_DIR/.user-data"` | `Valid schema` and exit 0; `Invalid user-data` (exit 1) stops the run | | P2 | sh | `sha256sum "$RENDER_DIR/.user-data"` | the render's SHA-256, equal to W3's | -| P3 | sh | `sudo journalctl -k -b 0 --no-pager \| grep hv_storvsc \| grep -vc 'registering driver hv_storvsc'` | the count, recorded as the baseline (a count of `0` makes the last `grep` exit 1) | -| P3 | sh | `sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname \| grep hv_storvsc \| grep -v 'registering driver hv_storvsc' \| tail -n 1` | the newest error line with its kernel time in brackets, or nothing | +| P3 | sh | `sudo journalctl -k -b 0 --no-pager \| grep hv_storvsc \| grep -Evc 'registering driver hv_storvsc\|[Cc]ommand line:'` | the count, recorded as the baseline (a count of `0` makes the last `grep` exit 1) | +| P3 | sh | `sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname \| grep hv_storvsc \| grep -Ev 'registering driver hv_storvsc\|[Cc]ommand line:' \| tail -n 1` | the newest error line with its kernel time in brackets, or nothing | | P3 | sh | `cat /proc/uptime` | the seconds since boot as the first number; minus the line's kernel time, an age under 3600 s stops the run (microsoft/WSL#41482) | | P3 | sh | `swapon --show` | recorded; active swap is the issue's condition, not a failure by itself | | W1 | powershell | `New-Item -ItemType Directory -Force -Path 'Z:\WSL\downloads'` | the folder for the transcript and the image exists | | W1 | powershell | `$env:WSL_UTF8 = '1'` | wsl.exe writes UTF-8 instead of UTF-16 | -| W1 | powershell | `wsl.exe --version` | `WSL version:` 2.4.10 or later; lines recorded | +| W1 | powershell | `wsl.exe --version` | WSL version: 3.0.1 or later for a second systemd distribution; older stops before W4 or any W6 import | | W1 | powershell | `wsl.exe --list --verbose` | the starred line is recorded as the default | | W1 | powershell | `wsl.exe --list --quiet` | `` is not listed | | W1 | powershell | `Test-Path -LiteralPath 'Z:\WSL\'` | `False` | @@ -443,15 +648,31 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | W1 | powershell | `Select-String -LiteralPath (Join-Path $env:USERPROFILE '.ubuntupro\.cloud-init\agent.yaml') -Pattern '^[A-Za-z_][A-Za-z0-9_-]*:' -ErrorAction SilentlyContinue` | no output, or the top-level keys recorded and neither `users:` nor `write_files:` among them | | W1 | powershell | `Select-String -LiteralPath (Join-Path $env:USERPROFILE '.wslconfig') -Pattern '^\s*\[', '^\s*instanceIdleTimeout\s*=', '^\s*vmIdleTimeout\s*=' -ErrorAction SilentlyContinue` | a read only: the section headers and the two idle keys, or nothing; recorded as `idle_keys` | | W1 | powershell | `Get-PSDrive -Name Z \| Select-Object -Property Name, Used, Free` | `Free` recorded | +| W1 | sh | `id -u` | planned default uid 1000; otherwise stop before W4 or any W6 import | +| W1 | sh | `systemctl is-system-running` | running with no failed unit, or degraded with the failed set contained in {systemd-binfmt.service, getty@tty1.service}; otherwise stop | +| W1 | sh | `systemctl --failed --no-legend --plain` | first column of each row, or none; failed getty only as a prior collision with Result=start-limit-hit | +| W1 | sh | `systemctl is-active "user@$(id -u).service"` | active; otherwise stop before W4 or any W6 import | +| W1 | sh | `readlink /proc/self/ns/cgroup` | not cgroup:[4026531835], PROC_CGROUP_INIT_INO; necessary, with W5 required to prove isolation | +| W1 | sh | `systemctl show getty@tty1.service -p Result -p NRestarts` | only when the baseline includes failed getty: Result=start-limit-hit; record NRestarts in optional getty_tty1_result; another result stops | | W2 | powershell | `$ProgressPreference = 'SilentlyContinue'` | no progress rendering during the download | -| W2 | powershell | `Invoke-WebRequest -UseBasicParsing -Uri 'https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl' -OutFile 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl'` | 388,975,696 bytes saved | -| W2 | powershell | `Invoke-WebRequest -UseBasicParsing -Uri 'https://releases.ubuntu.com/24.04.5/SHA256SUMS' -OutFile 'Z:\WSL\downloads\SHA256SUMS'` | the checksum list saved | -| W2 | powershell | `$Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e'` | the pinned value | -| W2 | powershell | `$Published = (Select-String -LiteralPath 'Z:\WSL\downloads\SHA256SUMS' -Pattern ' \*ubuntu-24\.04\.5-wsl-amd64\.wsl$').Line.Split(' ')[0]` | Canonical's value | -| W2 | powershell | `$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content \| ConvertFrom-Json).ModernDistributions.Ubuntu \| Where-Object Name -eq 'Ubuntu-24.04'` | Microsoft's entry at the pinned commit | -| W2 | powershell | `$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl').Hash.ToLowerInvariant()` | the computed value | -| W2 | powershell | `"computed $Actual published $Published listed $($Listed.Amd64Url.Sha256) url $($Listed.Amd64Url.Url)"` | one hash three times and the 24.04.5 URL | -| W2 | powershell | `if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected) { throw 'sha256 mismatch: do not install' }` | no `throw` | +| W2 | powershell | `$Release = ''` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `switch ($Release) {` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `'26.04.1' { $Expected = '48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104'; $CatalogName = 'Ubuntu-26.04' }` | the exact hash and catalog entry for the single clean-install default | +| W2 | powershell | `'24.04.5' { $Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e'; $CatalogName = 'Ubuntu-24.04' }` | the exact hash and catalog entry for the release-caused-failure rollback only | +| W2 | powershell | `default { throw 'unsupported release: do not download or install' }` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `}` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$Image = "ubuntu-$Release-wsl-amd64.wsl"` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$ImagePath = "Z:\WSL\downloads\$Image"` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$ImageUrl = "https://releases.ubuntu.com/$Release/$Image"` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$SumsPath = "Z:\WSL\downloads\$Release-SHA256SUMS"` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `Invoke-WebRequest -UseBasicParsing -Uri $ImageUrl -OutFile $ImagePath` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `Invoke-WebRequest -UseBasicParsing -Uri "https://releases.ubuntu.com/$Release/SHA256SUMS" -OutFile $SumsPath` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$Published = (Select-String -LiteralPath $SumsPath -Pattern (' \*' + [regex]::Escape($Image) + '$')).Line.Split(' ')[0]` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content \| ConvertFrom-Json).ModernDistributions.Ubuntu \| Where-Object Name -eq $CatalogName` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $ImagePath).Hash.ToLowerInvariant()` | W2: record the returned exit and output required by the recipe | +| W2 | powershell | `"computed $Actual published $Published listed $($Listed.Amd64Url.Sha256) url $($Listed.Amd64Url.Url)"` | the selected release's exact hash three times and its pinned release URL | +| W2 | powershell | `if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected -or $Listed.Amd64Url.Url -ne $ImageUrl) { throw 'sha256 mismatch: do not install' }` | no throw: whole-image, signed-publication, catalog hash and URL match | +| W2 | powershell | `(Get-Item -LiteralPath $ImagePath).Length` | actual selected-image byte count for this host run; run 2 observed 418,495,746 bytes for 26.04.1 | | W3 | powershell | `New-Item -ItemType Directory -Force -Path (Join-Path $env:USERPROFILE '.cloud-init')` | the folder exists | | W3 | powershell | `$Text = [System.IO.File]::ReadAllText('\adoption\templates\wsl\cloud-init.user-data.template').Replace('${WSL_USER}', '')` | the template rendered in memory | | W3 | powershell | `[System.IO.File]::WriteAllText((Join-Path $env:USERPROFILE '.cloud-init\.user-data'), $Text)` | UTF-8 without a byte order mark | @@ -460,14 +681,17 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | W3 | powershell | `(Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $env:USERPROFILE '.cloud-init\.user-data')).Hash.ToLowerInvariant()` | equal to P2's `sha256sum` value | | W3 | powershell | `Select-String -LiteralPath (Join-Path $env:USERPROFILE '.cloud-init\.user-data') -SimpleMatch -Pattern '${'` | no output | | W4 | powershell | `$env:WSL_UTF8 = '1'` | as in W1 | -| W4 | powershell | `wsl.exe --install --from-file 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl' --name '' --location 'Z:\WSL\' --no-launch` | `Installing: ...`, then `Distribution successfully installed. ...` | +| W4 | powershell | `wsl.exe --install --from-file 'Z:\WSL\downloads\ubuntu--wsl-amd64.wsl' --name '' --location 'Z:\WSL\' --no-launch` | W4: record the returned exit and output required by the recipe | | W4 | powershell | `$LASTEXITCODE` | `0` | | W4 | powershell | `wsl.exe --list --verbose` | `` `Stopped`, version 2; the starred line unchanged | | W5 | powershell | `$env:WSL_UTF8 = '1'` | as in W1 | -| W5 | powershell | `cmd.exe /d /c "wsl.exe -d < NUL"` | `Provisioning the new WSL instance `; no prompt marker | +| W5 | powershell | `cmd.exe /d /c "wsl.exe -d < NUL"` | Provisioning the new WSL instance ; no prompt marker; wsl: Failed to start the systemd user session stops even with exit 0 | | W5 | powershell | `$LASTEXITCODE` | `0`; nonzero with the markers means W6 | | W5 | powershell | `wsl.exe -d '' --exec id -un` | `` | | W5 | powershell | `wsl.exe -d '' --exec id -u` | `1000` | +| W5 | powershell | `wsl.exe -d '' --exec systemctl is-enabled getty@tty1.service` | masked; exit 1 is normal for a masked unit; otherwise W5 recovery | +| W5 | powershell | `wsl.exe -d '' --exec systemctl show getty@tty1.service -p LoadState -p ActiveState -p NRestarts` | LoadState=masked, ActiveState=inactive, NRestarts=0; otherwise W5 recovery | +| W5 | powershell | `wsl.exe -d '' -u root --exec cloud-init --version` | selected-image packaged cloud-init version; not the P2 workstation version | | W5 | powershell | `wsl.exe -d '' -u root --exec cloud-init status --long` | `status: disabled` and `boot_status_code: disabled-by-marker-file`; proves the marker, not the run | | W5 | powershell | `wsl.exe -d '' -u root --exec cat /var/lib/cloud/data/result.json` | `"datasource": "DataSourceWSL"` and `"errors": []`: the run completed without errors | | W5 | powershell | `wsl.exe -d '' -u root --exec cat /var/lib/cloud/data/status.json` | each of the four stages `finished` with empty `errors`; `recoverable_errors` recorded | @@ -476,7 +700,33 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | W5 | powershell | `wsl.exe -d '' -u root --exec ls -l /etc/cloud/cloud-init.disabled` | the marker exists | | W5 | powershell | `wsl.exe -d '' -u root --exec sudo -l -U ''` | `(ALL) NOPASSWD: ALL` | | W5 | powershell | `wsl.exe --list --verbose` | the starred line equals W1's | -| W5 | sh | `sudo journalctl -k -b 0 --no-pager \| grep hv_storvsc \| grep -vc 'registering driver hv_storvsc'` | both paths: equal to P3's baseline; a larger count is exposure to microsoft/WSL#41482, recorded with the new lines, and holds the run before stage 2 | +| W5 | sh | `systemctl is-active ''` | natively on the already-running distribution: active and exit 0 | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-active ''` | the same system unit is active in the new distribution, exit 0 | +| W5 | sh | `cat '/sys/fs/cgroup/system.slice//cgroup.procs'` | read from the already-running distribution while both run: local process ids, no exact 0 entry; empty or unreadable stops | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active "user@$(id -u).service"'` | the new default user has uid 1000 and user@1000.service is active, exit 0; otherwise W5 recovery | +| W5 | sh | `id -u` | workstation uid, equal to host.workstation_baseline; record its own exit and printed value | +| W5 | sh | `systemctl is-system-running` | workstation system_state, equal to host.workstation_baseline; record its own exit and printed value | +| W5 | sh | `systemctl --failed --no-legend --plain` | workstation failed_units, equal to host.workstation_baseline; record its own exit and printed value | +| W5 | sh | `systemctl is-active "user@$(id -u).service"` | workstation user_manager, equal to host.workstation_baseline; record its own exit and printed value | +| W5 | sh | `readlink /proc/self/ns/cgroup` | workstation cgroup_namespace, equal to host.workstation_baseline; record its own exit and printed value | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec id -u` | same uid as the workstation (1000) | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-system-running` | running with no failed unit, or degraded with systemd-binfmt.service as its only failed unit; its message is proved at F1 | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl --failed --no-legend --plain` | first column of each row, or none; getty@tty1.service must not be failed | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec sh -c 'systemctl is-active "user@$(id -u).service"'` | new default user manager active; paired observation with its own exit | +| W5 | sh | `/mnt/c/Windows/System32/wsl.exe -d '' --exec readlink /proc/self/ns/cgroup` | different from the workstation namespace and not cgroup:[4026531835] | +| W5 | powershell | `$env:WSL_UTF8 = '1'` | W5: record the returned exit and output required by the recipe | +| W5 | powershell | `wsl.exe --list --quiet` | W5: record the returned exit and output required by the recipe | +| W5 | powershell | `wsl.exe --terminate ''` | any failed W5 proof: terminate the new distribution, never stop or restart a system unit in either distribution | +| W5 | powershell | `wsl.exe --export '' 'Z:\WSL\downloads\-w5-failed.tar'` | the failed W5 state saved before unregister; nonzero export prevents deletion | +| W5 | powershell | `$LASTEXITCODE` | W5: record the returned exit and output required by the recipe | +| W5 | powershell | `if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' }` | export failure stops with the name still registered; otherwise continue to hash, size and unregister | +| W5 | powershell | `(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\-w5-failed.tar').Hash.ToLowerInvariant()` | record the export hash in w5_failure_export | +| W5 | powershell | `(Get-Item -LiteralPath 'Z:\WSL\downloads\-w5-failed.tar').Length` | record the export size in w5_failure_export | +| W5 | powershell | `wsl.exe --unregister ''` | any failed W5 proof: guarded export succeeded; remove only the new distribution and immediately check surviving interop | +| W5 | powershell | `wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'` | on the surviving distribution: WSLInterop exists and /mnt/c/Windows/System32/cmd.exe /d /c ver launches with exit 0 | +| W5 | powershell | `if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }` | a failure stops the block; R1's recovery records the observations and stops for review | +| W5 | powershell | `wsl.exe --list --verbose` | W5: record the returned exit and output required by the recipe | +| W5 | sh | `sudo journalctl -k -b 0 --no-pager \| grep hv_storvsc \| grep -Evc 'registering driver hv_storvsc\|[Cc]ommand line:'` | On both paths, record P3's baseline before the import and `second_count` after the first launch. An increase confined to the window in which the new disk is attached, before cloud-init starts, is recorded with the new lines and does not stop the run. Any storage error after that window, or any provisioning step that fails with a storage cause, stops the run. | | W6 | powershell | `$env:WSL_UTF8 = '1'` | as in W1 | | W6 | powershell | `wsl.exe --list --quiet` | read before unregistering: `` is the new distribution | | W6 | powershell | `wsl.exe --terminate ''` | `` stopped before the export; no other distribution stops | @@ -486,8 +736,11 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | W6 | powershell | `(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\WSL\downloads\-failed.tar').Hash.ToLowerInvariant()` | the export's SHA-256, recorded in `failed_attempt_export` | | W6 | powershell | `(Get-Item -LiteralPath 'Z:\WSL\downloads\-failed.tar').Length` | the export's size in bytes, recorded in `failed_attempt_export` | | W6 | powershell | `wsl.exe --unregister ''` | `` removed, nothing else | -| W6 | powershell | `wsl.exe --import '' 'Z:\WSL\' 'Z:\WSL\downloads\ubuntu-24.04.5-wsl-amd64.wsl' --version 2` | exit 0 | +| W6 | powershell | `wsl.exe -d '' --exec sh -c 'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'` | on the surviving distribution: WSLInterop exists and /mnt/c/Windows/System32/cmd.exe /d /c ver launches with exit 0 | +| W6 | powershell | `if ($LASTEXITCODE -ne 0) { throw 'interop failed: recover in the surviving distribution before continuing' }` | a failure stops the block; R1's recovery records the observations and stops for review | +| W6 | powershell | `wsl.exe --import '' 'Z:\WSL\' 'Z:\WSL\downloads\ubuntu--wsl-amd64.wsl' --version 2` | W6: record the returned exit and output required by the recipe | | W6 | powershell | `wsl.exe -d '' -u root --exec cloud-init status --wait --long` | `done` or `error` when cloud-init ran, `disabled` by `disabled-by-generator` when it found no datasource; recorded (open question 1) | +| W6 | sh | `systemctl mask --now getty@tty1.service` | the imported getty masked; first boot already started it, so W5 paired proof after relaunch must show workstation unchanged | | W6 | sh | `id -u '' \|\| useradd --create-home --uid 1000 --groups adm,cdrom,sudo,dip,plugdev --shell /bin/bash ''` | uid 1000 exists | | W6 | sh | `printf '%s ALL=(ALL) NOPASSWD:ALL\n' '' > /etc/sudoers.d/90-wsl-default-user` | the drop-in written | | W6 | sh | `chmod 0440 /etc/sudoers.d/90-wsl-default-user` | mode 0440 | @@ -504,20 +757,24 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | W7 | powershell | `New-Item -ItemType File -Path '\\wsl.localhost\\home\\wsl-owner-probe'` | an empty file created from Windows under the user's home | | W7 | powershell | `wsl.exe -d '' --exec stat -c %u:%g '/home//wsl-owner-probe'` | `1000:1000`; `0:0` is recorded and stops every write from Windows | | W7 | powershell | `Remove-Item -LiteralPath '\\wsl.localhost\\home\\wsl-owner-probe'` | the probe file deleted | -| F1 | sh | `systemctl is-system-running --wait` | `running` | -| F1 | sh | `systemctl --failed --no-legend` | no output | +| F1 | sh | `systemctl is-system-running --wait` | `running` with no failed unit; or `degraded` (exit 1) only when the next two rows show `systemd-binfmt.service` as the only failed unit, with its read-only flush message | +| F1 | sh | `systemctl --failed --no-legend --plain` | no output when `running`; when `degraded`, a list of exactly `systemd-binfmt.service`; any other failed unit stops the run | +| F1 | sh | `journalctl -b 0 -t systemd-binfmt --no-pager -n 4` | when `degraded`: the unit's log holds `Failed to flush binfmt_misc rules, ignoring: Read-only file system`; read without `sudo` through the `adm` group, or repeated once with `sudo` when only the journal's permission notice prints | | F1 | sh | `systemctl list-unit-files --type=service --no-pager` | the service list prints | +| F1 | sh | `sudo journalctl -b 0 -t systemd-binfmt --no-pager -n 4` | only after an unreadable degraded journal: record both outputs and judge this read-only flush message | | F2 | sh | `sudo loginctl enable-linger "$(id -un)"` | exit 0 | | F2 | sh | `loginctl show-user "$(id -un)" --property=Linger --value` | `yes` | | F2 | powershell | `$env:WSL_UTF8 = '1'` | as in W1 | | F2 | powershell | `foreach ($Poll in 1..12) { Start-Sleep -Seconds 10; [DateTime]::UtcNow.ToString('HH:mm:ss'); wsl.exe --list --running --quiet }` | twelve times and lists with no client attached; whether `` stays listed, recorded as `idle_observation` | | F3 | sh | `stat -c '%U %F' "/run/user/$(id -u)" "/run/user/$(id -u)/bus"` | ` directory`, then ` socket` | | F3 | sh | `systemctl --user is-system-running --wait` | `running` | +| F3 | sh | `docker --context rootless info --format '{{json .SecurityOptions}}'` | exit 0 and name=rootless before attempting the container; otherwise stop | +| F3 | sh | `docker --context rootless run --rm hello-world` | as the user without sudo: Hello from Docker! and exit 0; failure stops with issue 41492; owed on the first run after update | | F4 | sh | `sudo apt-get update` | exit 0 | | F4 | sh | `sudo apt-get install -y --no-install-recommends ca-certificates curl git tar gzip xz-utils jq libatomic1 uidmap` | exit 0 | | F4 | sh | `dpkg-query -W -f='${Package} ${Version}\n' jq libatomic1 uidmap` | three versions | | F5 | sh | `grep "^$(id -un):" /etc/subuid /etc/subgid` | a range in both files, or nothing | -| F5 | sh | `sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(id -un)"` | only when the first `grep` printed nothing | +| F5 | sh | `grep -q "^$(id -un):" /etc/subuid \|\| sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(id -un)"` | only when the first `grep` printed nothing | | F5 | sh | `grep "^$(id -un):" /etc/subuid /etc/subgid` | 65,536 ids in both files | | F6 | sh | `test -e ~/.bash_profile \|\| printf '%s\n' 'if [ -r "$HOME/.profile" ]; then . "$HOME/.profile"; fi' > ~/.bash_profile` | written only when absent | | F6 | sh | `cat ~/.bash_profile` | exactly the hand-off line | @@ -534,7 +791,7 @@ the recipe. P1 to P3's commands ran as artifact checks in the workstation distri | F9 | sh | `adoption/bootstrap-linux.sh --profile ''` | stage 2 (bootstrap step 2) | | F9 | sh | `adoption/bootstrap-linux.sh --profile '' --configure-full-profile --host ''` | stage 2, after native sign-in | | F10 | powershell | `wsl.exe -d '' -u '' --exec /bin/bash -lc 'type -P claude codex'` | two absolute paths | -| F10 | powershell | `wsl.exe -d '' -u '' --exec /bin/bash -lc 'for p in $(type -P claude codex); do test -f $p && test -x $p && echo executable: $p; done'` | `executable:` and each path: both are regular executable files | +| F10 | powershell | `wsl.exe -d '' -u '' --exec /bin/bash -lc 'for n in claude codex; do p=$(type -P $n); [[ -f $p && -x $p ]] && echo executable: $p; done'` | `executable:` and each path: both are regular executable files | | F11 | sh | `test -x "${ECO_INSTALL_ROOT:-$HOME/.local/share/codex-ecosystem}/bin/jcodemunch-mcp"` | exit 0; exit 1 means `not installed`: record it and skip the rest of F11 | | F11 | sh | `cd ~/code/native-agent-stack` | the clone, the project the local scope belongs to | | F11 | sh | `claude mcp add --scope local jcodemunch -e "CODE_INDEX_PATH=$HOME/.code-index" -e JCODEMUNCH_SHARE_SAVINGS=0 -- "${ECO_INSTALL_ROOT:-$HOME/.local/share/codex-ecosystem}/bin/jcodemunch-mcp"` | an `Added ...` line; on a re-run the name already exists | @@ -556,22 +813,31 @@ Each one stays open until a host run records the observation named here. with it; changing the global file is the user's decision, outside the recipe. W1 records both keys, and F2 lists the running distributions for two minutes with no client attached and records whether `` stays listed. Whether `wsl.exe --list --running` itself counts as a client is not verified. -3. **binfmt after terminate.** Whether binfmt registrations survive `wsl --terminate ` (`protectBinfmt`). Record - `ls /proc/sys/fs/binfmt_misc` before and after a terminate. -4. **Subordinate ids from `useradd`.** Whether `useradd` allocated subordinate ids on 24.04.5. cloud-init 26.1 creates +3. **Interop after unregister.** On WSL 2.7.13 the surviving distribution lost the VM-wide `WSLInterop` registration + after the failed rehearsal was unregistered. Restarting its `systemd-binfmt` restored Windows execution there. Every + R1, W5 and W6 unregister now checks the registration and a Windows executable. On the adopted release the + registration is protected (microsoft/WSL#40621) and that restart exits 1, so a failed check stops for review with + R1's records. This is a required observation and a stop, rather than an open binfmt question. +4. **Subordinate ids from `useradd`.** Whether `useradd` allocated subordinate ids on the selected release. cloud-init 26.1 creates users with `useradd` (`cloudinit/distros/__init__.py:683` at tag 26.1). useradd(8) says it allocates `SUB_UID_COUNT` ids when `/etc/subuid` exists, and the image ships both; F5 records which tool wrote the range. -5. **The 26.04.1 comparison.** The bounded unit of Alternatives 1. +5. **Both image comparisons.** The preregistered R1 criteria cover the 26.04.1 default and 24.04.5 rollback on + throwaway names. Full acceptance on the updated host remains owed; the older 24.04.5 receipts do not settle it. 6. **File ownership after a terminate (microsoft/WSL#40941).** Whether `wsl --terminate ` clears the 0:0 owner of files Windows creates after the first-run setup. The fix's pull request says the state "only recovers after a distro - termination", the issue's reproduction says it lasts "until the next wsl --shutdown", and no one has observed a - terminate. W7's probe after its terminate records it, first in the rehearsal. + termination", and the issue's reproduction says it lasts "until the next wsl --shutdown". The 2.7.13 rehearsal + returned `1000:1000` after W7's terminate; the updated host must repeat that observation. 7. **Kernel storage errors on this host (microsoft/WSL#41482).** Whether `hv_storvsc` errors break a first launch on this kernel. On 2026-10-01 P3's count in the workstation's journal of the current boot was 21 (Evidence classes): one burst of `cmd 0x2a` write errors (`WRITE_10`) on 2026-09-24 around 03:20Z and none since, where the issue shows `cmd 0x28` read errors (`READ_10`). Under the coordinator's rule that count is P3's baseline, and its newest line, - about 7.4 days old, does not stop the run. W5's second count, first in the rehearsal, shows whether a first launch on - this kernel meets new errors; the question stays open until a run records it. + about 7.4 days old, did not stop that authoring check. The later rehearsal's baseline 7 and second count 13 retain + their own boot scope; the six new lines preceded cloud-init in the attachment window. Future runs must establish + their own window, retain its lines and stop on later errors or a provisioning failure with a storage cause. +8. **Isolation with two distributions running on 3.0.1.** Whether the two distributions have distinct cgroup + namespaces, healthy managers and the same uid while both run. The updated host's reading covers one distribution + only (Updated-host correction). W5's paired record, `paired_isolation`, is the observation that settles it, and W1's + baseline shows whether the new distribution disturbed the workstation's values. ## Completeness critic (2026-10-01) @@ -617,7 +883,7 @@ searches were samples of eight results per query, not sweeps. P1 to P3 verbatim from 11:50:17Z to 11:50:19Z, and P3 again in its final form at 12:19:29Z, extracted from the page with `` as `example` and `` as `rehearsal-example`, `TMPDIR` in its scratch area and `sudo` without a prompt, each command followed by its exit code: - - P1: both `curl` lines exited 0. The `gpgv` line exited 0: + - P1 for 24.04.5 in that earlier recipe: both `curl` lines exited 0. The `gpgv` line exited 0: `gpgv --homedir "$SUMS_DIR" --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg "$SUMS_DIR/SHA256SUMS.gpg" "$SUMS_DIR/SHA256SUMS"` printed `Signature made Tue Sep 15 15:11:12 2026 EDT`, `using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092` and `Good signature from "Ubuntu CD Image Automatic Signing Key (2012) "`. The `grep` exited 0 and @@ -665,8 +931,25 @@ searches were samples of eight results per query, not sweeps. The one-hour threshold is this recipe's choice, not an upstream figure; microsoft/WSL#41482 names no age. - **Local integration.** `tests/test_wsl_new_distro_recipe.py`, checks over repository text and an in-memory render; it runs nothing on a host. -- **Not run.** Stage 1 and the first boot: nothing in stage 1 has run on a host, the rehearsal included. The first host - run records `native_proven` evidence in the stage-1 receipt. +- **Reported artifact verification, W-IMG.** The retained integration receipt contains both signature verification + exit codes, the signer fingerprint and the reported 26.04.1 stream hash match. It retains no verification output + for that 26.04.1 signature check; it does not carry the signature-output evidence of the historical 24.04.5 P1 + check above. +- **Retained rehearsal, 2026-10-02.** The separate stage-1 receipt records Ubuntu 26.04.1's schema, install, launch + and owner results on WSL 2.7.13, then F1's failed user manager. Its coordinator review establishes the shared + cgroup diagnosis. The later unregister/interop observations and the initial namespace observation are retained in + the supplied repair brief; they are not invented as output in the earlier worker receipt. +- **Reported host observation, 2026-10-02.** The coordinator's brief gives the five reads of the Updated-host + correction, taken on WSL 3.0.1.0 with kernel 6.18.40.1-1 in the workstation's distribution from 06:39Z to 06:57Z. They + cover one distribution and are not a run of the recipe. The third follow-up's unit repeated the five commands in the + same distribution afterwards and read the same values: `1000`; `degraded` with exit 1; `systemd-binfmt.service` as + the only failed unit; `active`; `cgroup:[4026532183]`. It also ran + `journalctl -b 0 -t systemd-binfmt --no-pager | tail -n 4` as that distribution's default user, who is not in + `adm`, and read only the notice that other users' and the system's messages are hidden, which is why F1 states its + dependence on `adm` and repeats the line once with `sudo` when only that notice prints. Nothing was observed with two + distributions running. +- **Not run.** The corrected recipe on WSL 3.0.1 with a second distribution, including W5's paired record, F1's accepted + `degraded` and a rootless container in the new distribution, remains owed. F11 rests on a source read and has not run on a new distribution. The addendum records a run of the same command form on 2026-09-25, under a temporary `CLAUDE_CONFIG_DIR` on one WSL2 host. - **Cross-family review.** GPT-6.1 Sol, read-only, 2026-10-01, of PR #569, returned `needs_changes` with three findings. @@ -833,3 +1116,209 @@ URL and file:line and is quoted here as it quotes them, and by the unit where no - By the unit: the pinned raw files of MicrosoftDocs/WSL (HTTP 200 for build-custom-distro, basic-commands, use-custom-distro, systemd and wsl-config), `adoption/bootstrap-linux.sh:175-178` on this branch, and `/usr/include/scsi/scsi.h:59-60` (libc6-dev) for the SCSI command codes. + +Primary sources reused for W-IMG parameterization (2026-10-01): + +- https://releases.ubuntu.com/26.04.1/SHA256SUMS and https://releases.ubuntu.com/26.04.1/SHA256SUMS.gpg; + the retained integration receipt's reported signature exits/fingerprint and image stream hash match, with no + retained 26.04.1 signature output and no new download for this unit. +- https://documentation.ubuntu.com/release-notes/26.04/ and the Microsoft creation docs pinned above support creation, + not full-stack compatibility. https://ubuntu.com/wsl/docs/stable/howto/cloud-init/ is explicitly a 24.04/22.04 guide. +- https://github.com/ubuntu/wsl-setup/blob/73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8/wsl-setup and + https://github.com/ubuntu/wsl-setup/blob/73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8/ubuntu-insights.sh, tag `0.6.3`. +- https://learn.microsoft.com/en-us/windows/wsl/tutorials/gpu-compute and + https://docs.nvidia.com/cuda/wsl-user-guide/index.html (preserved CUDA on WSL 13.4 guide): native GPU visibility only; + `nvidia-smi` has limited WSL features and can be invoked at `/usr/lib/wsl/lib/nvidia-smi`. +- Existing `adoption/bootstrap.md`'s Python 3.13 prerequisite and `adoption/pins-linux-x86_64.json`'s official Node + 24.21.0 tarball pin; native uv `run --help` observation recorded above. No replacement installer was written. + +Sources for the rehearsal correction (2026-10-02), retained review and supplied primary references: + +- `origin/foundation/new-wsl-rehearsal-20261002` at `76b7c0b4ee45d3a6f0a60fa68d053e0f2039e8e4`, + `evidence/artifacts/new-wsl-rehearsal-20261002/coordinator-review.md` + and `stage1-receipt.json`; these were read with `git show`. The repair brief adds the interop recovery and + initial-namespace observation after the worker's stop. +- microsoft/WSL release notes at https://github.com/microsoft/WSL/releases/tag/2.9.8, + https://github.com/microsoft/WSL/releases/tag/2.9.13 and https://github.com/microsoft/WSL/releases/tag/3.0.1; + https://github.com/microsoft/WSL/pull/40519 and https://github.com/microsoft/WSL/pull/41512. +- https://github.com/microsoft/WSL/issues/41492, still open in the retained review; no 3.0.1 rootless acceptance. +- Linux `v6.18`, https://github.com/torvalds/linux/blob/v6.18/include/linux/proc_ns.h, `PROC_CGROUP_INIT_INO`. +- https://docs.docker.com/engine/security/rootless/, the supported setup and rootless Docker context already selected + by this recipe; the container check uses its native Docker CLI. +- This worktree's `evidence/artifacts/new-wsl-definitive-defaults-20261001/definitive-manifest.json`, + `cross:wsl-distro` / `base-distribution`: the single Ubuntu 26.04.1 default. The repair brief supplies the later + critics' confirmation and the release-caused-failure rollback condition. + +Sources for the updated-host correction (2026-10-02), read that day by the third follow-up's unit: + +- https://github.com/microsoft/WSL/pull/40621, "protect binfmt_misc from cross-distro wipe at shutdown" (merged): it + bind-mounts a read-only file over `/proc/sys/fs/binfmt_misc/status` in each per-distro mount namespace, so that the + write with which `systemd-shutdown` wiped the shared registry fails with `EROFS`. Read through the GitHub API. +- https://github.com/microsoft/WSL/issues/41226, "`systemd-binfmt.service` fails with "Read-only file system" on boot" + (closed), and a contributor's answer of 2026-08-04: "The systemd error is benign and won't affect registration of user + defined binfmt settings. Please ignore it if it's not affecting any functions." Read through the GitHub API. +- systemd `src/binfmt/binfmt.c`: `Failed to flush binfmt_misc rules, ignoring: %m` is line 253 of tag `v255` and line + 249 of tag `v259`. `tmpfiles.d/systemd.conf.in`: the `adm` group's read access to the system journal, lines 47-53 of + `v255` and 44-50 of `v259`. The 26.04.1 image packages systemd 259.5-0ubuntu3.4 (the experiment record); tag `v259` + was read, not `v259.5`. +- The coordinator's brief of 2026-10-02: the host observations of the Updated-host correction and the contract for the + three changes. + +## Amendment 2026-10-02 (rehearsal run 2 on WSL 3.0.1, and the scoped cross-family read) + +The coordinator accepted run 2 and the scoped cross-family read of `fed1e93c` for PR #593. This amendment records +their source reviews and host observations and changes the next run's commands and proofs; it is not another host +run. Earlier sections retain their dated evidence and planning statements. The observations below supersede their +statements that simultaneous distributions on 3.0.1 were unobserved. No image comparison has run; the criteria are +the preregistered ones with the amendments of 2026-10-02 made before any comparison ran. + +Run 2, 2026-10-02T08:32Z to 08:40Z, used a throwaway name, Ubuntu 26.04.1, path A, WSL 3.0.1.0 and kernel +6.18.40.1-1. P1, P2, W1, W2, W3, W4 and W5's path-A proofs passed: launch exit 0, uid 1000, cloud-init +26.1-0ubuntu3~26.04.1, retained `result.json` and `status.json` without errors, `Valid schema user-data`, +`/etc/wsl.conf`, the marker and sudo. It stopped at W5's paired rule and was removed through W5's recovery block. +The seven findings are: + +1. **P3 counted the kernel's command line.** The baseline of `2` was `kernel: Command line:` at 0.000000 s and + `kernel: Kernel command line:` at 0.018761 s, both naming `hv_storvsc.storvsc_max_hw_queues=4`. Neither is a + storage error. The old age rule would stop a run within one hour of boot on these echoes. P3's two filters and + W5's second count now exclude the registration line and `[Cc]ommand line:` with the same extended-regexp filter. +2. **Cgroup isolation held.** The workstation printed `cgroup:[4026532183]` and the new distribution + `cgroup:[4026532407]`; the common unit's `cgroup.procs` held one local pid and no `0`. Both user managers were + `active`, and the launch printed no user-session warning. This observes two distributions running together on + 3.0.1, while a complete corrected recipe run and F3 remain owed. +3. **The shared console failed the paired rule.** In the second the new distribution booted, both + `getty@tty1.service` units received SIGHUP and restarted five times, then showed `Result=start-limit-hit`. + Both system managers listed that unit and `systemd-binfmt.service` failed. The workstation changed from its W1 + baseline: `1000`, `degraded`, only `systemd-binfmt.service` failed, `active`, `cgroup:[4026532183]`. + Both expose `/dev/tty1` as character device 4,1 of the one VM kernel. Source review of the two running images + (workstation Ubuntu 24.04.5/systemd 255; new Ubuntu 26.04.1/systemd 259) found `TTYPath=/dev/tty1`, + `TTYVHangup=yes`, `Restart=always`, `RestartSec=0` and enablement in `/etc/systemd/system/getty.target.wants/`; + both `console-getty.service` units read `masked-runtime`. The workstation's getty stays failed until the next + WSL start; no unit action is permitted there by this page, and nothing uses the VM console. +4. **One real storage error appeared.** The second count was `3`. The new driver line, `cmd 0x2a`, `srb 0x4`, + host `0xc00000a1`, was at kernel time 7118.5 s in W4's install window. The disk was unmounted at 7126.9 s, + and cloud-init's `init-local` started at 7134.15 s. The written attachment-window rule recorded the line and + did not stop the run; the other two lines were the command-line echoes. +5. **Removal preserved interop.** Export and unregister both exited 0, and the survivor check passed. On 3.0.1 + `WSLInterop` survived both unregisters of 2026-10-02, in run 2 and probe E1, unlike the 2.7.13 rehearsal. + The check after every unregister and its stop-for-review rule remain required. +6. **The image size was observed.** W2 printed `418495746` bytes for `ubuntu-26.04.1-wsl-amd64.wsl`. +7. **Journal access depends on the user's group.** The workstation user is not in `adm`: without `sudo` the + identifier query printed the permission hint and `-- No entries --`; with `sudo` it printed + `Failed to flush binfmt_misc rules, ignoring: Read-only file system`. The new user belongs to `adm` through + the user-data. `journalctl -b 0 -t systemd-binfmt --no-pager -n 4`, without `tail`, printed the same lines. + +The mechanism's upstream sources were reviewed at microsoft/WSL tag `3.0.1`: + +- [`src/linux/init/init.cpp:363-365`](https://github.com/microsoft/WSL/blob/3.0.1/src/linux/init/init.cpp#L363-L365): + "Mask console-getty.service since /dev/tty devices are shared at the VM level across all distros. When multiple + distros are running, the second distro's getty fails because the tty is already held." + [PR #14490](https://github.com/microsoft/WSL/pull/14490), merged 2026-04-09, says "Fixes #13595". +- [`distributions/validate-modern.py:27-40`](https://github.com/microsoft/WSL/blob/3.0.1/distributions/validate-modern.py#L27-L40): + `DISCOURAGED_SYSTEM_UNITS` includes `console-getty.service` and does not include `getty@tty1.service`. +- A maintainer in [issue #13595](https://github.com/microsoft/WSL/issues/13595): "the best way to solve this would + probably to try to get distros to disable the `getty` and adjacent units in WSL". + +**Probe E1, 2026-10-02T08:42Z.** Outside the recipe, a second throwaway name used the same template with the new +`bootcmd`. Its rendered SHA-256 was `c0732303a0004121f9d373f7856454e3e59eeadea395bac46eb17e1671b32af4`. +It returned `Valid schema` on the workstation and `Valid schema user-data` in the image. The getty was `masked` +(`is-enabled` exit 1, normal for a masked unit), with `LoadState=masked`, `ActiveState=inactive` and `NRestarts=0`. +Only `systemd-binfmt.service` was failed. The journal recorded cloud-init creating the mask symlink and, 0.18 s later, +systemd's `Failed to start getty@tty1.service.`: the refused job of a masked unit, not a failed unit. Cloud-init +finished without errors; the user manager was `running`; namespaces were `cgroup:[4026532183]` and +`cgroup:[4026532410]`. The workstation's values did not change, and the probe was removed afterwards. + +Ordering is a source review of the pinned 26.04.1 image's own files: `cloud-init-network.service` has +`Before=systemd-user-sessions.service`, `getty@.service` has `After=systemd-user-sessions.service`, and +`/etc/cloud/cloud.cfg` lists `bootcmd` among `cloud_init_modules`. The workstation's cloud-init +26.1-0ubuntu1~24.04.1 has the same ordering in `cloud-init.service`; that is the version the 24.04.5 image packages, +but the 24.04.5 image's own unit files were not read. + +**Decision.** Mask `getty@tty1.service` in the new distribution through the template's `bootcmd`, during cloud-init's +network stage before systemd starts the unit. W5 proves the mask and retains all five paired observations with +separate exits. The workstation's five values must equal its W1 baseline. W1 accepts a failed workstation getty +only as an earlier collision's leftover with `Result=start-limit-hit`; it does not prove the workstation's unit +messages again, because that distribution is compared with itself and F1 proves the new distribution's message. +Any other baseline stops before W4 or any W6 import, including a uid other than 1000, an inactive user manager or +the initial cgroup namespace. Any failed W5 proof uses `w5_failure_export`, with `cause` (`cgroup`, `tty` or a short +text), and `-w5-failed.tar`; do not take path B for any such cause. Only the OOBE prompt failure permits W6. +On path B the root block masks the getty after its first boot has already started it, so W5's paired proof after +the relaunch decides whether the workstation changed; a difference stops and uses W5's recovery. + +**Run 3 (2026-10-02T10:54Z to 11:01Z, the repaired recipe, a third throwaway name).** P1 to P3, W1 to W5, W7 and F1 +to F5 passed. W5: launch exit 0; `systemctl is-enabled getty@tty1.service` printed `masked`; `LoadState=masked`, +`ActiveState=inactive`, `NRestarts=0`; the paired values were `1000`, `degraded`, {`getty@tty1.service`, +`systemd-binfmt.service`}, `active`, `cgroup:[4026532183]` for the workstation, equal to its W1 baseline, and `1000`, +`degraded`, {`systemd-binfmt.service`}, `active`, `cgroup:[4026532408]` for the new distribution. W7's owner probe +printed `1000:1000` on 3.0.1. F1's journal line printed the read-only flush message without `sudo`. F2: linger `yes`, +and the distribution was listed in all twelve idle polls (`.wslconfig` has `instanceIdleTimeout=-1`). F3: a user-owned +directory and socket, user manager `running`; after F4, F5 and Docker's rootless setup (Docker Engine 29.8.2), +`docker --context rootless info` listed `name=rootless` and `docker --context rootless run --rm hello-world` printed +`Hello from Docker!`: microsoft/WSL#41492 does not affect a new distribution on this host. The second storage count +rose by one line inside W4's install window (15573.8 s), as in run 2 and the probe. Second-instance probes: user +manager `running`, `/dev/dxg` present, `nvidia-smi` exit 0, system `Python 3.14.4`; uv and Node are not installed +before stage 2, so those two probes are owed. + +Run 3 also found three command defects, each corrected and re-run on the same distribution. (1) With the `bootcmd`, +cloud-init records one recoverable error, `Failed to wait for network`, and `cloud-init status --long` exits 2: +cloud-init waits for the network when user-data holds a `bootcmd` (26.1, `cloudinit/cmd/main.py:351-402`), and WSL +masks `systemd-networkd-wait-online.service` (`init.cpp:356-358`). It is harmless and now stated as expected in W5. +(2) Windows PowerShell 5.1 does not escape a double quote inside an argument to a native program, so R1's first probe +returned `stat: missing operand`, and the quoted F10 loop that this repair had introduced returned `unexpected EOF`; +both now carry no double quote (`stat -c %U,%F ...`; `[[ -f $p && -x $p ]]`) and printed the expected lines. (3) F5's +block ran `usermod` unconditionally although its text said otherwise; its second line now guards itself. + +**P3's age rule, one exception (added in the coordinator's review of this repair, 2026-10-02).** With the corrected +filter the workstation's kernel journal holds two real driver errors of the same shape, at 7118.5 s (run 2's install) +and 7678.6 s (probe E1's install): each `wsl.exe --install --from-file` on this host left one line in its install +window. P3 as written would stop a second arm that starts within the hour on its predecessor's line. P3 now lets a +run continue when the newest line's kernel time is one that an earlier run of the page recorded as its own +attachment-window line; a line no earlier receipt explains keeps the stop. Overturned when an install leaves more +than its one line, or lines appear outside an install window: then the count is exposure to microsoft/WSL#41482 and +the stop applies. F10's sentence now renames every profile of the example without naming a count, so it holds for +the example with three profiles and with five (pull request 604 added two resume profiles). + +Alternatives considered, with what would overturn each rejection: + +- **Accept getty as a second by-design failed unit in both distributions.** This hides a real cross-distribution + effect and changes the workstation's state at every import. Reconsider only if upstream evidence eliminates the + effect or a separate decision accepts that workstation change; a failed getty is not part of F1's pass condition. +- **Mask it in the workstation as well.** This changes the owner's running distribution and would also protect it + from other distributions' gettys. It is left to the owner. Reconsider if a run proves the new-distribution mask + takes effect after the unit started, with the owner's decision required for that broader change. +- **Use a kernel command-line or `.wslconfig` setting.** The page never changes `.wslconfig`; a kernel choice also + reaches every distribution. Reconsider only with an upstream-supported setting and a separate host-wide decision. +- **Change the image.** The page installs the official image unmodified. Reconsider if Canonical's official image + stops enabling the getty, or a separate image decision replaces that requirement. + +Drop the `bootcmd` when WSL masks `getty@tty1.service` itself or Canonical's image stops enabling it. If a run shows +the mask taking effect after the unit started, the proof fails and masking the workstation is reconsidered. +A host whose other systemd distributions retain an unmasked getty will see both fail when two of them run; changing +those units belongs to the host owner, outside this page. + +**Not established.** A live workstation getty surviving a new distribution's first boot follows from the masked +unit never starting, but was not observed: the workstation's getty had already failed in run 2 before E1. The +24.04.5 image's own unit files and path B's first boot are unobserved. A complete recipe run with the amended +template through F3 remains owed; E1 is a probe, not a recipe run. Full image comparisons and full-stack acceptance +remain owed. + +The scoped review's points and their repairs are: + +- W1's incomplete stop: W1's four baseline conditions and the checklist now stop every incompatible baseline; + tests remove the stop paragraph, uid, namespace and user-manager conditions separately. +- Receipt equality and abbreviated F1 exceptions: every workstation field requires equality to + `host.workstation_baseline`; the comparison table, receipt and experiment use F1's complete pass condition. +- Linux entry paths and F10 quoting: the launch forms use full Windows paths in Linux shells, and F10 tests each + quoted executable path without word splitting. +- Hidden exits: W1 and both W5 halves have five separate commands and receipt entries; F1 and its sudo fallback + use `journalctl -n 4` directly. +- Test blind spots: the new controls reject every weakened baseline/equality/message instruction, and the full + original quality rule is compared after removing the dated amendments. +- Preregistered label and stale framing: the page, checklist and named record sentence identify the amendments; + the W7 sentence describes the 2.7.13 rehearsal in the past, and overturn now concerns a later WSL update. + +The command table mirrors the amended operational commands. Historical command text in earlier evidence sections +remains as the command actually run then; it is not a command for the next run. The supplied primary reviews, the +official image's files and the installed commands above are the sources for this bounded repair. The existing +Python `unittest` harness tests repository contracts only; its controls do not qualify a host. The completeness +check leaves the same decision-changing gaps: the corrected full run through F3, the rollback image and path B. diff --git a/evidence/artifacts/new-wsl-dual-image-20261001/receipt.json b/evidence/artifacts/new-wsl-dual-image-20261001/receipt.json new file mode 100644 index 000000000..1c4bad0d0 --- /dev/null +++ b/evidence/artifacts/new-wsl-dual-image-20261001/receipt.json @@ -0,0 +1,44 @@ +{ + "schema_version": 1, + "kind": "new-wsl-dual-image-recipe-integration", + "accepted_main": "85543efe5abcddb7b7cddb14e8774e83b6758616", + "source_recipe_base": "798ac445307e2cd8eba6e74d7722ac0e16da02c7", + "evidence_class": "local_integration", + "source_patch_sha256": "019af42cc09aed576b26702c75537a1eed1ee0421ab0516f7aadd2211c480019", + "release_status": {"26.04.1": "provisional", "24.04.5": "provisional", "acceptance_precedence": null}, + "image_sha256": { + "26.04.1": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "24.04.5": "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e" + }, + "validation": [ + {"command": "python3 -m unittest tests.test_wsl_new_distro_recipe", "tests": 50, "native_exit_code": 0, "independent": true, "duration_seconds": 0.428}, + {"command": "python3 scripts/validate_convergence.py blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json --root . --json", "native_exit_code": 0, "independent": true, "observations": 0, "returned_output": {"records": [{"errors": [], "observations": 0, "path": "blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json", "valid": true}], "valid": true}}, + {"scope": "swapped selected release recipe and receipt digest rejection", "native_exit_code": 0, "independent": true}, + {"scope": "frozen seven-file patch application check on accepted main", "native_exit_code": 0}, + {"scope": "frozen seven-file patch application on accepted main", "native_exit_code": 0} + ], + "source_verification": { + "evidence_class": "reported signature verification exit codes and signer fingerprint; reported 26.04.1 stream hash match; no retained verification output", + "signed_checksum_verification_exit_codes": {"26.04.1": 0, "24.04.5": 0}, + "signer_fingerprint": "843938DF228D22F7B3742BC0D94AA3F0EFE21092", + "catalog_commit": "8bc98bc33b246fe66710eec9eaa1b24c323da987", + "wsl_setup_commit": "73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8", + "image_26_stream_hash_match": true, + "image_26_download_bytes": null + }, + "retained_failed_conditions": [ + {"scope": "original native Claude Opus actor", "native_exit_code": 1, "cause": "returned weekly limit", "changes_made": false}, + {"scope": "dual-image recipe regression before repair", "native_exit_code": 1, "tests": 2}, + {"scope": "integration commit secret scanner", "native_exit_code": 1, "cause": "public GPG signer fingerprint labeled as a key matched generic-api-key", "correction": "rename the receipt field to signer_fingerprint; preserve the public fingerprint and scanner"}, + {"scope": "dependent publication sequencing", "correction": "the first push after the rejected commit published only accepted main; wait for successful commit before publishing the repaired branch"} + ], + "not_run": ["new distro import or installation", "first boot", "systemd user lifecycle", "second isolated instance", "GPU runtime acceptance", "provider acceptance"], + "boundaries_preserved": ["export before unregister", "historical 24.04 receipts", "no global WSL shutdown or kernel change", "native operating-system consent", "Ubuntu insights consent guard"], + "sources": [ + "https://github.com/microsoft/WSL/blob/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json", + "https://releases.ubuntu.com/26.04.1/", + "https://releases.ubuntu.com/24.04.5/", + "https://github.com/ubuntu/wsl-setup/tree/73418e32bb48d514c2c2853fa7e5cacdcaf3dfe8", + "adoption/platforms/linux-wsl2-new-distro.md" + ] +} diff --git a/evidence/artifacts/new-wsl-rehearsal-20261002/coordinator-review.md b/evidence/artifacts/new-wsl-rehearsal-20261002/coordinator-review.md new file mode 100644 index 000000000..29fe473f3 --- /dev/null +++ b/evidence/artifacts/new-wsl-rehearsal-20261002/coordinator-review.md @@ -0,0 +1,64 @@ +# Coordinator's review of the 26.04.1 rehearsal (2026-10-02) + +The receipt beside this file is the worker's record and is unchanged. This note adds what the coordinator did and +checked afterwards. + +## What the rehearsal showed + +Stage 1 works for the 26.04.1 image on this host: signed sums, schema check, install, first launch through cloud-init +(path A) and the file-ownership probe all held. The first boot then stopped at F1: `systemctl is-system-running --wait` +printed `degraded`, and `user@1000.service` had failed with `Failed to spawn executor: Device or resource busy`. + +## Cause: distributions share one cgroup tree on WSL 2.7.13 + +Checked from the workstation's distribution at 2026-10-02T02:40Z, with a third distribution also running: + +- `/sys/fs/cgroup/system.slice/cron.service/cgroup.procs` printed `0 0`: one local process and two processes + from other process namespaces, in one cgroup. Same-named system units of every distribution share a cgroup. +- The other running distribution reports its `user@1000.service` as active with + `ControlGroup=/user.slice/user-1000.slice/user@1000.service`, the workstation's own path. +- The rehearsal's failed unit showed the workstation's accounting (about 1,840 tasks, 16 GiB), as the receipt records. + +So a second systemd distribution whose user has the same uid cannot start its user manager while the first one runs, +and a stop or restart of a system unit in one distribution acts on a cgroup that holds another distribution's +processes. + +## Upstream state (microsoft/WSL release notes, read 2026-10-02) + +- 2.9.8 (pre-release, 2026-08-24): "Protect critical WSL processes under heavy load with cgroup & isolate distro + cgroups" (pull request 40519). +- 2.9.13 (pre-release, 2026-09-25): "Create new namespaces for distro cgroups" (pull request 41512). +- 3.0.1 (stable, 2026-09-29) is the first stable release after those. The host runs 2.7.13; the 2.7 line (2.7.14, + 2026-09-11) has neither change. +- Issue 41492 (rootless Docker and Podman cannot start a container after the hierarchy moved) is still open, last + updated 2026-09-28. Whether 2.9.13's namespaces close it on 3.0.1 is not verified here. + +## What the coordinator did + +- Removed the failed rehearsal by the recipe's failure path at 2026-10-02T02:40:47Z: terminate, export, unregister. + Export: `StackRehearsal2604-rehearsal.tar`, sha256 + `3dadd24ab1af5e9639d26f5ecd3fff36a9df5e3b1dfa204e5f8a70cbaa42e9ff`, 1,276,456,960 bytes. The default distribution + is unchanged. +- Did not touch the other distributions. + +## Consequences for the recipe + +1. The recipe's uid-1000 user cannot have a working user manager beside the workstation on WSL 2.7.x. Either the + host first moves to a WSL release with isolated distro cgroups, or the new distribution's user gets another uid + (which removes the user-manager collision only; system units still share cgroups). +2. W5's proof should name the launch warning `wsl: Failed to start the systemd user session`. +3. P3/W5: six new storage-error lines appeared about 15 seconds before cloud-init started, when the new disk was + attached; the rule "equal to the baseline" does not hold on a host that attaches a disk. +4. The rootless-container check (issue 41492) belongs right after F3 on whichever WSL release the host ends on. + +## Runs on WSL 3.0.1 (2026-10-02) + +`runs-on-wsl-3.0.1.json` holds the compact, sanitized record of three units on the updated host, each on its own +throwaway name: run 2 (08:32Z to 08:39Z, the recipe before its repair, stopped at W5's paired rule on the shared VM +console), probe E1 (08:42Z, outside the recipe, the template plus the `bootcmd` mask) and run 3 (10:54Z to 11:01Z, the +repaired recipe, P1 to F5 with a rootless container). Each step names its raw output by sha256 and keeps a bounded +excerpt; the raw files stay in the coordinator's private state folder. The record is built by a script that replaces +the user name, the Windows computer name, device ids and profile paths and refuses to write if one survives. + +These are rehearsals. They qualify neither image arm of the preregistered comparison; stage 2 with its uv and Node +probes, the 24.04.5 rollback arm and path B are still owed. diff --git a/evidence/artifacts/new-wsl-rehearsal-20261002/runs-on-wsl-3.0.1.json b/evidence/artifacts/new-wsl-rehearsal-20261002/runs-on-wsl-3.0.1.json new file mode 100644 index 000000000..cee6096ce --- /dev/null +++ b/evidence/artifacts/new-wsl-rehearsal-20261002/runs-on-wsl-3.0.1.json @@ -0,0 +1,1090 @@ +{ + "schema_version": 1, + "kind": "wsl_new_distro_rehearsal_runs", + "host": { + "wsl": "3.0.1.0", + "kernel": "6.18.40.1-1", + "workstation_image": "Ubuntu 24.04.5 LTS, systemd 255", + "new_image": "Ubuntu 26.04.1 LTS (ubuntu-26.04.1-wsl-amd64.wsl, 418495746 bytes), systemd 259" + }, + "evidence_class": "native execution on one host; rehearsals on throwaway names, not one of the two preregistered image comparisons", + "sanitization": "the user name, the Windows computer name, device ids, Windows profile paths and temporary directories are replaced; raw outputs stay in the coordinator's private state folder and are named here by sha256", + "runs": [ + { + "id": "run-2", + "throwaway_name": "StackRehearsal2604r2", + "recipe_revision": "fed1e93c (pull request 593 before the repair)", + "result": "stopped at W5's paired rule", + "started": "2026-10-02T08:32:31Z", + "ended": "2026-10-02T08:39:55Z", + "steps": [ + { + "step": "P1", + "raw_sha256": "bc3df15df8b254a2e0c22ad73f5215cc19abbb2d6de41cc9817ef83eb33cde72", + "raw_lines": 8, + "excerpt": [ + "gpgv: Signature made Thu Aug 27 18:43:09 2026 EDT", + "gpgv: using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092", + "gpgv: Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"", + "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 *ubuntu-26.04.1-wsl-amd64.wsl", + "gpgv: Signature made Tue Sep 15 15:11:12 2026 EDT", + "gpgv: using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092", + "gpgv: Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"", + "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e *ubuntu-24.04.5-wsl-amd64.wsl" + ] + }, + { + "step": "P2", + "raw_sha256": "d139396267e41aa867a198fecef0e9f265bb1ff4a3fdbc15d69791fb8ebe8e66", + "raw_lines": 8, + "excerpt": [ + "[render exit=0]", + "/usr/bin/cloud-init 26.1-0ubuntu1~24.04.1", + "[cloud-init --version exit=0]", + "2026-10-02 08:32:33,532 - schema.py[WARNING]: datasource not detected, using default instance-data/user-data paths.", + "Valid schema /StackRehearsal2604r2.user-data", + "[cloud-init schema -c exit=0]", + "f5f290eb98581ae823e9ad03b6a8d0d68dd95f71a7b68a6f15c7711b6521906c /StackRehearsal2604r2.user-data", + "[sha256sum exit=0]" + ] + }, + { + "step": "P3", + "raw_sha256": "300f819cd69c332fe57261ca1630eb2629d9c8078b0d83879b2cabd2cea1fdba", + "raw_lines": 9, + "excerpt": [ + "2", + "[count exit=0]", + "[ 0.018761] kernel: Kernel command line: initrd=\\initrd.img WSL_ROOT_INIT=1 panic=-1 nr_cpus=48 hv_utils.timesync_implicit=1 printk.devkmsg=on page_reporting.page_reporting_order=5 hv_storvsc.storvsc_max_hw_queues=4 c", + "[newest-line exit=0]", + "7069.20 332802.74", + "[uptime exit=0]", + "NAME TYPE SIZE USED PRIO", + "/dev/sdc partition 24G 0B -2", + "[swapon exit=0]" + ] + }, + { + "step": "W1-baseline", + "raw_sha256": "e290486614391fcc1a06ca88780695cb4e6666ea7a649f0527569c7343a4c000", + "raw_lines": 5, + "excerpt": [ + "1000", + "degraded", + "systemd-binfmt.service", + "active", + "cgroup:[4026532183]" + ] + }, + { + "step": "W2", + "raw_sha256": "94d66144c3669a0a4a43ea494b9cb51b13250c38913e9c19fbc7a34cf67c0757", + "raw_lines": 5, + "excerpt": [ + "computed 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 published 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 listed 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "418495746", + "[W2 completed without throw]" + ] + }, + { + "step": "W3", + "raw_sha256": "9c4720029205c52e6270471693ff953cb479a42213942608acea2407b82a3554", + "raw_lines": 45, + "excerpt": [ + " Directory: %USERPROFILE%", + "Mode LastWriteTime Length Name", + "---- ------------- ------ ----", + "d----- 10/1/2026 10:22 PM .cloud-init", + "[New-Item ok=True]", + "[ReadAllText ok=True]", + "[WriteAllText ok=True]", + "+ Get-Content first line", + "#cloud-config", + "[ok=True]", + "+ Select-String name and default lines", + "IgnoreCase : True", + "LineNumber : 13", + "Line : - name: ", + "Filename : StackRehearsal2604r2.user-data", + "Path : %USERPROFILE%\\.cloud-init\\StackRehearsal2604r2.user-data", + "Pattern : ^- name:", + "Context :", + "Matches : {0}", + "IgnoreCase : True", + "LineNumber : 25", + "Line : default=", + "Filename : StackRehearsal2604r2.user-data", + "Path : %USERPROFILE%\\.cloud-init\\StackRehearsal2604r2.user-data", + "Pattern : ^ default=", + "Context :", + "Matches : {0}", + "[ok=True]", + "+ Get-FileHash", + "f5f290eb98581ae823e9ad03b6a8d0d68dd95f71a7b68a6f15c7711b6521906c", + "[ok=True]", + "+ Select-String leftover placeholder (expect no output)", + "[ok=True]" + ] + }, + { + "step": "W4", + "raw_sha256": "867a5c30efd5e2932db165de22bbad94eeca371f319c821901bdc7b9da5981d6", + "raw_lines": 10, + "excerpt": [ + "Installing: Z:\\WSL\\downloads\\ubuntu-26.04.1-wsl-amd64.wsl", + "Distribution successfully installed. It can be launched via 'wsl.exe -d StackRehearsal2604r2'", + "0", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + " StackRehearsal2604r2 Stopped 2", + "[list exit=0]" + ] + }, + { + "step": "W5", + "raw_sha256": "31079a231097280bc8b288c9ef3112fd739f3f357df1df60f18f82bb2bff19ae", + "raw_lines": 86, + "excerpt": [ + "Provisioning the new WSL instance StackRehearsal2604r2", + "This might take a while...", + "0", + "+ id -un", + "", + "[exit=0]", + "+ id -u", + "1000", + "[exit=0]", + "+ cloud-init --version", + "/bin/cloud-init 26.1-0ubuntu3~26.04.1", + "[exit=0]", + "+ cloud-init status --long", + "status: disabled", + "extended_status: disabled", + "boot_status_code: disabled-by-marker-file", + "last_update: Thu, 01 Jan 1970 01:58:56 +0000", + "detail: DataSourceWSL", + "errors: []", + "recoverable_errors: {}", + "[exit=0]", + "+ cat /var/lib/cloud/data/result.json", + "{", + " \"v1\": {", + " \"datasource\": \"DataSourceWSL\",", + "... (39 lines not shown; see the raw file's sha256) ...", + "+ cat /etc/wsl.conf", + "[boot]", + "systemd=true", + "[user]", + "default=", + "[exit=0]", + "+ ls -l /etc/cloud/cloud-init.disabled", + "-rw-r--r-- 1 root root 0 Oct 2 04:33 /etc/cloud/cloud-init.disabled", + "[exit=0]", + "+ sudo -l -U ", + "User may run the following commands on :", + " (ALL : ALL) ALL", + " (ALL) NOPASSWD: ALL", + "[exit=0]", + "+ wsl.exe --list --verbose", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + " StackRehearsal2604r2 Running 2", + "[exit=0]" + ] + }, + { + "step": "W5-cgroup", + "raw_sha256": "8b33179bfd2e3a36b1e6ab094cb7af028577d1f43f416af8a3f4b0990a4c7321", + "raw_lines": 8, + "excerpt": [ + "active", + "[workstation is-active exit=0]", + "active", + "[new is-active exit=0]", + "43", + "[cgroup.procs exit=0]", + "active", + "[new user manager exit=0]" + ] + }, + { + "step": "W5-paired", + "raw_sha256": "c5a59ac1fdbd6f96e7a26d232136c5f463449bfa2662605ed8b4df0086f5aa79", + "raw_lines": 14, + "excerpt": [ + "1000", + "degraded", + "getty@tty1.service", + "systemd-binfmt.service", + "active", + "cgroup:[4026532183]", + "[workstation line done]", + "1000", + "degraded", + "getty@tty1.service", + "systemd-binfmt.service", + "active", + "cgroup:[4026532407]", + "[new line exit=0]" + ] + }, + { + "step": "W5-second-count", + "raw_sha256": "ed83f64fbc9d3bcb8a7e7b0978b8e6e3c70a22d42d628c3219beb303ba7edad4", + "raw_lines": 2, + "excerpt": [ + "3", + "[count exit=0]" + ] + }, + { + "step": "W5-new-lines", + "raw_sha256": "e678bdfd43a50782a3177c948eb8bae765a313e73f28e90c3df330a8e7988d40", + "raw_lines": 1, + "excerpt": [ + "[ 7118.513211] kernel: hv_storvsc : tag#2558 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1" + ] + }, + { + "step": "W5-getty-diagnosis", + "raw_sha256": "61caf40d7486de12384001558e454a313aa62ee4811e98104d2555173616ed4f", + "raw_lines": 54, + "excerpt": [ + "=== workstation 2026-10-02T08:36:07Z", + "console-getty.service masked inactive dead console-getty.service", + "getty-static.service loaded inactive dead getty on tty2-tty6 if dbus and logind are not available", + "getty@tty1.service loaded failed failed Getty on tty1", + "getty-pre.target loaded active active Preparation for Logins", + "getty.target loaded active active Login Prompts", + "[enabled]", + "enabled", + "masked-runtime", + "[wants]", + "total 0", + "lrwxrwxrwx 1 root root 38 Sep 7 20:13 getty@tty1.service -> /usr/lib/systemd/system/getty@.service", + "[generator masks]", + "lrwxrwxrwx 1 root root 9 Oct 2 02:34 NetworkManager-wait-online.service -> /dev/null", + "lrwxrwxrwx 1 root root 9 Oct 2 02:34 console-getty.service -> /dev/null", + "drwxr-xr-x 2 root root 60 Oct 2 02:34 getty.target.wants", + "lrwxrwxrwx 1 root root 9 Oct 2 02:34 systemd-networkd-wait-online.service -> /dev/null", + "[failed]", + "getty@tty1.service loaded failed failed Getty on tty1", + "systemd-binfmt.service loaded failed failed Set Up Additional Binary Formats", + "[user]", + "running", + "[ns]", + "cgroup:[4026532183]", + "[os]", + "Ubuntu 24.04.5 LTS systemd 255", + "=== new", + "console-getty.service masked inactive dead console-getty.service", + "getty-static.service loaded inactive dead getty on tty2-tty6 if dbus and logind are not available", + "getty@tty1.service loaded failed failed Getty on tty1", + "getty-pre.target loaded active active Preparation for Logins", + "getty.target loaded active active Login Prompts", + "[enabled]", + "enabled", + "masked-runtime", + "[wants]", + "total 0", + "lrwxrwxrwx 1 root root 38 Aug 27 10:11 getty@tty1.service -> /usr/lib/systemd/system/getty@.service", + "[generator masks]", + "lrwxrwxrwx 1 root root 9 Oct 2 04:33 NetworkManager-wait-online.service -> /dev/null", + "lrwxrwxrwx 1 root root 9 Oct 2 04:33 console-getty.service -> /dev/null", + "drwxr-xr-x 2 root root 60 Oct 2 04:33 getty.target.wants", + "lrwxrwxrwx 1 root root 9 Oct 2 04:33 systemd-networkd-wait-online.service -> /dev/null", + "[failed]", + "getty@tty1.service loaded failed failed Getty on tty1", + "systemd-binfmt.service loaded failed failed Set Up Additional Binary Formats", + "[user]", + "running", + "[ns]", + "cgroup:[4026532407]", + "[os]", + "Ubuntu 26.04.1 LTS systemd 259" + ] + }, + { + "step": "W5-remove-failed", + "raw_sha256": "0cb2ba22911d2e561ba2239e090e6bb7f370e72c7c418a3772829bc095cfdd6c", + "raw_lines": 24, + "excerpt": [ + "+ wsl.exe --list --quiet", + "NativeStack", + "", + "StackRehearsal2604r2", + "+ wsl.exe --terminate StackRehearsal2604r2", + "The operation completed successfully.", + "[exit=0]", + "+ wsl.exe --export", + "The operation completed successfully.", + "0", + "b58cfd81321fc744d67660ca115420ee0306388daba34022c6031808378ca3cd", + "1273774080", + "+ wsl.exe --unregister StackRehearsal2604r2", + "The operation completed successfully.", + "[exit=0]", + "+ survivor interop check", + "Microsoft Windows [Version 10.0.26200.9168]", + "[exit=0]", + "+ wsl.exe --list --verbose", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + "[exit=0]" + ] + } + ] + }, + { + "id": "probe-E1", + "throwaway_name": "StackRehearsal2604e1", + "recipe_revision": "outside the recipe: the template plus the bootcmd", + "result": "the mask took effect before the unit started", + "started": "2026-10-02T08:42:42Z", + "ended": "2026-10-02T08:43:26Z", + "steps": [ + { + "step": "W3", + "raw_sha256": "33928c5ea78c1ed1daadc919a22bad44f81b736ab656d7051daf64372ea805d1", + "raw_lines": 45, + "excerpt": [ + " Directory: %USERPROFILE%", + "Mode LastWriteTime Length Name", + "---- ------------- ------ ----", + "d----- 10/2/2026 4:33 AM .cloud-init", + "[New-Item ok=True]", + "[ReadAllText ok=True]", + "[WriteAllText ok=True]", + "+ Get-Content first line", + "#cloud-config", + "[ok=True]", + "+ Select-String name and default lines", + "IgnoreCase : True", + "LineNumber : 13", + "Line : - name: ", + "Filename : StackRehearsal2604e1.user-data", + "Path : %USERPROFILE%\\.cloud-init\\StackRehearsal2604e1.user-data", + "Pattern : ^- name:", + "Context :", + "Matches : {0}", + "IgnoreCase : True", + "LineNumber : 25", + "Line : default=", + "Filename : StackRehearsal2604e1.user-data", + "Path : %USERPROFILE%\\.cloud-init\\StackRehearsal2604e1.user-data", + "Pattern : ^ default=", + "Context :", + "Matches : {0}", + "[ok=True]", + "+ Get-FileHash", + "c0732303a0004121f9d373f7856454e3e59eeadea395bac46eb17e1671b32af4", + "[ok=True]", + "+ Select-String leftover placeholder (expect no output)", + "[ok=True]" + ] + }, + { + "step": "W4", + "raw_sha256": "86cd5a5fc5a59e2b88a7639378c751e425c93f2ba07bc383d19ed9d827f9fc25", + "raw_lines": 10, + "excerpt": [ + "Installing: Z:\\WSL\\downloads\\ubuntu-26.04.1-wsl-amd64.wsl", + "Distribution successfully installed. It can be launched via 'wsl.exe -d StackRehearsal2604e1'", + "0", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + " StackRehearsal2604e1 Stopped 2", + "[list exit=0]" + ] + }, + { + "step": "W5", + "raw_sha256": "998f66cccd95078f7746ea686c95aba46c1385fb128128f8b2f5da0b913d57cf", + "raw_lines": 97, + "excerpt": [ + "Provisioning the new WSL instance StackRehearsal2604e1", + "This might take a while...", + "0", + "+ id -un", + "", + "[exit=0]", + "+ id -u", + "1000", + "[exit=0]", + "+ cloud-init --version", + "/bin/cloud-init 26.1-0ubuntu3~26.04.1", + "[exit=0]", + "+ cloud-init status --long", + "status: disabled", + "extended_status: disabled", + "boot_status_code: disabled-by-marker-file", + "last_update: Thu, 01 Jan 1970 02:08:12 +0000", + "detail: DataSourceWSL", + "errors: []", + "recoverable_errors:", + "ERROR:", + "\t- Failed to wait for network: Unexpected error while running command. Command: ['systemctl', 'start', 'systemd-networkd-wait-online.service'] Exit code: 1 Reason: - Stdout: Stderr: Failed to start systemd-networkd-wait", + "\t- Failed to wait for network: Unexpected error while running command. Command: ['systemctl', 'start', 'systemd-networkd-wait-online.service'] Exit code: 1 Reason: - Stdout: Stderr: Failed to start systemd-networkd-wait", + "[exit=2]", + "+ cat /var/lib/cloud/data/result.json", + "... (50 lines not shown; see the raw file's sha256) ...", + "+ cat /etc/wsl.conf", + "[boot]", + "systemd=true", + "[user]", + "default=", + "[exit=0]", + "+ ls -l /etc/cloud/cloud-init.disabled", + "-rw-r--r-- 1 root root 0 Oct 2 04:42 /etc/cloud/cloud-init.disabled", + "[exit=0]", + "+ sudo -l -U ", + "User may run the following commands on :", + " (ALL : ALL) ALL", + " (ALL) NOPASSWD: ALL", + "[exit=0]", + "+ wsl.exe --list --verbose", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + " StackRehearsal2604e1 Running 2", + "[exit=0]" + ] + }, + { + "step": "W5-cgroup", + "raw_sha256": "8b33179bfd2e3a36b1e6ab094cb7af028577d1f43f416af8a3f4b0990a4c7321", + "raw_lines": 8, + "excerpt": [ + "active", + "[workstation is-active exit=0]", + "active", + "[new is-active exit=0]", + "43", + "[cgroup.procs exit=0]", + "active", + "[new user manager exit=0]" + ] + }, + { + "step": "W5-paired", + "raw_sha256": "e49b7748f321186167e2fe258ade6483800f05b9a329ccfabcf1a1182906e174", + "raw_lines": 13, + "excerpt": [ + "1000", + "degraded", + "getty@tty1.service", + "systemd-binfmt.service", + "active", + "cgroup:[4026532183]", + "[workstation line done]", + "1000", + "degraded", + "systemd-binfmt.service", + "active", + "cgroup:[4026532410]", + "[new line exit=0]" + ] + }, + { + "step": "E1-getty", + "raw_sha256": "3e5e5082aa9f6c504952d83746145a1ce2c98ca356a9b241527f4401d09ca3ce", + "raw_lines": 27, + "excerpt": [ + "=== workstation", + "enabled", + "getty@tty1.service", + "systemd-binfmt.service", + "Result=start-limit-hit", + "NRestarts=6", + "ActiveState=failed", + "=== new", + "masked", + "[is-enabled exit=1]", + "lrwxrwxrwx 1 root root 9 Oct 2 04:42 /etc/systemd/system/getty@tty1.service -> /dev/null", + "systemd-binfmt.service", + "LoadState=masked", + "ActiveState=inactive", + "Result=success", + "NRestarts=0", + "[journal for the unit]", + "Failed to start getty@tty1.service.", + "[journal mentions]", + "[ 7690.190541] cloud-init[160]: Created symlink '/etc/systemd/system/getty@tty1.service' → '/dev/null'.", + "[ 7690.372158] systemd[1]: Failed to start getty@tty1.service.", + "[cloud-init]", + "{\"v1\":{\"datasource\":\"DataSourceWSL\",\"errors\":[]}}", + "Valid schema user-data", + "degraded", + "running", + "[new exit=0]" + ] + }, + { + "step": "E1-remove", + "raw_sha256": "fdb763e32cad4d45933d41c27cfec5118c9d3212b3e76ef7964ca3476c1528db", + "raw_lines": 19, + "excerpt": [ + "+ wsl.exe --list --quiet", + "NativeStack", + "", + "StackRehearsal2604e1", + "+ wsl.exe --terminate StackRehearsal2604e1", + "The operation completed successfully.", + "[exit=0]", + "+ wsl.exe --unregister StackRehearsal2604e1", + "The operation completed successfully.", + "[exit=0]", + "+ survivor interop check", + "Microsoft Windows [Version 10.0.26200.9168]", + "[exit=0]", + "+ wsl.exe --list --verbose", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + "[exit=0]" + ] + } + ] + }, + { + "id": "run-3", + "throwaway_name": "StackRehearsal2604r3", + "recipe_revision": "b4653663 (pull request 593 after the repair)", + "result": "P1 to F5 passed, with a rootless container", + "started": "2026-10-02T10:54:04Z", + "ended": "2026-10-02T11:01:42Z", + "steps": [ + { + "step": "P1", + "raw_sha256": "bc3df15df8b254a2e0c22ad73f5215cc19abbb2d6de41cc9817ef83eb33cde72", + "raw_lines": 8, + "excerpt": [ + "gpgv: Signature made Thu Aug 27 18:43:09 2026 EDT", + "gpgv: using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092", + "gpgv: Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"", + "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 *ubuntu-26.04.1-wsl-amd64.wsl", + "gpgv: Signature made Tue Sep 15 15:11:12 2026 EDT", + "gpgv: using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092", + "gpgv: Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"", + "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e *ubuntu-24.04.5-wsl-amd64.wsl" + ] + }, + { + "step": "P2", + "raw_sha256": "4d25cfe9e427e4c00f54507791d0c3033392424d5dad356953141017dc37d1a0", + "raw_lines": 8, + "excerpt": [ + "[render exit=0]", + "/usr/bin/cloud-init 26.1-0ubuntu1~24.04.1", + "[cloud-init --version exit=0]", + "2026-10-02 10:54:05,545 - schema.py[WARNING]: datasource not detected, using default instance-data/user-data paths.", + "Valid schema /StackRehearsal2604r3.user-data", + "[cloud-init schema -c exit=0]", + "6eebab9fe9892c48931e4eccb1935411203ee27286edb7d6a6b535864f104e7e /StackRehearsal2604r3.user-data", + "[sha256sum exit=0]" + ] + }, + { + "step": "P3", + "raw_sha256": "dfd7516f1f5f26e08d12f3135e68207c2ac0398f5f436cfbae9002e5fe4cf362", + "raw_lines": 9, + "excerpt": [ + "2", + "[count exit=0]", + "[ 7678.647875] kernel: hv_storvsc : tag#2472 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[newest-line exit=0]", + "15561.21 734745.01", + "[uptime exit=0]", + "NAME TYPE SIZE USED PRIO", + "/dev/sdc partition 24G 0B -2", + "[swapon exit=0]" + ] + }, + { + "step": "W1-baseline", + "raw_sha256": "649e47c0665c43762e110e6b50cd7a0d40615bd31e2dc06a4cfe49adcabb5ab2", + "raw_lines": 14, + "excerpt": [ + "1000", + "[exit=0]", + "degraded", + "[exit=1]", + "getty@tty1.service loaded failed failed Getty on tty1", + "systemd-binfmt.service loaded failed failed Set Up Additional Binary Formats", + "[exit=0]", + "active", + "[exit=0]", + "cgroup:[4026532183]", + "[exit=0]", + "Result=start-limit-hit", + "NRestarts=6", + "[exit=0]" + ] + }, + { + "step": "W2", + "raw_sha256": "5090261abff2554b49f92083fc4fa4748fd2faa6037d8c583e685896a1fe175a", + "raw_lines": 5, + "excerpt": [ + "computed 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 published 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 listed 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "418495746", + "[W2 completed without throw]" + ] + }, + { + "step": "W3", + "raw_sha256": "f3e17c2b86103d4363d07b6137a10d3a94223fd9ae8960801c913dacdbfaff64", + "raw_lines": 45, + "excerpt": [ + " Directory: %USERPROFILE%", + "Mode LastWriteTime Length Name", + "---- ------------- ------ ----", + "d----- 10/2/2026 4:42 AM .cloud-init", + "[New-Item ok=True]", + "[ReadAllText ok=True]", + "[WriteAllText ok=True]", + "+ Get-Content first line", + "#cloud-config", + "[ok=True]", + "+ Select-String name and default lines", + "IgnoreCase : True", + "LineNumber : 13", + "Line : - name: ", + "Filename : StackRehearsal2604r3.user-data", + "Path : %USERPROFILE%\\.cloud-init\\StackRehearsal2604r3.user-data", + "Pattern : ^- name:", + "Context :", + "Matches : {0}", + "IgnoreCase : True", + "LineNumber : 25", + "Line : default=", + "Filename : StackRehearsal2604r3.user-data", + "Path : %USERPROFILE%\\.cloud-init\\StackRehearsal2604r3.user-data", + "Pattern : ^ default=", + "Context :", + "Matches : {0}", + "[ok=True]", + "+ Get-FileHash", + "6eebab9fe9892c48931e4eccb1935411203ee27286edb7d6a6b535864f104e7e", + "[ok=True]", + "+ Select-String leftover placeholder (expect no output)", + "[ok=True]" + ] + }, + { + "step": "W4", + "raw_sha256": "c4aae64361d9e7aaf8c8151e983d1cc336cfdfcac3489a92bd02e56aa29f57bb", + "raw_lines": 10, + "excerpt": [ + "Installing: Z:\\WSL\\downloads\\ubuntu-26.04.1-wsl-amd64.wsl", + "Distribution successfully installed. It can be launched via 'wsl.exe -d StackRehearsal2604r3'", + "0", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + " StackRehearsal2604r3 Stopped 2", + "[list exit=0]" + ] + }, + { + "step": "W5", + "raw_sha256": "b581181c540a566181e9c55f764a97821d1205cf38a4b752a810f9a3d5f4f662", + "raw_lines": 105, + "excerpt": [ + "Provisioning the new WSL instance StackRehearsal2604r3", + "This might take a while...", + "0", + "+ id -un", + "", + "[exit=0]", + "+ id -u", + "1000", + "[exit=0]", + "+ systemctl is-enabled getty@tty1.service", + "masked", + "[exit=1]", + "+ systemctl show getty@tty1.service", + "LoadState=masked", + "ActiveState=inactive", + "NRestarts=0", + "[exit=0]", + "+ cloud-init --version", + "/bin/cloud-init 26.1-0ubuntu3~26.04.1", + "[exit=0]", + "+ cloud-init status --long", + "status: disabled", + "extended_status: disabled", + "boot_status_code: disabled-by-marker-file", + "last_update: Thu, 01 Jan 1970 04:19:54 +0000", + "... (58 lines not shown; see the raw file's sha256) ...", + "+ cat /etc/wsl.conf", + "[boot]", + "systemd=true", + "[user]", + "default=", + "[exit=0]", + "+ ls -l /etc/cloud/cloud-init.disabled", + "-rw-r--r-- 1 root root 0 Oct 2 06:54 /etc/cloud/cloud-init.disabled", + "[exit=0]", + "+ sudo -l -U user", + "User may run the following commands on :", + " (ALL : ALL) ALL", + " (ALL) NOPASSWD: ALL", + "[exit=0]", + "+ wsl.exe --list --verbose", + " NAME STATE VERSION", + "* NativeStack Running 2", + " Stopped 2", + " StackRehearsal2604r3 Running 2", + "[exit=0]" + ] + }, + { + "step": "W5-cgroup", + "raw_sha256": "8b33179bfd2e3a36b1e6ab094cb7af028577d1f43f416af8a3f4b0990a4c7321", + "raw_lines": 8, + "excerpt": [ + "active", + "[workstation is-active exit=0]", + "active", + "[new is-active exit=0]", + "43", + "[cgroup.procs exit=0]", + "active", + "[new user manager exit=0]" + ] + }, + { + "step": "W5-paired", + "raw_sha256": "efc854c76011e72a23f6bd8134f6e40fe86e9b40d6e898982df058e95f8de95d", + "raw_lines": 23, + "excerpt": [ + "=== workstation", + "1000", + "[exit=0]", + "degraded", + "[exit=1]", + "getty@tty1.service loaded failed failed Getty on tty1", + "systemd-binfmt.service loaded failed failed Set Up Additional Binary Formats", + "[exit=0]", + "active", + "[exit=0]", + "cgroup:[4026532183]", + "[exit=0]", + "=== new distribution", + "1000", + "[exit=0]", + "degraded", + "[exit=1]", + "systemd-binfmt.service loaded failed failed Set Up Additional Binary Formats", + "[exit=0]", + "active", + "[exit=0]", + "cgroup:[4026532408]", + "[exit=0]" + ] + }, + { + "step": "W5-second-count", + "raw_sha256": "d94c2e5ec9bd923eea26d65bd4653cb7194ba26523dc6493af81378d8e163374", + "raw_lines": 6, + "excerpt": [ + "3", + "[count exit=0]", + "[ 7118.513211] kernel: hv_storvsc : tag#2558 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[ 7678.647875] kernel: hv_storvsc : tag#2472 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[15573.834020] kernel: hv_storvsc : tag#628 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[lines exit=0]" + ] + }, + { + "step": "W7", + "raw_sha256": "e7c75c123d7c2dc3647413d9ae4f292034272af461131629d5f7771884747b11", + "raw_lines": 28, + "excerpt": [ + "+ wsl.exe --terminate StackRehearsal2604r3", + "The operation completed successfully.", + "[exit=0]", + "+ wsl.exe --list --running --quiet", + "NativeStack", + "[exit=0]", + "+ id -un", + "", + "[exit=0]", + "+ New-Item probe file", + " Directory: \\\\wsl.localhost\\StackRehearsal2604r3\\home\\", + "Mode LastWriteTime Length Name", + "---- ------------- ------ ----", + "------ 10/2/2026 6:56 AM 0 wsl-owner-probe", + "[ok=True]", + "+ stat -c %u:%g probe file", + "1000:1000", + "[exit=0]", + "+ Remove-Item probe file", + "[ok=True]" + ] + }, + { + "step": "F1", + "raw_sha256": "4b1bf2147edc9b00ef5eea01f2aa494ce97beabd1d7d6d91b417c770eedf87d8", + "raw_lines": 214, + "excerpt": [ + "degraded", + "[is-system-running exit=1]", + "systemd-binfmt.service loaded failed failed Set Up Additional Binary Formats", + "[failed exit=0]", + "Oct 02 06:54:35 systemd-binfmt[105]: Failed to flush binfmt_misc rules, ignoring: Read-only file system", + "Oct 02 06:56:31 systemd-binfmt[90]: Failed to flush binfmt_misc rules, ignoring: Read-only file system", + "[journalctl exit=0]", + "UNIT FILE STATE PRESET", + "apparmor.service enabled enabled", + "apport-autoreport.service static -", + "apport-coredump-hook@.service static -", + "apport-forward@.service static -", + "apport.service enabled enabled", + "apt-daily-upgrade.service static -", + "console-getty.service masked-runtime disabled", + "cryptdisks-early.service masked enabled", + "cryptdisks.service masked enabled", + "getty@tty1.service masked enabled", + "hwclock.service masked enabled", + "NetworkManager-wait-online.service masked-runtime enabled", + "screen-cleanup.service masked enabled", + "sudo.service masked enabled", + "systemd-networkd-wait-online.service masked-runtime enabled", + "x11-common.service masked enabled", + "[list-unit-files exit=0]", + "[groups exit=0]" + ] + }, + { + "step": "F2", + "raw_sha256": "ae07205c94ffc756996cb2dd3c243da2f96071a411736570688363756f364e45", + "raw_lines": 3, + "excerpt": [ + "[enable-linger exit=0]", + "yes", + "[show-user exit=0]" + ] + }, + { + "step": "F2-idle", + "raw_sha256": "2cd29ab814810ae05d3b1b8f4ce24cda6984303ba22ead7da30f0a2810d4959f", + "raw_lines": 38, + "excerpt": [ + "10:56:51", + "NativeStack", + "StackRehearsal2604r3", + "10:57:01", + "NativeStack", + "StackRehearsal2604r3", + "10:57:11", + "NativeStack", + "StackRehearsal2604r3", + "10:57:21", + "NativeStack", + "StackRehearsal2604r3", + "10:57:31", + "NativeStack", + "StackRehearsal2604r3", + "10:57:41", + "NativeStack", + "StackRehearsal2604r3", + "10:57:52", + "NativeStack", + "StackRehearsal2604r3", + "10:58:02", + "NativeStack", + "StackRehearsal2604r3", + "10:58:12", + "NativeStack", + "StackRehearsal2604r3", + "10:58:22", + "NativeStack", + "StackRehearsal2604r3", + "10:58:32", + "NativeStack", + "StackRehearsal2604r3", + "10:58:42", + "NativeStack", + "StackRehearsal2604r3" + ] + }, + { + "step": "F3", + "raw_sha256": "e00254fe004008d60eb4e73377a5976ec9b5d45c62bb541071838a79c5d84a6f", + "raw_lines": 5, + "excerpt": [ + " directory", + " socket", + "[stat exit=0]", + "running", + "[user is-system-running exit=0]" + ] + }, + { + "step": "F4", + "raw_sha256": "e6ea3b80eedfe2e8c69fae22803fa0c0855318cd51de3023f8524fd45d089039", + "raw_lines": 120, + "excerpt": [ + "[apt-get update exit=0]", + "[apt-get install exit=0]", + "[dpkg-query exit=0]" + ] + }, + { + "step": "F5", + "raw_sha256": "0660e8b650ff6109c52a573c68deca499af8e9436caf5e7a3fa05eac281adfd7", + "raw_lines": 7, + "excerpt": [ + "/etc/subuid::100000:65536", + "/etc/subgid::100000:65536", + "[first grep printed 2 line(s)]", + "[usermod skipped: a range exists]", + "/etc/subuid::100000:65536", + "/etc/subgid::100000:65536", + "[second grep exit=0]" + ] + }, + { + "step": "D1-docker", + "raw_sha256": "b31beabcb4a73be522573c023427b719bccf323d217432e0de7518d7c9c0e3c6", + "raw_lines": 214, + "excerpt": [ + " Active: active (running) since Fri 2026-10-02 07:00:10 EDT; 3s ago", + "[INFO] Installed docker.service successfully.", + "[install container-engine exit=0]" + ] + }, + { + "step": "F3-docker", + "raw_sha256": "659e612497cb46b2b57ab6a8a084715e3298c388f907e2caa0cf2151b6e35989", + "raw_lines": 40, + "excerpt": [ + "[\"name=seccomp,profile=builtin\",\"name=rootless\",\"name=cgroupns\"]", + "[info exit=0]", + "Unable to find image 'hello-world:latest' locally", + "latest: Pulling from library/hello-world", + "4f55086f7dd0: Pulling fs layer", + "4f55086f7dd0: Download complete", + "4f55086f7dd0: Pull complete", + "d5e71e642bf5: Download complete", + "Digest: sha256:5e23090353324d887c48ad5e5c56d294eab81588df9605b07d1afe895f9cc8f8", + "Status: Downloaded newer image for hello-world:latest", + "Hello from Docker!", + "This message shows that your installation appears to be working correctly.", + "To generate this message, Docker took the following steps:", + " 1. The Docker client contacted the Docker daemon.", + " 2. The Docker daemon pulled the \"hello-world\" image from the Docker Hub.", + " (amd64)", + " 3. The Docker daemon created a new container from that image which runs the", + " executable that produces the output you are currently reading.", + " 4. The Docker daemon streamed that output to the Docker client, which sent it", + " to your terminal.", + "To try something more ambitious, you can run an Ubuntu container with:", + " $ docker run -it ubuntu bash", + "Share images, automate workflows, and more with a free Docker ID:", + " https://hub.docker.com/", + "For more examples and ideas, visit:", + " https://docs.docker.com/get-started/", + "[run exit=0]", + "Docker version 29.8.2, build 8af9fe3", + "[version exit=0]", + "active", + "[user unit exit=0]", + "disabled", + "disabled", + "[rootful units exit=1]" + ] + }, + { + "step": "R1-probes", + "raw_sha256": "184038172ea270acd14b3fd844b8402e4c1552921cd90d2adb7d7d82b47b38c2", + "raw_lines": 43, + "excerpt": [ + "+ probe 1 stat user directory and bus", + "stat: missing operand", + "Try 'stat --help' for more information.", + "[exit=1]", + "+ probe 2 systemctl --user is-system-running --wait", + "running", + "[exit=0]", + "+ probe 3 test -c /dev/dxg", + "[exit=0]", + "+ probe 4 nvidia-smi", + "Fri Oct 2 06:58:52 2026", + "+-----------------------------------------------------------------------------------------+", + "| NVIDIA-SMI 615.65.07 KMD Version: 616.64 CUDA UMD Version: 13.4 |", + "+-----------------------------------------+------------------------+----------------------+", + "| GPU Name Persistence-M | Bus-Id Disp.A | Volatile Uncorr. ECC |", + "| Fan Temp Perf Pwr:Usage/Cap | Memory-Usage | GPU-Util Compute M. |", + "| | | MIG M. |", + "|=========================================+========================+======================|", + "| 0 NVIDIA GeForce RTX 4090 On | 00000000:21:00.0 Off | Off |", + "| 0% 35C P8 7W / 450W | 6483MiB / 24564MiB | 1% Default |", + "| | | N/A |", + "+-----------------------------------------+------------------------+----------------------+", + "+-----------------------------------------------------------------------------------------+", + "| Processes: |", + "| GPU GI CI PID Type Process name GPU Memory |", + "| ID ID Usage |", + "|=========================================================================================|", + "| 0 N/A N/A 337 C /llama-server N/A |", + "| 0 N/A N/A 2990 C /python3.13 N/A |", + "+-----------------------------------------------------------------------------------------+", + "[exit=0]", + "+ probe 5 python3 --version", + "Python 3.14.4", + "[exit=0]", + "+ probe 6 uv run python 3.13 --version", + "bash: line 1: uv: command not found", + "[exit=127]", + "+ probe 7 node --version", + "bash: line 1: node: command not found", + "[exit=127]" + ] + }, + { + "step": "Q-quoting", + "raw_sha256": "c0f9daeeb51d99c558276f849242be8f4cda909e8d4355b71179b38cb152fba9", + "raw_lines": 15, + "excerpt": [ + "+ probe 1, form without embedded double quotes", + ",directory", + ",socket", + "[exit=0]", + "+ F10 loop as the repair wrote it (embedded double quotes), on two names that exist", + "-P: -c: line 2: unexpected EOF while looking for matching `)'", + "[exit=2]", + "+ F10 loop without double quotes", + "executable: /usr/bin/bash", + "executable: /usr/bin/sh", + "[exit=0]", + "+ F10 first probe", + "/usr/bin/bash", + "/usr/bin/sh", + "[exit=0]" + ] + } + ] + } + ], + "findings": [ + "Cgroup isolation holds on WSL 3.0.1 with two systemd distributions running: distinct cgroup namespaces, local pids only, both user managers active (run 2, probe E1, run 3).", + "The VM console /dev/tty1 is shared: without a mask, getty@tty1.service fails with start-limit-hit in both distributions when the second one boots (run 2). The user-data bootcmd masks it before systemd starts the unit (probe E1, run 3).", + "With the bootcmd, cloud-init records one recoverable error, 'Failed to wait for network', because WSL masks systemd-networkd-wait-online.service; result.json has no errors (run 3).", + "Each install left one hv_storvsc driver error (cmd 0x2a, srb 0x4) inside its install window: kernel times 7118.5 s, 7678.6 s and 15573.8 s.", + "On 3.0.1 the WSLInterop registration survived every unregister, and a file created from Windows after one terminate is owned 1000:1000.", + "A rootless container starts in a new distribution (Docker Engine 29.8.2): microsoft/WSL issue 41492 does not affect this host.", + "Windows PowerShell 5.1 does not escape a double quote inside an argument to a native program: two probe commands failed and were corrected (run 3, step Q-quoting)." + ], + "not_established": [ + "Stage 2 (the bootstrap) and its uv and Node probes; the 24.04.5 rollback arm; path B.", + "A live workstation getty surviving a new distribution's first boot: the workstation's unit had failed in run 2 and no unit was touched.", + "Run 3's throwaway distribution was still registered when this record was built; its removal and the interop check after it are not in this record." + ] +} diff --git a/evidence/artifacts/new-wsl-rehearsal-20261002/stage1-receipt.json b/evidence/artifacts/new-wsl-rehearsal-20261002/stage1-receipt.json new file mode 100644 index 000000000..1c47f1426 --- /dev/null +++ b/evidence/artifacts/new-wsl-rehearsal-20261002/stage1-receipt.json @@ -0,0 +1,1141 @@ +{ + "schema_version": 1, + "artifact_class": "local_integration_check", + "id": "wsl-new-distro-stage1-stack-rehearsal-2604-20261002", + "kind": "native_cli_e2e", + "component_ids": [ + "systemd" + ], + "claim": "P1 printed 'Good signature' by RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092 for 26.04.1 and 24.04.5, P2 printed 'Valid schema', W2 printed one hash 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 three times for a 418495746-byte image, W4 and W5 exited 0, cloud-init created (uid 1000) with 'cloud-init schema --system' printing 'Valid schema user-data', and W7 read 1000:1000. The hv_storvsc count rose from 7 (P3) to 13 (W5), and F1 printed 'degraded' with user@1000.service failed ('Failed to spawn executor: Device or resource busy'), which stopped the run before F2; F2 to F8 and the comparison probes did not run.", + "limitations": [ + "One run on one Windows host: a new distribution on the existing WSL kernel and Windows installation, not a new physical machine.", + "Stopped at F1 on its recipe stop (`degraded`): F2 to F8, the seven comparison probes and R1's removal block did not run, and stage 2 (F9), the Windows Terminal PATH proof (F10) and F11 are outside this run; native sign-in, services and model-mediated behavior are not covered.", + "The 24.04.5 arm did not run (null and \"not run\"). This run is the 26.04.1 rehearsal arm only: no real run exists and the throwaway name stays registered, which is the recorded deviation from R1.", + "Sanitized: Windows and Linux user names, home and profile paths, the checkout path, the machine host name and device ids are replaced by placeholders; the raw transcript and outputs stay private on the host.", + "jcodemunch_registration is skipped: stage 2 did not run, and a registration would not show that a session or subagent uses jCodeMunch.", + "PowerShell cmdlet lines record exit 0 when the cmdlet completed without a terminating error (the script exited 0); native commands record their own exit code. Per-line exits inside P1's loop are derived from its exit-on-failure structure and the script exit code 0.", + "The F1-diag entries and the workstation-side cgroup read are read-only checks outside the recipe, run after the stop. The statement that both distributions' user@1000.service resolve to one cgroup is an inference from equal accounting counters (Tasks, Memory, CPU), not a reproduced mechanism.", + "Local integration check of our recipe with actual native returned output; not an upstream test, new-host acceptance, or a fresh model or GPU run." + ], + "recorded_at_utc": "2026-10-02T02:36:13Z", + "recipe": { + "path": "adoption/platforms/linux-wsl2-new-distro.md", + "recipe_commit": "9bfe2d577e9b4997412112635393e3c72ef96292 (branch foundation/new-wsl-rehearsal-20261002, pull request 593 head)", + "catalog_revision": null, + "creation_path": "A" + }, + "supported_images": { + "26.04.1": { + "file": "ubuntu-26.04.1-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl", + "sha256": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "catalog_name": "Ubuntu-26.04", + "bytes": null + }, + "24.04.5": { + "file": "ubuntu-24.04.5-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl", + "sha256": "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e", + "catalog_name": "Ubuntu-24.04", + "bytes": 388975696 + } + }, + "rehearsal": { + "name": "StackRehearsal2604", + "release": "26.04.1", + "result": "stopped at F1: systemctl is-system-running --wait printed degraded (exit 1) and systemctl --failed listed user@1000.service. P1 to P3, W1 to W5 and W7 proofs held, except W5's second storage count (13, baseline 7). Deviation from R1: the removal block (terminate, unregister) and the export were not run; StackRehearsal2604 stays registered for the coordinator. F2 to F8 and the seven comparison probes did not run.", + "creation_path": "A", + "schema_system": "Valid schema user-data (exit 0)", + "ownership_probe": "1000:1000 (W7, after one terminate; exit 0)", + "idle_observation": "not run: stopped at F1 before F2 (instanceIdleTimeout=-1, vmIdleTimeout=-1 in the global configuration)", + "storage_readings": "P3 baseline 7; newest line at kernel time 336.821467 s, 5022.79 s (83.7 min) old at /proc/uptime 5359.61 s; swap /dev/sdc 24G, 0B used; W5 second count 13 (+6: six lines at kernel 5542.98 to 5543.03 s)", + "export": { + "file": null, + "sha256": null, + "bytes": null + } + }, + "pre_checks": { + "signed_sums": "P1 exit 0, no key import. 26.04.1: 'using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092', 'Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"', signed line '48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 *ubuntu-26.04.1-wsl-amd64.wsl'. 24.04.5: the same key and Good signature, signed line 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e *ubuntu-24.04.5-wsl-amd64.wsl'.", + "user_data_schema": "P2 exit 0 in the workstation distribution (/usr/bin/cloud-init 26.1-0ubuntu1~24.04.1): 'Valid schema $RENDER_DIR/StackRehearsal2604.user-data' after the warning 'schema.py[WARNING]: datasource not detected, using default instance-data/user-data paths.'; render sha256 c7573fcd5ef1ea52dda89630565956c657c665f17d3b36733faacce4f7e7c3df, equal to W3's." + }, + "storage_errors": { + "baseline": 7, + "newest_line_time": "336.821467 s since boot (P3 second line, device id dropped: [ 336.821467] kernel: hv_storvsc : tag#543 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1); /proc/uptime read right after: 5359.61 251121.92; age 5022.79 s (83.7 min), not under 3600 s, so the run continued", + "swap": [ + "NAME TYPE SIZE USED PRIO", + "/dev/sdc partition 24G 0B -2" + ], + "second_count": 13, + "new_lines": [ + "[ 5542.982850] kernel: hv_storvsc : tag#1026 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[ 5542.989792] kernel: hv_storvsc : tag#1058 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[ 5542.996906] kernel: hv_storvsc : tag#1055 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[ 5543.018069] kernel: hv_storvsc : tag#1087 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[ 5543.024662] kernel: hv_storvsc : tag#1055 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1", + "[ 5543.032208] kernel: hv_storvsc : tag#1087 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1" + ], + "new_lines_placement": "Derived, not measured: /proc/uptime 5620.12 s was read at about 02:23:30Z, which puts kernel time 5543.0 s at about 02:22:13Z, the start of W4 (02:22:13Z to 02:22:22Z), 15.5 s before cloud-init init-local started at kernel 5558.52 s (W5 launch 02:22:28Z).", + "wsl_41482_decision": "not taken by this run: the workaround (swap=0 in the global WSL configuration and a WSL shutdown) is outside the recipe and forbidden here; left to the coordinator. W5 itself exited 0 and its proofs held; stage 2 (F9) was not in scope." + }, + "host": { + "wsl_version": [ + "WSL version: 2.7.13.0", + "Kernel version: 6.18.33.2-2", + "WSLg version: 1.0.73.2", + "MSRDC version: 1.2.7214", + "Direct3D version: 1.611.1-81528511", + "DXCore version: 10.0.26100.1-240331-1435.ge-release", + "Windows version: 10.0.26200.9168" + ], + "default_distribution_before": "* NativeStack Running 2", + "default_distribution_after": "* NativeStack Running 2 (whitespace normalized; the NAME column widened with the new distribution)", + "z_free_bytes": 2505999912960, + "idle_keys": "instanceIdleTimeout=-1 ([general]); vmIdleTimeout=-1 ([wsl2]); line 27: [wsl2] | line 165: vmIdleTimeout=-1 | line 192: [experimental] | line 234: [general] | line 265: instanceIdleTimeout=-1" + }, + "image": { + "release": "26.04.1", + "file": "ubuntu-26.04.1-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl", + "bytes": 418495746, + "sha256_published": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "sha256_distributioninfo": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "sha256_computed": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104" + }, + "first_launch": { + "user_prompt_marker": false, + "oobe_failed_marker": false, + "schema_system": "Valid schema user-data (exit 0)", + "cloud_init_version": "/bin/cloud-init 26.1-0ubuntu3~26.04.1 (exit 0)", + "wsl_warnings": [ + "wsl: Failed to start the systemd user session for ''. See journalctl for more details. (W5 launch output)", + "wsl: Failed to start the systemd user session for 'root'. See journalctl for more details. (first --exec call as root, W5 cloud-init --version)", + "wsl: Failed to start the systemd user session for ''. See journalctl for more details. (W7, first --exec call after the terminate)" + ] + }, + "ownership_probe": "1000:1000 (W7: stat -c %u:%g of an empty file created from Windows under the user's home after one terminate; exit 0)", + "idle_observation": "not run: stopped at F1 before F2", + "subordinate_ids": "not run: stopped at F1 before F5", + "jcodemunch_registration": "skipped: stage 2 did not run (outside this run's scope)", + "failed_attempt_export": { + "file": null, + "sha256": null, + "bytes": null + }, + "steps": [ + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec bash -lc 'stat -c \"%U %F\" \"/run/user/$(id -u)\" \"/run/user/$(id -u)/bus\"'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec systemctl --user is-system-running --wait", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec test -c /dev/dxg", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec /usr/lib/wsl/lib/nvidia-smi", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec bash -lc 'python3 --version'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec bash -lc 'uv run --no-project --python 3.13 python --version'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec bash -lc 'node --version'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded) before the comparison probes" + }, + { + "step": "R1", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --list --quiet", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --terminate 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --unregister 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --list --verbose", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --list --quiet", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --terminate 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --export 'StackRehearsal2604' 'Z:\\WSL\\downloads\\StackRehearsal2604-rehearsal.tar'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "$LASTEXITCODE", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' }", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\\WSL\\downloads\\StackRehearsal2604-rehearsal.tar').Hash.ToLowerInvariant()", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "(Get-Item -LiteralPath 'Z:\\WSL\\downloads\\StackRehearsal2604-rehearsal.tar').Length", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --unregister 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "R1", + "cmd": "wsl.exe --list --verbose", + "exit": null, + "output_excerpt": "not run: recorded deviation from R1; the removal block is skipped and StackRehearsal2604 stays registered for the coordinator" + }, + { + "step": "P1", + "cmd": "for RELEASE in 26.04.1 24.04.5; do", + "exit": 0, + "output_excerpt": "loop ran for 26.04.1 and 24.04.5; the script exited 0 (P1 rc=0)" + }, + { + "step": "P1", + "cmd": "SUMS_DIR=\"$(mktemp -d)\"", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "P1", + "cmd": "curl -fsSL -o \"$SUMS_DIR/SHA256SUMS\" \"https://releases.ubuntu.com/$RELEASE/SHA256SUMS\" || exit 1", + "exit": 0, + "output_excerpt": "(no output); a nonzero curl exits the script with 1, and the script exited 0" + }, + { + "step": "P1", + "cmd": "curl -fsSL -o \"$SUMS_DIR/SHA256SUMS.gpg\" \"https://releases.ubuntu.com/$RELEASE/SHA256SUMS.gpg\" || exit 1", + "exit": 0, + "output_excerpt": "(no output); a nonzero curl exits the script with 1, and the script exited 0" + }, + { + "step": "P1", + "cmd": "gpgv --homedir \"$SUMS_DIR\" --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg \"$SUMS_DIR/SHA256SUMS.gpg\" \"$SUMS_DIR/SHA256SUMS\"", + "exit": 0, + "output_excerpt": "26.04.1: using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092 | Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"\n24.04.5: using RSA key 843938DF228D22F7B3742BC0D94AA3F0EFE21092 | Good signature from \"Ubuntu CD Image Automatic Signing Key (2012) \"" + }, + { + "step": "P1", + "cmd": "if [ \"$?\" -ne 0 ]; then exit 1; fi", + "exit": 0, + "output_excerpt": "(no output); did not exit, so gpgv exited 0 for both releases" + }, + { + "step": "P1", + "cmd": "grep -F \" *ubuntu-$RELEASE-wsl-amd64.wsl\" \"$SUMS_DIR/SHA256SUMS\" || exit 1", + "exit": 0, + "output_excerpt": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 *ubuntu-26.04.1-wsl-amd64.wsl\nbb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e *ubuntu-24.04.5-wsl-amd64.wsl" + }, + { + "step": "P1", + "cmd": "done", + "exit": 0, + "output_excerpt": "script exit 0" + }, + { + "step": "P2", + "cmd": "RENDER_DIR=\"$(mktemp -d)\"", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "P2", + "cmd": "python3 -c 'import string, sys; sys.stdout.write(string.Template(open(sys.argv[1], encoding=\"utf-8\").read()).substitute(WSL_USER=sys.argv[2]))' adoption/templates/wsl/cloud-init.user-data.template '' > \"$RENDER_DIR/StackRehearsal2604.user-data\"", + "exit": 0, + "output_excerpt": "(no output; render exit 0)" + }, + { + "step": "P2", + "cmd": "cloud-init --version", + "exit": 0, + "output_excerpt": "/usr/bin/cloud-init 26.1-0ubuntu1~24.04.1 (the workstation distribution)" + }, + { + "step": "P2", + "cmd": "cloud-init schema -c \"$RENDER_DIR/StackRehearsal2604.user-data\"", + "exit": 0, + "output_excerpt": "schema.py[WARNING]: datasource not detected, using default instance-data/user-data paths. | Valid schema $RENDER_DIR/StackRehearsal2604.user-data" + }, + { + "step": "P2", + "cmd": "sha256sum \"$RENDER_DIR/StackRehearsal2604.user-data\"", + "exit": 0, + "output_excerpt": "c7573fcd5ef1ea52dda89630565956c657c665f17d3b36733faacce4f7e7c3df $RENDER_DIR/StackRehearsal2604.user-data" + }, + { + "step": "P3", + "cmd": "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc'", + "exit": 0, + "output_excerpt": "7" + }, + { + "step": "P3", + "cmd": "sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | grep -v 'registering driver hv_storvsc' | tail -n 1", + "exit": 0, + "output_excerpt": "[ 336.821467] kernel: hv_storvsc : tag#543 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1 (kernel time 336.821467 s)" + }, + { + "step": "P3", + "cmd": "cat /proc/uptime", + "exit": 0, + "output_excerpt": "5359.61 251121.92 (first number: 5359.61 s since boot, so the newest line was 5022.79 s old, 83.7 min; at least 3600 s, so the run continued)" + }, + { + "step": "P3", + "cmd": "swapon --show", + "exit": 0, + "output_excerpt": "NAME TYPE SIZE USED PRIO\n/dev/sdc partition 24G 0B -2" + }, + { + "step": "W1", + "cmd": "New-Item -ItemType Directory -Force -Path 'Z:\\WSL\\downloads'", + "exit": 0, + "output_excerpt": "Directory: Z:\\WSL | d----- downloads" + }, + { + "step": "W1", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W1", + "cmd": "wsl.exe --version", + "exit": 0, + "output_excerpt": "WSL version: 2.7.13.0\nKernel version: 6.18.33.2-2\nWSLg version: 1.0.73.2\nMSRDC version: 1.2.7214\nDirect3D version: 1.611.1-81528511\nDXCore version: 10.0.26100.1-240331-1435.ge-release\nWindows version: 10.0.26200.9168" + }, + { + "step": "W1", + "cmd": "wsl.exe --list --verbose", + "exit": 0, + "output_excerpt": " NAME STATE VERSION\n* NativeStack Running 2\n Running 2" + }, + { + "step": "W1", + "cmd": "wsl.exe --list --quiet", + "exit": 0, + "output_excerpt": "NativeStack\n" + }, + { + "step": "W1", + "cmd": "Test-Path -LiteralPath 'Z:\\WSL\\StackRehearsal2604'", + "exit": 0, + "output_excerpt": "False" + }, + { + "step": "W1", + "cmd": "Test-Path -LiteralPath (Join-Path $env:USERPROFILE '.ubuntupro\\.cloud-init\\StackRehearsal2604.user-data')", + "exit": 0, + "output_excerpt": "False" + }, + { + "step": "W1", + "cmd": "Test-Path -LiteralPath (Join-Path $env:USERPROFILE '.ubuntupro\\.cloud-init\\agent.yaml')", + "exit": 0, + "output_excerpt": "False" + }, + { + "step": "W1", + "cmd": "Select-String -LiteralPath (Join-Path $env:USERPROFILE '.ubuntupro\\.cloud-init\\agent.yaml') -Pattern '^[A-Za-z_][A-Za-z0-9_-]*:' -ErrorAction SilentlyContinue", + "exit": 0, + "output_excerpt": "(prints nothing: agent.yaml is absent and the error is suppressed by -ErrorAction SilentlyContinue)" + }, + { + "step": "W1", + "cmd": "Select-String -LiteralPath (Join-Path $env:USERPROFILE '.wslconfig') -Pattern '^\\s*\\[', '^\\s*instanceIdleTimeout\\s*=', '^\\s*vmIdleTimeout\\s*=' -ErrorAction SilentlyContinue", + "exit": 0, + "output_excerpt": "line 27: [wsl2] | line 165: vmIdleTimeout=-1 | line 192: [experimental] | line 234: [general] | line 265: instanceIdleTimeout=-1" + }, + { + "step": "W1", + "cmd": "Get-PSDrive -Name Z | Select-Object -Property Name, Used, Free", + "exit": 0, + "output_excerpt": "Name Z; Used 1494768410624; Free 2505999912960" + }, + { + "step": "W2", + "cmd": "$ProgressPreference = 'SilentlyContinue'", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "$Release = '26.04.1'", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "switch ($Release) {", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "'26.04.1' { $Expected = '48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104'; $CatalogName = 'Ubuntu-26.04' }", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "'24.04.5' { $Expected = 'bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e'; $CatalogName = 'Ubuntu-24.04' }", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "default { throw 'unsupported release: do not download or install' }", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "}", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "$Image = \"ubuntu-$Release-wsl-amd64.wsl\"", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "$ImagePath = \"Z:\\WSL\\downloads\\$Image\"", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "$ImageUrl = \"https://releases.ubuntu.com/$Release/$Image\"", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "$SumsPath = \"Z:\\WSL\\downloads\\$Release-SHA256SUMS\"", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W2", + "cmd": "Invoke-WebRequest -UseBasicParsing -Uri $ImageUrl -OutFile $ImagePath", + "exit": 0, + "output_excerpt": "(no output); image downloaded to Z:\\WSL\\downloads (progress output off)" + }, + { + "step": "W2", + "cmd": "Invoke-WebRequest -UseBasicParsing -Uri \"https://releases.ubuntu.com/$Release/SHA256SUMS\" -OutFile $SumsPath", + "exit": 0, + "output_excerpt": "(no output); signed sums list downloaded" + }, + { + "step": "W2", + "cmd": "$Published = (Select-String -LiteralPath $SumsPath -Pattern (' \\*' + [regex]::Escape($Image) + '$')).Line.Split(' ')[0]", + "exit": 0, + "output_excerpt": "(no output); $Published = 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104" + }, + { + "step": "W2", + "cmd": "$Listed = ((Invoke-WebRequest -UseBasicParsing -Uri 'https://raw.githubusercontent.com/microsoft/WSL/8bc98bc33b246fe66710eec9eaa1b24c323da987/distributions/DistributionInfo.json').Content | ConvertFrom-Json).ModernDistributions.Ubuntu | Where-Object Name -eq $CatalogName", + "exit": 0, + "output_excerpt": "(no output); the catalog entry Ubuntu-26.04 was found at the pinned commit" + }, + { + "step": "W2", + "cmd": "$Actual = (Get-FileHash -Algorithm SHA256 -LiteralPath $ImagePath).Hash.ToLowerInvariant()", + "exit": 0, + "output_excerpt": "(no output); $Actual = 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104" + }, + { + "step": "W2", + "cmd": "\"computed $Actual published $Published listed $($Listed.Amd64Url.Sha256) url $($Listed.Amd64Url.Url)\"", + "exit": 0, + "output_excerpt": "computed 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 published 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 listed 48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104 url https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl" + }, + { + "step": "W2", + "cmd": "if ($Actual -ne $Expected -or $Published -ne $Expected -or $Listed.Amd64Url.Sha256 -ne $Expected -or $Listed.Amd64Url.Url -ne $ImageUrl) { throw 'sha256 mismatch: do not install' }", + "exit": 0, + "output_excerpt": "no throw: all four values equal the expected hash and the catalog URL equals the release URL" + }, + { + "step": "W2", + "cmd": "(Get-Item -LiteralPath $ImagePath).Length", + "exit": 0, + "output_excerpt": "418495746" + }, + { + "step": "W3", + "cmd": "New-Item -ItemType Directory -Force -Path (Join-Path $env:USERPROFILE '.cloud-init')", + "exit": 0, + "output_excerpt": "Directory: %USERPROFILE% | d----- .cloud-init" + }, + { + "step": "W3", + "cmd": "$Text = [System.IO.File]::ReadAllText('\\adoption\\templates\\wsl\\cloud-init.user-data.template').Replace('${WSL_USER}', '')", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W3", + "cmd": "[System.IO.File]::WriteAllText((Join-Path $env:USERPROFILE '.cloud-init\\StackRehearsal2604.user-data'), $Text)", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W3", + "cmd": "Get-Content -LiteralPath (Join-Path $env:USERPROFILE '.cloud-init\\StackRehearsal2604.user-data') -TotalCount 1", + "exit": 0, + "output_excerpt": "#cloud-config" + }, + { + "step": "W3", + "cmd": "Select-String -LiteralPath (Join-Path $env:USERPROFILE '.cloud-init\\StackRehearsal2604.user-data') -Pattern '^- name: ', '^ default='", + "exit": 0, + "output_excerpt": "line 13: - name: \nline 25: default=" + }, + { + "step": "W3", + "cmd": "(Get-FileHash -Algorithm SHA256 -LiteralPath (Join-Path $env:USERPROFILE '.cloud-init\\StackRehearsal2604.user-data')).Hash.ToLowerInvariant()", + "exit": 0, + "output_excerpt": "c7573fcd5ef1ea52dda89630565956c657c665f17d3b36733faacce4f7e7c3df (equals P2 sha256sum)" + }, + { + "step": "W3", + "cmd": "Select-String -LiteralPath (Join-Path $env:USERPROFILE '.cloud-init\\StackRehearsal2604.user-data') -SimpleMatch -Pattern '${'", + "exit": 0, + "output_excerpt": "(prints nothing)" + }, + { + "step": "W4", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W4", + "cmd": "wsl.exe --install --from-file 'Z:\\WSL\\downloads\\ubuntu-26.04.1-wsl-amd64.wsl' --name 'StackRehearsal2604' --location 'Z:\\WSL\\StackRehearsal2604' --no-launch", + "exit": 0, + "output_excerpt": "Installing: Z:\\WSL\\downloads\\ubuntu-26.04.1-wsl-amd64.wsl | Distribution successfully installed. It can be launched via 'wsl.exe -d StackRehearsal2604'" + }, + { + "step": "W4", + "cmd": "$LASTEXITCODE", + "exit": 0, + "output_excerpt": "0" + }, + { + "step": "W4", + "cmd": "wsl.exe --list --verbose", + "exit": 0, + "output_excerpt": " NAME STATE VERSION\n* NativeStack Running 2\n Running 2\n StackRehearsal2604 Stopped 2" + }, + { + "step": "W5", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W5", + "cmd": "cmd.exe /d /c \"wsl.exe -d StackRehearsal2604 < NUL\"", + "exit": 0, + "output_excerpt": "Provisioning the new WSL instance StackRehearsal2604\nThis might take a while...\nwsl: Failed to start the systemd user session for ''. See journalctl for more details." + }, + { + "step": "W5", + "cmd": "$LASTEXITCODE", + "exit": 0, + "output_excerpt": "0" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec id -un", + "exit": 0, + "output_excerpt": "" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec id -u", + "exit": 0, + "output_excerpt": "1000" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cloud-init --version", + "exit": 0, + "output_excerpt": "wsl: Failed to start the systemd user session for 'root'. See journalctl for more details.\n/bin/cloud-init 26.1-0ubuntu3~26.04.1" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cloud-init status --long", + "exit": 0, + "output_excerpt": "status: disabled\nextended_status: disabled\nboot_status_code: disabled-by-marker-file\nlast_update: Thu, 01 Jan 1970 01:32:40 +0000\ndetail: DataSourceWSL\nerrors: []\nrecoverable_errors: {}" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cat /var/lib/cloud/data/result.json", + "exit": 0, + "output_excerpt": "{\"v1\": {\"datasource\": \"DataSourceWSL\", \"errors\": []}}" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cat /var/lib/cloud/data/status.json", + "exit": 0, + "output_excerpt": "init-local: errors [] recoverable_errors {} start 5558.52 finished 5559.14; init: errors [] recoverable_errors {} start 5559.2 finished 5559.81; modules-config: errors [] recoverable_errors {} start 5560.0 finished 5560.06; modules-final: errors [] recoverable_errors {} start 5560.78 finished 5560.85 (times are seconds since kernel boot, not wall clock)" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cloud-init schema --system", + "exit": 0, + "output_excerpt": "Valid schema user-data" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cat /etc/wsl.conf", + "exit": 0, + "output_excerpt": "[boot]\nsystemd=true\n[user]\ndefault=" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec ls -l /etc/cloud/cloud-init.disabled", + "exit": 0, + "output_excerpt": "-rw-r--r-- 1 root root 0 Oct 1 22:22 /etc/cloud/cloud-init.disabled" + }, + { + "step": "W5", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec sudo -l -U ''", + "exit": 0, + "output_excerpt": "User may run the following commands on :\n (ALL : ALL) ALL\n (ALL) NOPASSWD: ALL" + }, + { + "step": "W5", + "cmd": "wsl.exe --list --verbose", + "exit": 0, + "output_excerpt": " NAME STATE VERSION\n* NativeStack Running 2\n Running 2\n StackRehearsal2604 Running 2" + }, + { + "step": "W5", + "cmd": "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc'", + "exit": 0, + "output_excerpt": "13 (P3 baseline 7; +6: the 'count equals the baseline' proof did not hold)" + }, + { + "step": "W5", + "cmd": "sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | grep -v 'registering driver hv_storvsc' | tail -n 6", + "exit": 0, + "output_excerpt": "[ 5542.982850] kernel: hv_storvsc : tag#1026 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1\n[ 5542.989792] kernel: hv_storvsc : tag#1058 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1\n[ 5542.996906] kernel: hv_storvsc : tag#1055 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1\n[ 5543.018069] kernel: hv_storvsc : tag#1087 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1\n[ 5543.024662] kernel: hv_storvsc : tag#1055 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1\n[ 5543.032208] kernel: hv_storvsc : tag#1087 cmd 0x2a status: scsi 0x0 srb 0x4 host 0xc00000a1\n(recipe: new lines when the count grew; tail -n set to the difference of 6)" + }, + { + "step": "W6", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --list --quiet", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --terminate 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --export 'StackRehearsal2604' 'Z:\\WSL\\downloads\\StackRehearsal2604-failed.tar'", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "$LASTEXITCODE", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "if ($LASTEXITCODE -ne 0) { throw 'export failed: do not unregister' }", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "(Get-FileHash -Algorithm SHA256 -LiteralPath 'Z:\\WSL\\downloads\\StackRehearsal2604-failed.tar').Hash.ToLowerInvariant()", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "(Get-Item -LiteralPath 'Z:\\WSL\\downloads\\StackRehearsal2604-failed.tar').Length", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --unregister 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --import 'StackRehearsal2604' 'Z:\\WSL\\StackRehearsal2604' 'Z:\\WSL\\downloads\\ubuntu-26.04.1-wsl-amd64.wsl' --version 2", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u root --exec cloud-init status --wait --long", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "id -u '' || useradd --create-home --uid 1000 --groups adm,cdrom,sudo,dip,plugdev --shell /bin/bash ''", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "printf '%s ALL=(ALL) NOPASSWD:ALL\\n' '' > /etc/sudoers.d/90-wsl-default-user", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "chmod 0440 /etc/sudoers.d/90-wsl-default-user", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "visudo -cf /etc/sudoers.d/90-wsl-default-user", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "grep -q '^\\[user\\]' /etc/wsl.conf || printf '\\n[user]\\ndefault=%s\\n' '' >> /etc/wsl.conf", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "touch /etc/cloud/cloud-init.disabled", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --terminate 'StackRehearsal2604'", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe --list --running", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W6", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec id -un", + "exit": null, + "output_excerpt": "not run: W5 showed neither path-B marker, so creation_path is A" + }, + { + "step": "W7", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": 0, + "output_excerpt": "(no output)" + }, + { + "step": "W7", + "cmd": "wsl.exe --terminate 'StackRehearsal2604'", + "exit": 0, + "output_excerpt": "The operation completed successfully." + }, + { + "step": "W7", + "cmd": "wsl.exe --list --running --quiet", + "exit": 0, + "output_excerpt": "NativeStack\n (StackRehearsal2604 absent)" + }, + { + "step": "W7", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec id -un", + "exit": 0, + "output_excerpt": "wsl: Failed to start the systemd user session for ''. See journalctl for more details.\n" + }, + { + "step": "W7", + "cmd": "New-Item -ItemType File -Path '\\\\wsl.localhost\\StackRehearsal2604\\home\\\\wsl-owner-probe'", + "exit": 0, + "output_excerpt": "created \\\\wsl.localhost\\StackRehearsal2604\\home\\\\wsl-owner-probe (0 bytes)" + }, + { + "step": "W7", + "cmd": "wsl.exe -d 'StackRehearsal2604' --exec stat -c %u:%g '/home//wsl-owner-probe'", + "exit": 0, + "output_excerpt": "1000:1000" + }, + { + "step": "W7", + "cmd": "Remove-Item -LiteralPath '\\\\wsl.localhost\\StackRehearsal2604\\home\\\\wsl-owner-probe'", + "exit": 0, + "output_excerpt": "(prints nothing)" + }, + { + "step": "F1", + "cmd": "systemctl is-system-running --wait", + "exit": 1, + "output_excerpt": "degraded" + }, + { + "step": "F1", + "cmd": "systemctl --failed --no-legend", + "exit": 0, + "output_excerpt": "● user@1000.service loaded failed failed User Manager for UID 1000" + }, + { + "step": "F1", + "cmd": "systemctl list-unit-files --type=service --no-pager", + "exit": 0, + "output_excerpt": "199 unit files listed." + }, + { + "step": "F2", + "cmd": "sudo loginctl enable-linger \"$(id -un)\"", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F2", + "cmd": "loginctl show-user \"$(id -un)\" --property=Linger --value", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F2", + "cmd": "$env:WSL_UTF8 = '1'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F2", + "cmd": "foreach ($Poll in 1..12) { Start-Sleep -Seconds 10; [DateTime]::UtcNow.ToString('HH:mm:ss'); wsl.exe --list --running --quiet }", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F3", + "cmd": "stat -c '%U %F' \"/run/user/$(id -u)\" \"/run/user/$(id -u)/bus\"", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F3", + "cmd": "systemctl --user is-system-running --wait", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F4", + "cmd": "sudo apt-get update", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F4", + "cmd": "sudo apt-get install -y --no-install-recommends ca-certificates curl git tar gzip xz-utils jq libatomic1 uidmap", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F4", + "cmd": "dpkg-query -W -f='${Package} ${Version}\\n' jq libatomic1 uidmap", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F5", + "cmd": "grep \"^$(id -un):\" /etc/subuid /etc/subgid", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F5", + "cmd": "sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 \"$(id -un)\"", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F5", + "cmd": "grep \"^$(id -un):\" /etc/subuid /etc/subgid", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F6", + "cmd": "test -e ~/.bash_profile || printf '%s\\n' 'if [ -r \"$HOME/.profile\" ]; then . \"$HOME/.profile\"; fi' > ~/.bash_profile", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F6", + "cmd": "cat ~/.bash_profile", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F7", + "cmd": "mkdir -p ~/code", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F7", + "cmd": "git clone https://github.com/seathatflowsinourveins/native-agent-stack.git ~/code/native-agent-stack", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F7", + "cmd": "git -C ~/code/native-agent-stack rev-parse HEAD", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F7", + "cmd": "git -C ~/code/native-agent-stack ls-remote origin refs/heads/main", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F8", + "cmd": "cd ~/code/native-agent-stack", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F8", + "cmd": "ss -ltnH '( sport = :24318 or sport = :29374 or sport = :26333 or sport = :28231 )'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F8", + "cmd": "python3 -c 'import string, sys; sys.stdout.write(string.Template(open(sys.argv[1], encoding=\"utf-8\").read()).substitute(WSL_USER=sys.argv[2]))' adoption/templates/wsl/host.new-distro.json.template \"$(id -un)\" > 'adoption/hosts/stack-rehearsal-2604.json'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F8", + "cmd": "python3 -m json.tool 'adoption/hosts/stack-rehearsal-2604.json'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F8", + "cmd": "git check-ignore 'adoption/hosts/stack-rehearsal-2604.json'", + "exit": null, + "output_excerpt": "not run: stopped at F1 (degraded)" + }, + { + "step": "F9", + "cmd": "cd ~/code/native-agent-stack", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F9", + "cmd": "adoption/bootstrap-linux.sh --profile ''", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F9", + "cmd": "adoption/bootstrap-linux.sh --profile '' --configure-full-profile --host 'stack-rehearsal-2604'", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F10", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u '' --exec /bin/bash -lc 'type -P claude codex'", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F10", + "cmd": "wsl.exe -d 'StackRehearsal2604' -u '' --exec /bin/bash -lc 'for p in $(type -P claude codex); do test -f $p && test -x $p && echo executable: $p; done'", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F11", + "cmd": "test -x \"${ECO_INSTALL_ROOT:-$HOME/.local/share/codex-ecosystem}/bin/jcodemunch-mcp\"", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F11", + "cmd": "cd ~/code/native-agent-stack", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F11", + "cmd": "claude mcp add --scope local jcodemunch -e \"CODE_INDEX_PATH=$HOME/.code-index\" -e JCODEMUNCH_SHARE_SAVINGS=0 -- \"${ECO_INSTALL_ROOT:-$HOME/.local/share/codex-ecosystem}/bin/jcodemunch-mcp\"", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F11", + "cmd": "claude mcp get jcodemunch", + "exit": null, + "output_excerpt": "not run: outside the scope of this run (stage 2 and later)" + }, + { + "step": "F1-diag (not in the recipe)", + "cmd": "systemctl status user@1000.service --no-pager -l", + "exit": 3, + "output_excerpt": "Active: failed (Result: resources); Tasks: 1841; Memory: 16.4G (peak: 47.4G); CPU: 21min 29.225s; CGroup: /user.slice/user-1000.slice/user@1000.service; journal: \"Failed to spawn executor: Device or resource busy\" | \"Failed to spawn 'start' task: Device or resource busy\" | \"Failed with result 'resources'\" | \"Start request repeated too quickly\"; attempts at 22:22:33, 22:22:34 and 22:27:38 EDT (W5 launch, W7 relaunch)" + }, + { + "step": "F1-diag (not in the recipe)", + "cmd": "journalctl -b --no-pager -u user@1000.service", + "exit": 0, + "output_excerpt": "attempts at 22:22:33, 22:22:34 and 22:27:38 EDT, each ending \"Failed to start user@1000.service - User Manager for UID 1000.\"" + }, + { + "step": "F1-diag (not in the recipe)", + "cmd": "systemctl status user-runtime-dir@1000.service --no-pager -l", + "exit": 0, + "output_excerpt": "Active: active (exited) since Thu 2026-10-01 22:27:38 EDT; ExecStart systemd-user-runtime-dir start 1000 (code=exited, status=0/SUCCESS)" + }, + { + "step": "F1-diag (not in the recipe)", + "cmd": "workstation distribution, run natively, not via wsl.exe -d: read pids.current, memory.current and systemctl status of its own user@1000.service", + "exit": 0, + "output_excerpt": "running since Thu 2026-10-01 20:55:58 EDT; Tasks: 1843; Memory: 16.5G (peak: 47.4G); CPU: 21min 32.766s; cgroup pids.current 1843" + }, + { + "step": "final-state (not in the recipe)", + "cmd": "wsl.exe --list --verbose", + "exit": 0, + "output_excerpt": " NAME STATE VERSION\n* NativeStack Running 2\n Running 2\n StackRehearsal2604 Running 2" + } + ], + "comparison_arms": { + "26.04.1": { + "status": "partial: stopped at F1 (degraded); the comparison probes did not run", + "name": "StackRehearsal2604", + "first_boot": "W5 exit 0 without the user or OOBE markers; id -un , id -u 1000; cloud-init 26.1-0ubuntu3~26.04.1; status disabled / disabled-by-marker-file; result.json DataSourceWSL errors []; status.json four stages finished with empty errors; schema --system 'Valid schema user-data' exit 0; F1 printed degraded with user@1000.service failed", + "systemd_user_from_second_instance": "F1 printed degraded: user@1000.service failed (Result: resources; 'Failed to spawn executor: Device or resource busy'); F3 and probes 1 and 2 did not run", + "wsl_gpu": "not run", + "uv_cpython_3_13": "not run", + "node_24": "not run" + }, + "24.04.5": { + "status": "not run", + "name": null, + "first_boot": null, + "systemd_user_from_second_instance": null, + "wsl_gpu": null, + "uv_cpython_3_13": null, + "node_24": null + } + }, + "stop": { + "step": "F1", + "proof_required": "systemctl is-system-running --wait prints running and systemctl --failed lists no unit", + "returned": "degraded (exit 1); systemctl --failed --no-legend: ● user@1000.service loaded failed failed User Manager for UID 1000", + "recipe_rule": "On degraded, record systemctl --failed and stop for review." + }, + "diagnostics_outside_recipe": { + "status": "read-only, run after the stop; no repair attempted", + "failed_unit": "user@1000.service: Active failed (Result: resources); journal 'Failed to spawn executor: Device or resource busy', 'Failed to spawn 'start' task: Device or resource busy', 'Start request repeated too quickly'; attempts at 22:22:33 to 22:22:34 EDT (W5 launch) and 22:27:38 EDT (W7 relaunch); user-runtime-dir@1000.service active (exited)", + "shared_cgroup_inference": "The failed unit's accounting in the new distribution (Tasks 1841, Memory 16.4G peak 47.4G, CPU 21min 29.225s) equals the workstation's running user@1000.service read about 40 s later (Tasks 1843, Memory 16.5G peak 47.4G, CPU 21min 32.766s): a freshly provisioned distribution cannot have accumulated those counters, so both distributions' user@1000.service point at one cgroup, /user.slice/user-1000.slice/user@1000.service, and both users have uid 1000. Inference only." + }, + "deviations": [ + "R1 removal block not run: StackRehearsal2604 stays registered (terminate, export and unregister skipped) because the coordinator uses it next.", + "Run stopped at F1 on its recipe stop; F2 to F8 and the seven comparison probes were not run.", + "Added instrumentation only: exit-code markers after commands and a transcript per PowerShell block; no recipe command was altered." + ] +} diff --git a/manifests/evidence.json b/manifests/evidence.json index bdb3e0747..a7a84e3bc 100644 --- a/manifests/evidence.json +++ b/manifests/evidence.json @@ -4298,6 +4298,11 @@ "sha256": "1d693b66002e8931af2370fc9da919f667bfbf3a1a22ddd54bdd2c1d81cf19e4", "bytes": 43700 }, + { + "path": "adoption/platforms/linux-wsl2-new-distro.md", + "sha256": "91e8052e075bff33aa92e2253a8309e21381f8c5ca2aab680996b39e79ae6cb1", + "bytes": 82092 + }, { "path": "adoption/platforms/linux-wsl2.md", "sha256": "149b05c61320363dca3264f88962019fcd51523dbbc78f1be818a6df9fbd3cba", @@ -4403,6 +4408,21 @@ "sha256": "47e88b71600dcb50f05f22ec70361a5fdb8338d207165008760715656502c0f8", "bytes": 9260 }, + { + "path": "adoption/templates/wsl/cloud-init.user-data.template", + "sha256": "08e6a3f6b078dfa54d78297596d54729d10528d57147b9b9eae063c37160eeb9", + "bytes": 1527 + }, + { + "path": "adoption/templates/wsl/first-boot-checklist.md", + "sha256": "2ecc17772416e39390a11d5cf30414bbc34540812114a6dfc44b9dc36eb7f23d", + "bytes": 13674 + }, + { + "path": "adoption/templates/wsl/stage1-receipt.example.json", + "sha256": "5dd17f19b447e92e1ecbbec82e5d38d219bd4cbc1f3402e03d9202c0afaa466d", + "bytes": 60178 + }, { "path": "adoption/tools/README.md", "sha256": "26cd1c2ac3d50e26f71e6080c9cff72711dd38f8fe9c5b2c4dfbb913e0f50d0a", @@ -6466,8 +6486,8 @@ }, { "path": "blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json", - "sha256": "d8bb8c5edcc19e9d143e6edf4368e0b156732803fc26edc04219310e21fad73d", - "bytes": 15659 + "sha256": "4c23cd164947f8eee9a3eeb456a5a8b234f13224df0e968f42a537271ede94f1", + "bytes": 23065 }, { "path": "blueprints/convergence-practice/wsl-restore/README.md", @@ -15704,6 +15724,11 @@ "sha256": "19431b3f3bb47ae7f85a2dd5e91b231713e41d518705d55f532e2a494bbee92f", "bytes": 62594 }, + { + "path": "docs/decisions/2026-10-01-new-wsl-distro-recipe.md", + "sha256": "0fb3f1fd31e1cfea0b49923486eb705d29e51c4da00c9cc24bfed959235fed7a", + "bytes": 137555 + }, { "path": "docs/decisions/2026-10-02-github-automation-practice.md", "sha256": "e90950fe72016e3d4732d6beb36ed0f898e9ccf550e2fa69866b6e3b5730efb2", @@ -36544,6 +36569,11 @@ "sha256": "4424d1abb1208518fb27ecec6219bec7f101b25b4d01f4b8d4c3e0ea0e76cf57", "bytes": 10505 }, + { + "path": "evidence/artifacts/new-wsl-dual-image-20261001/receipt.json", + "sha256": "a591594fd223b81d60597ebbaeb9a3e55a3b36d97646756a0c540fc7394a7d97", + "bytes": 3558 + }, { "path": "evidence/artifacts/new-wsl-final-architecture-20261002/added-slots/copy-notes.json", "sha256": "9827d2a034b8e266877a146994cecb5e08fc54410911c3a71672c32dfea558d0", @@ -37399,6 +37429,21 @@ "sha256": "f42db81b904dd51c0475d2d953e2a92b0720477cc29ef5eb5a67c8c3d685693a", "bytes": 8224 }, + { + "path": "evidence/artifacts/new-wsl-rehearsal-20261002/coordinator-review.md", + "sha256": "7bc9fdf96035fe2ca63ec5aacc8f9ac8fcd2ada94589ccafb6bbe1e094745526", + "bytes": 4199 + }, + { + "path": "evidence/artifacts/new-wsl-rehearsal-20261002/runs-on-wsl-3.0.1.json", + "sha256": "dcac7abf8b9cca8ee931f592810217121cdc99e78b432f96d8d9a63ed481bc66", + "bytes": 41233 + }, + { + "path": "evidence/artifacts/new-wsl-rehearsal-20261002/stage1-receipt.json", + "sha256": "7a4c400decc6d5a4317ec36571377971c5c82c89fec4959ee18a605c379fa780", + "bytes": 51588 + }, { "path": "evidence/artifacts/notification-types-20260929/README.md", "sha256": "a056580fbe32371bfdddb46aa0c9c1947f27b91066be644d4382288670b4424c", @@ -49674,6 +49719,11 @@ "sha256": "020530918ede07f057d28a8c4234d3ed1e0c0f4cb886f5fee1f772beaed7787f", "bytes": 12555 }, + { + "path": "tests/test_wsl_new_distro_recipe.py", + "sha256": "ba9046d7730bb72f95b52408271c26c13151085a855db00132eb740571d061cb", + "bytes": 191668 + }, { "path": "tests/test_wsl_restore.py", "sha256": "9df52e8b163627955662dadf5ca72ab19b1db6ab340a29522fb8b69faa928833", diff --git a/tests/test_wsl_new_distro_recipe.py b/tests/test_wsl_new_distro_recipe.py index bc977fc87..85a86c7a7 100644 --- a/tests/test_wsl_new_distro_recipe.py +++ b/tests/test_wsl_new_distro_recipe.py @@ -14,7 +14,7 @@ - every command line in the recipe's ``powershell`` and ``sh`` code blocks is a row of the decision record's command table under the same step and shell, once per occurrence, and the table lists nothing else; -- the image sha256 is one value in the recipe, the record and the stage-1 receipt example, and the +- the two release-to-sha256 pairs match the primary sources in the recipe, record and synthetic receipt example, and the receipt example carries the payload keys scripts/validate.py compares with a ``receipts[]`` row; - no recipe command shuts WSL down, updates it, edits .wslconfig, changes the default distribution, or terminates, unregisters or manages any distribution but ```` (decision 4), and the one @@ -46,15 +46,27 @@ - C2: W5 runs ``cloud-init schema --system`` as root on path A, and W6 skips it on path B; - D: P3 records the current boot's ``hv_storvsc`` kernel journal count, without the driver's registration line, as a baseline and stops only when the newest error line is less than one hour old by its kernel time; W5 counts again - after the first launch on both paths, and a larger count is exposure to microsoft/WSL#41482 (the coordinator's - rule of 2026-10-01, which replaced the stop on any nonzero count); + after the first launch on both paths; attachment-only increases before cloud-init starts are recorded, and later + storage errors or a provisioning failure with a storage cause stop the run (the 2026-10-02 rehearsal correction); - E1 and E2: W7 terminates ```` once after the first launch, relaunches it and reads the owner of a file created from Windows (microsoft/WSL#40941, PR #40977); - F: W1 reads the two idle keys of the global WSL configuration, F2 observes ```` for two minutes with no client attached, and the host-wide check passes a plain ``Select-String`` or ``grep`` read of ``.wslconfig`` and nothing else that names it; - G: R1, the page's first step, rehearses on a throwaway name with both storage readings and removes only that - name, exporting it first when the rehearsal failed. + name, exporting it first when the rehearsal failed; +- the follow-up of 2026-10-02 to the first observations of the updated host (WSL 3.0.1.0), each rule bound to the recipe, + the record, the checklist and the receipt example, and each with in-memory controls that restore the pre-change text: + - Host-wide rules adopt stable WSL 3.0.1 or later for a second systemd distribution, as policy, and say the host was + updated and passed its check with the two-distribution proof still owed; + - W1 records the workstation's baseline of five values without sudo (``/proc/self`` where W1 read ``/proc/1``), and W5 + reads the same five values from both running distributions, after its cgroup block and before F1, under the receipt's + ``paired_isolation`` (one object per distribution); W6 repeats that record; + - F1 accepts ``degraded`` only when ``systemctl --failed --no-legend --plain`` lists exactly ``systemd-binfmt.service`` + and its log holds the read-only flush message (microsoft/WSL#40621 and #41226); any other failed unit stops the run; + - no recipe command controls ``systemd-binfmt``: the restart that served WSL 2.7.x exits 1 on the adopted release, so a + failed interop check after an unregister stops for review with two records after the check (a history sentence may + still name the restart). These are local consistency checks over repository text and an in-memory render of the templates. Nothing here runs wsl.exe, PowerShell, gpgv, journalctl or cloud-init; a pass is not a host run. @@ -79,13 +91,99 @@ HOST_TEMPLATE = TEMPLATES / "host.new-distro.json.template" RECEIPT_EXAMPLE = TEMPLATES / "stage1-receipt.example.json" CHECKLIST = TEMPLATES / "first-boot-checklist.md" +SOURCE_RECEIPT = ROOT / "evidence/artifacts/new-wsl-dual-image-20261001/receipt.json" HOST_EXAMPLE = ROOT / "adoption/hosts/example.json" STACK = ROOT / "manifests/stack.json" BOOTSTRAP = ROOT / "adoption/bootstrap.md" -IMAGE = "ubuntu-24.04.5-wsl-amd64.wsl" +IMAGE = "ubuntu--wsl-amd64.wsl" IMAGE_BYTES = 388975696 +RELEASE_PINS = { + "26.04.1": { + "file": "ubuntu-26.04.1-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/26.04.1/ubuntu-26.04.1-wsl-amd64.wsl", + "sha256": "48d56724b5c8e60f24893e83e73bbb58c60b3ca22fba3da977075420acd54104", + "catalog_name": "Ubuntu-26.04", + "bytes": 418495746, + }, + "24.04.5": { + "file": "ubuntu-24.04.5-wsl-amd64.wsl", + "url": "https://releases.ubuntu.com/24.04.5/ubuntu-24.04.5-wsl-amd64.wsl", + "sha256": "bb415d824822c4b878125729af451a5d18fb13d1cf5cbed9a7393ad64ac6039e", + "catalog_name": "Ubuntu-24.04", + "bytes": IMAGE_BYTES, + }, +} PLACEHOLDER = "${WSL_USER}" +# The review contract is independent of the recipe, checklist and receipt. Losing a stage from all three must fail. +REQUIRED_STAGE_IDS = ( + "R1", "P1", "P2", "P3", "W1", "W2", "W3", "W4", "W5", "W6", "W7", + "F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", +) +SELECTED_IMAGE_DIGEST_FIELDS = ("sha256_published", "sha256_computed", "sha256_distributioninfo") +INTEROP_VERIFY = ("wsl.exe -d '' --exec sh -c " + "'test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver'") +INTEROP_GUARD = ("if ($LASTEXITCODE -ne 0) { throw " + "'interop failed: recover in the surviving distribution before continuing' }") +# The follow-up of 2026-10-02 (the updated host, WSL 3.0.1). W1 records the workstation's baseline of five values and W5 +# reads the same five from both running distributions. The namespace read is `/proc/self`, which needs no sudo, where the +# earlier W1 read `/proc/1`. +OLD_NAMESPACE_READ = "readlink /proc/1/ns/cgroup" +NAMESPACE_READ = "readlink /proc/self/ns/cgroup" +WORKSTATION_COMMANDS = ["id -u", "systemctl is-system-running", "systemctl --failed --no-legend --plain", + 'systemctl is-active "user@$(id -u).service"', NAMESPACE_READ] +NEW_COMMANDS = ["/mnt/c/Windows/System32/wsl.exe -d '' --exec " + command for command in + (*WORKSTATION_COMMANDS[:3], "sh -c '" + WORKSTATION_COMMANDS[3] + "'", NAMESPACE_READ)] +WORKSTATION_BASELINE = "\n".join(WORKSTATION_COMMANDS) +PAIRED_NEW = "\n".join(NEW_COMMANDS) +ISOLATION_KEYS = {"uid", "system_state", "failed_units", "user_manager", "cgroup_namespace"} +CGROUP_PROCS = "cat '/sys/fs/cgroup/system.slice//cgroup.procs'" +USER_MANAGER_ACTIVE = ('/mnt/c/Windows/System32/wsl.exe -d \'\' --exec sh -c ' + '\'systemctl is-active "user@$(id -u).service"\'') +STORAGE_RULE = ("On both paths, record P3's baseline before the import and `second_count` after the first launch. " + "An increase confined to the window in which the new disk is attached, before cloud-init starts, " + "is recorded with the new lines and does not stop the run. Any storage error after that window, " + "or any provisioning step that fails with a storage cause, stops the run.") +OLD_STORAGE_RULE = ("On both paths, the second storage count equals P3's baseline; a larger count is recorded " + "with the new lines, a failed W5 counts as exposure to microsoft/WSL#41482, and nothing " + "continues to stage 2 until that is decided.") +# The follow-up of 2026-10-02, change 1: the adopted target, as policy, and the two sentences it replaced. +ADOPTED_TARGET = ("This recipe adopts stable WSL 3.0.1 or later for a second systemd distribution, and requires it " + "before W4 or any W6 import.") +HOST_UPDATE = ("The host was updated to WSL 3.0.1.0 on 2026-10-02 and passed its check (rootless container, interop, " + "user manager); run 2 observed distinct cgroup namespaces, local process ids and two active user " + "managers while both distributions ran.") +OLD_ADOPTED_TARGET = "A second systemd distribution requires WSL 3.0.1 or later before W4 or any W6 import." +OLD_HOST_UPDATE = "The host's 2.7.13 must be updated outside this page before this second-distribution run." +# Change 3: on the adopted release WSL mounts the binfmt status file read-only (microsoft/WSL#40621) while the +# distribution's protectBinfmt setting is on (its default; a best-effort step), and systemd-binfmt.service then fails, +# which upstream calls benign (#41226). F1 accepts that one failed unit. +BINFMT_UNIT = "systemd-binfmt.service" +F1_FAILED = "systemctl --failed --no-legend --plain" +F1_LOG = "journalctl -b 0 -t systemd-binfmt --no-pager -n 4" +F1_PASS = ("`degraded` when `systemctl --failed --no-legend --plain` lists exactly `systemd-binfmt.service` and that " + "unit's log, from the `journalctl` line, holds") +FLUSH_MESSAGE = "Failed to flush binfmt_misc rules, ignoring: Read-only file system" +# F1 reads that log as `` without sudo (the user is in `adm`); on `degraded` with only the journal's permission +# notice, the line is repeated once with sudo, which the recipe's other journal commands also use. +F1_SUDO_FALLBACK = ("On `degraded`, if the `journalctl` line prints only `Hint: You are currently not seeing messages from " + "other users and the system.` or `-- No entries --`, the user cannot read the journal: repeat that line " + "once with `sudo`, record both outputs and judge the second.") +BINFMT_PR = "https://github.com/microsoft/WSL/pull/40621" +BINFMT_ISSUE = "https://github.com/microsoft/WSL/issues/41226" +BINFMT_BENIGN = "The systemd error is benign and won't affect registration of user defined binfmt settings." +OLD_F1 = ("```sh\nsystemctl is-system-running --wait\nsystemctl --failed --no-legend\n" + "systemctl list-unit-files --type=service --no-pager\n```\n\n" + "Proof: `running`, no failed unit, and the service list prints. Ubuntu's own setup tests require `running` for " + "a new\ninstance. On `degraded`, record `systemctl --failed` and stop for review.\n\n") +# The interop recovery: the restart was the WSL 2.7.x remedy; on the adopted release it exits 1, so a failed check stops +# for review with two records. The native check stays in front of them. +NATIVE_INTEROP = "test -e /proc/sys/fs/binfmt_misc/WSLInterop && /mnt/c/Windows/System32/cmd.exe /d /c ver" +INTEROP_RECORDS = ("ls /proc/sys/fs/binfmt_misc", "systemctl status systemd-binfmt.service --no-pager") +RESTART_UNIT = "sudo systemctl restart systemd-binfmt || exit 1" +OLD_INTEROP_RECOVERY = RESTART_UNIT + "\n" + NATIVE_INTEROP + "\n" + 'if [ "$?" -ne 0 ]; then exit 1; fi' + "\n" +UNIT_CONTROL_RE = re.compile(r"\bsystemctl\b.*\b(?:restart|try-restart|reload-or-restart|start|stop)\b.*systemd-binfmt") +USER_SESSION_WARNING = "wsl: Failed to start the systemd user session" # Loopback ports the workstation distribution already uses: the four in adoption/hosts/example.json, the gateway, # memory and the 2026-09-25 relocations named in adoption/platforms/linux-wsl2.md ("Listeners and ports"), and every # 127.0.0.1 port in observability/backends/templates and configure.py. The recipe lists the same set (F8). @@ -111,15 +209,15 @@ # The 2026-10-01 follow-up (the completeness critic's pre-stage-1 checks). R1 rehearses and removes a throwaway # distribution, P1 to P3 are sh pre-checks in the workstation distribution, W7 terminates once and probes ownership. EXPERIMENT = ROOT / "blueprints/convergence-practice/wsl-new-distro-image-20261001/experiment.json" -SUMS_URL = "https://releases.ubuntu.com/24.04.5/SHA256SUMS" +SUMS_URL = "https://releases.ubuntu.com/$RELEASE/SHA256SUMS" CD_IMAGE_SIGNER = "843938DF228D22F7B3742BC0D94AA3F0EFE21092" P1_COMMANDS = [ 'SUMS_DIR="$(mktemp -d)"', - f'curl -fsSL -o "$SUMS_DIR/SHA256SUMS" {SUMS_URL}', - f'curl -fsSL -o "$SUMS_DIR/SHA256SUMS.gpg" {SUMS_URL}.gpg', + f'curl -fsSL -o "$SUMS_DIR/SHA256SUMS" "{SUMS_URL}" || exit 1', + f'curl -fsSL -o "$SUMS_DIR/SHA256SUMS.gpg" "{SUMS_URL}.gpg" || exit 1', 'gpgv --homedir "$SUMS_DIR" --keyring /usr/share/keyrings/ubuntu-archive-keyring.gpg "$SUMS_DIR/SHA256SUMS.gpg" ' '"$SUMS_DIR/SHA256SUMS"', - "grep ' \\*ubuntu-24\\.04\\.5-wsl-amd64\\.wsl$' \"$SUMS_DIR/SHA256SUMS\"", + 'grep -F " *ubuntu-$RELEASE-wsl-amd64.wsl" "$SUMS_DIR/SHA256SUMS" || exit 1', ] P2_RENDER = ("python3 -c 'import string, sys; sys.stdout.write(string.Template(open(sys.argv[1], encoding=\"utf-8\")" ".read()).substitute(WSL_USER=sys.argv[2]))' adoption/templates/wsl/cloud-init.user-data.template " @@ -131,11 +229,15 @@ SCHEMA_SYSTEM = "wsl.exe -d '' -u root --exec cloud-init schema --system" SCHEMA_LINE = "`^\\s*Valid schema user-data$`" CLOUD_INIT_HOWTO = "https://ubuntu.com/wsl/docs/stable/howto/cloud-init/" -STORVSC_COUNT = "sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | grep -vc 'registering driver hv_storvsc'" +STORVSC_COUNT = ("sudo journalctl -k -b 0 --no-pager | grep hv_storvsc | " + "grep -Evc 'registering driver hv_storvsc|[Cc]ommand line:'") # The coordinator's P3 rule (2026-10-01): the count is a baseline; the newest error line's kernel time against the # uptime decides whether errors are current; W5 counts again after the first launch. STORVSC_NEWEST = ("sudo journalctl -k -b 0 --no-pager -o short-monotonic --no-hostname | grep hv_storvsc | " - "grep -v 'registering driver hv_storvsc' | tail -n 1") + "grep -Ev 'registering driver hv_storvsc|[Cc]ommand line:' | tail -n 1") +# Earlier evidence sections retain the commands actually run in the 2026-10-01 artifact check (B6). +HISTORICAL_STORAGE_COMMANDS = tuple(command.replace("grep -Ev", "grep -v").replace("|[Cc]ommand line:", "") + for command in (STORVSC_COUNT, STORVSC_NEWEST)) UPTIME = "cat /proc/uptime" P3_COMMANDS = [STORVSC_COUNT, STORVSC_NEWEST, UPTIME, "swapon --show"] STORAGE_FIELDS = {"baseline": "' -u '' --exec /bin/bash -lc " + "'for n in claude codex; do p=$(type -P $n); [[ -f $p && -x $p ]] && echo executable: $p; done'") +R1_BUS_PROBE = "wsl.exe -d '' --exec bash -lc 'stat -c %U,%F /run/user/$(id -u) /run/user/$(id -u)/bus'" +F5_GUARDED = ('grep -q "^$(id -un):" /etc/subuid || sudo usermod --add-subuids 100000-165535 ' + '--add-subgids 100000-165535 "$(id -un)"') IDLE_READ = ("Select-String -LiteralPath (Join-Path $env:USERPROFILE '.wslconfig') -Pattern '^\\s*\\[', " "'^\\s*instanceIdleTimeout\\s*=', '^\\s*vmIdleTimeout\\s*=' -ErrorAction SilentlyContinue") IDLE_POLL = ("foreach ($Poll in 1..12) { Start-Sleep -Seconds 10; [DateTime]::UtcNow.ToString('HH:mm:ss'); " @@ -257,6 +370,10 @@ def user_data_errors(template: str, user: str = "example") -> list[str]: r"^ append: true$", r"^ content: \|$", r"^ \[user\]$", rf"^ default={re.escape(user)}$") errors += [f"the render lacks a line matching {pattern}" for pattern in required if not re.search(pattern, rendered, re.M)] + if template.count(GETTY_BOOTCMD) != 1: + errors.append("the user-data must hold the exact bootcmd getty mask once") + if not re.search(r"^ default=\$\{WSL_USER\}\n\n(?:#[^\n]*\n){1,4}" + re.escape(GETTY_BOOTCMD), template, re.M): + errors.append("the bootcmd must follow write_files with one blank line and at most four comment lines") if re.search(r"^\s*(passwd|hashed_passwd|plain_text_passwd):", rendered, re.M): errors.append("the user-data sets a password; the recipe's user has a locked password and NOPASSWD sudo") return errors @@ -298,23 +415,65 @@ def host_template_errors(template: str, example: dict, recipe: str, user: str = def sha256_errors(recipe: str, record: str, receipt: dict, checklist: str) -> list[str]: + """Require the exact primary-source release pairs, not merely membership in a set of two hashes.""" errors = [] - recipe_values = set(SHA256_RE.findall(recipe)) - if len(recipe_values) != 1: - return [f"the recipe names {len(recipe_values)} distinct sha256 values, expected exactly one (the image's)"] - (expected,) = recipe_values - record_values = {value for line in record.splitlines() if IMAGE in line for value in SHA256_RE.findall(line)} - if not record_values: - errors.append(f"the record names no sha256 on a line that names {IMAGE}") - elif record_values != {expected}: - errors.append(f"the record's {IMAGE} sha256 {sorted(record_values)} differs from the recipe's {expected}") - if set(SHA256_RE.findall(checklist)) != {expected}: - errors.append("the checklist's W2 sha256 differs from the recipe's") + expected_values = {pin["sha256"] for pin in RELEASE_PINS.values()} + for name, text in (("recipe", recipe), ("checklist", checklist)): + if set(SHA256_RE.findall(text)) != expected_values: + errors.append(f"the {name} does not name exactly the two primary-source image hashes") + for release, pin in RELEASE_PINS.items(): + for name, text in (("recipe", recipe), ("record", record), ("checklist", checklist)): + values = {value for line in text.splitlines() if pin["file"] in line for value in SHA256_RE.findall(line)} + if values != {pin["sha256"]}: + errors.append(f"the {name}'s {release} file-to-sha256 pairing differs from its primary-source pin") + arms = re.findall(r"^\s*'([^']+)' \{ \$Expected = '([0-9a-f]{64})'; \$CatalogName = '([^']+)' \}$", + section(recipe, "W2"), re.M) + expected_arms = {(release, pin["sha256"], pin["catalog_name"]) for release, pin in RELEASE_PINS.items()} + if len(arms) != 2 or set(arms) != expected_arms: + errors.append("W2 does not control the two release-to-hash-to-catalog mappings") + w2 = section(recipe, "W2") + for needed in ("default { throw 'unsupported release: do not download or install' }", + "$Actual -ne $Expected", "$Published -ne $Expected", "$Listed.Amd64Url.Sha256 -ne $Expected", + "$Listed.Amd64Url.Url -ne $ImageUrl", "throw 'sha256 mismatch: do not install'", + "(Get-FileHash -Algorithm SHA256 -LiteralPath $ImagePath).Hash.ToLowerInvariant()"): + if needed not in w2: + errors.append(f"W2 lacks its selected-image verification: {needed}") + if receipt.get("supported_images") != RELEASE_PINS: + errors.append("the synthetic receipt's controlled release pins or observed sizes differ from the primary sources") image = receipt.get("image", {}) if isinstance(receipt, dict) else {} - if image.get("file") != IMAGE or image.get("bytes") != IMAGE_BYTES: - errors.append("the receipt example's image is not the recipe's file and size") - if image.get("sha256_published") != expected: - errors.append("the receipt example's sha256_published differs from the recipe's") + if image.get("release") != "" or image.get("file") != IMAGE or image.get("url") != ( + "https://releases.ubuntu.com//" + IMAGE): + errors.append("the synthetic receipt does not name the selected release's file and URL") + for key in ("bytes", *SELECTED_IMAGE_DIGEST_FIELDS): + if not is_placeholder(image.get(key), " list[str]: + """Check every selected-image digest, including additional digest fields, against the recipe's release pair.""" + image = receipt.get("image", {}) + errors = [f"image lacks {key}" for key in SELECTED_IMAGE_DIGEST_FIELDS if key not in image] + release = image.get("release") + synthetic = receipt.get("artifact_class") == "synthetic_fixture" and release == "" + if not synthetic and release not in RELEASE_PINS: + return errors + ["image has no supported selected release"] + selected = RELEASE_PINS if synthetic else {release: RELEASE_PINS[release]} + recipe_hashes = {} + for name, pin in selected.items(): + hashes = {value for line in recipe.splitlines() if pin["file"] in line for value in SHA256_RE.findall(line)} + if len(hashes) != 1: + errors.append(f"recipe has no single digest for {name}") + else: + recipe_hashes[name] = hashes.pop() + for key, value in image.items(): + if "sha256" not in key.lower(): + continue + if synthetic: + if not is_placeholder(value, " list[ # recipe's command rows (the rows the command-table check reads), once per occurrence, and nothing else. rows = recipe_rows(recipe) steps = set(STEP_RE.findall(recipe)) + if steps != set(REQUIRED_STAGE_IDS): + errors.append("the recipe does not contain the independent required stage ids") if {step for step, _, _ in rows} != steps: errors.append(f"recipe steps without a command line: {sorted(steps - {step for step, _, _ in rows})}") entries = [entry for entry in receipt.get("steps", []) if isinstance(entry, dict)] @@ -350,6 +511,20 @@ def receipt_errors(receipt: dict, recipe: str, component_ids: set[str]) -> list[ for entry in receipt.get("steps", []): if not isinstance(entry, dict) or not {"step", "cmd", "exit", "output_excerpt"} <= set(entry): errors.append(f"receipt step {entry!r} lacks step, cmd, exit or output_excerpt") + mask = field(receipt, "first_launch", "getty_mask") + if not isinstance(mask, dict) or set(mask) != {"is_enabled", "load_state", "active_state", "n_restarts"} or not all( + is_placeholder(value, " list[str]: if not steps: return ["the recipe has no ### W. or ### F. step headings"] errors = [] + if tuple(steps) != REQUIRED_STAGE_IDS or tuple(listed) != REQUIRED_STAGE_IDS: + errors.append("the recipe or checklist differs from the independent required stage ids and order") if sorted(set(listed)) != sorted(set(steps)): errors.append(f"the checklist names {sorted(set(listed))}, the recipe has {sorted(set(steps))}") if len(listed) != len(set(listed)): @@ -452,9 +629,24 @@ def path_proof_errors(recipe: str) -> list[str]: (adoption/platforms/linux-wsl2.md, Windows Terminal step 2), so F10 tests every printed path with ``test -f`` and ``test -x``.""" f10 = [command for step, _, command in recipe_rows(recipe) if step == "F10"] - if any("type -P claude codex" in command and "test -f" in command and "test -x" in command for command in f10): + if F10_PROBE in f10: return [] - return ["F10 does not test each path `type -P claude codex` prints with `test -f` and `test -x`"] + return ["F10 does not obtain each path without word splitting and test it inside [[ -f ... && -x ... ]]"] + + +def powershell_quote_errors(recipe: str) -> list[str]: + """Rehearsal run 3 (2026-10-02): Windows PowerShell 5.1 does not escape a double quote inside an argument it hands + to a native program, so `wsl.exe ... 'stat -c "%U %F" ...'` reached Bash as `stat -c %U` and returned + `stat: missing operand`. No single-quoted argument of a `wsl.exe` command in a PowerShell block holds one.""" + errors = [] + for step, shell, command in recipe_rows(recipe): + if shell != "powershell" or not command.startswith("wsl.exe"): + continue + for argument in re.findall(r"'([^']*)'", command): + if '"' in argument: + errors.append(f"{step}: a PowerShell command hands wsl.exe an argument with a double quote: {command}") + break + return errors def step_blocks(recipe: str, step: str) -> list[list[str]]: @@ -587,6 +779,16 @@ def prose(text: str) -> str: return re.sub(r"\s+", " ", text) +def reword(text: str, old: str, new: str) -> str: + """``text`` with the one phrase ``old`` replaced by ``new``, however the Markdown wraps it. It fails when the phrase is + not there exactly once, so a control that restores the pre-change wording cannot pass by changing nothing.""" + pattern = r"\s+".join(re.escape(word) for word in old.split()) + mutated, count = re.subn(pattern, lambda _match: new, text) + if count != 1: + raise AssertionError(f"{old!r} occurs {count} times, expected one") + return mutated + + def checklist_line(checklist: str, step: str) -> str: return next((line for line in checklist.splitlines() if line.startswith(f"- [ ] **{step}**")), "") @@ -619,6 +821,9 @@ def signed_sums_errors(recipe: str, record: str, checklist: str, receipt: dict) if {shell for shell, _ in commands} != {"sh"}: errors.append("P1 is not an sh step of the workstation distribution") lines = [command for _, command in commands] + for needed in ("for RELEASE in 26.04.1 24.04.5; do", 'if [ "$?" -ne 0 ]; then exit 1; fi', "done"): + if needed not in lines: + errors.append(f"P1 lacks both-arm signature verification: {needed}") missing = [line for line in P1_COMMANDS if line not in lines] errors += [f"P1 lacks `{line}`" for line in missing] if not missing and [lines.index(line) for line in P1_COMMANDS] != sorted(lines.index(line) for line in P1_COMMANDS): @@ -704,8 +909,8 @@ def kernel_storage_errors(recipe: str, record: str, checklist: str, receipt: dic without the driver's registration line, which every boot logs. The count is a baseline, not a verdict. P3 prints the newest error line with its kernel time and the uptime: a line less than one hour old means errors are happening now, and the run stops. The one-hour threshold is the recipe's choice. W5 counts again after the first launch, on both - paths: an equal count passes; a larger one is recorded with the new lines as exposure to microsoft/WSL#41482, and - nothing continues to stage 2 until that is decided. A bare count that stops on any line was rejected: it cannot tell + paths: attachment errors before cloud-init starts are recorded; later errors or a provisioning failure with a + storage cause stop the run. A bare count that stops on any line was rejected: it cannot tell an old burst from errors that are happening now.""" steps = STEP_RE.findall(recipe) if "P3" not in steps: @@ -722,7 +927,8 @@ def kernel_storage_errors(recipe: str, record: str, checklist: str, receipt: dic rows = recipe_rows(recipe) second = [index for index, (step, shell, command) in enumerate(rows) if step == "W5" and shell == "sh"] launch = [index for index, (step, _, command) in enumerate(rows) if step == "W5" and "< NUL" in command] - if [rows[index][2] for index in second] != [STORVSC_COUNT] or not launch or second[0] < launch[0]: + counts = [index for index in second if rows[index][2] == STORVSC_COUNT] + if len(counts) != 1 or not launch or counts[0] < launch[0]: errors.append("W5 does not count the storage errors again, in the workstation distribution, after the launch") w5 = prose(section(recipe, "W5")) errors += [f"W5's text does not name {needed}" for needed in @@ -730,7 +936,7 @@ def kernel_storage_errors(recipe: str, record: str, checklist: str, receipt: dic if needed not in w5] evidence = prose(chapter(record, "Evidence classes")) errors += [f"the record's Evidence classes lack P3's {needed}" for needed in - (STORVSC_COUNT, STORVSC_NEWEST, UPTIME, "swapon --show", "registering driver hv_storvsc", "baseline") + (*HISTORICAL_STORAGE_COMMANDS, UPTIME, "swapon --show", "registering driver hv_storvsc", "baseline") if needed not in evidence] if THRESHOLD_SOURCE not in prose(record) or "bare count" not in prose(chapter(record, "Alternatives")): errors.append("the record does not say the threshold is the recipe's choice and why a bare count was rejected") @@ -847,7 +1053,7 @@ def rehearsal_errors(recipe: str, record: str, checklist: str, receipt: dict, ex errors += [f"R1's text does not name {needed}" for needed in ("throwaway", "through F3", "`cloud-init schema --system`", "W7", "F2", "only its own distribution", "W6's export rule", "`rehearsal`", "P3's baseline and W5's second count") if needed not in r1] - blocks = step_blocks(recipe, "R1") + blocks = [block for block in step_blocks(recipe, "R1") if UNREGISTER in block] passed = [block for block in blocks if not any("--export" in command for command in block)] failed = [block for block in blocks if any("--export" in command for command in block)] if len(passed) != 1 or len(failed) != 1: @@ -959,20 +1165,34 @@ def test_commands_are_read_from_indented_fences_with_continuations(self): class ImageHashTests(unittest.TestCase): - def test_the_recipe_record_receipt_example_and_checklist_name_one_image_sha256(self): + def test_the_recipe_record_receipt_example_and_checklist_pin_both_image_sha256_pairs(self): receipt = json.loads(read(RECEIPT_EXAMPLE)) self.assertEqual(sha256_errors(read(RECIPE), read(RECORD), receipt, read(CHECKLIST)), []) def test_the_check_rejects_a_drifted_hash(self): recipe, record, checklist = read(RECIPE), read(RECORD), read(CHECKLIST) receipt = json.loads(read(RECEIPT_EXAMPLE)) - (expected,) = set(SHA256_RE.findall(recipe)) other = "0" * 64 - self.assertTrue(sha256_errors(recipe, record.replace(expected, other), receipt, checklist)) - self.assertTrue(sha256_errors(recipe.replace(expected, other, 1), record, receipt, checklist)) + for release, pin in RELEASE_PINS.items(): + expected = pin["sha256"] + with self.subTest(release=release): + self.assertTrue(sha256_errors(recipe, record.replace(expected, other), receipt, checklist)) + self.assertTrue(sha256_errors(recipe.replace(expected, other, 1), record, receipt, checklist)) + self.assertTrue(sha256_errors(recipe, record, receipt, checklist.replace(expected, other))) + pins = {key: dict(value) for key, value in receipt["supported_images"].items()} + pins[release]["sha256"] = other + self.assertTrue(sha256_errors(recipe, record, dict(receipt, supported_images=pins), checklist)) self.assertTrue(sha256_errors(recipe, record, dict(receipt, image=dict(receipt["image"], sha256_published=other)), checklist)) - self.assertTrue(sha256_errors(recipe, record, receipt, checklist.replace(expected, other))) + trial, fallback = (RELEASE_PINS[release]["sha256"] for release in ("26.04.1", "24.04.5")) + swapped = recipe.replace(trial, "SWAP").replace(fallback, trial).replace("SWAP", fallback) + self.assertTrue(sha256_errors(swapped, record, receipt, checklist)) + pins = {key: dict(value) for key, value in receipt["supported_images"].items()} + pins["26.04.1"]["bytes"] = IMAGE_BYTES + self.assertTrue(sha256_errors(recipe, record, dict(receipt, supported_images=pins), checklist)) + self.assertTrue(sha256_errors(recipe + "\n" + other, record, receipt, checklist)) + self.assertTrue(sha256_errors(recipe.replace("$Actual -ne $Expected", "$Actual -eq $Expected"), record, + receipt, checklist)) class ReceiptExampleTests(unittest.TestCase): @@ -1104,8 +1324,8 @@ def test_the_path_proof_tests_each_printed_path(self): def test_the_path_proof_check_rejects_type_p_alone(self): recipe = read(RECIPE) - self.assertIn("test -x $p && ", recipe) - self.assertTrue(path_proof_errors(recipe.replace("test -x $p && ", ""))) + self.assertIn("[[ -f $p && -x $p ]] && ", recipe) + self.assertTrue(path_proof_errors(recipe.replace("[[ -f $p && -x $p ]] && ", ""))) class JCodeMunchStepTests(unittest.TestCase): @@ -1225,7 +1445,7 @@ def assert_mutants_fail(self, check, mutants): good = self.inputs() for name, mutant in mutants.items(): with self.subTest(mutant=name): - self.assertNotEqual(mutant, good) + self.assertTrue(mutant != good, f"control made no change: {name}") self.assertTrue(check(*mutant)) @@ -1250,6 +1470,10 @@ def test_the_check_rejects_a_key_import_a_shared_home_another_keyring_a_late_run later = "### W5. " import_key = "gpg --keyserver hkp://keyserver.ubuntu.com --recv-keys 0xD94AA3F0EFE21092\n" self.assert_mutants_fail(signed_sums_errors, { + "missing trial arm": (recipe.replace("for RELEASE in 26.04.1 24.04.5; do", "for RELEASE in 24.04.5; do"), + record, checklist, receipt), + "signature failure continues": (recipe.replace('if [ "$?" -ne 0 ]; then exit 1; fi\n', ""), record, + checklist, receipt), "keyserver import": (recipe.replace(p1, p1.replace(gpgv, import_key + gpgv)), record, checklist, receipt), "the operator's GnuPG home": (recipe.replace(gpgv, gpgv.replace('--homedir "$SUMS_DIR" ', "")), record, checklist, receipt), @@ -1352,7 +1576,8 @@ def test_the_check_rejects_the_old_rule_no_recency_rule_no_second_reading_and_lo "no exposure rule": (recipe.replace(w5, w5.replace("exposure to microsoft/WSL#41482", "a warning")), record, checklist, receipt), "after the install": (recipe.replace(p3, "").replace(later, p3 + later, 1), record, checklist, receipt), - "record without the run": (recipe, record.replace(STORVSC_NEWEST, "the newest line"), checklist, receipt), + "record without the run": (recipe, record.replace("sudo journalctl -k -b 0 --no-pager", "the journal read"), + checklist, receipt), "record without the threshold's source": (recipe, record.replace(THRESHOLD_SOURCE, "an upstream figure"), checklist, receipt), "checklist with the old rule": (recipe, record, checklist.replace(checklist_line(checklist, "P3"), @@ -1472,7 +1697,7 @@ def test_the_check_rejects_a_late_rehearsal_an_unexported_failure_a_wrong_order_ record, checklist, receipt, experiment), "no host-wide rule": (recipe.replace(r1, r1.replace("only its own distribution", "its distribution")), record, checklist, receipt, experiment), - "no storage readings": (recipe.replace(r1, r1.replace("P3's baseline and W5's second count", "the counts")), + "no storage readings": (recipe.replace(r1, reword(r1, "P3's baseline and W5's second count", "the counts")), record, checklist, receipt, experiment), "checklist without R1": (recipe, record, checklist.replace(checklist_line(checklist, "R1") + "\n", ""), receipt, experiment), @@ -1484,9 +1709,1111 @@ def test_the_check_rejects_a_late_rehearsal_an_unexported_failure_a_wrong_order_ } for name, mutant in mutants.items(): with self.subTest(mutant=name): - self.assertNotEqual(mutant, (recipe, record, checklist, receipt, experiment)) + self.assertTrue(mutant != (recipe, record, checklist, receipt, experiment), f"control made no change: {name}") self.assertTrue(rehearsal_errors(*mutant)) +class DualImageParameterizationTests(unittest.TestCase): + """Public recipe artifacts, checked against the two signed primary-source pins; no host execution.""" + + def test_both_releases_are_pinned_and_install_the_selected_file(self): + recipe = read(RECIPE) + self.assertEqual(set(SHA256_RE.findall(recipe)), {pin["sha256"] for pin in RELEASE_PINS.values()}) + for release, pin in RELEASE_PINS.items(): + with self.subTest(release=release): + self.assertIn(f"'{release}' {{ $Expected = '{pin['sha256']}'; $CatalogName = '{pin['catalog_name']}' }}", + section(recipe, "W2")) + self.assertIn("default { throw 'unsupported release: do not download or install' }", section(recipe, "W2")) + for step in ("W4", "W6"): + self.assertIn("Z:\\WSL\\downloads\\ubuntu--wsl-amd64.wsl", section(recipe, step)) + receipt = json.loads(read(RECEIPT_EXAMPLE)) + self.assertEqual(receipt.get("supported_images"), RELEASE_PINS) + + def test_preregistered_rehearsals_cover_both_images_without_claiming_acceptance(self): + recipe, receipt = read(RECIPE), json.loads(read(RECEIPT_EXAMPLE)) + criteria = {"first_boot", "systemd_user_from_second_instance", "wsl_gpu", "uv_cpython_3_13", "node_24"} + arms = receipt.get("comparison_arms", {}) + self.assertEqual(set(arms), set(RELEASE_PINS)) + for release, arm in arms.items(): + with self.subTest(release=release): + self.assertEqual(arm["status"], "unrun") + self.assertEqual(set(arm) - {"status", "name"}, criteria) + self.assertTrue(all(is_placeholder(arm[key], "' -u root --exec cloud-init --version", + [line for _, line in step_commands(recipe, "W5")]) + experiment = json.loads(read(EXPERIMENT)) + self.assertEqual(experiment["status"], "planned") + self.assertEqual(experiment["observations"], []) + self.assertEqual(experiment["decision_and_scope"]["qualification_run_ids"], []) + self.assertEqual(quality_rule_errors(experiment), []) + + +def version_gate_errors(recipe: str, checklist: str) -> list[str]: + errors = [] + w1 = prose(section(recipe, "W1")) + if ("powershell", "wsl.exe --version") not in step_commands(recipe, "W1"): + errors.append("W1 does not record the installed WSL version") + for needed in ("3.0.1 or later", "second systemd distribution", "before W4 or W6", "stop", "40519", "41512"): + if needed not in w1: + errors.append(f"W1 lacks the cgroup version precondition: {needed}") + if "3.0.1 or later" not in checklist_line(checklist, "W1"): + errors.append("the checklist still accepts an older second systemd distribution") + rules = prose(chapter(recipe, "Host-wide rules")) + for needed in ("single systemd distribution", "2.4.10 or later", "2.7.13", "shared cgroup"): + if needed not in rules: + errors.append(f"the host rules lack the older single-distribution boundary: {needed}") + if "No WSL update is needed to install" in rules or "2.4.10 or later" in w1: + errors.append("the second-distribution preflight still accepts the old install-only minimum") + return errors + + +def namespace_observation_errors(recipe: str, checklist: str) -> list[str]: + errors = [] + if not any(block == WORKSTATION_COMMANDS for block in step_blocks(recipe, "W1")): + errors.append("W1 lacks the native observation on the running distribution") + for needed in ("cgroup:[4026531835]", "PROC_CGROUP_INIT_INO", "initial cgroup namespace", + "A noninitial namespace is necessary but does not prove isolation"): + if needed not in prose(section(recipe, "W1")): + errors.append(f"W1 lacks the namespace observation's interpretation: {needed}") + if NAMESPACE_READ not in checklist: + errors.append("the checklist omits the namespace observation") + return errors + + +def cgroup_launch_errors(recipe: str, checklist: str) -> list[str]: + errors = [] + w5 = section(recipe, "W5") + order = ("systemctl is-active ''", + "/mnt/c/Windows/System32/wsl.exe -d '' --exec systemctl is-active ''", CGROUP_PROCS, USER_MANAGER_ACTIVE) + blocks = step_blocks(recipe, "W5") + if not any(all(command in block for command in order) and + [block.index(command) for command in order] == sorted(block.index(command) for command in order) + for block in blocks): + errors.append("W5 lacks the native cross-distribution cgroup and user-manager checks in order") + for needed in ("after the first launch", "before F1", "while both distributions run", "no `0` entry", + "user@.service", "empty or unreadable", "never stop or restart a unit in either distribution"): + if needed not in prose(w5): + errors.append(f"W5 lacks its conclusive cgroup proof or recovery rule: {needed}") + failed_tar = r"Z:\WSL\downloads\-w5-failed.tar" + export = f"wsl.exe --export '' '{failed_tar}'" + failures = [block for block in blocks if export in block] + if len(failures) != 1: + errors.append("W5 lacks the recovery for any failed proof") + else: + block = failures[0] + recovery = (TERMINATE, export, EXPORT_GUARD, UNREGISTER, INTEROP_VERIFY, INTEROP_GUARD) + if not all(command in block for command in recovery) or ( + [block.index(command) for command in recovery] != sorted(block.index(command) for command in recovery)): + errors.append("cgroup failure must terminate only the new distro, guard the export, unregister and check interop") + if any(re.search(r"systemctl\s+(?:stop|restart)\b", command) for command in block): + errors.append("cgroup failure recovery tries to stop or restart a system unit") + if "cgroup.procs" not in checklist_line(checklist, "W5"): + errors.append("the checklist lacks the post-launch cgroup proof") + return errors + + +def unregister_interop_errors(recipe: str, checklist: str) -> list[str]: + errors = [] + sites = Counter() + for step in REQUIRED_STAGE_IDS: + for block in step_blocks(recipe, step): + for index, command in enumerate(block): + if command != UNREGISTER: + continue + sites[step] += 1 + if block[index + 1:index + 3] != [INTEROP_VERIFY, INTEROP_GUARD]: + errors.append(f"{step} unregister {sites[step]} lacks the immediate surviving-distribution interop check") + if sites != Counter({"R1": 2, "W5": 1, "W6": 1}): + errors.append(f"unregister sites {dict(sites)} differ from the four reviewed removals") + r1 = prose(section(recipe, "R1")) + # The follow-up of 2026-10-02: on the adopted release the restart exits 1, so the recovery is a stop for review with two + # records (binfmt_recovery_errors); the stop when Windows execution does not return stays a requirement here. + if "stop for review" not in r1: + errors.append("interop recovery lacks a stop for review when Windows execution does not return") + for step in ("R1", "W6"): + if "WSLInterop" not in checklist_line(checklist, step) or "Windows executable" not in checklist_line(checklist, step): + errors.append(f"the checklist's {step} removal lacks both interop observations") + if "whether binfmt registrations survive" in recipe: + errors.append("the recipe still presents the observed binfmt loss as an open question") + return errors + + +def storage_window_errors(recipe: str, checklist: str) -> list[str]: + errors = [] + for name, text in (("recipe", section(recipe, "W5")), ("checklist", checklist_line(checklist, "W5"))): + if STORAGE_RULE not in prose(text): + errors.append(f"the {name} lacks the single storage-count and attachment-window rule") + if "count equals P3's baseline" in prose(text) or "count equals the baseline" in prose(text): + errors.append(f"the {name} still stops on an attachment-only increase") + if USER_SESSION_WARNING not in text: + errors.append(f"the {name} does not stop on the systemd user-session launch warning") + p3 = STEP_RE.findall(recipe).index("P3") if "P3" in STEP_RE.findall(recipe) else -1 + w4 = STEP_RE.findall(recipe).index("W4") if "W4" in STEP_RE.findall(recipe) else -1 + if not 0 <= p3 < w4 or ("sh", STORVSC_COUNT) not in step_commands(recipe, "P3"): + errors.append("the baseline is not recorded before the import") + if ("sh", STORVSC_COUNT) not in step_commands(recipe, "W5"): + errors.append("there is no second count after the first launch") + return errors + + +def adopted_target_errors(recipe: str) -> list[str]: + """Change 1 of the follow-up of 2026-10-02. "Host-wide rules" states, as policy, that the recipe adopts stable WSL + 3.0.1 or later for a second systemd distribution. The 2.9.8 and 2.9.13 pre-release history stays beside it, and one + sentence says the host was updated to 3.0.1.0 on 2026-10-02 and passed its check, with the two-distribution proof still + owed. The two sentences the change replaced must not come back.""" + rules = prose(chapter(recipe, "Host-wide rules")) + errors = [f"Host-wide rules lack: {needed}" for needed in + (ADOPTED_TARGET, HOST_UPDATE, "Neither pre-release is adopted", "2.9.8 pre-release notes", + "2.9.13 pre-release notes") if needed not in rules] + errors += [f"Host-wide rules still say: {old}" for old in (OLD_ADOPTED_TARGET, OLD_HOST_UPDATE) if old in rules] + return errors + + +def paired_isolation_errors(recipe: str, record: str, checklist: str, receipt: dict) -> list[str]: + """Change 2 of the follow-up of 2026-10-02. A reading of one distribution cannot show the same uid, distinct namespaces + and two healthy managers, so W5 runs, natively in the workstation's shell after its cgroup block and before F1, one + block of ten command lines that read the same five values from both running distributions, and records them under the + receipt's ``paired_isolation`` (one object per distribution). W1 holds the workstation's baseline of the five, without + sudo, and W6 repeats the record as it repeats the cgroup proof.""" + errors = [] + w1 = step_commands(recipe, "W1") + if not all(("sh", command) in w1 for command in WORKSTATION_COMMANDS): + errors.append("W1 lacks the workstation's baseline of the five observations") + if ("sh", OLD_NAMESPACE_READ) in w1: + errors.append("W1 still records the lone `/proc/1` namespace read, which needs privilege") + blocks = step_blocks(recipe, "W5") + paired = [block for block in blocks if "systemctl is-system-running" in block or NEW_COMMANDS[0] in block] + if paired != [WORKSTATION_COMMANDS + NEW_COMMANDS]: + errors.append("W5 does not hold one block of ten separate paired commands, the workstation's five first") + else: + cgroup = next((index for index, block in enumerate(blocks) if CGROUP_PROCS in block), len(blocks)) + if blocks.index(paired[0]) <= cgroup: + errors.append("W5's paired block does not come after its cgroup block") + w5 = prose(section(recipe, "W5")) + errors += [f"W5's proof does not say: {needed}" for needed in ( + "both distributions print the same uid", "both user managers print `active`", + "the new distribution's system state is `running` with no failed unit, or `degraded` with " + "`systemd-binfmt.service` as its only failed unit", + "differ from each other and neither is `cgroup:[4026531835]`", + "equal its own values recorded in W1 before the import", + "the WSL version and kernel from W1 and the image revision from W2", "`paired_isolation`", + *(f"`{key}`" for key in sorted(ISOLATION_KEYS))) if needed not in w5] + if "paired record" not in prose(section(recipe, "W6")): + errors.append("W6 does not repeat the paired record with the cgroup proof") + rows = command_table(record) + errors += [f"the record's command table lacks {step} {command}" for step, command in + [*(('W1', command) for command in WORKSTATION_COMMANDS), + *(('W5', command) for command in WORKSTATION_COMMANDS + NEW_COMMANDS)] + if (step, "sh", command) not in rows] + w1_line, w5_line = checklist_line(checklist, "W1"), checklist_line(checklist, "W5") + if NAMESPACE_READ not in checklist or "baseline" not in w1_line: + errors.append("the checklist's W1 line does not require the workstation's baseline") + if "paired_isolation" not in w5_line or "W1 baseline" not in w5_line: + errors.append("the checklist's W5 line does not require the paired record against the W1 baseline") + if "paired record" not in checklist_line(checklist, "W6"): + errors.append("the checklist's W6 line does not repeat the paired record") + record_block = field(receipt, "paired_isolation") + objects = [value for value in record_block.values() if isinstance(value, dict)] if isinstance(record_block, dict) else [] + if len(objects) != 2 or not all(set(value) == ISOLATION_KEYS and all(is_placeholder(item, " list[str]: + """Change 3 of the follow-up of 2026-10-02, the unit. On the adopted release WSL mounts the binfmt status file + read-only (microsoft/WSL#40621) while ``protectBinfmt`` is on, its default, so ``systemd-binfmt.service`` then fails + with ``Failed to flush binfmt_misc rules, ignoring: Read-only file system``, which upstream calls benign (#41226). F1 passes on ``running``, or on + ``degraded`` only when ``systemctl --failed --no-legend --plain`` lists exactly that unit and its log holds the + message; any other failed unit stops the run. A text that accepts every ``degraded`` state, or stops at every one, fails.""" + errors = [] + f1 = [command for _, command in step_commands(recipe, "F1")] + wanted = ["systemctl is-system-running --wait", F1_FAILED, F1_LOG] + if any(command not in f1 for command in wanted) or [f1.index(command) for command in wanted] != sorted( + f1.index(command) for command in wanted): + errors.append(f"F1 does not run {wanted} in that order") + if "systemctl --failed --no-legend" in f1: + errors.append("F1 still lists the failed units without --plain") + text = prose(section(recipe, "F1")) + errors += [f"F1's text does not say: {needed}" for needed in + (F1_PASS, FLUSH_MESSAGE, "Any other failed unit stops the run", BINFMT_PR, BINFMT_ISSUE, BINFMT_BENIGN, + "`adm`", F1_SUDO_FALLBACK) if needed not in text] + if "On `degraded`, record" in text: + errors.append("F1 still stops at every `degraded`") + decision = prose(chapter(record, "Decision")) + errors += [f"the record's decision does not say: {needed}" for needed in + ("`degraded` when `systemctl --failed --no-legend --plain` lists exactly `systemd-binfmt.service`", + "microsoft/WSL#40621", "#41226") if needed not in decision] + if "F1 reports `degraded` on a clean run" in prose(chapter(record, "Overturn condition")): + errors.append("the record's overturn condition still revisits every `degraded` F1") + rows = command_table(record) + errors += [f"the record's command table lacks F1 {command}" for command in (F1_FAILED, F1_LOG) + if ("F1", "sh", command) not in rows] + errors += [f"the record's Sources lack {url}" for url in (BINFMT_PR, BINFMT_ISSUE) if url not in record] + line = checklist_line(checklist, "F1") + if not all(part in line for part in ("`running`", "`degraded`", "exactly `systemd-binfmt.service`", + "any other failed unit stops", FLUSH_MESSAGE, "with no failed unit")): + errors.append("the checklist's F1 line does not accept `degraded` for the one unit only") + first = next((entry for entry in receipt.get("steps", []) if entry.get("step") == "F1" + and entry.get("cmd") == "systemctl is-system-running --wait"), {}) + if not all(isinstance(first.get(key), str) and "degraded" in first[key] for key in ("exit", "output_excerpt")): + errors.append("the receipt example's F1 entry for the wait command still holds a fixed `running`") + arms = receipt.get("comparison_arms", {}) + if not arms or not all("F1's pass condition" in str(arm.get("first_boot")) for arm in arms.values()): + errors.append("the receipt example's first_boot criteria do not refer to F1's complete pass condition") + return errors + + +def binfmt_recovery_errors(recipe: str, record: str, checklist: str, receipt: dict) -> list[str]: + """Change 3 of the follow-up of 2026-10-02, the recovery. ``sudo systemctl restart systemd-binfmt`` was the WSL 2.7.x + remedy for the lost WSLInterop registration. On the adopted release the registration is protected and that restart + exits 1, so no recipe command may control the unit (a history sentence may still name the restart). Every unregister + keeps its check. After R1's removal blocks, a native ``sh`` block repeats the check and records ``ls + /proc/sys/fs/binfmt_misc`` and ``systemctl status systemd-binfmt.service --no-pager``, and the page stops for review, + importing and provisioning nothing.""" + errors = [f"the recipe runs `{command}`, a control of systemd-binfmt" for _, command in fenced_commands(recipe) + if UNIT_CONTROL_RE.search(command)] + errors += [f"the record's command table has `{command}`, a control of systemd-binfmt" + for _, _, command in command_table(record) if UNIT_CONTROL_RE.search(command)] + r1 = step_commands(recipe, "R1") + blocks = [block for block in step_blocks(recipe, "R1") if INTEROP_RECORDS[0] in block] + if len(blocks) != 1 or blocks[0] != [NATIVE_INTEROP, *INTEROP_RECORDS]: + errors.append("R1 lacks one sh block of the native check, `ls /proc/sys/fs/binfmt_misc` and " + "`systemctl status systemd-binfmt.service --no-pager`, in that order") + else: + guards = [index for index, (_, command) in enumerate(r1) if command == INTEROP_GUARD] + first = next(index for index, (shell, command) in enumerate(r1) if shell == "sh" and command == NATIVE_INTEROP) + if not guards or first < guards[-1]: + errors.append("R1's records do not come after its interop checks") + r1_text = prose(section(recipe, "R1")) + errors += [f"R1's text does not say: {needed}" for needed in + ("On WSL 2.7.13", "no remedy on the adopted release", BINFMT_PR, "exits 1", "stop for review", + "do not import or provision another distribution", "`interop_after_unregister`") + if needed not in r1_text] + errors += [f"R1 still says: {old}" for old in + ("If either check still fails, stop;", "Proof: the restart exits 0", "Recover from a native shell") + if old in r1_text] + w6 = prose(section(recipe, "W6")) + if "do not import" not in w6 or "repeat both observations before resuming" in w6: + errors.append("W6 does not stop for review without an import when the guard throws") + for step in ("R1", "W6"): + line = checklist_line(checklist, step) + if "stops for review" not in line or "systemctl restart" in line: + errors.append(f"the checklist's {step} line does not stop for review, or still runs the restart") + decision = prose(chapter(record, "Decision")) + if "stops for review" not in decision or "supported `sudo systemctl restart systemd-binfmt` recovery" in decision: + errors.append("the record's decision does not stop for review, or still names the supported restart recovery") + interop = receipt.get("interop_after_unregister") if isinstance(receipt, dict) else None + if not isinstance(interop, str) or "stop for review" not in interop or "systemd-binfmt recovery" in interop: + errors.append("the receipt example's interop_after_unregister does not record the stop for review") + return errors + + +class RehearsalRepairTests(unittest.TestCase): + """Source contracts from the real rehearsal; controls restore the unsafe old text in memory only.""" + + def test_required_stages_are_independent_even_when_all_artifacts_lose_one(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + receipt = json.loads(read(RECEIPT_EXAMPLE)) + components = {c["id"] for c in json.loads(read(STACK))["components"]} + self.assertEqual(tuple(STEP_RE.findall(recipe)), REQUIRED_STAGE_IDS) + self.assertTrue(all(("W1", "sh", command) in recipe_rows(recipe) for command in WORKSTATION_COMMANDS)) + # The old checks derived their entire contract from the recipe and accepted this coordinated deletion. + old_page = recipe.replace(section(recipe, "F4"), "") + old_ticks = checklist.replace(checklist_line(checklist, "F4") + "\n", "") + old_receipt = dict(receipt, steps=[e for e in receipt["steps"] if e["step"] != "F4"]) + self.assertTrue(checklist_errors(old_ticks, old_page)) + self.assertTrue(receipt_errors(old_receipt, old_page, components)) + + def test_every_selected_image_digest_rejects_the_old_unchecked_fields(self): + recipe, record, checklist = read(RECIPE), read(RECORD), read(CHECKLIST) + receipt = json.loads(read(RECEIPT_EXAMPLE)) + self.assertEqual(sha256_errors(recipe, record, receipt, checklist), []) + for key in SELECTED_IMAGE_DIGEST_FIELDS: + with self.subTest(field=key, control="old unchecked selected digest"): + old = dict(receipt, image=dict(receipt["image"], **{key: "0" * 64})) + self.assertTrue(sha256_errors(recipe, record, old, checklist)) + for release, pin in RELEASE_PINS.items(): + image = dict(pin, release=release, **{key: pin["sha256"] for key in SELECTED_IMAGE_DIGEST_FIELDS}) + selected = dict(receipt, image=image) + self.assertEqual(selected_image_digest_errors(recipe, selected), []) + other = next(p["sha256"] for r, p in RELEASE_PINS.items() if r != release) + for key in (*SELECTED_IMAGE_DIGEST_FIELDS, "sha256_additional"): + with self.subTest(release=release, field=key, control="swapped selected digest"): + self.assertTrue(selected_image_digest_errors(recipe, dict(selected, image=dict(image, **{key: other})))) + for key in SELECTED_IMAGE_DIGEST_FIELDS: + self.assertTrue(selected_image_digest_errors(recipe, dict(selected, image={k:v for k,v in image.items() + if k != key}))) + + def test_version_gate_rejects_the_old_install_only_minimum(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(version_gate_errors(recipe, checklist), []) + old_w1 = section(recipe, "W1").replace("3.0.1 or later", "2.4.10 or later") + self.assertNotEqual(old_w1, section(recipe, "W1")) + self.assertTrue(version_gate_errors(recipe.replace(section(recipe, "W1"), old_w1), checklist)) + self.assertTrue(version_gate_errors(recipe, checklist.replace("3.0.1 or later", "2.4.10 or later"))) + + def test_namespace_is_observed_and_never_used_as_the_version_gate(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(namespace_observation_errors(recipe, checklist), []) + old = recipe.replace(WORKSTATION_BASELINE + "\n", "") + self.assertNotEqual(old, recipe) + self.assertTrue(namespace_observation_errors(old, checklist)) + self.assertTrue(namespace_observation_errors(recipe, checklist.replace(NAMESPACE_READ, "the namespace"))) + self.assertTrue(namespace_observation_errors(recipe.replace("does not prove isolation", "proves isolation"), + checklist)) + + def test_cgroups_are_checked_from_the_running_distribution_before_f1(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(cgroup_launch_errors(recipe, checklist), []) + for command in (CGROUP_PROCS, USER_MANAGER_ACTIVE): + with self.subTest(control="old launch without isolation proof", command=command): + old = recipe.replace(command + "\n", "") + self.assertNotEqual(old, recipe) + self.assertTrue(cgroup_launch_errors(old, checklist)) + + def test_cgroup_failure_uses_only_the_new_distribution_recovery(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(cgroup_launch_errors(recipe, checklist), []) + w5 = section(recipe, "W5") + old = recipe.replace(w5, w5.replace("wsl.exe --export '' 'Z:\\WSL\\downloads\\-w5-failed.tar'\n", "")) + self.assertNotEqual(old, recipe) + self.assertTrue(cgroup_launch_errors(old, checklist)) + unsafe = recipe.replace(w5, w5.replace(TERMINATE + "\n", "sudo systemctl restart user@1000.service\n", 1)) + self.assertTrue(cgroup_launch_errors(unsafe, checklist)) + + def test_every_unregister_checks_surviving_interop_and_stops_on_failed_recovery(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(unregister_interop_errors(recipe, checklist), []) + self.assertEqual(sum(command == UNREGISTER for _, _, command in recipe_rows(recipe)), 4) + removal = UNREGISTER + "\n" + INTEROP_VERIFY + "\n" + INTEROP_GUARD + "\n" + self.assertEqual(recipe.count(removal), 4) + for site in range(4): + with self.subTest(control="old unregister without interop check", site=site + 1): + parts = recipe.split(removal) + parts[site] += UNREGISTER + "\n" + old = removal.join(parts[:site + 1]) + removal.join(parts[site + 1:]) + self.assertEqual(old.count(removal), 3) + self.assertTrue(unregister_interop_errors(old, checklist)) + # The interop recovery is a stop for review since 2026-10-02 (BinfmtRecoveryTests); dropping that stop is still rejected. + r1 = section(recipe, "R1") + old = recipe.replace(r1, reword(r1, "then stop for review", "then continue")) + self.assertNotEqual(old, recipe) + self.assertTrue(unregister_interop_errors(old, checklist)) + + def test_both_storage_rules_reject_the_old_equal_count_requirement(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(storage_window_errors(recipe, checklist), []) + pattern = re.escape(STORAGE_RULE).replace(r"\ ", r"\s+") + for name in ("recipe", "checklist"): + with self.subTest(control="old equal-count storage rule", file=name): + page = re.sub(pattern, OLD_STORAGE_RULE, recipe) if name == "recipe" else recipe + ticks = re.sub(pattern, OLD_STORAGE_RULE, checklist) if name == "checklist" else checklist + self.assertNotEqual((page, ticks), (recipe, checklist)) + self.assertTrue(storage_window_errors(page, ticks)) + + def test_launch_warning_stops_even_when_the_launch_exits_zero(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(storage_window_errors(recipe, checklist), []) + for name in ("recipe", "checklist"): + with self.subTest(control="old exit-zero launch proof", file=name): + page = recipe.replace(USER_SESSION_WARNING, "launch exit 0") if name == "recipe" else recipe + ticks = checklist.replace(USER_SESSION_WARNING, "launch exit 0") if name == "checklist" else checklist + self.assertTrue(storage_window_errors(page, ticks)) + + def test_default_and_rollback_reject_the_old_symmetric_arms(self): + inputs = (read(RECIPE), read(RECORD), read(CHECKLIST)) + self.assertEqual(default_image_errors(*inputs), []) + for index, text in enumerate(inputs): + with self.subTest(control="old symmetric image arms", file=index): + old = text + "\nBoth are symmetric provisional arms with no merit precedence.\n" + mutant = (*inputs[:index], old, *inputs[index + 1:]) + self.assertTrue(default_image_errors(*mutant)) + + def test_signature_class_rejects_unretained_output_claims(self): + record, receipt = read(RECORD), json.loads(read(SOURCE_RECEIPT)) + self.assertEqual(signature_class_errors(record, receipt), []) + old = record + "\nThe retained logs are each release's native-signed-sums.stdout and native-signed-sums.stderr.\n" + self.assertTrue(signature_class_errors(old, receipt)) + old_receipt = dict(receipt, source_verification=dict(receipt['source_verification'], + evidence_class='native signature output for both releases')) + self.assertTrue(signature_class_errors(record, old_receipt)) + + def test_rootless_start_rejects_the_old_bus_only_f3(self): + recipe, checklist = read(RECIPE), read(CHECKLIST) + self.assertEqual(rootless_start_errors(recipe, checklist), []) + f3 = section(recipe, "F3") + old_f3 = f3.split("Right after the bus proof,", 1)[0] + self.assertNotEqual(old_f3, f3) + self.assertTrue(rootless_start_errors(recipe.replace(f3, old_f3), checklist)) + old_ticks = checklist.replace(checklist_line(checklist, "F3"), + "- [ ] **F3** The bus is a socket and the user manager is running.") + self.assertTrue(rootless_start_errors(recipe, old_ticks)) + + +def default_image_errors(recipe: str, record: str, checklist: str) -> list[str]: + errors = [] + for name, text in (("recipe", recipe), ("record", record), ("checklist", checklist)): + for needed in ("single default", "26.04.1", "24.04.5", "rollback", + "release-caused 26.04 failure with no in-release remedy"): + if needed not in prose(text): + errors.append(f"the {name} lacks the default/rollback contract: {needed}") + if "no merit precedence" in text: + errors.append(f"the {name} still presents symmetric clean-install arms") + return errors + + +def signature_class_errors(record: str, receipt: dict) -> list[str]: + errors = [] + if "no retained verification output" not in prose(record): + errors.append("the record does not qualify the 26.04.1 signature evidence") + if any(name in record for name in ("native-signed-sums.stdout", "native-signed-sums.stderr")): + errors.append("the record claims unretained signature output streams") + evidence = field(receipt, "source_verification", "evidence_class") + if not isinstance(evidence, str) or not all(part in evidence for part in + ("reported", "exit codes", "fingerprint", "stream hash match", "no retained verification output")): + errors.append("the receipt's signature class exceeds its retained reported facts") + return errors + + +def rootless_start_errors(recipe: str, checklist: str) -> list[str]: + errors = [] + info = "docker --context rootless info --format '{{json .SecurityOptions}}'" + run = "docker --context rootless run --rm hello-world" + blocks = step_blocks(recipe, "F3") + if not any(info in block and run in block and block.index(info) < block.index(run) for block in blocks): + errors.append("F3 does not prove rootless mode before starting a container as the user") + for needed in ("owed on the first run after the WSL update", "41492", "name=rootless", + "failed container start", "stops the run", "before F9"): + if needed not in prose(section(recipe, "F3")): + errors.append(f"F3 lacks its rootless-container proof, stop or owed result: {needed}") + if run not in checklist_line(checklist, "F3") or "owed" not in checklist_line(checklist, "F3"): + errors.append("the checklist still accepts the bus proof without the rootless-container result") + return errors + + +class AdoptedTargetTests(unittest.TestCase): + """Change 1 of the follow-up of 2026-10-02: Host-wide rules adopt stable WSL 3.0.1 or later as policy.""" + + def test_host_wide_rules_adopt_wsl_3_0_1_and_record_the_host_update(self): + self.assertEqual(adopted_target_errors(read(RECIPE)), []) + + def test_the_check_rejects_the_pre_change_rule_a_stale_or_lost_update_sentence_and_a_misplaced_policy(self): + recipe = read(RECIPE) + rules = chapter(recipe, "Host-wide rules") + mutants = { + "pre-change rule": recipe.replace(rules, reword(rules, ADOPTED_TARGET, OLD_ADOPTED_TARGET)), + "pre-change update sentence": recipe.replace(rules, reword(rules, HOST_UPDATE, OLD_HOST_UPDATE)), + "update sentence lost": recipe.replace(rules, reword(rules, HOST_UPDATE, "")), + "pre-releases adopted": recipe.replace(rules, reword(rules, "Neither pre-release is adopted:", + "Both pre-releases are adopted:")), + "policy outside Host-wide rules": recipe.replace(rules, reword(rules, ADOPTED_TARGET, "")).replace( + "Start with R1", ADOPTED_TARGET + " Start with R1", 1), + } + for name, mutant in mutants.items(): + with self.subTest(mutant=name): + self.assertNotEqual(mutant, recipe) + self.assertTrue(adopted_target_errors(mutant)) + + +class PairedIsolationTests(FollowUpCase): + """Change 2 of the follow-up of 2026-10-02: W5 reads the same five observations from both running distributions.""" + + def test_w5_pairs_the_five_observations_and_w1_holds_the_workstation_baseline(self): + self.assertEqual(paired_isolation_errors(*self.inputs()), []) + + def test_the_check_rejects_the_pre_change_pages_a_half_pair_and_lost_records(self): + recipe, record, checklist, receipt = self.inputs() + w1, w5, w6 = section(recipe, "W1"), section(recipe, "W5"), section(recipe, "W6") + block = f"```sh\n{WORKSTATION_BASELINE}\n{PAIRED_NEW}\n```\n" + cgroup_block = "```sh\nsystemctl is-active ''\n" + self.assertIn(WORKSTATION_BASELINE + "\n", w1) + self.assertIn(block, w5) + self.assertIn(cgroup_block, w5) + row = next(line for line in record.splitlines() if line.startswith(f"| W5 | sh | `{NEW_COMMANDS[0]}` |")) + pair = receipt["paired_isolation"] + names = [name for name, value in pair.items() if isinstance(value, dict)] + host = {key: value for key, value in receipt["host"].items() if key != "workstation_baseline"} + page = lambda old_w5: recipe.replace(w5, old_w5) # noqa: E731 + self.assert_mutants_fail(paired_isolation_errors, { + "pre-change W1": (recipe.replace(w1, w1.replace(WORKSTATION_BASELINE, OLD_NAMESPACE_READ)), record, checklist, + receipt), + "pre-change W5": (page(w5.replace(block, "")), record, checklist, receipt), + "the workstation's line only": (page(w5.replace(block, f"```sh\n{WORKSTATION_BASELINE}\n```\n")), record, + checklist, receipt), + "the new distribution's line only": (page(w5.replace(block, f"```sh\n{PAIRED_NEW}\n```\n")), record, checklist, + receipt), + "lines in the wrong order": (page(w5.replace(block, f"```sh\n{PAIRED_NEW}\n{WORKSTATION_BASELINE}\n```\n")), + record, checklist, receipt), + "lines in two blocks": (page(w5.replace(block, f"```sh\n{WORKSTATION_BASELINE}\n```\n\n```sh\n{PAIRED_NEW}\n```\n")), + record, checklist, receipt), + "before the cgroup block": (page(w5.replace(block, "").replace(cgroup_block, block + "\n" + cgroup_block, 1)), + record, checklist, receipt), + "no five keys in the proof": (page(reword(w5, "each with `uid`, `system_state`, `failed_units`, " + "`user_manager` and `cgroup_namespace`", "each with its values")), + record, checklist, receipt), + "no comparison with W1": (page(reword(w5, "equal its own values recorded in W1 before the import", + "are healthy")), record, checklist, receipt), + "no namespace comparison": (page(reword(w5, "differ from each other and neither is `cgroup:[4026531835]`", + "are printed")), record, checklist, receipt), + "no version, kernel or image record": (page(reword(w5, "with the WSL version and kernel from W1 and the image " + "revision from W2,", "")), record, checklist, receipt), + "W6 without the paired record": (recipe.replace(w6, reword(w6, "Repeat W5's cgroup-isolation proof and its " + "paired record", "Repeat W5's cgroup-isolation " + "proof")), record, checklist, receipt), + "record without the row": (recipe, record.replace(row + "\n", ""), checklist, receipt), + "checklist without the baseline": (recipe, record, checklist.replace( + checklist_line(checklist, "W1"), "- [ ] **W1** WSL 3.0.1 or later is recorded."), receipt), + "checklist without the pair": (recipe, record, checklist.replace("paired_isolation", "the receipt"), receipt), + "no receipt key": (recipe, record, checklist, without(receipt, "paired_isolation")), + "one distribution in the receipt": (recipe, record, checklist, + dict(receipt, paired_isolation={key: value for key, value in pair.items() + if key != names[1]})), + "a lost key in the receipt": (recipe, record, checklist, dict(receipt, paired_isolation=dict( + pair, **{names[1]: {key: value for key, value in pair[names[1]].items() if key != "uid"}}))), + "pre-change receipt host": (recipe, record, checklist, dict(receipt, host=dict( + host, cgroup_namespace=""))), + }) + + +class BinfmtUnitTests(FollowUpCase): + """Change 3 of the follow-up of 2026-10-02: F1 accepts `degraded` only for systemd-binfmt.service, the by-design failure.""" + + def test_f1_accepts_degraded_only_with_the_binfmt_unit_as_the_single_failed_unit(self): + self.assertEqual(binfmt_unit_errors(*self.inputs()), []) + + def test_the_check_rejects_the_old_f1_any_degraded_state_and_lost_references(self): + recipe, record, checklist, receipt = self.inputs() + f1, overturn = section(recipe, "F1"), chapter(record, "Overturn condition") + for line in (F1_FAILED, F1_LOG): + self.assertIn(line + "\n", f1) + log_row = next(line for line in record.splitlines() if line.startswith("| F1 | sh | `journalctl -b 0 -t ")) + pass_rule = f"Proof: `running` with no failed unit, or {F1_PASS} `{FLUSH_MESSAGE}`;" + first = next(entry for entry in receipt["steps"] if entry["step"] == "F1" + and entry["cmd"] == "systemctl is-system-running --wait") + old_steps = [dict(entry, exit=0, output_excerpt="running") if entry is first else entry + for entry in receipt["steps"]] + old_arms = {name: dict(arm, first_boot=arm["first_boot"].replace("F1's pass condition", "F1 running")) for name, arm in + receipt["comparison_arms"].items()} + page = lambda new_f1: (recipe.replace(f1, new_f1), record, checklist, receipt) # noqa: E731 + self.assert_mutants_fail(binfmt_unit_errors, { + "pre-change F1": page(f1[:f1.index("```sh")] + OLD_F1), + "any degraded state passes": page(reword(f1, pass_rule, "Proof: `running` or `degraded`;")), + "no single-unit condition": page(reword(f1, "lists exactly `systemd-binfmt.service`", "lists a failed unit")), + "no log condition": page(reword(f1, "that unit's log, from the `journalctl` line, holds", "it holds")), + "other failed units pass": page(reword(f1, "Any other failed unit stops the run", + "Another failed unit is recorded")), + "no flush message": page(f1.replace(FLUSH_MESSAGE, "the message")), + "no pull request": page(f1.replace(BINFMT_PR, "https://example.invalid/pull")), + "no issue": page(f1.replace(BINFMT_ISSUE, "https://example.invalid/issue")), + "no upstream quote": page(reword(f1, BINFMT_BENIGN, "It is fine.")), + "no journal command": page(f1.replace(F1_LOG + "\n", "")), + "failed units without --plain": page(f1.replace(F1_FAILED + "\n", "systemctl --failed --no-legend\n")), + "journal read before the failed list": page(f1.replace(F1_FAILED + "\n" + F1_LOG + "\n", + F1_LOG + "\n" + F1_FAILED + "\n")), + "no adm statement": page(f1.replace("`adm`", "`wheel`")), + "no sudo fallback for an unreadable journal": page(reword(f1, F1_SUDO_FALLBACK, "")), + "a stop at the journal notice, even on running": page(reword( + f1, F1_SUDO_FALLBACK, "A `Hint: You are currently not seeing messages from other users and the system.` " + "notice or `-- No entries --` means it cannot: record it and stop for review.")), + "old overturn condition": (recipe, record.replace(overturn, reword( + overturn, "F1 reports `degraded` with a failed unit other than `systemd-binfmt.service`, or without that " + "unit's read-only flush message, on a clean run;", "F1 reports `degraded` on a clean run;")), + checklist, receipt), + "record without the journal row": (recipe, record.replace(log_row + "\n", ""), checklist, receipt), + "record without the pull request": (recipe, record.replace(BINFMT_PR, "https://example.invalid/pull"), checklist, + receipt), + "checklist with the old F1": (recipe, record, checklist.replace( + checklist_line(checklist, "F1"), "- [ ] **F1** `systemctl is-system-running --wait` prints `running`; " + "no failed unit."), receipt), + "receipt with a fixed running": (recipe, record, checklist, dict(receipt, steps=old_steps)), + "arms that still read running": (recipe, record, checklist, dict(receipt, comparison_arms=old_arms)), + }) + + +class BinfmtRecoveryTests(FollowUpCase): + """Change 3 of the follow-up of 2026-10-02: the interop recovery stops for review; nothing restarts systemd-binfmt.""" + + def test_no_recipe_command_controls_the_unit_and_r1_stops_for_review_with_two_records(self): + self.assertEqual(binfmt_recovery_errors(*self.inputs()), []) + + def test_a_history_sentence_may_name_the_restart_but_a_command_block_may_not(self): + recipe = read(RECIPE) + self.assertIn("`sudo systemctl restart systemd-binfmt` there restored it", prose(section(recipe, "R1"))) + self.assertEqual([command for _, command in fenced_commands(recipe) if UNIT_CONTROL_RE.search(command)], []) + for command in (RESTART_UNIT, "sudo systemctl start systemd-binfmt.service", "systemctl stop systemd-binfmt", + "sudo systemctl try-restart systemd-binfmt.service"): + with self.subTest(command=command): + self.assertTrue(UNIT_CONTROL_RE.search(command)) + for command in (*INTEROP_RECORDS, F1_FAILED, F1_LOG): + with self.subTest(command=command): + self.assertFalse(UNIT_CONTROL_RE.search(command)) + + def test_the_check_rejects_the_old_restart_recovery_and_a_lost_record_or_stop(self): + recipe, record, checklist, receipt = self.inputs() + r1, w6, decision = section(recipe, "R1"), section(recipe, "W6"), chapter(record, "Decision") + block = "\n".join([NATIVE_INTEROP, *INTEROP_RECORDS]) + "\n" + self.assertIn("```sh\n" + block + "```\n", r1) + native_row = next(line for line in record.splitlines() if line.startswith(f"| R1 | sh | `{NATIVE_INTEROP}` |")) + old_row = f"| R1 | sh | `{RESTART_UNIT.replace('|', chr(92) + '|')}` | only after removal, on the survivor: exit 0; " \ + "failure stops recovery |" + new_checklist = {"R1": (", and when either check fails R1's records (`ls /proc/sys/fs/binfmt_misc` and " + "`systemctl status systemd-binfmt.service --no-pager`) are kept and the run stops for review, " + "importing or provisioning nothing"), + "W6": ", and when either check fails R1's records are kept and the run stops for review with no import"} + old_checklist = ", with R1's `sudo systemctl restart systemd-binfmt` recovery if needed and a stop if either check " \ + "still fails" + for step, fragment in new_checklist.items(): + self.assertIn(fragment, checklist_line(checklist, step)) + new_decision = ("If registration or Windows execution does not return, the run stops for review after the new " + "distribution is removed, with `ls /proc/sys/fs/binfmt_misc` and `systemctl status " + "systemd-binfmt.service --no-pager` recorded; nothing is imported or provisioned. The WSL 2.7.x " + "remedy, restarting `systemd-binfmt`, is not used on the adopted release, where that restart exits 1.") + old_decision = ("R1's supported `sudo systemctl restart systemd-binfmt` recovery runs only after the new distribution " + "is removed; if registration or Windows execution does not return, the run stops.") + new_w6 = ("If the guard throws, do not import: take R1's interop recovery there, which records the observations and " + "stops for review.") + old_w6 = ("If the guard throws, run R1's interop recovery there and repeat both observations before resuming at " + "`--import`; if interop does not return, stop.") + old_interop = ("") + moved = r1.replace("```sh\n" + block + "```\n", "").replace("\n\n", "\n\n```sh\n" + block + "```\n\n", 1) + page = lambda new_r1: (recipe.replace(r1, new_r1), record, checklist, receipt) # noqa: E731 + self.assert_mutants_fail(binfmt_recovery_errors, { + "the old restart recovery": page(r1.replace(block, OLD_INTEROP_RECOVERY)), + "the restart beside the records": page(r1.replace(block, RESTART_UNIT + "\n" + block)), + "a restart in another step": (recipe.replace("```sh\n" + GETTY_MASK + "\n", "```sh\nsudo systemctl restart " + "systemd-binfmt\n" + GETTY_MASK + "\n", 1), record, checklist, + receipt), + "a start beside the records": page(r1.replace(block, block + "sudo systemctl start systemd-binfmt.service\n")), + "no listing of the formats": page(r1.replace(INTEROP_RECORDS[0] + "\n", "")), + "no status of the unit": page(r1.replace(INTEROP_RECORDS[1] + "\n", "")), + "records in the wrong order": page(r1.replace("\n".join(INTEROP_RECORDS) + "\n", + "\n".join(reversed(INTEROP_RECORDS)) + "\n")), + "records before the interop checks": page(moved), + "no stop for review": page(reword(r1, "then stop for review", "then continue")), + "another distribution may be imported": page(reword(r1, "stop, do not import or provision another distribution, " + "and record", "stop and record")), + "no history of the 2.7.13 loss": page(reword(r1, "On WSL 2.7.13 the rehearsal's unregister removed this VM-wide " + "registration on the surviving distribution, and `sudo " + "systemctl restart systemd-binfmt` there restored it.", "")), + "W6 resumes after the recovery": (recipe.replace(w6, reword(w6, new_w6, old_w6)), record, checklist, receipt), + "R1 checklist with the restart": (recipe, record, checklist.replace(new_checklist["R1"], old_checklist), receipt), + "W6 checklist with the restart": (recipe, record, checklist.replace(new_checklist["W6"], old_checklist), receipt), + "record with the restart row": (recipe, record.replace(native_row, native_row + "\n" + old_row), checklist, + receipt), + "record decision with the restart": (recipe, record.replace(decision, reword(decision, new_decision, + old_decision)), checklist, receipt), + "receipt with the old recovery note": (recipe, record, checklist, dict(receipt, interop_after_unregister=old_interop)), + }) + + +ORIGINAL_QUALITY_RULE = ( + "Both 26.04.1 trial and 24.04.5 fallback are symmetric provisional arms with no merit precedence. " + "Preregister the same separate-throwaway R1 comparisons on one unchanged host/kernel/driver, checkout, " + "user-data and bootstrap profile. Both signed sums pass P1; P2 workstation schema is not image qualification. " + "W2 hashes the whole selected image against its exact signed/catalog release mapping; W4/W6 use that filename. " + "Each arm must record first_boot (W5 default uid 1000, retained results, selected-image cloud-init version/schema " + "and F1 running); systemd_user_from_second_instance (F2 linger/idle and F3 directory/socket ownership plus manager " + "running from the workstation second instance); wsl_gpu (/dev/dxg and native nvidia-smi visibility on the same " + "Windows driver); uv_cpython_3_13 (separate managed Python 3.13.x through uv); node_24 (bootstrap v24.21.0 startup). " + "Preserve P3 age/baseline, W5 second count, W7 owner 1000:1000 and failed-rehearsal/W6 export-before-unregister " + "guards. Missing, failed or skipped criteria do not qualify an arm or rank its peer. Record sanitized native " + "per-arm output before selecting within the measured host scope. Full-stack, GPU-workload and model acceptance " + "remain separate and unrun." +) +FIRST_QUALITY_AMENDMENT = ( + "; amended on 2026-10-02 before any comparison ran: on WSL 3.0.1 the criterion is F1's pass condition, running, " + "or degraded with systemd-binfmt.service as the only failed unit, which fails by design there" +) +QUALITY_AMENDMENT_RE = re.compile(r"; amended on 2026-10-02 .*?(?=; amended on 2026-10-02 |\); " + r"systemd_user_from_second_instance)") +BASELINE_STOP_PARTS = ( + "The workstation's baseline passes only when all four hold:", + "its uid equals the new distribution's planned default uid `1000`", + "its system state is `running` with no failed unit, or `degraded` with its failed set contained in " + "{`systemd-binfmt.service`, `getty@tty1.service`}", + "its user manager is `active`", + "its cgroup namespace is not `cgroup:[4026531835]`", + "Anything else stops the run before W4 and before any W6 import", +) +BASELINE_STOP_RE = re.compile(r"^- The workstation's baseline passes.*?(?=^- |^```|\Z)", re.M | re.S) +LINUX_ENTRIES = ( + '/mnt/c/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -NoProfile -ExecutionPolicy Bypass ' + '-File "$(wslpath -w step.ps1)"', + "/mnt/c/Windows/System32/wsl.exe -d ", + "/mnt/c/Windows/System32/wsl.exe -d -- bash -s < steps.sh", + "/mnt/c/Windows/System32/wsl.exe -d -u root", + "/mnt/c/Windows/System32/wsl.exe -d -u root -- bash -s < path-b.sh", +) + + +def quality_rule_errors(experiment: dict) -> list[str]: + """Keep the full original preregistration and the first amendment; remove only the two dated amendments.""" + rule = field(experiment, "predeclared_metrics", "quality_rule") + if not isinstance(rule, str): + return ["quality_rule is not text"] + retained, count = QUALITY_AMENDMENT_RE.subn("", rule) + errors = [] + if retained != ORIGINAL_QUALITY_RULE: + errors.append("the quality rule outside its dated amendments differs from the full original wording") + if count != 2 or rule.count(FIRST_QUALITY_AMENDMENT) != 1: + errors.append("the quality rule must retain the first amendment and exactly one further dated amendment") + amendments = QUALITY_AMENDMENT_RE.findall(rule) + if len(amendments) != 2 or "F1's complete pass condition" not in amendments[-1] or FLUSH_MESSAGE not in amendments[-1]: + errors.append("the second amendment must interpret the first short form as F1's complete pass condition") + return errors + + +def workstation_stop_errors(recipe: str) -> list[str]: + """W1 must reject every incompatible baseline before any install or import, in its own stop paragraph.""" + w1 = section(recipe, "W1") + paragraph = BASELINE_STOP_RE.search(w1) + stop = prose(paragraph.group(0)) if paragraph else "" + errors = [f"W1's baseline stop lacks: {part}" for part in BASELINE_STOP_PARTS if part not in stop] + for part in ("leftover of an earlier collision", "`Result=start-limit-hit` is required", + "another result stops the run", "`getty_tty1_result`"): + if part not in prose(w1): + errors.append(f"W1 lacks the conditional getty baseline proof: {part}") + if ("sh", GETTY_RESULT) not in step_commands(recipe, "W1"): + errors.append("W1 lacks the extra read-only command for an already failed getty") + return errors + + +def receipt_baseline_errors(receipt: dict) -> list[str]: + """Each of the five workstation instructions must require equality to its own W1 baseline.""" + workstation = field(receipt, "paired_isolation", "workstation") + return [f"workstation {key} does not require equality to host.workstation_baseline" for key in sorted(ISOLATION_KEYS) + if not is_placeholder(field(workstation, key), " list[str]: + """Commands and receipt entries retain each observation's exit; no compound or tail pipeline hides it.""" + errors = [] + if WORKSTATION_COMMANDS not in step_blocks(recipe, "W1"): + errors.append("W1 must hold five native commands on five lines") + if WORKSTATION_COMMANDS + NEW_COMMANDS not in step_blocks(recipe, "W5"): + errors.append("W5 must hold five commands per distribution on ten lines") + if [command for _, command in fenced_commands(checklist)] != ( + WORKSTATION_COMMANDS + WORKSTATION_COMMANDS + NEW_COMMANDS): + errors.append("the checklist must repeat W1's five commands and W5's two halves separately") + table = Counter(command_table(record)) + entries = Counter((entry.get("step"), entry.get("cmd")) for entry in receipt.get("steps", [])) + for step, commands in (("W1", WORKSTATION_COMMANDS), ("W5", WORKSTATION_COMMANDS + NEW_COMMANDS)): + for command in commands: + expected = 2 if step == "W5" and command == USER_MANAGER_ACTIVE else 1 + if table[(step, "sh", command)] != expected or entries[(step, command)] != expected: + errors.append(f"{step} must record each occurrence of {command} in the table and receipt") + texts = (recipe, checklist, json.dumps(receipt), "\n".join(command for _, _, command in command_table(record))) + for text in texts: + if "id -u; systemctl is-system-running;" in text or "| tail -n 4" in text: + errors.append("an operational command still hides exits in the old compound or journal pipeline") + return errors + + +def storage_filter_errors(recipe: str, record: str, receipt: dict) -> list[str]: + """All three operational filters exclude registration and both forms of the kernel's command-line echo.""" + expected = Counter({STORVSC_COUNT: 2, STORVSC_NEWEST: 1}) + inventories = ([command for _, _, command in recipe_rows(recipe)], + [command for _, _, command in command_table(record)], + [entry.get("cmd", "") for entry in receipt.get("steps", [])]) + errors = [] + for commands in inventories: + filters = [command for command in commands if "grep hv_storvsc" in command] + if Counter(filters) != expected: + errors.append("the operational inventory does not hold exactly the three corrected hv_storvsc filters") + return errors + + +def getty_proof_errors(recipe: str, record: str, receipt: dict) -> list[str]: + errors = [] + for command in (GETTY_ENABLED, GETTY_SHOW): + if ("powershell", command) not in step_commands(recipe, "W5") or ( + "W5", "powershell", command) not in command_table(record): + errors.append(f"W5 or the command table lacks {command}") + roots = [block for block in step_blocks(recipe, "W6") if any(line.startswith("id -u ''") for line in block)] + if len(roots) != 1 or roots[0][:1] != [GETTY_MASK]: + errors.append("W6's root block must start with the getty mask") + for part in ("`masked`", "exit 1", "`LoadState=masked`", "`ActiveState=inactive`", "`NRestarts=0`", + "refused job of a masked unit"): + if part not in prose(section(recipe, "W5")): + errors.append(f"W5 lacks its getty proof: {part}") + if "starts the getty before the first line below can run" not in prose(section(recipe, "W6")): + errors.append("W6 must state the import's first-boot ordering limitation") + values = field(receipt, "first_launch", "getty_mask") + for key, part in (("is_enabled", "masked"), ("load_state", "LoadState=masked"), + ("active_state", "ActiveState=inactive"), ("n_restarts", "NRestarts=0")): + if not is_placeholder(field(values, key), " list[str]: + errors = [] + for text in (recipe, record, checklist, json.dumps(receipt)): + if "cgroup" + "_failure_export" in text or "-cgroup" + "-failed.tar" in text: + errors.append("an old W5 recovery name remains") + w5 = prose(section(recipe, "W5")) + for part in ("When any W5 proof fails", "Do not take path B for any such failure", "`w5_failure_export`", + "`cgroup`, `tty` or a short text", "this recovery block serves every failed W5 proof"): + if part not in w5: + errors.append(f"W5's recovery lacks: {part}") + export = field(receipt, "w5_failure_export") + if not isinstance(export, dict) or set(export) != {"file", "sha256", "bytes", "cause"}: + errors.append("w5_failure_export lacks file, sha256, bytes or cause") + elif "-w5-failed.tar" not in str(export["file"]) or not all(part in str(export["cause"]) for part in + ("cgroup", "tty", "short text")): + errors.append("w5_failure_export does not name the recovery file and its possible causes") + return errors + + +def linux_entry_errors(recipe: str) -> list[str]: + errors = [f"the Linux entry lacks its full Windows path: {command}" for command in LINUX_ENTRIES + if "`" + command + "`" not in recipe] + inline = re.findall(r"`([^`\n]*)`", recipe) + if any(command.startswith("powershell.exe -NoProfile") or (command.startswith("wsl.exe -d ") and + ("bash -s" in command or "-u root" in command)) for command in inline): + errors.append("a Linux entry starts a Windows program by a bare name") + if any(shell == "sh" and re.match(r"(?:wsl|powershell)\.exe\b", command) + for shell, command in fenced_commands(recipe)): + errors.append("an sh block starts a Windows program by a bare name") + return errors + + +def release_size_errors(recipe: str, receipt: dict) -> list[str]: + row = next((line for line in recipe.splitlines() if line.startswith("| 26.04.1 |")), "") + errors = [] + if "418,495,746; rehearsal run 2, 2026-10-02" not in row: + errors.append("the release table lacks run 2's observed 26.04.1 size") + if field(receipt, "supported_images", "26.04.1", "bytes") != 418495746: + errors.append("the receipt's supported image size does not match run 2") + return errors + + +class Run2RepairTests(FollowUpCase): + """The accepted PR #593 repair; every test asserts the new contract before trying a pre-change mutation.""" + + def test_bootcmd_is_exact_and_the_template_has_only_the_wsl_user_placeholder(self): + template = read(USER_DATA) + self.assertEqual(user_data_errors(template), []) + self.assertEqual(set(string.Template(template).get_identifiers()), {"WSL_USER"}) + self.assertEqual(string.Template(template).substitute(WSL_USER="example"), template.replace(PLACEHOLDER, "example")) + for old in (template.replace(GETTY_BOOTCMD, ""), template.replace("mask, --now", "mask"), + template + "# ${OTHER_USER}\n"): + with self.subTest(control="old or incorrect bootcmd"): + self.assertNotEqual(old, template) + self.assertTrue(user_data_errors(old)) + + def test_observations_are_separate_in_page_checklist_receipt_and_table(self): + inputs = self.inputs() + self.assertEqual(separate_observation_errors(*inputs), []) + recipe, record, checklist, receipt = inputs + for step, commands in (("W1", WORKSTATION_COMMANDS), ("W5", WORKSTATION_COMMANDS + NEW_COMMANDS)): + for command in commands: + with self.subTest(control="lost separate observation", step=step, command=command): + current = WORKSTATION_BASELINE if step == "W1" else WORKSTATION_BASELINE + "\n" + PAIRED_NEW + removed = re.sub(r"^" + re.escape(command) + r"\n", "", current + "\n", count=1, flags=re.M) + old = section(recipe, step).replace(current + "\n", removed, 1) + self.assertTrue(separate_observation_errors(recipe.replace(section(recipe, step), old), record, + checklist, receipt)) + self.assertTrue(separate_observation_errors(recipe, record, checklist.replace(WORKSTATION_BASELINE, "id -u; " + "systemctl is-system-running; " + NAMESPACE_READ), receipt)) + for step, command in (("W1", WORKSTATION_COMMANDS[1]), ("W5", NEW_COMMANDS[1])): + row = next(line for line in record.splitlines() if line.startswith(f"| {step} | sh | `{command}` |")) + self.assertTrue(separate_observation_errors(recipe, record.replace(row + "\n", ""), checklist, receipt)) + self.assertTrue(separate_observation_errors(recipe, record, checklist, dict(receipt, steps=[entry for entry in + receipt["steps"] if (entry["step"], entry["cmd"]) != (step, command)]))) + + def test_w1_stop_paragraph_is_required(self): + recipe = read(RECIPE) + self.assertEqual(workstation_stop_errors(recipe), []) + self.assertTrue(workstation_stop_errors(BASELINE_STOP_RE.sub("", recipe))) + + def test_w1_stop_requires_the_uid_1000_condition(self): + recipe = read(RECIPE) + self.assertEqual(workstation_stop_errors(recipe), []) + self.assertTrue(workstation_stop_errors(reword(recipe, BASELINE_STOP_PARTS[1], ""))) + + def test_w1_stop_requires_a_noninitial_namespace(self): + recipe = read(RECIPE) + self.assertEqual(workstation_stop_errors(recipe), []) + self.assertTrue(workstation_stop_errors(reword(recipe, BASELINE_STOP_PARTS[4], ""))) + + def test_w1_stop_requires_an_active_user_manager(self): + recipe = read(RECIPE) + self.assertEqual(workstation_stop_errors(recipe), []) + self.assertTrue(workstation_stop_errors(reword(recipe, BASELINE_STOP_PARTS[3], ""))) + + def test_w1_stop_requires_the_system_state_and_conditional_getty_result(self): + recipe = read(RECIPE) + self.assertEqual(workstation_stop_errors(recipe), []) + for part in (BASELINE_STOP_PARTS[2], "`Result=start-limit-hit` is required", "another result stops the run"): + with self.subTest(control=part): + self.assertTrue(workstation_stop_errors(reword(recipe, part, ""))) + self.assertTrue(workstation_stop_errors(recipe.replace(GETTY_RESULT + "\n", ""))) + + def test_all_five_receipt_instructions_require_the_workstation_baseline(self): + receipt = json.loads(read(RECEIPT_EXAMPLE)) + self.assertEqual(receipt_baseline_errors(receipt), []) + for key in sorted(ISOLATION_KEYS): + with self.subTest(control="weakened baseline equality", key=key): + old = json.loads(json.dumps(receipt)) + value = old["paired_isolation"]["workstation"][key] + old["paired_isolation"]["workstation"][key] = value.replace("equal to host.workstation_baseline", "recorded") + self.assertEqual(len(receipt_baseline_errors(old)), 1) + + def test_checklist_f1_requires_the_read_only_flush_message(self): + inputs = self.inputs() + self.assertEqual(binfmt_unit_errors(*inputs), []) + recipe, record, checklist, receipt = inputs + old = checklist.replace(FLUSH_MESSAGE, "any message is accepted") + self.assertNotEqual(old, checklist) + self.assertTrue(binfmt_unit_errors(recipe, record, old, receipt)) + + def test_quality_rule_retains_every_original_word_and_the_first_amendment(self): + experiment = json.loads(read(EXPERIMENT)) + self.assertEqual(quality_rule_errors(experiment), []) + rule = experiment["predeclared_metrics"]["quality_rule"] + controls = ("no merit precedence", rule.replace("and F1 running", "and any state"), + rule.replace(FIRST_QUALITY_AMENDMENT, ""), QUALITY_AMENDMENT_RE.sub("", rule), + rule.replace(FLUSH_MESSAGE, "any message")) + for old in controls: + with self.subTest(control="original or amendment weakened"): + mutant = dict(experiment, predeclared_metrics=dict(experiment["predeclared_metrics"], quality_rule=old)) + self.assertTrue(quality_rule_errors(mutant)) + + def test_all_three_storage_filters_exclude_the_command_line_echo(self): + recipe, record, _, receipt = self.inputs() + self.assertEqual(storage_filter_errors(recipe, record, receipt), []) + rows = [(step, command) for step, _, command in recipe_rows(recipe) if "grep hv_storvsc" in command] + self.assertEqual(len(rows), 3) + for step, command in rows: + with self.subTest(control="pre-change filter", step=step, command=command): + old_filter = command.replace("grep -Ev", "grep -v").replace("|[Cc]ommand line:", "") + old = section(recipe, step).replace(command, old_filter, 1) + self.assertTrue(storage_filter_errors(recipe.replace(section(recipe, step), old), record, receipt)) + + def test_w5_getty_proofs_are_commands_and_command_table_rows(self): + recipe, record, _, receipt = self.inputs() + self.assertEqual(getty_proof_errors(recipe, record, receipt), []) + for command in (GETTY_ENABLED, GETTY_SHOW): + with self.subTest(control="lost getty command or row", command=command): + self.assertTrue(getty_proof_errors(recipe.replace(command + "\n", ""), record, receipt)) + row = next(line for line in record.splitlines() if line.startswith(f"| W5 | powershell | `{command}` |")) + self.assertTrue(getty_proof_errors(recipe, record.replace(row + "\n", ""), receipt)) + + def test_w6_root_block_starts_with_the_getty_mask(self): + recipe, record, _, receipt = self.inputs() + self.assertEqual(getty_proof_errors(recipe, record, receipt), []) + self.assertTrue(getty_proof_errors(recipe.replace(GETTY_MASK + "\n", ""), record, receipt)) + w6 = section(recipe, "W6") + old = w6.replace(GETTY_MASK + "\n", "").replace("touch /etc/cloud/cloud-init.disabled\n", + "touch /etc/cloud/cloud-init.disabled\n" + GETTY_MASK + "\n") + self.assertTrue(getty_proof_errors(recipe.replace(w6, old), record, receipt)) + + def test_recovery_uses_the_w5_filename_key_and_cause_for_every_failure(self): + inputs = self.inputs() + self.assertEqual(w5_recovery_errors(*inputs), []) + recipe, record, checklist, receipt = inputs + old_file = "-cgroup" + "-failed.tar" + for index, text in enumerate((recipe, record, checklist)): + old = text.replace("-w5-failed.tar", old_file) + self.assertNotEqual(old, text) + self.assertTrue(w5_recovery_errors(*((*inputs[:index], old, *inputs[index + 1:])))) + old = json.loads(json.dumps(receipt)) + old["cgroup" + "_failure_export"] = old.pop("w5_failure_export") + self.assertTrue(w5_recovery_errors(recipe, record, checklist, old)) + self.assertTrue(w5_recovery_errors(recipe, record, checklist, without(receipt, "w5_failure_export", "cause"))) + self.assertTrue(w5_recovery_errors(reword(recipe, "Do not take path B for any such failure", "Take path B"), + record, checklist, receipt)) + + def test_linux_entry_commands_use_full_windows_paths(self): + recipe = read(RECIPE) + self.assertEqual(linux_entry_errors(recipe), []) + for command in LINUX_ENTRIES: + with self.subTest(command=command): + self.assertIn("`" + command + "`", recipe) + old = recipe.replace("`" + command + "`", "`" + command.rsplit("/", 1)[-1] + "`", 1) + self.assertTrue(linux_entry_errors(old)) + + def test_f10_quotes_each_path_without_word_splitting(self): + recipe, record, _, receipt = self.inputs() + self.assertEqual(path_proof_errors(recipe), []) + self.assertIn(("F10", "powershell", F10_PROBE), command_table(record)) + self.assertTrue(any(entry["step"] == "F10" and entry["cmd"] == F10_PROBE for entry in receipt["steps"])) + for old_probe in (F10_PROBE.replace("[[ -f $p && -x $p ]]", "test -f $p && test -x $p"), + F10_PROBE.replace("for n in claude codex; do p=$(type -P $n);", "for p in $(type -P claude codex); do")): + self.assertTrue(path_proof_errors(recipe.replace(F10_PROBE, old_probe))) + + def test_no_powershell_command_hands_wsl_an_argument_with_a_double_quote(self): + recipe, record, _, receipt = self.inputs() + self.assertEqual(powershell_quote_errors(recipe), []) + # the two forms that failed in rehearsal run 3 must be caught + quoted_stat = "wsl.exe -d '' --exec bash -lc 'stat -c \"%U %F\" \"/run/user/$(id -u)\" \"/run/user/$(id -u)/bus\"'" + quoted_loop = ("wsl.exe -d '' -u '' --exec /bin/bash -lc 'for n in claude codex; do p=\"$(type -P \"$n\")\"; " + "test -f \"$p\" && test -x \"$p\" && echo \"executable: $p\"; done'") + self.assertIn(R1_BUS_PROBE, recipe) + self.assertTrue(powershell_quote_errors(recipe.replace(R1_BUS_PROBE, quoted_stat))) + self.assertTrue(powershell_quote_errors(recipe.replace(F10_PROBE, quoted_loop))) + self.assertIn(("R1", "powershell", R1_BUS_PROBE), command_table(record)) + self.assertTrue(any(entry["cmd"] == R1_BUS_PROBE for entry in receipt["steps"])) + + def test_f5_adds_a_range_only_when_none_exists(self): + recipe, record, _, receipt = self.inputs() + f5 = [command for step, _, command in recipe_rows(recipe) if step == "F5"] + self.assertIn(F5_GUARDED, f5) + self.assertNotIn(F5_GUARDED.split(" || ", 1)[1], f5, "the unguarded usermod line would add a second range") + self.assertIn(("F5", "sh", F5_GUARDED), command_table(record)) + self.assertTrue(any(entry["step"] == "F5" and entry["cmd"] == F5_GUARDED for entry in receipt["steps"])) + + def test_release_table_and_receipt_hold_the_observed_image_size(self): + recipe, _, _, receipt = self.inputs() + self.assertEqual(release_size_errors(recipe, receipt), []) + row = next(line for line in recipe.splitlines() if line.startswith("| 26.04.1 |")) + self.assertIn("418,495,746; rehearsal run 2, 2026-10-02", row) + self.assertEqual(receipt["supported_images"]["26.04.1"]["bytes"], 418495746) + self.assertTrue(release_size_errors(recipe.replace(row, row.replace("418,495,746", "unknown")), receipt)) + old = json.loads(json.dumps(receipt)) + old["supported_images"]["26.04.1"]["bytes"] = None + self.assertTrue(release_size_errors(recipe, old)) + + def test_receipt_new_keys_validate_and_the_getty_baseline_key_is_optional(self): + recipe, record, checklist, receipt = self.inputs() + components = {component["id"] for component in json.loads(read(STACK))["components"]} + self.assertEqual(receipt_errors(receipt, recipe, components), []) + self.assertEqual(getty_proof_errors(recipe, record, receipt), []) + self.assertEqual(paired_isolation_errors(recipe, record, checklist, receipt), []) + optional = without(receipt, "host", "workstation_baseline", "getty_tty1_result") + self.assertEqual(receipt_errors(optional, recipe, components), []) + self.assertEqual(paired_isolation_errors(recipe, record, checklist, optional), []) + for key in ("is_enabled", "load_state", "active_state", "n_restarts"): + with self.subTest(control="lost mask key", key=key): + self.assertTrue(receipt_errors(without(receipt, "first_launch", "getty_mask", key), recipe, components)) + self.assertTrue(receipt_errors(without(receipt, "w5_failure_export", "cause"), recipe, components)) + old = json.loads(json.dumps(receipt)) + old["host"]["workstation_baseline"]["getty_tty1_result"] = "" + self.assertTrue(receipt_errors(old, recipe, components)) + + if __name__ == "__main__": unittest.main()