From 84f5e6870958c7c8f269ebb78aeaf73eba9e7325 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 20 Aug 2026 22:03:09 +0900 Subject: [PATCH] ww build: honor Go output permission modes --- Makefile | 8 +- cmd/w6l/main.c | 18 +- docs/build-system.md | 97 ++++++++++ docs/spec.md | 9 + selfhost/cmd/w6l/main.ww | 4 +- selfhost/cmd/ww/main.ww | 4 +- test/package/package_test.ww | 355 +++++++++++++++++++++++++++++++++++ test/package/umaskexec.c | 27 +++ 8 files changed, 513 insertions(+), 9 deletions(-) create mode 100644 test/package/umaskexec.c diff --git a/Makefile b/Makefile index 0c5753cf..3639f07a 100644 --- a/Makefile +++ b/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 ------------------------------------------------------- diff --git a/cmd/w6l/main.c b/cmd/w6l/main.c index f88eb78f..8628adfc 100644 --- a/cmd/w6l/main.c +++ b/cmd/w6l/main.c @@ -8,8 +8,8 @@ #include #include #include +#include #include -#include /* `path` is acceptable iff it's either an archive ("!\n") or * an ELF file ("\x7fELF"). Distros often ship lib.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; } diff --git a/docs/build-system.md b/docs/build-system.md index ddeaa4e5..b81e2a91 100644 --- a/docs/build-system.md +++ b/docs/build-system.md @@ -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. diff --git a/docs/spec.md b/docs/spec.md index d15e6a38..5494ba0e 100644 --- a/docs/spec.md +++ b/docs/spec.md @@ -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, diff --git a/selfhost/cmd/w6l/main.ww b/selfhost/cmd/w6l/main.ww index 54d8f89d..d29e9cbf 100644 --- a/selfhost/cmd/w6l/main.ww +++ b/selfhost/cmd/w6l/main.ww @@ -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); diff --git a/selfhost/cmd/ww/main.ww b/selfhost/cmd/ww/main.ww index fc26e22e..58f919d9 100644 --- a/selfhost/cmd/ww/main.ww +++ b/selfhost/cmd/ww/main.ww @@ -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; diff --git a/test/package/package_test.ww b/test/package/package_test.ww index 54c06101..92743d5f 100644 --- a/test/package/package_test.ww +++ b/test/package/package_test.ww @@ -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); +}; diff --git a/test/package/umaskexec.c b/test/package/umaskexec.c new file mode 100644 index 00000000..58759a28 --- /dev/null +++ b/test/package/umaskexec.c @@ -0,0 +1,27 @@ +#include +#include +#include +#include +#include + +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; +}