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:
@@ -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};
|
||||
}
|
||||
Reference in New Issue
Block a user