Add --user/--group to run as a different uid/gid as root
bwrap --uid/--gid require --unshare-user, which is never requested
when running as root (since last session's fix), and user namespaces
aren't a near-term option anyway -- the actual Android target doesn't
support them.
--user <name-or-uid> / --group <name-or-gid> work around this:
user_spec.cpp resolves them against the *mounted image's own*
/etc/passwd and /etc/group (names like "git" only mean anything inside
that image's user database), and bwrap.cpp bind-mounts a small helper
into the sandbox to do the actual privilege drop before exec'ing the
real command, since bwrap itself can't switch uid/gid without a user
namespace.
The helper has to be a separate, statically-linked binary
(priv_drop_helper.cpp -> slocker-lite-priv-drop, built with -static)
rather than slocker_lite's own binary: bind-mounting a dynamically
linked executable into an arbitrary container image fails ("error
while loading shared libraries") since that image's own /lib won't
have slocker_lite's dependencies. find_priv_drop_helper() locates it
next to slocker_lite's own binary; run_bwrap() fails fast if it's
missing rather than silently running as root.
Only works without a user namespace (root): under --unshare-user the
sandbox's uid map has only one valid entry, so the helper's own
setuid() fails cleanly there instead of doing nothing silently.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -45,7 +45,37 @@ Source layout (all under `src/`):
|
||||
triggers the kernel's unprivileged-userns setgroups() restriction, which showed up
|
||||
as every other supplementary group collapsing to the overflow gid ("nobody") in
|
||||
`id`, and `su` inside the sandbox failing with "can't set groups: Operation not
|
||||
permitted".
|
||||
permitted". Because of that, bwrap's own `--uid`/`--gid` (which require
|
||||
`--unshare-user`) aren't usable when running as root either — `--user`/`--group`
|
||||
work around this: when set, `build_bwrap_args()`/`run_bwrap()` bind-mount the
|
||||
separate `slocker-lite-priv-drop` helper (see below) into the sandbox at a fixed
|
||||
hidden path and route the real command through it as
|
||||
`<uid>:<gid> -- <command...>`. This only actually works without a user namespace
|
||||
(i.e. running as root) — under `--unshare-user`, the sandbox's uid map has only
|
||||
one valid entry, so the helper's own `setuid()` fails cleanly there instead of
|
||||
silently doing nothing. `run_bwrap()` fails fast (returns -1) if the helper can't
|
||||
be found next to this binary when `--user` was requested, rather than silently
|
||||
running the command as root.
|
||||
- `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
|
||||
fully statically linked: it gets bind-mounted *into the container image's own
|
||||
filesystem*, which won't have `slocker_lite`'s own shared library dependencies —
|
||||
a dynamically linked binary bind-mounted that way fails outright ("error while
|
||||
loading shared libraries"), which is exactly what happened before this was split
|
||||
out (the original approach bind-mounted `slocker_lite`'s own — dynamically linked
|
||||
— binary via `/proc/self/exe` and reexeced it; kept only as a lesson, not as
|
||||
working code). Usage: `slocker-lite-priv-drop <uid>:<gid> -- <command> [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`.
|
||||
- `user_spec.{h,cpp}` — `resolve_user_and_group()` resolves `--user`/`--group` (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.
|
||||
- `process.{h,cpp}` — argv-based subprocess helpers (fork/execvp, no shell):
|
||||
`run_process()` captures stdout (used for `containers-storage` calls),
|
||||
`run_process_foreground()` inherits all of stdio (used for the interactive `bwrap`
|
||||
@@ -82,11 +112,13 @@ out to already be directly visible.
|
||||
Build directory is `buildDir/` (already configured).
|
||||
|
||||
- Configure (only needed if `buildDir/` is missing or deleted): `meson setup buildDir`
|
||||
- Build: `meson compile -C buildDir` (or `ninja -C buildDir`)
|
||||
- Build: `meson compile -C buildDir` (or `ninja -C buildDir`) — also builds
|
||||
`buildDir/slocker-lite-priv-drop`, the statically-linked helper `-r --user` needs
|
||||
(see `priv_drop_helper.cpp` in "Project state")
|
||||
- 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`, `-n/--no-nsenter`, `-t/--test`, `--log-level`, `-h/--help`,
|
||||
`-V/--version`)
|
||||
`-l/--list-images`, `-n/--no-nsenter`, `--user`, `--group`, `-t/--test`,
|
||||
`--log-level`, `-h/--help`, `-V/--version`)
|
||||
- Run tests: `meson test -C buildDir`
|
||||
|
||||
## Code style
|
||||
|
||||
+9
-1
@@ -18,11 +18,19 @@ 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/bwrap.cpp', 'src/user_spec.cpp'],
|
||||
include_directories : include_directories('.'),
|
||||
dependencies : [fmt_dep, catch2_dep, yaml_dep, archive_dep, json_dep, spdlog_dep],
|
||||
install : true)
|
||||
|
||||
# Bind-mounted into the sandbox by -r/--run's --user/--group handling (src/bwrap.cpp),
|
||||
# so it must be dependency-free and statically linked to run regardless of what
|
||||
# libc/libraries the container image itself has.
|
||||
priv_drop_helper = executable('slocker-lite-priv-drop',
|
||||
['src/priv_drop_helper.cpp'],
|
||||
link_args : ['-static'],
|
||||
install : true)
|
||||
|
||||
fixture_tar = custom_target('oci-fixture',
|
||||
output : 'fixture.tar',
|
||||
command : [find_program('python3'), files('tests/gen_fixture.py'), '@OUTPUT@'])
|
||||
|
||||
+58
-3
@@ -36,6 +36,33 @@
|
||||
|
||||
namespace {
|
||||
|
||||
// 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* kPrivDropPath = "/.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* kPrivDropHelperName = "slocker-lite-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<std::filesystem::path> find_priv_drop_helper() {
|
||||
std::error_code ec;
|
||||
auto self_path = std::filesystem::read_symlink("/proc/self/exe", ec);
|
||||
if (ec) {
|
||||
return std::nullopt;
|
||||
}
|
||||
std::filesystem::path candidate = self_path.parent_path() / kPrivDropHelperName;
|
||||
if (access(candidate.c_str(), X_OK) != 0) {
|
||||
return std::nullopt;
|
||||
}
|
||||
return candidate;
|
||||
}
|
||||
|
||||
struct NamespaceProbe {
|
||||
int clone_flag;
|
||||
const char* bwrap_arg;
|
||||
@@ -150,7 +177,8 @@ std::vector<std::string> detect_bwrap_unshare_args() {
|
||||
}
|
||||
|
||||
std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
const std::vector<std::string>& command) {
|
||||
const std::vector<std::string>& command,
|
||||
std::optional<ResolvedUser> user) {
|
||||
// --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.
|
||||
@@ -208,15 +236,42 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
args.insert(args.end(), {"--setenv", "TERM", term});
|
||||
}
|
||||
|
||||
bool bound_priv_drop_helper = false;
|
||||
if (user) {
|
||||
auto helper_path = find_priv_drop_helper();
|
||||
if (helper_path) {
|
||||
args.insert(args.end(), {"--ro-bind", helper_path->string(), kPrivDropPath});
|
||||
bound_priv_drop_helper = true;
|
||||
} else {
|
||||
spdlog::warn("could not find the {} helper next to this binary; --user/--group will "
|
||||
"have no effect",
|
||||
kPrivDropHelperName);
|
||||
}
|
||||
}
|
||||
|
||||
args.push_back("--");
|
||||
|
||||
if (bound_priv_drop_helper) {
|
||||
args.push_back(kPrivDropPath);
|
||||
args.push_back(fmt::format("{}:{}", user->uid, user->gid));
|
||||
args.push_back("--");
|
||||
}
|
||||
|
||||
args.insert(args.end(), command.begin(), command.end());
|
||||
|
||||
return args;
|
||||
}
|
||||
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter) {
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
std::optional<ResolvedUser> user) {
|
||||
std::vector<std::string> argv;
|
||||
|
||||
if (user && !find_priv_drop_helper()) {
|
||||
spdlog::error("could not find the {} helper next to this binary; --user/--group requires it",
|
||||
kPrivDropHelperName);
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (use_nsenter) {
|
||||
auto pid = find_fuse_overlayfs_pid(root);
|
||||
if (!pid) {
|
||||
@@ -231,7 +286,7 @@ int run_bwrap(const std::string& root, const std::vector<std::string>& command,
|
||||
fmt::format("--mount=/proc/{}/ns/mnt", *pid), "--"};
|
||||
}
|
||||
|
||||
auto bwrap_args = build_bwrap_args(root, command);
|
||||
auto bwrap_args = build_bwrap_args(root, command, user);
|
||||
argv.insert(argv.end(), bwrap_args.begin(), bwrap_args.end());
|
||||
|
||||
return run_process_foreground(argv);
|
||||
|
||||
+17
-4
@@ -16,6 +16,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -26,11 +27,20 @@
|
||||
// --unshare-xxx flag to bwrap would make it fail outright.
|
||||
std::vector<std::string> detect_bwrap_unshare_args();
|
||||
|
||||
struct ResolvedUser {
|
||||
int uid;
|
||||
int gid;
|
||||
};
|
||||
|
||||
// Assembles the full bwrap argv (program name included) to run `command` with
|
||||
// `root` bound as the sandbox's filesystem root, using whichever --unshare-xxx
|
||||
// flags the kernel supports (see detect_bwrap_unshare_args()).
|
||||
// flags the kernel supports (see detect_bwrap_unshare_args()). If `user` is set,
|
||||
// the command is wrapped so it drops to that uid/gid before running -- see
|
||||
// run_bwrap() for how, since bwrap's own --uid/--gid require --unshare-user, which
|
||||
// isn't requested when running as root (see detect_bwrap_unshare_args()).
|
||||
std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
const std::vector<std::string>& command);
|
||||
const std::vector<std::string>& command,
|
||||
std::optional<ResolvedUser> user);
|
||||
|
||||
// Runs bwrap against `root` (the merged mount path from mount_layer()) in the
|
||||
// foreground and waits for it to exit. If `use_nsenter` is true, first locates the
|
||||
@@ -40,5 +50,8 @@ std::vector<std::string> build_bwrap_args(const std::string& root,
|
||||
// plain child process on kernels where fuse-overlayfs isolates it that way. Pass
|
||||
// use_nsenter=false on kernels where the mount is already directly visible
|
||||
// (observed on kernels older than 4.18, per fuse-overlayfs's own release notes).
|
||||
// Returns bwrap's exit code, or -1 on failure to launch.
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter);
|
||||
// If `user` is set, this process's own binary is bind-mounted into the sandbox and
|
||||
// used to drop privileges to that uid/gid before running `command` -- see
|
||||
// build_bwrap_args(). Returns bwrap's exit code, or -1 on failure to launch.
|
||||
int run_bwrap(const std::string& root, const std::vector<std::string>& command, bool use_nsenter,
|
||||
std::optional<ResolvedUser> user);
|
||||
|
||||
+48
-9
@@ -34,6 +34,7 @@
|
||||
#include "containers_storage.h"
|
||||
#include "oci_image.h"
|
||||
#include "process.h"
|
||||
#include "user_spec.h"
|
||||
|
||||
namespace {
|
||||
|
||||
@@ -41,11 +42,14 @@ constexpr std::array<std::string_view, 2> kRequiredTools = {"containers-storage"
|
||||
|
||||
enum class Mode { kNone, kMount, kUnmount, kTest, kRun, kCleanup, kListImages };
|
||||
|
||||
// --log-level has no short form (freed up so -l could become --list-images), so it
|
||||
// needs a long-option val outside the printable-char range short options use.
|
||||
// --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
|
||||
// vals outside the printable-char range short options use.
|
||||
constexpr int kLogLevelOpt = 256;
|
||||
constexpr int kUserOpt = 257;
|
||||
constexpr int kGroupOpt = 258;
|
||||
|
||||
constexpr std::array<struct option, 11> kLongOptions = {{
|
||||
constexpr std::array<struct option, 13> kLongOptions = {{
|
||||
{"help", no_argument, nullptr, 'h'},
|
||||
{"version", no_argument, nullptr, 'V'},
|
||||
{"test", no_argument, nullptr, 't'},
|
||||
@@ -56,6 +60,8 @@ constexpr std::array<struct option, 11> kLongOptions = {{
|
||||
{"cleanup", required_argument, nullptr, 'c'},
|
||||
{"no-nsenter", no_argument, nullptr, 'n'},
|
||||
{"list-images", required_argument, nullptr, 'l'},
|
||||
{"user", required_argument, nullptr, kUserOpt},
|
||||
{"group", required_argument, nullptr, kGroupOpt},
|
||||
{nullptr, 0, nullptr, 0},
|
||||
}};
|
||||
|
||||
@@ -87,6 +93,13 @@ void print_usage(const char* prog) {
|
||||
" (this is automatic when running as root, where\n"
|
||||
" the mount is already directly visible; pass\n"
|
||||
" this to force it off otherwise)\n"
|
||||
" --user <user> with --run, run the command as this user (name\n"
|
||||
" or numeric uid) instead of root, resolved\n"
|
||||
" against the image's own /etc/passwd; only takes\n"
|
||||
" effect when --run executes as root (no user\n"
|
||||
" namespace involved)\n"
|
||||
" --group <group> with --user, use this group (name or numeric\n"
|
||||
" gid) instead of the user's own primary group\n"
|
||||
" -l, --list-images <dir> list OCI Image Layout tars (*.tar, *.tar.*) found\n"
|
||||
" directly in <dir>, with their name:tag\n"
|
||||
" -t, --test run the test suite\n"
|
||||
@@ -242,13 +255,23 @@ int list_images_command(const std::filesystem::path& dir) {
|
||||
}
|
||||
|
||||
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) {
|
||||
auto mounted = mount_image(image_tar);
|
||||
if (!mounted) {
|
||||
return 1;
|
||||
}
|
||||
fmt::print("mounted image at: {} (layer {})\n", mounted->merged_path, mounted->top_layer_id);
|
||||
|
||||
bool ok = true;
|
||||
std::optional<ResolvedUser> resolved_user;
|
||||
if (user) {
|
||||
resolved_user = resolve_user_and_group(*user, group, mounted->merged_path);
|
||||
if (!resolved_user) {
|
||||
ok = false;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector<std::string> command = requested_command;
|
||||
if (command.empty()) {
|
||||
auto config = read_oci_image_config(image_tar);
|
||||
@@ -256,9 +279,12 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
: std::vector<std::string>{"/bin/sh"};
|
||||
}
|
||||
|
||||
int exit_code = run_bwrap(mounted->merged_path, command, use_nsenter);
|
||||
if (exit_code < 0) {
|
||||
spdlog::error("failed to run bwrap");
|
||||
int exit_code = -1;
|
||||
if (ok) {
|
||||
exit_code = run_bwrap(mounted->merged_path, command, use_nsenter, resolved_user);
|
||||
if (exit_code < 0) {
|
||||
spdlog::error("failed to run bwrap");
|
||||
}
|
||||
}
|
||||
|
||||
if (!unmount_layer(mounted->top_layer_id)) {
|
||||
@@ -268,7 +294,7 @@ int run_container(const std::filesystem::path& image_tar,
|
||||
spdlog::error("failed to clean up layer {}", mounted->top_layer_id);
|
||||
}
|
||||
|
||||
return exit_code < 0 ? 1 : exit_code;
|
||||
return (!ok || exit_code < 0) ? 1 : exit_code;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
@@ -277,6 +303,8 @@ int main(int argc, char* argv[]) {
|
||||
Mode mode = Mode::kNone;
|
||||
std::string mode_arg;
|
||||
bool disable_nsenter = false;
|
||||
std::optional<std::string> user_flag;
|
||||
std::optional<std::string> group_flag;
|
||||
|
||||
opterr = 0;
|
||||
int opt;
|
||||
@@ -334,6 +362,12 @@ int main(int argc, char* argv[]) {
|
||||
return 1;
|
||||
}
|
||||
break;
|
||||
case kUserOpt:
|
||||
user_flag = optarg;
|
||||
break;
|
||||
case kGroupOpt:
|
||||
group_flag = optarg;
|
||||
break;
|
||||
case ':':
|
||||
spdlog::error("option requires an argument: -{}", static_cast<char>(optopt));
|
||||
print_usage(argv[0]);
|
||||
@@ -354,6 +388,11 @@ int main(int argc, char* argv[]) {
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
if (group_flag && !user_flag) {
|
||||
spdlog::error("--group requires --user");
|
||||
print_usage(argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (mode == Mode::kTest) {
|
||||
return run_tests();
|
||||
@@ -377,7 +416,7 @@ int main(int argc, char* argv[]) {
|
||||
if (geteuid() == 0 && !disable_nsenter) {
|
||||
spdlog::debug("running as root; skipping nsenter (the mount is already directly visible)");
|
||||
}
|
||||
return run_container(mode_arg, command, use_nsenter);
|
||||
return run_container(mode_arg, command, use_nsenter, user_flag, group_flag);
|
||||
}
|
||||
|
||||
auto mounted = mount_image(mode_arg);
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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.
|
||||
|
||||
// Minimal standalone helper, built fully statically (no shared library dependencies
|
||||
// at all) so it can be bind-mounted into an arbitrary container image and still run
|
||||
// regardless of what libc/libraries that image does or doesn't have. Deliberately
|
||||
// avoids fmt/spdlog/anything else from the rest of this project -- a dynamically
|
||||
// linked binary bind-mounted into a container fails with "error while loading
|
||||
// shared libraries", since the container's own /lib won't have slocker_lite's
|
||||
// dependencies.
|
||||
//
|
||||
// Usage: slocker-lite-priv-drop <uid>:<gid> -- <command> [args...]
|
||||
// Drops supplementary groups, then gid, then uid (in that order -- dropping gid
|
||||
// needs CAP_SETGID, which is lost once setuid() drops root), then execs <command>.
|
||||
|
||||
#include <grp.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cerrno>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
int main(int argc, char* argv[]) {
|
||||
if (argc < 4 || std::strcmp(argv[2], "--") != 0) {
|
||||
std::fprintf(stderr, "usage: %s <uid>:<gid> -- <command> [args...]\n", argv[0]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
char* colon = std::strchr(argv[1], ':');
|
||||
if (!colon) {
|
||||
std::fprintf(stderr, "%s: invalid uid:gid spec: %s\n", argv[0], argv[1]);
|
||||
return 1;
|
||||
}
|
||||
*colon = '\0';
|
||||
uid_t uid = static_cast<uid_t>(std::strtol(argv[1], nullptr, 10));
|
||||
gid_t gid = static_cast<gid_t>(std::strtol(colon + 1, nullptr, 10));
|
||||
|
||||
if (setgroups(0, nullptr) != 0 || setgid(gid) != 0 || setuid(uid) != 0) {
|
||||
std::fprintf(stderr, "%s: failed to drop privileges to %u:%u: %s\n", argv[0],
|
||||
static_cast<unsigned>(uid), static_cast<unsigned>(gid), std::strerror(errno));
|
||||
return 1;
|
||||
}
|
||||
|
||||
execvp(argv[3], argv + 3);
|
||||
std::fprintf(stderr, "%s: failed to exec %s: %s\n", argv[0], argv[3], std::strerror(errno));
|
||||
return 127;
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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 "user_spec.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cctype>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
|
||||
#include <spdlog/spdlog.h>
|
||||
|
||||
namespace {
|
||||
|
||||
std::vector<std::string> split(const std::string& line, char delim) {
|
||||
std::vector<std::string> fields;
|
||||
std::istringstream stream(line);
|
||||
std::string field;
|
||||
while (std::getline(stream, field, delim)) {
|
||||
fields.push_back(field);
|
||||
}
|
||||
return fields;
|
||||
}
|
||||
|
||||
bool is_all_digits(const std::string& s) {
|
||||
return !s.empty() && std::all_of(s.begin(), s.end(), [](unsigned char c) { return std::isdigit(c); });
|
||||
}
|
||||
|
||||
std::optional<int> parse_int(const std::string& s) {
|
||||
try {
|
||||
return std::stoi(s);
|
||||
} catch (const std::exception&) {
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
// 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<std::vector<std::string>> lookup_entry(const std::filesystem::path& db_file,
|
||||
const std::string& key, size_t id_field) {
|
||||
std::ifstream in(db_file, std::ios::binary);
|
||||
if (!in) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::string line;
|
||||
while (std::getline(in, line)) {
|
||||
auto fields = split(line, ':');
|
||||
if (fields.size() <= id_field) {
|
||||
continue;
|
||||
}
|
||||
if (fields[0] == key || fields[id_field] == key) {
|
||||
return fields;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
std::optional<ResolvedUser> resolve_user_and_group(const std::string& user,
|
||||
const std::optional<std::string>& group,
|
||||
const std::filesystem::path& image_root) {
|
||||
std::filesystem::path passwd_file = image_root / "etc" / "passwd";
|
||||
|
||||
int uid = 0;
|
||||
int gid = 0;
|
||||
|
||||
if (is_all_digits(user)) {
|
||||
uid = *parse_int(user);
|
||||
auto entry = lookup_entry(passwd_file, 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);
|
||||
if (!entry || entry->size() <= 3) {
|
||||
spdlog::error("could not resolve user '{}' in the image's /etc/passwd", user);
|
||||
return std::nullopt;
|
||||
}
|
||||
auto entry_uid = parse_int((*entry)[2]);
|
||||
auto entry_gid = parse_int((*entry)[3]);
|
||||
if (!entry_uid || !entry_gid) {
|
||||
spdlog::error("malformed /etc/passwd entry for user '{}'", user);
|
||||
return std::nullopt;
|
||||
}
|
||||
uid = *entry_uid;
|
||||
gid = *entry_gid;
|
||||
}
|
||||
|
||||
if (group) {
|
||||
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_gid = entry ? parse_int((*entry)[2]) : std::nullopt;
|
||||
if (!entry_gid) {
|
||||
spdlog::error("could not resolve group '{}' in the image's /etc/group", *group);
|
||||
return std::nullopt;
|
||||
}
|
||||
gid = *entry_gid;
|
||||
}
|
||||
}
|
||||
|
||||
return ResolvedUser{uid, gid};
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
// 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 <filesystem>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#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.
|
||||
std::optional<ResolvedUser> resolve_user_and_group(const std::string& user,
|
||||
const std::optional<std::string>& group,
|
||||
const std::filesystem::path& image_root);
|
||||
Reference in New Issue
Block a user