ww build: honor Go output permission modes
This commit is contained in:
8
Makefile
8
Makefile
@@ -101,6 +101,7 @@ EXEC_TEST_C = $(BIN)/test_exec
|
||||
EXEC_TEST_WW = $(BIN)/test_exec_ww
|
||||
|
||||
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 \
|
||||
lib/test/run.ww lib/fnmatch/fnmatch.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
|
||||
@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_UMASKEXEC_BIN) \
|
||||
$(BIN)/ww $(BIN)/ww_ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
@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 \
|
||||
$(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
|
||||
|
||||
# ---- directories -------------------------------------------------------
|
||||
|
||||
@@ -8,8 +8,8 @@
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
#include <fcntl.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
|
||||
/* `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
|
||||
@@ -121,17 +121,23 @@ main(int argc, char **argv)
|
||||
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) {
|
||||
close(fd);
|
||||
(void)unlink(out);
|
||||
fprintf(stderr, "w6l: cannot open %s\n", out);
|
||||
return 1;
|
||||
}
|
||||
int rc = l_emit_elf(&l, f, base, base + 0x1000 + entry->val);
|
||||
fclose(f);
|
||||
if (rc == 0 && chmod(out, 0755) != 0) {
|
||||
fprintf(stderr, "w6l: cannot make %s executable\n", out);
|
||||
rc = 1;
|
||||
}
|
||||
free(inputs);
|
||||
return rc;
|
||||
}
|
||||
|
||||
@@ -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
|
||||
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 459–558](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 1904–2000](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 119–220](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 7–44](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 1–16](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
|
||||
|
||||
Five candidates were developed as coherent systems, not as feature bins.
|
||||
|
||||
@@ -310,6 +310,15 @@ ImportPath = ident { "." ident } .
|
||||
`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
|
||||
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.
|
||||
|
||||
Import paths remain unquoted and dotted. Grouped imports, quoted import paths,
|
||||
|
||||
@@ -302,8 +302,10 @@ export fn main(argc: i32, argv: **u8) i32 = {
|
||||
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 fd: i32 = os.open(pathstr(outpath), flags, 493i32); // 0o755
|
||||
let fd: i32 = os.open(pathstr(outpath), flags, 511i32); // 0o777
|
||||
if (fd < 0) {
|
||||
let m: str = "w6l: cannot open output\n";
|
||||
os.write(2, m.ptr, m.len: u64);
|
||||
|
||||
@@ -5896,8 +5896,10 @@ fn fileequal(a: *u8, b: *u8) bool = {
|
||||
fn copyfilestage(src: *u8, dst: *u8) i32 = {
|
||||
let in: i32 = os.open(pathstr(src), os.flag.RDONLY, 0i32);
|
||||
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),
|
||||
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; };
|
||||
let buf: [65536]u8;
|
||||
let bad: bool = false;
|
||||
|
||||
@@ -62,6 +62,14 @@ fn readfile(path: str) str = {
|
||||
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 = {
|
||||
let fd: i32 = os.open(path,
|
||||
os.flag.WRONLY | os.flag.CREATE | os.flag.EXCL, 384i32);
|
||||
@@ -12635,3 +12643,350 @@ fn runtimepath(relative: str) str = {
|
||||
"/occupied/alpha.test.new")) == 0);
|
||||
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
27
test/package/umaskexec.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user