mirror of
https://git.savannah.gnu.org/git/guix.git
synced 2026-04-06 21:20:33 +02:00
daemon: Allow running as non-root with unprivileged user namespaces.
Many thanks to Reepca Russelstein for their review and guidance on these
changes.
* nix/libstore/build.cc (guestUID, guestGID): New variables.
(DerivationGoal)[readiness]: New field.
(initializeUserNamespace): New function.
(DerivationGoal::runChild): When ‘readiness.readSide’ is positive, read
from it.
(DerivationGoal::startBuilder): Call ‘chown’
only when ‘buildUser.enabled()’ is true. Pass CLONE_NEWUSER to ‘clone’
when ‘buildUser.enabled()’ is false or not running as root. Retry
‘clone’ without CLONE_NEWUSER upon EPERM.
(DerivationGoal::registerOutputs): Make ‘actualPath’ writable before
‘rename’.
(DerivationGoal::deleteTmpDir): Catch ‘SysError’ around ‘_chown’ call.
* nix/libstore/local-store.cc (LocalStore::createUser): Do nothing if
‘dirs’ already exists. Warn instead of failing when failing to chown
‘dir’.
* guix/substitutes.scm (%narinfo-cache-directory): Check for
‘_NIX_OPTIONS’ rather than getuid() == 0 to determine the cache
location.
* doc/guix.texi (Build Environment Setup): Reorganize a bit. Add
section headings “Daemon Running as Root” and “The Isolated Build
Environment”. Add “Daemon Running Without Privileges” subsection.
Remove paragraph about ‘--disable-chroot’.
(Invoking guix-daemon): Warn against ‘--disable-chroot’ and explain why.
* tests/derivations.scm ("builder is outside the store"): New test.
Reviewed-by: Reepca Russelstein <reepca@russelstein.xyz>
This commit is contained in:
committed by
Ludovic Courtès
parent
40f69b586a
commit
ae18b3d9e6
@@ -744,6 +744,10 @@ private:
|
||||
|
||||
friend int childEntry(void *);
|
||||
|
||||
/* Pipe to notify readiness to the child process when using unprivileged
|
||||
user namespaces. */
|
||||
Pipe readiness;
|
||||
|
||||
/* Check that the derivation outputs all exist and register them
|
||||
as valid. */
|
||||
void registerOutputs();
|
||||
@@ -1619,6 +1623,24 @@ int childEntry(void * arg)
|
||||
}
|
||||
|
||||
|
||||
/* UID and GID of the build user inside its own user namespace. */
|
||||
static const uid_t guestUID = 30001;
|
||||
static const gid_t guestGID = 30000;
|
||||
|
||||
/* Initialize the user namespace of CHILD. */
|
||||
static void initializeUserNamespace(pid_t child,
|
||||
uid_t hostUID = getuid(),
|
||||
gid_t hostGID = getgid())
|
||||
{
|
||||
writeFile("/proc/" + std::to_string(child) + "/uid_map",
|
||||
(format("%d %d 1") % guestUID % hostUID).str());
|
||||
|
||||
writeFile("/proc/" + std::to_string(child) + "/setgroups", "deny");
|
||||
|
||||
writeFile("/proc/" + std::to_string(child) + "/gid_map",
|
||||
(format("%d %d 1") % guestGID % hostGID).str());
|
||||
}
|
||||
|
||||
void DerivationGoal::startBuilder()
|
||||
{
|
||||
auto f = format(
|
||||
@@ -1682,7 +1704,7 @@ void DerivationGoal::startBuilder()
|
||||
then an attacker could create in it a hardlink to a root-owned file
|
||||
such as /etc/shadow. If 'keepFailed' is true, the daemon would
|
||||
then chown that hardlink to the user, giving them write access to
|
||||
that file. */
|
||||
that file. See CVE-2021-27851. */
|
||||
tmpDir += "/top";
|
||||
if (mkdir(tmpDir.c_str(), 0700) == 1)
|
||||
throw SysError("creating top-level build directory");
|
||||
@@ -1799,7 +1821,7 @@ void DerivationGoal::startBuilder()
|
||||
if (mkdir(chrootRootDir.c_str(), 0750) == -1)
|
||||
throw SysError(format("cannot create ‘%1%’") % chrootRootDir);
|
||||
|
||||
if (chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
|
||||
if (buildUser.enabled() && chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
|
||||
throw SysError(format("cannot change ownership of ‘%1%’") % chrootRootDir);
|
||||
|
||||
/* Create a writable /tmp in the chroot. Many builders need
|
||||
@@ -1818,8 +1840,8 @@ void DerivationGoal::startBuilder()
|
||||
(format(
|
||||
"nixbld:x:%1%:%2%:Nix build user:/:/noshell\n"
|
||||
"nobody:x:65534:65534:Nobody:/:/noshell\n")
|
||||
% (buildUser.enabled() ? buildUser.getUID() : getuid())
|
||||
% (buildUser.enabled() ? buildUser.getGID() : getgid())).str());
|
||||
% (buildUser.enabled() ? buildUser.getUID() : guestUID)
|
||||
% (buildUser.enabled() ? buildUser.getGID() : guestGID)).str());
|
||||
|
||||
/* Declare the build user's group so that programs get a consistent
|
||||
view of the system (e.g., "id -gn"). */
|
||||
@@ -1854,7 +1876,7 @@ void DerivationGoal::startBuilder()
|
||||
createDirs(chrootStoreDir);
|
||||
chmod_(chrootStoreDir, 01775);
|
||||
|
||||
if (chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
|
||||
if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
|
||||
throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
|
||||
|
||||
foreach (PathSet::iterator, i, inputPaths) {
|
||||
@@ -1960,14 +1982,36 @@ void DerivationGoal::startBuilder()
|
||||
if (useChroot) {
|
||||
char stack[32 * 1024];
|
||||
int flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | SIGCHLD;
|
||||
if (!fixedOutput) flags |= CLONE_NEWNET;
|
||||
if (!fixedOutput) {
|
||||
flags |= CLONE_NEWNET;
|
||||
}
|
||||
if (!buildUser.enabled() || getuid() != 0) {
|
||||
flags |= CLONE_NEWUSER;
|
||||
readiness.create();
|
||||
}
|
||||
|
||||
/* Ensure proper alignment on the stack. On aarch64, it has to be 16
|
||||
bytes. */
|
||||
pid = clone(childEntry,
|
||||
pid = clone(childEntry,
|
||||
(char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~(uintptr_t)0xf),
|
||||
flags, this);
|
||||
if (pid == -1)
|
||||
throw SysError("cloning builder process");
|
||||
if (pid == -1) {
|
||||
if ((flags & CLONE_NEWUSER) != 0 && getuid() != 0)
|
||||
/* 'clone' fails with EPERM on distros where unprivileged user
|
||||
namespaces are disabled. Error out instead of giving up on
|
||||
isolation. */
|
||||
throw SysError("cannot create process in unprivileged user namespace");
|
||||
else
|
||||
throw SysError("cloning builder process");
|
||||
}
|
||||
|
||||
readiness.readSide.close();
|
||||
if ((flags & CLONE_NEWUSER) != 0) {
|
||||
/* Initialize the UID/GID mapping of the child process. */
|
||||
initializeUserNamespace(pid);
|
||||
writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
|
||||
}
|
||||
readiness.writeSide.close();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
@@ -2013,23 +2057,37 @@ void DerivationGoal::runChild()
|
||||
|
||||
_writeToStderr = 0;
|
||||
|
||||
if (readiness.writeSide >= 0) readiness.writeSide.close();
|
||||
|
||||
if (readiness.readSide >= 0) {
|
||||
/* Wait for the parent process to initialize the UID/GID mapping
|
||||
of our user namespace. */
|
||||
char str[20] = { '\0' };
|
||||
readFull(readiness.readSide, (unsigned char*)str, 3);
|
||||
readiness.readSide.close();
|
||||
if (strcmp(str, "go\n") != 0)
|
||||
throw Error("failed to initialize process in unprivileged user namespace");
|
||||
}
|
||||
|
||||
restoreAffinity();
|
||||
|
||||
commonChildInit(builderOut);
|
||||
|
||||
#if CHROOT_ENABLED
|
||||
if (useChroot) {
|
||||
/* Initialise the loopback interface. */
|
||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||
if (fd == -1) throw SysError("cannot open IP socket");
|
||||
if (!fixedOutput) {
|
||||
/* Initialise the loopback interface. */
|
||||
AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
|
||||
if (fd == -1) throw SysError("cannot open IP socket");
|
||||
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, "lo");
|
||||
ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING;
|
||||
if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1)
|
||||
throw SysError("cannot set loopback interface flags");
|
||||
struct ifreq ifr;
|
||||
strcpy(ifr.ifr_name, "lo");
|
||||
ifr.ifr_flags = IFF_UP | IFF_LOOPBACK | IFF_RUNNING;
|
||||
if (ioctl(fd, SIOCSIFFLAGS, &ifr) == -1)
|
||||
throw SysError("cannot set loopback interface flags");
|
||||
|
||||
fd.close();
|
||||
fd.close();
|
||||
}
|
||||
|
||||
/* Set the hostname etc. to fixed values. */
|
||||
char hostname[] = "localhost";
|
||||
@@ -2180,6 +2238,27 @@ void DerivationGoal::runChild()
|
||||
/* Remount root as read-only. */
|
||||
if (mount("/", "/", 0, MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
|
||||
throw SysError(format("read-only remount of build root '%1%' failed") % chrootRootDir);
|
||||
|
||||
if (getuid() != 0) {
|
||||
/* Create a new mount namespace to "lock" previous mounts.
|
||||
See mount_namespaces(7). */
|
||||
auto uid = getuid();
|
||||
auto gid = getgid();
|
||||
|
||||
if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
|
||||
throw SysError(format("creating new user and mount namespaces"));
|
||||
|
||||
initializeUserNamespace(getpid(), uid, gid);
|
||||
|
||||
/* Check that mounts within the build environment are "locked"
|
||||
together and cannot be separated from within the build
|
||||
environment namespace. Since
|
||||
umount(2) is documented to fail with EINVAL when attempting
|
||||
to unmount one of the mounts that are locked together,
|
||||
check that this is what we get. */
|
||||
int ret = umount(tmpDirInSandbox.c_str());
|
||||
assert(ret == -1 && errno == EINVAL);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -2262,6 +2341,7 @@ void DerivationGoal::runChild()
|
||||
writeFull(STDERR_FILENO, "\n");
|
||||
|
||||
/* Execute the program. This should not return. */
|
||||
string builderBasename;
|
||||
if (isBuiltin(drv)) {
|
||||
try {
|
||||
logType = ltFlat;
|
||||
@@ -2285,11 +2365,28 @@ void DerivationGoal::runChild()
|
||||
writeFull(STDERR_FILENO, "error: " + string(e.what()) + "\n");
|
||||
_exit(1);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
/* Ensure that the builder is within the store. This prevents
|
||||
users from using /proc/self/exe (or a symlink to it) as their
|
||||
builder, which could allow them to overwrite the guix-daemon
|
||||
binary (CVE-2019-5736).
|
||||
|
||||
This attack is possible even if the target of /proc/self/exe is
|
||||
outside the chroot (it's as if it were a hard link), though it
|
||||
requires that its ELF interpreter and dependencies be in the
|
||||
chroot.
|
||||
|
||||
Note: 'canonPath' throws if 'drv.builder' cannot be resolved
|
||||
within the chroot. */
|
||||
builderBasename = baseNameOf(drv.builder);
|
||||
drv.builder = canonPath(drv.builder, true);
|
||||
|
||||
if (!isInStore(drv.builder))
|
||||
throw Error(format("derivation builder '%1%' is outside the store") % drv.builder);
|
||||
}
|
||||
|
||||
/* Fill in the arguments. */
|
||||
Strings args;
|
||||
string builderBasename = baseNameOf(drv.builder);
|
||||
args.push_back(builderBasename);
|
||||
foreach (Strings::iterator, i, drv.args)
|
||||
args.push_back(rewriteHashes(*i, rewritesToTmp));
|
||||
@@ -2476,8 +2573,16 @@ void DerivationGoal::registerOutputs()
|
||||
if (buildMode == bmRepair)
|
||||
replaceValidPath(path, actualPath);
|
||||
else
|
||||
if (buildMode != bmCheck && rename(actualPath.c_str(), path.c_str()) == -1)
|
||||
throw SysError(format("moving build output `%1%' from the chroot to the store") % path);
|
||||
if (buildMode != bmCheck) {
|
||||
if (S_ISDIR(st.st_mode))
|
||||
/* Change mode on the directory to allow for
|
||||
rename(2). */
|
||||
chmod(actualPath.c_str(), st.st_mode | 0700);
|
||||
if (rename(actualPath.c_str(), path.c_str()) == -1)
|
||||
throw SysError(format("moving build output `%1%' from the chroot to the store") % path);
|
||||
if (S_ISDIR(st.st_mode) && chmod(path.c_str(), st.st_mode) == -1)
|
||||
throw SysError(format("restoring permissions on directory `%1%'") % actualPath);
|
||||
}
|
||||
}
|
||||
if (buildMode != bmCheck) actualPath = path;
|
||||
}
|
||||
@@ -2736,16 +2841,46 @@ void DerivationGoal::deleteTmpDir(bool force)
|
||||
// Change the ownership if clientUid is set. Never change the
|
||||
// ownership or the group to "root" for security reasons.
|
||||
if (settings.clientUid != (uid_t) -1 && settings.clientUid != 0) {
|
||||
_chown(tmpDir, settings.clientUid,
|
||||
settings.clientGid != 0 ? settings.clientGid : -1);
|
||||
uid_t uid = settings.clientUid;
|
||||
gid_t gid = settings.clientGid != 0 ? settings.clientGid : -1;
|
||||
bool reown = false;
|
||||
|
||||
/* First remove setuid/setgid bits. */
|
||||
secureFilePerms(tmpDir);
|
||||
|
||||
try {
|
||||
_chown(tmpDir, uid, gid);
|
||||
|
||||
if (getuid() != 0) {
|
||||
/* If, without being root, the '_chown' call above
|
||||
succeeded, then it means we have CAP_CHOWN. Retake
|
||||
ownership of tmpDir itself so it can be renamed
|
||||
below. */
|
||||
reown = true;
|
||||
}
|
||||
|
||||
} catch (SysError & e) {
|
||||
/* When running as an unprivileged user and without
|
||||
CAP_CHOWN, we cannot chown the build tree. Print a
|
||||
message and keep going. */
|
||||
printMsg(lvlInfo, format("cannot change ownership of build directory '%1%': %2%")
|
||||
% tmpDir % strerror(e.errNo));
|
||||
}
|
||||
|
||||
if (top != tmpDir) {
|
||||
if (reown) chown(tmpDir.c_str(), getuid(), getgid());
|
||||
|
||||
// Rename tmpDir to its parent, with an intermediate step.
|
||||
string pivot = top + ".pivot";
|
||||
if (rename(top.c_str(), pivot.c_str()) == -1)
|
||||
throw SysError("pivoting failed build tree");
|
||||
if (rename((pivot + "/top").c_str(), top.c_str()) == -1)
|
||||
throw SysError("renaming failed build tree");
|
||||
|
||||
if (reown)
|
||||
/* Running unprivileged but with CAP_CHOWN. */
|
||||
chown(top.c_str(), uid, gid);
|
||||
|
||||
rmdir(pivot.c_str());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1614,11 +1614,19 @@ void LocalStore::createUser(const std::string & userName, uid_t userId)
|
||||
{
|
||||
auto dir = settings.nixStateDir + "/profiles/per-user/" + userName;
|
||||
|
||||
createDirs(dir);
|
||||
if (chmod(dir.c_str(), 0755) == -1)
|
||||
throw SysError(format("changing permissions of directory '%s'") % dir);
|
||||
if (chown(dir.c_str(), userId, -1) == -1)
|
||||
throw SysError(format("changing owner of directory '%s'") % dir);
|
||||
auto created = createDirs(dir);
|
||||
if (!created.empty()) {
|
||||
if (chmod(dir.c_str(), 0755) == -1)
|
||||
throw SysError(format("changing permissions of directory '%s'") % dir);
|
||||
|
||||
/* The following operation requires CAP_CHOWN or can be handled
|
||||
manually by a user with CAP_CHOWN. */
|
||||
if (chown(dir.c_str(), userId, -1) == -1) {
|
||||
rmdir(dir.c_str());
|
||||
string message = strerror(errno);
|
||||
printMsg(lvlInfo, format("failed to change owner of directory '%1%' to %2%: %3%") % dir % userId % message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user