pkgcache: reject 0-byte artifacts on store and lookup, self-heal torn writes (#10)
A torn producer write (e.g. disk-full mid-copy) could leave a 0-byte P.wwi or
P.o in out/.pkgcache under a self-consistent key; cache_lookup checked only
existence, so every later build HIT and served the empty artifact forever
(silent serve-wrong). Reject size==0 on both sides, symmetric across stages:
store refuses to commit a 0-byte temp before the key write, lookup treats a
0-byte cached artifact as a MISS so existing poison self-heals on re-derive.
A valid .wwi/.o is never 0 bytes, so the guard cannot misfire.
Regression: test/wcc/989_pkgcache_poison_run.c, table-driven over
{poison P.wwi | P.o | both}, non-vacuity proven by guard-neuter.
This commit is contained in:
11
Makefile
11
Makefile
@@ -535,6 +535,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
|||||||
$(BIN)/test_separchive_run \
|
$(BIN)/test_separchive_run \
|
||||||
$(BIN)/test_pkgcache_run \
|
$(BIN)/test_pkgcache_run \
|
||||||
$(BIN)/test_pkgcache_concurrent_run \
|
$(BIN)/test_pkgcache_concurrent_run \
|
||||||
|
$(BIN)/test_pkgcache_poison_run \
|
||||||
$(BIN)/test_c6soak_run \
|
$(BIN)/test_c6soak_run \
|
||||||
$(BIN)/test_septest_run \
|
$(BIN)/test_septest_run \
|
||||||
$(BIN)/test_coloimport_sep \
|
$(BIN)/test_coloimport_sep \
|
||||||
@@ -2951,6 +2952,16 @@ $(BIN)/test_pkgcache_concurrent_run: test/wcc/989_pkgcache_concurrent_run.c $(BI
|
|||||||
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
||||||
$(CC) $(CFLAGS) -o $@ $<
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
|
# 989_pkgcache_poison_run — #10 (BUG-B): a 0-byte cached P.wwi/P.o under a valid
|
||||||
|
# P.key (torn producer write) must self-heal — rejected on both lookup (MISS,
|
||||||
|
# re-derive) and store (never commit a 0-byte). Table-driven over which artifact
|
||||||
|
# is poisoned {wwi/o/both}, both driver stages. COLD/dev-only (off every byte-id/
|
||||||
|
# bootstrap gate). Needs both driver + compiler + asm + linker stages + libwwrt.a.
|
||||||
|
$(BIN)/test_pkgcache_poison_run: test/wcc/989_pkgcache_poison_run.c $(BIN)/ww $(BIN)/ww_ww \
|
||||||
|
$(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6a_ww \
|
||||||
|
$(BIN)/w6l $(BIN)/w6l_ww $(LIB)/libwwrt.a | $(BIN)
|
||||||
|
$(CC) $(CFLAGS) -o $@ $<
|
||||||
|
|
||||||
# 989_c6soak_run — M3-tail commit-6 dual-path soak gate, phase-1 leg (#46
|
# 989_c6soak_run — M3-tail commit-6 dual-path soak gate, phase-1 leg (#46
|
||||||
# c6). Builds real lib chains (encoding.utf8 dotted + strings/bytes same-leaf
|
# c6). Builds real lib chains (encoding.utf8 dotted + strings/bytes same-leaf
|
||||||
# `contains`) BOTH combined and --sep, asserts combined==sep run-equivalence
|
# `contains`) BOTH combined and --sep, asserts combined==sep run-equivalence
|
||||||
|
|||||||
@@ -780,6 +780,17 @@ pkgcache_dir(struct sepgraph *g, int pi, char *out, size_t outsz)
|
|||||||
snprintf(out, outsz, "%s/%s", pkgcache_root(), base);
|
snprintf(out, outsz, "%s/%s", pkgcache_root(), base);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* A torn producer write (e.g. disk-full mid-copy) can leave a 0-byte P.wwi/P.o
|
||||||
|
* under a self-consistent key; size==0 is unambiguous poison (wwi_emit always
|
||||||
|
* writes >=1 line, a valid .o is never empty), so both store and lookup reject
|
||||||
|
* it — self-healing, Go-build-cache style (#10). */
|
||||||
|
static int
|
||||||
|
filenonempty(const char *p)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
return stat(p, &st) == 0 && st.st_size > 0;
|
||||||
|
}
|
||||||
|
|
||||||
/* HIT iff a freshly recomputed manifest equals the stored P.key byte-for-byte
|
/* HIT iff a freshly recomputed manifest equals the stored P.key byte-for-byte
|
||||||
* AND both cached artifacts exist; on HIT copy them into the scratch wwi/obj
|
* AND both cached artifacts exist; on HIT copy them into the scratch wwi/obj
|
||||||
* paths so the producer loop can skip compose+w6c+w6a. */
|
* paths so the producer loop can skip compose+w6c+w6a. */
|
||||||
@@ -802,6 +813,7 @@ cache_lookup(struct sepgraph *g, int pi, const char *manifest,
|
|||||||
if (more || sn != strlen(manifest) || memcmp(stored, manifest, sn) != 0)
|
if (more || sn != strlen(manifest) || memcmp(stored, manifest, sn) != 0)
|
||||||
return 0;
|
return 0;
|
||||||
if (access(cwwi, 0) != 0 || access(cobj, 0) != 0) return 0;
|
if (access(cwwi, 0) != 0 || access(cobj, 0) != 0) return 0;
|
||||||
|
if (!filenonempty(cwwi) || !filenonempty(cobj)) return 0;
|
||||||
snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", cwwi, wwi);
|
snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", cwwi, wwi);
|
||||||
if (run(cmd) != 0) return 0;
|
if (run(cmd) != 0) return 0;
|
||||||
snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", cobj, obj);
|
snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", cobj, obj);
|
||||||
@@ -847,6 +859,7 @@ cache_store(struct sepgraph *g, int pi, const char *manifest,
|
|||||||
if (run(cmd) != 0) goto cleanup;
|
if (run(cmd) != 0) goto cleanup;
|
||||||
snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", obj, tobj);
|
snprintf(cmd, sizeof cmd, "cp -f '%s' '%s'", obj, tobj);
|
||||||
if (run(cmd) != 0) goto cleanup;
|
if (run(cmd) != 0) goto cleanup;
|
||||||
|
if (!filenonempty(twwi) || !filenonempty(tobj)) goto cleanup;
|
||||||
f = fopen(tkey, "wb");
|
f = fopen(tkey, "wb");
|
||||||
if (f == NULL) goto cleanup;
|
if (f == NULL) goto cleanup;
|
||||||
fputs(manifest, f);
|
fputs(manifest, f);
|
||||||
|
|||||||
@@ -1241,6 +1241,22 @@ fn copyfile(src: *u8, dst: *u8) i32 = {
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// cachefilesize — open+filesize+close; -1 on any error. A torn producer write
|
||||||
|
// (e.g. disk-full mid-copy) can leave a 0-byte P.wwi/P.o under a self-consistent
|
||||||
|
// key; size==0 is unambiguous poison (wwi_emit always writes >=1 line, a valid
|
||||||
|
// .o is never empty), so both cachelookup and cachestore reject it — self-healing
|
||||||
|
// (#10). Twin of cstage filenonempty.
|
||||||
|
fn cachefilesize(path: *u8) i64 = {
|
||||||
|
let fd: i32 = os.open(pathstr(path), os.flag.RDONLY, 0i32);
|
||||||
|
if (fd < 0) { return -1i64; };
|
||||||
|
let szr: (i64 | os.oserror) = os.filesize(fd);
|
||||||
|
os.close(fd);
|
||||||
|
match (szr) {
|
||||||
|
case let v: i64 => return v;
|
||||||
|
case let e: os.oserror => return -1i64;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
// cachelookup — HIT iff the manifest equals the stored P.key byte-for-byte
|
// cachelookup — HIT iff the manifest equals the stored P.key byte-for-byte
|
||||||
// AND both cached artifacts exist; on HIT copy them into the scratch
|
// AND both cached artifacts exist; on HIT copy them into the scratch
|
||||||
// wwi/objf so the producer loop can skip compose+w6c+w6a.
|
// wwi/objf so the producer loop can skip compose+w6c+w6a.
|
||||||
@@ -1260,6 +1276,8 @@ fn cachelookup(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
|
|||||||
};
|
};
|
||||||
if (os.access(pathstr(cwwi), 0i32) != 0) { return 0; };
|
if (os.access(pathstr(cwwi), 0i32) != 0) { return 0; };
|
||||||
if (os.access(pathstr(cobj), 0i32) != 0) { return 0; };
|
if (os.access(pathstr(cobj), 0i32) != 0) { return 0; };
|
||||||
|
if (cachefilesize(cwwi) <= 0i64) { return 0; };
|
||||||
|
if (cachefilesize(cobj) <= 0i64) { return 0; };
|
||||||
if (copyfile(cwwi, wwi) != 0) { return 0; };
|
if (copyfile(cwwi, wwi) != 0) { return 0; };
|
||||||
if (copyfile(cobj, objf) != 0) { return 0; };
|
if (copyfile(cobj, objf) != 0) { return 0; };
|
||||||
return 1;
|
return 1;
|
||||||
@@ -1340,6 +1358,8 @@ fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
|
|||||||
let tkey: *u8 = cachetmp(dir, "P.key", pid);
|
let tkey: *u8 = cachetmp(dir, "P.key", pid);
|
||||||
if (copyfile(wwi, twwi) != 0) { cachermtmp(twwi, tobj, tkey); return; };
|
if (copyfile(wwi, twwi) != 0) { cachermtmp(twwi, tobj, tkey); return; };
|
||||||
if (copyfile(objf, tobj) != 0) { cachermtmp(twwi, tobj, tkey); return; };
|
if (copyfile(objf, tobj) != 0) { cachermtmp(twwi, tobj, tkey); return; };
|
||||||
|
if (cachefilesize(twwi) <= 0i64) { cachermtmp(twwi, tobj, tkey); return; };
|
||||||
|
if (cachefilesize(tobj) <= 0i64) { cachermtmp(twwi, tobj, tkey); return; };
|
||||||
let fd: i32 = os.open(pathstr(tkey),
|
let fd: i32 = os.open(pathstr(tkey),
|
||||||
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
|
||||||
if (fd < 0) { cachermtmp(twwi, tobj, tkey); return; };
|
if (fd < 0) { cachermtmp(twwi, tobj, tkey); return; };
|
||||||
|
|||||||
247
test/wcc/989_pkgcache_poison_run.c
Normal file
247
test/wcc/989_pkgcache_poison_run.c
Normal file
@@ -0,0 +1,247 @@
|
|||||||
|
/*
|
||||||
|
* 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
|
||||||
|
* <scratch>/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 <stdio.h>
|
||||||
|
#include <stdlib.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/wait.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* <scratch>/<pkg>.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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user