ww: clean per-pid sep-build scratch dir on run/test (#59)

ww run / ww test created /tmp/ww_{run,test}_<pid>.sepwork/ but removed only
the built binary, leaking the scratch DIR every invocation — the tmpfs filler
(98,811 entries blocked the gate twice). Add a keepscratch param to
build_one_sep: a thin wrapper rm -rfs the impl scratch at the single
choke-point when keepscratch==0 AND the path ends ".sepwork" (covers every
return, success+error; fires post-link, pre-run). do_build keeps it (the
byte-id gates read <stem>.sepwork from ww build -o); do_run/do_test clean;
do_test no-o redirects scratch into /tmp. Both stages symmetric; reuses the
existing shell rm -rf idiom (lib/os.removeall = #109). Test 989_sepscratch_run
is self-scoped by child pid (non-flaky) with a KEEP control + revert-verified
non-vacuity. Full gate: 448 pass, zero new run/test leaks. (ww_d_* relic +
historical bulk = one-time sweep + agent-probe discipline, not code.)
This commit is contained in:
2026-06-19 00:58:57 +09:00
parent 5adacd2eee
commit 3bb381ef48
4 changed files with 262 additions and 14 deletions

View File

@@ -0,0 +1,152 @@
/*
* 989_sepscratch_run (#59) — the driver must not LEAK its per-build
* `<stem>.sepwork` scratch dir. Pre-fix, `ww run` / `ww test` removed only
* the built binary (unlink) and left `/tmp/ww_run_<pid>.sepwork/` behind
* every invocation — the tmpfs filler. The fix (build_one_sep keepscratch
* param + guarded rm at the wrapper choke-point) removes run/test scratch
* while KEEPING build -o scratch (the byte-id gates read it).
*
* Two deterministic, self-scoped checks per driver stage (ww + ww_ww), so
* they never flake under parallel phase-2 (no global /tmp glob):
* A. KEEP control: `<drv> build -o <td>/prog <root.ww>` → assert
* `<td>/prog.sepwork` STILL EXISTS (keepscratch 1). Proves the test is
* non-vacuous AND that the gate-read build scratch survives.
* B. CLEAN: fork a child that exec's `<drv> run <root.ww>`; the child's
* pid P fixes the driver temp at `/tmp/ww_run_<P>.sepwork` EXACTLY
* (do_run / makeruntmp both key on getpid()). After the child exits,
* assert that dir is GONE (keepscratch 0). Self-scoped to P → immune to
* concurrent builds.
*
* run/test share ONE cleanup choke-point (the build_one_sep wrapper), so
* proving `run` proves the `test` path by construction. Revert the wrapper
* rm → check B reddens (the mandatory non-vacuity revert-experiment).
*
* Light wwstage-driver test (CLAUDE.md rule 14): the trivial no-import root
* builds fast; all artifacts live under a per-pid /tmp td or the driver's
* own /tmp temp, so it is phase-1 parallel-safe. Models 989_sepbuild_run.c.
*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <fcntl.h>
static const char *
absbin(void)
{
const char *b = getenv("BIN");
if (!b) b = "out/bin";
if (b[0] == '/') return b;
static char buf[2048];
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return NULL;
snprintf(buf, sizeof buf, "%s/%s", cwd, b);
return buf;
}
static int
write_file(const char *path, const char *body)
{
FILE *f = fopen(path, "wb");
if (!f) return -1;
fputs(body, f);
fclose(f);
return 0;
}
/* fork+exec `<bin>/<drv> run <root>` with output muted; return the child's
* pid via *outpid so the caller can name the driver's /tmp scratch. */
static int
run_child(const char *bin, const char *drv, const char *root, pid_t *outpid)
{
char drvpath[2048];
snprintf(drvpath, sizeof drvpath, "%s/%s", bin, drv);
pid_t pid = fork();
if (pid < 0) return -1;
if (pid == 0) {
int dn = open("/dev/null", O_WRONLY);
if (dn >= 0) { dup2(dn, 1); dup2(dn, 2); }
/* argv[0] MUST be the full path: the driver derives self_dir
* (to locate w6c/w6a/w6l/libwwrt) from argv[0]. */
execl(drvpath, drvpath, "run", root, (char *)NULL);
_exit(127);
}
*outpid = pid;
int status = 0;
waitpid(pid, &status, 0);
if (WIFEXITED(status)) return WEXITSTATUS(status);
return 1;
}
/* A no-import root: builds fast, still produces a real `__root` scratch. */
static const char *root_src =
"package main;\n"
"fn main() i32 = { return 0; };\n";
int
main(void)
{
const char *bin = absbin();
if (!bin) return 1;
char td[64], cmd[4096], rootww[1024];
int fail = 0;
snprintf(td, sizeof td, "/tmp/wwscratch_%d", getpid());
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
if (system(cmd) == -1) return 1;
mkdir(td, 0755);
snprintf(rootww, sizeof rootww, "%s/root.ww", td);
if (write_file(rootww, root_src)) { fail++; goto out; }
const char *drvs[] = { "ww", "ww_ww" };
for (int s = 0; s < 2; s++) {
/* A. KEEP control — build -o scratch must SURVIVE. */
char prog[1024], buildscr[1100];
snprintf(prog, sizeof prog, "%s/prog_%s", td, drvs[s]);
snprintf(cmd, sizeof cmd,
"timeout 240 %s/%s build -o %s %s >/dev/null 2>&1",
bin, drvs[s], prog, rootww);
int brc = system(cmd);
if (brc == -1 || (WIFEXITED(brc) && WEXITSTATUS(brc) != 0)) {
fprintf(stderr, "sepscratch FAIL: %s build -o\n", drvs[s]);
fail++;
}
snprintf(buildscr, sizeof buildscr, "%s.sepwork", prog);
if (access(buildscr, 0) != 0) {
fprintf(stderr, "sepscratch FAIL: %s build -o scratch %s was "
"removed (gates read it; keepscratch must be 1)\n",
drvs[s], buildscr);
fail++;
}
/* B. CLEAN — run scratch must be GONE post-exit (self-scoped P). */
pid_t p = 0;
int rrc = run_child(bin, drvs[s], rootww, &p);
if (rrc != 0) {
fprintf(stderr, "sepscratch FAIL: %s run exit=%d (expected 0; "
"build must have created+run, proving non-vacuity)\n",
drvs[s], rrc);
fail++;
}
char runscr[64];
snprintf(runscr, sizeof runscr, "/tmp/ww_run_%d.sepwork", (int)p);
if (access(runscr, 0) == 0) {
fprintf(stderr, "sepscratch FAIL: %s LEAKED %s (keepscratch 0 "
"must rm run scratch)\n", drvs[s], runscr);
fail++;
}
}
out:
snprintf(cmd, sizeof cmd, "rm -rf %s", td);
if (system(cmd) == -1) { /* best-effort cleanup */ }
if (fail) {
fprintf(stderr, "sepscratch: %d check(s) failed\n", fail);
return 1;
}
printf("sepscratch: ww+ww_ww — build -o scratch kept, run scratch "
"removed (no /tmp .sepwork leak)\n");
return 0;
}