1
0
Fork 0
mirror of https://github.com/NixOS/nix synced 2025-06-24 22:11:15 +02:00

linux: support supplementary groups with auto-allocate-uids

Add a new option "supplementary-groups" that allows specifying
additional groups to be mapped into the build sandbox and assigned as
supplementary groups for the build user.

This makes it possible to, for example, use the "kvm" group to provide
access to /dev/kvm even when auto-allocate-uids is enabled—something
that was not previously possible. It also enables use of supplementary
groups to grant sandboxed builds read-only access to secrets or other
shared resources.

Closes: https://github.com/NixOS/nix/issues/9276
Signed-off-by: Samuli Thomasson <samuli.thomasson@pm.me>
This commit is contained in:
Samuli Thomasson 2025-06-01 09:22:24 +02:00
parent 587b5f5361
commit 62cc737d19
No known key found for this signature in database
GPG key ID: 6B8903D2645A5B48
2 changed files with 161 additions and 9 deletions

View file

@ -484,6 +484,35 @@ public:
"id-count",
"The number of UIDs/GIDs to use for dynamic ID allocation."};
Setting<StringSet> supplementaryGroups{
this, {"kvm"}, "supplementary-groups",
R"(
A list of supplementary groups to be added to build users when dynamic
UID/GID allocation [`auto-allocate-uids`](#conf-auto-allocate-uids) is
enabled.
By default, each listed group is mapped into the build sandbox using its
host system GID. You can override the mapped GID by appending `:GID` to
the group name. If a group does not exist on the host (i.e., has no
assigned GID), it is silently ignored.
Example:
```nix
extra-supplementary-groups = nixbld users:10100
```
This maps the `nixbld` group using its assigned GID (usually 30000).
And the `users` group gets assigned GID 10100.
> **Note**
>
> When using [`build-users-group`](#conf-build-users-group),
> supplementary groups should instead be specified directly in the
> user definitions.
)",
{"build-supplementary-groups"}};
#ifdef __linux__
Setting<bool> useCgroups{
this, false, "use-cgroups",

View file

@ -221,6 +221,105 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
return usingUserNamespace ? (!buildUser || buildUser->getUIDCount() == 1 ? 100 : 0) : buildUser->getGID();
}
bool setSupplementaryGroups;
/**
* Return parent_gid -> (mapped_gid, mapped_name)
*
* Keys are host/parent gid's (can't have duplicates obviously). Values
* are the mapped/target (gid, name) pairs. Each mapped gid (name) may
* only appear once in the result.
*/
std::map<gid_t, std::tuple<gid_t, std::string>> getSupplementaryGIDMap()
{
// Primary GID (sandboxGid) is always mapped and it would make no
// sense to assign as a supplementary group as well.
//
// It would be technically harmless to allow mapping and assigning 0,
// aside from some potential confusion. It's only disallowed here
// because the root group is declared always anyway. Allowing it here
// would result in a duplicate /etc/group entry.
//
// Allowing assigning nogroup 65534 would be very bad, especially if
// root 0 wasn't mapped, as it's the fallback group to which all
// unmapped GIDs get mapped to (including root).
std::vector<gid_t> reserved_gids = {sandboxGid(), 0, 65534};
std::vector<std::string> reserved_names = {"root", "nixbld", "nogroup"};
std::map<gid_t, std::tuple<gid_t, std::string>> gid_map;
struct group grp;
struct group * gr = nullptr;
long bufsize = sysconf(_SC_GETGR_R_SIZE_MAX);
if (bufsize == -1) bufsize = 16384;
std::vector<char> buffer(bufsize);
for (const auto & group_entry : settings.supplementaryGroups.get()) {
std::string group_name = group_entry;
std::optional<gid_t> mapped_gid;
// parse "name[:gid]"
auto pos = group_entry.find(":");
if (pos != std::string::npos) {
std::string gid_str = group_entry.substr(pos + 1);
group_name = group_entry.substr(0, pos);
try {
mapped_gid = static_cast<gid_t>(std::stoul(gid_str));
} catch (...) {
throw Error("Invalid GID number in '%s'", gid_str);
}
}
int ret = getgrnam_r(group_name.c_str(), &grp, buffer.data(), buffer.size(), &gr);
if (ret != 0)
throw Error("getgrnam_r failed for group '%s': %s", group_name, strerror(ret));
if (!gr) {
debug("Supplementary group '%s' not found", group_name);
continue;
}
// host GID sanity checks
gid_t parent_gid = gr->gr_gid;
if (parent_gid == 0)
throw Error("Group '%s': mapping the root group (GID 0) is not a good idea", group_name);
if (gid_map.contains(parent_gid))
throw Error("Group '%s': parent GID %d is already mapped", group_name, parent_gid);
/* Mapped GID sanity checks
65535 is the special invalid/overflow GID distinct from
nogroup. We don't want to allow that.
2^16 and above are not allowed because it would seem impossible
to assign them. In a quick test higher GIDs got truncated to
65534. Might have something to do with how we're forced to call
setgroups before setting up the namespace. */
if (!mapped_gid)
mapped_gid = parent_gid;
if (mapped_gid > 65534)
throw Error("Group '%s': mapped GID %d is too large (>65534)", group_name, mapped_gid.value());
if (std::find(reserved_gids.begin(), reserved_gids.end(), mapped_gid.value()) != reserved_gids.end())
throw Error("Group '%s': mapped GID %d conflicts with reserved GID", group_name, mapped_gid.value());
// mapped name sanity checks
std::string mapped_name = gr->gr_name;
if (std::find(reserved_names.begin(), reserved_names.end(), mapped_name) != reserved_names.end()) {
mapped_name += "-host";
debug("Group '%s': name conflicts with reserved name; renaming to '%s'", group_name, mapped_name);
if (std::find(reserved_names.begin(), reserved_names.end(), mapped_name) != reserved_names.end()) {
warn("Group '%s': original and alternative name '%s' both conflict with reserved names; skipping this group", group_name, mapped_name);
continue;
}
}
gid_map[parent_gid] = std::make_tuple(mapped_gid.value(), std::move(mapped_name));
reserved_gids.push_back(mapped_gid.value());
reserved_names.push_back(std::get<1>(gid_map[parent_gid]));
}
return gid_map;
}
bool needsHashRewrite() override
{
return false;
@ -292,6 +391,8 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
}
}
setSupplementaryGroups = settings.autoAllocateUids && getuid() == 0;
// Kill any processes left in the cgroup or build user.
DerivationBuilderImpl::prepareUser();
}
@ -343,12 +444,17 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
/* Declare the build user's group so that programs get a consistent
view of the system (e.g., "id -gn"). */
writeFile(
chrootRootDir + "/etc/group",
fmt("root:x:0:\n"
"nixbld:!:%1%:\n"
"nogroup:x:65534:\n",
sandboxGid()));
std::ostringstream oss;
oss << fmt(
"root:x:0:\n"
"nogroup:x:65534:\n"
"nixbld:!:%d:\n", sandboxGid());
if (setSupplementaryGroups)
for (const auto & [parent_gid, mapped] : getSupplementaryGIDMap())
oss << fmt("%s:x:%d:nixbld\n", std::get<1>(mapped), std::get<0>(mapped));
writeFile(chrootRootDir + "/etc/group", oss.str());
/* Create /etc/hosts with localhost entry. */
if (derivationType.isSandboxed())
@ -452,6 +558,13 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
usingUserNamespace = userNamespacesSupported();
// NOTE: setting supplementary groups like this only only works in
// certain conditions (root permissions).
std::vector<gid_t> supplementaryGroups;
if (setSupplementaryGroups)
for (const auto & [parent_gid, mapped] : getSupplementaryGIDMap())
supplementaryGroups.push_back(std::get<0>(mapped));
Pipe sendPid;
sendPid.create();
@ -464,9 +577,9 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
openSlave();
try {
/* Drop additional groups here because we can't do it
/* Drop and/or set additional groups here because we can't do it
after we've created the new user namespace. */
if (setgroups(0, 0) == -1) {
if (setgroups(supplementaryGroups.size(), supplementaryGroups.data()) == -1) {
if (errno != EPERM)
throw SysError("setgroups failed");
if (settings.requireDropSupplementaryGroups)
@ -522,7 +635,17 @@ struct ChrootLinuxDerivationBuilder : LinuxDerivationBuilder
if (!buildUser || buildUser->getUIDCount() == 1)
writeFile("/proc/" + std::to_string(pid) + "/setgroups", "deny");
writeFile("/proc/" + std::to_string(pid) + "/gid_map", fmt("%d %d %d", sandboxGid(), hostGid, nrIds));
std::ostringstream oss;
// Primary GID mapping
oss << fmt("%d %d %d", sandboxGid(), hostGid, nrIds);
if (setSupplementaryGroups)
// Supplementary GIDs one by one
for (const auto & [parent_gid, mapped] : getSupplementaryGIDMap())
oss << fmt("\n%d %d 1", std::get<0>(mapped), parent_gid);
writeFile("/proc/" + std::to_string(pid) + "/gid_map", oss.str());
} else {
debug("note: not using a user namespace");
if (!buildUser)