Add --env/--env-file to set environment variables under -r/--run

Both flags are repeatable and share a single ordered EnvSpec list
(env_spec.{h,cpp}) so a later --env or --env-file always overrides an
earlier one for the same name, regardless of which flag set it.
--env-file reads one VAR=VALUE per line, skipping blank lines and
#-comments. resolve_env_specs()'s result is appended after
build_sandbox_env()'s own PATH/HOME/PWD/TERM defaults, letting an
explicit --env override any of them too.

Testing this surfaced a real bug in the environment-at-exec-time
mechanism from the previous change (dropping bwrap's own --clearenv):
since bwrap/nsenter are now exec'd with the same explicitly-built
environment the sandbox sees, a --env PATH=... override broke
execvp()'s ability to even locate bwrap/nsenter themselves (bare-name
PATH lookup happens in the child, using the already-overridden PATH).
Fixed by resolving both to absolute paths via find_in_path(), called
from this process's own unmodified environment before fork() --
confirmed by testing that only the sandboxed command's own lookup is
now affected by a PATH override, not bwrap/nsenter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-23 15:12:22 +00:00
parent bec5456c33
commit e7dac86eee
8 changed files with 247 additions and 31 deletions
+47 -10
View File
@@ -74,7 +74,31 @@ Source layout (all under `src/`):
below): `read_image_ref()` (`oci_image.{h,cpp}`) applied to the single image
tar being run, formatted as `name:tag`, falling back to the tar's own filename
stem if `read_image_ref()` can't determine one — passed through to
`run_bwrap()` alongside everything else.
`run_bwrap()` alongside everything else. `--env VAR=VALUE`/`--env-file <file>`
(both long-option only, both repeatable) accumulate into a single ordered
`std::vector<EnvSpec>``--env` pushes `{false, optarg}`, `--env-file` pushes
`{true, optarg}` — preserving their exact relative command-line order across
*both* flags (not two separate lists), since `resolve_env_specs()`
(`env_spec.{h,cpp}`, see below) needs that order to let a later one override
an earlier one for the same variable name. `run_container()` calls it once
(same `ok = false`-on-failure pattern as volume/user resolution) and passes
the resolved list to `run_bwrap()` as `extra_env`.
- `env_spec.{h,cpp}``resolve_env_specs()` turns an ordered list of
`EnvSpec {is_file, value}` (see `main.cpp` above) into a flat, ordered list of
`(key, value)` pairs. A literal (`--env`) is split at its *first* `=` (the
value may itself contain `=`; the key must be non-empty). A file (`--env-file`)
is read line by line: blank/whitespace-only lines and lines whose first
non-whitespace character is `#` are skipped (comments), with a trailing `\r`
stripped first for CRLF files; every other line is parsed the same way as a
literal. Logs a specific error and returns `nullopt` on the first hard failure
(malformed line, empty key, or an unreadable file) — deliberately stops at the
first line, not "skip and warn", since an env file with a typo should fail
loudly rather than silently omit a variable a container might depend on.
`build_sandbox_env()` (`bwrap.cpp`, see below) appends the resolved list after
its own built-in `PATH`/`HOME`/`PWD`/`TERM` — no deduplication needed there,
since `run_process_foreground()`'s own `setenv(..., 1)` loop already lets the
later occurrence in iteration order win for a repeated key, so an explicit
`--env PATH=...` still overrides the default.
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
@@ -145,14 +169,27 @@ Source layout (all under `src/`):
(`PATH`, `HOME`, `PWD` — hardcoded to `"/"`, matching `--chdir`'s own value; note
per bwrap's own man page `--clearenv` never actually unset `PWD` in the first
place, so this isn't a straight port of a prior `--setenv` — and `TERM`, only
if the host process has one) and `run_bwrap()` passes it straight to
`run_process_foreground()`'s own `env` override (see `process.{h,cpp}` below).
This works because `bwrap` (and `nsenter`, when interposed via
`wrap_for_root_namespace()`) doesn't alter its own inherited environment unless
told to, and neither does `slocker-lite-priv-drop` (just
`setgroups()`/`setgid()`/`setuid()`/`execvp()`, no env manipulation) — so
controlling it once, at the outermost exec, is sufficient for it to reach the
final sandboxed command unchanged.
if the host process has one, followed by `extra_env` — the resolved
`--env`/`--env-file` list from `resolve_env_specs()` (`env_spec.h`), appended
last so it can override the built-in defaults for the same key) and
`run_bwrap()` passes it straight to `run_process_foreground()`'s own `env`
override (see `process.{h,cpp}` below). This works because `bwrap` (and
`nsenter`, when interposed via `wrap_for_root_namespace()`) doesn't alter its
own inherited environment unless told to, and neither does
`slocker-lite-priv-drop` (just `setgroups()`/`setgid()`/`setuid()`/`execvp()`,
no env manipulation) — so controlling it once, at the outermost exec, is
sufficient for it to reach the final sandboxed command unchanged. **Because
that outermost exec now uses this same explicitly-built environment**,
`build_bwrap_args()`/`wrap_for_root_namespace()` resolve `bwrap`'s and
`nsenter`'s own argv[0] to an absolute path via `find_in_path()` (called from
this process's own, unmodified environment, before `fork()`) instead of
leaving them as bare names — confirmed by direct testing: `--env PATH=...`
used to break `execvp()`'s ability to even *locate* `bwrap`/`nsenter`
(bare-name lookup happens in the child, using the already-overridden PATH),
not just what the sandboxed command itself sees. With the fix, only the
*sandboxed command's own* lookup is affected by a `--env PATH=...` override
(as expected — same as overriding `PATH` in any real shell before running a
bare command name), and `bwrap`/`nsenter` are always found regardless.
- `priv_drop_helper.cpp` → the separate `slocker-lite-priv-drop` binary (its own
`executable()` target in `meson.build`, **built with `-static`**). Deliberately
has zero dependencies on the rest of this project (no fmt/spdlog/etc.) and is
@@ -362,7 +399,7 @@ Build directory is `buildDir/` (already configured).
- Run the executable: `./buildDir/slocker-lite -m <image.tar>` (see `--help` for the
full flag list: `-m/--mount`, `-r/--run`, `-u/--umount`, `-c/--cleanup`,
`-l/--list-images`, `-i/--inspect`, `-e/--exec`, `-n/--no-nsenter`, `--user`, `--group`,
`--hostname`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--hostname`, `--env`, `--env-file`, `-v/--volume`, `--list-volumes`, `--delete-volume`,
`--delete-volume-full`, `--list-processes`, `--clean-processes`, `-t/--test`, `--log-level`,
`-h/--help`, `-V/--version`)
- Run tests: `meson test -C buildDir`