/* * 989_pkgcache_poison_run — out/.pkgcache 0-byte-artifact self-heal gate (#10, * BUG-B). A torn producer write (e.g. disk-full mid-copy) can leave a 0-byte * P.wwi/P.o under a SELF-CONSISTENT P.key. Pre-fix the cache served that empty * artifact forever (the key matched and access() saw the file), so every later * build link-failed or produced a wrong binary — a SILENT serve-wrong that * never surfaces on its own. The fix rejects size==0 on BOTH the lookup (read) * and the store (write) side: a poisoned entry self-heals (treated as MISS → * re-derived), and a torn store never commits the key. The guard is symmetric * cstage (filenonempty, stat) / wwstage (cachefilesize, open+filesize) — the * hit/miss DECISION must match (rule 10). * * Target: a 2-level graph root(main) -> leaf in a private temp tree. ROOT is * never cached (it is the build target); `leaf` is the cacheable package. We * seed a warm cache, POISON the cached artifact to 0 bytes, rebuild, and assert * the build self-heals: leaf re-compiles (MISS sentinel) AND the program runs * to its correct value (not an empty/link-failed binary) AND the cache is * re-stored non-empty. * * HIT/MISS sentinel (same as 989_pkgcache_run): a MISS runs w6c → writes * /leaf.s; a HIT copies the cached .o/.wwi and skips compose+w6c+w6a, * so no leaf.s. The scratch is wiped before each build so leaf.s presence * reflects exactly the last build. * * Table-driven over WHICH artifact is poisoned: { P.wwi | P.o | both }. Each * row, per stage (ww, ww_ww): * - warm baseline: an unchanged rebuild HITs (leaf.s ABSENT, prog runs) — * proves the cache is genuinely warm before we poison it (non-vacuity); * - POISON the row's artifact(s) to 0 bytes; * - heal rebuild: leaf re-compiles (leaf.s PRESENT = MISS, not served poison) * AND prog runs == EXPECT_EXIT AND both cached artifacts are non-empty again * (re-stored). Pre-fix this row served the 0-byte artifact and the assert * on leaf.s (and on prog exit) fails. * * Light wwstage-driver test (CLAUDE.md rule 14): all build outputs and caches * live under a private /tmp tree, wiped on exit, so it is parallel-safe and off * every byte-id/bootstrap gate. Models 989_pkgcache_run conventions. */ #include #include #include #include #include #include #define EXPECT_EXIT 7 static int runwait(const char *cmd) { int rc = system(cmd); if (rc == -1) return -1; if (WIFEXITED(rc)) return WEXITSTATUS(rc); return 1; } 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; } /* Truncate `path` to 0 bytes (models a torn producer write under a valid key); * "wb" opens-and-truncates, leaving the file present but empty. */ static int truncate0(const char *path) { FILE *f = fopen(path, "wb"); if (!f) return -1; fclose(f); return 0; } static long fsize(const char *path) { struct stat st; if (stat(path, &st) != 0) return -1; return (long)st.st_size; } /* /.s present ⇒ that package compiled this build (MISS). */ static int sdot(const char *scratch, const char *pkg) { char p[1100]; snprintf(p, sizeof p, "%s/%s.s", scratch, pkg); return access(p, 0) == 0; } static const char *leaf_src = "package leaf;\n" "export fn base() i32 = { return 7; };\n"; static const char *root_src = "package main;\n" "import leaf;\n" "fn main() i32 = { return leaf.base(); };\n"; int main(void) { const char *bin = absbin(); if (!bin) return 1; int fail = 0; char td[64], cmd[8192]; char rootww[1024], leafww[1024]; char scratch[1024], prog[1024]; snprintf(td, sizeof td, "/tmp/wwpkgpoison_%d", getpid()); snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); snprintf(cmd, sizeof cmd, "mkdir -p %s/leaf", td); runwait(cmd); snprintf(rootww, sizeof rootww, "%s/root.ww", td); snprintf(leafww, sizeof leafww, "%s/leaf/leaf.ww", td); snprintf(prog, sizeof prog, "%s/p", td); snprintf(scratch, sizeof scratch, "%s/p.sepwork", td); if (write_file(rootww, root_src) || write_file(leafww, leaf_src)) { fail++; goto out; } /* Which cached artifact(s) the row poisons. */ struct { const char *tag; int wwi, obj; } rows[] = { { "P.wwi", 1, 0 }, { "P.o", 0, 1 }, { "both", 1, 1 }, }; struct { const char *drv; } stg[] = { { "ww" }, { "ww_ww" } }; for (int s = 0; s < 2; s++) { const char *drv = stg[s].drv; char cache[1024], cwwi[1100], cobj[1100]; snprintf(cache, sizeof cache, "%s/cache.%d", td, s); snprintf(cwwi, sizeof cwwi, "%s/leaf/P.wwi", cache); snprintf(cobj, sizeof cobj, "%s/leaf/P.o", cache); /* The cstage driver rm -rf's the scratch each build; the wwstage * driver only mkdir's it (#58(a) asymmetry), so a stale leaf.s could * persist and defeat the MISS/HIT sentinel. The gate owns its scratch: * wipe it before every build so a fresh leaf.s reflects the last build. */ #define BUILD() do { \ snprintf(cmd, sizeof cmd, \ "rm -rf %s; WW_PKGCACHE='%s' timeout 240 %s/%s build --sep " \ "-o %s %s >/dev/null 2>&1", scratch, cache, bin, drv, prog, \ rootww); \ } while (0) /* fresh cache for this stage, cold-seed it (MISS) */ snprintf(cmd, sizeof cmd, "rm -rf %s", cache); runwait(cmd); BUILD(); if (runwait(cmd) != 0) { fprintf(stderr, "poison FAIL[%s]: cold seed build failed\n", drv); fail++; continue; } if (!sdot(scratch, "leaf")) { fprintf(stderr, "poison FAIL[%s]: cold seed — leaf did not " "compile (MISS expected)\n", drv); fail++; } if (runwait(prog) != EXPECT_EXIT) { fprintf(stderr, "poison FAIL[%s]: cold seed prog exit != %d\n", drv, EXPECT_EXIT); fail++; } for (size_t r = 0; r < sizeof rows / sizeof rows[0]; r++) { /* warm baseline — the cache is warm (heals re-store it), so an * unchanged rebuild must HIT; proves we poison a LIVE entry. */ BUILD(); runwait(cmd); if (sdot(scratch, "leaf")) { fprintf(stderr, "poison FAIL[%s/%s]: warm baseline — leaf " "recompiled (cache not warm before poison)\n", drv, rows[r].tag); fail++; } /* poison the row's artifact(s) to 0 bytes, key stays valid */ if (rows[r].wwi && truncate0(cwwi) != 0) { fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.wwi\n", drv, rows[r].tag); fail++; } if (rows[r].obj && truncate0(cobj) != 0) { fprintf(stderr, "poison FAIL[%s/%s]: cannot truncate P.o\n", drv, rows[r].tag); fail++; } /* heal rebuild — a 0-byte artifact under a valid key must be * treated as MISS (re-derived), NOT served. */ BUILD(); runwait(cmd); if (!sdot(scratch, "leaf")) { fprintf(stderr, "poison FAIL[%s/%s]: served poison — leaf did " "NOT re-derive after 0-byte artifact (silent serve-wrong)\n", drv, rows[r].tag); fail++; } if (runwait(prog) != EXPECT_EXIT) { fprintf(stderr, "poison FAIL[%s/%s]: heal prog exit != %d " "(empty/link-failed binary)\n", drv, rows[r].tag, EXPECT_EXIT); fail++; } /* re-store must leave both artifacts non-empty (write guard let a * good store through, AND a torn store never commits a 0-byte). */ if (fsize(cwwi) <= 0 || fsize(cobj) <= 0) { fprintf(stderr, "poison FAIL[%s/%s]: cached artifact still " "0-byte after heal (re-store did not repair)\n", drv, rows[r].tag); fail++; } } #undef BUILD } out: snprintf(cmd, sizeof cmd, "rm -rf %s", td); runwait(cmd); if (fail) { fprintf(stderr, "poison: %d check(s) failed\n", fail); return 1; } printf("poison: root->leaf via build_one_sep — a 0-byte cached P.wwi/P.o/both " "under a valid key self-heals (treated as MISS, re-derived to correct " "value, re-stored non-empty) — both stages\n"); return 0; }