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

@@ -1515,6 +1515,7 @@ type sepproduct = struct {
directoryproduct: bool,
notests: bool,
buildaction: bool, // loaded product retained in the action list
publicout: bool,
context: i32,
root: i32,
variantroot: i32,
@@ -6134,6 +6135,7 @@ type septxnentry = struct {
backup: *u8,
hadold: bool,
installed: bool,
publicoutput: bool,
};
fn sepalloctxnentries(cap: i32) ([]septxnentry | nomem) = {
@@ -6207,8 +6209,8 @@ fn septxnbackup(dst: *u8) *u8 = {
return buf.ptr;
};
fn septxnadd(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8) bool = {
fn septxnaddmode(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8, publicoutput: bool) bool = {
if (cstreq(stage, dst)) {
cerrpath("ww: transaction path collision: ", dst, "\n");
return false;
@@ -6253,10 +6255,21 @@ fn septxnadd(entries: *[]septxnentry, n: *i32,
(*entries)[*n].backup = backup;
(*entries)[*n].hadold = false;
(*entries)[*n].installed = false;
(*entries)[*n].publicoutput = publicoutput;
*n += 1;
return true;
};
fn septxnadd(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8) bool = {
return septxnaddmode(entries, n, stage, dst, false);
};
fn septxnaddpublic(entries: *[]septxnentry, n: *i32,
stage: *u8, dst: *u8) bool = {
return septxnaddmode(entries, n, stage, dst, true);
};
fn septxnaddpkgsuffix(entries: *[]septxnentry, n: *i32,
g: *sepgraph, pi: i32, scratch: *u8,
stagesuffix: str, dstsuffix: str) bool = {
@@ -6271,6 +6284,91 @@ fn septxndiscard(entries: []septxnentry, n: i32) void = {
for (i < n) { os.remove(pathstr(entries[i].stage)); i += 1; };
};
fn sepoutputprefix(buf: *u8, n: i64, magic: str) bool = {
if (n < 0i64 || n < magic.len: i64) { return false; };
let i: i32 = 0;
for (i < magic.len) {
if (buf[i] != magic.ptr[i]) { return false; };
i += 1;
};
return true;
};
fn sepoutput2(buf: *u8, n: i64, a: u8, b: u8) bool = {
return n >= 2i64 && buf[0u64] == a && buf[1u64] == b;
};
fn sepoutput4(buf: *u8, n: i64, a: u8, b: u8, c: u8, d: u8) bool = {
return n >= 4i64 && buf[0u64] == a && buf[1u64] == b
&& buf[2u64] == c && buf[3u64] == d;
};
fn sepoutput6(buf: *u8, n: i64, a: u8, b: u8, c: u8, d: u8,
e: u8, f: u8) bool = {
return n >= 6i64 && buf[0u64] == a && buf[1u64] == b
&& buf[2u64] == c && buf[3u64] == d
&& buf[4u64] == e && buf[5u64] == f;
};
// Go 1.26.5 work.objectMagic, plus WW's compiler-owned interface prefix.
fn sepisobjectoutput(path: *u8) bool = {
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
if (fd < 0) { return false; };
let buf: [64]u8;
let got: u64 = 0u64;
let bad: bool = false;
for (got < 64u64) {
let n: i64 = os.read(fd, &buf[0] + got, 64u64 - got);
if (n > 0) { got += n: u64; }
else if (n == 0) { break; }
else if (n != -4i64) { bad = true; break; };
};
os.close(fd);
if (bad) { return false; };
let n: i64 = got: i64;
if (sepoutputprefix(&buf[0], n, "!<arch>\n")
|| sepoutputprefix(&buf[0], n, "<bigaf>\n")
|| sepoutput4(&buf[0], n, 127u8, 69u8, 76u8, 70u8)
|| sepoutput4(&buf[0], n, 254u8, 237u8, 250u8, 206u8)
|| sepoutput4(&buf[0], n, 254u8, 237u8, 250u8, 207u8)
|| sepoutput4(&buf[0], n, 206u8, 250u8, 237u8, 254u8)
|| sepoutput4(&buf[0], n, 207u8, 250u8, 237u8, 254u8)
|| sepoutput6(&buf[0], n, 77u8, 90u8, 144u8, 0u8, 3u8, 0u8)
|| sepoutput6(&buf[0], n, 77u8, 90u8, 120u8, 0u8, 1u8, 0u8)
|| sepoutput4(&buf[0], n, 0u8, 0u8, 1u8, 235u8)
|| sepoutput4(&buf[0], n, 0u8, 0u8, 138u8, 151u8)
|| sepoutput4(&buf[0], n, 0u8, 0u8, 6u8, 71u8)
|| sepoutput4(&buf[0], n, 0u8, 97u8, 115u8, 109u8)
|| sepoutput2(&buf[0], n, 1u8, 223u8)
|| sepoutput2(&buf[0], n, 1u8, 247u8)
|| sepoutputprefix(&buf[0], n, "//ww:module ")) {
return true;
};
return false;
};
fn sepcheckdstoverwrite(dst: *u8) bool = {
let fi: os.filestat;
match (os.stat(&fi, pathstr(dst))) {
case void => {
let typ: u32 = (fi.mode: u32) & 61440u32;
if (typ == os.mode.DIR: u32) {
cerr("ww: build output "); sepputquoted(dst);
cerr(" already exists and is a directory\n");
return false;
};
if (typ == os.mode.REG: u32 && fi.sz != 0u64
&& !sepisobjectoutput(dst)) {
cerr("ww: build output "); sepputquoted(dst);
cerr(" already exists and is not an object file\n");
return false;
};
};
case let e: os.oserror => void;
};
return true;
};
fn septxncommit(entries: []septxnentry, n: i32) bool = {
let i: i32 = 0;
let valid: bool = true;
@@ -6283,6 +6381,17 @@ fn septxncommit(entries: []septxnentry, n: i32) bool = {
};
i += 1;
};
if (valid) {
i = 0;
for (i < n) {
if (entries[i].publicoutput
&& !sepcheckdstoverwrite(entries[i].dst)) {
valid = false;
break;
};
i += 1;
};
};
if (valid) {
i = 0;
for (i < n) {
@@ -6614,6 +6723,50 @@ fn septxnrelease(entries: *[]septxnentry, n: i32) void = {
entries.cap = 0;
};
// Running retained tests enter this Go-like install action only after their
// request-private executable has returned successfully.
fn sepinstalltestoutput(stage: *u8, dst: *u8) i32 = {
let installstage: *u8 = sepappendlit(stage, ".install");
if (installstage == nil) { return 1; };
if (cstrlen(installstage) + 1u64 > os.PATH_MAX: u64
|| pathexistsnofollow(installstage) != 0
|| copyexecutablestage(stage, installstage) != 0) {
cerr("ww: cannot stage retained test output\n");
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
let parent: *u8 = seplexicalparent(dst);
if (parent == nil) {
os.remove(pathstr(installstage));
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
let created: sepcreateddirs;
created.n = 0;
if (sepmkdirsrecord(parent, 511, &created) != 0) {
cerrpath("ww: cannot create test output directory ", parent, "\n");
os.free(parent: *void, cstrlen(parent) + 1u64);
os.remove(pathstr(installstage));
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
os.free(parent: *void, cstrlen(parent) + 1u64);
let entries: []septxnentry;
let n: i32 = 0;
if (!septxnaddpublic(&entries, &n, installstage, dst)
|| !septxncommit(entries, n)) {
septxndiscard(entries, n);
septxnrelease(&entries, n);
seprollbackdirs(&created);
os.remove(pathstr(installstage));
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 1;
};
septxnrelease(&entries, n);
os.free(installstage: *void, cstrlen(installstage) + 1u64);
return 0;
};
fn sepfinishfail(entries: *[]septxnentry, n: i32) i32 = {
septxndiscard(*entries, n);
septxnrelease(entries, n);
@@ -6924,19 +7077,24 @@ fn sepfinishrequest(selfdir: *u8, l6: *u8, c6: *u8, a6: *u8,
producti = 0;
for (producti < nproducts) {
if (products[producti].stageout != nil
&& !septxnadd(&entries, &ntxn, products[producti].stageout,
products[producti].out)) {
return sepfinishfail(&entries, ntxn);
&& ((products[producti].publicout
&& !septxnaddpublic(&entries, &ntxn,
products[producti].stageout, products[producti].out))
|| (!products[producti].publicout
&& !septxnadd(&entries, &ntxn,
products[producti].stageout, products[producti].out)))) {
return sepfinishfail(&entries, ntxn);
};
if (products[producti].stagepublish != nil
&& !septxnadd(&entries, &ntxn, products[producti].stagepublish,
&& !septxnaddpublic(&entries, &ntxn,
products[producti].stagepublish,
products[producti].publish)) {
return sepfinishfail(&entries, ntxn);
};
if (products[producti].stageiface != nil) {
let outiface: *u8 = sepappendlit(products[producti].out, ".wwi");
if (outiface == nil
|| !septxnadd(&entries, &ntxn,
|| !septxnaddpublic(&entries, &ntxn,
products[producti].stageiface, outiface)) {
return sepfinishfail(&entries, ntxn);
};
@@ -8213,7 +8371,8 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
objstem: *u8, incs: *u8, lf: *lflags, publishpackage: i32,
requirecommand: i32, istest: i32,
rootvariant: i32, testpackage: *u8, emitasm: i32,
keepscratch: i32, workdir: *u8, createoutputdir: *u8,
publicoutput: bool, keepscratch: i32, workdir: *u8,
createoutputdir: *u8,
defaultoutputdir: *u8, outputpatherror: bool) i32 = {
let scratch: *u8 = nil;
let g: *sepgraph = nil;
@@ -8235,6 +8394,7 @@ fn buildonesep(selfdir: *u8, src: *u8, entryisdir: i32,
product.directoryproduct = false;
product.notests = false;
product.buildaction = true;
product.publicout = publicoutput;
product.root = -1;
product.variantroot = -1;
product.support = -1;
@@ -8691,7 +8851,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let rc: i32 = buildonesep(selfdir, resolved, isdir, rootidentity,
outp, outp, incs.ptr, &lf,
0i32, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, 0i32, workdir, nil, nil, false);
emitasm, false, 0i32, workdir, nil, nil, false);
let cleanbad: bool = false;
let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) {
@@ -8710,7 +8870,7 @@ fn dobuild(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
return buildonesep(selfdir, resolved, isdir, rootidentity,
out, objstem, incs.ptr, &lf,
publishpackage, 0i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, 1i32, workdir, createoutputdir, defaultoutputdir,
emitasm, true, 1i32, workdir, createoutputdir, defaultoutputdir,
outputpatherror);
};
@@ -8893,7 +9053,7 @@ fn dorun(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (buildonesep(selfdir, resolved, isdir, rootidentity,
outp, outp, incs.ptr, &lf,
0i32, 1i32, 0i32, SEP_VARIANT_PRODUCTION, nil,
0i32, 0i32, nil, nil, nil, false) != 0) {
0i32, false, 0i32, nil, nil, nil, false) != 0) {
let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) {
cerr("ww: cannot remove temporary output\n");
@@ -8958,10 +9118,12 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
let owntmp: bool = false;
let retainout: bool = outstem != nil
&& !cstreqlit(outstem, "/dev/null");
if (retainout) {
let deferredinstall: bool = retainout && compileonly == 0
&& emitasm == 0;
if (retainout && !deferredinstall) {
outp = outstem;
objstem = outstem;
} else { if (workdir != nil) {
} else { if (workdir != nil && !deferredinstall) {
// The workdir owns the persistent test binary the same way it
// owns the package artifacts.
outp = joinpathlit(workdir, "main");
@@ -8975,6 +9137,7 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
};
outp = joinpathlit(tmp.ptr, "main");
objstem = outp;
if (retainout) { objstem = outstem; };
}; };
// E3-C1: separate compilation is the sole build path (task #87).
let lf: lflags;
@@ -8986,7 +9149,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
if (retainout) { keep = 1; };
let bres: i32 = buildonesep(selfdir, src, 0, nil, outp, objstem, incs, &lf,
0i32, 0i32, 1i32, SEP_VARIANT_PRODUCTION, nil,
emitasm, keep, workdir, nil, nil, false);
emitasm, retainout && !deferredinstall, keep, workdir,
nil, nil, false);
if (bres != 0) {
if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp));
@@ -9037,6 +9201,8 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
rc = -1;
};
}; };
if (rc == 0 && deferredinstall
&& sepinstalltestoutput(outp, outstem) != 0) { rc = 1; };
if (owntmp) {
let cleanrc: i32 = os.remove(pathstr(outp));
if (cleanrc != 0 && cleanrc != -2i32) {
@@ -9052,6 +9218,10 @@ fn runsingletest(selfdir: *u8, src: *u8, incs: *u8, compileonly: i32,
};
fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
if (argc - start == 3
&& cstreqlit(argv[start], "--ww-install-test-output")) {
return sepinstalltestoutput(argv[start + 1], argv[start + 2]);
};
// -I parsing mirrors dobuild/dorun so a single-file test can resolve
// transitive imports (e.g. 905_nkname asttest → tok); coupled to the
// -T flip (task #5/#10).
@@ -9203,7 +9373,10 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
let publish: *u8 = argv[i + 8];
let status: *u8 = argv[i + 9];
let pn: u64 = cstrlen(name);
let buildproduct: bool = cstreqlit(kind, "build");
let publicbuildproduct: bool =
cstreqlit(kind, "build-public");
let buildproduct: bool = cstreqlit(kind, "build")
|| publicbuildproduct;
let testproduct: bool = cstreqlit(kind, "test");
let hasproduction: bool = !cstreqlit(production, "-");
let hasinternal: bool = !cstreqlit(internal, "-");
@@ -9248,6 +9421,7 @@ fn dotest(selfdir: *u8, argv: **u8, argc: i32, start: i32) i32 = {
product.directoryproduct = true;
product.notests = testproduct && !hasinternal && !hasexternal;
product.buildaction = true;
product.publicout = publicbuildproduct;
product.root = -1;
product.variantroot = -1;
product.productionroot = -1;