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
+123
View File
@@ -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};
}