ww: protect public install outputs

This commit is contained in:
2026-08-21 04:11:01 +09:00
parent ce91f6662c
commit 1b71250ad5
7 changed files with 1064 additions and 90 deletions

View File

@@ -12443,7 +12443,6 @@ fn runtimepath(relative: str) str = {
let rollbackdiag: str = "";
let missingdiag: str = "";
let occupieddiag: str = "";
let failurebytes: str = "";
let persistbasebytes: str = "";
let persistchangedbytes: str = "";
let baseenv: []str = os.getenvs();
@@ -12595,29 +12594,23 @@ fn runtimepath(relative: str) str = {
" [no tests]\n")));
assert(out.stderr.len == 0 && !os.exists(noneroot));
// Publication commits before execution. A failing test therefore leaves
// a runnable retained binary while its product failure stays on stdout.
// Pinned builderTest makes the install action depend on the run action.
// A failing run therefore preserves a prior destination and never enters
// the retained-output overwrite guard.
let failurebin: str = strings.concat(root, "/failure-", tags[si],
".test");
writefile(failurebin, "failure-sentinel\n");
let failureav: []str = [driver(stages[si]), "test", "-I", suite,
"-o", failurebin, failed];
runcommandenvdir(root, strings.concat("publish-runtime-", tags[si]),
failureav, env, root,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(os.exists(failurebin) && !os.exists(strings.concat(failurebin,
".new")));
assert(same(readfile(failurebin), "failure-sentinel\n"));
assert(!os.exists(strings.concat(failurebin, ".new")));
assert(has(out.stdout,
"failed_decl.runtime_failure ... FAIL (exit 1)\n"));
assert(out.stderr.len == 0);
let fi: os.filestat;
match (os.stat(&fi, failurebin)) {
case void => void;
case let e: os.oserror => abort("retained failure binary missing");
};
assert(((fi.mode: u32) & 73u32) != 0u32);
if (si == 0) { failurebytes = readfile(failurebin); }
else { assert(same(failurebytes, readfile(failurebin))); };
// -w keeps semantic actions persistent while -c and -o remain execution
// and presentation policy. Compile-only can cold-build and invalidate;
@@ -12681,6 +12674,377 @@ fn runtimepath(relative: str) str = {
clean(root);
};
// Go 1.26.5's BuildInstallFunc protects caller data at the install boundary:
// after producers, and after a successful run for retained running tests.
// Exercise that one predicate through direct, raw, package, and test routes.
@test fn public_output_overwrite_safety() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let dep: str = strings.concat(source, "/lib/dep");
let app: str = strings.concat(source, "/cmd/app");
let bad: str = strings.concat(source, "/cmd/bad");
let checked: str = strings.concat(source, "/pkg/check");
let failed: str = strings.concat(source, "/pkg/failed");
let invocation: str = strings.concat(root, "/invoke");
mkdirall(dep); mkdirall(app); mkdirall(bad); mkdirall(checked);
mkdirall(failed); mkdirall(invocation);
writefile(strings.concat(dep, "/dep.ww"), strings.concat(
"package renamed;\n",
"export fn value() int = { return 9; };\n"));
let appsource: str = strings.concat(app, "/main.ww");
let appbase: str = strings.concat(
"package main;\n",
"import stable lib.dep;\n",
"export fn main() int = { return stable.value(); };\n");
let appchanged: str = strings.concat(
"package main;\n",
"import stable lib.dep;\n",
"export fn main() int = { return stable.value() + 1; };\n");
writefile(appsource, appbase);
writefile(strings.concat(bad, "/main.ww"), strings.concat(
"package main;\n",
"import missing nowhere.present;\n",
"export fn main() int = { return missing.value(); };\n"));
writefile(strings.concat(checked, "/check.ww"),
"package checked;\nfn value() int = { return 4; };\n");
writefile(strings.concat(checked, "/check_test.ww"), strings.concat(
"package checked;\n",
"@test fn guarded_run() void = { assert(value() == 4); };\n"));
writefile(strings.concat(failed, "/failed_test.ww"), strings.concat(
"package failed;\n",
"@test fn failed_run() void = { assert(false); };\n"));
let raw: str = strings.concat(root, "/raw.ww");
writefile(raw,
"package main;\nexport fn main() int = { return 3; };\n");
let linkerwrapper: str = strings.concat(root, "/overwrite-w6l.sh");
writeexecutable(linkerwrapper, strings.concat(
"#!/bin/sh\n",
"printf 'link\\n' >> \"$WW_OVERWRITE_TRACE\"\n",
"exec \"$WW_OVERWRITE_REAL_L\" \"$@\"\n"));
let drivers: []str = ["ww", "ww_ww"];
let linkers: []str = ["w6l", "w6l_ww"];
let tags: []str = ["c", "ww"];
let commandout: str = strings.concat(root, "/command.bin");
let rawout: str = strings.concat(root, "/raw.bin");
let defaultout: str = strings.concat(invocation, "/app");
let libout: str = strings.concat(root, "/library.a");
let testbin: str = strings.concat(root, "/check.test");
let symlinktarget: str = strings.concat(root, "/symlink-target");
let symlinkout: str = strings.concat(root, "/symlink.bin");
let fifoout: str = strings.concat(root, "/fifo.bin");
writefile(symlinktarget, "symlink-sentinel\n");
assert(os.symlink(symlinktarget, symlinkout) == 0);
let builddir: str = strings.concat(root, "/build-output");
let buildchild: str = strings.concat(builddir, "/app");
let testdir: str = strings.concat(root, "/test-output");
let testchild: str = strings.concat(testdir, "/check.test");
mkdirall(buildchild); mkdirall(testchild);
writefile(strings.concat(buildchild, "/marker"), "build-directory\n");
writefile(strings.concat(testchild, "/marker"), "test-directory\n");
let commanddiag: str = "";
let rawdiag: str = "";
let defaultdiag: str = "";
let symlinkdiag: str = "";
let builddirdiag: str = "";
let librarydiag: str = "";
let libraryarchivediag: str = "";
let testcompilediag: str = "";
let testrundiag: str = "";
let testdirdiag: str = "";
let loaddiag: str = "";
let commandbytes: str = "";
let librarybytes: str = "";
let interfacebytes: str = "";
let testbytes: str = "";
let unitbytes: str = "";
let baseenv: []str = os.getenvs();
let out: commandout;
let si: i32 = 0;
for (si < drivers.len) {
let trace: str = strings.concat(root, "/overwrite-", tags[si],
".trace");
writefile(trace, "");
let env: []str = alloc([], (baseenv.len + 3): u64)!;
let ei: i32 = 0;
for (ei < baseenv.len) {
if (!strings.hasprefix(baseenv[ei], "WW_W6L=")
&& !strings.hasprefix(baseenv[ei], "WW_OVERWRITE_TRACE=")
&& !strings.hasprefix(baseenv[ei], "WW_OVERWRITE_REAL_L=")) {
append(env, baseenv[ei]);
};
ei += 1;
};
append(env, strings.concat("WW_W6L=", linkerwrapper));
append(env, strings.concat("WW_OVERWRITE_TRACE=", trace));
append(env, strings.concat("WW_OVERWRITE_REAL_L=",
driver(linkers[si])));
let work: str = strings.concat(root, "/command-work-", tags[si]);
let badwork: str = strings.concat(root, "/bad-work-", tags[si]);
let rawwork: str = strings.concat(root, "/raw-work-", tags[si]);
let defaultwork: str = strings.concat(root, "/default-work-", tags[si]);
let symlinkwork: str = strings.concat(root, "/symlink-work-", tags[si]);
let fifowork: str = strings.concat(root, "/fifo-work-", tags[si]);
let dirwork: str = strings.concat(root, "/dir-work-", tags[si]);
let libwork: str = strings.concat(root, "/lib-work-", tags[si]);
let testwork: str = strings.concat(root, "/test-work-", tags[si]);
let testdirwork: str = strings.concat(root, "/test-dir-work-", tags[si]);
mkdirall(work); mkdirall(badwork); mkdirall(rawwork);
mkdirall(defaultwork); mkdirall(symlinkwork); mkdirall(fifowork);
mkdirall(dirwork);
mkdirall(libwork); mkdirall(testwork); mkdirall(testdirwork);
// A nonempty caller file rejects only after the linker ran, and a cold
// persistent generation remains unpublished.
if (os.exists(commandout)) { assert(os.remove(commandout) == 0); };
writefile(commandout, "caller-command\n");
rewritefile(trace, "");
let commandav: []str = [driver(drivers[si]), "build", "-w", work,
"-I", source, "-o", commandout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-command-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, strings.concat("ww: build output \"", commandout,
"\" already exists and is not an object file\n")));
assert(same(readfile(commandout), "caller-command\n"));
assert(permissionmode(commandout) == 384u32);
assert(readfile(trace).len != 0 && directoryisempty(work));
assert(!os.exists(strings.concat(commandout, ".new"))
&& !directoryhasfragment(root, "command.bin.wwtxn."));
if (si == 0) { commanddiag = strings.dup(out.stderr); }
else { assert(same(commanddiag, out.stderr)); };
// Package/import failure keeps load precedence and starts no linker.
rewritefile(trace, "");
let badav: []str = [driver(drivers[si]), "build", "-w", badwork,
"-I", source, "-o", commandout, "cmd.bad"];
runcommandenv(root, strings.concat("overwrite-load-", tags[si]), badav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "cannot find package nowhere.present"));
assert(!has(out.stderr, "already exists and is not an object file"));
assert(readfile(trace).len == 0 && directoryisempty(badwork));
assert(same(readfile(commandout), "caller-command\n"));
if (si == 0) { loaddiag = strings.dup(out.stderr); }
else { assert(same(loaddiag, out.stderr)); };
// Empty reservations and recognized ELF outputs are replaceable. A
// rejected invalidation preserves the prior persistent generation.
rewritefile(commandout, ""); rewritefile(trace, "");
runcommandenv(root, strings.concat("overwrite-empty-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let built: str = readfile(commandout);
assert(strings.hasprefix(built, "\x7fELF"));
let unit: str = readfile(strings.concat(work, "/cmd.app.unit.ww"));
if (si == 0) {
commandbytes = strings.dup(built);
unitbytes = strings.dup(unit);
} else {
assert(same(commandbytes, built) && same(unitbytes, unit));
};
rewritefile(appsource, appchanged);
rewritefile(commandout, "caller-after-change\n");
rewritefile(trace, "");
runcommandenv(root, strings.concat("overwrite-invalidated-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(readfile(trace).len != 0);
assert(same(readfile(commandout), "caller-after-change\n"));
assert(same(readfile(strings.concat(work, "/cmd.app.unit.ww")), unit));
rewritefile(appsource, appbase);
rewritefile(commandout, built);
runcommandenv(root, strings.concat("overwrite-elf-", tags[si]),
commandav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let runav: []str = [commandout];
runcommand(root, strings.concat("overwrite-command-run-", tags[si]),
runav, (10i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 9);
// Default, raw, symlink-following, and output-directory child routes
// use the same late guard and never strand rollback backups.
if (os.exists(defaultout)) { rewritefile(defaultout, "default-sentinel\n"); }
else { writefile(defaultout, "default-sentinel\n"); };
let defaultav: []str = [driver(drivers[si]), "build", "-w",
defaultwork, "-I", source, "cmd.app"];
runcommandenvdir(root, strings.concat("overwrite-default-", tags[si]),
defaultav, env, invocation,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(defaultout), "default-sentinel\n"));
if (si == 0) { defaultdiag = strings.dup(out.stderr); }
else { assert(same(defaultdiag, out.stderr)); };
if (os.exists(rawout)) { rewritefile(rawout, "raw-sentinel\n"); }
else { writefile(rawout, "raw-sentinel\n"); };
let rawav: []str = [driver(drivers[si]), "build", "-w", rawwork,
"-o", rawout, raw];
runcommandenv(root, strings.concat("overwrite-raw-", tags[si]), rawav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(rawout), "raw-sentinel\n"));
if (si == 0) { rawdiag = strings.dup(out.stderr); }
else { assert(same(rawdiag, out.stderr)); };
let symlinkav: []str = [driver(drivers[si]), "build", "-w",
symlinkwork, "-I", source, "-o", symlinkout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-symlink-", tags[si]),
symlinkav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(symlinkout), "symlink-sentinel\n"));
if (si == 0) { symlinkdiag = strings.dup(out.stderr); }
else { assert(same(symlinkdiag, out.stderr)); };
if (os.exists(fifoout)) { assert(os.remove(fifoout) == 0); };
let fifoav: []str = ["/usr/bin/mkfifo", fifoout];
runcommand(root, strings.concat("overwrite-mkfifo-", tags[si]), fifoav,
(10i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let fifobuildav: []str = [driver(drivers[si]), "build", "-w",
fifowork, "-I", source, "-o", fifoout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-fifo-", tags[si]),
fifobuildav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(strings.hasprefix(readfile(fifoout), "\x7fELF"));
let dirav: []str = [driver(drivers[si]), "build", "-w", dirwork,
"-I", source, "-o", strings.concat(builddir, "/"), "cmd.app"];
runcommandenv(root, strings.concat("overwrite-build-dir-", tags[si]),
dirav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "already exists and is a directory"));
assert(same(readfile(strings.concat(buildchild, "/marker")),
"build-directory\n"));
assert(!directoryhasfragment(builddir, ".wwtxn."));
if (si == 0) { builddirdiag = strings.dup(out.stderr); }
else { assert(same(builddirdiag, out.stderr)); };
// Archive and WW-interface outputs each participate in the guard. An
// ordinary repeat accepts both recognized prior outputs; arbitrary
// interface or archive text rejects without changing either member.
if (os.exists(libout)) { assert(os.remove(libout) == 0); };
if (os.exists(strings.concat(libout, ".wwi"))) {
assert(os.remove(strings.concat(libout, ".wwi")) == 0);
};
let libav: []str = [driver(drivers[si]), "build", "-w", libwork,
"-I", source, "-o", libout, "lib.dep"];
runcommandenv(root, strings.concat("overwrite-library-new-", tags[si]),
libav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let archive: str = readfile(libout);
let iface: str = readfile(strings.concat(libout, ".wwi"));
assert(strings.hasprefix(archive, "!<arch>\n"));
assert(strings.hasprefix(iface, "//ww:module "));
runcommandenv(root, strings.concat("overwrite-library-repeat-", tags[si]),
libav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(archive, readfile(libout))
&& same(iface, readfile(strings.concat(libout, ".wwi"))));
if (si == 0) {
librarybytes = strings.dup(archive);
interfacebytes = strings.dup(iface);
} else {
assert(same(librarybytes, archive)
&& same(interfacebytes, iface));
};
rewritefile(strings.concat(libout, ".wwi"), "caller-interface\n");
runcommandenv(root, strings.concat("overwrite-library-reject-", tags[si]),
libav, env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(libout), archive));
assert(same(readfile(strings.concat(libout, ".wwi")),
"caller-interface\n"));
if (si == 0) { librarydiag = strings.dup(out.stderr); }
else { assert(same(librarydiag, out.stderr)); };
rewritefile(libout, "caller-archive\n");
rewritefile(strings.concat(libout, ".wwi"), iface);
runcommandenv(root, strings.concat("overwrite-library-archive-",
tags[si]), libav, env,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(libout), "caller-archive\n"));
assert(same(readfile(strings.concat(libout, ".wwi")), iface));
if (si == 0) { libraryarchivediag = strings.dup(out.stderr); }
else { assert(same(libraryarchivediag, out.stderr)); };
// Compile-only installs check after linking. Running retention executes
// the private test first; a successful run then checks, while a failed
// run never installs or diagnoses the caller destination.
if (os.exists(testbin)) { rewritefile(testbin, "caller-test\n"); }
else { writefile(testbin, "caller-test\n"); };
let testcompileav: []str = [driver(drivers[si]), "test", "-c", "-w",
testwork, "-I", source, "-o", testbin, "pkg.check"];
runcommandenv(root, strings.concat("overwrite-test-c-", tags[si]),
testcompileav, env,
(90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(same(readfile(testbin), "caller-test\n"));
if (si == 0) { testcompilediag = strings.dup(out.stderr); }
else { assert(same(testcompilediag, out.stderr)); };
rewritefile(testbin, "");
runcommandenv(root, strings.concat("overwrite-test-empty-", tags[si]),
testcompileav, env,
(90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let compiledtest: str = readfile(testbin);
assert(strings.hasprefix(compiledtest, "\x7fELF"));
if (si == 0) { testbytes = strings.dup(compiledtest); }
else { assert(same(testbytes, compiledtest)); };
rewritefile(testbin, "caller-running-test\n");
let testrunav: []str = [driver(drivers[si]), "test", "-w", testwork,
"-I", source, "-o", testbin, "pkg.check"];
runcommandenv(root, strings.concat("overwrite-test-run-", tags[si]),
testrunav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stdout, "checked.guarded_run ... ok\n"));
assert(same(readfile(testbin), "caller-running-test\n"));
if (si == 0) { testrundiag = strings.dup(out.stderr); }
else { assert(same(testrundiag, out.stderr)); };
rewritefile(testbin, compiledtest);
runcommandenv(root, strings.concat("overwrite-test-elf-", tags[si]),
testrunav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(has(out.stdout, "checked.guarded_run ... ok\n"));
let beforefailure: str = readfile(testbin);
let failav: []str = [driver(drivers[si]), "test", "-w", testwork,
"-I", source, "-o", testbin, "pkg.failed"];
runcommandenv(root, strings.concat("overwrite-test-fail-", tags[si]),
failav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stdout, "failed.failed_run ... FAIL (exit 1)\n"));
assert(out.stderr.len == 0 && same(readfile(testbin), beforefailure));
assert(!os.exists(strings.concat(testbin, ".new"))
&& !os.exists(strings.concat(testbin, ".install")));
let testdirav: []str = [driver(drivers[si]), "test", "-c", "-w",
testdirwork, "-I", source, "-o", strings.concat(testdir, "/"),
"pkg.check"];
runcommandenv(root, strings.concat("overwrite-test-dir-", tags[si]),
testdirav, env, (90i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "already exists and is a directory"));
assert(same(readfile(strings.concat(testchild, "/marker")),
"test-directory\n"));
assert(!directoryhasfragment(testdir, ".wwtxn."));
if (si == 0) { testdirdiag = strings.dup(out.stderr); }
else { assert(same(testdirdiag, out.stderr)); };
// Assembly and exact-null routes have no public install action.
let asmout: str = strings.concat(root, "/assembly-", tags[si]);
writefile(asmout, "assembly-sentinel\n");
let asmav: []str = [driver(drivers[si]), "build", "-S", "-w",
work, "-I", source, "-o", asmout, "cmd.app"];
runcommandenv(root, strings.concat("overwrite-asm-", tags[si]), asmav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(same(readfile(asmout), "assembly-sentinel\n"));
let nullav: []str = [driver(drivers[si]), "build", "-I", source,
"-o", "/dev/null", "cmd.app"];
runcommandenv(root, strings.concat("overwrite-null-", tags[si]), nullav,
env, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
si += 1;
};
clean(root);
};
// A single directory root used to bypass the package coordinator and treat
// every non-null -o spelling as one file. Exercise the Go output-directory
// branch at that dispatch boundary while the established graph transaction