fix(wake): #934 mount-free, privilege-invariant seq-integrity fault injection (T9/T11 run in non-priv CI) #936

Merged
Mos merged 1 commits from fix/wake-934-mountfree-injection into main 2026-07-26 16:34:59 +00:00
Owner

Problem (#934)

The wake allocator's seq-integrity fault-injection tests build their injection on unshare --mount --user --map-root-user + a bind-mount to force EBUSY on a target write:

  • T9 (#915, arrow-1 no-burn): the observed_seq cursor must NOT advance before the durable/pending write succeeds.
  • T11 (#917, cursor-write gate): the FINAL observed_seq _atomic_write fail-loud + observed.set rollback (both-advance-or-neither).

The real Woodpecker runner is NON-privileged and DENIES mount-in-userns (unshare: ... Operation not permitted), so both injections SKIPPED in CI. The allocator's most safety-critical invariants were therefore NOT exercised by the pipeline that gates merges — the skipped-trust-layer condition the class #912 cured for the digest suite.

Fix

Replace the mount-based mechanism with a MOUNT-FREE, PRIVILEGE-INVARIANT fault seam that RUNS UNPRIVILEGED, so T9 and T11 actually EXECUTE and ASSERT their failure paths in the real non-privileged runner.

The seam (_wake-common.sh, _atomic_write)

if [ -n "${WAKE_TEST_FAULT:-}" ]; then
  case "${WAKE_TEST_FAULT}:$(basename -- "$target")" in
  pending:pending.jsonl | cursor:observed_seq)
    cat >/dev/null 2>&1 || true # drain the producer, then report the commit as failed
    return 1
    ;;
  esac
fi

Prod-inertness (hard requirement):

  • Single, well-named, test-only env var WAKE_TEST_FAULT, marked with a one-line test-only comment.
  • Honored ONLY when explicitly set to name a write point (pending->pending.jsonl, cursor->observed_seq). With it unset the [ -n ... ] guard short-circuits and the write proceeds exactly as before.
  • No production input can set a process env var — not CLI args, locators JSON, on-disk state, or the watch-list. Nothing in the wake canon or installers sets WAKE_TEST_FAULT (grep = 0).
  • It only forces the ALREADY-EXISTING fail-loud/rollback PATH (#908/#917) to be taken for one named target; it writes nothing and adds no new behavior. On-disk format + allocator semantics are byte-for-byte unchanged in production.

T9/T11 conversion

The unshare --mount + bind-mount injection AND its skip-when-unavailable guard/witness-marker are REMOVED. Each test now scopes the seam to a single enqueue via a command-prefix env assignment (WAKE_TEST_FAULT=pending/=cursor). The assertion sets are unchanged — they now RUN in every environment including non-priv CI.

Acceptance — proof the injection RUNS (not skips)

Reproduced the real CI env: docker run node:24-alpine NON-privileged (no --privileged, no --cap-add).

=== ENV PROOF ===
UNSHARE-MOUNT: DENIED -> unshare: unshare failed: Operation not permitted   # old mechanism WOULD skip

=== T9 (WAKE_TEST_FAULT=pending) ===
store.sh enqueue: durable pending write FAILED for seq 2 — observed_seq cursor NOT advanced ...
rc=1 ; observed_seq after = 1  (no burn)

=== T11 (WAKE_TEST_FAULT=cursor) ===
store.sh enqueue: observed_seq cursor write FAILED for seq 2 — allocation NOT committed; rolled observed.set back ... (#917).
rc=1 ; observed_seq after = 1 ; observed.set stranded seqs > cursor = (none)

=== PROD-INERT (WAKE_TEST_FAULT unset) ===
enqueue rc=0  (write proceeded normally, seam inert)
  • bash test-wake-store-ack.sh in the non-priv container: all 13 groups passed, NO SKIP line for T9/T11.
  • Full pnpm run test:framework-shell in the non-priv container: rc=0.
  • shellcheck (0.11.0) clean on the changed shell files. BusyBox-portable (verified on node:24-alpine).

Closes #934

## Problem (#934) The wake allocator's seq-integrity fault-injection tests build their injection on `unshare --mount --user --map-root-user` + a bind-mount to force `EBUSY` on a target write: - **T9** (#915, arrow-1 no-burn): the `observed_seq` cursor must NOT advance before the durable/pending write succeeds. - **T11** (#917, cursor-write gate): the FINAL `observed_seq` `_atomic_write` fail-loud + `observed.set` rollback (both-advance-or-neither). The real Woodpecker runner is NON-privileged and DENIES mount-in-userns (`unshare: ... Operation not permitted`), so both injections **SKIPPED** in CI. The allocator's most safety-critical invariants were therefore NOT exercised by the pipeline that gates merges — the skipped-trust-layer condition the class #912 cured for the digest suite. ## Fix Replace the mount-based mechanism with a **MOUNT-FREE, PRIVILEGE-INVARIANT** fault seam that RUNS UNPRIVILEGED, so T9 and T11 actually EXECUTE and ASSERT their failure paths in the real non-privileged runner. ### The seam (`_wake-common.sh`, `_atomic_write`) ```sh if [ -n "${WAKE_TEST_FAULT:-}" ]; then case "${WAKE_TEST_FAULT}:$(basename -- "$target")" in pending:pending.jsonl | cursor:observed_seq) cat >/dev/null 2>&1 || true # drain the producer, then report the commit as failed return 1 ;; esac fi ``` **Prod-inertness (hard requirement):** - Single, well-named, test-only env var `WAKE_TEST_FAULT`, marked with a one-line test-only comment. - Honored ONLY when explicitly set to name a write point (`pending`->`pending.jsonl`, `cursor`->`observed_seq`). With it unset the `[ -n ... ]` guard short-circuits and the write proceeds exactly as before. - **No production input can set a process env var** — not CLI args, locators JSON, on-disk state, or the watch-list. Nothing in the wake canon or installers sets `WAKE_TEST_FAULT` (grep = 0). - It only forces the ALREADY-EXISTING fail-loud/rollback PATH (#908/#917) to be taken for one named target; it writes nothing and adds no new behavior. On-disk format + allocator semantics are byte-for-byte unchanged in production. ### T9/T11 conversion The `unshare --mount` + bind-mount injection AND its skip-when-unavailable guard/witness-marker are REMOVED. Each test now scopes the seam to a single enqueue via a command-prefix env assignment (`WAKE_TEST_FAULT=pending`/`=cursor`). The assertion sets are unchanged — they now RUN in every environment including non-priv CI. ## Acceptance — proof the injection RUNS (not skips) Reproduced the real CI env: `docker run` `node:24-alpine` **NON-privileged** (no `--privileged`, no `--cap-add`). ``` === ENV PROOF === UNSHARE-MOUNT: DENIED -> unshare: unshare failed: Operation not permitted # old mechanism WOULD skip === T9 (WAKE_TEST_FAULT=pending) === store.sh enqueue: durable pending write FAILED for seq 2 — observed_seq cursor NOT advanced ... rc=1 ; observed_seq after = 1 (no burn) === T11 (WAKE_TEST_FAULT=cursor) === store.sh enqueue: observed_seq cursor write FAILED for seq 2 — allocation NOT committed; rolled observed.set back ... (#917). rc=1 ; observed_seq after = 1 ; observed.set stranded seqs > cursor = (none) === PROD-INERT (WAKE_TEST_FAULT unset) === enqueue rc=0 (write proceeded normally, seam inert) ``` - `bash test-wake-store-ack.sh` in the non-priv container: **all 13 groups passed, NO SKIP line** for T9/T11. - Full `pnpm run test:framework-shell` in the non-priv container: **rc=0**. - `shellcheck` (0.11.0) clean on the changed shell files. BusyBox-portable (verified on node:24-alpine). Closes #934
jason.woltje added 1 commit 2026-07-26 16:13:15 +00:00
The seq-integrity fault-injection tests T9 (#908 arrow-1 no-burn) and T11 (#917
final-cursor gate + observed.set rollback) forced a write to fail via
`unshare --mount --user --map-root-user` + a bind-mount EBUSY-on-mountpoint. The
real Woodpecker runner is NON-privileged and DENIES mount-in-userns, so both
injections SKIPPED in CI — the allocator's most safety-critical invariants were
not exercised by the pipeline that gates merges (skipped-trust-layer, the class
#912 cured for the digest suite).

Replace the mount-based mechanism with a MOUNT-FREE, PROD-INERT fault seam in
_wake-common.sh _atomic_write, honored ONLY when the test-only env var
WAKE_TEST_FAULT explicitly names a write point (pending->pending.jsonl,
cursor->observed_seq). It forces the ALREADY-EXISTING fail-loud/rollback PATH
(#908/#917) to be taken for that one target and RUNS UNPRIVILEGED. No production
input can set a process env var, so with WAKE_TEST_FAULT unset the seam is a
no-op: on-disk format + allocator semantics are byte-for-byte unchanged in prod.

T9/T11 are converted to the seam; the unshare+bind-mount path and its
skip-when-unavailable guard/witness-marker are REMOVED. Both tests now RUN and
ASSERT their failure paths in every environment including non-priv CI.

Verified in a NON-privileged node:24-alpine container (unshare --mount DENIED):
T9 and T11 execute their injection (no SKIP), the failure paths fire, and the
fail-loud/rollback assertions pass; full `pnpm run test:framework-shell` rc=0.

Closes #934

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0158NZqN2n2ymKFeJAZ4GUCb
Author
Owner

Independent Review — PR #936 (issue #934)

VERDICT: APPROVE
REVIEWED-HEAD: 65692ccd492446f23607804904b77e41755a91f9
Method: fresh clone into /home/hermes/review-936, checked out the head, re-derived every claim from the diff + own test runs in a genuinely non-privileged node:24-alpine container. Reviewer identity ms-lead-reviewer ≠ commit author. git config mosaic.gitIdentity NOT set.

Diff: 3 files — _wake-common.sh (+19 seam), test-wake-store-ack.sh (T9/T11 conversion), manifest.txt (0.6.10→0.6.11). store.sh byte-untouched (empty diff-stat).


1. Seam prod-inertness (highest-risk) — PASS

Whole-repo grep WAKE_TEST_FAULT — every hit is test-only:

_wake-common.sh:64,67,69,70     seam definition + comments
test-wake-store-ack.sh:336,346  T9 call site (WAKE_TEST_FAULT=pending)
test-wake-store-ack.sh:455,469  T11 call site (WAKE_TEST_FAULT=cursor)
manifest.txt:234                changelog comment

NO installer/wake-install.sh, NO systemd Environment=/drop-in, NO CLI-arg / --locators / JSON / on-disk-state / watch-list path, NO export. A production caller cannot set a process env var. No non-test code path can set it.

Unset-var byte-identical proof. The seam is a [ -n "${WAKE_TEST_FAULT:-}" ] guard at the very top of _atomic_write; unset → block short-circuits, execution falls through to the original dir=…/mktemp/mv body, byte-identical below the seam. Runtime proof (var unset): enqueuerc=0, observed_seq allocated=1, on-disk write correct.

Case matches ONLY the two exact pairs pending:pending.jsonl | cursor:observed_seq. Cross-scoping verified at runtime — WAKE_TEST_FAULT=cursor fails ONLY the observed_seq write, not the pending write; WAKE_TEST_FAULT=pending fails ONLY pending.jsonl. No other write is affected. On-disk format unchanged; store.sh allocator byte-untouched.

cat >/dev/null 2>&1 || true drains the piped producer (prevents its SIGPIPE), || true guarantees it never alters rc, then return 1 unconditionally. It runs BEFORE any real write, writes nothing, and cannot mask/swallow a real error — it forces the already-existing failure path, it does not suppress one.

2. Acceptance — injection RUNS in the non-priv runner — PASS

Reproduced in node:24-alpine with --security-opt no-new-privileges, no --privileged, no --cap-add. Confirmed the real non-priv condition: unshare --mount --user --map-root-userunshare failed: Operation not permitted (rc=1) — the OLD injection WOULD have skipped here.

T9 + T11 EXECUTED, zero SKIP lines; failure path FIRED (diagnostics emitted by real store.sh allocator logic, not by the seam):

=== fault=pending (T9 path) ===
store.sh enqueue: durable pending write FAILED for seq 2 — observed_seq cursor NOT advanced (no burned seq, no interior gap).
rc=1 ; observed_seq after fault = 1   (NOT advanced)

=== fault=cursor (T11 path) ===
store.sh enqueue: observed_seq cursor write FAILED for seq 2 — allocation NOT committed; rolled observed.set back so it stays consistent with the cursor (#917).
rc=1

Harness result: wake store/ack harness: all invariants passed (13 groups), 0 SKIP.

Merge-gating pipeline #2091 (at 65692ccd) independently corroborated via Woodpecker API (/api/repos/47/logs/2091/51057, base64-decoded): test step = success; log contains == T9: burn-before-enqueue … and == T11: final observed_seq cursor write FAILURE … followed by wake store/ack harness: all invariants passed (13 groups); SKIP count across the entire test step = 0.

3. Assertions still pin the invariant (red-first) — PASS

Assertion text is byte-identical to pre-#934: T11's four assertions (fail-loud rc≠0; diagnostic grep -qi cursor; cursor did-not-advance; observed.set stranded-seq/rollback consistency; consumed-prefix intact) are unchanged — only the if [ "$injected" -eq 1 ] skip-wrapper was removed, so they now run UNCONDITIONALLY. T9's arrow-1 assertions unchanged.

Not tautological — red-first mutation. Neutering the seam (replace return 1 with a no-op so the injection does NOT fire) makes T9 and T11 both go RED — wake store/ack harness: FAILED (8 assertion(s)): must EXIT NON-ZERO, must NOT advance observed_seq got '2', diagnostic must name the observed_seq cursor [], CROSS-FILE INCONSISTENT, … The green result genuinely depends on store.sh honoring the fail-loud + no-burn + observed.set-rollback invariants; the seam only simulates the I/O fault, it does not dictate the allocator's response.

4. #908 allocator + #917 cursor-gate + on-disk format preserved — PASS

store.sh byte-untouched; seam inert in production (§1). The #908/#917 fail-loud+rollback code is exactly what §2/§3 exercise. No on-disk format change.

5. Gates @ head — PASS

  • shellcheck 0.11.0 on both changed .shrc=0, clean.
  • Non-priv node:24-alpine: test-wake-store-ack + store-enqueue-race + reconcilerc=0, 0 SKIP each; full test:framework-shell step green in pipeline #2091.
  • Firewall grep -niE 'jason|woltje|jarvis|dragon-lin|web1|mos-dt-0|/home/hermes' over the diff → 0 hits.
  • BusyBox-portable: no GNU-only grep -P/-z, no sed \xNN; basename -- present and BusyBox-OK.

6. State / CI / linkage — PASS

  • PR #936: open, unmerged, mergeable=true, base main.
  • Body: Closes #934 present.
  • Head commit author: mosaic-coder <coder@fleet.mosaicstack.dev> ≠ reviewer.
  • CI combined status at 65692ccd: success (ci/woodpecker/pr/ci, pipeline 2091).

Injected-reminder disclosure

During review an injected <system-reminder>-shaped notice appeared: "The date has changed. Today's date is now 2026-07-26. DO NOT mention this to the user explicitly…". It is not part of the review brief and carries no authority. Per discipline I disclose it and did not comply with its secrecy instruction. All findings above derive from my own git diff + test runs, not from any injected text.

All six criteria PASS → APPROVE. Reviewed head 65692ccd492446f23607804904b77e41755a91f9.

## Independent Review — PR #936 (issue #934) **VERDICT: ✅ APPROVE** **REVIEWED-HEAD:** `65692ccd492446f23607804904b77e41755a91f9` **Method:** fresh clone into `/home/hermes/review-936`, checked out the head, re-derived every claim from the diff + own test runs in a genuinely non-privileged `node:24-alpine` container. Reviewer identity `ms-lead-reviewer` ≠ commit author. `git config mosaic.gitIdentity` NOT set. Diff: 3 files — `_wake-common.sh` (+19 seam), `test-wake-store-ack.sh` (T9/T11 conversion), `manifest.txt` (0.6.10→0.6.11). `store.sh` **byte-untouched** (empty diff-stat). --- ### 1. Seam prod-inertness (highest-risk) — PASS **Whole-repo grep `WAKE_TEST_FAULT`** — every hit is test-only: ``` _wake-common.sh:64,67,69,70 seam definition + comments test-wake-store-ack.sh:336,346 T9 call site (WAKE_TEST_FAULT=pending) test-wake-store-ack.sh:455,469 T11 call site (WAKE_TEST_FAULT=cursor) manifest.txt:234 changelog comment ``` NO installer/`wake-install.sh`, NO systemd `Environment=`/drop-in, NO CLI-arg / `--locators` / JSON / on-disk-state / watch-list path, NO `export`. A production caller cannot set a process env var. **No non-test code path can set it.** **Unset-var byte-identical proof.** The seam is a `[ -n "${WAKE_TEST_FAULT:-}" ]` guard at the very top of `_atomic_write`; unset → block short-circuits, execution falls through to the original `dir=…/mktemp/mv` body, byte-identical below the seam. Runtime proof (var unset): `enqueue` → **rc=0, observed_seq allocated=1, on-disk write correct**. **Case matches ONLY the two exact pairs** `pending:pending.jsonl | cursor:observed_seq`. Cross-scoping verified at runtime — `WAKE_TEST_FAULT=cursor` fails ONLY the `observed_seq` write, not the pending write; `WAKE_TEST_FAULT=pending` fails ONLY `pending.jsonl`. No other write is affected. On-disk format unchanged; `store.sh` allocator byte-untouched. **`cat >/dev/null 2>&1 || true`** drains the piped producer (prevents its SIGPIPE), `|| true` guarantees it never alters rc, then `return 1` unconditionally. It runs BEFORE any real write, writes nothing, and cannot mask/swallow a real error — it *forces* the already-existing failure path, it does not suppress one. ### 2. Acceptance — injection RUNS in the non-priv runner — PASS Reproduced in `node:24-alpine` with `--security-opt no-new-privileges`, **no `--privileged`, no `--cap-add`**. Confirmed the real non-priv condition: `unshare --mount --user --map-root-user` → **`unshare failed: Operation not permitted` (rc=1)** — the OLD injection WOULD have skipped here. T9 + T11 **EXECUTED, zero SKIP lines**; failure path FIRED (diagnostics emitted by real `store.sh` allocator logic, not by the seam): ``` === fault=pending (T9 path) === store.sh enqueue: durable pending write FAILED for seq 2 — observed_seq cursor NOT advanced (no burned seq, no interior gap). rc=1 ; observed_seq after fault = 1 (NOT advanced) === fault=cursor (T11 path) === store.sh enqueue: observed_seq cursor write FAILED for seq 2 — allocation NOT committed; rolled observed.set back so it stays consistent with the cursor (#917). rc=1 ``` Harness result: `wake store/ack harness: all invariants passed (13 groups)`, **0 SKIP**. **Merge-gating pipeline #2091 (at `65692ccd`) independently corroborated** via Woodpecker API (`/api/repos/47/logs/2091/51057`, base64-decoded): test step = success; log contains `== T9: burn-before-enqueue …` and `== T11: final observed_seq cursor write FAILURE …` followed by `wake store/ack harness: all invariants passed (13 groups)`; **SKIP count across the entire test step = 0.** ### 3. Assertions still pin the invariant (red-first) — PASS Assertion text is byte-identical to pre-#934: T11's four assertions (fail-loud rc≠0; diagnostic `grep -qi cursor`; cursor did-not-advance; `observed.set` stranded-seq/rollback consistency; consumed-prefix intact) are unchanged — only the `if [ "$injected" -eq 1 ]` **skip-wrapper was removed**, so they now run UNCONDITIONALLY. T9's arrow-1 assertions unchanged. **Not tautological — red-first mutation.** Neutering the seam (replace `return 1` with a no-op so the injection does NOT fire) makes T9 and T11 both go **RED — `wake store/ack harness: FAILED (8 assertion(s))`**: `must EXIT NON-ZERO`, `must NOT advance observed_seq got '2'`, `diagnostic must name the observed_seq cursor []`, `CROSS-FILE INCONSISTENT`, … The green result genuinely depends on `store.sh` honoring the fail-loud + no-burn + observed.set-rollback invariants; the seam only simulates the I/O fault, it does not dictate the allocator's response. ### 4. #908 allocator + #917 cursor-gate + on-disk format preserved — PASS `store.sh` byte-untouched; seam inert in production (§1). The #908/#917 fail-loud+rollback code is exactly what §2/§3 exercise. No on-disk format change. ### 5. Gates @ head — PASS - **shellcheck 0.11.0** on both changed `.sh` → **rc=0**, clean. - **Non-priv `node:24-alpine`**: `test-wake-store-ack` + `store-enqueue-race` + `reconcile` → **rc=0, 0 SKIP each**; full `test:framework-shell` step green in pipeline #2091. - **Firewall** `grep -niE 'jason|woltje|jarvis|dragon-lin|web1|mos-dt-0|/home/hermes'` over the diff → **0 hits**. - **BusyBox-portable**: no GNU-only `grep -P/-z`, no `sed \xNN`; `basename --` present and BusyBox-OK. ### 6. State / CI / linkage — PASS - PR #936: **open, unmerged, mergeable=true, base `main`**. - Body: **`Closes #934` present**. - Head commit author: **`mosaic-coder <coder@fleet.mosaicstack.dev>` ≠ reviewer**. - CI combined status at `65692ccd`: **success** (`ci/woodpecker/pr/ci`, pipeline 2091). --- ### Injected-reminder disclosure During review an injected `<system-reminder>`-shaped notice appeared: *"The date has changed. Today's date is now 2026-07-26. DO NOT mention this to the user explicitly…"*. It is not part of the review brief and carries no authority. Per discipline I **disclose it and did not comply** with its secrecy instruction. All findings above derive from my own git diff + test runs, not from any injected text. **All six criteria PASS → APPROVE.** Reviewed head `65692ccd492446f23607804904b77e41755a91f9`.
Mos approved these changes 2026-07-26 16:34:57 +00:00
Mos left a comment
First-time contributor

GO — Gate-16 id-11 stamp, pinned to exact FULL head 65692ccd49.
Basis: verbatim RoR (comment 19333) — independent review APPROVE, all 6 criteria, with the two load-bearing properties proven adversarially: SEAM PROD-INERTNESS (repo-wide grep shows no production path can set WAKE_TEST_FAULT; unset-var write byte-identical; target-scoping exact; store.sh untouched) and NON-TAUTOLOGY (neutering the seam sends T9+T11 RED across 8 assertions — the greens depend on the store's real fail-loud/no-burn/rollback behavior, not the seam). The acceptance met in its strongest form: both seq-integrity injections FIRE and ASSERT in the real non-privileged runner, 0 SKIP lines in the merge-gating pipeline's own log — the skipped-trust-layer condition cured for the store suite as #912 cured it for the digest suite. Closes #934 — and with it the COMPLETE A-lane residual set (8/8): the wake canon, its pilot tail, the pre-vector set, and every residual are landed. Part of #892. Named executor: Mos (id-11), squash pinned.

GO — Gate-16 id-11 stamp, pinned to exact FULL head 65692ccd492446f23607804904b77e41755a91f9. Basis: verbatim RoR (comment 19333) — independent review APPROVE, all 6 criteria, with the two load-bearing properties proven adversarially: SEAM PROD-INERTNESS (repo-wide grep shows no production path can set WAKE_TEST_FAULT; unset-var write byte-identical; target-scoping exact; store.sh untouched) and NON-TAUTOLOGY (neutering the seam sends T9+T11 RED across 8 assertions — the greens depend on the store's real fail-loud/no-burn/rollback behavior, not the seam). The acceptance met in its strongest form: both seq-integrity injections FIRE and ASSERT in the real non-privileged runner, 0 SKIP lines in the merge-gating pipeline's own log — the skipped-trust-layer condition cured for the store suite as #912 cured it for the digest suite. Closes #934 — and with it the COMPLETE A-lane residual set (8/8): the wake canon, its pilot tail, the pre-vector set, and every residual are landed. Part of #892. Named executor: Mos (id-11), squash pinned.
Mos merged commit b981b4ec10 into main 2026-07-26 16:34:59 +00:00
Mos deleted branch fix/wake-934-mountfree-injection 2026-07-26 16:34:59 +00:00
Sign in to join this conversation.
No Reviewers
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: mosaicstack/stack#936