Skip to content

fix(core): carry the image's declared Env into the OCI bundle - #256

Merged
Lupus merged 3 commits into
mainfrom
fix/exec-image-path-env
Aug 22, 2026
Merged

fix(core): carry the image's declared Env into the OCI bundle#256
Lupus merged 3 commits into
mainfrom
fix/exec-image-path-env

Conversation

@Lupus

@Lupus Lupus commented Aug 21, 2026

Copy link
Copy Markdown
Owner

Closes #222

The reported axis is a red herring

izba exec <sandbox> -- cat /etc/os-release failed with crun's not found in $PATH while absolute paths worked. The issue framed this as ubuntu-vs-alpine. It is not: I pulled both raw config blobs and ubuntu:24.04 and alpine:3.20 declare a byte-identical PATH in identically-shaped OCI configs (both OCI index -> OCI manifest -> OCI config media types).

The real axis is cache vintage. Any image whose cache entry predates crun support breaks this way, on any distro.

Root cause, hop by hop

  1. ImageStore::is_cached keys only on rootfs.erofs — it never checks config.json.
  2. An image dir written by a pre-crun izba has the layers and no config blob, so it counts as fully cached. (All three entries in my local data root are exactly that shape.)
  3. The self-heal in ensure_image only runs on the registry path — the local-tag fast path returns before it, and izba start on an existing sandbox never calls ensure_image at all.
  4. load_config -> Ok(None).
  5. sandbox::start accepted that silently (the old comment said so verbatim).
  6. generate_spec's cfg.and_then(..).unwrap_or_default() -> empty image env, so oci/config.json's process.env shipped with no PATH.
  7. crun clearenv()s, seeds an exec from exactly that env (merge_env = true), then resolves the binary with getenv("PATH") — NULL. Hence the error.

izba's design comment at exec.rs:283 is correct and is preserved. crun does apply the image's PATH; izba must not guess a default. The bundle simply had nothing to apply. No default PATH is injected anywhere in this PR.

Changes

  • ImageStore::is_complete (rootfs and config.json). ensure_image's local-tag fast path now gates on it, so a config-less entry falls through to the registry path where the cheap self-heal already lives, instead of short-circuiting past it.
  • require_image_configsandbox::start fails loudly and actionably instead of silently building an env-less container. The distinction that matters: config absent = "we don't know the image's env" (a defect); config present but declaring no Env = "the image declares none" (oci-archive:, FROM scratch) and is passed through untouched.
  • Fixtures seed COMPLETE cache entries — the shape ensure_image guarantees. The fixture image declares a deliberately implausible PATH so a propagation assertion cannot pass by coincidentally matching a guessed default.
  • exec_collect_env — the integration suite injected PATH=STD_PATH into every exec and no call site could override it, so it would have passed even if the guest delivered an env-less container. Tests can now send an empty env, like the real CLI does.

Acceptance criteria

AC Status
1. bare cat/sh/ls succeed on ubuntu:24.04 non_alpine_image_bare_commands_resolve_via_the_images_declared_path (KVM)
2. observed PATH == image's declared PATH, not a hardcoded expectation asserted against the image's own OCI config read back from the image store, both host-side (start_propagates_the_images_declared_path_into_the_bundle) and in-VM (printenv PATH)
3. no default PATH injected; guard at exec.rs:890 still passes yes — plus a new matching guard on the ssh path
4. at least one non-alpine e2e ubuntu:24.04, the first non-Alpine-family sandbox rootfs in the suite
5. izba ssh / restricted login shell checked same crun exec, same container-definition env — identical exposure, fixed by the same host-side change, now guarded by ssh_session_crun_argv_never_injects_a_path

Verification

All six workspace gates green locally: cargo test --workspace (1382 in izba-core alone, 0 failed), clippy --workspace --all-targets -D warnings, fmt --check, izba-init musl (static-pie confirmed), windows-gnu check + clippy.

The host-side propagation test was verified non-vacuous by sabotage: with the image config forced to None it fails with process.env containing only the six trust-CA vars and no PATH — a verbatim reproduction of #222.

The new integration test is KVM-gated and e2e.yml does not run on PRs, so I am dispatching it manually on this branch; local artifacts here predate the crun/kernel work and would fail for unrelated reasons.

Note for review

start now refuses a sandbox whose image cache entry has no config.json. That is deliberate — such a sandbox is already broken (every bare command fails) — but it does turn a silent degradation into a hard error at start, so it is the judgement call most worth a second opinion. The alternative considered was self-healing at start via a manifest fetch, rejected because it would make izba start network-dependent.

🤖 Generated with Claude Code

Greptile Summary

This revision carries the image’s declared environment into generated OCI bundles and rejects legacy cache entries whose image configuration is unavailable.

  • Adds an explicit complete-cache predicate while preserving local-tag precedence.
  • Requires image configuration during sandbox startup and provides non-destructive recovery guidance.
  • Seeds complete image fixtures and adds host-side and KVM-gated PATH propagation coverage.
  • Adds guards ensuring exec and SSH paths do not inject a default PATH.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
crates/izba-core/src/image/mod.rs Preserves local-tag precedence for cached entries while documenting that missing runtime configuration is rejected during startup.
crates/izba-core/src/image/store.rs Adds is_complete to distinguish rootfs-only legacy entries from cache entries containing runtime configuration.
crates/izba-core/src/sandbox.rs Requires cached image configuration before generating the OCI bundle and propagates the image’s declared environment.
crates/izba-core/tests/integration.rs Adds caller-controlled exec environments and KVM-gated coverage for bare-command resolution through the image-declared PATH.
crates/izba-init/src/ssh.rs Adds a regression guard confirming SSH sessions do not inject their own PATH.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Start sandbox] --> B[Load cached image config]
  B -->|Present| C[Generate OCI bundle]
  C --> D[Copy image Env into process.env]
  D --> E[crun exec resolves bare commands using image PATH]
  B -->|Missing| F[Fail startup with cache-repair guidance]
Loading

Reviews (3): Last reviewed commit: "fix(core): keep local-tag precedence and..." | Re-trigger Greptile

Context used (3)

`izba exec <sandbox> -- cat` failed with crun's `not found in $PATH` on
some sandboxes while absolute paths worked. The reported axis (ubuntu vs
alpine) is a red herring: both images ship a byte-identical `PATH` in
identically-shaped OCI configs. The real axis is CACHE VINTAGE.

`ImageStore::is_cached` keys only on `rootfs.erofs`, so an image dir
written by a pre-crun izba -- layers present, `config.json` absent --
counts as fully cached. `load_config` then returns `Ok(None)`, which
`sandbox::start` accepted silently, and `generate_spec`'s
`unwrap_or_default()` turned into an empty image env. The bundle's
`process.env` shipped with no `PATH` at all. crun `clearenv()`s, seeds an
exec from exactly that env, then resolves the binary via
`getenv("PATH")` -- NULL -- so every bare command failed.

izba's design comment at `exec.rs:283` is correct and is preserved: crun
does apply the image's `PATH`, and izba must not guess a default. The
bundle simply had nothing to apply.

- add `ImageStore::is_complete` (rootfs AND config.json) and gate
  `ensure_image`'s local-tag fast path on it, so a config-less entry
  falls through to the registry path where the cheap self-heal lives
  instead of short-circuiting past it.
- `sandbox::start` now fails loudly and actionably via
  `require_image_config` instead of silently building an env-less
  container. A config that is PRESENT but declares no `Env` stays legal
  (`oci-archive:`, `FROM scratch`) -- "we don't know the image's env" is
  a defect, "the image declares none" is the truth about that image.
- test fixtures now seed COMPLETE cache entries, the shape `ensure_image`
  guarantees; the fixture image declares an implausible `PATH` so a
  propagation assertion cannot pass by matching a guessed default.
- integration: `exec_collect_env` lets a test send an EMPTY env like the
  real CLI does. The suite injected `PATH=STD_PATH` into every exec and
  would have passed even with an env-less container.
- integration: first non-Alpine sandbox in the suite (`ubuntu:24.04`) --
  bare `cat`/`sh`/`ls` plus `printenv PATH` compared against the image's
  own declared PATH read back from the image store, never a constant.
- ssh: guard that the restricted login shell injects no `PATH` either; it
  reaches the workload through the same `crun exec` and the same
  container-definition env, so it had the identical exposure.

Refs #222

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01T8RtbNTsnQ47n5iH1C8guP
@Lupus Lupus added this to the v0.1.0 (MVP) milestone Aug 21, 2026
Comment thread crates/izba-core/src/image/mod.rs Outdated
Comment thread crates/izba-core/src/sandbox.rs
Lupus and others added 2 commits August 21, 2026 19:14
The 14 `daemon::server::tests` that boot a sandbox go through the same
`start` path and so hit the new "incomplete cache entry" refusal. Move
the fixture publish from `sandbox::tests` into `testutil::test_paths`
itself, so every fixture data root holds the COMPLETE entry shape that
`ensure_image` guarantees, and drop the now-redundant local shadow.

These tests were green locally and red in CI: they runtime-skip when the
sandbox denies `bind`, so a sandboxed `cargo test` run silently passes
over them. Verified this time by disabling the fixture and watching
`start_then_stop_via_mock_driver` fail unsandboxed with the exact CI
error, then restoring it.

Refs #222

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

Two P1 review findings, both verified against the code before changing
anything.

1. Gating the local-tag fast path on `is_complete` opened an image
   SUBSTITUTION hole: `image_ref` there is a local tag, so falling
   through handed that bare name to registry resolution, letting a remote
   repository of the same name stand in for the locally tagged image --
   and breaking the local image outright when no such repository exists.
   Tag precedence is a trust property and outranks cache repair, so the
   gate goes back to `is_cached`. A locally built image has no registry
   to heal from anyway; the config-less case is caught loudly at start.
   The test that pinned the fall-through is replaced by one pinning
   precedence (offline, so returning the tagged digest also proves no
   network substitution was attempted).

2. The refusal told users to `izba rm` and re-create the sandbox. `rm`
   removes the entire sandbox dir -- rw.img and ephemeral volumes -- so
   the prescribed fix destroyed data to repair an IMAGE CACHE entry. The
   message now repairs the cache in place (re-pull via a throwaway
   create, or `izba build` for a local image) and explicitly warns
   against rm-ing the sandbox. Asserted by the test, which no longer
   accepts "mentions izba rm" as evidence of being actionable.

Refs #222

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

Copy link
Copy Markdown

@Lupus
Lupus merged commit e505627 into main Aug 22, 2026
56 checks passed
@Lupus
Lupus deleted the fix/exec-image-path-env branch August 22, 2026 08:22
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.

izba exec: no PATH in env for some images (ubuntu:24.04) — bare commands fail with 'not found in $PATH'

1 participant