● Done — implemented in patch-prolog2, verified, and #4 updated. #5

Merged
navicore merged 2 commits from file-cleanup into main 2026-06-05 05:50:11 +00:00
Owner

What was implemented

build.rs — hashes libplg_runtime.a (FNV-1a 64, hand-rolled, zero deps) at
the moment it's located for include_bytes!, and bakes the digest in as
PLG_RUNTIME_HASH. Exactly the key you pushed for: derived from the bytes,
so dev rebuilds with a changed runtime get a new key, while identical
rebuilds keep reusing the cache — no cargo-version lies, no timestamp
noise, no runtime hashing cost.

link.rs::extracted_runtime() — the pid-keyed /tmp/plgc- is gone. Now:

~/.cache/plgc/runtime-ed158a59ce0c7e68/libplg_runtime.a

  • Fast path: exists + length matches → reuse, no write at all.
  • Slow path: write to a process-unique temp name, atomic rename into place
    (concurrent-safe), then sweep stale runtime-* siblings — plgc's own cache
    namespace, so reclaiming is legitimate.
  • Hash in the directory name, canonical filename kept — because clang links
    via -L -lplg_runtime.
  • $XDG_CACHE_HOME → ~/.cache → temp-dir fallback; OnceLock retained for
    in-process compiles.

Verified

  • 20 sequential runs → 1 cache dir, 0 /tmp/plgc-* dirs (the pattern no
    longer exists at all)
  • 20 concurrent cold-cache runs → 20/20 succeed (exit 1), 1 dir, 0 temp
    leftovers
  • Planted fake runtime-deadbeef… → swept on next link
  • patch-prolog2's full just ci →

One honest note: warm-cache runs time ~0.13s — about the same as before,
since clang dominates, not the 21MB write. The win here is steady-state
disk usage of exactly one ~21MB entry, forever, not speed.

What was implemented build.rs — hashes libplg_runtime.a (FNV-1a 64, hand-rolled, zero deps) at the moment it's located for include_bytes!, and bakes the digest in as PLG_RUNTIME_HASH. Exactly the key you pushed for: derived from the bytes, so dev rebuilds with a changed runtime get a new key, while identical rebuilds keep reusing the cache — no cargo-version lies, no timestamp noise, no runtime hashing cost. link.rs::extracted_runtime() — the pid-keyed /tmp/plgc-<pid> is gone. Now: ~/.cache/plgc/runtime-ed158a59ce0c7e68/libplg_runtime.a - Fast path: exists + length matches → reuse, no write at all. - Slow path: write to a process-unique temp name, atomic rename into place (concurrent-safe), then sweep stale runtime-* siblings — plgc's own cache namespace, so reclaiming is legitimate. - Hash in the directory name, canonical filename kept — because clang links via -L <dir> -lplg_runtime. - $XDG_CACHE_HOME → ~/.cache → temp-dir fallback; OnceLock retained for in-process compiles. Verified - 20 sequential runs → 1 cache dir, 0 /tmp/plgc-* dirs (the pattern no longer exists at all) - 20 concurrent cold-cache runs → 20/20 succeed (exit 1), 1 dir, 0 temp leftovers - Planted fake runtime-deadbeef… → swept on next link - patch-prolog2's full just ci → ✅ One honest note: warm-cache runs time ~0.13s — about the same as before, since clang dominates, not the 21MB write. The win here is steady-state disk usage of exactly one ~21MB entry, forever, not speed.
● Done — implemented in patch-prolog2, verified, and #4 updated.
All checks were successful
CI - Linux / CI - Linux x86_64 (pull_request) Successful in 47s
2d2be7a320
What was implemented

  build.rs — hashes libplg_runtime.a (FNV-1a 64, hand-rolled, zero deps) at
  the moment it's located for include_bytes!, and bakes the digest in as
  PLG_RUNTIME_HASH. Exactly the key you pushed for: derived from the bytes,
  so dev rebuilds with a changed runtime get a new key, while identical
  rebuilds keep reusing the cache — no cargo-version lies, no timestamp
  noise, no runtime hashing cost.

  link.rs::extracted_runtime() — the pid-keyed /tmp/plgc-<pid> is gone. Now:

  ~/.cache/plgc/runtime-ed158a59ce0c7e68/libplg_runtime.a

  - Fast path: exists + length matches → reuse, no write at all.
  - Slow path: write to a process-unique temp name, atomic rename into place
  (concurrent-safe), then sweep stale runtime-* siblings — plgc's own cache
  namespace, so reclaiming is legitimate.
  - Hash in the directory name, canonical filename kept — because clang links
  via -L <dir> -lplg_runtime.
  - $XDG_CACHE_HOME → ~/.cache → temp-dir fallback; OnceLock retained for
  in-process compiles.

  Verified

  - 20 sequential runs → 1 cache dir, 0 /tmp/plgc-* dirs (the pattern no
  longer exists at all)
  - 20 concurrent cold-cache runs → 20/20 succeed (exit 1), 1 dir, 0 temp
  leftovers
  - Planted fake runtime-deadbeef… → swept on next link
  - patch-prolog2's full just ci → 

  One honest note: warm-cache runs time ~0.13s — about the same as before,
  since clang dominates, not the 21MB write. The win here is steady-state
  disk usage of exactly one ~21MB entry, forever, not speed.
Author
Owner

Review

Verified on the branch: just ci green; 8-way concurrent cold-cache start → all succeed, exactly one runtime-<hash> dir, no temp leftovers; warm reuse works; hash derivation point is correct (same file include_bytes! reads, content-keyed not version-keyed); FNV-1a is the right tool for invalidation and the comment honestly says so. The atomic temp+rename install is sound (same fs, so rename is atomic).

Five findings, in priority order:

1. Orphaned .libplg_runtime.a.<pid> temps are never cleaned — and the motivating scenario produces them (bug)

A process killed between fs::write(&tmp, …) and fs::rename leaves a 21MB orphan inside the kept dir, and sweep_stale_runtimes only removes sibling dirs — the orphans survive every future sweep. Reproduced:

cp $D/libplg_runtime.a $D/.libplg_runtime.a.99991   # simulate 2 killed extractions
cp $D/libplg_runtime.a $D/.libplg_runtime.a.99992
plgc build …                                         # links fine, sweeps nothing
du -sh ~/.cache/plgc                                 # 66M and climbing

Issue #4's dynamics — a watcher invoking plgc 7×/s — is exactly a kill-mid-extraction generator (each save can interrupt a cold-cache run). So the "steady-state disk usage of exactly one ~21MB entry, forever" claim doesn't hold under the very workload this PR fixes; it just degrades slower. Suggested fix: when sweeping, also remove .libplg_runtime.a.* entries in the kept dir whose mtime is older than a few minutes (the age gate avoids racing a live writer about to rename).

2. The sibling sweep can fail a concurrently running different plgc build (the "self-healing" claim is too strong)

Window: old-build plgc passes the fast path (or finishes extraction), new-build plgc sweeps its dir, old plgc's clang then can't find -lplg_runtimethat invocation fails with a link error — not self-healing for the victim, and a long-lived embedder of plgc's lib would keep failing (the OnceLock memoizes the now-deleted path for the process lifetime). Mixed-build concurrency is routine on a dev box: cargo test-built plgc sweeping while the installed plgc is mid-link in another terminal. Cheap fix that also preserves the steady-state goal: age-gate the sibling sweep (only remove runtime-* dirs with mtime older than ~7 days). Disk stays bounded; the race now requires actively linking with a week-old build at the exact moment of a sweep.

3. The HOME-less fallback is cross-user shared with a predictable name (security)

temp_dir().join("plgc-cache") on a system without TMPDIR is /tmp/plgc-cache — world-writable parent, predictable path, and the hash in the dir name is public (deterministic from released runtime bytes). Another user can pre-plant runtime-<hash>/libplg_runtime.a; the reuse check is length-only, which a padded malicious archive satisfies — attacker code linked into the victim's binaries. Given this project's whole attested-immutable-binary posture, the fallback should be per-user: suffix the uid (plgc-cache-<uid>) and/or verify content hash on reuse in the fallback path. (~/.cache is fine as-is — user-owned.)

4. Optional hardening: verify content on reuse

The cache is content-named but not content-verified: reuse trusts dir name + length. Re-hashing 21MB with the same FNV on the fast path costs ~10ms against clang's seconds, and would make corruption/poisoning self-correcting everywhere, not just in the fallback. Optional in ~/.cache; I'd consider it required for the temp fallback (finding 3).

5. Doc staleness (one line)

docs/ARCHITECTURE.md:27 still says the archive is "extracted to a temp dir, passed to clang, and deleted" — needs the cache-scheme wording.


Overall: the design is right (content-keyed shared cache is exactly what #4 needs) and the implementation is careful where it's careful (atomic install, build-time hashing, XDG chain). Findings 1–2 are small, surgical changes to the sweep; 3 is a one-line path change. With those, this closes #4 properly.

## Review Verified on the branch: `just ci` green; 8-way concurrent cold-cache start → all succeed, exactly one `runtime-<hash>` dir, no temp leftovers; warm reuse works; hash derivation point is correct (same file `include_bytes!` reads, content-keyed not version-keyed); FNV-1a is the right tool for invalidation and the comment honestly says so. The atomic temp+rename install is sound (same fs, so `rename` is atomic). Five findings, in priority order: ### 1. Orphaned `.libplg_runtime.a.<pid>` temps are never cleaned — and the motivating scenario produces them (bug) A process killed between `fs::write(&tmp, …)` and `fs::rename` leaves a 21MB orphan **inside the kept dir**, and `sweep_stale_runtimes` only removes sibling *dirs* — the orphans survive every future sweep. Reproduced: ``` cp $D/libplg_runtime.a $D/.libplg_runtime.a.99991 # simulate 2 killed extractions cp $D/libplg_runtime.a $D/.libplg_runtime.a.99992 plgc build … # links fine, sweeps nothing du -sh ~/.cache/plgc # 66M and climbing ``` Issue #4's dynamics — a watcher invoking plgc 7×/s — is exactly a kill-mid-extraction generator (each save can interrupt a cold-cache run). So the "steady-state disk usage of exactly one ~21MB entry, forever" claim doesn't hold under the very workload this PR fixes; it just degrades slower. Suggested fix: when sweeping, also remove `.libplg_runtime.a.*` entries in the kept dir whose mtime is older than a few minutes (the age gate avoids racing a live writer about to rename). ### 2. The sibling sweep can fail a concurrently running *different* plgc build (the "self-healing" claim is too strong) Window: old-build plgc passes the fast path (or finishes extraction), new-build plgc sweeps its dir, old plgc's clang then can't find `-lplg_runtime` → **that invocation fails with a link error** — not self-healing for the victim, and a long-lived embedder of `plgc`'s lib would keep failing (the `OnceLock` memoizes the now-deleted path for the process lifetime). Mixed-build concurrency is routine on a dev box: `cargo test`-built plgc sweeping while the installed plgc is mid-link in another terminal. Cheap fix that also preserves the steady-state goal: age-gate the sibling sweep (only remove `runtime-*` dirs with mtime older than ~7 days). Disk stays bounded; the race now requires actively linking with a week-old build at the exact moment of a sweep. ### 3. The HOME-less fallback is cross-user shared with a predictable name (security) `temp_dir().join("plgc-cache")` on a system without `TMPDIR` is `/tmp/plgc-cache` — world-writable parent, predictable path, and the hash in the dir name is public (deterministic from released runtime bytes). Another user can pre-plant `runtime-<hash>/libplg_runtime.a`; the reuse check is length-only, which a padded malicious archive satisfies — attacker code linked into the victim's binaries. Given this project's whole attested-immutable-binary posture, the fallback should be per-user: suffix the uid (`plgc-cache-<uid>`) and/or verify content hash on reuse in the fallback path. (`~/.cache` is fine as-is — user-owned.) ### 4. Optional hardening: verify content on reuse The cache is content-*named* but not content-*verified*: reuse trusts dir name + length. Re-hashing 21MB with the same FNV on the fast path costs ~10ms against clang's seconds, and would make corruption/poisoning self-correcting everywhere, not just in the fallback. Optional in `~/.cache`; I'd consider it required for the temp fallback (finding 3). ### 5. Doc staleness (one line) `docs/ARCHITECTURE.md:27` still says the archive is "extracted to a temp dir, passed to clang, and deleted" — needs the cache-scheme wording. --- Overall: the design is right (content-keyed shared cache is exactly what #4 needs) and the implementation is careful where it's careful (atomic install, build-time hashing, XDG chain). Findings 1–2 are small, surgical changes to the sweep; 3 is a one-line path change. With those, this closes #4 properly.
Author
Owner

Review assessment — findings applied on this branch

All five findings were assessed; 1, 2, and 5 confirmed and applied as suggested
(one refinement), 3 applied with a stronger variant, 4 declined with rationale.

Applied

1. Orphaned temps (confirmed by repro — planted orphans survived warm runs,
66M cache).
Fixed with one refinement to the suggested patch: the sweep now
runs before the fast path inside the OnceLock init, not on the slow path
— otherwise warm runs (the common case) would never reclaim orphans. Aged
.libplg_runtime.a.* entries (>10 min) in the kept dir are removed;
fresh ones are spared so a live writer about to rename is never raced.
Verified: 1-hour-old orphan reclaimed on a warm run, fresh temp untouched.

2. Sibling sweep race. Age-gated as suggested: runtime-* siblings are
only removed once older than 7 days. Disk stays bounded at "builds actually
used this week"; sweeping a running older build now requires it to be a
week old at the exact moment of a cold-start sweep. Verified: 8-day-old
sibling swept, fresh sibling kept.

3. HOME-less fallback (applied, stronger variant). The suggested
uid-suffix is just as predictable (/tmp/plgc-cache-1000 can be pre-planted),
and FNV content-verification is not adversary-proof — FNV-1a is malleable, so
a padded malicious archive can match length and hash. Instead the fallback
now shares nothing: with no $XDG_CACHE_HOME/$HOME, the archive is
extracted into a per-link tempfile::TempDir (private, unpredictable name)
held across the clang call and removed on drop. ~21MB per link in that rare
environment buys zero attack surface. Verified: run succeeds, no
/tmp/plgc-cache, zero /tmp entries left behind.

5. Doc staleness. docs/ARCHITECTURE.md now describes the
content-addressed cache + age sweeps + ephemeral fallback (the old text's
"and deleted" was never true even pre-PR).

Declined

4. Content verification on reuse — with finding 3 resolved by
not-sharing, the "required for the fallback" half is moot; in user-owned
~/.cache the remaining threat is corruption, which the atomic
rename + length check already cover. And the same FNV-malleability point
means re-hashing wouldn't deliver the poisoning protection it implies, at
the cost of a 21MB read on every warm run. If verification is ever wanted,
it should be a cryptographic digest — a different, deliberate change.

State

just ci on the branch after the changes; full behavior matrix re-verified
(warm reuse, cold concurrent, orphan sweep, sibling age gate, ephemeral
fallback).

## Review assessment — findings applied on this branch All five findings were assessed; 1, 2, and 5 confirmed and applied as suggested (one refinement), 3 applied with a stronger variant, 4 declined with rationale. ### Applied **1. Orphaned temps (confirmed by repro — planted orphans survived warm runs, 66M cache).** Fixed with one refinement to the suggested patch: the sweep now runs **before the fast path** inside the `OnceLock` init, not on the slow path — otherwise warm runs (the common case) would never reclaim orphans. Aged `.libplg_runtime.a.*` entries (>10 min) in the kept dir are removed; fresh ones are spared so a live writer about to `rename` is never raced. Verified: 1-hour-old orphan reclaimed on a warm run, fresh temp untouched. **2. Sibling sweep race.** Age-gated as suggested: `runtime-*` siblings are only removed once older than 7 days. Disk stays bounded at "builds actually used this week"; sweeping a *running* older build now requires it to be a week old at the exact moment of a cold-start sweep. Verified: 8-day-old sibling swept, fresh sibling kept. **3. HOME-less fallback (applied, stronger variant).** The suggested uid-suffix is just as predictable (`/tmp/plgc-cache-1000` can be pre-planted), and FNV content-verification is not adversary-proof — FNV-1a is malleable, so a padded malicious archive can match length *and* hash. Instead the fallback now **shares nothing**: with no `$XDG_CACHE_HOME`/`$HOME`, the archive is extracted into a per-link `tempfile::TempDir` (private, unpredictable name) held across the clang call and removed on drop. ~21MB per link in that rare environment buys zero attack surface. Verified: run succeeds, no `/tmp/plgc-cache`, zero `/tmp` entries left behind. **5. Doc staleness.** `docs/ARCHITECTURE.md` now describes the content-addressed cache + age sweeps + ephemeral fallback (the old text's "and deleted" was never true even pre-PR). ### Declined **4. Content verification on reuse** — with finding 3 resolved by not-sharing, the "required for the fallback" half is moot; in user-owned `~/.cache` the remaining threat is corruption, which the atomic rename + length check already cover. And the same FNV-malleability point means re-hashing wouldn't deliver the poisoning protection it implies, at the cost of a 21MB read on every warm run. If verification is ever wanted, it should be a cryptographic digest — a different, deliberate change. ### State `just ci` ✅ on the branch after the changes; full behavior matrix re-verified (warm reuse, cold concurrent, orphan sweep, sibling age gate, ephemeral fallback).
● All applied, verified, and recorded on PR 5. Summary of the disposition:
All checks were successful
CI - Linux / CI - Linux x86_64 (pull_request) Successful in 46s
28164b1cab
Finding: 1 — orphaned temps
  Action:  Applied with a refinement: sweep runs before the fast path (the
    review's placement on the slow path would never fire on warm runs, which
    are the common case). Age gate 10 min.
  Verified: 1-hour-old orphan reclaimed on a warm run; fresh temp
  (live-writer
    stand-in) untouched
  ────────────────────────────────────────
  Finding: 2 — sibling-sweep race
  Action:  Applied as suggested: 7-day age gate
  Verified: 8-day-old sibling swept; fresh sibling kept
  ────────────────────────────────────────
  Finding: 3 — poisonable fallback
  Action:  Applied stronger variant: HOME-less environments now share
    nothing — per-link TempDir, dropped after clang. (The suggested
  uid-suffix
     is equally predictable, and FNV content-verify isn't adversary-proof —
    FNV-1a is malleable.)
  Verified: run succeeds, no /tmp/plgc-cache, zero /tmp residue
  ────────────────────────────────────────
  Finding: 4 — verify on reuse
  Action:  Declined with rationale: moot for the fallback after #3,
    corruption already covered by atomic rename + length in user-owned
    ~/.cache, and FNV re-hash wouldn't deliver the implied poisoning
    protection while costing a 21MB read per warm run
  Verified:
  ────────────────────────────────────────
  Finding: 5 — doc staleness
  Action:  ARCHITECTURE.md now describes the cache + sweeps + ephemeral
    fallback (the old "and deleted" was never true even pre-PR)
  Verified:

  State: just ci  on file-cleanup after the changes; the full behavior
  matrix re-verified (warm reuse, cold-concurrent, both sweeps, fallback).
  The PR 5 comment records the assessment, what was applied, the #1
  refinement, and the #3/#4 deviations with reasoning.
navicore deleted branch file-cleanup 2026-06-05 05:50:11 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
navicore/patch-prolog!5
No description provided.