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:
2026-08-21 07:06:13 +00:00
parent 095ae8397a
commit 69df728e09
8 changed files with 386 additions and 21 deletions
+17 -4
View File
@@ -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);