Skip to content

fix: two reachable panics + two OCI image digest nondeterminisms - #1105

Merged
bryan-minimal merged 1 commit into
mainfrom
fix/audit-phase0
Jul 30, 2026
Merged

fix: two reachable panics + two OCI image digest nondeterminisms#1105
bryan-minimal merged 1 commit into
mainfrom
fix/audit-phase0

Conversation

@bryan-minimal

@bryan-minimal bryan-minimal commented Jul 30, 2026

Copy link
Copy Markdown
Member

Four small independent fixes: two reachable panics and two sources of OCI image digest instability. None of them change BuildSpec, the spec-hash encoding, or any cache key — behavior only changes on inputs that previously crashed, and for OCI images whose digests were previously unstable between identical runs.

1. mfile: empty exec string panicked the task parser

Task::exec_and_args tokenized a task's exec string with shlex and called .next().unwrap() on the token stream. An exec that is empty, whitespace-only, or otherwise yields no tokens has no first token, so minimal panicked on a malformed-but-easy-to-write minimal.toml:

[tasks.oops]
exec = ""

The list form already handles this case: exec = [] resolves to ("", []) and fails downstream with a real error. The fix makes the string form take the same path. Regression test included (exec_str_empty_does_not_panic).

2. lcache: special files in a cache directory panicked the walker

lcache's DirEntry::file_type classified entries as File or Dir and hit unreachable!() for anything else. But the local cache directory is ordinary host filesystem — a stray symlink, socket, or fifo (leftover tooling, a curious user, a crashed process) is entirely reachable, and one such entry made any cache walk panic. Added a FileType::Other variant so walks classify these instead of crashing. The cache itself never creates such entries; this only makes foreign ones survivable. No callers matched exhaustively on the enum, so no behavior changes for File/Dir.

3. op: OCI image layer order depended on thread scheduling

Image layers are built in parallel under a rayon::scope, and each worker pushed its finished layer into a shared Vec — i.e. layers landed in completion order, which varies run to run with thread scheduling. Layer order is part of the OCI manifest, and the manifest digest is the image digest, so two builds of the identical graph could produce different image digests. Workers now tag results with their dependency index and the results are sorted back into dependency order before manifest assembly — same layers, same content, stable order.

4. op: OCI config env order depended on HashMap iteration

The image config's env list was built by iterating a HashMap directly. Rust's HashMap iteration order is randomized per process, so the config blob — and with it the image digest — differed between identical builds. The vars are now sorted by key before serialization.

Why 3 and 4 matter beyond tidiness

These made op's OCI output non-reproducible even for perfectly reproducible inputs: the same graph, same artifacts, same everything produced a different image digest each run. Anything that wants to compare, cache, sign, or attest image digests needs digest stability first. (Because the previous digests were unstable, nothing could have validly pinned one — so stabilizing them breaks no existing expectation.)

Provenance

All four were findings of the formal-verification readiness audit of minimal's pure cores (write-up circulating separately); they are its "phase 0" — fixes that stand on their own regardless of any decision about that effort.

🤖 Generated with Claude Code

Note

Fix two reachable panics and two OCI image digest nondeterminisms

  • lcache::fs::FileType gains an Other variant; file_type() in fs.rs returns it for symlinks, sockets, and other non-file/non-directory entries instead of calling unreachable!().
  • Task::exec_and_args() in tasks.rs returns Some(("", vec![])) for empty or whitespace-only exec strings instead of panicking on an unwrap.
  • Layer order in the OCI image manifest is now deterministic: parallel layer results are sorted by dependency index after the rayon scope completes in oci_image.rs.
  • Environment variables in the OCI image config are sorted by key before being written, fixing non-deterministic digest output.
  • Behavioral Change: callers matching on lcache::fs::FileType must now handle the new Other variant.

Macroscope summarized d8ae2d9.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bryan-minimal
bryan-minimal requested a review from a team as a code owner July 30, 2026 19:52
@coderabbitai

coderabbitai Bot commented Jul 30, 2026

Copy link
Copy Markdown

Warning

Review limit reached

You’ve reached a temporary PR review limit under our Fair Usage Limits Policy.

Your recent review volume is higher than typical usage, so adaptive limits are currently applied.

Next review available in: 29 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 1c3ed5fe-a985-484f-9db0-6c9c9c7d8176

📥 Commits

Reviewing files that changed from the base of the PR and between 063d478 and d8ae2d9.

📒 Files selected for processing (3)
  • crates/lcache/src/fs.rs
  • crates/mfile/src/tasks.rs
  • crates/op/src/oci_image.rs

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

@bryan-minimal
bryan-minimal merged commit 6c94948 into main Jul 30, 2026
29 checks passed
@bryan-minimal
bryan-minimal deleted the fix/audit-phase0 branch July 30, 2026 20:00
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.

2 participants