Add -v/--volume to create a named volume mapped to a host directory

Creates the host directory if missing (warning if it already exists,
another if it's non-empty) and records name -> directory in the config
file's new "volumes" section. Fails if the name or directory is already
used by an existing volume.

This is a distinct concept from OciImageConfig::volumes (an image's own
declared mount points, still unconsumed) -- a user-defined volume, meant
to be referenced by name once -r/--run starts actually mounting volumes.

config_file.{h,cpp} gains write_config_file(), symmetric to the existing
load_config_file(), built on libyaml's document-building/emitter API.
Rewrites the whole file each time; global.log-level round-trips
untouched alongside the new volumes section.

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 12:59:37 +00:00
parent be5a0b8202
commit aac656be31
5 changed files with 223 additions and 39 deletions
+30 -17
View File
@@ -16,12 +16,15 @@ status); this file stays the dense, file-by-file reference. Still early-stage.
Source layout (all under `src/`): Source layout (all under `src/`):
- `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`, - `main.cpp` — CLI entry point, dependency checks, orchestration (`mount_image()`,
`run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`). `run_container()`, `cleanup_image()`, `unmount_image()`, `list_images_command()`,
`run_container()` unconditionally calls `read_oci_image_config()` and reuses the `create_volume_command()`). `run_container()` unconditionally calls
result for two independent defaults: the command to run (`Entrypoint ++ Cmd`) when `read_oci_image_config()` and reuses the result for two independent defaults: the
none is given on the command line, and, when `--user` wasn't given, the sandboxed command to run (`Entrypoint ++ Cmd`) when none is given on the command line, and,
process's user/group (`config.User`, split into `OciImageConfig::user`/`group`) — when `--user` wasn't given, the sandboxed process's user/group (`config.User`,
an explicit `--user`/`--group` on the command line always takes precedence. split into `OciImageConfig::user`/`group`) — an explicit `--user`/`--group` on the
command line always takes precedence. `create_volume_command()` implements
`-v/--volume <name> <directory>`: see `config_file.{h,cpp}` below for what a
"volume" means here (a distinct concept from `OciImageConfig::volumes`).
- `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive + - `oci_image.{h,cpp}` — validates/parses the OCI Image Layout tar (libarchive +
nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory nlohmann_json) and extracts layer blobs. `list_oci_images()` scans a directory
(non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive, (non-recursively) for `*.tar`/`*.tar.*` files and, for each valid OCI archive,
@@ -96,17 +99,27 @@ Source layout (all under `src/`):
run would skip `run_container()`'s unmount/cleanup entirely, leaving the layer run would skip `run_container()`'s unmount/cleanup entirely, leaving the layer
imported and/or mounted. imported and/or mounted.
- `config_file.{h,cpp}``load_config_file()` reads and parses (via libyaml's - `config_file.{h,cpp}``load_config_file()` reads and parses (via libyaml's
document API, `<yaml.h>`) the `global` section of the local YAML config file document API, `<yaml.h>`) the `global` and `volumes` sections of the local YAML
located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`, config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`,
falling back to `$HOME/.config/slocker-lite/config.yaml`). Only `global.log-level` falling back to `$HOME/.config/slocker-lite/config.yaml`). `global.log-level` is the
is supported today — other long options are one-shot flags, not settings, so they only supported `global` key — other long options are one-shot flags, not settings,
don't belong in a persistent config file. A missing file returns a so they don't belong in a persistent config file. A missing file returns a
default-constructed (empty) `AppConfig`, not an error; unknown sections/keys are default-constructed (empty) `AppConfig`, not an error; unknown sections/keys (and
ignored for forward-compatibility; malformed YAML syntax is a hard error. `main()` malformed individual volume entries) are ignored for forward-compatibility;
applies `config->log_level` (via the existing `apply_log_level()`) right after malformed YAML syntax is a hard error. `main()` applies `config->log_level` (via
`spdlog::cfg::load_env_levels()` and before parsing CLI options, so an explicit the existing `apply_log_level()`) right after `spdlog::cfg::load_env_levels()` and
`--log-level` on the command line always overwrites it afterward — same precedence before parsing CLI options, so an explicit `--log-level` on the command line always
pattern already used for `SPDLOG_LEVEL`. overwrites it afterward — same precedence pattern already used for `SPDLOG_LEVEL`.
`write_config_file()` writes the whole file back out (via libyaml's
document-building/emitter API, symmetric to the read side) — used by
`-v/--volume` (`create_volume_command()`, `main.cpp`) to persist a new
`VolumeEntry {name, directory}` into the `volumes` section, preserving `global`
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.
Errors are logged via `spdlog::error`; every external command is also traced at debug 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 level in `run_process()`/`run_process_foreground()` (`src/process.cpp`) — visible via
+19 -8
View File
@@ -17,9 +17,11 @@ running kernel actually supports, instead of requiring the full set.
## Status ## Status
Early-stage. Mounting, running, and dropping privileges to a specific user/group all Early-stage. Mounting, running, and dropping privileges to a specific user/group all
work. Volumes and image-declared networking (`Volumes`/`ExposedPorts`/`Env` from the work. Named volumes (`-v/--volume`) can be created and are persisted in the config
image config) are parsed but not yet applied, and there's no background/daemonized file, but aren't consumed by `-r/--run` yet. Image-declared networking
run mode yet. (`ExposedPorts`/`Env` from the image config, and the image's own separately-declared
`Volumes`) are parsed but not yet applied, and there's no background/daemonized run
mode yet.
## Requirements ## Requirements
@@ -55,6 +57,7 @@ slocker-lite -r|--run <image.tar> [-- <command> [args...]]
slocker-lite -u|--umount <layer-id> slocker-lite -u|--umount <layer-id>
slocker-lite -c|--cleanup <layer-id> slocker-lite -c|--cleanup <layer-id>
slocker-lite -l|--list-images <directory> slocker-lite -l|--list-images <directory>
slocker-lite -v|--volume <name> <directory>
slocker-lite -t|--test slocker-lite -t|--test
slocker-lite -h|--help slocker-lite -h|--help
slocker-lite -V|--version slocker-lite -V|--version
@@ -70,6 +73,7 @@ slocker-lite -V|--version
| `--user <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. | | `--user <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 <group>` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. | | `--group <group>` | With `--user`, use this group (name or numeric gid) instead of the user's primary group. |
| `-l, --list-images <dir>` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in `<dir>`, with their `name:tag`. | | `-l, --list-images <dir>` | List OCI Image Layout tars (`*.tar`, `*.tar.*`) found directly in `<dir>`, with their `name:tag`. |
| `-v, --volume <name> <dir>` | Create a named volume mapped to a host directory (created if missing), recorded in the config file's `volumes` section. Fails if the name or directory is already used by an existing volume. |
| `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. | | `-t, --test` | Print which `bwrap --unshare-xxx` namespaces the running kernel supports. |
| `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). | | `--log-level <level>` | Set log verbosity (`trace`, `debug`, `info`, `warn`, `error`, `critical`, `off`). |
| `-h, --help` | Print usage and exit. | | `-h, --help` | Print usage and exit. |
@@ -92,6 +96,9 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
# List every OCI image tar in a directory # List every OCI image tar in a directory
./buildDir/slocker-lite -l ./images ./buildDir/slocker-lite -l ./images
# Create a named volume backed by a host directory
./buildDir/slocker-lite -v mydata ~/slocker-volumes/mydata
``` ```
## Configuration ## Configuration
@@ -99,17 +106,21 @@ sudo ./buildDir/slocker-lite -r myimage.tar --user git
Persistent settings can be kept in a local YAML config file at Persistent settings can be kept in a local YAML config file at
`$XDG_CONFIG_HOME/slocker-lite/config.yaml` (falling back to `$XDG_CONFIG_HOME/slocker-lite/config.yaml` (falling back to
`$HOME/.config/slocker-lite/config.yaml` if `XDG_CONFIG_HOME` isn't set). The file is `$HOME/.config/slocker-lite/config.yaml` if `XDG_CONFIG_HOME` isn't set). The file is
organized into sections; only `global` exists today: organized into sections:
```yaml ```yaml
global: global:
log-level: debug log-level: debug
volumes:
mydata: /home/user/slocker-volumes/mydata
``` ```
Only options that make sense as a standing preference are supported here — right now `global.log-level` is the only standing preference supported today (one-shot
just `log-level` (one-shot commands like `--mount`/`--run`/`--user` don't belong in a commands like `--mount`/`--run`/`--user` don't belong in a config file). An explicit
config file). A missing config file is fine (nothing is overridden); an explicit `--log-level` on the command line always overrides the config file. The `volumes`
`--log-level` on the command line always overrides the config file. section is managed by `-v/--volume` (see above) rather than hand-edited — it's not
consumed by `-r/--run` yet. A missing config file is fine either way (nothing is
overridden, and one gets created the first time `-v/--volume` is used).
## How it works ## How it works
+85
View File
@@ -44,6 +44,17 @@ const yaml_node_t* find_in_mapping(yaml_document_t& document, const yaml_node_t&
return nullptr; return nullptr;
} }
int add_scalar(yaml_document_t& document, std::string_view value) {
return yaml_document_add_scalar(&document, reinterpret_cast<yaml_char_t*>(const_cast<char*>(YAML_STR_TAG)),
reinterpret_cast<const yaml_char_t*>(value.data()),
static_cast<int>(value.size()), YAML_PLAIN_SCALAR_STYLE);
}
int add_mapping(yaml_document_t& document) {
return yaml_document_add_mapping(&document, reinterpret_cast<yaml_char_t*>(const_cast<char*>(YAML_MAP_TAG)),
YAML_BLOCK_MAPPING_STYLE);
}
} // namespace } // namespace
std::filesystem::path config_file_path() { std::filesystem::path config_file_path() {
@@ -88,8 +99,82 @@ std::optional<AppConfig> load_config_file(const std::filesystem::path& path) {
} }
} }
} }
if (const yaml_node_t* volumes = find_in_mapping(document, *root, "volumes")) {
if (volumes->type == YAML_MAPPING_NODE) {
for (auto* pair = volumes->data.mapping.pairs.start;
pair < volumes->data.mapping.pairs.top; ++pair) {
yaml_node_t* key_node = yaml_document_get_node(&document, pair->key);
yaml_node_t* value_node = yaml_document_get_node(&document, pair->value);
if (key_node && key_node->type == YAML_SCALAR_NODE && value_node &&
value_node->type == YAML_SCALAR_NODE) {
config.volumes.push_back(
{std::string(scalar_value(*key_node)), std::string(scalar_value(*value_node))});
}
}
}
}
} }
yaml_document_delete(&document); yaml_document_delete(&document);
return config; return config;
} }
bool write_config_file(const std::filesystem::path& path, const AppConfig& config) {
std::error_code ec;
std::filesystem::create_directories(path.parent_path(), ec);
if (ec) {
spdlog::error("failed to create directory {}: {}", path.parent_path().string(), ec.message());
return false;
}
yaml_document_t document;
yaml_document_initialize(&document, nullptr, nullptr, nullptr, 1, 1);
int root = add_mapping(document);
if (config.log_level) {
int global = add_mapping(document);
yaml_document_append_mapping_pair(&document, global, add_scalar(document, "log-level"),
add_scalar(document, *config.log_level));
yaml_document_append_mapping_pair(&document, root, add_scalar(document, "global"), global);
}
if (!config.volumes.empty()) {
int volumes = add_mapping(document);
for (const auto& volume : config.volumes) {
yaml_document_append_mapping_pair(&document, volumes, add_scalar(document, volume.name),
add_scalar(document, volume.directory));
}
yaml_document_append_mapping_pair(&document, root, add_scalar(document, "volumes"), volumes);
}
FILE* file = std::fopen(path.c_str(), "w");
if (!file) {
spdlog::error("failed to open {} for writing", path.string());
yaml_document_delete(&document);
return false;
}
yaml_emitter_t emitter;
yaml_emitter_initialize(&emitter);
yaml_emitter_set_output_file(&emitter, file);
bool ok = yaml_emitter_open(&emitter) != 0;
if (!ok) {
// yaml_emitter_dump() below is what normally consumes/destroys `document` --
// since open failed and dump never runs, it must be deleted explicitly here.
yaml_document_delete(&document);
} else {
ok = yaml_emitter_dump(&emitter, &document) != 0;
ok = yaml_emitter_close(&emitter) != 0 && ok;
}
yaml_emitter_delete(&emitter);
std::fclose(file);
if (!ok) {
spdlog::error("failed to write config file {}", path.string());
return false;
}
return true;
}
+24 -7
View File
@@ -19,21 +19,38 @@
#include <filesystem> #include <filesystem>
#include <optional> #include <optional>
#include <string> #include <string>
#include <vector>
// A user-defined named volume (-v/--volume): maps `name` to a host `directory`.
// Unrelated to OciImageConfig::volumes, which are mount points an *image* declares
// it wants -- this is a separate, user-driven concept, tracked here so it can be
// referenced by name later once -r/--run starts consuming volumes.
struct VolumeEntry {
std::string name;
std::string directory; // stored absolute + lexically-normalized
};
// Fields that make sense to persist across invocations (one-shot flags like // Fields that make sense to persist across invocations (one-shot flags like
// -m/-r/--user don't belong here). Only the "global" section's log-level is // -m/-r/--user don't belong here). Only the "global" section's log-level and the
// supported today; add more optional fields as more long options gain config-file // "volumes" section are supported today; add more optional fields as more long
// support. // options gain config-file support.
struct AppConfig { struct AppConfig {
std::optional<std::string> log_level; // global.log-level std::optional<std::string> log_level; // global.log-level
std::vector<VolumeEntry> volumes; // volumes section: name -> directory
}; };
// $XDG_CONFIG_HOME/slocker-lite/config.yaml, or $HOME/.config/slocker-lite/config.yaml // $XDG_CONFIG_HOME/slocker-lite/config.yaml, or $HOME/.config/slocker-lite/config.yaml
// if XDG_CONFIG_HOME is unset/empty. // if XDG_CONFIG_HOME is unset/empty.
std::filesystem::path config_file_path(); std::filesystem::path config_file_path();
// Loads and parses `path`'s "global" section. A missing file is not an error -- // Loads and parses `path`'s "global" and "volumes" sections. A missing file is not
// returns a default-constructed AppConfig (nothing set). Unknown sections/keys are // an error -- returns a default-constructed AppConfig (nothing set). Unknown
// ignored, so the format stays forward-compatible. Malformed YAML syntax logs a // sections/keys (and malformed individual volume entries) are ignored, so the
// specific error and returns nullopt. // format stays forward-compatible. Malformed YAML syntax logs a specific error and
// returns nullopt.
std::optional<AppConfig> load_config_file(const std::filesystem::path& path); std::optional<AppConfig> load_config_file(const std::filesystem::path& path);
// Writes `config` back to `path` as YAML (global + volumes sections), creating
// `path`'s parent directory if needed. Rewrites the whole file. Logs a specific
// error and returns false on failure.
bool write_config_file(const std::filesystem::path& path, const AppConfig& config);
+65 -7
View File
@@ -41,7 +41,7 @@ namespace {
constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage", "bwrap"}; constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage", "bwrap"};
enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup, kListImages }; enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup, kListImages, kVolume };
// --log-level/--user/--group have no short form (--log-level's was freed up so -l // --log-level/--user/--group have no short form (--log-level's was freed up so -l
// could become --list-images; -u is already --umount), so they need long-option // could become --list-images; -u is already --umount), so they need long-option
@@ -50,7 +50,7 @@ constexpr int kLogLevelOpt = 256;
constexpr int kUserOpt = 257; constexpr int kUserOpt = 257;
constexpr int kGroupOpt = 258; constexpr int kGroupOpt = 258;
constexpr std::array<struct option, 13> kLongOptions = {{ constexpr std::array<struct option, 14> kLongOptions = {{
{"help", no_argument, nullptr, 'h'}, {"help", no_argument, nullptr, 'h'},
{"version", no_argument, nullptr, 'V'}, {"version", no_argument, nullptr, 'V'},
{"test", no_argument, nullptr, 't'}, {"test", no_argument, nullptr, 't'},
@@ -63,6 +63,7 @@ constexpr std::array<struct option, 13> kLongOptions = {{
{"list-images", required_argument, nullptr, 'l'}, {"list-images", required_argument, nullptr, 'l'},
{"user", required_argument, nullptr, kUserOpt}, {"user", required_argument, nullptr, kUserOpt},
{"group", required_argument, nullptr, kGroupOpt}, {"group", required_argument, nullptr, kGroupOpt},
{"volume", required_argument, nullptr, 'v'},
{nullptr, 0, nullptr, 0}, {nullptr, 0, nullptr, 0},
}}; }};
@@ -73,6 +74,7 @@ void print_usage(const char* prog) {
" {0} -u|--umount <layer-id>\n" " {0} -u|--umount <layer-id>\n"
" {0} -c|--cleanup <layer-id>\n" " {0} -c|--cleanup <layer-id>\n"
" {0} -l|--list-images <directory>\n" " {0} -l|--list-images <directory>\n"
" {0} -v|--volume <name> <directory>\n"
" {0} -t|--test\n" " {0} -t|--test\n"
" {0} -h|--help\n" " {0} -h|--help\n"
" {0} -V|--version\n" " {0} -V|--version\n"
@@ -104,6 +106,9 @@ void print_usage(const char* prog) {
" gid) instead of the user's own primary group\n" " gid) instead of the user's own primary group\n"
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n" " -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
" directly in <dir>, with their name:tag\n" " directly in <dir>, with their name:tag\n"
" -v, --volume <name> <dir> create a named volume mapped to a host directory\n"
" (created if missing), recorded in the config\n"
" file's volumes section\n"
" -t, --test run the test suite\n" " -t, --test run the test suite\n"
" --log-level <level> set log verbosity (trace, debug, info, warn,\n" " --log-level <level> set log verbosity (trace, debug, info, warn,\n"
" error, critical, off)\n" " error, critical, off)\n"
@@ -256,6 +261,45 @@ int list_images_command(const std::filesystem::path& dir) {
return 0; return 0;
} }
int create_volume_command(const std::string& name, const std::string& directory,
const std::filesystem::path& config_path, AppConfig& config) {
std::filesystem::path resolved = std::filesystem::absolute(directory).lexically_normal();
for (const auto& volume : config.volumes) {
if (volume.name == name) {
spdlog::error("a volume named '{}' already exists (directory: {})", name, volume.directory);
return 1;
}
if (volume.directory == resolved.string()) {
spdlog::error("directory {} is already used by volume '{}'", resolved.string(), volume.name);
return 1;
}
}
std::error_code ec;
bool created = std::filesystem::create_directories(resolved, ec);
if (ec) {
spdlog::error("failed to create directory {}: {}", resolved.string(), ec.message());
return 1;
}
if (!created) {
spdlog::warn("directory {} already exists", resolved.string());
std::error_code empty_ec;
bool empty = std::filesystem::is_empty(resolved, empty_ec);
if (!empty_ec && !empty) {
spdlog::warn("directory {} is not empty", resolved.string());
}
}
config.volumes.push_back({name, resolved.string()});
if (!write_config_file(config_path, config)) {
return 1;
}
fmt::print("created volume '{}' -> {}\n", name, resolved.string());
return 0;
}
int run_container(const std::filesystem::path& image_tar, int run_container(const std::filesystem::path& image_tar,
const std::vector<std::string>& requested_command, bool use_nsenter, const std::vector<std::string>& requested_command, bool use_nsenter,
const std::optional<std::string>& user, const std::optional<std::string>& group) { const std::optional<std::string>& user, const std::optional<std::string>& group) {
@@ -312,7 +356,8 @@ int run_container(const std::filesystem::path& image_tar,
int main(int argc, char* argv[]) { int main(int argc, char* argv[]) {
spdlog::cfg::load_env_levels(); spdlog::cfg::load_env_levels();
auto config = load_config_file(config_file_path()); std::filesystem::path config_path = config_file_path();
auto config = load_config_file(config_path);
if (!config) { if (!config) {
return 1; return 1;
} }
@@ -328,7 +373,7 @@ int main(int argc, char* argv[]) {
opterr = 0; opterr = 0;
int opt; int opt;
while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:", kLongOptions.data(), nullptr)) != -1) { while ((opt = getopt_long(argc, argv, ":hVtm:u:r:c:nl:v:", kLongOptions.data(), nullptr)) != -1) {
switch (opt) { switch (opt) {
case 'h': case 'h':
print_usage(argv[0]); print_usage(argv[0]);
@@ -341,7 +386,8 @@ int main(int argc, char* argv[]) {
case 'u': case 'u':
case 'r': case 'r':
case 'c': case 'c':
case 'l': { case 'l':
case 'v': {
Mode requested; Mode requested;
switch (opt) { switch (opt) {
case 't': case 't':
@@ -359,9 +405,12 @@ int main(int argc, char* argv[]) {
case 'c': case 'c':
requested = Mode::kCleanup; requested = Mode::kCleanup;
break; break;
default: case 'l':
requested = Mode::kListImages; requested = Mode::kListImages;
break; break;
default:
requested = Mode::kVolume;
break;
} }
if (mode != Mode::kNone && mode != requested) { if (mode != Mode::kNone && mode != requested) {
spdlog::error("multiple actions specified"); spdlog::error("multiple actions specified");
@@ -404,7 +453,13 @@ int main(int argc, char* argv[]) {
print_usage(argv[0]); print_usage(argv[0]);
return 1; return 1;
} }
if (mode != Mode::kRun && optind != argc) { if (mode == Mode::kVolume) {
if (optind + 1 != argc) {
spdlog::error("--volume requires a name and a directory");
print_usage(argv[0]);
return 1;
}
} else if (mode != Mode::kRun && optind != argc) {
print_usage(argv[0]); print_usage(argv[0]);
return 1; return 1;
} }
@@ -426,6 +481,9 @@ int main(int argc, char* argv[]) {
if (mode == Mode::kListImages) { if (mode == Mode::kListImages) {
return list_images_command(mode_arg); return list_images_command(mode_arg);
} }
if (mode == Mode::kVolume) {
return create_volume_command(mode_arg, argv[optind], config_path, *config);
}
if (mode == Mode::kRun) { if (mode == Mode::kRun) {
std::vector<std::string> command(argv + optind, argv + argc); std::vector<std::string> command(argv + optind, argv + argc);
// As root, containers-storage mount doesn't need to reexec into a private // As root, containers-storage mount doesn't need to reexec into a private