From 98046a76a28fef390fa2cd215171d152b7751d8e Mon Sep 17 00:00:00 2001 From: f10-coder Date: Fri, 31 Jul 2026 19:25:44 -0500 Subject: [PATCH] fix: reject malformed generated roots --- README.md | 7 +++++-- .../scratchpads/rm-01-reproducible-checkout.md | 2 +- scripts/preflight.mjs | 18 +++++++++++------- scripts/preflight.test.mjs | 11 +++++++++++ 4 files changed, 28 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index a232db81..9bccfc60 100644 --- a/README.md +++ b/README.md @@ -209,8 +209,11 @@ pnpm install # Missing dependencies exit 42; stale/foreign apps/web/.next state exits 43. # The web build certifies its exact standalone symlink manifest; added, removed, # retargeted, or manifest-only-tampered generated links also exit 43. This detects -# accidental/independent drift, not a same-UID actor that can rewrite both records -# consistently (CWE-345); an external trust anchor is required for that boundary. +# accidental, independent, stale, and foreign-residue mutation—the class exposed by +# a five-month-stale .next that produced 19 phantom TS2307 errors. +# It does NOT defend against a same-UID actor that can rewrite both manifest and +# marker consistently (CWE-345). RM-59 tracks the required executor/spine-side +# trust anchor outside worktree authority. pnpm preflight # Optional local queue service only. This does not start PostgreSQL. diff --git a/docs/scratchpads/rm-01-reproducible-checkout.md b/docs/scratchpads/rm-01-reproducible-checkout.md index 97a10779..3c6feac0 100644 --- a/docs/scratchpads/rm-01-reproducible-checkout.md +++ b/docs/scratchpads/rm-01-reproducible-checkout.md @@ -46,7 +46,7 @@ - The original blanket symlink wording conflicts with AC4 because canonical Next `output: 'standalone'` emits legitimate pnpm dependency symlinks. The coordinator independently verified 42 such links and approved the operative restatement: `.next` itself must not be a symlink; descendant symlinks must exactly match the successful build's certified manifest. - RED-first controls were observed failing together against the prior implementation (exit 1): `.next` root, added, removed, retargeted, tampered-manifest, and canonical-style certified-link cases. The build now publishes the manifest atomically before the existing source certification commit marker; that marker binds the manifest SHA-256. Missing/partial/modified manifests remain untrusted. - GREEN evidence: the six-case symlink control passes; the exact reviewer-added link exits 43; removing it restores preflight exit 0. The added RED-first build-publication control also proves a symlinked `.next` cannot redirect certification writes outside the checkout. `pnpm test:checkout` passes 23 top-level tests / 29 including subtests. Canonical `pnpm --filter @mosaicstack/web build` and the following `pnpm preflight` both exit 0. -- Threat-model ruling: the manifest detects accidental, independent, and stale mutation only. It does not defend against a same-UID actor able to rewrite both manifest and marker consistently (CWE-345); no local worktree construction can without an external trust anchor. The residual belongs to the planned choke-point executor / PostgreSQL spine where verification can occur outside worktree authority. +- Threat-model ruling: the manifest detects accidental, independent, stale, and foreign-residue mutation—the class exposed by the five-month-stale `.next` that produced 19 phantom TS2307 errors. It does not defend against a same-UID actor able to rewrite both manifest and marker consistently (CWE-345); no local worktree construction can without an external trust anchor. RM-59 tracks the residual: executor/spine-side attestation outside worktree authority, dependent on RM-12, RM-21, and RM-25. - AC8 concrete proof at `df7530ae`: a clean clone ran in `ci-base:latest` with Docker `--read-only`; its only writable mounts were `/workspace` (the worktree) and `/home/ci` (`HOME`, with `NPM_CONFIG_STORE_DIR=/home/ci/store`). `pnpm install --frozen-lockfile && pnpm -w typecheck` exited 0 with 45/45 uncached tasks. This proves the implemented checkout path requires no writable location outside `$HOME` and the worktree. An initial fixture attempt failed only because Git required `/workspace` safe-directory setup; it is not counted as evidence. ## Handoff diff --git a/scripts/preflight.mjs b/scripts/preflight.mjs index f3e21ae8..c1f5c99a 100644 --- a/scripts/preflight.mjs +++ b/scripts/preflight.mjs @@ -164,11 +164,11 @@ export async function runPreflight({ root = process.cwd(), uid = process.getuid? let generated = []; try { const nextStats = await lstat(nextDir); - if (nextStats.isSymbolicLink()) { + if (!nextStats.isDirectory() || nextStats.isSymbolicLink()) { return { code: GENERATED_STATE_EXIT, message: - 'MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next contains a symbolic link and is not trustworthy; run pnpm clean:generated, then rerun the gate', + 'MOSAIC_PREFLIGHT_GENERATED_STATE: apps/web/.next must be a real directory, not a symbolic link, and is not trustworthy; run pnpm clean:generated, then rerun the gate', }; } generated = [nextDir, ...(await entries(nextDir))]; @@ -183,11 +183,15 @@ export async function runPreflight({ root = process.cwd(), uid = process.getuid? if (uid !== undefined && stats.uid !== uid) foreign.push(path.relative(root, target)); } - // Threat model: this detects accidental, independent, or stale generated-state - // mutation. It does NOT defend against an actor with same-UID write access to - // the generated tree: that actor can regenerate both the manifest and marker - // consistently (CWE-345). No local construction can without a trust anchor - // outside that actor's authority. + // Detects accidental, independent, stale, and foreign-residue mutation of + // generated state: the class this check was born from was a five-month-stale + // .next whose validator referenced deleted pages and produced 19 phantom TS2307 + // errors indistinguishable from real type errors. + // + // Does NOT defend against an actor with same-UID write access to the generated + // tree, which can regenerate both the manifest and marker consistently + // (CWE-345). No local construction can, absent a trust anchor outside that + // actor's authority. RM-59 tracks executor/spine-side attestation. let certification = null; let certifiedManifest = null; try { diff --git a/scripts/preflight.test.mjs b/scripts/preflight.test.mjs index a035df4c..1e351912 100644 --- a/scripts/preflight.test.mjs +++ b/scripts/preflight.test.mjs @@ -128,6 +128,17 @@ test('generated-state symbolic links are accepted only when exactly build-certif assert.match(result.message, /symbolic link/); }); + await t.test('apps/web/.next is rejected when it is not a directory', async () => { + const root = await fixture('non-directory-next-root'); + await installRequiredBins(root); + await writeFile(path.join(root, 'apps', 'web', '.next'), 'not a Next build\n'); + + const result = await runPreflight({ root }); + assert.equal(result.code, 43); + assert.match(result.message, /MOSAIC_PREFLIGHT_GENERATED_STATE/); + assert.match(result.message, /real directory/); + }); + await t.test('an added descendant symlink is rejected', async () => { const root = await fixture('symbolic-next-added'); await installRequiredBins(root);