Reuse -v/--volume to mount named/host volumes into -r/--run

Volume names now reject '/', which lets a -v spec used with -r be told
apart as either an existing named volume or a host directory path. -v
becomes repeatable with -r, each mounting a volume at an absolute
container path; if the host directory is empty and the image already
has content there, it's copied in first (preserving numeric
ownership/permissions/links/xattrs-ACLs, degrading gracefully with a
warning if the host filesystem doesn't support xattrs). The
existence-check and copy run through the same nsenter-wrapped
namespace bwrap itself needs, since a rootless containers-storage
mount's content isn't otherwise visible to this process at all.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz
This commit is contained in:
2026-08-21 17:08:16 +00:00
parent d69b408d14
commit 545762d6de
8 changed files with 397 additions and 57 deletions
+47 -5
View File
@@ -34,7 +34,20 @@ Source layout (all under `src/`):
(also `std::filesystem::remove_all()`s the host directory — errors out before
touching the config if that fails, warns instead of failing if the directory was
already gone) — see `config_file.{h,cpp}` below for what a "volume" means here (a
distinct concept from `OciImageConfig::volumes`).
distinct concept from `OciImageConfig::volumes`). `-v/--volume` is dual-purpose:
used alone it's `create_volume_command()`; combined with `-r/--run` it instead
requests a volume mount (repeatable) and is resolved by `resolve_volume_mount()`
(see `volume_mount.{h,cpp}` below) instead. Since `-v` must be repeatable with
`-r` but each occurrence still takes two space-separated tokens, `main()`'s
getopt loop no longer lets `'v'` set `Mode` itself: it accumulates
`(spec, path)` pairs into `volume_specs` (consuming the second token manually,
with a guard against swallowing the next flag if there isn't one), and only
*after* the loop decides whether that means one standalone `Mode::kVolume` call
or, together with `-r`, threads `volume_specs` through to `run_container()`.
`run_container()` resolves each spec (erroring out, `ok = false`, same as a
failed `--user` resolution — `bwrap` is skipped but unmount/cleanup still runs)
into a `ResolvedVolumeMount`, rejecting a duplicate or non-absolute container
path first, and passes the resolved list to `run_bwrap()`.
- `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,
@@ -55,6 +68,13 @@ Source layout (all under `src/`):
- `bwrap.{h,cpp}``detect_bwrap_unshare_args()` probes the kernel (via a forked
`unshare(2)` per namespace type) for which `--unshare-xxx` flags `bwrap` can actually
use; `build_bwrap_args()`/`run_bwrap()` assemble and run the sandboxed command.
`build_bwrap_args()` also takes a `std::vector<ResolvedVolumeMount>` (see
`volume_mount.h` below) and appends one writable `--bind <host_directory>
<container_path>` per entry. `wrap_for_root_namespace()` is `run_bwrap()`'s own
nsenter-wrapping logic pulled out into a reusable, exported function — it's also
what `volume_mount.cpp`'s copy-into-an-empty-volume step uses to reach the
image's content when running rootless (see below); `run_bwrap()` itself now just
calls it once on the assembled `bwrap` argv.
`build_bwrap_args()` deliberately drops `--unshare-net` from what's actually passed
to `bwrap` even when the kernel supports it — without any network setup (e.g.
`slirp4netns`), unsharing it just leaves the sandbox with no network at all. Re-add
@@ -128,9 +148,28 @@ Source layout (all under `src/`):
untouched. **`VolumeEntry`/the `volumes` section is a distinct concept from
`OciImageConfig::volumes`**: this is a user-defined `name -> host directory`
mapping created via `-v/--volume`, not an image's own declared mount points (still
unconsumed, see `oci_image.{h,cpp}` above) — the two aren't connected yet, though a
future `-r/--run` volume-mounting feature would presumably look volumes up here by
name.
unconsumed, see `oci_image.{h,cpp}` above). `AppConfig`/`config.volumes` is looked
up by name in `resolve_volume_mount()` (`volume_mount.{h,cpp}`, see below), which
is how `-r/--run`'s own `-v` usage finds a named volume's host directory.
- `volume_mount.{h,cpp}``is_valid_volume_name()` (no `/`, checked by both
`create_volume_command()` and to tell a `-v` spec's name/path apart) and
`resolve_volume_mount()`, called once per `-v` occurrence from `run_container()`
when running with `-r`. A spec with no `/` is looked up in `config.volumes` by
name (error if unknown); one with `/` is treated as a host directory path and
`create_directories()`'d if missing. If the resulting host directory is empty and
the image already has non-empty content at the given container path, that content
is copied in first. Both the existence check and the copy run as a single
`sh -c '[ -d ... ] && cp -a ...'` invocation wrapped through
`wrap_for_root_namespace()` (`bwrap.h`) — **not** a plain `std::filesystem` check
— because a rootless `containers-storage mount`'s content isn't visible to this
process at all without `nsenter`, the same constraint `run_bwrap()` itself works
around (see the root-vs-rootless paragraph below). `cp -a
--preserve=mode,ownership,timestamps,links[,xattr]` does the copy; whether
`,xattr` is included is decided by a direct `setxattr()`/`removexattr()` probe on
the host directory (no new library dependency — Linux POSIX ACLs are themselves
stored as xattrs, so this one probe stands in for both, logging a single
`spdlog::warn` if unsupported). A nonzero `cp` exit is only ever a warning, never
fatal — often just an ownership-preservation shortfall when not running as root.
Errors are logged via `spdlog::error`; every external command is also traced at debug
level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via
@@ -145,7 +184,10 @@ leaves the result invisible to a plain shell or child process outside that names
Confirmed `containers-storage unshare` does **not** rejoin an already-running mount's
namespace; only `nsenter` targeting the live `fuse-overlayfs` daemon's PID does.
`-r/--run` handles this automatically by locating that PID and running `bwrap` via
`nsenter` into its namespaces. **Running as root sidesteps all of this**: no privilege
`nsenter` into its namespaces (`wrap_for_root_namespace()`, `src/bwrap.h`) — reused
as-is by `volume_mount.cpp`'s copy-into-an-empty-volume step, since that also needs
to read image content that's otherwise invisible outside the same namespace.
**Running as root sidesteps all of this**: no privilege
reexec is needed, so the mount is already directly visible in the current namespace,
and `nsenter --user=...` into it then fails ("reassociate to namespace 'ns/user'
failed: Invalid argument") since the caller is already in that same user namespace.