Found by the breaker on lap 2 while landing #43 (ByteBuddy checksum verification).
Where
agent/build.sh hashes $BB_JAR once (sha256sum -c -), then reads the same path again later: javac -cp "$BB_JAR:$BUILD/boot" ... (compile) and unzip -oq "$BB_JAR" 'net/*' (shade into the shipped agent jar). Nothing pins the bytes verified to the bytes consumed — it's classic verify-then-reread.
Why it matters
Demonstrated: sandbox copy of build.sh, a real (hash-matching) ByteBuddy jar in lib/, and a javac wrapper that swaps $BB_JAR for a poisoned copy immediately after the checksum step runs but before unzip shades it in. Result: build exits 0, and dreamconnect-agent.jar contains a class (net/PWNED.class) that was never hashed. The adversary this defends against — issue #43's "poisoned local cache" — only needs write access to agent/lib/ timed to that window, not to the whole checkout (which would already be a bigger compromise the pin can't help with anyway — that part was already ruled out in lap 1).
Related, same root cause (no locking/staging around agent/lib/): two concurrent build.sh invocations can race on the same file — one process's checksum-mismatch cleanup (rm -f, added in #43's follow-up fix) can delete a jar another process just verified and is about to consume. Low severity, self-healing on re-run, but same fix would address both.
Why not fixed alongside #43
Closing this needs a design decision, not a mechanical change — e.g. stage the verified jar into an immutable build-local copy right after the hash check and consume only that copy, or hold a lock (flock) around the fetch-verify-consume sequence. Either is more than a one-line diff and deserves its own review.
Suggested fix
After verification passes, copy $BB_JAR into $BUILD (e.g. $BUILD/byte-buddy-verified.jar) before any other file in $LIB could plausibly change, and have javac/unzip reference only that copy from then on. This also incidentally fixes the concurrent-build race, since each build gets its own copy under its own $BUILD tree.
Confidence: Confirmed (mutant demonstrated). Severity: Low probability (requires a locally-writable agent/lib/ and precise timing, or a genuinely concurrent second build) but it is the residual gap in the exact control #43 added, so worth closing deliberately rather than by accident.
Found by the breaker on lap 2 while landing #43 (ByteBuddy checksum verification).
Where
agent/build.shhashes$BB_JARonce (sha256sum -c -), then reads the same path again later:javac -cp "$BB_JAR:$BUILD/boot" ...(compile) andunzip -oq "$BB_JAR" 'net/*'(shade into the shipped agent jar). Nothing pins the bytes verified to the bytes consumed — it's classic verify-then-reread.Why it matters
Demonstrated: sandbox copy of
build.sh, a real (hash-matching) ByteBuddy jar inlib/, and ajavacwrapper that swaps$BB_JARfor a poisoned copy immediately after the checksum step runs but beforeunzipshades it in. Result: build exits 0, anddreamconnect-agent.jarcontains a class (net/PWNED.class) that was never hashed. The adversary this defends against — issue #43's "poisoned local cache" — only needs write access toagent/lib/timed to that window, not to the whole checkout (which would already be a bigger compromise the pin can't help with anyway — that part was already ruled out in lap 1).Related, same root cause (no locking/staging around
agent/lib/): two concurrentbuild.shinvocations can race on the same file — one process's checksum-mismatch cleanup (rm -f, added in #43's follow-up fix) can delete a jar another process just verified and is about to consume. Low severity, self-healing on re-run, but same fix would address both.Why not fixed alongside #43
Closing this needs a design decision, not a mechanical change — e.g. stage the verified jar into an immutable build-local copy right after the hash check and consume only that copy, or hold a lock (
flock) around the fetch-verify-consume sequence. Either is more than a one-line diff and deserves its own review.Suggested fix
After verification passes, copy
$BB_JARinto$BUILD(e.g.$BUILD/byte-buddy-verified.jar) before any other file in$LIBcould plausibly change, and havejavac/unzipreference only that copy from then on. This also incidentally fixes the concurrent-build race, since each build gets its own copy under its own$BUILDtree.Confidence: Confirmed (mutant demonstrated). Severity: Low probability (requires a locally-writable
agent/lib/and precise timing, or a genuinely concurrent second build) but it is the residual gap in the exact control #43 added, so worth closing deliberately rather than by accident.