fix: certify generated symlink state

This commit is contained in:
2026-07-31 19:20:39 -05:00
parent 7ae3f97789
commit df7530aeac
6 changed files with 269 additions and 29 deletions
+62 -12
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env node
import { constants } from 'node:fs';
import { access, lstat, readFile, readdir } from 'node:fs/promises';
import { access, lstat, readFile, readdir, readlink } from 'node:fs/promises';
import { createHash } from 'node:crypto';
import { createRequire } from 'node:module';
import { fileURLToPath } from 'node:url';
@@ -32,6 +32,19 @@ async function entries(root) {
return result;
}
export async function generatedSymlinkManifest(nextDir) {
const links = [];
for (const target of (await entries(nextDir)).sort()) {
const stats = await lstat(target);
if (!stats.isSymbolicLink()) continue;
links.push({
path: path.relative(nextDir, target).split(path.sep).join('/'),
target: await readlink(target),
});
}
return `${JSON.stringify({ version: 1, links })}\n`;
}
const webSourceRoots = (root) => [
path.join(root, 'apps', 'web', 'src'),
path.join(root, 'apps', 'web', 'public'),
@@ -150,7 +163,14 @@ export async function runPreflight({ root = process.cwd(), uid = process.getuid?
const nextDir = path.join(root, 'apps', 'web', '.next');
let generated = [];
try {
await lstat(nextDir);
const nextStats = await lstat(nextDir);
if (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',
};
}
generated = [nextDir, ...(await entries(nextDir))];
} catch (error) {
if (error.code !== 'ENOENT') throw error;
@@ -158,25 +178,55 @@ export async function runPreflight({ root = process.cwd(), uid = process.getuid?
if (generated.length > 0) {
const foreign = [];
if (uid !== undefined) {
for (const target of generated) {
if ((await lstat(target)).uid !== uid) foreign.push(path.relative(root, target));
}
for (const target of generated) {
const stats = await lstat(target);
if (uid !== undefined && stats.uid !== uid) foreign.push(path.relative(root, target));
}
let generatedFingerprint = null;
// 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.
let certification = null;
let certifiedManifest = null;
try {
generatedFingerprint = (
await readFile(path.join(nextDir, '.mosaic-source-hash'), 'utf8')
).trim();
const [certificationContents, manifestContents] = await Promise.all([
readFile(path.join(nextDir, '.mosaic-source-hash'), 'utf8'),
readFile(path.join(nextDir, '.mosaic-symlink-manifest'), 'utf8'),
]);
try {
const parsed = JSON.parse(certificationContents);
if (
parsed.version === 1 &&
typeof parsed.sourceFingerprint === 'string' &&
typeof parsed.symlinkManifestHash === 'string'
) {
certification = parsed;
certifiedManifest = manifestContents;
}
} catch {
// Invalid certification is handled as untrusted generated state below.
}
} catch (error) {
if (error.code !== 'ENOENT') throw error;
}
const stale = generatedFingerprint !== (await sourceFingerprint(root));
if (foreign.length > 0 || stale) {
const stale = certification?.sourceFingerprint !== (await sourceFingerprint(root));
const actualManifest = await generatedSymlinkManifest(nextDir);
const certifiedManifestHash =
certifiedManifest === null
? null
: createHash('sha256').update(certifiedManifest).digest('hex');
const changedSymlinks =
certification?.symlinkManifestHash !== certifiedManifestHash ||
certifiedManifest !== actualManifest;
if (foreign.length > 0 || stale || changedSymlinks) {
const reasons = [
foreign.length > 0 ? `foreign-owned paths: ${foreign.slice(0, 3).join(', ')}` : '',
stale ? 'generated source fingerprint does not match web source/configuration' : '',
changedSymlinks
? 'generated symbolic-link manifest does not match the certified build'
: '',
].filter(Boolean);
return {
code: GENERATED_STATE_EXIT,