Skip to content

Prune stale firmware cache versions after download - #199

Merged
JAORMX merged 2 commits into
mainfrom
fix/12-prune-stale-firmware
Jun 29, 2026
Merged

Prune stale firmware cache versions after download#199
JAORMX merged 2 commits into
mainfrom
fix/12-prune-stale-firmware

Conversation

@JAORMX

@JAORMX JAORMX commented Jun 26, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #12 — the firmware cache grew unbounded across go-microvm version bumps.

Each version bump creates a new ~/.cache/broodbox/firmware/<version>/ directory (~tens of MB), but only the version pinned in go.mod is ever used. Old version dirs were never deleted.

Approach

In downloadFirmware, after a successful fresh download (manifest written, firmware lock still held), call pruneStaleFirmwareVersions:

  • Scan sibling entries under the cache root.
  • Remove any directory whose name isn't the version just downloaded.
  • Skip transient/temp entries: .firmware.lock (a file), firmware-*.tar.gz temp archives (files), and firmware-extract-* temp dirs (firmware- prefix guard).
  • Runs only on a fresh download, never on a cache hit (nothing changed, no point scanning).
  • Failures are logged at debug level and never propagated — the firmware is already cached successfully, so pruning is strictly best-effort.

The file lock is held throughout, so there are no concurrency concerns.

Also adds a task firmware-clean target to wipe the whole firmware cache manually (honors XDG_CACHE_HOME), as suggested in the issue.

Tests

  • multiple stale versions removed; current version, lock file, and transient temp entries all preserved
  • no-op when only the current version is present
  • no panic when the cache root doesn't exist

Verified: task lint (0 issues), full task test passes (incl. -race).

🤖 Generated with Claude Code

Each go-microvm version bump creates a new firmware cache directory at
~/.cache/broodbox/firmware/<version>/, but only the pinned version is
ever used. Old version dirs (tens of MB each) were never deleted, so the
cache grew unbounded.

After a successful fresh download (with the firmware lock held), scan
sibling version dirs under the cache root and remove any that don't match
the version just downloaded. Pruning runs only on a fresh download, not
on a cache hit, and never on transient temp entries (.firmware.lock,
firmware-*.tar.gz, firmware-extract-*). Failures are logged at debug
level but never fail the download — the firmware is already cached.

Also add a `task firmware-clean` target for manual cache removal.

Fixes #12

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VP887qH8BMW4PMUXBuEqGc

@JAORMX JAORMX left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Panel review — 3 axes, 4 reviewers

Ran a three-axis panel review (Spec / Standards / Domain) with the Domain axis fanned out to secure-code-reviewer, software-architect, devops-expert, and library-reuse-reviewer. Findings are inline as line-anchored comments; summary below.

Spec — ✅ clean

All five implementation requirements in issue #12 are satisfied (list cacheRoot, skip lock/temp entries, remove non-current version dirs, debug log per prune, failures don't fail the download). No scope creep — the task firmware-clean target is explicitly in scope. Call site is fresh-download-only with the flock held, matching the spec.

Standards — ✅ clean

SPDX headers present, log/slog exclusively, correct DDD layer (internal/infra/vm/, no domain/sandbox/client leakage), table-driven tests alongside code, imperative commit message.

Domain — 0 ship-blockers, 1 mechanical, 1 cross-confirmed Info, 1 polish

  • [Medium] firmware-clean resolves the cache root differently from the Go code on macOS → silently no-ops on a supported platform. See Taskfile.yaml comment.
  • [Info — cross-confirmed] firmware- prefix guard is magic-string coupled to the download temp prefixes. Two reviewers independently concluded it's defense-in-depth, not load-bearing (failure mode is "leaves garbage," not "deletes something needed"). Optional shared constant.
  • [Info] Prune failures at Debug may hide a consistently-failing prune from default log output. Consider Warn for unexpected RemoveAll errors.

Most important single issue: the Medium macOS path drift in firmware-clean — the only finding that changes actual behavior on a supported platform. The Go-side change (pruneStaleFirmwareVersions) is sound; secure-code-reviewer verified no symlink/TOCTOU/path-traversal exposure under the actual threat model (0700 owner-only cache root, lock held, stdlib RemoveAll doesn't follow symlinks).

🤖 Generated with Claude Code

Comment thread Taskfile.yaml Outdated
Comment thread internal/infra/vm/firmware.go Outdated
Comment thread internal/infra/vm/firmware.go Outdated
- firmware-clean Taskfile target now mirrors xdg.CacheHome resolution
  (XDG_CACHE_HOME → Darwin /var/home/jaosorior/Library/Caches → /var/home/jaosorior/.cache) so
  macOS no longer removes the wrong path while firmware accumulates.
- Hoist the transient-entry "firmware-" prefix into firmwareTempPrefix,
  referenced by both os.CreateTemp/os.MkdirTemp patterns and the prune
  skip-guard, so the coupling is explicit.
- Bump unexpected os.RemoveAll failures in pruneStaleFirmwareVersions
  to Warn (only "not exist" stays at Debug) so a persistently-failing
  removal doesn't silently refill the cache disk.
@JAORMX
JAORMX merged commit e3ffe7a into main Jun 29, 2026
8 checks passed
@JAORMX
JAORMX deleted the fix/12-prune-stale-firmware branch June 29, 2026 19:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Prune stale firmware cache versions

1 participant