Skip to content

fix(rootfs): key guest rootfs cache on the resolved guest binary - #1096

Merged
DorianZheng merged 1 commit into
mainfrom
worktree-cheerful-cooking-lamport
Jul 30, 2026
Merged

DorianZheng merged 1 commit into
mainfrom
worktree-cheerful-cooking-lamport

Conversation

@DorianZheng

@DorianZheng DorianZheng commented Jul 30, 2026 •

Copy link
Copy Markdown
Member

Summary

The guest rootfs cache key came from BOXLITE_GUEST_HASH, a compile-time digest, but the binary it describes is resolved at run time by find_binary, which ranks BOXLITE_RUNTIME_DIR above the embedded runtime cache. A guest rebuilt after boxlite was compiled kept the old key, so an existing cache entry was reused and the VM booted the previous guest. GuestBinary becomes the single resolution point, which makes that divergence unrepresentable.

Changes

  • GuestBinary resolves, validates and identifies the guest binary once. One read feeds both the ELF check and the digest, and version_key, GC retention and the injected bytes all derive from that one value.
  • The reconciliation guard is deleted rather than extended. It only ran on a cache miss, so the cache-hit path — the silent one — was never covered.
  • BOXLITE_GUEST_HASH and its build-script emitter are removed, along with the now-dead validate_guest_binary.
  • sha2 0.10 → 0.11, which is what makes always-hashing affordable: 0.10's portable backend costs ~690 ms debug / ~36 ms release on the 18 MB guest. digest 0.11 drops LowerHex, so 16 digest sites move to hex::encode.
  • BaseDiskManager::gc_orphans reclaims base disks that no base_disk row and no backing chain claims. The per-kind collectors iterate DB records, so a file whose row is gone was unreclaimable; one real cache held 7 files against 1 row.
  • The extracted runtime cache directory is stamped with the build's commit for provenance. Correctness still comes from the manifest hash, and the commit is deliberately kept out of the rootfs version key.

How to verify

  • make test:unit:rust
  • End to end: make guest to rebuild the guest without rebuilding boxlite, then start a box. The cache key moves and a new rootfs is built instead of the stale one being reused; starting again with an unchanged guest hits the cache.

Risks / rollout

  • gc_orphans deletes files. It removes one only when no base_disk row of any kind names it and nothing backs onto it, walking chains from both box overlays and from the bases themselves. It fails closed on a database error and skips anything inside a 5-minute grace window.
  • Two tests (id_tracks_the_bytes_on_disk, version_key_follows_the_guest_binary_on_disk) cannot have a revert-and-watch-it-fail red side, because they call a symbol the fix introduces. That gap is recorded at both test sites; they were checked by mutation and by a booted VM instead.

Summary by CodeRabbit

  • New Features

    • Added automatic cleanup of unused base disk files, while protecting active and recently created data.
    • Added build commit information to runtime cache naming when available.
    • Improved guest runtime identification so cache updates reflect changes to the guest binary.
  • Bug Fixes

    • Recovery now attempts orphaned disk cleanup without interrupting the overall recovery process.
    • Improved validation and tracking of guest runtime binaries.
    • Standardized digest handling to preserve consistent image and cache verification.

The cache key came from BOXLITE_GUEST_HASH, a compile-time digest, but the
binary it describes is resolved at run time by find_binary, which ranks
BOXLITE_RUNTIME_DIR above the embedded runtime cache. A guest rebuilt after
boxlite was compiled kept the old key, so an existing cache entry was reused
and the VM booted the previous guest. The reconciliation guard that existed to
catch this only ran on a cache miss, and GC derived its retention suffix from
the same stale value.

GuestBinary becomes the single resolution point: one read feeds ELF validation
and the digest, and version_key, GC retention and the injected bytes all come
from that one value, so expected and actual cannot diverge. The guard and
BOXLITE_GUEST_HASH are removed rather than extended.

sha2 0.11 makes always hashing affordable — 0.10's portable backend costs
~690 ms debug / ~36 ms release on the 18 MB guest, which is why the
compile-time proxy existed. digest 0.11 drops LowerHex, so 16 digest sites
move to hex::encode.

Also reclaims base disks that no DB row and no backing chain claims. The
per-kind collectors iterate records, so a file whose row is gone is
unreclaimable; one real cache held 7 files against 1 row. The sweep is
filesystem-first, walks chains from both box overlays and from the bases
themselves, fails closed on a DB error, and skips files inside a grace window.

Also stamps the extracted runtime cache directory with the build's commit
(v{VERSION}-{COMMIT}-{HASH}) for provenance. Correctness still comes from the
manifest hash; the commit is deliberately kept out of the rootfs version key,
where it would split the cache for byte-identical guests.
@DorianZheng
DorianZheng requested a review from a team as a code owner July 30, 2026 23:07
@boxlite-agent

boxlite-agent Bot commented Jul 30, 2026 •

Copy link
Copy Markdown

📦 BoxLite review — looks good · 9279101

Review evidence

  • ✅ git diff --numstat origin/main...HEAD && git diff origin/main...HEAD — 20 files, +1000/-370 approx, reviewed in full
  • ✅ grep BOXLITE_GUEST_HASH/GuestBinaryHash/validate_guest_binary across src — no stale references left after refactor
  • ✅ grep fn read_backing_chain / MAX_BACKING_CHAIN_DEPTH in qcow2.rs — confirms docstring claims in base_disk.rs are accurate
  • ⚪ cargo check -p boxlite -p boxlite-shared — no rustc/cargo toolchain installed in sandbox

Risk notes

  • base_disk orphan GC (gc_orphans) — deletes files only startup-time, gated by ext filter, 300s mtime grace, DB fail-closed lookup, and dual (DB+backing-chain) claim check; tests cover snapshot-preservation, chain traversal, DB-error fail-closed, grace window, unrecognized files
  • guest binary resolution/caching (GuestBinary) — single process-wide OnceLock ties path+id together, eliminating prior possible drift between hashed and injected binary; verified logic only by reading code, no VM boot test run here
  • sha2 0.10->0.11 + hex crate migration — mechanical format!("{:x}") -> hex::encode(...) replacements across verifier.rs/storage.rs/store.rs/object.rs/archive.rs/layout.rs/build.rs; sampled all call sites, all consistent
  • git commit stamping (shared/build.rs, embedded.rs) — adds BOXLITE_GIT_COMMIT to cache dir name; guarded by is_tracked_here() to avoid stamping an unrelated enclosing repo; rerun-if-changed watches HEAD/symbolic-ref/packed-refs so stale stamps aren't reused
  • test/build execution — could not run cargo build/test/clippy — no rust toolchain in this sandbox; relied on static review of large, heavily-commented diff and existing unit tests in the diff
src/boxlite/src/vmm/guest_binary.rs
  GuestBinary::get/resolve_at  +197/-0  new cached path+id resolver
src/boxlite/src/rootfs/guest.rs
  GuestRootfsManager::build_and_install/gc  +76/-183  drops compile-time hash fallback logic
src/boxlite/src/disk/base_disk.rs
  BaseDiskManager::gc_orphans/referenced_backing_paths  +393/-0  new filesystem-first orphan sweep
src/boxlite/src/runtime/rt_impl.rs
  recover_boxes  +7/-0  wires gc_orphans into startup recovery
src/boxlite/src/vmm/guest_check.rs
  validate_guest_bytes  +24/-51  path->bytes API change, avoids double read
src/boxlite/build.rs
  GuestBinaryHash (removed)  +1/-99  deletes compile-time guest hash embedding
src/shared/build.rs
  GitProvenance::emit  +89/-1  new git commit stamping for builds
src/shared/src/lib.rs
  GIT_COMMIT  +54/-0  exposes stamped commit, adds test
src/boxlite/src/runtime/embedded.rs
  EmbeddedRuntime::dir_name  +71/-11  cache dir now includes commit segment
src/boxlite/Cargo.toml
  deps  +6/-2  sha2 0.10->0.11, add hex 0.4.3
src/boxlite/src/util/mod.rs
  inject_guest_binary  +6/-3  now sources path via GuestBinary::get()

reviewed 9279101 in a BoxLite microVM · @boxlite-agent review to re-run · powered by BoxLite

@coderabbitai

coderabbitai Bot commented Jul 30, 2026 •

Copy link
Copy Markdown
Contributor

Review Change Stack

📝 Walkthrough

Walkthrough

This PR upgrades sha2 and migrates digest formatting to hex::encode, introduces a process-wide GuestBinary type that resolves and validates the guest binary once for cache-key derivation, adds orphan base-disk garbage collection wired into recovery, and stamps embedded runtime cache directories with the build's git commit.

Changes

Hashing modernization

Layer / File(s) Summary
sha2 upgrade and hex::encode migration
src/boxlite/Cargo.toml, src/boxlite/build.rs, src/boxlite/src/images/*, src/boxlite/src/litebox/archive.rs, src/boxlite/src/runtime/layout.rs
sha2 bumped to 0.11, hex added as a build dependency, and digest formatting switched from format!("{:x}", ...) to hex::encode(...) across verification, storage, and cache-path code with matching test updates.

Guest binary identity refactor

Layer / File(s) Summary
GuestBinary resolution type
src/boxlite/src/vmm/guest_binary.rs, src/boxlite/src/vmm/mod.rs
New GuestBinary type resolves boxlite-guest once per process, validates it, and computes a truncated SHA-256 identity, with accessors and tests for stability, content sensitivity, and error cases.
Byte-based ELF validation
src/boxlite/src/vmm/guest_check.rs
validate_guest_binary(path) is replaced by validate_guest_bytes(data, path), removing file I/O from validation and updating all tests to use in-memory data.
Build script and util consumers switch to GuestBinary
src/boxlite/build.rs, src/boxlite/src/util/mod.rs
GuestBinaryHash and its build-time emission are removed; util::inject_guest_binary now resolves the guest binary via GuestBinary::get().
Guest rootfs manager uses GuestBinary for cache keys
src/boxlite/src/rootfs/guest.rs
Cached guest-hash state is removed; cache version keys, injection, and GC now derive directly from the resolved GuestBinary ID, dropping stale-hash validation and fallback-key logic.

Orphan base-disk garbage collection

Layer / File(s) Summary
Orphan scanning and deletion logic
src/boxlite/src/disk/base_disk.rs
Adds referenced_backing_paths and gc_orphans, which identify unclaimed .ext4/.qcow2 base files via overlay and ancestry references, a grace period, and DB fail-closed checks, deleting only settled orphans, with extensive tests.
GC invocation from recovery and guest rootfs
src/boxlite/src/runtime/rt_impl.rs, src/boxlite/src/rootfs/guest.rs
Box recovery now runs gc_orphans after guest rootfs cleanup, and guest rootfs GC delegates backing-path discovery to BaseDiskManager.

Git commit provenance for cache naming

Layer / File(s) Summary
BOXLITE_GIT_COMMIT emission and GIT_COMMIT constant
src/shared/build.rs, src/shared/src/lib.rs
build.rs emits BOXLITE_GIT_COMMIT from tracked-manifest Git metadata; shared exposes a public GIT_COMMIT constant with tests for tracked/untracked repositories.
Cache directory naming includes commit segment
src/boxlite/src/runtime/embedded.rs, src/boxlite/src/util/binary_finder.rs
Embedded runtime cache directory names now include the optional commit via a new dir_name helper, with updated docs, TTLs, and tests.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant GuestRootfsManager
  participant GuestBinary
  participant guest_check
  participant Filesystem

  GuestRootfsManager->>GuestBinary: get()
  GuestBinary->>Filesystem: read boxlite-guest bytes
  GuestBinary->>guest_check: validate_guest_bytes(data, path)
  guest_check-->>GuestBinary: validation result
  GuestBinary->>GuestBinary: compute truncated SHA-256 id
  GuestBinary-->>GuestRootfsManager: path() and id()
  GuestRootfsManager->>GuestRootfsManager: derive cache version key from id
  GuestRootfsManager->>Filesystem: inject resolved guest binary
Loading
sequenceDiagram
  participant Recovery
  participant BaseDiskManager
  participant Database
  participant Filesystem

  Recovery->>BaseDiskManager: gc_orphans()
  BaseDiskManager->>Filesystem: scan .ext4/.qcow2 base files
  BaseDiskManager->>BaseDiskManager: referenced_backing_paths()
  BaseDiskManager->>Database: lookup base-disk records
  Database-->>BaseDiskManager: claimed or error
  BaseDiskManager->>BaseDiskManager: skip referenced, claimed, or recent files
  BaseDiskManager->>Filesystem: delete unclaimed settled files
  BaseDiskManager-->>Recovery: reclaimed file count
Loading

Possibly related issues

Possibly related PRs

  • boxlite-ai/boxlite#742: Overlaps with runtime/layout.rs and runtime/rt_impl.rs modifications, though this PR addresses cache hashing and base-disk cleanup.
  • boxlite-ai/boxlite#962: Directly overlaps by removing GuestBinaryHash from src/boxlite/build.rs and replacing dependency-based guest hashing with the new GuestBinary flow.

Suggested labels: e2e-local

Suggested reviewers: ltstriker

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the primary change: keying the guest rootfs cache to the resolved guest binary.
Description check ✅ Passed The description covers the required summary, notable changes, verification steps, and risks with specific, relevant details.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-cheerful-cooking-lamport

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.73684% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/shared/src/lib.rs 94.73% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
src/boxlite/src/disk/base_disk.rs (1)

131-155: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extract the repeated read_dir + warn-on-real-failure pattern.

The same match fs::read_dir(path) { Ok(..) => .., Err(e) => { if path.exists() { warn!(..) } } } shape appears three times (boxes_dir scan and bases_dir scan in referenced_backing_paths, and the bases_dir scan in gc_orphans). Extracting a small helper would remove the duplication and keep the "only warn when the path genuinely exists but is unreadable" semantics consistent in one place.

♻️ Proposed helper
+    fn read_dir_or_warn(path: &Path, label: &str) -> Option<fs::ReadDir> {
+        match fs::read_dir(path) {
+            Ok(entries) => Some(entries),
+            Err(e) => {
+                if path.exists() {
+                    tracing::warn!("GC: failed to read {} {}: {}", label, path.display(), e);
+                }
+                None
+            }
+        }
+    }

Then each call site collapses to, e.g.:

-        match fs::read_dir(boxes_dir) {
-            Ok(entries) => { ... }
-            Err(e) => {
-                if boxes_dir.exists() {
-                    tracing::warn!("GC: failed to read boxes dir {}: {}", boxes_dir.display(), e);
-                }
-            }
-        }
+        if let Some(entries) = Self::read_dir_or_warn(boxes_dir, "boxes dir") {
+            for entry in entries.flatten() { ... }
+        }

Also applies to: 158-173, 200-212

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/disk/base_disk.rs` around lines 131 - 155, Extract the
repeated directory-reading and warning logic into a small helper near the
relevant disk utilities, preserving the behavior of warning only when the
directory still exists but cannot be read. Update the boxes_dir and both
bases_dir scans in referenced_backing_paths and gc_orphans to use the helper
while keeping each call site’s existing entry-processing logic unchanged.
src/boxlite/src/runtime/rt_impl.rs (1)

1449-1455: 🩺 Stability & Availability | 🔵 Trivial

Orphan base-disk GC only runs once, at process startup.

Since gc_orphans is invoked solely from recover_boxes, orphaned base disks created by crashes or DB inconsistencies during a long-lived runtime's uptime won't be reclaimed until the next restart. For long-running deployments this could let disk usage grow between restarts. Worth considering a periodic sweep (e.g., on a timer or after N box removals) in a follow-up if disk pressure from orphaned bases proves to be an issue in practice.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/runtime/rt_impl.rs` around lines 1449 - 1455, Add a recurring
orphan base-disk cleanup trigger beyond the startup-only call in recover_boxes,
such as a timer or threshold after box removals, and reuse
base_disk_mgr.gc_orphans with the existing boxes_dir and warning behavior.
Ensure long-lived runtimes periodically reclaim orphaned base files without
changing the existing startup recovery flow.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@src/boxlite/src/disk/base_disk.rs`:
- Around line 131-155: Extract the repeated directory-reading and warning logic
into a small helper near the relevant disk utilities, preserving the behavior of
warning only when the directory still exists but cannot be read. Update the
boxes_dir and both bases_dir scans in referenced_backing_paths and gc_orphans to
use the helper while keeping each call site’s existing entry-processing logic
unchanged.

In `@src/boxlite/src/runtime/rt_impl.rs`:
- Around line 1449-1455: Add a recurring orphan base-disk cleanup trigger beyond
the startup-only call in recover_boxes, such as a timer or threshold after box
removals, and reuse base_disk_mgr.gc_orphans with the existing boxes_dir and
warning behavior. Ensure long-lived runtimes periodically reclaim orphaned base
files without changing the existing startup recovery flow.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: fab28222-5aaa-474b-81ed-cb0a9e4001bd

📥 Commits

Reviewing files that changed from the base of the PR and between 49cf415 and 9279101.

⛔ Files ignored due to path filters (1)
  • Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (19)
  • src/boxlite/Cargo.toml
  • src/boxlite/build.rs
  • src/boxlite/src/disk/base_disk.rs
  • src/boxlite/src/images/archive/verifier.rs
  • src/boxlite/src/images/object.rs
  • src/boxlite/src/images/storage.rs
  • src/boxlite/src/images/store.rs
  • src/boxlite/src/litebox/archive.rs
  • src/boxlite/src/rootfs/guest.rs
  • src/boxlite/src/runtime/embedded.rs
  • src/boxlite/src/runtime/layout.rs
  • src/boxlite/src/runtime/rt_impl.rs
  • src/boxlite/src/util/binary_finder.rs
  • src/boxlite/src/util/mod.rs
  • src/boxlite/src/vmm/guest_binary.rs
  • src/boxlite/src/vmm/guest_check.rs
  • src/boxlite/src/vmm/mod.rs
  • src/shared/build.rs
  • src/shared/src/lib.rs

@DorianZheng
DorianZheng merged commit 65a276d into main Jul 30, 2026
38 checks passed
@DorianZheng
DorianZheng deleted the worktree-cheerful-cooking-lamport branch July 30, 2026 23:38
G4614 pushed a commit to G4614/boxlite that referenced this pull request Aug 3, 2026
…lite-ai#1096)

## Summary

The guest rootfs cache key came from `BOXLITE_GUEST_HASH`, a
compile-time digest, but the binary it describes is resolved at run time
by `find_binary`, which ranks `BOXLITE_RUNTIME_DIR` above the embedded
runtime cache. A guest rebuilt after boxlite was compiled kept the old
key, so an existing cache entry was reused and the VM booted the
previous guest. `GuestBinary` becomes the single resolution point, which
makes that divergence unrepresentable.

## Changes

- `GuestBinary` resolves, validates and identifies the guest binary
once. One read feeds both the ELF check and the digest, and
`version_key`, GC retention and the injected bytes all derive from that
one value.
- The reconciliation guard is deleted rather than extended. It only ran
on a cache miss, so the cache-hit path — the silent one — was never
covered.
- `BOXLITE_GUEST_HASH` and its build-script emitter are removed, along
with the now-dead `validate_guest_binary`.
- `sha2` 0.10 → 0.11, which is what makes always-hashing affordable:
0.10's portable backend costs ~690 ms debug / ~36 ms release on the 18
MB guest. `digest` 0.11 drops `LowerHex`, so 16 digest sites move to
`hex::encode`.
- `BaseDiskManager::gc_orphans` reclaims base disks that no `base_disk`
row and no backing chain claims. The per-kind collectors iterate DB
records, so a file whose row is gone was unreclaimable; one real cache
held 7 files against 1 row.
- The extracted runtime cache directory is stamped with the build's
commit for provenance. Correctness still comes from the manifest hash,
and the commit is deliberately kept out of the rootfs version key.

## How to verify

- `make test:unit:rust`
- End to end: `make guest` to rebuild the guest without rebuilding
boxlite, then start a box. The cache key moves and a new rootfs is built
instead of the stale one being reused; starting again with an unchanged
guest hits the cache.

## Risks / rollout

- `gc_orphans` deletes files. It removes one only when no `base_disk`
row of any kind names it *and* nothing backs onto it, walking chains
from both box overlays and from the bases themselves. It fails closed on
a database error and skips anything inside a 5-minute grace window.
- Two tests (`id_tracks_the_bytes_on_disk`,
`version_key_follows_the_guest_binary_on_disk`) cannot have a
revert-and-watch-it-fail red side, because they call a symbol the fix
introduces. That gap is recorded at both test sites; they were checked
by mutation and by a booted VM instead.


<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->

## Summary by CodeRabbit

- **New Features**
- Added automatic cleanup of unused base disk files, while protecting
active and recently created data.
- Added build commit information to runtime cache naming when available.
- Improved guest runtime identification so cache updates reflect changes
to the guest binary.

- **Bug Fixes**
- Recovery now attempts orphaned disk cleanup without interrupting the
overall recovery process.
  - Improved validation and tracking of guest runtime binaries.
- Standardized digest handling to preserve consistent image and cache
verification.

<!-- end of auto-generated comment: release notes by coderabbit.ai -->
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.

1 participant