diff --git a/CLAUDE.md b/CLAUDE.md index 1a9dbe0..6e165fc 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -270,22 +270,34 @@ Source layout (all under `src/`): working code). Usage: `slocker-lite-priv-drop : -- [args...]`; does `setgroups(0,…)` → `setgid()` → `setuid()` → `execvp()`, in that order (dropping the group needs `CAP_SETGID`, which is lost once `setuid()` drops root). - `find_priv_drop_helper()` (`src/bwrap.cpp`) locates it next to `slocker-lite`'s own - binary (via `/proc/self/exe`'s directory), which holds both when run straight from - `buildDir/` and after a real `meson install`. + `find_priv_drop_helper()` and the `priv_drop::path`/`priv_drop::helper_name` + constants live in `bwrap.h` (not just internal to `bwrap.cpp`) specifically so + `exec_in_session()` (`exec_session.cpp`, see below) can reuse the exact same + already-bind-mounted helper for `-e/--exec`'s own `--user`/`--group` support, + instead of a second copy needing to be bind-mounted for it (which wouldn't even + be possible — `-e/--exec` joins an *already-running* session's mount namespace, + it doesn't get to add bind mounts to it). `find_priv_drop_helper()` itself only + checks this binary's own host-side existence; it says nothing about whether a + given session actually has it bind-mounted (only true when that session's + `-r/--run` resolved a user in the first place). - `user_spec.{h,cpp}` — `resolve_user_and_group()` resolves a user/group spec (each - a name or numeric id) against the *mounted image's own* `/etc/passwd`/`/etc/group` - (not the host's), since names like `git` only mean anything inside that image's own - user database. A numeric user with no group and no matching `/etc/passwd` entry - defaults gid to the same numeric value as the uid. `ResolvedUser` (`bwrap.h`) also - carries `home`, looked up by the final resolved uid's `/etc/passwd` entry (field 5) - regardless of whether `user` was given as a name or a number; falls back to - `"/root"` for uid 0 or `"/"` otherwise when there's no matching row. - `build_sandbox_env()` (`bwrap.cpp`) sets the sandboxed process's `HOME` from - this — `"/root"` only when no user override applies at all (no `--user`, no - image-declared `config.User`). `run_container()` (`commands.cpp`) calls `resolve_user_and_group()` - with either the explicit `--user`/`--group` flags, or, when `--user` wasn't given, - the image's own declared `config.User` (`OciImageConfig::user`/`group`) — so a + a name or numeric id) against the *container's own* `/etc/passwd`/`/etc/group` + **content** (not the host's, and not a path — callers own reading it, since the + two current callers get that content two different ways: `run_container()` + reads it directly off the merged mount path, while `exec_in_session()` fetches + it over `nsenter`, since a running session's mount namespace isn't otherwise + reachable from this process — see below). `nullopt` content for either file + means "unreadable/absent"; a numeric user with no group still resolves fine + without it (defaults gid to the same numeric value as the uid) but a named one + doesn't. `ResolvedUser` (`bwrap.h`) also carries `home`, looked up by the final + resolved uid's `/etc/passwd` entry (field 5) regardless of whether `user` was + given as a name or a number; falls back to `"/root"` for uid 0 or `"/"` + otherwise when there's no matching row. `build_sandbox_env()` (`bwrap.cpp`) + sets the sandboxed process's `HOME` from this — `"/root"` only when no user + override applies at all (no `--user`, no image-declared `config.User`). + `run_container()` (`commands.cpp`) calls `resolve_user_and_group()` with either + the explicit `--user`/`--group` flags, or, when `--user` wasn't given, the + image's own declared `config.User` (`OciImageConfig::user`/`group`) — so a container defaults to running as whatever user the image itself declares, not root, unless the image declares none. - `process.{h,cpp}` — argv-based subprocess helpers (fork/execvp, no shell): @@ -456,6 +468,51 @@ Source layout (all under `src/`): `nsenter ... -- ` via the existing `run_process_foreground()` (`process.h`) — same inherited stdio and SIGINT/SIGTERM forwarding as every other foreground external command, no new process-running logic needed. + `exec_in_session()` also takes optional `--user`/`--group` (mirroring `-r/--run`'s + own): given, they resolve against the session's own `/etc/passwd`/`/etc/group` + (fetched via `cat` run through the same `nsenter` join, since this process can't + otherwise see into that namespace, then handed to `resolve_user_and_group()` — + `user_spec.h`, see below); if unset, defaults to whatever uid/gid the session's + own sandboxed command is *already* running as (read from `/proc//status`), + rather than root/the caller — fixing a real bug (reported after this project's + own `-e/--exec` and priv-drop features had both shipped separately): without this, + `-e/--exec` always ran as whatever the *host* invocation was, ignoring any + `--user`/`--group` the session itself was started with. Either way, the resolved + identity is applied by running `command` through the session's already + bind-mounted `slocker-lite-priv-drop` helper (`priv_drop::path`, `bwrap.h`) — + reused as-is, not bind-mounted again (`-e/--exec` can't add bind mounts to an + already-running session's namespace anyway). Skipped entirely when the resolved + uid *and* gid are both 0: a session that was never given a resolvable user at + `-r/--run` time never got the helper bind-mounted at all, and dropping to 0:0 + would be a no-op regardless; a missing helper for a genuinely non-root + resolution instead surfaces as `nsenter`'s own "No such file or directory" once + it tries to exec `priv_drop::path`, diagnostic enough on its own. **Second real + bug, caught by direct testing on a rootless dev machine before this shipped**: + when the session's own `-r/--run` used `--unshare-user` (i.e. ran rootless — + see the root-vs-rootless paragraph below), "root inside the container" is + achieved purely through the kernel's own uid mapping for that namespace, not a + real privilege drop — so `/proc//status`'s uid/gid, read from *outside* + that namespace, shows the host-mapped id (e.g. `1000`), not the + container-relative one (`0`). Treating that as "needs a priv-drop to 1000" is + wrong two ways: the helper is typically never bind-mounted for a session with + no resolved `--user`, and even when it is, `setuid()` fails outright under the + single-entry uid map an unprivileged user namespace gets (confirmed directly: + `failed to drop privileges to 0:0: Operation not permitted`). Fixed by tracking + whether the `user` namespace type was actually one of the ones joined (it only + is when it differs from this process's own, i.e. exactly when `-r/--run` used + `--unshare-user`) and, when so, leaving the default identity unresolved (no + priv-drop) for that case — joining that same user namespace with + `--preserve-credentials` (already done regardless) already reproduces the + container's own view correctly via that same kernel mapping, with nothing + further needed. Verified end-to-end on this same rootless dev machine: a + daemonized `-r --run` busybox session with no declared user, `--exec`'d with no + `--user`, now correctly shows `uid=0(root)` (previously would have attempted, + and failed, a priv-drop to the host-mapped uid); an explicit `--exec --user 0` + against the same session correctly resolves to `0:0` and skips the priv-drop + step; `--exec --user portage` against it correctly resolves the name to its + real `250:250` via the fetched `/etc/passwd` and then fails clearly (helper not + bind-mounted, since the session itself had no declared user) rather than + silently running as the wrong identity. - `config_file.{h,cpp}` — `load_config_file()` reads and parses (via libyaml's document API, ``) the `global` and `volumes` sections of the local YAML config file located by `config_file_path()` (`$XDG_CONFIG_HOME/slocker-lite/config.yaml`, diff --git a/src/bwrap.cpp b/src/bwrap.cpp index 1d47350..100fa89 100644 --- a/src/bwrap.cpp +++ b/src/bwrap.cpp @@ -35,21 +35,6 @@ #include "pid_file.h" #include "process.h" -namespace { - -namespace priv_drop { -// Hidden path inside the sandbox where the priv-drop helper binary is bind-mounted -// when dropping privileges to a --user/--group (see build_bwrap_args()). -constexpr const char* path = "/.slocker-lite-priv-drop"; - -// Name of the statically-linked helper binary built alongside slocker-lite -// (src/priv_drop_helper.cpp) -- it has to be a separate, dependency-free static -// binary rather than slocker-lite's own binary, since bind-mounting a dynamically -// linked executable into an arbitrary container image fails ("error while loading -// shared libraries") when that image's own /lib lacks slocker-lite's dependencies. -constexpr const char* helper_name = "slocker-lite-priv-drop"; -} // namespace priv_drop - // Locates the priv-drop helper installed next to this process's own binary (found // via /proc/self/exe), which holds whether run from buildDir/ or after a proper // `meson install` -- both put slocker-lite and the helper in the same directory. @@ -66,6 +51,8 @@ std::optional find_priv_drop_helper() { return candidate; } +namespace { + // Builds the exact environment the sandboxed command should see. Passed directly // to run_process_foreground() as the environment to exec bwrap with, rather than // relying on bwrap's own --clearenv/--setenv (which run_bwrap() no longer uses) -- diff --git a/src/bwrap.h b/src/bwrap.h index 2869b73..8e795bd 100644 --- a/src/bwrap.h +++ b/src/bwrap.h @@ -16,6 +16,7 @@ #pragma once +#include #include #include #include @@ -26,6 +27,27 @@ #include "volume_mount.h" +namespace priv_drop { +// Hidden path inside the sandbox where the priv-drop helper binary is bind-mounted +// by build_bwrap_args() whenever a --user/--group was resolved for that session. +// Exported (not just internal to bwrap.cpp) so exec_in_session() (exec_session.cpp) +// can reuse that already-bind-mounted helper for -e/--exec's own --user/--group +// support, rather than needing a second copy bind-mounted for it. +constexpr const char* path = "/.slocker-lite-priv-drop"; + +// Name of the statically-linked helper binary built alongside slocker-lite +// (src/priv_drop_helper.cpp) -- it has to be a separate, dependency-free static +// binary rather than slocker-lite's own binary, since bind-mounting a dynamically +// linked executable into an arbitrary container image fails ("error while loading +// shared libraries") when that image's own /lib lacks slocker-lite's dependencies. +constexpr const char* helper_name = "slocker-lite-priv-drop"; +} // namespace priv_drop + +// Locates the priv-drop helper installed next to this process's own binary (found +// via /proc/self/exe), which holds whether run from buildDir/ or after a proper +// `meson install` -- both put slocker-lite and the helper in the same directory. +std::optional find_priv_drop_helper(); + // Probes the running kernel for which Linux namespace types can actually be // unshared and returns the corresponding bwrap --unshare-xxx flags for the // ones that are supported. Intended for kernels with partial namespace diff --git a/src/cli_args.cpp b/src/cli_args.cpp index aeb0c05..28c901c 100644 --- a/src/cli_args.cpp +++ b/src/cli_args.cpp @@ -123,7 +123,10 @@ void print_usage(const char* prog) { " declared user (or root, if it declares none),\n" " resolved against the image's own /etc/passwd;\n" " only takes effect when --run executes as root\n" - " (no user namespace involved)\n" + " (no user namespace involved). With --exec,\n" + " same idea but against a running session: run\n" + " as this user instead of whatever the session's\n" + " own sandboxed command is already running as\n" " --group with --user, use this group (name or numeric\n" " gid) instead of the user's own primary group\n" " --hostname with --run, set the sandbox's hostname (only\n" diff --git a/src/commands.cpp b/src/commands.cpp index 3bd6d36..8624706 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -19,8 +19,10 @@ #include #include #include +#include #include #include +#include #include #include #include @@ -46,6 +48,20 @@ namespace { constexpr std::array required_tools = {"containers-storage", "bwrap"}; +// Used to feed resolve_user_and_group() (user_spec.h) the image's own +// /etc/passwd and /etc/group content directly, rather than a path -- shared with +// how exec_in_session() (exec_session.cpp) supplies the same function content +// fetched over nsenter instead of a local file. +std::optional read_file_if_exists(const std::filesystem::path& path) { + std::ifstream in(path, std::ios::binary); + if (!in) { + return std::nullopt; + } + std::ostringstream contents; + contents << in.rdbuf(); + return contents.str(); +} + // Shared by list_images_command()/list_volumes_command()/list_processes_command(): // pad each entry with tabs (not spaces) so columns line up on an 8-column tab // stop past the longest entry in that column, however long that is. @@ -432,7 +448,9 @@ int run_container(const std::filesystem::path& image_tar, std::optional resolved_user; if (effective_user) { - resolved_user = resolve_user_and_group(*effective_user, effective_group, mounted->merged_path); + auto passwd_content = read_file_if_exists(std::filesystem::path(mounted->merged_path) / "etc" / "passwd"); + auto group_content = read_file_if_exists(std::filesystem::path(mounted->merged_path) / "etc" / "group"); + resolved_user = resolve_user_and_group(*effective_user, effective_group, passwd_content, group_content); if (!resolved_user) { ok = false; } @@ -507,7 +525,7 @@ int dispatch_command(const ParsedArgs& args, const std::filesystem::path& config case Mode::test: return run_self_tests(); case Mode::exec: - return exec_in_session(*args.exec_pid, args.command); + return exec_in_session(*args.exec_pid, args.command, args.user_flag, args.group_flag); case Mode::run: { // As root, containers-storage mount doesn't need to reexec into a private // user namespace to gain privilege, so the mount is already directly diff --git a/src/exec_session.cpp b/src/exec_session.cpp index ed99c38..85c8825 100644 --- a/src/exec_session.cpp +++ b/src/exec_session.cpp @@ -23,13 +23,17 @@ #include #include #include +#include #include +#include #include #include +#include "bwrap.h" #include "pid_file.h" #include "process.h" +#include "user_spec.h" namespace { @@ -143,9 +147,57 @@ std::optional read_ns_link(const std::filesystem::path& path) { return target.string(); } +// Fetches a file's content from within the target session's own mount namespace +// (already joined via `nsenter_prefix` -- e.g. {"nsenter", "--mount=...", ..., +// "--preserve-credentials", "--"}) by running `cat` through that same prefix and +// capturing its stdout: the session's own filesystem isn't otherwise visible to +// this process (see resolve_namespace_pid() above). Returns nullopt, not fatal by +// itself, if `path` doesn't exist or `cat` isn't available in the session -- +// resolve_user_and_group() (user_spec.h) already degrades gracefully for a +// numeric user/group when passwd/group content is unavailable. +std::optional read_container_file(const std::vector& nsenter_prefix, + const std::string& path) { + std::vector argv = nsenter_prefix; + argv.push_back("cat"); + argv.push_back(path); + auto result = run_process(argv); + if (result.exit_code != 0) { + return std::nullopt; + } + return result.stdout_output; +} + +// Reads the real uid/gid `ns_pid` is already running as, from its own +// /proc//status (globally visible by pid, no namespace-joining needed) -- +// used as -e/--exec's default privilege when --user isn't given, matching +// whatever uid/gid -r/--run itself resolved the session to run as (an explicit +// --user, the image's own declared user, or root). +std::optional> read_running_uid_gid(pid_t ns_pid) { + std::ifstream status(fmt::format("/proc/{}/status", ns_pid)); + std::string line; + std::optional uid; + std::optional gid; + while (std::getline(status, line)) { + std::istringstream field(line); + std::string label; + field >> label; + int real = 0; + if (label == "Uid:" && (field >> real)) { + uid = real; + } else if (label == "Gid:" && (field >> real)) { + gid = real; + } + } + if (!uid || !gid) { + return std::nullopt; + } + return std::make_pair(*uid, *gid); +} + } // namespace -int exec_in_session(pid_t pid, const std::vector& command) { +int exec_in_session(pid_t pid, const std::vector& command, const std::optional& user, + const std::optional& group) { if (pid <= 0) { spdlog::error("invalid pid: {}", pid); return 1; @@ -163,9 +215,15 @@ int exec_in_session(pid_t pid, const std::vector& command) { return 1; } + if (!find_in_path("nsenter")) { + spdlog::error("nsenter not found in PATH"); + return 1; + } + pid_t ns_pid = resolve_namespace_pid(pid); - std::vector argv = {"nsenter"}; + std::vector nsenter_prefix = {"nsenter"}; + bool joined_user_namespace = false; for (const auto& ns : joinable_namespaces) { auto target_ns = read_ns_link(fmt::format("/proc/{}/ns/{}", ns_pid, ns.proc_name)); if (!target_ns) { @@ -186,12 +244,10 @@ int exec_in_session(pid_t pid, const std::vector& command) { continue; } - argv.push_back(fmt::format("{}=/proc/{}/ns/{}", ns.nsenter_flag, ns_pid, ns.proc_name)); - } - - if (!find_in_path("nsenter")) { - spdlog::error("nsenter not found in PATH"); - return 1; + nsenter_prefix.push_back(fmt::format("{}=/proc/{}/ns/{}", ns.nsenter_flag, ns_pid, ns.proc_name)); + if (std::string_view(ns.proc_name) == "user") { + joined_user_namespace = true; + } } // Without this, nsenter --user tries to setgroups()/setuid()/setgid() to the @@ -200,10 +256,68 @@ int exec_in_session(pid_t pid, const std::vector& command) { // setgroups denied -- the kernel-enforced default for any unprivileged user // namespace, which is exactly what bwrap creates when run_container() isn't // root. We don't want nsenter changing our credentials anyway -- just join - // the namespaces and keep running as whatever this process already is. - argv.push_back("--preserve-credentials"); + // the namespaces and keep running as whatever this process already is (the + // priv-drop helper, below, is what actually changes credentials once inside). + nsenter_prefix.push_back("--preserve-credentials"); + nsenter_prefix.push_back("--"); - argv.push_back("--"); + // Same default-vs-override split as run_container() (commands.cpp): an + // explicit --user resolves against the session's own /etc/passwd/group + // (fetched over the same nsenter join, since this process can't otherwise + // see into that namespace); with no override, default to whatever the + // session's own sandboxed command is already running as, rather than + // whatever this exec runs as (root, typically) -- "what the container + // needs" already having been decided once, at -r/--run time. + // + // **Real bug caught by direct testing, not assumed**: when the session was + // started with its own --unshare-user (rootless -r/--run, joined_user_namespace + // here), "root inside the container" is achieved purely through the kernel's + // own uid mapping for that namespace -- there's no real privilege drop, so + // /proc//status's uid/gid (read from *outside* that namespace) shows + // the host-mapped id (e.g. 1000), not the container-relative one (0). Treating + // that as "needs a priv-drop to 1000" is wrong two ways: the helper was never + // bind-mounted for a session with no resolved --user, and even when it was, + // its setuid() fails outright under the single-entry uid map an unprivileged + // user namespace gets (confirmed directly: "failed to drop privileges to 0:0: + // Operation not permitted"). Joining that same user namespace here (already + // done above, since it necessarily differs from ours) with + // --preserve-credentials already reproduces the container's own view + // correctly via that same kernel mapping -- no extra step needed, so the + // default is left unresolved (no priv-drop) whenever joined_user_namespace is + // true. Only without a joined user namespace (the real target scenario this + // whole feature exists for: running as root, no user namespace, an actual + // setuid()-based privilege drop via the image's declared user or --user) does + // /proc//status's uid/gid mean what it looks like, and matter. + std::optional resolved_user; + if (user) { + auto passwd_content = read_container_file(nsenter_prefix, "/etc/passwd"); + auto group_content = read_container_file(nsenter_prefix, "/etc/group"); + resolved_user = resolve_user_and_group(*user, group, passwd_content, group_content); + if (!resolved_user) { + return 1; + } + } else if (!joined_user_namespace) { + if (auto ids = read_running_uid_gid(ns_pid)) { + resolved_user = ResolvedUser{ids->first, ids->second, ""}; + } else { + spdlog::warn( + "could not determine session {}'s current user; running --exec without a privilege drop", pid); + } + } + + std::vector argv = nsenter_prefix; + // Skipped when the resolved identity is plain root: a session that was never + // given a resolvable user (no explicit --user, no image-declared user at + // -r/--run time) never got the priv-drop helper bind-mounted in the first + // place, and dropping to 0:0 would be a no-op anyway. A missing helper for a + // non-root resolution surfaces as nsenter's own "No such file or directory" + // once it tries to exec priv_drop::path, which is diagnostic enough on its + // own without a separate existence check here. + if (resolved_user && (resolved_user->uid != 0 || resolved_user->gid != 0)) { + argv.push_back(priv_drop::path); + argv.push_back(fmt::format("{}:{}", resolved_user->uid, resolved_user->gid)); + argv.push_back("--"); + } argv.insert(argv.end(), command.begin(), command.end()); return run_process_foreground(argv); diff --git a/src/exec_session.h b/src/exec_session.h index 078fab7..158e71b 100644 --- a/src/exec_session.h +++ b/src/exec_session.h @@ -16,6 +16,7 @@ #pragma once +#include #include #include @@ -28,7 +29,22 @@ // process, but the sandboxed command actually runs in a *child* of it (bwrap sets // up the mount/user namespaces itself, then clone()s the real command into fresh // pid/uts/ipc/cgroup namespaces -- the parent never enters those itself), so this -// resolves that child internally before building nsenter's argv. Returns the exit -// code, or 1 if `pid` isn't a tracked/running session (logged with -// spdlog::error), or -1 if nsenter itself couldn't be launched. -int exec_in_session(pid_t pid, const std::vector& command); +// resolves that child internally before building nsenter's argv. +// +// If `user` is set, resolves it (and `group`, if set) against the session's own +// /etc/passwd/group -- same semantics, same resolve_user_and_group() (user_spec.h), +// as -r/--run's own --user/--group -- and runs `command` through the already +// bind-mounted slocker-lite-priv-drop helper (see bwrap.h) to drop to that +// uid/gid, exactly like the session's own main process did at -r/--run time. If +// `user` is unset, defaults to whatever uid/gid the session's own sandboxed +// command is *already* running as (read from /proc//status) instead of +// running the exec'd command as root/the caller -- "what the container needs", +// mirroring -r/--run's own image-declared-user default. The priv-drop step is +// skipped entirely when the resolved uid and gid are both 0, so a session that +// was never given a resolvable user (and so never got the helper bind-mounted at +// all) still works for the common "exec as root" case. Returns the exit code, or +// 1 if `pid` isn't a tracked/running session or --user/--group couldn't be +// resolved (both logged with spdlog::error), or -1 if nsenter itself couldn't be +// launched. +int exec_in_session(pid_t pid, const std::vector& command, const std::optional& user, + const std::optional& group); diff --git a/src/user_spec.cpp b/src/user_spec.cpp index 7328488..416feec 100644 --- a/src/user_spec.cpp +++ b/src/user_spec.cpp @@ -18,7 +18,6 @@ #include #include -#include #include #include @@ -47,15 +46,18 @@ std::optional parse_int(const std::string& s) { } } -// Looks up `key` by name (field 0) or numeric id (field `id_field`) in a -// colon-separated database file (/etc/passwd or /etc/group). Malformed lines are -// skipped rather than treated as errors. -std::optional> lookup_entry(const std::filesystem::path& db_file, +// Looks up `key` by name (field 0) or numeric id (field `id_field`) in the +// content of a colon-separated database file (/etc/passwd or /etc/group). +// Malformed lines are skipped rather than treated as errors. `content` is +// nullopt when the caller couldn't read the file at all -- treated the same as +// "not found" here, letting a numeric key still degrade gracefully (see +// resolve_user_and_group()). +std::optional> lookup_entry(const std::optional& content, const std::string& key, size_t id_field) { - std::ifstream in(db_file, std::ios::binary); - if (!in) { + if (!content) { return std::nullopt; } + std::istringstream in(*content); std::string line; while (std::getline(in, line)) { @@ -74,22 +76,21 @@ std::optional> lookup_entry(const std::filesystem::path std::optional resolve_user_and_group(const std::string& user, const std::optional& group, - const std::filesystem::path& image_root) { - std::filesystem::path passwd_file = image_root / "etc" / "passwd"; - + const std::optional& passwd_content, + const std::optional& group_content) { int uid = 0; int gid = 0; if (is_all_digits(user)) { uid = *parse_int(user); - auto entry = lookup_entry(passwd_file, user, 2); + auto entry = lookup_entry(passwd_content, user, 2); if (entry && entry->size() > 3) { gid = parse_int((*entry)[3]).value_or(uid); } else { gid = uid; } } else { - auto entry = lookup_entry(passwd_file, user, 2); + auto entry = lookup_entry(passwd_content, user, 2); if (!entry || entry->size() <= 3) { spdlog::error("could not resolve user '{}' in the image's /etc/passwd", user); return std::nullopt; @@ -108,8 +109,7 @@ std::optional resolve_user_and_group(const std::string& user, if (is_all_digits(*group)) { gid = *parse_int(*group); } else { - std::filesystem::path group_file = image_root / "etc" / "group"; - auto entry = lookup_entry(group_file, *group, 2); + auto entry = lookup_entry(group_content, *group, 2); auto entry_gid = entry ? parse_int((*entry)[2]) : std::nullopt; if (!entry_gid) { spdlog::error("could not resolve group '{}' in the image's /etc/group", *group); @@ -124,7 +124,7 @@ std::optional resolve_user_and_group(const std::string& user, // actually owns that uid. No matching row -> fall back to "/root" for uid 0 // (matches useradd-less images' own convention for root) or "/" otherwise. std::string home = uid == 0 ? "/root" : "/"; - auto home_entry = lookup_entry(passwd_file, std::to_string(uid), 2); + auto home_entry = lookup_entry(passwd_content, std::to_string(uid), 2); if (home_entry && home_entry->size() > 5 && !(*home_entry)[5].empty()) { home = (*home_entry)[5]; } diff --git a/src/user_spec.h b/src/user_spec.h index 446555d..8f67474 100644 --- a/src/user_spec.h +++ b/src/user_spec.h @@ -16,19 +16,24 @@ #pragma once -#include #include #include #include "bwrap.h" // Resolves --user (name or numeric uid) and --group (optional, name or numeric gid) -// against the mounted image's own /etc/passwd and /etc/group (rooted at -// `image_root`, the merged mount path) -- NOT the host's. A numeric `user` with no -// `group` and no matching /etc/passwd entry defaults gid to the same numeric value -// as the uid; a named `user` always requires a resolvable /etc/passwd entry (for its -// uid and default gid). Logs a specific error and returns nullopt if a named -// user/group can't be resolved, or if /etc/passwd is missing entirely. +// against the container's own /etc/passwd and /etc/group *content* -- NOT the +// host's. Callers are responsible for obtaining that content however is +// appropriate for them: run_container() (commands.cpp) reads it directly off the +// merged mount path, while exec_in_session() (exec_session.cpp) fetches it via +// nsenter, since a running session's mount namespace isn't otherwise reachable +// from this process. `nullopt` for either means "file unreadable/absent" -- a +// numeric `user`/`group` still resolves fine without it (see below); a named one +// doesn't. A numeric `user` with no `group` and no matching /etc/passwd entry +// defaults gid to the same numeric value as the uid; a named `user` always +// requires a resolvable /etc/passwd entry (for its uid and default gid). Logs a +// specific error and returns nullopt if a named user/group can't be resolved. std::optional resolve_user_and_group(const std::string& user, const std::optional& group, - const std::filesystem::path& image_root); + const std::optional& passwd_content, + const std::optional& group_content);