summaryrefslogtreecommitdiff
path: root/nix
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2025-10-14 15:13:25 +0200
committerLudovic Courtès <ludo@gnu.org>2025-10-16 15:14:37 +0200
commita92d98a7fa7d6a7f3c11643d2f725b618d05643f (patch)
treed9595e46454ce7c6dcc391df373f85c1a032bc76 /nix
parentd1910384d3581dcbc564353a098089d1a52c08d6 (diff)
daemon: Attempt to map the “kvm” group inside the build user namespace.
Fixes <https://issues.guix.gnu.org/77862>. Previously, the ‘guix-daemon’ account (for unprivileged execution) would typically have “kvm” as a supplementary group, but that group would not be mapped in the build user namespace. Consequently, attempts to ‘chown’ a file to that supplementary group would fail with EINVAL. The test suites of Coreutils, Python, and Go (among others) exercise this chown-to-supplementary-group behavior, so they would all fail when started by the unprivileged ‘guix-daemon’ even though they succeed when started by ‘guix-daemon’ running as root. Thanks to keinflue <keinflue@posteo.net> and Reepca Russelstein <reepca@russelstein.xyz> for helping out. * nix/libstore/build.cc (initializeUserNamespace): Add ‘extraGIDs’ and ‘haveCapSetGID’ parameters. Invoke ‘newgidmap’ when ‘extraGIDs’ is non-empty and ‘haveCapSetGID’ is false. Honor ‘extraGIDs’ when ‘haveCapSetGID’ is true. (maxGroups, guestKVMGID): New variables. (kvmGIDMapping): New function. (DerivationGoal::startBuilder): Set ‘ctx.lockMountsMapAll’ in the CLONE_NEWUSER case. Pass ‘extraGIDs’ to ‘initializeUserNamespace’. * tests/store.scm ("kvm GID is mapped"): New test. Change-Id: I10ba710fc1b9ca1e3cd3122be1ec8ede5df18b40
Diffstat (limited to 'nix')
-rw-r--r--nix/libstore/build.cc95
1 files changed, 89 insertions, 6 deletions
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index a48214a9c0a..f455343c189 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -25,6 +25,7 @@
25#include <sys/utsname.h> 25#include <sys/utsname.h>
26#include <fcntl.h> 26#include <fcntl.h>
27#include <unistd.h> 27#include <unistd.h>
28#include <grp.h>
28#include <errno.h> 29#include <errno.h>
29#include <stdio.h> 30#include <stdio.h>
30#include <cstring> 31#include <cstring>
@@ -1646,15 +1647,81 @@ static void initializeUserNamespace(pid_t child,
1646 uid_t hostUID = getuid(), 1647 uid_t hostUID = getuid(),
1647 gid_t hostGID = getgid(), 1648 gid_t hostGID = getgid(),
1648 uid_t guestUID = guestUID, 1649 uid_t guestUID = guestUID,
1649 gid_t guestGID = guestGID) 1650 gid_t guestGID = guestGID,
1651 const std::vector<std::pair<gid_t, gid_t>> extraGIDs = {},
1652 bool haveCapSetGID = false)
1650{ 1653{
1651 writeFile("/proc/" + std::to_string(child) + "/uid_map", 1654 writeFile("/proc/" + std::to_string(child) + "/uid_map",
1652 (format("%d %d 1") % guestUID % hostUID).str()); 1655 (format("%d %d 1") % guestUID % hostUID).str());
1653 1656
1654 writeFile("/proc/" + std::to_string(child) + "/setgroups", "deny"); 1657 if (!haveCapSetGID && !extraGIDs.empty()) {
1658 try {
1659 Strings args = {
1660 std::to_string(child),
1661 std::to_string(guestGID), std::to_string(hostGID), "1"
1662 };
1663 for (auto &pair: extraGIDs) {
1664 args.push_back(std::to_string(pair.second));
1665 args.push_back(std::to_string(pair.first));
1666 args.push_back("1");
1667 }
1668
1669 runProgram("newgidmap", true, args);
1670 printMsg(lvlChatty,
1671 format("mapped %1% extra GIDs into namespace of PID %2%")
1672 % extraGIDs.size() % child);
1673
1674 return;
1675 } catch (const ExecError &e) {
1676 ignoreException();
1677 }
1678 }
1679
1680 if (!haveCapSetGID)
1681 writeFile("/proc/" + std::to_string(child) + "/setgroups", "deny");
1655 1682
1656 writeFile("/proc/" + std::to_string(child) + "/gid_map", 1683 auto content = (format("%d %d 1\n") % guestGID % hostGID).str();
1657 (format("%d %d 1") % guestGID % hostGID).str()); 1684 if (haveCapSetGID) {
1685 for (auto &mapping: extraGIDs) {
1686 content += (format("%d %d 1\n") % mapping.second % mapping.first).str();
1687 }
1688 }
1689 writeFile("/proc/" + std::to_string(child) + "/gid_map", content);
1690}
1691
1692/* Maximum number of supplementary groups per user account. */
1693static const size_t maxGroups = 64;
1694
1695/* Return the ID of the "kvm" group or -1 if it does not exist or is not part
1696 of the current user's supplementary groups. */
1697static gid_t kvmGID()
1698{
1699 struct group *kvm = getgrnam("kvm");
1700 if (kvm == NULL) return -1;
1701
1702 gid_t groups[maxGroups];
1703 int count = getgroups(maxGroups, groups);
1704 if (count < 0) return -1;
1705
1706 for (int i = 0; i < count; i++) {
1707 if (groups[i] == kvm->gr_gid) return kvm->gr_gid;
1708 }
1709
1710 return -1;
1711}
1712
1713/* GID of the "kvm" group in guest user namespaces. */
1714static gid_t guestKVMGID = 40000;
1715
1716static std::vector<std::pair<gid_t, gid_t>> kvmGIDMapping()
1717{
1718 gid_t kvm = kvmGID();
1719 if (kvm == (gid_t) -1)
1720 return {};
1721 else {
1722 std::pair<gid_t, gid_t> mapping(kvm, guestKVMGID);
1723 return { mapping };
1724 }
1658} 1725}
1659 1726
1660#if CHROOT_ENABLED 1727#if CHROOT_ENABLED
@@ -2931,13 +2998,29 @@ void DerivationGoal::startBuilder()
2931 enableRouteLocalnetAction); 2998 enableRouteLocalnetAction);
2932 } 2999 }
2933 3000
3001 if ((ctx.cloneFlags & CLONE_NEWUSER) != 0) {
3002 /* Have the 'lockMounts' phase re-map supplementary GIDs such as
3003 that of the "kvm" group. */
3004 ctx.lockMountsMapAll = true;
3005 }
3006
2934 pid = cloneChild(ctx); 3007 pid = cloneChild(ctx);
2935 3008
2936 if(childSetupSocket >= 0) childSetupSocket.close(); 3009 if(childSetupSocket >= 0) childSetupSocket.close();
2937 3010
2938 if ((ctx.cloneFlags & CLONE_NEWUSER) != 0) { 3011 if ((ctx.cloneFlags & CLONE_NEWUSER) != 0) {
2939 /* Initialize the UID/GID mapping of the builder. */ 3012 /* Initialize the UID/GID mapping of the child process.
2940 initializeUserNamespace(pid); 3013
3014 Try hard to map the "kvm" GID inside the user namespace ("kvm"
3015 is usually the only supplementary group of the 'guix-daemon'
3016 privilege separation user) so that package test suites that
3017 expect to be able to chown to supplementary groups can do so
3018 (without that mapping, attempts to chown to the supplementary
3019 group fail with EINVAL). */
3020 auto extraGIDs = kvmGIDMapping();
3021 initializeUserNamespace(pid,
3022 getuid(), getgid(),
3023 guestUID, guestGID, extraGIDs);
2941 writeFull(parentSetupSocket, (unsigned char*)"go\n", 3); 3024 writeFull(parentSetupSocket, (unsigned char*)"go\n", 3);
2942 } 3025 }
2943 3026