From e7dac86eeecde2d72bb233b87dd3bab5f1a9baac Mon Sep 17 00:00:00 2001 From: Viorel Munteanu Date: Sun, 23 Aug 2026 15:12:22 +0000 Subject: [PATCH] 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 Claude-Session: https://claude.ai/code/session_01Gv3s5jckJKzh6JkMoi2Akz --- CLAUDE.md | 57 ++++++++++++++++++++++++++++------ README.md | 5 +++ meson.build | 2 +- src/bwrap.cpp | 33 +++++++++++++++----- src/bwrap.h | 14 ++++++--- src/env_spec.cpp | 81 ++++++++++++++++++++++++++++++++++++++++++++++++ src/env_spec.h | 44 ++++++++++++++++++++++++++ src/main.cpp | 42 ++++++++++++++++++++----- 8 files changed, 247 insertions(+), 31 deletions(-) create mode 100644 src/env_spec.cpp create mode 100644 src/env_spec.h diff --git a/CLAUDE.md b/CLAUDE.md index 8e0fadb..8194888 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 ` + (both long-option only, both repeatable) accumulate into a single ordered + `std::vector` — `--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 ` (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` diff --git a/README.md b/README.md index e1a6414..e52bb48 100644 --- a/README.md +++ b/README.md @@ -80,6 +80,8 @@ slocker-lite -V|--version | `--user ` | With `--run`, run the command as this user (name or numeric uid) instead of the image's own declared user (or root, if it declares none). Resolved against the image's own `/etc/passwd`. Only takes effect when `--run` executes as root. | | `--group ` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. | | `--hostname ` | With `--run`, set the sandbox's hostname. Only takes effect if the running kernel supports `--unshare-uts`; ignored with a warning otherwise. | +| `--env VAR=VALUE` | With `--run`, set an environment variable in the sandbox (overrides the default `PATH`/`HOME`/`PWD`/`TERM` if given the same name). Repeatable; combined with `--env-file` in command-line order, each later one winning over an earlier one for the same name. | +| `--env-file ` | With `--run`, load environment variables from `` — one `VAR=VALUE` per line; blank lines and `#`-comments are skipped. Repeatable. | | `-l, --list-images ` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in ``, with their `name:tag`. | | `-i, --inspect ` | Print an image's declared user, exposed ports, env, volumes, and default command, without mounting or running it. | | `-e, --exec ` | Join an already-running `--run` session (`` must be one `--list-processes` shows as `running`) and run a command inside its container. Pass `-- [args...]` to specify it. | @@ -112,6 +114,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git # Run with a custom hostname inside the sandbox ./buildDir/slocker-lite -r myimage.tar --hostname mybox +# Run with extra environment variables, from flags and/or a file +./buildDir/slocker-lite -r myimage.tar --env FOO=bar --env-file ./app.env + # List every OCI image tar in a directory ./buildDir/slocker-lite -l ./images diff --git a/meson.build b/meson.build index 317c4c7..0d5409e 100644 --- a/meson.build +++ b/meson.build @@ -19,7 +19,7 @@ configure_file(output : 'config.h', configuration : conf_data) slocker_lite = executable('slocker-lite', ['src/main.cpp', 'src/process.cpp', 'src/oci_image.cpp', 'src/containers_storage.cpp', 'src/bwrap.cpp', 'src/user_spec.cpp', 'src/config_file.cpp', 'src/volume_mount.cpp', - 'src/pid_file.cpp', 'src/exec_session.cpp'], + 'src/pid_file.cpp', 'src/exec_session.cpp', 'src/env_spec.cpp'], include_directories : include_directories('.'), dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep], install : true) diff --git a/src/bwrap.cpp b/src/bwrap.cpp index 62149ba..86fa987 100644 --- a/src/bwrap.cpp +++ b/src/bwrap.cpp @@ -74,8 +74,13 @@ std::optional find_priv_drop_helper() { // unset PWD in the first place (bwrap manages it separately, alongside --chdir), // so this isn't a straight port of a prior --setenv, just keeping the explicitly // constructed environment a complete, accurate match for what the sandbox should -// see. -std::vector> build_sandbox_env(std::optional user) { +// see. `extra_env` (from --env/--env-file, already resolved by +// resolve_env_specs() in env_spec.h) is appended last -- a duplicate key doesn't +// need deduplicating here, since run_process_foreground()'s own setenv(..., 1) +// loop naturally lets the later occurrence in iteration order win, so an +// explicit --env PATH=... still overrides the default above. +std::vector> build_sandbox_env( + std::optional user, const std::vector>& extra_env) { std::vector> env = { {"PATH", "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}, {"HOME", user ? user->home : "/root"}, @@ -84,6 +89,7 @@ std::vector> build_sandbox_env(std::optional if (const char* term = std::getenv("TERM")) { env.emplace_back("TERM", term); } + env.insert(env.end(), extra_env.begin(), extra_env.end()); return env; } @@ -208,7 +214,14 @@ std::vector build_bwrap_args(const std::string& root, // --new-session detaches from the controlling terminal, which breaks job // control for an interactive foreground shell ("can't access tty"). Re-enable // once background/daemonized runs are implemented, where that's the point. - std::vector args = {"bwrap", "--die-with-parent"}; + // Resolved to an absolute path (via this process's own PATH, not the + // sandbox's) rather than left as the bare name "bwrap" -- run_process_foreground() + // execs this argv[0] using build_sandbox_env()'s environment, so a --env + // override of PATH must not be able to break locating bwrap itself. Falls + // back to the bare name if not found here (checked earlier and unconditionally + // by check_required_dependencies() anyway). + auto bwrap_path = find_in_path("bwrap"); + std::vector args = {bwrap_path ? bwrap_path->string() : "bwrap", "--die-with-parent"}; auto unshare_args = detect_bwrap_unshare_args(); bool has_uts_ns = false; @@ -301,12 +314,17 @@ std::optional> wrap_for_root_namespace(const std::strin spdlog::error("could not find the fuse-overlayfs process serving {}", root); return std::nullopt; } - if (!find_in_path("nsenter")) { + auto nsenter_path = find_in_path("nsenter"); + if (!nsenter_path) { spdlog::error("nsenter not found in PATH"); return std::nullopt; } - std::vector wrapped = {"nsenter", fmt::format("--user=/proc/{}/ns/user", *pid), + // Resolved to an absolute path (this process's own PATH) rather than left as + // the bare name "nsenter" -- when wrapping a bwrap invocation, argv[0] here + // gets exec'd using build_sandbox_env()'s environment (see run_bwrap()), so a + // --env override of PATH must not be able to break locating nsenter itself. + std::vector wrapped = {nsenter_path->string(), fmt::format("--user=/proc/{}/ns/user", *pid), fmt::format("--mount=/proc/{}/ns/mnt", *pid), "--"}; wrapped.insert(wrapped.end(), argv.begin(), argv.end()); return wrapped; @@ -314,7 +332,8 @@ std::optional> wrap_for_root_namespace(const std::strin int run_bwrap(const std::string& root, const std::vector& command, bool use_nsenter, const std::vector& volumes, std::optional user, - const std::optional& hostname, const std::string& container_name) { + const std::optional& hostname, const std::string& container_name, + const std::vector>& extra_env) { if (user && !find_priv_drop_helper()) { spdlog::error("could not find the {} helper next to this binary; --user/--group requires it", kPrivDropHelperName); @@ -330,7 +349,7 @@ int run_bwrap(const std::string& root, const std::vector& command, std::optional session_lock; int exit_code = run_process_foreground( *argv, [&](pid_t pid) { session_lock = create_session_lock(container_name, pid); }, - build_sandbox_env(user)); + build_sandbox_env(user, extra_env)); if (session_lock) { release_session_lock(*session_lock); diff --git a/src/bwrap.h b/src/bwrap.h index 0f10e45..0902651 100644 --- a/src/bwrap.h +++ b/src/bwrap.h @@ -18,6 +18,7 @@ #include #include +#include #include #include "volume_mount.h" @@ -78,10 +79,13 @@ std::vector build_bwrap_args(const std::string& root, // see there for when it does/doesn't take effect. While bwrap is running, // `container_name` (paired with its actual pid) is recorded as a locked session // pid file under $XDG_STATE_HOME (see pid_file.h) -- removed again once it exits. -// The sandboxed command's environment is built directly here (build_sandbox_env()) -// and passed to run_process_foreground()'s own env override, rather than relying -// on bwrap's own --clearenv/--setenv (which build_bwrap_args() no longer uses). -// Returns bwrap's exit code, or -1 on failure to launch. +// The sandboxed command's environment is built directly here (build_sandbox_env()), +// with `extra_env` (from --env/--env-file, see env_spec.h) appended after the +// built-in PATH/HOME/PWD/TERM, and passed to run_process_foreground()'s own env +// override, rather than relying on bwrap's own --clearenv/--setenv (which +// build_bwrap_args() no longer uses). Returns bwrap's exit code, or -1 on +// failure to launch. int run_bwrap(const std::string& root, const std::vector& command, bool use_nsenter, const std::vector& volumes, std::optional user, - const std::optional& hostname, const std::string& container_name); + const std::optional& hostname, const std::string& container_name, + const std::vector>& extra_env); diff --git a/src/env_spec.cpp b/src/env_spec.cpp new file mode 100644 index 0000000..03815dc --- /dev/null +++ b/src/env_spec.cpp @@ -0,0 +1,81 @@ +// Copyright (C) 2026 Viorel Munteanu +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#include "env_spec.h" + +#include + +#include + +namespace { + +std::optional> parse_env_line(const std::string& line) { + auto eq = line.find('='); + if (eq == std::string::npos || eq == 0) { + return std::nullopt; + } + return std::make_pair(line.substr(0, eq), line.substr(eq + 1)); +} + +bool is_blank_or_comment(const std::string& line) { + auto first = line.find_first_not_of(" \t\r\n"); + return first == std::string::npos || line[first] == '#'; +} + +} // namespace + +std::optional>> resolve_env_specs( + const std::vector& specs) { + std::vector> result; + + for (const auto& spec : specs) { + if (!spec.is_file) { + auto parsed = parse_env_line(spec.value); + if (!parsed) { + spdlog::error("--env requires VARIABLE=VALUE, got '{}'", spec.value); + return std::nullopt; + } + result.push_back(std::move(*parsed)); + continue; + } + + std::ifstream file(spec.value); + if (!file) { + spdlog::error("could not open --env-file {}", spec.value); + return std::nullopt; + } + + std::string line; + size_t line_no = 0; + while (std::getline(file, line)) { + ++line_no; + if (!line.empty() && line.back() == '\r') { + line.pop_back(); + } + if (is_blank_or_comment(line)) { + continue; + } + auto parsed = parse_env_line(line); + if (!parsed) { + spdlog::error("{}:{}: expected VARIABLE=VALUE, got '{}'", spec.value, line_no, line); + return std::nullopt; + } + result.push_back(std::move(*parsed)); + } + } + + return result; +} diff --git a/src/env_spec.h b/src/env_spec.h new file mode 100644 index 0000000..04a8755 --- /dev/null +++ b/src/env_spec.h @@ -0,0 +1,44 @@ +// Copyright (C) 2026 Viorel Munteanu +// +// This program is free software; you can redistribute it and/or modify +// it under the terms of the GNU General Public License as published by +// the Free Software Foundation; either version 2 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along +// with this program; if not, write to the Free Software Foundation, Inc., +// 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +#pragma once + +#include +#include +#include +#include + +// One --env or --env-file occurrence, in the exact order given on the command +// line -- both flags share a single ordered list so a later one of either kind +// can override an earlier one (see resolve_env_specs()). +struct EnvSpec { + bool is_file; // true: `value` is a path (--env-file); false: a literal VAR=VALUE (--env) + std::string value; +}; + +// Resolves `specs`, in order, into a flat list of (key, value) pairs ready to +// append to the sandbox's base environment (see build_sandbox_env() in +// bwrap.cpp) -- later entries for the same key win, same as a real environment, +// since the caller just appends them all and setenv() naturally overwrites. +// A literal --env entry is split at its *first* '=' (VALUE may itself contain +// '='; the key before it must be non-empty). A --env-file entry 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: a +// literal/file-line with no '=' or an empty key, or a file that can't be opened. +std::optional>> resolve_env_specs( + const std::vector& specs); diff --git a/src/main.cpp b/src/main.cpp index b4acae7..1ff2293 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -34,6 +34,7 @@ #include "config.h" #include "config_file.h" #include "containers_storage.h" +#include "env_spec.h" #include "exec_session.h" #include "oci_image.h" #include "pid_file.h" @@ -64,10 +65,10 @@ enum class Mode { }; // --log-level/--user/--group/--list-volumes/--delete-volume[-full]/--hostname/ -// --list-processes/--clean-processes have no short form (--log-level's was freed -// up so -l could become --list-images; -u is already --umount; the rest have no -// natural free letter left, or don't need one), so they need long-option vals -// outside the printable-char range short options use. +// --list-processes/--clean-processes/--env/--env-file have no short form +// (--log-level's was freed up so -l could become --list-images; -u is already +// --umount; the rest have no natural free letter left, or don't need one), so +// they need long-option vals outside the printable-char range short options use. constexpr int kLogLevelOpt = 256; constexpr int kUserOpt = 257; constexpr int kGroupOpt = 258; @@ -77,8 +78,10 @@ constexpr int kDeleteVolumeFullOpt = 261; constexpr int kHostnameOpt = 262; constexpr int kListProcessesOpt = 263; constexpr int kCleanProcessesOpt = 264; +constexpr int kEnvOpt = 265; +constexpr int kEnvFileOpt = 266; -constexpr std::array kLongOptions = {{ +constexpr std::array kLongOptions = {{ {"help", no_argument, nullptr, 'h'}, {"version", no_argument, nullptr, 'V'}, {"test", no_argument, nullptr, 't'}, @@ -100,6 +103,8 @@ constexpr std::array kLongOptions = {{ {"hostname", required_argument, nullptr, kHostnameOpt}, {"list-processes", no_argument, nullptr, kListProcessesOpt}, {"clean-processes", no_argument, nullptr, kCleanProcessesOpt}, + {"env", required_argument, nullptr, kEnvOpt}, + {"env-file", required_argument, nullptr, kEnvFileOpt}, {nullptr, 0, nullptr, 0}, }}; @@ -151,6 +156,15 @@ void print_usage(const char* prog) { " takes effect if the running kernel supports\n" " --unshare-uts; otherwise ignored with a\n" " warning)\n" + " --env VAR=VALUE with --run, set an environment variable in the\n" + " sandbox (overrides the default PATH/HOME/PWD/\n" + " TERM if given the same name). May be repeated;\n" + " combined with --env-file in command-line order,\n" + " each later one winning over an earlier one for\n" + " the same name\n" + " --env-file with --run, load environment variables from\n" + " (one VAR=VALUE per line; blank lines and\n" + " #-comments are skipped). May be repeated\n" " -l, --list-images list OCI Image Layout tars (*.tar, *.tar.*) found\n" " directly in , with their name:tag\n" " -i, --inspect print an image's declared user, exposed ports,\n" @@ -526,7 +540,7 @@ int run_container(const std::filesystem::path& image_tar, const std::optional& user, const std::optional& group, const std::optional& hostname, const std::vector>& volume_specs, - const AppConfig& app_config) { + const std::vector& env_specs, const AppConfig& app_config) { auto mounted = mount_image(image_tar); if (!mounted) { return 1; @@ -564,6 +578,11 @@ int run_container(const std::filesystem::path& image_tar, volume_mounts.push_back(std::move(*resolved)); } + auto resolved_env = resolve_env_specs(env_specs); + if (!resolved_env) { + ok = false; + } + // Falls back to the image's own declared user (config.User) when --user wasn't // given on the command line, rather than always defaulting to root. std::optional effective_user = user; @@ -594,7 +613,7 @@ int run_container(const std::filesystem::path& image_tar, int exit_code = -1; if (ok) { exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, volume_mounts, resolved_user, hostname, - container_name); + container_name, *resolved_env); if (exit_code < 0) { spdlog::error("failed to run bwrap"); } @@ -629,6 +648,7 @@ int main(int argc, char* argv[]) { std::optional group_flag; std::optional hostname_flag; std::vector> volume_specs; + std::vector env_specs; opterr = 0; int opt; @@ -738,6 +758,12 @@ int main(int argc, char* argv[]) { case kHostnameOpt: hostname_flag = optarg; break; + case kEnvOpt: + env_specs.push_back({false, optarg}); + break; + case kEnvFileOpt: + env_specs.push_back({true, optarg}); + break; case ':': spdlog::error("option requires an argument: -{}", static_cast(optopt)); print_usage(argv[0]); @@ -835,7 +861,7 @@ int main(int argc, char* argv[]) { spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)"); } return run_container(mode_arg, command, use_nsenter, user_flag, group_flag, hostname_flag, volume_specs, - *config); + env_specs, *config); } auto mounted = mount_image(mode_arg);