ww build: honor Go output permission modes

This commit is contained in:
2026-08-20 22:03:09 +09:00
parent 77168fd891
commit 84f5e68709
8 changed files with 513 additions and 9 deletions

View File

@@ -101,6 +101,7 @@ EXEC_TEST_C = $(BIN)/test_exec
EXEC_TEST_WW = $(BIN)/test_exec_ww EXEC_TEST_WW = $(BIN)/test_exec_ww
PACKAGE_TEST_BIN = $(BIN)/test_package PACKAGE_TEST_BIN = $(BIN)/test_package
PACKAGE_UMASKEXEC_BIN = $(BIN)/package-umaskexec
PACKAGE_TEST_SRC = test/package/package_test.ww lib/os/exec/exec.ww \ PACKAGE_TEST_SRC = test/package/package_test.ww lib/os/exec/exec.ww \
lib/test/run.ww lib/fnmatch/fnmatch.ww \ lib/test/run.ww lib/fnmatch/fnmatch.ww \
lib/ascii/ascii.ww lib/bytes/bytes.ww \ lib/ascii/ascii.ww lib/bytes/bytes.ww \
@@ -338,7 +339,11 @@ $(EXEC_TEST_WW): $(EXEC_TEST_SRC) $(WWFIXTURE_TOOLS) | $(BIN)
-I $(CURDIR)/internal $(CURDIR)/test/wwfixture/process/main.ww -I $(CURDIR)/internal $(CURDIR)/test/wwfixture/process/main.ww
@mv $(WWBUILD)/test_exec_ww/main $@ @mv $(WWBUILD)/test_exec_ww/main $@
$(PACKAGE_UMASKEXEC_BIN): test/package/umaskexec.c | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(PACKAGE_TEST_BIN): $(PACKAGE_TEST_SRC) $(WWTEST_BIN) \ $(PACKAGE_TEST_BIN): $(PACKAGE_TEST_SRC) $(WWTEST_BIN) \
$(PACKAGE_UMASKEXEC_BIN) \
$(BIN)/ww $(BIN)/ww_ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww $(BIN)/ww_ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN) $(LIB)/libwwrt.a | $(BIN)
@mkdir -p $(WWBUILD)/test_package @mkdir -p $(WWBUILD)/test_package
@@ -348,7 +353,8 @@ $(PACKAGE_TEST_BIN): $(PACKAGE_TEST_SRC) $(WWTEST_BIN) \
test-package: $(PACKAGE_TEST_BIN) $(WWTEST_BIN) $(BIN)/ww $(BIN)/ww_ww \ test-package: $(PACKAGE_TEST_BIN) $(WWTEST_BIN) $(BIN)/ww $(BIN)/ww_ww \
$(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \ $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a $(BIN)/w6l $(BIN)/w6l_ww $(PACKAGE_UMASKEXEC_BIN) \
$(LIB)/libwwrt.a
@WW_PACKAGE_REPO=$(CURDIR) $(CURDIR)/$(PACKAGE_TEST_BIN) -timeout-ms=120000 @WW_PACKAGE_REPO=$(CURDIR) $(CURDIR)/$(PACKAGE_TEST_BIN) -timeout-ms=120000
# ---- directories ------------------------------------------------------- # ---- directories -------------------------------------------------------

View File

@@ -8,8 +8,8 @@
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
#include <fcntl.h>
#include <unistd.h> #include <unistd.h>
#include <sys/stat.h>
/* `path` is acceptable iff it's either an archive ("!<arch>\n") or /* `path` is acceptable iff it's either an archive ("!<arch>\n") or
* an ELF file ("\x7fELF"). Distros often ship lib<name>.so as a GNU * an ELF file ("\x7fELF"). Distros often ship lib<name>.so as a GNU
@@ -121,17 +121,23 @@ main(int argc, char **argv)
return 1; return 1;
} }
FILE *f = fopen(out, "wb"); /* BuildInstallFunc creates ordinary linked commands with 0777 and lets the
* caller's umask select the installed mode. Public ww builds link into a
* fresh transaction stage, whose inode is later renamed to the output. */
int fd = open(out, O_WRONLY | O_CREAT | O_TRUNC, 0777);
if (fd < 0) {
fprintf(stderr, "w6l: cannot open %s\n", out);
return 1;
}
FILE *f = fdopen(fd, "wb");
if (f == NULL) { if (f == NULL) {
close(fd);
(void)unlink(out);
fprintf(stderr, "w6l: cannot open %s\n", out); fprintf(stderr, "w6l: cannot open %s\n", out);
return 1; return 1;
} }
int rc = l_emit_elf(&l, f, base, base + 0x1000 + entry->val); int rc = l_emit_elf(&l, f, base, base + 0x1000 + entry->val);
fclose(f); fclose(f);
if (rc == 0 && chmod(out, 0755) != 0) {
fprintf(stderr, "w6l: cannot make %s executable\n", out);
rc = 1;
}
free(inputs); free(inputs);
return rc; return rc;
} }

View File

@@ -6308,6 +6308,103 @@ residue. Existing package tests continue to own all action/test variants,
graph identity, output ordering, cwd/environment/stdin, timeout, and broader graph identity, output ordering, cwd/environment/stdin, timeout, and broader
transaction behavior. transaction behavior.
### 11.28 Implemented Go-like build-output permissions
Newly published build outputs now use Go 1.26.5's output-kind permission and
caller-umask contract. An ordinary linked command starts from `0777`; a
non-link archive starts from `0666`. The kernel filters either base permission
through the invoking process's umask when the request-private publication inode
is created. WW's required adjacent interface sidecar is data like its archive
and uses the same `0666` base. Assembly-only builds create neither kind of
public output.
#### Pinned Go evidence and direct pre-fix measurements
The authority is official Go 1.26.5 at commit
`c19862e5f8415b4f24b189d065ed739517c548ba`:
- `runBuild` routes explicit single and directory `-o` products through
`ModeInstall`, after deciding the caller-visible output path
([`cmd/go/internal/work/build.go`, lines 459558](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/build.go#L459-L558)).
- `BuildInstallFunc` begins with permission `0666`, changes it to `0777` for an
ordinary link action, creates the output parent, and gives that permission to
`moveOrCopyFile`
([`cmd/go/internal/work/exec.go`, lines 19042000](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/exec.go#L1904-L2000)).
- On its rename path, `moveOrCopyFile` creates a destination-adjacent dummy with
the requested permission, observes the caller-filtered mode, removes the
dummy, applies that mode to the linked source, and renames it. Its copy
fallback creates the destination with the requested permission and therefore
receives the same kernel filtering
([`cmd/go/internal/work/shell.go`, lines 119220](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/internal/work/shell.go#L119-L220)).
- Official `build_output.txt` asserts executable default, explicit-file,
nested-file, trailing-directory, and existing-directory command outputs
([lines 744](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_output.txt#L7-L44)).
`build_multi_main.txt` exercises directory fan-out for two main packages and
a local command-line package
([lines 116](https://github.com/golang/go/blob/c19862e5f8415b4f24b189d065ed739517c548ba/src/cmd/go/testdata/script/build_multi_main.txt#L1-L16)).
The testdata anchors directly specify that the public command routes produce
executables. The exact `0777`/`0666` bases and caller filtering are implemented
by the pinned source. Therefore the resulting mode formula is source-derived;
it is not an observation of the installed host Go toolchain.
Before this slice, fresh public-driver measurements of one byte-identical
command gave mode `0755` from both stages under umask `000`. Under umask `077`,
Cstage still gave `0755` while WWstage gave `0700`. The C linker created through
`fopen` and unconditionally applied `chmod(0755)` after emission; the WW linker
created with base `0755`. Thus both discarded permitted group/other write bits,
and Cstage additionally reintroduced bits forbidden by a restrictive mask.
The non-link branch had a separate stage mismatch under the same official rule.
With umask `000`, Cstage published a byte-identical archive/interface pair as
`0666` while WWstage published it as `0644`; both became `0600` under umask
`077`. Cstage's fresh `fopen` data stage already had base `0666`, while
WWstage's data-copy stage explicitly used base `0644`.
#### Ownership, publication, and identity boundaries
The C and WW linkers now open every fresh linked output with base `0777`. The C
linker emits through the resulting descriptor instead of applying a fixed mode
afterward; the WW linker uses the same creation base. The WWstage driver's
archive/interface copy now uses `0666`, matching Cstage's existing data-file
creation. No driver independently reads or stores a umask.
For public build routes, the coordinator has already rejected an occupied or
dangling output `.new` before the selected linker or copy owner opens the
request-private stage. Creation therefore receives the current child process's
umask exactly once. The established transaction renames that same inode to the
caller-visible destination, so neither the final name nor replacement of an old
destination changes its mode. A failed compiler, assembler, archiver, linker,
stage, or installation preserves the old destination's bytes and mode and
removes all request stages. Directory fan-out gives each independent command
the same request-local rule; concurrent driver processes retain independent
umasks and publication paths.
Permission bits are presentation metadata, not semantic inputs. Package and
action identity, graph edges, declared names, physical directory metadata,
compiler/assembler/archive/link argv, `.wwi` contents, artifact bytes, and
persistence keys are unchanged. An unchanged warm request may reuse every
semantic action but still relinks or copies the requested public product so its
mode reflects the current invocation. Source invalidation changes the applicable
artifact bytes without changing the formula. Retained test-binary copying
remains a distinct output-policy path and already uses the same `0777` linked-
executable rule. No Go-style dummy is needed and no `-go-tmp-umask` residue is
created because WW links or copies directly into its already-private fresh
stage.
No persisted byte schema changed. Build workdir format remains `18`, test
workdir format remains `19`, and semantic storage remains `3`.
The WW-native owner `build_output_permissions_follow_umask` uses a test-only
exec launcher to arrange exact process umasks. It covers both Cstage and
WWstage; cold, warm, and invalidated persistent builds; explicit, default,
raw-file, and multi-command directory outputs; `0777`, `0700`, `0750`, and
`0770` command results; `0666` and `0600` archive/interface results;
assembly-only omission; retained test-binary non-regression; direct execution;
injected late-link request rollback over old files and modes; occupied-stage
rejection; simultaneous builds with different umasks; diagnostic parity;
artifact and binary byte identity; and absence of `.new` or umask-probe residue.
## 12. Candidate architectures and hard-gate decision ## 12. Candidate architectures and hard-gate decision
Five candidates were developed as coherent systems, not as feature bins. Five candidates were developed as coherent systems, not as feature bins.

View File

@@ -310,6 +310,15 @@ ImportPath = ident { "." ident } .
`fn main`; path and directory spelling do not classify commands. An ordinary `fn main`; path and directory spelling do not classify commands. An ordinary
import of a package declared `main` is rejected, except for the toolchain's import of a package declared `main` is rejected, except for the toolchain's
colocated external-test wiring. colocated external-test wiring.
- A newly published `ww build` command is created with permission `0777`
filtered by the invoking process's umask. A newly published non-command
archive, and the adjacent WW interface required to consume it, use `0666`
filtered by that same umask. These inode permissions are output metadata:
they do not enter canonical package or action identity, artifact bytes,
import binding, symbols, `.wwi` contents, or persistent invalidation. A warm
build therefore reuses unchanged semantic actions while refreshing the
caller-visible output with the current invocation's mode. Assembly-only
builds publish no executable or archive.
- Only names marked `export` (§5) are visible across module boundaries. - Only names marked `export` (§5) are visible across module boundaries.
Import paths remain unquoted and dotted. Grouped imports, quoted import paths, Import paths remain unquoted and dotted. Grouped imports, quoted import paths,

View File

@@ -302,8 +302,10 @@ export fn main(argc: i32, argv: **u8) i32 = {
return 1; return 1;
}; };
// Pinned Go's BuildInstallFunc creates an ordinary linked command with
// 0o777; open applies the invoking process's umask to this fresh stage.
let flags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC; let flags: os.flag = os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC;
let fd: i32 = os.open(pathstr(outpath), flags, 493i32); // 0o755 let fd: i32 = os.open(pathstr(outpath), flags, 511i32); // 0o777
if (fd < 0) { if (fd < 0) {
let m: str = "w6l: cannot open output\n"; let m: str = "w6l: cannot open output\n";
os.write(2, m.ptr, m.len: u64); os.write(2, m.ptr, m.len: u64);

View File

@@ -5896,8 +5896,10 @@ fn fileequal(a: *u8, b: *u8) bool = {
fn copyfilestage(src: *u8, dst: *u8) i32 = { fn copyfilestage(src: *u8, dst: *u8) i32 = {
let in: i32 = os.open(pathstr(src), os.flag.RDONLY, 0i32); let in: i32 = os.open(pathstr(src), os.flag.RDONLY, 0i32);
if (in < 0) { return -1; }; if (in < 0) { return -1; };
// BuildInstallFunc gives non-link outputs base mode 0o666; the fresh
// publication stage applies the invoking process's umask at creation.
let out: i32 = os.open(pathstr(dst), let out: i32 = os.open(pathstr(dst),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 438i32);
if (out < 0) { os.close(in); return -1; }; if (out < 0) { os.close(in); return -1; };
let buf: [65536]u8; let buf: [65536]u8;
let bad: bool = false; let bad: bool = false;

View File

@@ -62,6 +62,14 @@ fn readfile(path: str) str = {
return out; return out;
}; };
fn permissionmode(path: str) u32 = {
let fi: os.filestat;
match (os.stat(&fi, path)) {
case void => return (fi.mode: u32) & 511u32;
case let e: os.oserror => abort("stat failed");
};
};
fn writefile(path: str, content: str) void = { fn writefile(path: str, content: str) void = {
let fd: i32 = os.open(path, let fd: i32 = os.open(path,
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32); os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
@@ -12635,3 +12643,350 @@ fn runtimepath(relative: str) str = {
"/occupied/alpha.test.new")) == 0); "/occupied/alpha.test.new")) == 0);
clean(root); clean(root);
}; };
@test fn build_output_permissions_follow_umask() void = {
let root: str = fresh();
let source: str = strings.concat(root, "/source");
let alpha: str = strings.concat(source, "/alpha");
let fan: str = strings.concat(source, "/fan");
let fanalpha: str = strings.concat(fan, "/alpha");
let fanbeta: str = strings.concat(fan, "/beta");
let fanlib: str = strings.concat(fan, "/library");
let library: str = strings.concat(source, "/library");
let check: str = strings.concat(source, "/check");
mkdirall(alpha); mkdirall(fanalpha); mkdirall(fanbeta);
mkdirall(fanlib); mkdirall(library); mkdirall(check);
let alphafile: str = strings.concat(alpha, "/main.ww");
let alphabase: str = strings.concat(
"package main;\n",
"fn main() i32 = { return 21; };\n");
let alphachanged: str = strings.concat(
"package main;\n",
"fn main() i32 = { return 22; };\n");
writefile(alphafile, alphabase);
writefile(strings.concat(fanalpha, "/main.ww"),
"package main;\nfn main() i32 = { return 31; };\n");
writefile(strings.concat(fanbeta, "/main.ww"),
"package main;\nfn main() i32 = { return 32; };\n");
writefile(strings.concat(fanlib, "/library.ww"),
"package library;\nexport fn value() i32 = { return 33; };\n");
writefile(strings.concat(library, "/library.ww"),
"package library;\nexport fn value() i32 = { return 41; };\n");
let raw: str = strings.concat(source, "/raw.ww");
writefile(raw, "package main;\nfn main() i32 = { return 23; };\n");
writefile(strings.concat(check, "/check.ww"),
"package check;\nfn value() i32 = { return 1; };\n");
writefile(strings.concat(check, "/check_test.ww"), strings.concat(
"package check;\n",
"@test fn retained() void = { assert(value() == 1); };\n"));
let launcher: str = driver("package-umaskexec");
let stages: []str = ["ww", "ww_ww"];
let tags: []str = ["c", "ww"];
let linkers: []str = ["w6l", "w6l_ww"];
let coldref: str = "";
let changedref: str = "";
let rawref: str = "";
let defaultref: str = "";
let fanalpharef: str = "";
let fanbetaref: str = "";
let archiverefs: []str = ["", ""];
let testref: str = "";
let faildiagref: str = "";
let occupieddiagref: str = "";
let linkerwrapper: str = strings.concat(root, "/mode-w6l.sh");
writeexecutable(linkerwrapper, strings.concat(
"#!/bin/sh\n",
"for arg do case \"$arg\" in */beta.new)\n",
" printf 'injected build-mode linker failure\\n' >&2\n",
" exit 97;; esac; done\n",
"exec \"$WW_MODE_REAL_LINKER\" \"$@\"\n"));
let baseenv: []str = os.getenvs();
let si: i32 = 0;
for (si < stages.len) {
let work: str = strings.concat(root, "/work-", tags[si]);
mkdirall(work);
let binary: str = strings.concat(root, "/command-", tags[si]);
let coldav: []str = [launcher, "000", driver(stages[si]), "build",
"-w", work, "-I", source, "-o", binary, alpha];
let out: commandout;
runcommand(root, strings.concat("mode-cold-", tags[si]), coldav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
assert(permissionmode(binary) == 511u32);
assert(!os.exists(strings.concat(binary, ".new")));
assert(!os.exists(strings.concat(binary, "-go-tmp-umask")));
let coldbytes: str = readfile(binary);
let coldarchive: str = readfile(strings.concat(work, "/alpha.a"));
if (si == 0) { coldref = strings.dup(coldbytes); }
else { assert(same(coldref, coldbytes)); };
let runav: []str = [binary];
runcommand(root, strings.concat("mode-cold-run-", tags[si]), runav,
time.second, &out);
expectexit(&out, 21);
// Umask is request-local publication metadata: the warm graph stays
// byte-stable while replacing the same output with the current mode.
let warmav: []str = [launcher, "077", driver(stages[si]), "build",
"-w", work, "-I", source, "-o", binary, alpha];
runcommand(root, strings.concat("mode-warm-", tags[si]), warmav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(out.stdout.len == 0 && out.stderr.len == 0);
assert(permissionmode(binary) == 448u32);
assert(same(coldbytes, readfile(binary)));
assert(same(coldarchive, readfile(strings.concat(work, "/alpha.a"))));
assert(!directoryhasnew(work));
// Source invalidation changes the command bytes, not the creation rule.
rewritefile(alphafile, alphachanged);
let changedav: []str = [launcher, "027", driver(stages[si]), "build",
"-w", work, "-I", source, "-o", binary, alpha];
runcommand(root, strings.concat("mode-changed-", tags[si]), changedav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(binary) == 488u32);
let changedbytes: str = readfile(binary);
assert(!same(coldbytes, changedbytes));
if (si == 0) { changedref = strings.dup(changedbytes); }
else { assert(same(changedref, changedbytes)); };
let changedrun: []str = [binary];
runcommand(root, strings.concat("mode-changed-run-", tags[si]),
changedrun, time.second, &out);
expectexit(&out, 22);
rewritefile(alphafile, alphabase);
// A default command output and an explicit raw-file output use the same
// link-install permission, independent of their naming route.
let defaultdir: str = strings.concat(root, "/default-", tags[si]);
let defaultwork: str = strings.concat(root, "/default-work-", tags[si]);
mkdirall(defaultdir); mkdirall(defaultwork);
let defaultav: []str = [launcher, "000", driver(stages[si]),
"build", "-w", defaultwork, "-I", source, alpha];
runcommanddir(root, strings.concat("mode-default-", tags[si]),
defaultdir, defaultav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let defaultbin: str = strings.concat(defaultdir, "/alpha");
assert(permissionmode(defaultbin) == 511u32);
let defaultbytes: str = readfile(defaultbin);
if (si == 0) { defaultref = strings.dup(defaultbytes); }
else { assert(same(defaultref, defaultbytes)); };
let rawbin: str = strings.concat(root, "/raw-", tags[si]);
let rawav: []str = [launcher, "007", driver(stages[si]), "build",
"-o", rawbin, raw];
runcommand(root, strings.concat("mode-raw-", tags[si]), rawav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(rawbin) == 504u32);
let rawbytes: str = readfile(rawbin);
if (si == 0) { rawref = strings.dup(rawbytes); }
else { assert(same(rawref, rawbytes)); };
// Directory fan-out creates each command independently, skips the
// library product, and leaves no request-private publication residue.
let fanout: str = strings.concat(root, "/fanout-", tags[si]);
let fanwork: str = strings.concat(root, "/fanwork-", tags[si]);
mkdirall(fanout); mkdirall(fanwork);
let fanav: []str = [launcher, "027", driver(stages[si]), "build",
"-w", fanwork, "-I", source, "-o", fanout,
strings.concat(fan, "/...")];
runcommand(root, strings.concat("mode-fan-", tags[si]), fanav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
let fanalphabin: str = strings.concat(fanout, "/alpha");
let fanbetabin: str = strings.concat(fanout, "/beta");
assert(permissionmode(fanalphabin) == 488u32);
assert(permissionmode(fanbetabin) == 488u32);
assert(!os.exists(strings.concat(fanout, "/library")));
assert(!directoryhasnew(fanout) && !directoryhasnew(fanwork));
let fanalphabytes: str = readfile(fanalphabin);
let fanbetabytes: str = readfile(fanbetabin);
if (si == 0) {
fanalpharef = strings.dup(fanalphabytes);
fanbetaref = strings.dup(fanbetabytes);
} else {
assert(same(fanalpharef, fanalphabytes));
assert(same(fanbetaref, fanbetabytes));
};
// The non-link branch of BuildInstallFunc uses 0666. WW's required
// adjacent interface follows the archive's data-file permission.
let libwork: str = strings.concat(root, "/libwork-", tags[si]);
mkdirall(libwork);
let archive: str = strings.concat(root, "/library-", tags[si], ".a");
let libav: []str = [launcher, "000", driver(stages[si]), "build",
"-w", libwork, "-I", source, "-o", archive, library];
runcommand(root, strings.concat("mode-library-", tags[si]), libav,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(archive) == 438u32);
assert(permissionmode(strings.concat(archive, ".wwi")) == 438u32);
archiverefs[si] = strings.dup(readfile(archive));
let libwarm: []str = [launcher, "077", driver(stages[si]), "build",
"-w", libwork, "-I", source, "-o", archive, library];
runcommand(root, strings.concat("mode-library-warm-", tags[si]), libwarm,
(60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(archive) == 384u32);
assert(permissionmode(strings.concat(archive, ".wwi")) == 384u32);
assert(same(archiverefs[si], readfile(archive)));
assert(!os.exists(strings.concat(archive, ".new")));
assert(!os.exists(strings.concat(archive, ".wwi.new")));
// Test-binary retention is a separate publication branch, but the
// ordinary linked-executable rule remains identical and byte-stable.
let testbin: str = strings.concat(root, "/check-", tags[si], ".test");
let testav: []str = [launcher, "000", driver(stages[si]), "test",
"-c", "-I", source, "-o", testbin, check];
runcommand(root, strings.concat("mode-test-", tags[si]), testav,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(testbin) == 511u32);
let testbytes: str = readfile(testbin);
if (si == 0) { testref = strings.dup(testbytes); }
else { assert(same(testref, testbytes)); };
let testwarm: []str = [launcher, "077", driver(stages[si]), "test",
"-c", "-I", source, "-o", testbin, check];
runcommand(root, strings.concat("mode-test-warm-", tags[si]), testwarm,
(120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(permissionmode(testbin) == 448u32);
assert(same(testbytes, readfile(testbin)));
assert(!os.exists(strings.concat(testbin, ".new")));
// Assembly-only builds do not publish an inode to which the permission
// contract could apply.
let asmdir: str = strings.concat(root, "/asm-dir-", tags[si]);
let asmwork: str = strings.concat(root, "/asm-work-", tags[si]);
mkdirall(asmdir); mkdirall(asmwork);
let asmav: []str = [launcher, "000", driver(stages[si]), "build",
"-S", "-w", asmwork, "-I", source, alpha];
runcommanddir(root, strings.concat("mode-asm-", tags[si]), asmdir,
asmav, (60i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 0);
assert(!os.exists(strings.concat(asmdir, "/alpha")));
// A later link failure rolls back already staged siblings. Existing
// destination bytes and modes survive; neither transaction nor action
// stages remain.
let failout: str = strings.concat(root, "/failout-", tags[si]);
let failwork: str = strings.concat(root, "/failwork-", tags[si]);
mkdirall(failout); mkdirall(failwork);
writefile(strings.concat(failout, "/alpha"), "old-alpha\n");
writefile(strings.concat(failout, "/beta"), "old-beta\n");
let failenv: []str = alloc([], (baseenv.len + 2): u64)!;
let ei: i32 = 0;
for (ei < baseenv.len) {
if (!strings.hasprefix(baseenv[ei], "WW_W6L=")
&& !strings.hasprefix(baseenv[ei], "WW_MODE_REAL_LINKER=")) {
append(failenv, baseenv[ei]);
};
ei += 1;
};
append(failenv, strings.concat("WW_W6L=", linkerwrapper));
append(failenv, strings.concat("WW_MODE_REAL_LINKER=",
driver(linkers[si])));
let failav: []str = [launcher, "000", driver(stages[si]), "build",
"-w", failwork, "-I", source, "-o", failout,
strings.concat(fan, "/...")];
runcommandenv(root, strings.concat("mode-failure-", tags[si]), failav,
failenv, (120i64 * (time.second: i64)): time.duration, &out);
expectexit(&out, 1);
assert(has(out.stderr, "injected build-mode linker failure\n"));
assert(has(out.stderr, "ww: w6l failed\n"));
assert(same(readfile(strings.concat(failout, "/alpha")),
"old-alpha\n"));
assert(same(readfile(strings.concat(failout, "/beta")),
"old-beta\n"));
assert(permissionmode(strings.concat(failout, "/alpha")) == 384u32);
assert(permissionmode(strings.concat(failout, "/beta")) == 384u32);
assert(!directoryhasnew(failout) && !directoryhasnew(failwork));
let faildiag: str = normalizedtrace(out.stderr,
strings.concat(failwork, "/"), failout);
if (si == 0) { faildiagref = strings.dup(faildiag); }
else { assert(same(faildiagref, faildiag)); };
// An occupied caller-visible stage is rejected before production and
// leaves both the prior output and the occupied inode untouched.
let occupied: str = strings.concat(root, "/occupied");
let occupiedstage: str = strings.concat(occupied, ".new");
if (si == 0) {
writefile(occupied, "old-output\n");
writefile(occupiedstage, "occupied-stage\n");
};
let occupiedav: []str = [launcher, "000", driver(stages[si]),
"build", "-I", source, "-o", occupied, alpha];
runcommand(root, strings.concat("mode-occupied-", tags[si]), occupiedav,
time.second, &out);
expectexit(&out, 1);
assert(same(readfile(occupied), "old-output\n"));
assert(same(readfile(occupiedstage), "occupied-stage\n"));
assert(permissionmode(occupied) == 384u32);
assert(permissionmode(occupiedstage) == 384u32);
if (si == 0) { occupieddiagref = strings.dup(out.stderr); }
else { assert(same(occupieddiagref, out.stderr)); };
si += 1;
};
assert(same(archiverefs[0], archiverefs[1]));
let artifacts: []str = [".unit.ww", ".wwi", ".s", ".o", ".a",
".init.unit.ww", ".init.s", ".init.o"];
let ai: i32 = 0;
for (ai < artifacts.len) {
assert(same(readfile(strings.concat(root, "/work-c/alpha",
artifacts[ai])), readfile(strings.concat(root,
"/work-ww/alpha", artifacts[ai]))));
ai += 1;
};
// Two independent drivers execute simultaneously with different umasks.
// Their process-local masks cannot leak across stages, works, or outputs.
let parallelcwork: str = strings.concat(root, "/parallel-c-work");
let parallelwwwork: str = strings.concat(root, "/parallel-ww-work");
mkdirall(parallelcwork); mkdirall(parallelwwwork);
let parallelcbin: str = strings.concat(root, "/parallel-c");
let parallelwwbin: str = strings.concat(root, "/parallel-ww");
let cav: []str = [launcher, "002", driver("ww"), "build", "-w",
parallelcwork, "-I", source, "-o", parallelcbin, fanalpha];
let wav: []str = [launcher, "077", driver("ww_ww"), "build", "-w",
parallelwwwork, "-I", source, "-o", parallelwwbin, fanbeta];
let cc: exec.command;
cc.path = launcher; cc.argv = cav; cc.env = os.getenvs(); cc.dir = repo();
cc.stdoutpath = strings.concat(root, "/parallel-c.stdout");
cc.stderrpath = strings.concat(root, "/parallel-c.stderr");
cc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
cc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let wc: exec.command;
wc.path = launcher; wc.argv = wav; wc.env = os.getenvs(); wc.dir = repo();
wc.stdoutpath = strings.concat(root, "/parallel-ww.stdout");
wc.stderrpath = strings.concat(root, "/parallel-ww.stderr");
wc.deadline = time.add(time.now(time.clock.monotonic),
(120i64 * (time.second: i64)): time.duration);
wc.grace = (100i64 * (time.millisecond: i64)): time.duration;
let cp: exec.process;
let wp: exec.process;
exec.start(&cp, &cc); exec.start(&wp, &wc);
let cdone: bool = false;
let wdone: bool = false;
for (!cdone || !wdone) {
if (!cdone) { cdone = exec.poll(&cp); };
if (!wdone) { wdone = exec.poll(&wp); };
if (!cdone || !wdone) {
time.sleep(time.millisecond, time.clock.monotonic);
};
};
assert(cp.result.errno == 0 && cp.result.cleanuperrno == 0);
assert(wp.result.errno == 0 && wp.result.cleanuperrno == 0);
assert(cp.result.termination == exec.termination.EXIT && cp.result.code == 0);
assert(wp.result.termination == exec.termination.EXIT && wp.result.code == 0);
assert(readfile(cc.stdoutpath).len == 0 && readfile(cc.stderrpath).len == 0);
assert(readfile(wc.stdoutpath).len == 0 && readfile(wc.stderrpath).len == 0);
assert(permissionmode(parallelcbin) == 509u32);
assert(permissionmode(parallelwwbin) == 448u32);
assert(!directoryhasnew(parallelcwork));
assert(!directoryhasnew(parallelwwwork));
clean(root);
};

27
test/package/umaskexec.c Normal file
View File

@@ -0,0 +1,27 @@
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/stat.h>
#include <unistd.h>
int
main(int argc, char **argv)
{
char *end;
unsigned long mask;
if (argc < 3) {
fputs("usage: package-umaskexec MASK PROGRAM [ARG ...]\n", stderr);
return 2;
}
errno = 0;
mask = strtoul(argv[1], &end, 8);
if (errno != 0 || end == argv[1] || *end != '\0' || mask > 0777) {
fputs("package-umaskexec: invalid mask\n", stderr);
return 2;
}
umask((mode_t)mask);
execv(argv[2], &argv[2]);
fputs("package-umaskexec: exec failed\n", stderr);
return 127;
}