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:
2026-06-22 20:52:11 +09:00
parent c0383274d1
commit a1484aef28
4 changed files with 291 additions and 0 deletions

View File

@@ -1241,6 +1241,22 @@ fn copyfile(src: *u8, dst: *u8) i32 = {
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
// AND both cached artifacts exist; on HIT copy them into the scratch
// 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(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(cobj, objf) != 0) { return 0; };
return 1;
@@ -1340,6 +1358,8 @@ fn cachestore(cacheroot: *u8, g: *sepgraph, pi: i32, manifest: *u8,
let tkey: *u8 = cachetmp(dir, "P.key", pid);
if (copyfile(wwi, twwi) != 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),
os.flag.WRONLY | os.flag.CREATE | os.flag.TRUNC, 420i32); // 0o644
if (fd < 0) { cachermtmp(twwi, tobj, tkey); return; };