GNU bug report logs - #75810
[PATCH 0/6] Rootless guix-daemon

Previous Next

Package: guix-patches;

Reported by: Ludovic Courtès <ludo <at> gnu.org>

Date: Fri, 24 Jan 2025 17:24:02 UTC

Severity: normal

Tags: patch

Done: Ludovic Courtès <ludo <at> gnu.org>

Bug is archived. No further changes may be made.

To add a comment to this bug, you must first unarchive it, by sending
a message to control AT debbugs.gnu.org, with unarchive 75810 in the body.
You can then email your comments to 75810 AT debbugs.gnu.org in the normal way.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:24:02 GMT) Full text and rfc822 format available.

Acknowledgement sent to Ludovic Courtès <ludo <at> gnu.org>:
New bug report received and forwarded. Copy sent to guix-patches <at> gnu.org. (Fri, 24 Jan 2025 17:24:02 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: guix-patches <at> gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>
Subject: [PATCH 0/6] Rootless guix-daemon
Date: Fri, 24 Jan 2025 18:23:08 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

Hello Guix!

That guix-daemon runs as root is not confidence-inspiring for many.
Initially, the main reason for running it as root was, in the absence
of user namespaces, the fact that builders would be started under one
of the build user accounts, which only root can do.  Now that
unprivileged user namespaces are almost ubiquitous (even on HPC
clusters), this is no longer a good reason.

This patch changes guix-daemon so it can run as an unprivileged
user, using unprivileged user namespaces to still support isolated
builds.  There’s a couple of cases where root is/was still necessary:

  1. To create /var/guix/profiles/per-user/$USER and chown it
     as $USER (see CVE-2019-18192).

  2. To chown /tmp/guix-build-* when using ‘--keep-failed’.

Both can be addressed by giving CAP_CHOWN to guix-daemon, and this is
what this patch series does on distros using systemd.  (For some
reason CAP_CHOWN had to be added to the set of “ambient capabilities”,
which are inherited by child processes; this is why there’s a patch
to drop ambient capabilities in build processes.)

On Guix System (not implemented here), we could address (1) by
creating /var/guix/profiles/per-user/$USER upfront for all the
user accounts.  We could leave (2) unaddressed (so failed build
directories would be owned by guix-daemon:guix-daemon) or we’d
have to pass CAP_CHOWN as well.

There’s another issue: /gnu/store can no longer be remounted
read-only (like we do on Guix System and on systemd with
‘gnu-store.mount’) because then unprivileged guix-daemon would
be unable to remount it read-write (or at least I couldn’t find
a way to do that).  Thus ‘guix-install.sh’ no longer installs
‘gnu-store.mount’ in that case.  It’s a bit sad to lose that
so if anyone can think of a way to achieve it, that’d be great.

I tested all this in a Debian VM¹, along these lines:

  1. GUIX_ALLOW_ME_TO_USE_PRIVATE_COMMIT=yes make update-guix-package
  2. ./pre-inst-env guix pack -C zstd guix --without-tests=guix \
        --localstatedir --profile-name=current-guix
  3. Copy ‘guix-install.sh’ and the tarball to the VM over SSH.
  4. In the VM: GUIX_BINARY_FILE_NAME=pack.tar.zst ./guix-install.sh

The next step (in another patch series) would be Guix System support
with automatic transition (essentially “chown -R
guix-daemon:guix-daemon /gnu/store”).

Thoughts?

Ludo’.

¹ https://cdimage.debian.org/debian-cd/current-live/amd64/iso-hybrid/debian-live-12.9.0-amd64-standard.iso

Ludovic Courtès (6):
  daemon: Allow running as non-root with unprivileged user namespaces.
  DRAFT tests: Run in a chroot and unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.

 build-aux/test-env.in       |  14 +++-
 config-daemon.ac            |   2 +-
 etc/guix-daemon.service.in  |  12 +++-
 etc/guix-install.sh         | 114 ++++++++++++++++++++++++-------
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 132 ++++++++++++++++++++++++++++++------
 nix/libstore/local-store.cc |  30 +++++---
 tests/store.scm             |  89 ++++++++++++++----------
 8 files changed, 300 insertions(+), 97 deletions(-)


base-commit: bc6769f1211104dbc9341c064275cd930f5dfa3a
-- 
2.47.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:26:02 GMT) Full text and rfc822 format available.

Message #8 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>
Subject: [PATCH 1/6] daemon: Allow running as non-root with unprivileged user
 namespaces.
Date: Fri, 24 Jan 2025 18:24:51 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

* nix/libstore/build.cc (guestUID, guestGID): New variables.
(initializeUserNamespace): New function.
(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.

Change-Id: I38fbe01f80fb45a99cd8a391e55a39a54d64fcb7
---
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 123 +++++++++++++++++++++++++++++-------
 nix/libstore/local-store.cc |  22 +++++--
 3 files changed, 118 insertions(+), 31 deletions(-)

diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index e31b394020..2761a3dafb 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013-2021, 2023-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2014 Nikita Karetnikov <nikita <at> karetnikov.org>
 ;;; Copyright © 2018 Kyle Meyer <kyle <at> kyleam.com>
 ;;; Copyright © 2020 Christopher Baines <mail <at> cbaines.net>
@@ -76,7 +76,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34..727472c77f 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1622,6 +1622,25 @@ 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)
+{
+    auto hostUID = getuid();
+    auto 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(
@@ -1685,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");
@@ -1802,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
@@ -1821,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"). */
@@ -1859,7 +1878,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) {
@@ -1971,14 +1990,42 @@ 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;
+	Pipe readiness;
+	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");
+	}
+
+	if ((flags & CLONE_NEWUSER) != 0) {
+	    /* Initialize the UID/GID mapping of the guest.  */
+	    if (pid == 0) {
+		char str[20] = { '\0' };
+		readFull(readiness.readSide, (unsigned char*)str, 3);
+		if (strcmp(str, "go\n") != 0)
+		    throw Error("failed to initialize process in unprivileged user namespace");
+	    } else {
+		initializeUserNamespace(pid);
+		writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
+	    }
+	}
     } else
 #endif
     {
@@ -2030,17 +2077,19 @@ void DerivationGoal::runChild()
 
 #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";
@@ -2463,8 +2512,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;
         }
@@ -2723,8 +2780,25 @@ 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;
+		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.  */
+			chown(tmpDir.c_str(), getuid(), getgid());
+		    }
+		} 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) {
 		    // Rename tmpDir to its parent, with an intermediate step.
@@ -2733,6 +2807,11 @@ void DerivationGoal::deleteTmpDir(bool force)
 			throw SysError("pivoting failed build tree");
 		    if (rename((pivot + "/top").c_str(), top.c_str()) == -1)
 			throw SysError("renaming failed build tree");
+
+		    if (getuid() != 0)
+			/* Running unprivileged but with CAP_CHOWN.  */
+			chown(top.c_str(), uid, gid);
+
 		    rmdir(pivot.c_str());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbce..4308264a4f 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -306,14 +306,14 @@ void LocalStore::openDB(bool create)
 void LocalStore::makeStoreWritable()
 {
 #if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_REMOUNT)
-    if (getuid() != 0) return;
     /* Check if /nix/store is on a read-only mount. */
     struct statvfs stat;
     if (statvfs(settings.nixStore.c_str(), &stat) != 0)
         throw SysError("getting info about the store mount point");
 
     if (stat.f_flag & ST_RDONLY) {
-        if (unshare(CLONE_NEWNS) == -1)
+	int flags = CLONE_NEWNS | (getpid() == 0 ? 0 : CLONE_NEWUSER);
+        if (unshare(flags) == -1)
             throw SysError("setting up a private mount namespace");
 
         if (mount(0, settings.nixStore.c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1)
@@ -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);
+	}
+    }
 }
 
 
-- 
2.47.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:26:02 GMT) Full text and rfc822 format available.

Message #11 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 2/6] DRAFT tests: Run in a chroot and unprivileged user
 namespaces.
Date: Fri, 24 Jan 2025 18:24:52 +0100
DRAFT:

  - Double-check the test suite.

* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("isolated environment"): New test.

Change-Id: Iedb816ef548c77799e5b2f9b6a3b7510ad19ec2a
---
 build-aux/test-env.in | 14 ++++++-
 tests/store.scm       | 89 ++++++++++++++++++++++++++-----------------
 2 files changed, 66 insertions(+), 37 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da58..5626152b34 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,20 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f43..bdbb026dd9 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -30,6 +30,8 @@ (define-module (test-store)
   #:use-module (guix derivations)
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +393,32 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:inputs `((,b) (,s))))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1361,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.47.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:26:03 GMT) Full text and rfc822 format available.

Message #14 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 3/6] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Fri, 24 Jan 2025 18:24:53 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 4308264a4f..6384669519 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.47.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:26:03 GMT) Full text and rfc822 format available.

Message #17 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 4/6] daemon: Drop Linux ambient capabilities before executing
 builder.
Date: Fri, 24 Jan 2025 18:24:54 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc3..aeec5f3239 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,7 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 727472c77f..c95bd2821f 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2077,6 +2080,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.47.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:26:04 GMT) Full text and rfc822 format available.

Message #20 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 5/6] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Fri, 24 Jan 2025 18:24:55 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(User, AmbientCapabilities): New fields.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/guix-daemon.service.in | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1..f9f0b28b35 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -7,9 +7,19 @@ Description=Build daemon for GNU Guix
 
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
 Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Provide the CAP_CHOWN capability so that guix-daemon cran create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.47.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 17:26:04 GMT) Full text and rfc822 format available.

Message #23 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH 6/6] guix-install.sh: Support the unprivileged daemon where
 possible.
Date: Fri, 24 Jan 2025 18:24:56 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.
(sys_enable_guix_daemon): Do not install ‘gnu-store.mount’ when running
an unprivileged daemon.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 114 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 91 insertions(+), 23 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index f07b2741bb..4f08eff847 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -389,6 +389,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg "${INF}Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg "${INF}Linking the root user's profile"
@@ -414,38 +419,82 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg "${INF}user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg "${PAS}user added <$user>"
+    fi
+}
+
+can_install_unprivileged_daemon()
+{ # Return true if we can install guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
+	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg "${INF}group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg "${PAS}group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg "${INF}group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg "${INF}user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild${KVMGROUP}     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild${KVMGROUP}     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg "${PAS}user added <guixbuilder${i}>"
-        fi
-    done
+    if [ "$INIT_SYS" = systemd ] && \
+	   grep -q "User=guix-daemon" \
+		~root/.config/guix/current/lib/systemd/system/guix-daemon.service
+    then
+	if getent group guix-daemon > /dev/null; then
+	    _msg "${INF}group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg "${PAS}group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon	\
+	      /gnu /var/guix
+
+	# The unprivileged cannot create the log directory by itself.
+	mkdir /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg "${INF}group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg "${PAS}group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -460,6 +509,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg "${INF}remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -503,7 +560,14 @@ sys_enable_guix_daemon()
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
               # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      #
+	      # FIXME: This feature is unavailable when running an
+	      # unprivileged daemon.
+	      if ! grep -q "User=guix-daemon" \
+		   /etc/systemd/system/guix-daemon.service
+	      then
+		  install_unit gnu-store.mount
+	      fi
 
               systemctl daemon-reload &&
                   systemctl start  guix-daemon; } &&
@@ -627,6 +691,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg "${PAS}Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg "${INF}Skipped authorizing build farm public keys"
     fi
-- 
2.47.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 19:22:02 GMT) Full text and rfc822 format available.

Message #26 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Janneke Nieuwenhuizen <janneke <at> gnu.org>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Fri, 24 Jan 2025 20:20:42 +0100
Ludovic Courtès writes:

Hello!

> That guix-daemon runs as root is not confidence-inspiring for many.

Certainly, in fact, this and the many build users was [sadly?] the
reason I didn't look further into Nix around 2010 or so...

[..]

> This patch changes guix-daemon so it can run as an unprivileged
> user, using unprivileged user namespaces to still support isolated
> builds.

Yay, awesome!

> There’s a couple of cases where root is/was still necessary:

[..]

> There’s another issue: /gnu/store can no longer be remounted
> read-only (like we do on Guix System and on systemd with
> ‘gnu-store.mount’) because then unprivileged guix-daemon would
> be unable to remount it read-write (or at least I couldn’t find
> a way to do that).  Thus ‘guix-install.sh’ no longer installs
> ‘gnu-store.mount’ in that case.  It’s a bit sad to lose that
> so if anyone can think of a way to achieve it, that’d be great.

Hmm.  So this is is about using guix as a package manager on foreign
systems, for now?  Will there be an option for users to choose between
a non-root guix-daemon or a read-only store?

I'm kind of afraid that having a writable /gnu/store, even if it's just
on foreign distributions, is going to cause a whole lot of problems/bug
reports with people changing files in the store.  When I came to guix I
ran it on Debian for a couple of months and I certainly changed files in
the store, even with the read-only mount hurdle, to "get stuff to
build".  Only later to realise that by doing so I was making things much
more difficult for myself.

Hopefully I'm either misunderstanding this patch set, or else too
pessimistict, and maybe other people aren't as stupid as I was when I
first came to Guix?

Greetings,
Janneke

-- 
Janneke Nieuwenhuizen <janneke <at> gnu.org>  | GNU LilyPond https://LilyPond.org
Freelance IT https://www.JoyOfSource.com | Avatar® https://AvatarAcademy.com




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 24 Jan 2025 22:19:01 GMT) Full text and rfc822 format available.

Message #29 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Janneke Nieuwenhuizen <janneke <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Fri, 24 Jan 2025 23:18:16 +0100
Hello,

Janneke Nieuwenhuizen <janneke <at> gnu.org> skribis:

>> There’s another issue: /gnu/store can no longer be remounted
>> read-only (like we do on Guix System and on systemd with
>> ‘gnu-store.mount’) because then unprivileged guix-daemon would
>> be unable to remount it read-write (or at least I couldn’t find
>> a way to do that).  Thus ‘guix-install.sh’ no longer installs
>> ‘gnu-store.mount’ in that case.  It’s a bit sad to lose that
>> so if anyone can think of a way to achieve it, that’d be great.
>
> Hmm.  So this is is about using guix as a package manager on foreign
> systems, for now?

Yes, but the goal is to eventually make it available (as an option) on
Guix System.

> Will there be an option for users to choose between a non-root
> guix-daemon or a read-only store?

I would prefer not having to choose between the two, but as I wrote, I
don’t know how to make it work.

Currently ‘makeStoreWritable’ does this:

    if (stat.f_flag & ST_RDONLY) {
        if (unshare(CLONE_NEWNS) == -1)
            throw SysError("setting up a private mount namespace");

        if (mount(0, settings.nixStore.c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1)
            throw SysError(format("remounting %1% writable") % settings.nixStore);
    }

But the remount trick only works if you’re actually root.

As non-root, what can guix-daemon do?  It could (bind-)mount the
underlying file system, but how to do that?  (Thinking out loud.)
Perhaps ‘gnu-store.mount’ could stash the read-write variant aside, say
in /gnu/.rw-store, and guix-daemon would bind-mount that to /gnu/store?

> I'm kind of afraid that having a writable /gnu/store, even if it's just
> on foreign distributions, is going to cause a whole lot of problems/bug
> reports with people changing files in the store.  When I came to guix I
> ran it on Debian for a couple of months and I certainly changed files in
> the store, even with the read-only mount hurdle, to "get stuff to
> build".  Only later to realise that by doing so I was making things much
> more difficult for myself.

Yeah, agreed.

Thanks for your feedback!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 26 Jan 2025 00:40:02 GMT) Full text and rfc822 format available.

Message #32 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: 75810 <at> debbugs.gnu.org
Cc: ludo <at> gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Sat, 25 Jan 2025 18:39:04 -0600
[Message part 1 (text/plain, inline)]
> Hello Guix!
> 
> That guix-daemon runs as root is not confidence-inspiring for many.
> Initially, the main reason for running it as root was, in the absence
> of user namespaces, the fact that builders would be started under one
> of the build user accounts, which only root can do.  Now that
> unprivileged user namespaces are almost ubiquitous (even on HPC
> clusters), this is no longer a good reason.

Without the build users, we're relying entirely on kernel-specific
sandboxing mechanisms to protect the system from rogue builders.  It's
probably (?) not impossible to make it work, but, as with every time
security mechanisms are changed, it does require some very careful
thought.

For example, consider the following:

--8<---------------cut here---------------start------------->8---
(use-modules (guix)
             (gnu)
             (guix build-system trivial))

(define-public sneakysneaky
  (package
    (name "sneakysneaky")
    (version "0")
    (source #f)
    (build-system trivial-build-system)
    (arguments
     (list
      #:builder
      #~(let ((hello (string-append #$(this-package-input "hello")
                                    "/bin/hello")))
          (chmod (dirname hello) #o775)
          (chmod hello #o775)
          (delete-file hello)
          (call-with-output-file hello
            (lambda (port)
              (chmod port #o775)
              (display "#!/bin/sh
echo \"GOOOOOD BYYEEEEEE\""
                       port)))
          (mkdir #$output))))
    (inputs (list (@ (gnu packages base) hello)))
    (home-page "")
    (synopsis "")
    (description "")
    (license #f)))

sneakysneaky
--8<---------------cut here---------------end--------------->8---


If we save this as /tmp/mal-test.scm on a debian VM with these patches
applied, we can see the following:


--8<---------------cut here---------------start------------->8---
user <at> debian:~$ guix build --no-grafts hello
/gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1
user <at> debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
Hello, world!
user <at> debian:~$ guix build --no-grafts -f /tmp/mal-test.scm
substitute: looking for substitutes on 'https://bordeaux.guix.gnu.org'... 100.0%
substitute: looking for substitutes on 'https://ci.guix.gnu.org'... 100.0%
The following derivation will be built:
  /gnu/store/p15g92hfs7254pqfa3kss63dprw2clis-sneakysneaky-0.drv
building /gnu/store/p15g92hfs7254pqfa3kss63dprw2clis-sneakysneaky-0.drv...
successfully built /gnu/store/p15g92hfs7254pqfa3kss63dprw2clis-sneakysneaky-0.drv
/gnu/store/y1jzqg30cgkydl8kymjsh99zqgzh1yj1-sneakysneaky-0
user <at> debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
GOOOOOD BYYEEEEEE
user <at> debian:~$ 
--8<---------------cut here---------------end--------------->8---

This happens because the daemon bind-mounts store items into the
container, so it's the same underlying inode both inside and out of the
container.  The build runs as the same user as the store owner, so
there's nothing stopping it from freely modifying its input store items
and any of their transitive references.

I suppose we could try to perform these bind-mounts with the MS_RDONLY
flag, but we would need some way to ensure that the builder can't just
remount them read-write (I haven't yet looked into how to do this).  The
nuclear option, of course, would be to simply do a full copy of the
store items in question instead of a bind-mount.

> This patch changes guix-daemon so it can run as an unprivileged
> user, using unprivileged user namespaces to still support isolated
> builds.  There’s a couple of cases where root is/was still necessary:
> 
>   1. To create /var/guix/profiles/per-user/$USER and chown it
>      as $USER (see CVE-2019-18192).
> 
>   2. To chown /tmp/guix-build-* when using ‘--keep-failed’.
> 
> Both can be addressed by giving CAP_CHOWN to guix-daemon, and this is
> what this patch series does on distros using systemd.  (For some
> reason CAP_CHOWN had to be added to the set of “ambient capabilities”,
> which are inherited by child processes; this is why there’s a patch
> to drop ambient capabilities in build processes.)
> 
> On Guix System (not implemented here), we could address (1) by
> creating /var/guix/profiles/per-user/$USER upfront for all the
> user accounts.  We could leave (2) unaddressed (so failed build
> directories would be owned by guix-daemon:guix-daemon) or we’d
> have to pass CAP_CHOWN as well.

The automatic chown of /tmp/guix-build-* has always been a litte strange
considering that multiple users could attempt the same doomed-to-failure
derivation build at the same time, and it comes down to a race to see
who gets the build (and therefore the build directory).  This does raise
the question, though, of how these failed build directories would get
deleted, aside from rebooting the system.  Perhaps the garbage collector
could be modified to get rid of them?  In which case it may be best to
make it so that the failed build directories are automatically added to
the temp roots for a client, and the client takes care to copy the
failed build directory to a fresh path owned by the current user?  Or we
could make it so that the failed build directory gets sent over the wire
in nar form to the client.  Not sure what the best approach there is.

> There’s another issue: /gnu/store can no longer be remounted
> read-only (like we do on Guix System and on systemd with
> ‘gnu-store.mount’) because then unprivileged guix-daemon would
> be unable to remount it read-write (or at least I couldn’t find
> a way to do that).  Thus ‘guix-install.sh’ no longer installs
> ‘gnu-store.mount’ in that case.  It’s a bit sad to lose that
> so if anyone can think of a way to achieve it, that’d be great.

We currently remount /gnu/store read-write at LocalStore-creation-time,
which happens in the newly-forked guix-daemon process at the start of a
connection.  I don't think there's any particularly elevated risk from
instead doing that before the per-connection process is forked.  There
are a number of ways we could do this: we could make it the
responsibility of the init system to create the mount namespace and do
the remounting, or we could have guix-daemon do it immediately on
startup and subsequently switch its uid and gid to
guix-daemon:guix-daemon.  These lack the slick appeal of "see, you never
have to give it root, and you can prove it just by looking at the
service file", but realistically should be just as secure.  It may be
useful to provide a small wrapper around guix-daemon that does the
remount and privilege-dropping, to more succinctly express this to
anybody wishing to see for themselves.

> The next step (in another patch series) would be Guix System support
> with automatic transition (essentially “chown -R
> guix-daemon:guix-daemon /gnu/store”).
> 
> Thoughts?

There are, effectively, 3 platforms that guix currently supports: posix,
linux, and hurd.  Posix doesn't get much attention since we don't chase
Mac like nix does, but there do exist configurations where we use
neither linux-specific nor hurd-specific functionality.  Additionally, a
given guix-daemon may be either privileged or unprivileged.  Thus, we
end up with a total of 6 configurations.  Except there is now also the
question of whether less-than-fully-trusted users are allowed access to
the guix-daemon's socket.  Now we're in theory at 12 configurations.
Which of these configurations to use is, in some circumstances, going to
come down to judgement calls.  For example, one user may not care at all
about the risk of malicious builders (e.g. "the admins on this shared
system all use the debian tools anyway"), but be quite concerned about
the possibility of a root-granting exploit being found in guix-daemon.
Another (like myself and other Guix System users) may consider a risk to
the store to be the same as a risk to the entire system itself.  In
theory splitting between "privileged-with-root" and
"privileged-with-capabilities" will only increase the number of
configurations further.

Personally, I think that if a guix-daemon can use privilege separation
users, it would probably be a good idea to.  We're certainly going to
need to support them on non-linux systems either way.  Could it be
possible to have guix-install.sh modify /etc/sudoers on systems that use
it to allow the guix-daemon user to run processes under guix builder
users?  I am currently less worried about arbitrary code execution
vulnerabilities being found in the daemon than about the possibility of
malicious builders (but it is possible I am underexposed to the ways
those can happen in C++).

Additionally, CAP_CHOWN, while not having a direct path to privilege
escalation due to setuid and setgid bits being reset when chown is
called, can nevertheless be easily leveraged into privilege escalation
in most real-world situations where arbitrary code execution is
possible, so switching to using just that capability would realistically
only add defense in less-than-arbitrary-code-execution scenarios.

Using unprivileged user namespaces would, however, be an excellent
addition for unprivileged daemons, like the one started by test-env, or
one started by an unprivileged user on a system without a whole-system
guix installation.

Hope that helps.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 27 Jan 2025 21:33:02 GMT) Full text and rfc822 format available.

Message #35 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Mon, 27 Jan 2025 22:31:43 +0100
[Message part 1 (text/plain, inline)]
Hello Reepca,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> user <at> debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
> GOOOOOD BYYEEEEEE

This particular issue is fixed with read-only mounts:

[Message part 2 (text/x-patch, inline)]
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c95bd2821f..e8e4a56e2d 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2175,7 +2175,7 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
-                if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
+                if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_RDONLY, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
             }
 
[Message part 3 (text/plain, inline)]
(I checked that it does the right thing.)

The fix is trivial, but I’m glad you found the bug in the first place;
it does stress that we have to be careful here.

> I suppose we could try to perform these bind-mounts with the MS_RDONLY
> flag, but we would need some way to ensure that the builder can't just
> remount them read-write

The example below tests that; ‘mount’ fails with EPERM when using the
unprivileged daemon (‘./test-env guix build -f …’):

--8<---------------cut here---------------start------------->8---
(use-modules (guix)
             (guix modules)
             (gnu packages bootstrap))

(computed-file "try-to-remount-input-read-write"
               (with-imported-modules (source-module-closure
                                       '((guix build syscalls)))
                 #~(begin
                     (use-modules (guix build syscalls))

                     (let ((input #$(plain-file "input-that-might-be-tampered-with"
                                                "All good!")))
                       (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
                       (call-with-output-file input
                         (lambda (port)
                           (display "BAD!" port)))
                       (mkdir #$output))))
               #:guile %bootstrap-guile)
--8<---------------cut here---------------end--------------->8---

This is similar to:

  guix shell -C guile guix -- \
    guile -c '(use-modules (guix build syscalls)) (mount "none" (getenv "GUIX_ENVIRONMENT") "none" (logior MS_BIND MS_REMOUNT))'

mount(2) has this:

   EPERM  An attempt was made to modify (MS_REMOUNT) the MS_RDONLY, MS_NO‐
          SUID, or MS_NOEXEC flag, or one of the "atime"  flags  (MS_NOAT‐
          IME,  MS_NODIRATIME,  MS_RELATIME) of an existing mount, but the
          mount is locked; see mount_namespaces(7).

I couldn’t find the definite answer in mount_namespaces(7) as to whether
this applies in this case (same namespace but after chroot); I can only
tell empirically that it does apply.

>> This patch changes guix-daemon so it can run as an unprivileged
>> user, using unprivileged user namespaces to still support isolated
>> builds.  There’s a couple of cases where root is/was still necessary:
>> 
>>   1. To create /var/guix/profiles/per-user/$USER and chown it
>>      as $USER (see CVE-2019-18192).
>> 
>>   2. To chown /tmp/guix-build-* when using ‘--keep-failed’.
>> 
>> Both can be addressed by giving CAP_CHOWN to guix-daemon, and this is
>> what this patch series does on distros using systemd.  (For some
>> reason CAP_CHOWN had to be added to the set of “ambient capabilities”,
>> which are inherited by child processes; this is why there’s a patch
>> to drop ambient capabilities in build processes.)
>> 
>> On Guix System (not implemented here), we could address (1) by
>> creating /var/guix/profiles/per-user/$USER upfront for all the
>> user accounts.  We could leave (2) unaddressed (so failed build
>> directories would be owned by guix-daemon:guix-daemon) or we’d
>> have to pass CAP_CHOWN as well.

[...]

> This does raise the question, though, of how these failed build
> directories would get deleted, aside from rebooting the system.

Note that in the early days (and in current Nix actually), build trees
were not chowned.  That’s OK: they’re deleted upon reboot or by the
system administrator.

Current Nix has this:

--8<---------------cut here---------------start------------->8---
void DerivationGoal::deleteTmpDir(bool force)
{
    if (tmpDir != "") {
        /* Don't keep temporary directories for builtins because they
           might have privileged stuff (like a copy of netrc). */
        if (settings.keepFailed && !force && !drv->isBuiltin()) {
            printError("note: keeping build directory '%s'", tmpDir);
            chmod(tmpDir.c_str(), 0755);
        }
        else
            deletePath(tmpDir);
        tmpDir = "";
    }
}
--8<---------------cut here---------------end--------------->8---

We could go back to this.  It’s less convenient, but okay.

In this patch series, it attempts to chown the tree; if it fails to do
so (because it lacks CAP_CHOWN), it prints a warning and keeps going.

> Perhaps the garbage collector could be modified to get rid of them?
> In which case it may be best to make it so that the failed build
> directories are automatically added to the temp roots for a client,
> and the client takes care to copy the failed build directory to a
> fresh path owned by the current user?  Or we could make it so that the
> failed build directory gets sent over the wire in nar form to the
> client.  Not sure what the best approach there is.

Dunno.  Sending it as nar may be too heavyweight and quite a bit of
work.

I’d say it goes beyond the scope of this patch series, though.

> We currently remount /gnu/store read-write at LocalStore-creation-time,
> which happens in the newly-forked guix-daemon process at the start of a
> connection.  I don't think there's any particularly elevated risk from
> instead doing that before the per-connection process is forked.  There
> are a number of ways we could do this: we could make it the
> responsibility of the init system to create the mount namespace and do
> the remounting, or we could have guix-daemon do it immediately on
> startup and subsequently switch its uid and gid to
> guix-daemon:guix-daemon.  These lack the slick appeal of "see, you never
> have to give it root, and you can prove it just by looking at the
> service file", but realistically should be just as secure.  It may be
> useful to provide a small wrapper around guix-daemon that does the
> remount and privilege-dropping, to more succinctly express this to
> anybody wishing to see for themselves.

I think I’d prefer to have a systemd (or other) service make a
read-write bind-mount at /gnu/store/.rw-store, and then we’d run
‘guix-daemon --backing-store=/gnu/store/.rw-store’.

WDYT?

> There are, effectively, 3 platforms that guix currently supports: posix,
> linux, and hurd.

Rather two: Linux and Hurd.  But note: we don’t use any Hurd-specific
features yet, and in practice all the energy and focus is on Linux (on
the Hurd we run ‘guix-daemon --disable-chroot’ anyway).

Adding the privileged/unprivileged setting, we’d have two configurations
really, again setting aside the Hurd.

The way I see it, if everything goes well, we’d default to unprivileged
guix-daemon on Guix System as well and eventually (longer term) drop the
privileged daemon.

> Personally, I think that if a guix-daemon can use privilege separation
> users, it would probably be a good idea to.  We're certainly going to
> need to support them on non-linux systems either way.  Could it be
> possible to have guix-install.sh modify /etc/sudoers on systems that use
> it to allow the guix-daemon user to run processes under guix builder
> users?  I am currently less worried about arbitrary code execution
> vulnerabilities being found in the daemon than about the possibility of
> malicious builders (but it is possible I am underexposed to the ways
> those can happen in C++).

What would you put in /etc/sudoers?  I’m not sure what you had in mind.

Aside, I’d rather avoid relying on external tools like ‘sudo’.

> Additionally, CAP_CHOWN, while not having a direct path to privilege
> escalation due to setuid and setgid bits being reset when chown is
> called, can nevertheless be easily leveraged into privilege escalation
> in most real-world situations where arbitrary code execution is
> possible, so switching to using just that capability would realistically
> only add defense in less-than-arbitrary-code-execution scenarios.

I agree about CAP_CHOWN, which is why I proposed scenarios without it.

Thanks a lot for your feedback!

I’ll send a second version addressing the immediate issue you found
and, if everything goes well, an attempt at restoring the /gnu/store
read-only bind-mount.

Ludo’.

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 27 Jan 2025 21:52:01 GMT) Full text and rfc822 format available.

Message #38 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Noé Lopez <noe <at> xn--no-cja.eu>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>,
 Janneke Nieuwenhuizen <janneke <at> gnu.org>
Subject: Re: [PATCH 0/6] Rootless guix-daemon
Date: Mon, 27 Jan 2025 22:51:08 +0100
Hi Ludovic,

If the store is not read-only, is there not a risk of applications
running as root modifying their own files in the store?

As a possible solution, maybe it is possible to have a modifiable store
directory for the daemon and a read-only bind mount as /gnu/store.  If
it does not have performance implications, applications would be started
from /gnu/store as usual and the builder can still use the other
directory.

What do you think?
Noé




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 27 Jan 2025 22:07:02 GMT) Full text and rfc822 format available.

Message #41 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Noé Lopez <noe <at> xn--no-cja.eu>
Cc: Reepca Russelstein <reepca <at> russelstein.xyz>, 75810 <at> debbugs.gnu.org,
 Janneke Nieuwenhuizen <janneke <at> gnu.org>
Subject: Re: [PATCH 0/6] Rootless guix-daemon
Date: Mon, 27 Jan 2025 23:05:21 +0100
Hi,

Noé Lopez <noe <at> noé.eu> skribis:

> If the store is not read-only, is there not a risk of applications
> running as root modifying their own files in the store?

Yes, there’s a risk.

> As a possible solution, maybe it is possible to have a modifiable store
> directory for the daemon and a read-only bind mount as /gnu/store.  If
> it does not have performance implications, applications would be started
> from /gnu/store as usual and the builder can still use the other
> directory.

I agree, that’s what I alluded to with having /gnu/.rw-store as the
backing store used by guix-daemon, while /gnu/store would be read-only.

Thanks,
Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Wed, 29 Jan 2025 07:53:02 GMT) Full text and rfc822 format available.

Message #44 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Wed, 29 Jan 2025 01:51:33 -0600
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

> Hello Reepca,
>
> Reepca Russelstein <reepca <at> russelstein.xyz> skribis:
>
>> user <at> debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
>> GOOOOOD BYYEEEEEE
>
> This particular issue is fixed with read-only mounts:
>
> diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
> index c95bd2821f..e8e4a56e2d 100644
> --- a/nix/libstore/build.cc
> +++ b/nix/libstore/build.cc
> @@ -2175,7 +2175,7 @@ void DerivationGoal::runChild()
>                      createDirs(dirOf(target));
>                      writeFile(target, "");
>                  }
> -                if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
> +                if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_RDONLY, 0) == -1)
>                      throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
>              }
>  
>
>
> (I checked that it does the right thing.)
>
> The fix is trivial, but I’m glad you found the bug in the first place;
> it does stress that we have to be careful here.

Not quite trivial, consider this section from mount(2):

    Creating a bind mount
       If mountflags includes MS_BIND (available since Linux 2.4),  then  per‐
       form  a  bind  mount.  A bind mount makes a file or a directory subtree
       visible at another point within the single directory  hierarchy.   Bind
       mounts may cross filesystem boundaries and span chroot(2) jails.

       The filesystemtype and data arguments are ignored.

       The  remaining  bits (other than MS_REC, described below) in the mount‐
       flags argument are also ignored.  (The bind mount has  the  same  mount
       options  as  the underlying mount.)  However, see the discussion of re‐
       mounting above, for a method of making an  existing  bind  mount  read-
       only.

If you run my sneakysneaky example from before, you'll find that it
still succeeds at replacing the "hello" binary because of this, even
with your MS_RDONLY patch.  This can be resolved by instead using
MS_RDONLY with a followup mount call using MS_REMOUNT.

Note also that store items that are files instead of directories (e.g. source
tarballs) are hardlinked if possible.  This seems to stem from an old
misconception that only directories can be bind-mounted.  The hardlinks,
of course, do not have any write-protection on them aside from their
permission bits.

This can be resolved by always bind-mounting them instead.  Despite the
name, there is actually already support for bind-mounting non-directory
files that are listed in dirsInChroot.

>> I suppose we could try to perform these bind-mounts with the MS_RDONLY
>> flag, but we would need some way to ensure that the builder can't just
>> remount them read-write
>
> The example below tests that; ‘mount’ fails with EPERM when using the
> unprivileged daemon (‘./test-env guix build -f …’):
>
> (use-modules (guix)
>              (guix modules)
>              (gnu packages bootstrap))
>
> (computed-file "try-to-remount-input-read-write"
>                (with-imported-modules (source-module-closure
>                                        '((guix build syscalls)))
>                  #~(begin
>                      (use-modules (guix build syscalls))
>
>                      (let ((input #$(plain-file "input-that-might-be-tampered-with"
>                                                 "All good!")))
>                        (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
>                        (call-with-output-file input
>                          (lambda (port)
>                            (display "BAD!" port)))
>                        (mkdir #$output))))
>                #:guile %bootstrap-guile)
>
>
> This is similar to:
>
>   guix shell -C guile guix -- \
>     guile -c '(use-modules (guix build syscalls)) (mount "none" (getenv "GUIX_ENVIRONMENT") "none" (logior MS_BIND MS_REMOUNT))'
>
> mount(2) has this:
>
>    EPERM  An attempt was made to modify (MS_REMOUNT) the MS_RDONLY, MS_NO‐
>           SUID, or MS_NOEXEC flag, or one of the "atime"  flags  (MS_NOAT‐
>           IME,  MS_NODIRATIME,  MS_RELATIME) of an existing mount, but the
>           mount is locked; see mount_namespaces(7).
>
> I couldn’t find the definite answer in mount_namespaces(7) as to whether
> this applies in this case (same namespace but after chroot); I can only
> tell empirically that it does apply.

I don't think that's why we're getting EPERM.  I think we're running
into this, from user_namespaces(7):

   Note that a call to execve(2) will cause a process's capabilities to
   be recalculated in the usual way (see capabilities(7)).
   Consequently, unless the process has a user ID of 0 within the
   namespace, or the executable file has a nonempty inheritable
   capabilities mask, the process will lose all capabilities.  See the
   discussion of user and group ID mappings, below.

As the builder is in the store, it can't have any associated capability
masks, and your added call to prctl to drop ambient capabilities,
together with the fact that the mapped UID inside the container is
nonzero, should make it so that it therefore wouldn't be able to inherit
any.

On a tangentially-related note, the ambient capability set didn't come
into being until Linux 4.3 (around 2016), which is a fair bit newer than
unprivileged user namespaces.  Take that for what you will.

Now, according to capabilities(7):

    Per-user-namespace "set-user-ID-root" programs
       A  set-user-ID  program  whose  UID matches the UID that created a user
       namespace will confer capabilities in the process's permitted  and  ef‐
       fective  sets when executed by any process inside that namespace or any
       descendant user namespace.

       The rules about the transformation of the process's capabilities during
       the  execve(2)  are exactly as described in Transformation of capabili‐
       ties during execve() and Capabilities and execution of programs by root
       above,  with  the  difference that, in the latter subsection, "root" is
       the UID of the creator of the user namespace.

This would seem to suggest that the capabilities within the user
namespace could be regained by creating a setuid binary and executing
it, but experimentally this doesn't happen, and I am unsure whether this
is a bug in the documentation, kernel, or my reading comprehension.  At
any rate, I am less than confident in relying on this behavior.

I think it would be a good idea to, in the no-build-user case, add an
extra call to unshare right at the point where the user and group would
be changed in the build-user case.  This extra call would create a fresh
user and mount namespace, ensuring that the mount-locking behavior you
referenced applies.  My understanding is that the setuid behavior
documented above only grants capabilities, it doesn't change the user
namespace that the process is in, so it should be impossible for the
builder to gain capabilities inside the user namespace owning the
bind-mounted store items, even if it somehow gained full capabilities
within this fresh user namespace.


> -	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");
> +	}
> +
> +	if ((flags & CLONE_NEWUSER) != 0) {
> +	    /* Initialize the UID/GID mapping of the guest.  */
> +	    if (pid == 0) {
> +		char str[20] = { '\0' };
> +		readFull(readiness.readSide, (unsigned char*)str, 3);
> +		if (strcmp(str, "go\n") != 0)
> +		    throw Error("failed to initialize process in unprivileged user namespace");
> +	    } else {
> +		initializeUserNamespace(pid);
> +		writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
> +	    }

This doesn't actually do any synchronizing with the child process,
because clone never returns 0.  It's not like fork where it returns
twice with a different return value each time, control in the new thread
instead goes straight to childEntry.  The parent doesn't get stuck and
hang when writing because PIPE_BUF > 3.

>> This does raise the question, though, of how these failed build
>> directories would get deleted, aside from rebooting the system.
>
> Note that in the early days (and in current Nix actually), build trees
> were not chowned.  That’s OK: they’re deleted upon reboot or by the
> system administrator.
>
> Current Nix has this:
>
> void DerivationGoal::deleteTmpDir(bool force)
> {
>     if (tmpDir != "") {
>         /* Don't keep temporary directories for builtins because they
>            might have privileged stuff (like a copy of netrc). */
>         if (settings.keepFailed && !force && !drv->isBuiltin()) {
>             printError("note: keeping build directory '%s'", tmpDir);
>             chmod(tmpDir.c_str(), 0755);
>         }
>         else
>             deletePath(tmpDir);
>         tmpDir = "";
>     }
> }
>
> We could go back to this.  It’s less convenient, but okay.
>
> In this patch series, it attempts to chown the tree; if it fails to do
> so (because it lacks CAP_CHOWN), it prints a warning and keeps going.

My concern comes from knowing that I've at times gone through 100
sequential failed builds while trying to package something tricky, and I
tend to keep my disk on the low end of free space to minimize how often
I need to rebuild stuff.  That and the one time I tried tinkering with
ungoogled-chromium.  I know I'd probably cause a lot of trouble if I
tried doing that stuff on a shared system I didn't have administrative
access to.

A best-effort chown attempt should do fine for now, though.

>> We currently remount /gnu/store read-write at LocalStore-creation-time,
>> which happens in the newly-forked guix-daemon process at the start of a
>> connection.  I don't think there's any particularly elevated risk from
>> instead doing that before the per-connection process is forked.  There
>> are a number of ways we could do this: we could make it the
>> responsibility of the init system to create the mount namespace and do
>> the remounting, or we could have guix-daemon do it immediately on
>> startup and subsequently switch its uid and gid to
>> guix-daemon:guix-daemon.  These lack the slick appeal of "see, you never
>> have to give it root, and you can prove it just by looking at the
>> service file", but realistically should be just as secure.  It may be
>> useful to provide a small wrapper around guix-daemon that does the
>> remount and privilege-dropping, to more succinctly express this to
>> anybody wishing to see for themselves.
>
> I think I’d prefer to have a systemd (or other) service make a
> read-write bind-mount at /gnu/store/.rw-store, and then we’d run
> ‘guix-daemon --backing-store=/gnu/store/.rw-store’.
>
> WDYT?

So if I understand correctly, we would have /gnu/store hold all of its
usual contents in the usual manner, and a service would bind-mount
/gnu/store to /gnu/store/.rw-store without MS_RDONLY, and then it (or
another service that depends on it) would bind-mount /gnu/store to
itself with MS_RDONLY, and then guix-daemon would, in its own mount
namespace, bind-mount /gnu/store/.rw-store to /gnu/store, again without
MS_RDONLY.

I assume that making /gnu/store read-only wouldn't make the
already-bind-mounted /gnu/store/.rw-store read-only too?  If it does,
it's not going to work, and if it doesn't, it's going to remain writable
for footgun appreciators.  But I suppose it's at least a little more
out-of-the-way.

I think it might be simpler to integrate the change if we instead made
it /gnu/.rw-store or something like that, since that way we don't have
to worry about updating the garbage collector and such to treat it
specially.

Actually, now that I think about it, another possibility would be having
a service that the read-only store-mount service depends on that first
creates a persistent user+mount namespace combo which saves a view of
the writable store (I don't recall exactly how creating the persistent
namespace works, but I know the 'ip netns ...' commands can do something
similar to create named network namespaces).  The process that creates
this namespace would run as the guix-daemon user, and therefore when
guix-daemon starts it would have full capabilities within that user
namespace, and could setns straight into it.  This would leave no
writable store in the root mount namespace.

>> Personally, I think that if a guix-daemon can use privilege separation
>> users, it would probably be a good idea to.  We're certainly going to
>> need to support them on non-linux systems either way.  Could it be
>> possible to have guix-install.sh modify /etc/sudoers on systems that use
>> it to allow the guix-daemon user to run processes under guix builder
>> users?  I am currently less worried about arbitrary code execution
>> vulnerabilities being found in the daemon than about the possibility of
>> malicious builders (but it is possible I am underexposed to the ways
>> those can happen in C++).
>
> What would you put in /etc/sudoers?  I’m not sure what you had in
> mind.

I'm not sure what I had in mind either, I've only seen some opine that
it's usually better to configure sudo than to write your own setuid
programs, which was the first thing that came to mind for how to use
dedicated build users without needing the entire daemon running as root.
I recall reading somewhere that it could be configured to allow certain
users to run certain commands as certain other users?  So maybe it could
be configured to allow the guix-daemon user to run any command as any of
the guixbuilder users.  Although granted, the way that container setup
is currently done wouldn't work very well with that, since by the time
we're ready to execute the builder we're already fully in the container,
where setuid-root binaries should probably not be.

I know that "how to use dedicated build users without root" probably
isn't what you were asking for feedback on, but it did show up in my
thoughts quite a bit.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 31 Jan 2025 22:37:02 GMT) Full text and rfc822 format available.

Message #47 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Fri, 31 Jan 2025 16:35:24 -0600
[Message part 1 (text/plain, inline)]
I've found another vulnerability in using guix-daemon as the build user:
the chroot root directory is owned by the build user.  By itself this
would normally only cause some reproducibility issues, but that
directory is also visible from the outside world as
/gnu/store/...-packagename.drv.chroot.  Consequently, a simple chmod
from the builder can expose the contents of the chroot, including any
setuid programs.

Demonstration:

--8<---------------cut here---------------start------------->8---
(use-modules (guix)
             (gnu)
             (guix build-system trivial))

(define-public sneakysneaky
  (package
    (name "sneakysneaky")
    (version "0")
    (source #f)
    (build-system trivial-build-system)
    (arguments
     (list
      #:builder
      #~(let ((guile (string-append (assoc-ref %guile-build-info
                                               'bindir)
                                    "/guile")))
          (chmod "/" #o777)
          (copy-file guile "/guile")
          (chmod "/guile" #o6755)
          (sleep 1000)
          (mkdir #$output))))
    (home-page "")
    (synopsis "")
    (description "")
    (license #f)))

sneakysneaky
--8<---------------cut here---------------end--------------->8---

If I save this as /tmp/mal-test3.scm, I can observe the following:

--8<---------------cut here---------------start------------->8---
user <at> debian:~$ guix build --derivations --no-grafts -f /tmp/mal-test3.scm
/gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv
user <at> debian:~$ guix build /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv
substitute: looking for substitutes on 'https://bordeaux.guix.gnu.org'... 100.0%
substitute: looking for substitutes on 'https://ci.guix.gnu.org'... 100.0%
The following derivation will be built:
  /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv
building /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv...
  C-c C-z
[1]+  Stopped                 guix build /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv
user <at> debian:~$ 
user <at> debian:~$ ls /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv.chroot
dev  etc  gnu  guile  proc  tmp
user <at> debian:~$ /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv.chroot/guile
guile: warning: failed to install locale
warning: failed to install locale: Invalid argument
GNU Guile 3.0.9
Copyright (C) 1995-2023 Free Software Foundation, Inc.

Guile comes with ABSOLUTELY NO WARRANTY; for details type `,show w'.
This program is free software, and you are welcome to redistribute it
under certain conditions; type `,show c' for details.

Enter `,help' for help.
scheme@(guile-user)> (geteuid)
$1 = 999
scheme@(guile-user)> (getegid)
$2 = 996
scheme@(guile-user)> 
user <at> debian:~$ id
uid=1000(user) gid=1000(user) groups=1000(user),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),100(users),106(netdev),113(bluetooth),117(scanner)
user <at> debian:~$ 
--8<---------------cut here---------------end--------------->8---


The security impact of this could be resolved by doing the same thing we
do with build directories - have the actual mounted-into-the-chroot
directory be the "/top" subdirectory of the externally-visible chroot
directory.  In the example above, that would be
/gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv.chroot/top.
Due to the use of pivot_root, the upper .chroot directory would become
completely inaccessible to the builder, ensuring that it remains
inaccessible for unprivileged users.

I'm less sure about how to resolve the impact to reproducibility.  We
could try mounting the root directory specifically as read-only,
perhaps, though my understanding is that this may cause open, chmod, etc
to return EROFS instead of EACCES or EPERM.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:01 GMT) Full text and rfc822 format available.

Message #50 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 0/9] Rootless daemon
Date: Thu, 13 Feb 2025 13:13:03 +0100
Hello,

Here’s an update with some of the fixes suggested by Reepca:

  • Remounting inputs as read-only since MS_BIND | MS_RDONLY
    doesn’t do what one might think;

  • Bind-mounting everything and not just directories;

  • Adding tests to ensure that inputs cannot be remounted
    as read-write, overwritten, etc.;

  • Fix bogus synchronization for uid_map/gid_map creation;

  • Use ‘clone_range’ (unrelated to the rest of this series
    but nice).

One of the critical open issues that remain is the fact that
the root file system in the build environment is writable, and
thus a build process can (chmod "/" #o777) and expose setuid
binaries etc.

The other one is lack of support for read-only store remount
(‘--backing-store’ option has yet to be added).

Ludo’.

Ludovic Courtès (9):
  daemon: Use ‘close_range’ where available.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  DRAFT tests: Run in a chroot and unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.

 build-aux/test-env.in       |  14 ++-
 config-daemon.ac            |   5 +-
 etc/guix-daemon.service.in  |  12 ++-
 etc/guix-install.sh         | 114 +++++++++++++++++++-----
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 171 ++++++++++++++++++++++++++----------
 nix/libstore/local-store.cc |  30 ++++---
 nix/libutil/util.cc         |  23 +++--
 tests/store.scm             | 144 ++++++++++++++++++++++--------
 9 files changed, 388 insertions(+), 129 deletions(-)


base-commit: bc6769f1211104dbc9341c064275cd930f5dfa3a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:02 GMT) Full text and rfc822 format available.

Message #53 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 1/9] daemon: Use ‘close_range’ where available.
Date: Thu, 13 Feb 2025 13:13:04 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc39..4e949bc88a3 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b1..eb2d16e1cc3 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:02 GMT) Full text and rfc822 format available.

Message #56 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v2 2/9] daemon: Bind-mount all the inputs,
 not just directories.
Date: Thu, 13 Feb 2025 13:13:05 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 27 ++-------------------------
 1 file changed, 2 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34d..f4cd2131c84 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1850,9 +1850,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1861,7 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
-            if (lstat(i->c_str(), &st))
-                throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
-
-                regularInputPaths.insert(*i);
-            }
+	    dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:03 GMT) Full text and rfc822 format available.

Message #59 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 6/9] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Thu, 13 Feb 2025 13:13:09 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 4308264a4f3..63846695194 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:03 GMT) Full text and rfc822 format available.

Message #62 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 7/9] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Thu, 13 Feb 2025 13:13:10 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a3..35d9c8cd56b 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 18dd27460b7..4280b8abff8 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2059,6 +2062,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:03 GMT) Full text and rfc822 format available.

Message #65 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 5/9] DRAFT tests: Run in a chroot and unprivileged user
 namespaces.
Date: Thu, 13 Feb 2025 13:13:08 +0100
DRAFT:

  - Double-check the test suite.

* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write"): New tests.

Change-Id: Iedb816ef548c77799e5b2f9b6a3b7510ad19ec2a
---
 build-aux/test-env.in |  14 +++-
 tests/store.scm       | 144 ++++++++++++++++++++++++++++++++----------
 2 files changed, 121 insertions(+), 37 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da581..5626152b346 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,20 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f433..cf19cf91211 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,85 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-remount-input-read-write"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1416,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:04 GMT) Full text and rfc822 format available.

Message #68 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>
Subject: [PATCH v2 4/9] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Thu, 13 Feb 2025 13:13:07 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

* 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.

Change-Id: I38fbe01f80fb45a99cd8a391e55a39a54d64fcb7
---
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 128 +++++++++++++++++++++++++++++-------
 nix/libstore/local-store.cc |  22 +++++--
 3 files changed, 123 insertions(+), 31 deletions(-)

diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index e31b3940203..2761a3dafb4 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013-2021, 2023-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2014 Nikita Karetnikov <nikita <at> karetnikov.org>
 ;;; Copyright © 2018 Kyle Meyer <kyle <at> kyleam.com>
 ;;; Copyright © 2020 Christopher Baines <mail <at> cbaines.net>
@@ -76,7 +76,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 7151bb6c6f1..18dd27460b7 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -747,6 +747,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();
@@ -1622,6 +1626,25 @@ 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)
+{
+    auto hostUID = getuid();
+    auto 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(
@@ -1685,7 +1708,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");
@@ -1802,7 +1825,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
@@ -1821,8 +1844,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"). */
@@ -1857,7 +1880,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) {
@@ -1948,14 +1971,34 @@ 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");
+	}
+
+	if ((flags & CLONE_NEWUSER) != 0) {
+	     /* Initialize the UID/GID mapping of the child process.  */
+	     initializeUserNamespace(pid);
+	     writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
+	}
     } else
 #endif
     {
@@ -2001,23 +2044,34 @@ void DerivationGoal::runChild()
 
         _writeToStderr = 0;
 
+	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);
+	     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";
@@ -2447,8 +2501,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;
         }
@@ -2707,8 +2769,25 @@ 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;
+		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.  */
+			chown(tmpDir.c_str(), getuid(), getgid());
+		    }
+		} 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) {
 		    // Rename tmpDir to its parent, with an intermediate step.
@@ -2717,6 +2796,11 @@ void DerivationGoal::deleteTmpDir(bool force)
 			throw SysError("pivoting failed build tree");
 		    if (rename((pivot + "/top").c_str(), top.c_str()) == -1)
 			throw SysError("renaming failed build tree");
+
+		    if (getuid() != 0)
+			/* Running unprivileged but with CAP_CHOWN.  */
+			chown(top.c_str(), uid, gid);
+
 		    rmdir(pivot.c_str());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbcee..4308264a4f3 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -306,14 +306,14 @@ void LocalStore::openDB(bool create)
 void LocalStore::makeStoreWritable()
 {
 #if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_REMOUNT)
-    if (getuid() != 0) return;
     /* Check if /nix/store is on a read-only mount. */
     struct statvfs stat;
     if (statvfs(settings.nixStore.c_str(), &stat) != 0)
         throw SysError("getting info about the store mount point");
 
     if (stat.f_flag & ST_RDONLY) {
-        if (unshare(CLONE_NEWNS) == -1)
+	int flags = CLONE_NEWNS | (getpid() == 0 ? 0 : CLONE_NEWUSER);
+        if (unshare(flags) == -1)
             throw SysError("setting up a private mount namespace");
 
         if (mount(0, settings.nixStore.c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1)
@@ -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);
+	}
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:04 GMT) Full text and rfc822 format available.

Message #71 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 9/9] guix-install.sh: Support the unprivileged daemon where
 possible.
Date: Thu, 13 Feb 2025 13:13:12 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.
(sys_enable_guix_daemon): Do not install ‘gnu-store.mount’ when running
an unprivileged daemon.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 114 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 91 insertions(+), 23 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index f07b2741bb9..4f08eff8476 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -389,6 +389,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg "${INF}Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg "${INF}Linking the root user's profile"
@@ -414,38 +419,82 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg "${INF}user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg "${PAS}user added <$user>"
+    fi
+}
+
+can_install_unprivileged_daemon()
+{ # Return true if we can install guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
+	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg "${INF}group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg "${PAS}group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg "${INF}group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg "${INF}user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild${KVMGROUP}     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild${KVMGROUP}     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg "${PAS}user added <guixbuilder${i}>"
-        fi
-    done
+    if [ "$INIT_SYS" = systemd ] && \
+	   grep -q "User=guix-daemon" \
+		~root/.config/guix/current/lib/systemd/system/guix-daemon.service
+    then
+	if getent group guix-daemon > /dev/null; then
+	    _msg "${INF}group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg "${PAS}group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon	\
+	      /gnu /var/guix
+
+	# The unprivileged cannot create the log directory by itself.
+	mkdir /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg "${INF}group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg "${PAS}group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -460,6 +509,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg "${INF}remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -503,7 +560,14 @@ sys_enable_guix_daemon()
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
               # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      #
+	      # FIXME: This feature is unavailable when running an
+	      # unprivileged daemon.
+	      if ! grep -q "User=guix-daemon" \
+		   /etc/systemd/system/guix-daemon.service
+	      then
+		  install_unit gnu-store.mount
+	      fi
 
               systemctl daemon-reload &&
                   systemctl start  guix-daemon; } &&
@@ -627,6 +691,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg "${PAS}Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg "${INF}Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:05 GMT) Full text and rfc822 format available.

Message #74 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v2 3/9] daemon: Remount inputs as read-only.
Date: Thu, 13 Feb 2025 13:13:06 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index f4cd2131c84..7151bb6c6f1 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2094,8 +2094,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source != tmpDir) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 12:14:05 GMT) Full text and rfc822 format available.

Message #77 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v2 8/9] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Thu, 13 Feb 2025 13:13:11 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(User, AmbientCapabilities): New fields.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/guix-daemon.service.in | 12 +++++++++++-
 1 file changed, 11 insertions(+), 1 deletion(-)

diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1b..f9f0b28b356 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -7,9 +7,19 @@ Description=Build daemon for GNU Guix
 
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
 Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Provide the CAP_CHOWN capability so that guix-daemon cran create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Feb 2025 13:30:02 GMT) Full text and rfc822 format available.

Message #80 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Thu, 13 Feb 2025 14:29:10 +0100
Hello Reepca,

Thanks a lot for your feedback, very useful as always.

I’ve sent a v2 addressing some of the issues you mentioned before.

Crucially, this one remains:

>       #~(let ((guile (string-append (assoc-ref %guile-build-info
>                                                'bindir)
>                                     "/guile")))
>           (chmod "/" #o777)
>           (copy-file guile "/guile")
>           (chmod "/guile" #o6755)
>           (sleep 1000)

That is, / is currently writable inside the build environment, and
that’s:

  1. a security issue, but it could be addressed with a /top
     sub-directory as you wrote;

  2. a reproducibility issue because a build process now be able to
     create/modify files anywhere.

I looked for solutions to this and couldn’t find anything so far.

In particular, re-mounting / read-only makes everything beneath it
read-only, including mount points that were initially read-write.  It
might be that the wealth of MS_ options could be used to address that,
but honestly, it’s a mess and a maze (“shared subtrees”?).
Alternatively, I wondered if we could make / owned by the overflow user,
but that’s probably not possible.

Perhaps yet another option would be to use subordinate IDs to map two
different users in the container, but that sounds more involved and I’m
not sure how to get that done.

Thoughts?

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Feb 2025 15:24:02 GMT) Full text and rfc822 format available.

Message #83 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Fri, 14 Feb 2025 16:23:28 +0100
Ludovic Courtès <ludo <at> gnu.org> skribis:

> In particular, re-mounting / read-only makes everything beneath it
> read-only, including mount points that were initially read-write.

OK, I think I was sleepy or something yesterday: it’s enough to create
separate mount points for /tmp and for the store in the mount
namespaces, and these will remain writable after / has been remounted
read-only.  Working on it!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 15 Feb 2025 01:50:02 GMT) Full text and rfc822 format available.

Message #86 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Fri, 14 Feb 2025 19:47:27 -0600
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

> I’ve sent a v2 addressing some of the issues you mentioned before.
>
> Crucially, this one remains:
>
>>       #~(let ((guile (string-append (assoc-ref %guile-build-info
>>                                                'bindir)
>>                                     "/guile")))
>>           (chmod "/" #o777)
>>           (copy-file guile "/guile")
>>           (chmod "/guile" #o6755)
>>           (sleep 1000)
>
> That is, / is currently writable inside the build environment, and
> that’s:
>
>   1. a security issue, but it could be addressed with a /top
>      sub-directory as you wrote;
>
>   2. a reproducibility issue because a build process now be able to
>      create/modify files anywhere.
>
> I looked for solutions to this and couldn’t find anything so far.
>
> In particular, re-mounting / read-only makes everything beneath it
> read-only, including mount points that were initially read-write.  It
> might be that the wealth of MS_ options could be used to address that,
> but honestly, it’s a mess and a maze (“shared subtrees”?).

(Note: I've since seen your followup email on this, but I think there's
still some interesting ideas in what I wrote before then)

Unless there is special behavior for /, I don't see this (every mount
point beneath it becoming read-only) happening.  When a bind-mount is
created, it inherits its options from the filesystem that the source is
on ("The bind mount has the same mount options as the underlying mount"
in mount(2)).  This does not prevent MS_REMOUNT from being used with the
MS_RDONLY bit zeroed to subsequently make the newly-created mount point
writable, nor, to my knowledge, does it modify the flags of any existing
mount points underneath the bind-mount when MS_REC is used with MS_BIND.

I expect that it should work to:
1. go through the entire normal chroot setup
2. bind-mount /gnu/store and /tmp to themselves within the chroot using
   MS_REC so that they are treated as distinct filesystems but also
   still have their existing bind-mounts underneath them
3. bind-mount / to itself using MS_REC
4. remount / read-only using MS_RDONLY | MS_REMOUNT | MS_BIND

This should ensure that the only writable files in the chroot are those
either in /tmp, /gnu/store, or in another filesystem inside the chroot
(e.g. /dev, /proc, any of the bind mounts in /gnu/store if we were to
forget to remount them MS_RDONLY, etc).

But note that this will cause open(2) and chmod(2) for filenames in the
same filesystem as / to return EROFS instead of EACCES, and it will
still be visible to builders that it's owned by the build user.  For
that matter the same difference will be observable for bind-mounted
store items, but this should matter less because we are already in the
practice of registered store items being in a store mounted read-only in
practical usage.

We could try setting the user-writable permission bit to 0 for /, so
that it will give EACCES, which might avoid some of the worst of the
unreproducibility.

Another option would be to use a root-owned "template" root directory
that just contains the (empty) subdirectories gnu, gnu/store, tmp, proc,
and dev.  This template directory would become the root directory used
by pivot_root, with individual filesystems and bind mounts created on
top of its subdirectories inside the container's mount namespace.  This
requires no special permissions, the template directory just has to
exist and be publicly-visible.

It does occur to me now, though, that we wouldn't be able to actually
map any other uids within the container to anything without CAP_SETUID,
so / would end up appearing as being owned by the overflow uid.  Aside
from the actual number, though, it should behave like it's owned by
root, EACCES and all.  I suppose the same behavior would also be
observed if the template were owned by any user other than the build
user, not just root.

> Alternatively, I wondered if we could make / owned by the overflow user,
> but that’s probably not possible.
>
> Perhaps yet another option would be to use subordinate IDs to map two
> different users in the container, but that sounds more involved and I’m
> not sure how to get that done.

My still-young understanding of subordinate IDs is that they're not
really a kernel thing, but rather are honored by two setuid programs
from the shadow package, newuidmap and newgidmap, so that would be a bit
like using a configured sudo, albeit probably easier to integrate with
the daemon since they basically just replace the initializeUserNamespace
procedure with running a command.

We would basically just pick a uid and gid for a guixbuild user (there's
no reason not to use the regular user-and-group-adding processes for
this), then add entries in /etc/subuid and /etc/subgid indicating that
guix-daemon is allowed to map exactly that user and exactly that group,
as well as its own user and group.  We would then add a case in
initializeUserNamespace that would fork+exec+wait calls to newuidmap and
newgidmap that map two uids and gids: uid and gid 0 map to the
guix-daemon user and group, and guestUID and guestGID are mapped to the
guixbuild user and group.

In the child, we initially have CAP_SETUID within the user namespace,
and can therefore set our user and group ids to the newly-mapped
guixbuild user / group.  The directories created during the container
setup will all appear to be owned by uid and gid 0.  Note that when
creating the chroot store we'll need to make sure that its group is
guixbuild so that the builder can write to it, and I'm not sure how to
handle chown'ing of build directories in this case (is it even possible
for two cooperating unprivileged users to transfer ownership of an
inode?).

The earliest reference I can find to new*map in the shadow changelog is
from 2013, so it's at least that old.  We should probably keep the
map-single-id case around in initializeUserNamespace as a fallback for
the fully-unprivileged use case, e.g. test-env.

While this adds an external dependency on a setuid program, it is at
least a setuid program that should be fairly common and have a lot of
security-minded attention on it, and be less complex than something like
sudo.  In exchange, we would get the cleanest rootless-with-an-asterisk
daemon configuration I can think of, with no known reproducibility
issues, little modification to the daemon required, and the extra safety
net of a dedicated build user.

It sounds like a pretty decent route to take for the
privileged-but-rootless case.


> @@ -2707,8 +2769,25 @@ 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;
> +		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.  */
> +			chown(tmpDir.c_str(), getuid(), getgid());
> +		    }
> +		} 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) {
>  		    // Rename tmpDir to its parent, with an intermediate step.

(Note: pedantic aside here, there aren't currently issues with what is
written immediately below as long as the top directory is used to shield
the tmpDir - the top directory is doing a LOT of heavy lifting)

It shouldn't be a problem in practice due to top only being
owner-accessible, but I feel like I should still note that the second
chown here would be of a file that was previously owned by the client
user, and as such could, in the most general case, have been replaced
with anything, such as a setuid program or symlink.  While chown(2)
resets setuid and setgid bits for unprivileged users, it's unspecified
by posix whether this occurs for privileged users.  Linux currently does
this permission resetting for privileged users, but it wouldn't surprise
me if there's still ways to screw things up by chown'ing a symlink.

Also, _chown does the actual chown on descent, not on return, so it
first chowns a directory and then goes through its contents.  This means
that, again, if there weren't the top directory there to block access,
it would be possible to access a setuid program before it was chown'ed.

We seem to be relying entirely on the Linux behavior of chown to reset
setuid / setgid bits.  And the man page isn't even entirely clear on
this: it says those bits are cleared for "an unprivileged user", that a
"privileged" user here means one with CAP_CHOWN, and that in Linux since
2.2.13 "root" is treated like other users.  This doesn't answer the
question of what happens for privileged non-root users.  It's also not
clear what happens when a user chown's a file with a uid and gid that
aren't -1, but are the same as the current owner and group of the file
(experimentally, it still resets the setid bits).  It would probably be
a good idea to explicitly reset these bits in _chown, and perhaps modify
_chown to operate bottom-up instead of top-down.  Alternatively, we
could use secureFilePerms before calling _chown.

Also, not shown here, but there's a chmod(tmpDir.c_str(), 0755) shortly
before all of this, which means there's a window before _chown could be
called in which a setuid program could be exposed, if not for the top
directory shielding tmpDir.  And if settings.clientUid is -1 or 0, then
that window has no end.

Just something to keep in mind.

(End pedantic aside)



Now, for the non-pedantic, significant issue that I came across while
writing all that: previously, it was not possible for the
tmpDir-exposing code to be reached without doing the _chown that also
reset setuid and setgid bits.  But with this patch, in the non-root,
non-CAP_CHOWN case (which is what is currently proposed for Guix
System), it can be reached through the catch clause.  In that case it
will expose tmpDir without changing any permission bits of files beneath
it, allowing anybody who can access a setuid program in tmpDir (which,
due to that 0755 chmod, is "everybody") to take control of the build
user (which in this case would be guix-daemon).

Going by the "Running unprivileged but with CAP_CHOWN" comment, it would
seem that code is meant to only be reached by reaching the end of the
"try" block, not by reaching the end of the "catch" block.  I think it
would be a good idea to call secureFilePerms(tmpDir) before any attempt
at chown'ing.



I still think it would be a good idea to call unshare to create an extra
user and mount namespace just before executing the builder in the
unprivileged case, just to be sure that the mount-locking behavior is
triggered in a way that is documented.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 15 Feb 2025 21:35:01 GMT) Full text and rfc822 format available.

Message #89 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Sat, 15 Feb 2025 22:33:58 +0100
Hi,

(Just a quick reply; there’s a lot in here. :-))

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> I expect that it should work to:
> 1. go through the entire normal chroot setup
> 2. bind-mount /gnu/store and /tmp to themselves within the chroot using
>    MS_REC so that they are treated as distinct filesystems but also
>    still have their existing bind-mounts underneath them
> 3. bind-mount / to itself using MS_REC
> 4. remount / read-only using MS_RDONLY | MS_REMOUNT | MS_BIND

Yes.  I pushed what I have now at
<https://codeberg.org/civodul/guix/src/branch/wip-rootless-daemon>.  It
does work as expected: / is read-only, /tmp and /gnu/store are
read-write, individual inputs in /gnu/store are read-only.

> Also, _chown does the actual chown on descent, not on return, so it
> first chowns a directory and then goes through its contents.  This means
> that, again, if there weren't the top directory there to block access,
> it would be possible to access a setuid program before it was chown'ed.

Right.

> We seem to be relying entirely on the Linux behavior of chown to reset
> setuid / setgid bits.

Yes, and I think chown(2) is quite clear:

  When the owner or group of an executable file is changed by an
  unprivileged user, the S_ISUID and S_ISGID mode bits are cleared.
  […] since Linux 2.2.13, root is treated like other users.

> Now, for the non-pedantic, significant issue that I came across while
> writing all that: previously, it was not possible for the
> tmpDir-exposing code to be reached without doing the _chown that also
> reset setuid and setgid bits.  But with this patch, in the non-root,
> non-CAP_CHOWN case (which is what is currently proposed for Guix
> System), it can be reached through the catch clause.  In that case it
> will expose tmpDir without changing any permission bits of files beneath
> it, allowing anybody who can access a setuid program in tmpDir (which,
> due to that 0755 chmod, is "everybody") to take control of the build
> user (which in this case would be guix-daemon).

I’m not sure I understand what you mean by “the tmpDir-exposing code”;
are you talking about ‘DerivationGoal::deleteTmpDir’?

> Going by the "Running unprivileged but with CAP_CHOWN" comment, it would
> seem that code is meant to only be reached by reaching the end of the
> "try" block, not by reaching the end of the "catch" block.  I think it
> would be a good idea to call secureFilePerms(tmpDir) before any attempt
> at chown'ing.

Yeah, we can do that to be on the safe side.

> I still think it would be a good idea to call unshare to create an extra
> user and mount namespace just before executing the builder in the
> unprivileged case, just to be sure that the mount-locking behavior is
> triggered in a way that is documented.

The problem with “mount locking” (and “peer group” and in fact most
“concepts” mentioned in mount(2)) is that it’s not clearly defined.
Here I’m relying on unit tests to ensure that the various bits can
indeed not be remounted read-write, for instance.  (“make check” tests
the same setup as unprivileged daemon, which is an advantage over the
current situation where the separate-build-user setup is not covered by
the test suite.)

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 16 Feb 2025 02:41:02 GMT) Full text and rfc822 format available.

Message #92 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Sat, 15 Feb 2025 20:40:04 -0600
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

>> We seem to be relying entirely on the Linux behavior of chown to reset
>> setuid / setgid bits.
>
> Yes, and I think chown(2) is quite clear:
>
>   When the owner or group of an executable file is changed by an
>   unprivileged user, the S_ISUID and S_ISGID mode bits are cleared.
>   […] since Linux 2.2.13, root is treated like other users.

Ah, I misread it as "changed by an unprivileged process", not "changed
by an unprivileged user".  That clears things up.

>> Now, for the non-pedantic, significant issue that I came across while
>> writing all that: previously, it was not possible for the
>> tmpDir-exposing code to be reached without doing the _chown that also
>> reset setuid and setgid bits.  But with this patch, in the non-root,
>> non-CAP_CHOWN case (which is what is currently proposed for Guix
>> System), it can be reached through the catch clause.  In that case it
>> will expose tmpDir without changing any permission bits of files beneath
>> it, allowing anybody who can access a setuid program in tmpDir (which,
>> due to that 0755 chmod, is "everybody") to take control of the build
>> user (which in this case would be guix-daemon).
>
> I’m not sure I understand what you mean by “the tmpDir-exposing code”;
> are you talking about ‘DerivationGoal::deleteTmpDir’?

by "the tmpDir-exposing code" I mean this section inside of
DerivationGoal::deleteTmpDir, starting at nix/libstore/build.cc line
2818 in commit e6c588:

--8<---------------cut here---------------start------------->8---
		if (top != tmpDir) {
		    // 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 (getuid() != 0)
			/* Running unprivileged but with CAP_CHOWN.  */
			chown(top.c_str(), uid, gid);

		    rmdir(pivot.c_str());
		}
--8<---------------cut here---------------end--------------->8---

>> I still think it would be a good idea to call unshare to create an extra
>> user and mount namespace just before executing the builder in the
>> unprivileged case, just to be sure that the mount-locking behavior is
>> triggered in a way that is documented.
>
> The problem with “mount locking” (and “peer group” and in fact most
> “concepts” mentioned in mount(2)) is that it’s not clearly defined.
> Here I’m relying on unit tests to ensure that the various bits can
> indeed not be remounted read-write, for instance.  (“make check” tests
> the same setup as unprivileged daemon, which is an advantage over the
> current situation where the separate-build-user setup is not covered by
> the test suite.)

Both of those concepts are described in mount_namespaces(7).  While my
reading of that leaves me with several questions, the section
"restrictions on mount namespaces" does have this:

       [5] The mount(2) flags MS_RDONLY, MS_NOSUID, MS_NOEXEC, and the "atime"
           flags  (MS_NOATIME,  MS_NODIRATIME,  MS_RELATIME)  settings  become
           locked when propagated from a more privileged to a less  privileged
           mount  namespace,  and  may  not  be changed in the less privileged
           mount namespace.

           This point is illustrated in the following example where, in a more
           privileged  mount  namespace, we create a bind mount that is marked
           as read-only.  For security reasons, it should not be  possible  to
           make  the  mount writable in a less privileged mount namespace, and
           indeed the kernel prevents this:

               $ sudo mkdir /mnt/dir
               $ sudo mount --bind -o ro /some/path /mnt/dir
               $ sudo unshare --user --map-root-user --mount \
                              mount -o remount,rw /mnt/dir
               mount: /mnt/dir: permission denied.

which seems to indicate that it is sufficient for preventing
modification of mount flags that the caller be in a less privileged
mount namespace than the one the mount was inherited from.  "Less
privileged" is defined as:

       [1] Each mount namespace has an owner  user  namespace.   As  explained
           above,  when  a  new  mount namespace is created, its mount list is
           initialized as a copy of the mount list of another mount namespace.
           If  the  new  namespace and the namespace from which the mount list
           was copied are owned by different user  namespaces,  then  the  new
           mount namespace is considered less privileged.

So putting the builder in a fresh mount namespace owned by a fresh user
namespace should suffice to achieve this.

It's worth noting that EPERM is returned by mount both for attempts to
modify locked mount points and for just generally not having the
required capability, so a unit test may have trouble establishing why a
particular behavior is being observed.  Ideally it wouldn't be possible
to modify the inputs even if the builder managed to acquire the required
capability in its user namespace.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Feb 2025 16:53:02 GMT) Full text and rfc822 format available.

Message #95 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludovic.courtes <at> inria.fr>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Remounting the store read-write for guix-daemon
Date: Mon, 17 Feb 2025 17:52:30 +0100
Hi,

Ludovic Courtès <ludo <at> gnu.org> skribis:

> I think I’d prefer to have a systemd (or other) service make a
> read-write bind-mount at /gnu/store/.rw-store, and then we’d run
> ‘guix-daemon --backing-store=/gnu/store/.rw-store’.

For a moment, I thought we could just do nothing on our side and instead
take advantage of what systemd (and shepherd) have to offer.

On the systemd side, there are several things that looked promising¹.
First option:

  PrivateMounts=true
  PrivateUsers=true
  ReadWritePaths=/gnu/store

But that doesn’t work: the doc says that files in ‘ReadWritePaths’ “are
accessible from within the namespace with the same access modes as from
outside of it” (so read-only in our case).

Second option:

  BindPaths=/gnu/store

… but that does essentially nothing, and we can’t specify that we want
“remount,rw”.

Third option:

  ExecStartPre=mount --bind -o rw,remount /gnu/store

… but the doc for ‘PrivateMounts’ says that “[m]ounts established in the
namespace of the process created by ExecStartPre= will hence be cleaned
up automatically as soon as that process exits and will not be available
to subsequent processes forked off for ExecStart=”.

If anyone familiar with systemd has other ideas, I’m all ears!

Otherwise I think we’ll have to have that ‘--backing-store’ option
(which would be useful in other contexts anyway).

Thanks,
Ludo’.

¹ https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:07:08 GMT) Full text and rfc822 format available.

Message #98 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 01/11] daemon: Use ‘close_range’ where available.
Date: Fri, 21 Feb 2025 14:05:49 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc39..4e949bc88a3 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b1..eb2d16e1cc3 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:07:11 GMT) Full text and rfc822 format available.

Message #101 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v3 00/11] Rootless guix-daemon
Date: Fri, 21 Feb 2025 14:05:48 +0100
Hello!

Here’s an updated version, addressing most issues brought up
by Reepca, also available from
<https://codeberg.org/civodul/guix/src/branch/wip-rootless-daemon>.
Main changes compared to v2:

  • Derivation inputs and / are mounted read-only; additional
    tests check the ability to write to these, to /tmp, to
    /dev/{full,null}, and to remount any of these as read-write.

  • Unit files for systemd tweaked so that (1) guix-daemon sees
    a private read-write mount of the store, and (2) gnu-store.mount
    actually remounts the store read-only after guix-daemon has
    started.

  • ‘DerivationGoal::deleteTmpDir’ bails out when it fails to
    chown ‘tmpDir’ (i.e., it does not try to “pivot” the /top
    sub-directory).

Did I forget anything, Reepca?

The one observable difference compared to current guix-daemon
operational mode is that, in the build environment, writing to
the root file system results in EROFS instead of EPERM, as you
pointed out earlier.  That’s not great but probably acceptable.
We’ll only know whether this is a problem in practice once we’ve
run the test suites of tens of thousands of packages.

I tested this patch series by:

  • running ‘make check’;

  • manually running ‘guix-install.sh’ in a Debian VM, as
    explained before.

Next up:

  • automating ‘guix-install.sh’ VM tests;

  • updating ‘guix-service-type’ to optionally support
    unprivileged guix-daemon.

I think these two bits can come later though.

Thoughts?

Ludo’.

Ludovic Courtès (11):
  daemon: Use ‘close_range’ where available.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Remount root directory as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  tests: Run in a chroot and unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  daemon: Move comments where they belong.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.

 build-aux/test-env.in       |  14 ++-
 config-daemon.ac            |   5 +-
 etc/gnu-store.mount.in      |   3 +-
 etc/guix-daemon.service.in  |  20 +++-
 etc/guix-install.sh         | 108 ++++++++++++++----
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 219 ++++++++++++++++++++++++++----------
 nix/libstore/local-store.cc |  30 +++--
 nix/libutil/util.cc         |  23 +++-
 tests/processes.scm         |   9 +-
 tests/store.scm             | 206 +++++++++++++++++++++++++++------
 11 files changed, 494 insertions(+), 147 deletions(-)


base-commit: 00787cd61611d74d3e54b160e94176905d36ef39
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:08:01 GMT) Full text and rfc822 format available.

Message #104 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 07/11] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Fri, 21 Feb 2025 14:05:55 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 4308264a4f3..63846695194 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:08:02 GMT) Full text and rfc822 format available.

Message #107 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v3 02/11] daemon: Bind-mount all the inputs,
 not just directories.
Date: Fri, 21 Feb 2025 14:05:50 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 27 ++-------------------------
 1 file changed, 2 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34d..f4cd2131c84 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1850,9 +1850,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1861,7 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
-            if (lstat(i->c_str(), &st))
-                throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
-
-                regularInputPaths.insert(*i);
-            }
+	    dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:08:05 GMT) Full text and rfc822 format available.

Message #110 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 04/11] daemon: Remount root directory as read-only.
Date: Fri, 21 Feb 2025 14:05:52 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Bind-mount the store
and /tmp under ‘chrootRootDir’ to themselves as read-write.
Remount / as read-only.

Change-Id: I79565094c8ec8448401897c720aad75304fd1948
---
 nix/libstore/build.cc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 6244c99e751..c87f4f767c5 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2078,6 +2078,18 @@ void DerivationGoal::runChild()
 
             for (auto & i : ss) dirsInChroot[i] = i;
 
+	    /* Make new mounts for the store and for /tmp.  That way, when
+	       'chrootRootDir' is made read-only below, these two mounts will
+	       remain writable (the store needs to be writable so derivation
+	       outputs can be written to it, and /tmp is writable by
+	       convention).  */
+	    auto chrootStoreDir = chrootRootDir + settings.nixStore;
+	    if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir);
+	    auto chrootTmpDir = chrootRootDir + "/tmp";
+	    if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir);
+
             /* Bind-mount all the directories from the "host"
                filesystem that we want in the chroot
                environment. */
@@ -2151,6 +2163,10 @@ void DerivationGoal::runChild()
 
             if (rmdir("real-root") == -1)
                 throw SysError("cannot remove real-root directory");
+
+	    /* 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);
         }
 #endif
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:08:08 GMT) Full text and rfc822 format available.

Message #113 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v3 03/11] daemon: Remount inputs as read-only.
Date: Fri, 21 Feb 2025 14:05:51 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index f4cd2131c84..6244c99e751 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2094,8 +2094,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:09:02 GMT) Full text and rfc822 format available.

Message #116 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 08/11] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Fri, 21 Feb 2025 14:05:56 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a3..35d9c8cd56b 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 107ffcfea06..213ed635933 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2059,6 +2062,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:09:04 GMT) Full text and rfc822 format available.

Message #119 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 06/11] tests: Run in a chroot and unprivileged user
 namespaces.
Date: Fri, 21 Feb 2025 14:05:54 +0100
* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write")
("build root cannot be made world-readable")
("/tmp, store, and /dev/{null,full} are writable"): New tests.
* tests/processes.scm ("client + lock"): Skip when
‘unprivileged-user-namespace-supported?’ returns true.

Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84
---
 build-aux/test-env.in |  14 ++-
 tests/processes.scm   |   9 +-
 tests/store.scm       | 206 +++++++++++++++++++++++++++++++++++-------
 3 files changed, 191 insertions(+), 38 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da581..5626152b346 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,20 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/processes.scm b/tests/processes.scm
index ba518f2d9e3..a72ba16f587 100644
--- a/tests/processes.scm
+++ b/tests/processes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2018 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2019 Mathieu Othacehe <m.othacehe <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -25,6 +25,8 @@ (define-module (test-processes)
   #:use-module (guix gexp)
   #:use-module ((guix utils) #:select (call-with-temporary-directory))
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix tests)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-64)
@@ -84,6 +86,11 @@ (define-syntax-rule (test-assert* description exp)
       (and (kill (process-id daemon) 0)
            (string-suffix? "guix-daemon" (first (process-command daemon)))))))
 
+(when (unprivileged-user-namespace-supported?)
+  ;; The test below assumes the build process can communicate with the outside
+  ;; world via the TOKEN1 and TOKEN2 files, which is impossible when
+  ;; guix-daemon is set up to build in separate namespaces.
+  (test-skip 1))
 (test-assert* "client + lock"
   (with-store store
     (call-with-temporary-directory
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f433..4ba0916e3fe 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,147 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-remount-input-read-write"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "build root cannot be made world-readable"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-make-root-world-readable"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((guile (string-append (assoc-ref %guile-build-info
+                                                         'bindir)
+                                              "/guile")))
+                    (catch 'system-error
+                      (lambda ()
+                        (chmod "/" #o777))
+                      (lambda args
+                        (format #t "failed to make root writable: ~a~%"
+                                (strerror (system-error-errno args)))
+                        (format #t "attempting read-write remount~%")
+                        (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
+                        (chmod "/" #o777)))
+                    (copy-file guile "/guile")
+                    (chmod "/guile" #o6755)
+                    ;; At this point, there's a world-readable setuid 'guile'
+                    ;; binary in the store that remains visible until this
+                    ;; build completes.
+                    (list #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "/tmp, store, and /dev/{null,full} are writable"
+  ;; All of /tmp and all of the store must be writable (the store is writable
+  ;; so that derivation outputs can be written to it, but in practice it's
+  ;; always been wide open).  Things like /dev/null must be writable too.
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-tmp-and-store-are-writable"
+                #~(begin
+                    (mkdir "/tmp/something")
+                    (mkdir (in-vicinity (getenv "NIX_STORE")
+                                        "some-other-thing"))
+                    (call-with-output-file "/dev/null"
+                      (lambda (port)
+                        (display "Welcome to the void." port)))
+                    (catch 'system-error
+                      (lambda ()
+                        (call-with-output-file "/dev/full"
+                          (lambda (port)
+                            (display "No space left!" port)))
+                        (error "Should have thrown!"))
+                      (lambda args
+                        (unless (= ENOSPC (system-error-errno args))
+                          (apply throw args))))
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1478,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:10:04 GMT) Full text and rfc822 format available.

Message #122 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 09/11] daemon: Move comments where they belong.
Date: Fri, 21 Feb 2025 14:05:57 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Shuffle
comments for clarity.

Change-Id: I6557c103ade4a3ab046354548ea193c68f8c9c05
---
 nix/libstore/build.cc | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 213ed635933..c8a0667c7b5 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1874,18 +1874,19 @@ void DerivationGoal::startBuilder()
         }
         dirsInChroot[tmpDirInSandbox] = tmpDir;
 
-        /* Make the closure of the inputs available in the chroot,
-           rather than the whole store.  This prevents any access
-           to undeclared dependencies.  !!! As an extra security
-           precaution, make the fake store only writable by the
-           build user. */
+	/* Create the fake store.  */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
-            throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
+	     /* As an extra security precaution, make the fake store only
+		writable by the build user.  */
+	     throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
+        /* Make the closure of the inputs available in the chroot, rather than
+           the whole store.  This prevents any access to undeclared
+           dependencies. */
         foreach (PathSet::iterator, i, inputPaths) {
 	    dirsInChroot[*i] = *i;
         }
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:10:05 GMT) Full text and rfc822 format available.

Message #125 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 10/11] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Fri, 21 Feb 2025 14:05:58 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(Before, User, AmbientCapabilities, PrivateMounts, BindPaths): New fields.
* etc/gnu-store.mount.in (Before): Remove.
(WantedBy): Change to ‘multi-user.target’.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/gnu-store.mount.in     |  3 +--
 etc/guix-daemon.service.in | 20 +++++++++++++++++++-
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/etc/gnu-store.mount.in b/etc/gnu-store.mount.in
index c94f2db72be..f9918c9e52e 100644
--- a/etc/gnu-store.mount.in
+++ b/etc/gnu-store.mount.in
@@ -2,10 +2,9 @@
 Description=Read-only @storedir@ for GNU Guix
 DefaultDependencies=no
 ConditionPathExists=@storedir@
-Before=guix-daemon.service
 
 [Install]
-WantedBy=guix-daemon.service
+WantedBy=multi-user.target
 
 [Mount]
 What=@storedir@
diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1b..a04cf1f2f0f 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -5,11 +5,29 @@
 [Unit]
 Description=Build daemon for GNU Guix
 
+# Start before 'gnu-store.mount' to get a writable view of the store.
+Before=gnu-store.mount
+
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
 Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Bind-mount the store read-write in a private namespace, to counter the
+# effect of 'gnu-store.mount'.
+PrivateMounts=true
+BindPaths=@storedir@
+
+# Provide the CAP_CHOWN capability so that guix-daemon cran create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:10:06 GMT) Full text and rfc822 format available.

Message #128 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v3 11/11] guix-install.sh: Support the unprivileged daemon
 where possible.
Date: Fri, 21 Feb 2025 14:05:59 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 108 ++++++++++++++++++++++++++++++++++----------
 1 file changed, 84 insertions(+), 24 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index 22d54c0c832..c6f0812b5cf 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -390,6 +390,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg "${INF}Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg "${INF}Linking the root user's profile"
@@ -415,38 +420,82 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg "${INF}user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg "${PAS}user added <$user>"
+    fi
+}
+
+can_install_unprivileged_daemon()
+{ # Return true if we can install guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
+	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg "${INF}group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg "${PAS}group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg "${INF}group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg "${INF}user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild${KVMGROUP}     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild${KVMGROUP}     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg "${PAS}user added <guixbuilder${i}>"
-        fi
-    done
+    if [ "$INIT_SYS" = systemd ] && \
+	   grep -q "User=guix-daemon" \
+		~root/.config/guix/current/lib/systemd/system/guix-daemon.service
+    then
+	if getent group guix-daemon > /dev/null; then
+	    _msg "${INF}group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg "${PAS}group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon	\
+	      /gnu /var/guix
+
+	# The unprivileged cannot create the log directory by itself.
+	mkdir /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg "${INF}group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg "${PAS}group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -461,6 +510,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg "${INF}remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -503,8 +560,7 @@ sys_enable_guix_daemon()
 
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
-              # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      install_unit gnu-store.mount
 
               systemctl daemon-reload &&
                   systemctl start  guix-daemon; } &&
@@ -628,6 +684,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg "${PAS}Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg "${INF}Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:10:10 GMT) Full text and rfc822 format available.

Message #131 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] Remounting the store read-write for guix-daemon
Date: Fri, 21 Feb 2025 14:08:50 +0100
Ludovic Courtès <ludovic.courtes <at> inria.fr> skribis:

> Second option:
>
>   BindPaths=/gnu/store
>
> … but that does essentially nothing, and we can’t specify that we want
> “remount,rw”.

Actually, adding “Before=gnu-store.mount” does the trick; I implemented
that in v3.

Ludo’.




Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 13:10:14 GMT) Full text and rfc822 format available.

Message #134 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>
Subject: [PATCH v3 05/11] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Fri, 21 Feb 2025 14:05:53 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

* 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.

Change-Id: I38fbe01f80fb45a99cd8a391e55a39a54d64fcb7
---
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 149 ++++++++++++++++++++++++++++--------
 nix/libstore/local-store.cc |  22 ++++--
 3 files changed, 135 insertions(+), 40 deletions(-)

diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index e31b3940203..2761a3dafb4 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013-2021, 2023-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2014 Nikita Karetnikov <nikita <at> karetnikov.org>
 ;;; Copyright © 2018 Kyle Meyer <kyle <at> kyleam.com>
 ;;; Copyright © 2020 Christopher Baines <mail <at> cbaines.net>
@@ -76,7 +76,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c87f4f767c5..107ffcfea06 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -747,6 +747,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();
@@ -1622,6 +1626,25 @@ 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)
+{
+    auto hostUID = getuid();
+    auto 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(
@@ -1685,7 +1708,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");
@@ -1802,7 +1825,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
@@ -1821,8 +1844,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"). */
@@ -1857,7 +1880,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) {
@@ -1948,14 +1971,34 @@ 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");
+	}
+
+	if ((flags & CLONE_NEWUSER) != 0) {
+	     /* Initialize the UID/GID mapping of the child process.  */
+	     initializeUserNamespace(pid);
+	     writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
+	}
     } else
 #endif
     {
@@ -2001,23 +2044,34 @@ void DerivationGoal::runChild()
 
         _writeToStderr = 0;
 
+	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);
+	     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";
@@ -2463,8 +2517,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;
         }
@@ -2723,17 +2785,42 @@ 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);
-
-		if (top != tmpDir) {
-		    // 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");
-		    rmdir(pivot.c_str());
+		uid_t uid = settings.clientUid;
+		gid_t gid = settings.clientGid != 0 ? settings.clientGid : -1;
+		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.  */
+			chown(tmpDir.c_str(), getuid(), getgid());
+		    }
+
+		    if (top != tmpDir) {
+			 /* Rename 'tmpDir' to its parent with an intermediate
+			    step.  Skip that if the '_chown' call above fails
+			    since in that case the setuid bits are not
+			    removed.  */
+			 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 (getuid() != 0)
+			      /* Running unprivileged but with CAP_CHOWN.  */
+			      chown(top.c_str(), uid, gid);
+
+			 rmdir(pivot.c_str());
+		    }
+		} 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));
 		}
             }
         }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbcee..4308264a4f3 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -306,14 +306,14 @@ void LocalStore::openDB(bool create)
 void LocalStore::makeStoreWritable()
 {
 #if HAVE_UNSHARE && HAVE_STATVFS && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_REMOUNT)
-    if (getuid() != 0) return;
     /* Check if /nix/store is on a read-only mount. */
     struct statvfs stat;
     if (statvfs(settings.nixStore.c_str(), &stat) != 0)
         throw SysError("getting info about the store mount point");
 
     if (stat.f_flag & ST_RDONLY) {
-        if (unshare(CLONE_NEWNS) == -1)
+	int flags = CLONE_NEWNS | (getpid() == 0 ? 0 : CLONE_NEWUSER);
+        if (unshare(flags) == -1)
             throw SysError("setting up a private mount namespace");
 
         if (mount(0, settings.nixStore.c_str(), "none", MS_REMOUNT | MS_BIND, 0) == -1)
@@ -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);
+	}
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 19:24:03 GMT) Full text and rfc822 format available.

Message #137 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Simon Tournier <zimon.toutoune <at> gmail.com>
To: Ludovic Courtès <ludo <at> gnu.org>, 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: Re: [bug#75810] [PATCH v3 00/11] Rootless guix-daemon
Date: Fri, 21 Feb 2025 18:16:11 +0100
Hi Ludo,

On Fri, 21 Feb 2025 at 14:05, Ludovic Courtès <ludo <at> gnu.org> wrote:

> The one observable difference compared to current guix-daemon
> operational mode is that, in the build environment, writing to
> the root file system results in EROFS instead of EPERM, as you
> pointed out earlier.  That’s not great but probably acceptable.
> We’ll only know whether this is a problem in practice once we’ve
> run the test suites of tens of thousands of packages.

Clearly, I do not fully understand all the deep details of all the
series.

Quoting Janneke [1]:

        I'm kind of afraid that having a writable /gnu/store, even if it's just
        on foreign distributions, is going to cause a whole lot of problems/bug
        reports with people changing files in the store.  When I came to guix I
        ran it on Debian for a couple of months and I certainly changed files in
        the store, even with the read-only mount hurdle, to "get stuff to
        build".  Only later to realise that by doing so I was making things much
        more difficult for myself.

        Hopefully I'm either misunderstanding this patch set, or else too
        pessimistict, and maybe other people aren't as stupid as I was when I
        first came to Guix?

I’m not sure to get what’s the answer now with the v3?  Especially when
connected to this other question:

                           Will there be an option for users to choose between
        a non-root guix-daemon or a read-only store?

Where the answer, IIUC, is no.

Could you clarify the status about the store when running guix-daemon as
root on foreign distros?  Or maybe now, will guix-daemon always run as a
regular user on foreign distros?

From an user perspective, instead of running guix-daemon as root, now
guix-daemon will run as the regular user named ’guix-daemon’ without any
special privileges, right?

User still need root privileges once at guix-install.sh time but not
more.  Therefore, for updating the guix-daemon, the user guix-daemon
needs to run “guix pull“ and restart the service, right?

If yes, cool!  It’ll be a booster for cluster sysadmins. :-)

Cheers,
simon

1: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Janneke Nieuwenhuizen <janneke <at> gnu.org>
Fri, 24 Jan 2025 20:20:42 +0100
id:87ikq49fxx.fsf <at> gnu.org
https://issues.guix.gnu.org/75810
https://issues.guix.gnu.org/msgid/87ikq49fxx.fsf <at> gnu.org
https://yhetil.org/guix/87ikq49fxx.fsf <at> gnu.org




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Feb 2025 22:41:02 GMT) Full text and rfc822 format available.

Message #140 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [PATCH v3 00/11] Rootless guix-daemon
Date: Fri, 21 Feb 2025 16:39:06 -0600
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

> Hello!
>
> Here’s an updated version, addressing most issues brought up
> by Reepca, also available from
> <https://codeberg.org/civodul/guix/src/branch/wip-rootless-daemon>.
> Main changes compared to v2:
>
>   • Derivation inputs and / are mounted read-only; additional
>     tests check the ability to write to these, to /tmp, to
>     /dev/{full,null}, and to remount any of these as read-write.
>
>   • Unit files for systemd tweaked so that (1) guix-daemon sees
>     a private read-write mount of the store, and (2) gnu-store.mount
>     actually remounts the store read-only after guix-daemon has
>     started.

I'm not familiar with how systemd does service dependencies, but does
this mean that the store becomes writable when the daemon is stopped?

>
>   • ‘DerivationGoal::deleteTmpDir’ bails out when it fails to
>     chown ‘tmpDir’ (i.e., it does not try to “pivot” the /top
>     sub-directory).
>
> Did I forget anything, Reepca?

I believe that if you try a "--keep-failed" build that fails in the
CAP_CHOWN case, you'll find that only root or the guix-daemon user can
delete the kept build directory, though the user that started the build
can delete everything inside it.  This is because in that case the build
directory was chown'ed back to guix-daemon so that it could be moved,
but wasn't chown'ed to the client user afterward.  If I recall correctly
there was code included to perform this extra chown in the (getuid() !=
0) case in the v2 series - was it accidentally forgotten?

Also, there are potential issues with how wide the scope of the try
block in DerivationGoal::deleteTmpDir is - _chown isn't the only place
within it that can raise a SysError, and there are failure modes present
that may merit more user attention than lvlInfo.  For example, if

rename((pivot + "/top").c_str(), top.c_str())

fails (which can be rather easily arranged by a local attacker), then
the build directory path reported in the "note: keeping build directory"
message remains up for grabs by anyone.  If the user doesn't go out of
their way to verify that the build directory isn't attacker-controlled,
they could be rather easily tricked into executing malicious code.  But
currently the exception from this rename failing will be turned into a
lvlInfo message, and I'm not sure how that interacts with the verbosity
defaults in the various CLI programs.

This does somewhat raise the question of why we're even doing the
pivoting in a way that creates a window during which failure can be
induced.  For example, we could move the inner build directory to the
pivot path, at which point the outer build directory should become
empty, so it should work to then rename the pivot path to the outer
build directory path, thereby atomically replacing it.

Also, in the unprivileged case (non-root, no CAP_CHOWN), the build
directory never gets pivoted out.  This is better for security than the
previous situation (which allowed setuid programs to be exposed), but it
should be quite doable to simply secure the file permissions first and
then carry on with the pivot.  I believe I previously mentioned perhaps
using secureFilePerms to do this?

It may work well to use the v2 patch for this with a call to
secureFilePerms added right before the try block and a have_cap_chown
boolean flag being saved for later recall after the pivot instead of the
(getuid() != 0) check.  That way in the fully-unprivileged case it
doesn't successfully pivot the now-sanitized build directory only to
immediately fail to chown it.  Actually, because that chown call doesn't
result in an exception on failure, it would also work to only add the
secureFilePerms call.


Also, I've discovered that while mount(2) uses EPERM for both a locked
mount point and insufficient privileges, umount(2) uses EINVAL for the
former and EPERM for the latter.  This may be a good way to test that
we're triggering the mount-locking behavior as intended.

> The one observable difference compared to current guix-daemon
> operational mode is that, in the build environment, writing to
> the root file system results in EROFS instead of EPERM, as you
> pointed out earlier.  That’s not great but probably acceptable.
> We’ll only know whether this is a problem in practice once we’ve
> run the test suites of tens of thousands of packages.

Strictly speaking, it's also observable that the root file system,
store, /tmp, etc is not owned by uid 0, and that the input store items
are all mounted read-only.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 22 Feb 2025 17:13:02 GMT) Full text and rfc822 format available.

Message #143 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: Re: [bug#75810] [PATCH v3 00/11] Rootless guix-daemon
Date: Sat, 22 Feb 2025 18:12:02 +0100
Ludovic Courtès <ludo <at> gnu.org> skribis:

> Next up:
>
>   • automating ‘guix-install.sh’ VM tests;

Done in <https://issues.guix.gnu.org/76488>.

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 22 Feb 2025 17:17:02 GMT) Full text and rfc822 format available.

Message #146 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Simon Tournier <zimon.toutoune <at> gmail.com>
Cc: Reepca Russelstein <reepca <at> russelstein.xyz>, 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v3 00/11] Rootless guix-daemon
Date: Sat, 22 Feb 2025 18:16:17 +0100
Hi,

Simon Tournier <zimon.toutoune <at> gmail.com> skribis:

> Quoting Janneke [1]:
>
>         I'm kind of afraid that having a writable /gnu/store, even if it's just
>         on foreign distributions,

This problem is fixed in v3: the store will be remounted readonly as is
currently the case.

> Could you clarify the status about the store when running guix-daemon as
> root on foreign distros?  Or maybe now, will guix-daemon always run as a
> regular user on foreign distros?

As currently written, guix-daemon will always run as non-root on foreign
distros (on systemd-based distros specifically.)

>>From an user perspective, instead of running guix-daemon as root, now
> guix-daemon will run as the regular user named ’guix-daemon’ without any
> special privileges, right?

Correct.

> User still need root privileges once at guix-install.sh time but not
> more.  Therefore, for updating the guix-daemon, the user guix-daemon
> needs to run “guix pull“ and restart the service, right?

The upgrade procedure remains unchanged: you would run ‘guix pull’ as
root and restart the service¹ (the service itself runs as user
‘guix-daemon’).

> If yes, cool!  It’ll be a booster for cluster sysadmins. :-)

Yup!

Ludo’.

¹ https://guix.gnu.org/manual/devel/en/html_node/Upgrading-Guix.html




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 09:44:02 GMT) Full text and rfc822 format available.

Message #149 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v3 00/11] Rootless guix-daemon
Date: Fri, 28 Feb 2025 10:43:24 +0100
Hi,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

>>   • Unit files for systemd tweaked so that (1) guix-daemon sees
>>     a private read-write mount of the store, and (2) gnu-store.mount
>>     actually remounts the store read-only after guix-daemon has
>>     started.
>
> I'm not familiar with how systemd does service dependencies, but does
> this mean that the store becomes writable when the daemon is stopped?

I had to check because it’s not crystal clear.

‘systemctl stop guix-daemon’ also stops ‘gnu-store.mount’.

But then you can do ‘systemctl start gnu-store.mount’, which does *not*
start guix-daemon; at that point, ‘systemctl start guix-daemon’ spawns
guix-daemon, but it cannot write to the store.

It’s messy, but I don’t know how to do better.

[...]

> It may work well to use the v2 patch for this with a call to
> secureFilePerms added right before the try block and a have_cap_chown
> boolean flag being saved for later recall after the pivot instead of the
> (getuid() != 0) check.  That way in the fully-unprivileged case it
> doesn't successfully pivot the now-sanitized build directory only to
> immediately fail to chown it.  Actually, because that chown call doesn't
> result in an exception on failure, it would also work to only add the
> secureFilePerms call.

I went back to v2 + ‘secureFilePerms’ call.

> Also, I've discovered that while mount(2) uses EPERM for both a locked
> mount point and insufficient privileges, umount(2) uses EINVAL for the
> former and EPERM for the latter.  This may be a good way to test that
> we're triggering the mount-locking behavior as intended.

The tests try to MS_REMOUNT the inputs, which is exactly what we want to
prevent; we could test the low-level semantics you describe, but it’s
quite obscure and maybe unnecessary given that we test MS_REMOUNT?

>> The one observable difference compared to current guix-daemon
>> operational mode is that, in the build environment, writing to
>> the root file system results in EROFS instead of EPERM, as you
>> pointed out earlier.  That’s not great but probably acceptable.
>> We’ll only know whether this is a problem in practice once we’ve
>> run the test suites of tens of thousands of packages.
>
> Strictly speaking, it's also observable that the root file system,
> store, /tmp, etc is not owned by uid 0, and that the input store items
> are all mounted read-only.

Right.

I’ll send v4 shortly.  Thanks again for your feedback!

Ludo’.




Added blocking bug(s) 76376 Request was from Ludovic Courtès <ludo <at> gnu.org> to control <at> debbugs.gnu.org. (Fri, 28 Feb 2025 11:04:01 GMT) Full text and rfc822 format available.

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:03 GMT) Full text and rfc822 format available.

Message #154 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 01/14] daemon: Use ‘close_range’ where available.
Date: Fri, 28 Feb 2025 15:29:20 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc39..4e949bc88a3 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b1..eb2d16e1cc3 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:03 GMT) Full text and rfc822 format available.

Message #157 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v4 00/14] Rootless guix-daemon
Date: Fri, 28 Feb 2025 15:29:19 +0100
Hello Guix!

Changes in v4, hopefully the last revision of this patch set:

  • For ‘deleteTmpDir’, go back to v2, but add ‘secureFilePerms’ call and
    define ‘reown’ variable to determine whether to re-chown after pivoting
    (suggested by Reepca).

  • For fixed outputs, bind-mount /etc/nsswitch.conf & co. only if they exist
    (necessary when running ‘guix build guix’, where these files are missing).

  • In ‘Derivationgoal::startBuilder’, when an input is a symlink, symlink it
    instead of bind-mounting it (bind mounts would reveal the symlink target,
    not the symlink itself.)  Add a test for that.

    Consequently, an input that is a symlink may be deleted by a build process.
    This is a harmless (only the copy of the symlink in the temporary store is
    deleted) but observable change.

  • Fix several tests that were missing explicit inputs (discovered by running
    ‘guix build guix’; this had gone unnoticed when I first ran ‘make check’
    because I was sharing ‘ac_cv_guix_test_root’ with my main Guix checkout,
    so these derivation results were already in store.)

  • Leave ‘makeStoreWritable’ unchanged compared to current ‘master’.

  • ‘guix-install.sh’ uses the ‘can_install_unprivileged_daemon’ function (it
    was defined but unused).

  • ‘./test-env’ warns when resorting to ‘--disable-chroot’.

  • Unprivileged daemon documented under “Build Environment Setup”.

I would like to push the two guix-daemon tests before this series:

  https://issues.guix.gnu.org/76488
  https://issues.guix.gnu.org/76636

Thoughts?  Are we done?

Ludo’.

Ludovic Courtès (14):
  daemon: Use ‘close_range’ where available.
  daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Remount root directory as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  daemon: Move comments where they belong.
  tests: Add missing derivation inputs.
  tests: Run in a chroot and unprivileged user namespaces.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.
  DRAFT gexp: No symlinks for ‘imported-files/derivation’.

 build-aux/test-env.in       |  16 ++-
 config-daemon.ac            |   5 +-
 doc/guix.texi               | 100 +++++++++++----
 etc/gnu-store.mount.in      |   3 +-
 etc/guix-daemon.service.in  |  20 ++-
 etc/guix-install.sh         | 106 +++++++++++----
 guix/gexp.scm               |   5 +-
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 226 ++++++++++++++++++++++++--------
 nix/libstore/local-store.cc |  26 ++--
 nix/libutil/util.cc         |  23 +++-
 tests/derivations.scm       |  24 ++--
 tests/packages.scm          |  13 +-
 tests/processes.scm         |   9 +-
 tests/store.scm             | 250 +++++++++++++++++++++++++++++++-----
 15 files changed, 650 insertions(+), 180 deletions(-)


base-commit: a76708a872e65230931f3c5c3b079d0a39d5cb84
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:04 GMT) Full text and rfc822 format available.

Message #160 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 02/14] daemon: Bind-mount /etc/nsswitch.conf & co. only if
 it exists.
Date: Fri, 28 Feb 2025 15:29:21 +0100
Those files may be missing in some contexts, for instance within the
build environment.

* nix/libstore/build.cc (DerivationGoal::runChild): Add /etc/resolv.conf
and related files to ‘ss’ only if they exist.

Change-Id: Ie19664a86c8101a1dc82cf39ad4b7abb10f8250a
---
 nix/libstore/build.cc | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34d..8ca5e5b732c 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2093,10 +2093,11 @@ void DerivationGoal::runChild()
                network, so give them access to /etc/resolv.conf and so
                on. */
             if (fixedOutput) {
-                ss.push_back("/etc/resolv.conf");
-                ss.push_back("/etc/nsswitch.conf");
-                ss.push_back("/etc/services");
-                ss.push_back("/etc/hosts");
+		auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf",
+			       "/etc/services", "/etc/hosts" };
+		for (auto & file: files) {
+		    if (pathExists(file)) ss.push_back(file);
+		}
             }
 
             for (auto & i : ss) dirsInChroot[i] = i;
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:05 GMT) Full text and rfc822 format available.

Message #163 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v4 03/14] daemon: Bind-mount all the inputs,
 not just directories.
Date: Fri, 28 Feb 2025 15:29:22 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.
Special-case symlinks.
(DerivationGoal)[regularInputPaths]: Remove.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 39 ++++++++++++++-------------------------
 1 file changed, 14 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 8ca5e5b732c..193b279b88a 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -659,9 +659,6 @@ private:
     /* RAII object to delete the chroot directory. */
     std::shared_ptr<AutoDelete> autoDelChroot;
 
-    /* All inputs that are regular files. */
-    PathSet regularInputPaths;
-
     /* Whether this is a fixed-output derivation. */
     bool fixedOutput;
 
@@ -1850,9 +1847,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1858,22 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
+	    struct stat st;
             if (lstat(i->c_str(), &st))
                 throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
 
-                regularInputPaths.insert(*i);
-            }
+	    if (S_ISLNK(st.st_mode)) {
+		/* Since bind-mounts follow symlinks, thus representing their
+		   target and not the symlink itself, special-case
+		   symlinks. XXX: When running unprivileged, TARGET can be
+		   deleted by the build process.  Use 'open_tree' & co. when
+		   it's more widely available.  */
+                Path target = chrootRootDir + *i;
+		if (symlink(readLink(*i).c_str(), target.c_str()) == -1)
+		    throw SysError(format("failed to create symlink '%1%' to '%2%'") % target % readLink(*i));
+	    }
+	    else
+		dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:06 GMT) Full text and rfc822 format available.

Message #166 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 07/14] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Fri, 28 Feb 2025 15:29:26 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 83e6c3e16ec..f6540c2117d 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:07 GMT) Full text and rfc822 format available.

Message #169 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 05/14] daemon: Remount root directory as read-only.
Date: Fri, 28 Feb 2025 15:29:24 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Bind-mount the store
and /tmp under ‘chrootRootDir’ to themselves as read-write.
Remount / as read-only.

Change-Id: I79565094c8ec8448401897c720aad75304fd1948
---
 nix/libstore/build.cc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 3861a1ffd90..c8b778362ac 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2091,6 +2091,18 @@ void DerivationGoal::runChild()
 
             for (auto & i : ss) dirsInChroot[i] = i;
 
+	    /* Make new mounts for the store and for /tmp.  That way, when
+	       'chrootRootDir' is made read-only below, these two mounts will
+	       remain writable (the store needs to be writable so derivation
+	       outputs can be written to it, and /tmp is writable by
+	       convention).  */
+	    auto chrootStoreDir = chrootRootDir + settings.nixStore;
+	    if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir);
+	    auto chrootTmpDir = chrootRootDir + "/tmp";
+	    if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir);
+
             /* Bind-mount all the directories from the "host"
                filesystem that we want in the chroot
                environment. */
@@ -2164,6 +2176,10 @@ void DerivationGoal::runChild()
 
             if (rmdir("real-root") == -1)
                 throw SysError("cannot remove real-root directory");
+
+	    /* 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);
         }
 #endif
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:08 GMT) Full text and rfc822 format available.

Message #172 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 09/14] daemon: Move comments where they belong.
Date: Fri, 28 Feb 2025 15:29:28 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Shuffle
comments for clarity.

Change-Id: I6557c103ade4a3ab046354548ea193c68f8c9c05
---
 nix/libstore/build.cc | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 2145955c4bd..47f73ac8d23 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1871,18 +1871,19 @@ void DerivationGoal::startBuilder()
         }
         dirsInChroot[tmpDirInSandbox] = tmpDir;
 
-        /* Make the closure of the inputs available in the chroot,
-           rather than the whole store.  This prevents any access
-           to undeclared dependencies.  !!! As an extra security
-           precaution, make the fake store only writable by the
-           build user. */
+	/* Create the fake store.  */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
-            throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
+	     /* As an extra security precaution, make the fake store only
+		writable by the build user.  */
+	     throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
+        /* Make the closure of the inputs available in the chroot, rather than
+           the whole store.  This prevents any access to undeclared
+           dependencies. */
         foreach (PathSet::iterator, i, inputPaths) {
 	    struct stat st;
             if (lstat(i->c_str(), &st))
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:09 GMT) Full text and rfc822 format available.

Message #175 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 08/14] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Fri, 28 Feb 2025 15:29:27 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a3..35d9c8cd56b 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 961894454f3..2145955c4bd 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2071,6 +2074,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:10 GMT) Full text and rfc822 format available.

Message #178 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 10/14] tests: Add missing derivation inputs.
Date: Fri, 28 Feb 2025 15:29:29 +0100
These missing inputs go unnoticed when running ‘guix-daemon
--disable-chroot’ but are immediately visible otherwise.

* tests/derivations.scm ("fixed-output derivation"): Add %BASH to #:sources.
("fixed-output derivation: output paths are equal"):
("fixed-output derivation, recursive"):
("derivation with a fixed-output input"):
("derivation with duplicate fixed-output inputs"):
("derivation with equivalent fixed-output inputs"):
("build derivation with coreutils"): Likewise.
* tests/packages.scm (bootstrap-binary): New procedure.
("package-source-derivation, origin, sha512"): Use it instead of
‘search-bootstrap-binary’ and add BASH to #:sources.
("package-source-derivation, origin, sha3-512"): Likewise.

Change-Id: I4c9087df23c47729a3aff15e9e1435b7266e36e2
---
 tests/derivations.scm | 24 +++++++++++++++---------
 tests/packages.scm    | 13 +++++++++----
 2 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/tests/derivations.scm b/tests/derivations.scm
index 72ea9aa9ccb..f30f05474e3 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -443,7 +443,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                      (string-append
                                       "fixed-" (symbol->string hash-algorithm))
                                      %bash `(,builder)
-                                     #:sources `(,builder) ;optional
+                                     #:sources (list %bash builder)
                                      #:hash hash
                                      #:hash-algo hash-algorithm)))
            (build-derivations %store (list drv))
@@ -462,9 +462,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv1       (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (drv2       (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (succeeded? (build-derivations %store (list drv1 drv2))))
     (and succeeded?
@@ -477,7 +479,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv        (derivation %store "fixed-rec"
                                  %bash `(,builder)
-                                 #:sources (list builder)
+                                 #:sources (list %bash builder)
                                  #:hash (base32 "0sg9f58l1jj88w6pdrfdpj5x9b1zrwszk84j81zvby36q9whhhqa")
                                  #:hash-algo 'sha256
                                  #:recursive? #t))
@@ -511,9 +513,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed-out  (derivation->output-path fixed1))
          (builder3   (add-text-to-store
@@ -548,9 +552,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (builder3   (add-text-to-store %store "builder.sh"
                                         "echo fake builder"))
@@ -580,21 +586,21 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                       '()))
          (hash     (gcrypt:sha256 (string->utf8 "hello")))
          (drv1     (derivation %store "fixed" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:hash hash #:hash-algo 'sha256))
          (drv2     (derivation %store "fixed" %bash (list builder2)
-                               #:sources (list builder2)
+                               #:sources (list %bash builder2)
                                #:hash hash #:hash-algo 'sha256))
          (drv3a    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv1))))
          (drv3b    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv2))))
          (drv4     (derivation %store "fixed-user-user" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:inputs (list (derivation-input drv3a '("one"))
                                               (derivation-input drv3b '("two"))))))
     (match (derivation-inputs drv4)
@@ -878,7 +884,7 @@ (define %coreutils
                                     ,(string-append
                                       (derivation->output-path %coreutils)
                                       "/bin")))
-                      #:sources (list builder)
+                      #:sources (list %bash builder)
                       #:inputs (list (derivation-input %coreutils))))
          (succeeded?
           (build-derivations %store (list drv))))
diff --git a/tests/packages.scm b/tests/packages.scm
index 2863fb5991e..701bcd4a333 100644
--- a/tests/packages.scm
+++ b/tests/packages.scm
@@ -79,6 +79,11 @@ (define %store
 ;; When grafting, do not add dependency on 'glibc-utf8-locales'.
 (%graft-with-utf8-locale? #f)
 
+(define (bootstrap-binary name)
+  (let ((bin (search-bootstrap-binary name (%current-system))))
+    (and %store
+         (add-to-store %store name #t "sha256" bin))))
+
 
 (test-begin "packages")
 
@@ -608,14 +613,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha512"
   "hello"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
@@ -634,14 +639,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha3-512"
   "hello, sha3"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello, sha3 > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha3-512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:11 GMT) Full text and rfc822 format available.

Message #181 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 14/14] DRAFT gexp: No symlinks for ‘imported-files/derivation’.
Date: Fri, 28 Feb 2025 15:29:33 +0100
DRAFT: Wait for <https://issues.guix.gnu.org/76376>.

* guix/gexp.scm (imported-files/derivation): Pass #:recursive? #f to
‘interned-file’ and call ‘readlink*’ on ‘file-name’.

Change-Id: Idc5b59cd8f0c1217e84c7cbfba64d97d5999429f
---
 guix/gexp.scm | 5 +++--
 1 file changed, 3 insertions(+), 2 deletions(-)

diff --git a/guix/gexp.scm b/guix/gexp.scm
index ad51bc55b78..ddd2e1a0812 100644
--- a/guix/gexp.scm
+++ b/guix/gexp.scm
@@ -1584,8 +1584,9 @@ (define* (imported-files/derivation files
   (define file-pair
     (match-lambda
      ((final-path . (? string? file-name))
-      (mlet %store-monad ((file (interned-file file-name
-                                               (basename final-path))))
+      (mlet %store-monad ((file (interned-file (readlink* file-name)
+                                               (basename final-path)
+                                               #:recursive? #f)))
         (return (list final-path file))))
      ((final-path . file-like)
       (mlet %store-monad ((file (lower-object file-like system)))
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:13 GMT) Full text and rfc822 format available.

Message #184 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 12/14] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Fri, 28 Feb 2025 15:29:31 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(Before, User, AmbientCapabilities, PrivateMounts, BindPaths): New fields.
* etc/gnu-store.mount.in (Before): Remove.
(WantedBy): Change to ‘multi-user.target’.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/gnu-store.mount.in     |  3 +--
 etc/guix-daemon.service.in | 20 +++++++++++++++++++-
 2 files changed, 20 insertions(+), 3 deletions(-)

diff --git a/etc/gnu-store.mount.in b/etc/gnu-store.mount.in
index c94f2db72be..f9918c9e52e 100644
--- a/etc/gnu-store.mount.in
+++ b/etc/gnu-store.mount.in
@@ -2,10 +2,9 @@
 Description=Read-only @storedir@ for GNU Guix
 DefaultDependencies=no
 ConditionPathExists=@storedir@
-Before=guix-daemon.service
 
 [Install]
-WantedBy=guix-daemon.service
+WantedBy=multi-user.target
 
 [Mount]
 What=@storedir@
diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1b..a04cf1f2f0f 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -5,11 +5,29 @@
 [Unit]
 Description=Build daemon for GNU Guix
 
+# Start before 'gnu-store.mount' to get a writable view of the store.
+Before=gnu-store.mount
+
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
 Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Bind-mount the store read-write in a private namespace, to counter the
+# effect of 'gnu-store.mount'.
+PrivateMounts=true
+BindPaths=@storedir@
+
+# Provide the CAP_CHOWN capability so that guix-daemon cran create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:14 GMT) Full text and rfc822 format available.

Message #187 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v4 04/14] daemon: Remount inputs as read-only.
Date: Fri, 28 Feb 2025 15:29:23 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 193b279b88a..3861a1ffd90 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2107,8 +2107,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:14 GMT) Full text and rfc822 format available.

Message #190 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 11/14] tests: Run in a chroot and unprivileged user
 namespaces.
Date: Fri, 28 Feb 2025 15:29:30 +0100
* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking and warn in that case.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("symlink is symlink")
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write")
("build root cannot be made world-readable")
("/tmp, store, and /dev/{null,full} are writable")
("network is unreachable"): New tests.
* tests/processes.scm ("client + lock"): Skip when
‘unprivileged-user-namespace-supported?’ returns true.

Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84
---
 build-aux/test-env.in |  16 ++-
 tests/processes.scm   |   9 +-
 tests/store.scm       | 250 ++++++++++++++++++++++++++++++++++++------
 3 files changed, 237 insertions(+), 38 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da581..a3f225582df 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,22 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+	echo "unprivileged user namespaces not supported; \
+running 'guix-daemon $extra_options'" >&2
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/processes.scm b/tests/processes.scm
index ba518f2d9e3..a72ba16f587 100644
--- a/tests/processes.scm
+++ b/tests/processes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2018 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2019 Mathieu Othacehe <m.othacehe <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -25,6 +25,8 @@ (define-module (test-processes)
   #:use-module (guix gexp)
   #:use-module ((guix utils) #:select (call-with-temporary-directory))
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix tests)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-64)
@@ -84,6 +86,11 @@ (define-syntax-rule (test-assert* description exp)
       (and (kill (process-id daemon) 0)
            (string-suffix? "guix-daemon" (first (process-command daemon)))))))
 
+(when (unprivileged-user-namespace-supported?)
+  ;; The test below assumes the build process can communicate with the outside
+  ;; world via the TOKEN1 and TOKEN2 files, which is impossible when
+  ;; guix-daemon is set up to build in separate namespaces.
+  (test-skip 1))
 (test-assert* "client + lock"
   (with-store store
     (call-with-temporary-directory
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f433..c22739afe6b 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,191 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(test-assert "symlink is symlink"
+  (let* ((a (add-text-to-store %store "hello.txt" (random-text)))
+         (b (build-expression->derivation
+             %store "symlink"
+             '(symlink (assoc-ref %build-inputs "a") %output)
+             #:inputs `(("a" ,a))))
+         (c (build-expression->derivation
+             %store "symlink-reference"
+             `(call-with-output-file %output
+                (lambda (port)
+                  ;; Check that B is indeed visible as a symlink.  This should
+                  ;; always be the case, both in the '--disable-chroot' and in
+                  ;; the user namespace setups.
+                  (pk 'stat (lstat (assoc-ref %build-inputs "b")))
+                  (display (readlink (assoc-ref %build-inputs "b"))
+                           port)))
+             #:inputs `(("b" ,b)))))
+    (and (build-derivations %store (list c))
+         (string=? (call-with-input-file (derivation->output-path c)
+                     get-string-all)
+                   a))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-remount-input-read-write"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "build root cannot be made world-readable"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-make-root-world-readable"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((guile (string-append (assoc-ref %guile-build-info
+                                                         'bindir)
+                                              "/guile")))
+                    (catch 'system-error
+                      (lambda ()
+                        (chmod "/" #o777))
+                      (lambda args
+                        (format #t "failed to make root writable: ~a~%"
+                                (strerror (system-error-errno args)))
+                        (format #t "attempting read-write remount~%")
+                        (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
+                        (chmod "/" #o777)))
+                    (copy-file guile "/guile")
+                    (chmod "/guile" #o6755)
+                    ;; At this point, there's a world-readable setuid 'guile'
+                    ;; binary in the store that remains visible until this
+                    ;; build completes.
+                    (list #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "/tmp, store, and /dev/{null,full} are writable"
+  ;; All of /tmp and all of the store must be writable (the store is writable
+  ;; so that derivation outputs can be written to it, but in practice it's
+  ;; always been wide open).  Things like /dev/null must be writable too.
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-tmp-and-store-are-writable"
+                #~(begin
+                    (mkdir "/tmp/something")
+                    (mkdir (in-vicinity (getenv "NIX_STORE")
+                                        "some-other-thing"))
+                    (call-with-output-file "/dev/null"
+                      (lambda (port)
+                        (display "Welcome to the void." port)))
+                    (catch 'system-error
+                      (lambda ()
+                        (call-with-output-file "/dev/full"
+                          (lambda (port)
+                            (display "No space left!" port)))
+                        (error "Should have thrown!"))
+                      (lambda args
+                        (unless (= ENOSPC (system-error-errno args))
+                          (apply throw args))))
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "network is unreachable"
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-network-unreachable"
+                #~(let ((check-connection-failure
+                         (lambda (address expected-code)
+                           (let ((s (socket AF_INET SOCK_STREAM 0)))
+                             (catch 'system-error
+                               (lambda ()
+                                 (connect s AF_INET (inet-pton AF_INET address) 80))
+                               (lambda args
+                                 (let ((errno (system-error-errno args)))
+                                   (unless (= expected-code errno)
+                                     (error "wrong error code"
+                                            errno (strerror errno))))))))))
+                    (check-connection-failure "127.0.0.1" ECONNREFUSED)
+                    (check-connection-failure "9.9.9.9" ENETUNREACH)
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1522,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:15 GMT) Full text and rfc822 format available.

Message #193 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v4 13/14] guix-install.sh: Support the unprivileged daemon
 where possible.
Date: Fri, 28 Feb 2025 15:29:32 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 106 ++++++++++++++++++++++++++++++++++----------
 1 file changed, 82 insertions(+), 24 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index 8887204df41..b0b0ee84ba5 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -414,6 +414,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg_info "Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg_info "Linking the root user's profile"
@@ -441,38 +446,80 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg_info "user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg_pass "user added <$user>"
+    fi
+}
+
+can_install_unprivileged_daemon()
+{ # Return true if we can install guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
+	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg_info "group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg_pass "group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg_info "group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg_info "user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg_pass "user added <guixbuilder${i}>"
-        fi
-    done
+    if can_install_unprivileged_daemon
+    then
+	if getent group guix-daemon > /dev/null; then
+	    _msg_info "group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg_pass "group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon	\
+	      /gnu /var/guix
+
+	# The unprivileged cannot create the log directory by itself.
+	mkdir /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg_info "group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg_pass "group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -487,6 +534,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg_info "remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -529,8 +584,7 @@ sys_enable_guix_daemon()
 
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
-              # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      install_unit gnu-store.mount
 
               systemctl daemon-reload &&
                   systemctl start  guix-daemon; } &&
@@ -654,6 +708,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg_pass "Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg_info "Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, maxim.cournoyer <at> gmail.com, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 14:31:17 GMT) Full text and rfc822 format available.

Message #196 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>
Subject: [PATCH v4 06/14] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Fri, 28 Feb 2025 15:29:25 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

* 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.
---
 doc/guix.texi               | 100 +++++++++++++++++++-------
 guix/substitutes.scm        |   4 +-
 nix/libstore/build.cc       | 135 ++++++++++++++++++++++++++++++------
 nix/libstore/local-store.cc |  18 +++--
 4 files changed, 203 insertions(+), 54 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 93380dc30d4..a2b65299e9f 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -877,6 +877,7 @@ Setting Up the Daemon
 @section Setting Up the Daemon
 
 @cindex daemon
+@cindex build daemon
 During installation, the @dfn{build daemon} that must be running
 to use Guix has already been set up and you can run @command{guix}
 commands in your terminal program, @pxref{Getting Started}:
@@ -921,20 +922,36 @@ Build Environment Setup
 @cindex build environment
 In a standard multi-user setup, Guix and its daemon---the
 @command{guix-daemon} program---are installed by the system
-administrator; @file{/gnu/store} is owned by @code{root} and
-@command{guix-daemon} runs as @code{root}.  Unprivileged users may use
-Guix tools to build packages or otherwise access the store, and the
-daemon will do it on their behalf, ensuring that the store is kept in a
-consistent state, and allowing built packages to be shared among users.
+administrator.  Unprivileged users may use Guix tools to build packages
+or otherwise access the store, and the daemon will do it on their
+behalf, ensuring that the store is kept in a consistent state, and
+allowing built packages to be shared among users.
+
+There are currently two ways to set up and run the build daemon:
+
+@enumerate
+@item
+running @command{guix-daemon} as ``root'', letting it run build
+processes as unprivileged users taken from a pool of build users---this
+is the historical approach;
+
+@item
+running @command{guix-daemon} as a separate unprivileged user, relying
+on Linux's @dfn{unprivileged user namespace} functionality to set up
+isolated environments---this option only appeared recently.
+@end enumerate
+
+The sections below describe each of these two configurations in more
+detail and summarize the kind of build isolation they provide.
+
+@unnumberedsubsubsec Daemon Running as Root
 
 @cindex build users
 When @command{guix-daemon} runs as @code{root}, you may not want package
 build processes themselves to run as @code{root} too, for obvious
 security reasons.  To avoid that, a special pool of @dfn{build users}
 should be created for use by build processes started by the daemon.
-These build users need not have a shell and a home directory: they will
-just be used when the daemon drops @code{root} privileges in build
-processes.  Having several such users allows the daemon to launch
+Having several such users allows the daemon to launch
 distinct build processes under separate UIDs, which guarantees that they
 do not interfere with each other---an essential feature since builds are
 regarded as pure functions (@pxref{Introduction}).
@@ -977,11 +994,45 @@ Build Environment Setup
 # guix-daemon --build-users-group=guixbuild
 @end example
 
+In this setup, @file{/gnu/store} is owned by @code{root}.
+
+@unnumberedsubsubsec Daemon Running Without Privileges
+
+@cindex rootless build daemon
+@cindex unprivileged build daemon
+@cindex build daemon, unprivileged
+The second option, which is new, is to run @command{guix-daemon}
+@emph{as an unprivileged user}.  It has the advantage of reducing the
+harm that can be done should a build process manage to exploit a
+vulnerability in the daemon.  This option requires the user of Linux's
+unprivileged user namespace mechanism; today it is available and enabled
+by most GNU/Linux distributions but can still be disabled.  The
+installation script automatically determines whether this option is
+available on your system (@pxref{Binary Installation}).
+
+When using this option, you only need to create one user account, and
+@command{guix-daemon} will run with the authority of that account:
+
+@example
+# groupadd --system guix-daemon
+# useradd -g guix-daemon -G guix-daemon              \
+          -d /var/empty -s $(which nologin)          \
+          -c "Guix daemon privilege separation user" \
+          --system guix-daemon
+@end example
+
+In this configuration, @file{/gnu/store} is owned by the
+@code{guix-daemon} user.
+
+@unnumberedsubsubsec The Isolated Build Environment
+
 @cindex chroot
-@noindent
-This way, the daemon starts build processes in a chroot, under one of
-the @code{guixbuilder} users.  On GNU/Linux, by default, the chroot
-environment contains nothing but:
+@cindex build environment isolation
+@cindex isolated build environment
+@cindex hermetic build environment
+In both cases, the daemon starts build processes without privileges in
+an @emph{isolated} or @emph{hermetic} build environment---a ``chroot''.
+On GNU/Linux, by default, the build environment contains nothing but:
 
 @c Keep this list in sync with libstore/build.cc! -----------------------
 @itemize
@@ -1015,7 +1066,7 @@ Build Environment Setup
 @file{/homeless-shelter}.  This helps to highlight inappropriate uses of
 @env{HOME} in the build scripts of packages.
 
-All this usually enough to ensure details of the environment do not
+All this is usually enough to ensure details of the environment do not
 influence build processes.  In some exceptional cases where more control
 is needed---typically over the date, kernel, or CPU---you can resort to
 a virtual build machine (@pxref{build-vm, virtual build machines}).
@@ -1035,14 +1086,6 @@ Build Environment Setup
 for fixed-output derivations (@pxref{Derivations}) or for substitutes
 (@pxref{Substitutes}).
 
-If you are installing Guix as an unprivileged user, it is still possible
-to run @command{guix-daemon} provided you pass @option{--disable-chroot}.
-However, build processes will not be isolated from one another, and not
-from the rest of the system.  Thus, build processes may interfere with
-each other, and may access programs, libraries, and other files
-available on the system---making it much harder to view them as
-@emph{pure} functions.
-
 
 @node Daemon Offload Setup
 @subsection Using the Offload Facility
@@ -1567,10 +1610,17 @@ Invoking guix-daemon
 @item --disable-chroot
 Disable chroot builds.
 
-Using this option is not recommended since, again, it would allow build
-processes to gain access to undeclared dependencies.  It is necessary,
-though, when @command{guix-daemon} is running under an unprivileged user
-account.
+@quotation Warning
+Using this option is not recommended since it allows build processes to
+gain access to undeclared dependencies, to interfere with one another,
+and more generally to do anything that can be done with the authority of
+the daemon---which includes at least the ability to tamper with any file
+in the store!
+
+You may find it necessary, though, when support for Linux unprivileged
+user namespaces is missing (@pxref{Build Environment Setup}).  Use at
+your own risk!
+@end quotation
 
 @item --log-compression=@var{type}
 Compress build logs according to @var{type}, one of @code{gzip},
diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index e31b3940203..2761a3dafb4 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2013-2021, 2023-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2014 Nikita Karetnikov <nikita <at> karetnikov.org>
 ;;; Copyright © 2018 Kyle Meyer <kyle <at> kyleam.com>
 ;;; Copyright © 2020 Christopher Baines <mail <at> cbaines.net>
@@ -76,7 +76,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c8b778362ac..961894454f3 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -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,25 @@ 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)
+{
+    auto hostUID = getuid();
+    auto 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 +1705,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 +1822,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 +1841,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 +1877,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 +1983,34 @@ 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");
+	}
+
+	if ((flags & CLONE_NEWUSER) != 0) {
+	     /* Initialize the UID/GID mapping of the child process.  */
+	     initializeUserNamespace(pid);
+	     writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
+	}
     } else
 #endif
     {
@@ -2013,23 +2056,34 @@ void DerivationGoal::runChild()
 
         _writeToStderr = 0;
 
+	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);
+	     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";
@@ -2476,8 +2530,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,8 +2798,32 @@ 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.  */
+			chown(tmpDir.c_str(), getuid(), getgid());
+			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) {
 		    // Rename tmpDir to its parent, with an intermediate step.
@@ -2746,6 +2832,11 @@ void DerivationGoal::deleteTmpDir(bool force)
 			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());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbcee..83e6c3e16ec 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -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);
+	}
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 28 Feb 2025 17:58:02 GMT) Full text and rfc822 format available.

Message #199 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Simon Tournier <zimon.toutoune <at> gmail.com>
To: Ludovic Courtès <ludo <at> gnu.org>, 75810 <at> debbugs.gnu.org
Cc: Josselin Poiret <dev <at> jpoiret.xyz>,
 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>,
 Mathieu Othacehe <othacehe <at> gnu.org>,
 Ludovic Courtès <ludo <at> gnu.org>,
 Tobias Geerinckx-Rice <me <at> tobias.gr>,
 Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 Christopher Baines <guix <at> cbaines.net>
Subject: Re: [bug#75810] [PATCH v4 06/14] daemon: Allow running as non-root
 with unprivileged user namespaces.
Date: Fri, 28 Feb 2025 17:49:53 +0100
Hi,

On Fri, 28 Feb 2025 at 15:29, Ludovic Courtès <ludo <at> gnu.org> wrote:

> * 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.
> ---
>  doc/guix.texi               | 100 +++++++++++++++++++-------

[...]

> diff --git a/doc/guix.texi b/doc/guix.texi
> index 93380dc30d4..a2b65299e9f 100644
> --- a/doc/guix.texi
> +++ b/doc/guix.texi
> @@ -877,6 +877,7 @@ Setting Up the Daemon
>  @section Setting Up the Daemon

[...]

> +There are currently two ways to set up and run the build daemon:
> +
> +@enumerate
> +@item
> +running @command{guix-daemon} as ``root'', letting it run build
> +processes as unprivileged users taken from a pool of build users---this
> +is the historical approach;
> +
> +@item
> +running @command{guix-daemon} as a separate unprivileged user, relying
> +on Linux's @dfn{unprivileged user namespace} functionality to set up
> +isolated environments---this option only appeared recently.
> +@end enumerate
> +
> +The sections below describe each of these two configurations in more
> +detail and summarize the kind of build isolation they provide.

The paragraph above could give the impression that there is a choice
between two options – well it was my understand when reading.  On
foreign distro, there is no option, IIUC.

Therefore, I would clarify, something like:

        Depending on your situation, the build daemon can set up and run in
        different ways:

        @enumerate
        @item        
        running @command{guix-daemon} as ``root'', letting it run build
        processes as unprivileged users taken from a pool of build
        users---this is the historical approach;

        @item       
        running @command{guix-daemon} as a separate unprivileged user,
        relying on Linux's @dfn{unprivileged user namespace}
        functionality to set up isolated environments---this option is
        recently become mandatory on foreign distribution.        
        @end enumerate

        The sections below describe each of these two configurations in more
        detail and summarize the kind of build isolation they provide.

Somehow, I would explicitly mention here what are my options when using
Guix System and what is my option when using foreign distro.

> +@unnumberedsubsubsec Daemon Running Without Privileges
> +
> +@cindex rootless build daemon
> +@cindex unprivileged build daemon
> +@cindex build daemon, unprivileged
> +The second option, which is new, is to run @command{guix-daemon}

I would remove “which is new”.

> +@emph{as an unprivileged user}.  It has the advantage of reducing the
> +harm that can be done should a build process manage to exploit a
> +vulnerability in the daemon.  This option requires the user of Linux's
> +unprivileged user namespace mechanism; today it is available and enabled
> +by most GNU/Linux distributions but can still be disabled.

>                                                              The
> +installation script automatically determines whether this option is
> +available on your system (@pxref{Binary Installation}).

I would write: When using the installation script, it automatically
determines whether …

> -If you are installing Guix as an unprivileged user, it is still possible
> -to run @command{guix-daemon} provided you pass @option{--disable-chroot}.
> -However, build processes will not be isolated from one another, and not
> -from the rest of the system.  Thus, build processes may interfere with
> -each other, and may access programs, libraries, and other files
> -available on the system---making it much harder to view them as
> -@emph{pure} functions.
> -

Yeah, good removal! :-)


Cheers,
simon




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 01 Mar 2025 13:54:02 GMT) Full text and rfc822 format available.

Message #202 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [PATCH v4 00/14] Rootless guix-daemon
Date: Sat, 01 Mar 2025 07:52:40 -0600
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

> Hello Guix!
>
> Changes in v4, hopefully the last revision of this patch set:
>
>   • For ‘deleteTmpDir’, go back to v2, but add ‘secureFilePerms’ call and
>     define ‘reown’ variable to determine whether to re-chown after pivoting
>     (suggested by Reepca).

After re-reading the v4 patch for this I've noticed one minor nitpick:
since it's technically possible (though unlikely) to both have CAP_CHOWN
and have (top == tmpdir), for example if --disable-chroot is given, it
is possible that it will unnecessarily chown tmpDir and then never
re-chown it back.

The diff in question, for clarity:

> @@ -2736,8 +2798,32 @@ 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.  */
> +			chown(tmpDir.c_str(), getuid(), getgid());
> +			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) {
>  		    // Rename tmpDir to its parent, with an intermediate step.
> @@ -2746,6 +2832,11 @@ void DerivationGoal::deleteTmpDir(bool force)
>  			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());
>  		}
>              }

This can be remedied by moving

chown(tmpDir.c_str(), getuid(), getgid());

to inside the

if (top != tmpDir)

block, and adding a test for 'reown', like so:

if (top != tmpDir) {
    if (reown) chown(tmpDir.c_str(), getuid(), getgid());
    // Rename tmpDir to its parent, with an intermediate step.
    ...
}

The extra symmetry should also make this section a bit clearer overall.


> The tests try to MS_REMOUNT the inputs, which is exactly what we want to
> prevent; we could test the low-level semantics you describe, but it’s
> quite obscure and maybe unnecessary given that we test MS_REMOUNT?

My concern is that it may be possible, now or in the future, for the
builder to gain the necessary capability within its user
namespace... somehow.  This concern comes from reading the
capabilities(7) manual page, where it says:

   Per-user-namespace "set-user-ID-root" programs
       A  set-user-ID  program  whose  UID matches the UID that created a user
       namespace will confer capabilities in the process's permitted  and  ef‐
       fective  sets when executed by any process inside that namespace or any
       descendant user namespace.

       The rules about the transformation of the process's capabilities during
       the  execve(2)  are exactly as described in Transformation of capabili‐
       ties during execve() and Capabilities and execution of programs by root
       above,  with  the  difference that, in the latter subsection, "root" is
       the UID of the creator of the user namespace.

Even with no effective capabilities whatsoever, nothing is stopping root
from making a setuid program and executing it, and I don't see what
would stop the builder from doing likewise.  If it works as described
("whose UID matches the UID that created a user namespace"), this should
cause the builder to gain full capabilities within its user namespace.

Now, experimentally, this doesn't /seem/ to work as described, but if
it's in the manual, it may be unwise to bet against it ever happening.
Additionally, even if it never is implemented as described, this text's
presence makes it less clear how security within a user namespace (not
just between user namespaces) is intended to work.  That's why I would
like for the security against remounting to not depend on the
capabilities that the builder has in its user namespace.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 02 Mar 2025 06:10:01 GMT) Full text and rfc822 format available.

Message #205 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: Josselin Poiret <dev <at> jpoiret.xyz>,
 Simon Tournier <zimon.toutoune <at> gmail.com>, Mathieu Othacehe <othacehe <at> gnu.org>,
 Tobias Geerinckx-Rice <me <at> tobias.gr>,
 Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 Christopher Baines <guix <at> cbaines.net>, 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v4 06/14] daemon: Allow running as non-root
 with unprivileged user namespaces.
Date: Sun, 02 Mar 2025 15:09:05 +0900
Hi Ludo,

Ludovic Courtès <ludo <at> gnu.org> writes:

> From: Ludovic Courtès <ludovic.courtes <at> inria.fr>
>
> * 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.

That's a nice improvement!

[...]

> +There are currently two ways to set up and run the build daemon:
> +
> +@enumerate
> +@item
> +running @command{guix-daemon} as ``root'', letting it run build
> +processes as unprivileged users taken from a pool of build users---this
> +is the historical approach;
> +
> +@item
> +running @command{guix-daemon} as a separate unprivileged user, relying
> +on Linux's @dfn{unprivileged user namespace} functionality to set up
> +isolated environments---this option only appeared recently.
> +@end enumerate

Similarly to what Simon pointed in their comments, I'd drop time-related
'recently' wording, as it won't age well, and is already made obvious by
the above being mentioned as the 'historical' approach.

> +
> +The sections below describe each of these two configurations in more
> +detail and summarize the kind of build isolation they provide.
> +
> +@unnumberedsubsubsec Daemon Running as Root
>  
>  @cindex build users
>  When @command{guix-daemon} runs as @code{root}, you may not want package
>  build processes themselves to run as @code{root} too, for obvious
>  security reasons.  To avoid that, a special pool of @dfn{build users}
>  should be created for use by build processes started by the daemon.
> -These build users need not have a shell and a home directory: they will
> -just be used when the daemon drops @code{root} privileges in build
> -processes.  Having several such users allows the daemon to launch
> +Having several such users allows the daemon to launch
>  distinct build processes under separate UIDs, which guarantees that they
>  do not interfere with each other---an essential feature since builds are
>  regarded as pure functions (@pxref{Introduction}).
> @@ -977,11 +994,45 @@ Build Environment Setup
>  # guix-daemon --build-users-group=guixbuild
>  @end example
>  
> +In this setup, @file{/gnu/store} is owned by @code{root}.
> +
> +@unnumberedsubsubsec Daemon Running Without Privileges
> +
> +@cindex rootless build daemon
> +@cindex unprivileged build daemon
> +@cindex build daemon, unprivileged
> +The second option, which is new, is to run @command{guix-daemon}

s/, which is new,// as Simon pointed.

[...]

>  void DerivationGoal::startBuilder()
>  {
>      auto f = format(
> @@ -1682,7 +1705,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 +1822,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 +1841,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 +1877,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);

I think adding the new check for buildUser.enabled() in the above ifs
should be split into a distinct commit since it's not relevant to this
specific new feature.

[...]

>  #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();
> +	    }

That hunk above is also orthogonal to this feature AFAICS, should be
split into a different commit to keep its diff focused.

The rest LGTM.  C++ is not that hard to parse after all; it seems the
daemon is written in a style close to that of C.

Reviewed-by: Maxim Cournoyer <maxim.cournoyer <at> gmail>

-- 
Thanks,
Maxim




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 03 Mar 2025 17:17:02 GMT) Full text and rfc822 format available.

Message #208 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Simon Tournier <zimon.toutoune <at> gmail.com>
Cc: Josselin Poiret <dev <at> jpoiret.xyz>,
 Maxim Cournoyer <maxim.cournoyer <at> gmail.com>,
 Mathieu Othacehe <othacehe <at> gnu.org>, Tobias Geerinckx-Rice <me <at> tobias.gr>,
 Christopher Baines <guix <at> cbaines.net>, 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v4 06/14] daemon: Allow running as non-root
 with unprivileged user namespaces.
Date: Mon, 03 Mar 2025 18:16:18 +0100
Hi,

Simon Tournier <zimon.toutoune <at> gmail.com> skribis:

>> +There are currently two ways to set up and run the build daemon:
>> +
>> +@enumerate
>> +@item
>> +running @command{guix-daemon} as ``root'', letting it run build
>> +processes as unprivileged users taken from a pool of build users---this
>> +is the historical approach;
>> +
>> +@item
>> +running @command{guix-daemon} as a separate unprivileged user, relying
>> +on Linux's @dfn{unprivileged user namespace} functionality to set up
>> +isolated environments---this option only appeared recently.
>> +@end enumerate
>> +
>> +The sections below describe each of these two configurations in more
>> +detail and summarize the kind of build isolation they provide.
>
> The paragraph above could give the impression that there is a choice
> between two options – well it was my understand when reading.  On
> foreign distro, there is no option, IIUC.

The installation script chooses one of these two options for you, but
the choice is still available.  Since this section talks about
guix-daemon in general, I thought we should maintain that generality
here, but you’re probably right that it should stress that the
installation script and Guix System config make choices.  I’ll change
that in the next revision.

>> +@unnumberedsubsubsec Daemon Running Without Privileges
>> +
>> +@cindex rootless build daemon
>> +@cindex unprivileged build daemon
>> +@cindex build daemon, unprivileged
>> +The second option, which is new, is to run @command{guix-daemon}
>
> I would remove “which is new”.

Or “more recent” maybe?  The idea was to clarify why there are two
options at all.

>>                                                              The
>> +installation script automatically determines whether this option is
>> +available on your system (@pxref{Binary Installation}).
>
> I would write: When using the installation script, it automatically
> determines whether …

Hmm I think that would be grammatically incorrect.

Thanks for your feedback!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 03 Mar 2025 17:26:01 GMT) Full text and rfc822 format available.

Message #211 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
Cc: Josselin Poiret <dev <at> jpoiret.xyz>,
 Simon Tournier <zimon.toutoune <at> gmail.com>, Mathieu Othacehe <othacehe <at> gnu.org>,
 Tobias Geerinckx-Rice <me <at> tobias.gr>, Christopher Baines <guix <at> cbaines.net>,
 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v4 06/14] daemon: Allow running as non-root
 with unprivileged user namespaces.
Date: Mon, 03 Mar 2025 18:24:39 +0100
Hello,

Maxim Cournoyer <maxim.cournoyer <at> gmail.com> skribis:

> Similarly to what Simon pointed in their comments, I'd drop time-related
> 'recently' wording, as it won't age well, and is already made obvious by
> the above being mentioned as the 'historical' approach.

Alright, noted for the next revision.

>> -        if (chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
>> +        if (buildUser.enabled() && chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)

> I think adding the new check for buildUser.enabled() in the above ifs
> should be split into a distinct commit since it's not relevant to this
> specific new feature.

It’s in fact related: previously you could not run guix-daemon with
useChroot == true unless running as root, and buildUser.enabled() was
implied in this case.

With this change, you can end up in the “if (useChroot)” block without
running as root, which is why this distinction needs to be made.

>>  #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();
>> +	    }
>
> That hunk above is also orthogonal to this feature AFAICS, should be
> split into a different commit to keep its diff focused.

It’s also related: setting up ‘lo’ would always work before, because we
were running as root, but now it only works when running in a separate
net namespace.

Thanks for your feedback!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 04 Mar 2025 00:27:01 GMT) Full text and rfc822 format available.

Message #214 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Maxim Cournoyer <maxim.cournoyer <at> gmail.com>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: Josselin Poiret <dev <at> jpoiret.xyz>,
 Simon Tournier <zimon.toutoune <at> gmail.com>, Mathieu Othacehe <othacehe <at> gnu.org>,
 Tobias Geerinckx-Rice <me <at> tobias.gr>, Christopher Baines <guix <at> cbaines.net>,
 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v4 06/14] daemon: Allow running as non-root
 with unprivileged user namespaces.
Date: Tue, 04 Mar 2025 09:25:41 +0900
Hi Ludovic,

Ludovic Courtès <ludo <at> gnu.org> writes:

[...]

>>> -        if (chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
>>> +        if (buildUser.enabled() && chown(chrootRootDir.c_str(), 0, buildUser.getGID()) == -1)
>
>> I think adding the new check for buildUser.enabled() in the above ifs
>> should be split into a distinct commit since it's not relevant to this
>> specific new feature.
>
> It’s in fact related: previously you could not run guix-daemon with
> useChroot == true unless running as root, and buildUser.enabled() was
> implied in this case.
>
> With this change, you can end up in the “if (useChroot)” block without
> running as root, which is why this distinction needs to be made.

Oh, I see (and for the other instance as well).  Thanks for explaining!

Reviewed-by: Maxim Cournoyer <maxim.cournoyer <at> gmail>

-- 
Thanks,
Maxim




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 11 Mar 2025 12:43:02 GMT) Full text and rfc822 format available.

Message #217 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Locked mounts
Date: Tue, 11 Mar 2025 13:41:50 +0100
[Message part 1 (text/plain, inline)]
Hi Reepca,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> I still think it would be a good idea to call unshare to create an extra
> user and mount namespace just before executing the builder in the
> unprivileged case, just to be sure that the mount-locking behavior is
> triggered in a way that is documented.

For some reason, it’s not working as advertised: mounts are seemingly
not locked together and umount(2) on one of them returns EPERM (instead
of EINVAL).  I suspect chroot, pivot_root, & co. spoil it all.

Attached is a patch and test case.

To be sure, I wrote a minimal C program: umount returns EINVAL as
expected.  However, when compiling it with -DWITH_CHROOT, unshare(2)
fails with EPERM because “the caller's root directory does not match the
root directory of the mount namespace in which it resides” (quoting
unshare(2)).

So I now get the idea of “locked mounts” but I’m at loss as to how this
is supposed to interact with chroots.

Thoughts?

Ludo’.

[Message part 2 (text/x-patch, inline)]
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 057a15ccd0..6a6a960a1c 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2244,6 +2244,13 @@ 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).  */
+		if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
+		    throw SysError(format("creating new user and mount namespaces"));
+	    }
         }
 #endif
 
diff --git a/tests/store.scm b/tests/store.scm
index c22739afe6..9da9345dd0 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -37,6 +37,8 @@ (define-module (test-store)
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu packages make-bootstrap)
+                #:select (%guile-static-stripped))
   #:use-module (ice-9 match)
   #:use-module (ice-9 regex)
   #:use-module (rnrs bytevectors)
@@ -59,6 +61,8 @@ (define %shell
 
 (test-begin "store")
 
+(test-skip 25)
+
 (test-assert "open-connection with file:// URI"
   (let ((store (open-connection (string-append "file://"
                                                (%daemon-socket-uri)))))
@@ -455,7 +459,7 @@ (define %shell
          (drv
           (run-with-store %store
             (gexp->derivation
-             "attempt-to-remount-input-read-write"
+             "attempt-to-write-to-input"
              (with-imported-modules (source-module-closure
                                      '((guix build syscalls)))
                #~(begin
@@ -496,6 +500,58 @@ (define %shell
       (build-derivations %store (list drv))
       #f)))
 
+(let ((guile (with-external-store external-store
+               (and external-store
+                    (run-with-store external-store
+                      (mlet %store-monad ((drv (lower-object %guile-static-stripped)))
+                        (mbegin %store-monad
+                          (built-derivations (list drv))
+                          (return (derivation->output-path (pk 'GDRV drv))))))))))
+
+  (unless (and guile (unprivileged-user-namespace-supported?))
+    (test-skip 1))
+  (test-equal "input mount is locked"
+    EINVAL
+    ;; Check that mounts within the build environment are "locked" together and
+    ;; cannot be separated from within the build environment namespace--see
+    ;; mount_namespaces(7).
+    ;;
+    ;; Since guile-bootstrap <at> 2.0 lacks 'umount', resort to the hack below to
+    ;; get a statically-linked Guile with 'umount'.
+    (let* ((guile (computed-file "guile-with-umount"
+                                 ;; The #:guile-for-build argument must be a
+                                 ;; derivation, hence this silly thing.
+                                 #~(symlink #$(local-file guile "guile-with-umount"
+                                                          #:recursive? #t)
+                                            #$output)
+                                 #:guile %bootstrap-guile))
+           (drv
+            (run-with-store %store
+              (mlet %store-monad ((guile (lower-object guile)))
+                (gexp->derivation
+                 "attempt-to-unmount-input"
+                 (with-imported-modules (source-module-closure
+                                         '((guix build syscalls)))
+                   #~(begin
+                       (use-modules (guix build syscalls))
+
+                       (let ((input #$(plain-file "input-that-might-be-unmounted"
+                                                  (random-text))))
+                         (catch 'system-error
+                           (lambda ()
+                             ;; umount(2) returns EINVAL when the target is locked.
+                             (umount input))
+                           (lambda args
+                             (call-with-output-file #$output
+                               (lambda (port)
+                                 (write (system-error-errno args) port))))))))
+                 #:guile-for-build guile)))))
+      (build-derivations %store (list (pk 'UMDRV drv)))
+      (call-with-input-file (derivation->output-path drv)
+        read))))
+
+(test-skip 100)
+
 (unless (unprivileged-user-namespace-supported?)
   (test-skip 1))
 (test-assert "build root cannot be made world-readable"
[mount-namespace-locking.c (text/plain, attachment)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 11 Mar 2025 21:56:02 GMT) Full text and rfc822 format available.

Message #220 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: Locked mounts
Date: Tue, 11 Mar 2025 16:54:13 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

>> I still think it would be a good idea to call unshare to create an extra
>> user and mount namespace just before executing the builder in the
>> unprivileged case, just to be sure that the mount-locking behavior is
>> triggered in a way that is documented.
>
> For some reason, it’s not working as advertised: mounts are seemingly
> not locked together and umount(2) on one of them returns EPERM (instead
> of EINVAL).  I suspect chroot, pivot_root, & co. spoil it all.

What this shows is that we're not currently testing the mount-locking
because the builder lacks the necessary capability in its user namespace
(this capability was removed from the effective set when the builder was
exec'ed).  That is, the kernel doesn't get as far as checking whether
the mount is locked because the caller wouldn't have the permission to
unmount it even if it weren't locked.  One way to test this would be to
use setns (perhaps via container-excursion) to enter the namespaces of
the builder, which will cause you to start out with a full set of
effective capabilities in its user namespace, then try umount.  Or a
test could be done within the daemon shortly prior to exec.

> Attached is a patch and test case.

[...]

> diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
> index 057a15ccd0..6a6a960a1c 100644
> --- a/nix/libstore/build.cc
> +++ b/nix/libstore/build.cc
> @@ -2244,6 +2244,13 @@ 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).  */
> +		if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1)
> +		    throw SysError(format("creating new user and mount namespaces"));
> +	    }
>          }
>  #endif

Note that we still need to initialize /proc/self/uid_map and friends for
the new user namespace, if I understand correctly.  My reading of
user_namespaces(7) is that it should be possible to do this from within
the new user namespace.

> To be sure, I wrote a minimal C program: umount returns EINVAL as
> expected.  However, when compiling it with -DWITH_CHROOT, unshare(2)
> fails with EPERM because “the caller's root directory does not match the
> root directory of the mount namespace in which it resides” (quoting
> unshare(2)).
>
> So I now get the idea of “locked mounts” but I’m at loss as to how this
> is supposed to interact with chroots.

I hadn't heard of that restriction on unshare and clone.  Thinking about
it, I suppose the reason could be that merely creating a user namespace
grants CAP_SYS_CHROOT, and because the current root directory is a
per-process property whose setting isn't limited by any namespace, it
would be possible to undo a chroot someone had tried to set as a
restriction on the current process by just calling chroot("/").  But if
we use pivot_root in conjunction with it, like we do in the daemon, it
should work.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Wed, 12 Mar 2025 22:29:01 GMT) Full text and rfc822 format available.

Message #223 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: bug#75810: [PATCH 0/6] Rootless guix-daemon
Date: Wed, 12 Mar 2025 23:27:54 +0100
Hi,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

>> For some reason, it’s not working as advertised: mounts are seemingly
>> not locked together and umount(2) on one of them returns EPERM (instead
>> of EINVAL).  I suspect chroot, pivot_root, & co. spoil it all.
>
> What this shows is that we're not currently testing the mount-locking
> because the builder lacks the necessary capability in its user namespace
> (this capability was removed from the effective set when the builder was
> exec'ed).  That is, the kernel doesn't get as far as checking whether
> the mount is locked because the caller wouldn't have the permission to
> unmount it even if it weren't locked.

Oh my.  These clumsy semantics just can’t fit in my head.

> One way to test this would be to use setns (perhaps via
> container-excursion) to enter the namespaces of the builder, which
> will cause you to start out with a full set of effective capabilities
> in its user namespace, then try umount.  Or a test could be done
> within the daemon shortly prior to exec.

I tried running a build that sleeps and then joining its namespaces but
failed:

--8<---------------cut here---------------start------------->8---
$ pgrep -fa builder
16091 guile --no-auto-compile -L /home/ludo/src/guix/test-tmp/store/ngrj4gl9lrbmbklcsbgcrq622n9nf0jw-module-import -C /home/ludo/src/guix/test-tmp/store/cskis66zjnhk28h11lbaxkd3j9lyzz6a-module-import-compiled /home/ludo/src/guix/test-tmp/store/akndbdx1lmnigf8bi29dr0vd3c8dbdrg-attempt-to-unmount-input-builder
$ nsenter -m -u -i -n -p -U  -t 16091 
nsenter: reassociate to namespace 'ns/ipc' failed: Operation not permitted
$ guix container exec 16091 /bin/sh
guix container: error: setns: 7 0: Operation not permitted
guix container: error: process exited with status 1
--8<---------------cut here---------------end--------------->8---

If you can think of ways to do that, I’m all ears.  :-)

You can try from the ‘wip-rootless-daemon’ at
<https://codeberg.org/civodul/guix> and apply the patch I sent earlier.

(Incidentally, I don’t think we could write an automated test for that;
in theory we could use (guix scripts processes) to determine the PID of
the build process but that would be too brittle, especially when running
“make check -j123” where it could pick the wrong guix-daemon process.)

> Note that we still need to initialize /proc/self/uid_map and friends for
> the new user namespace, if I understand correctly.

Yes, I left that for later (it triggers a test failure anyway, so we
won’t forget.)

> My reading of user_namespaces(7) is that it should be possible to do
> this from within the new user namespace.

Oh, interesting.

> I hadn't heard of that restriction on unshare and clone.  Thinking about
> it, I suppose the reason could be that merely creating a user namespace
> grants CAP_SYS_CHROOT, and because the current root directory is a
> per-process property whose setting isn't limited by any namespace, it
> would be possible to undo a chroot someone had tried to set as a
> restriction on the current process by just calling chroot("/").  But if
> we use pivot_root in conjunction with it, like we do in the daemon, it
> should work.

Interesting.  Thanks for your guidance, as always!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Mar 2025 06:05:02 GMT) Full text and rfc822 format available.

Message #226 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: bug#75810: [PATCH 0/6] Rootless guix-daemon
Date: Thu, 13 Mar 2025 01:04:09 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

> I tried running a build that sleeps and then joining its namespaces but
> failed:
>
> $ pgrep -fa builder
> 16091 guile --no-auto-compile -L /home/ludo/src/guix/test-tmp/store/ngrj4gl9lrbmbklcsbgcrq622n9nf0jw-module-import -C /home/ludo/src/guix/test-tmp/store/cskis66zjnhk28h11lbaxkd3j9lyzz6a-module-import-compiled /home/ludo/src/guix/test-tmp/store/akndbdx1lmnigf8bi29dr0vd3c8dbdrg-attempt-to-unmount-input-builder
> $ nsenter -m -u -i -n -p -U  -t 16091 
> nsenter: reassociate to namespace 'ns/ipc' failed: Operation not permitted
> $ guix container exec 16091 /bin/sh
> guix container: error: setns: 7 0: Operation not permitted
> guix container: error: process exited with status 1

I failed to take into account that the setns sequence needs to start by
joining the user namespace that owns all the other namespaces, so as to
gain the necessary capabilities for joining them.  But after unshare
creates the second user namespace, that first user namespace no longer
has a process in it; it only still exists due to indirect references.
Without a process to reference it by, we can't join it.  Trying to join
the user namespace of the builder instead joins the inner user
namespace, then tries to use the acquired credentials to join the
namespaces owned by the outer user namespace, which naturally fails.

> If you can think of ways to do that, I’m all ears.  :-)

It looks like the only easy way to test this - aside from something like
scripted gdb playthroughs - might legitimately be to include a test
inside the daemon itself.

> You can try from the ‘wip-rootless-daemon’ at
> <https://codeberg.org/civodul/guix> and apply the patch I sent earlier.
>
> (Incidentally, I don’t think we could write an automated test for that;
> in theory we could use (guix scripts processes) to determine the PID of
> the build process but that would be too brittle, especially when running
> “make check -j123” where it could pick the wrong guix-daemon process.)

If we included a randomly-generated string in the command line or
environment of something in the build environment, and then went looking
through /proc for the matching process, it could work.  But it's moot if
we put this particular test inside the daemon.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 13 Mar 2025 23:52:02 GMT) Full text and rfc822 format available.

Message #229 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
Date: Fri, 14 Mar 2025 00:51:28 +0100
Hey,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> I failed to take into account that the setns sequence needs to start by
> joining the user namespace that owns all the other namespaces, so as to
> gain the necessary capabilities for joining them.  But after unshare
> creates the second user namespace, that first user namespace no longer
> has a process in it; it only still exists due to indirect references.
> Without a process to reference it by, we can't join it.  Trying to join
> the user namespace of the builder instead joins the inner user
> namespace, then tries to use the acquired credentials to join the
> namespaces owned by the outer user namespace, which naturally fails.

I see; brilliant.

>> If you can think of ways to do that, I’m all ears.  :-)
>
> It looks like the only easy way to test this - aside from something like
> scripted gdb playthroughs - might legitimately be to include a test
> inside the daemon itself.

Yes, that makes sense.

I added a ‘umount’ call, checking that we get EINVAL, and confirmed that
this check fails if we comment out the ‘unshare’ call.

I pushed the updated branch to Codeberg.  There are test failures in the
‘debian-install’ test that I now need to investigate before I send v5,
notably CAP_SYS_CHOWN not working (?) when attempting to create root’s
profile:

--8<---------------cut here---------------start------------->8---
guix install: [1;31merror: [0mdirectory `/var/guix/profiles/per-user/root' is not owned by you
[1;36mhint: [0mPlease change the owner of `/var/guix/profiles/per-user/root' to user
"root".

ls: cannot access '/root/.guix-profile': No such file or directory
sh: 1: /root/.guix-profile/bin/hello: not found
[…]
PASS: marionette works
PASS: /etc/os-release
PASS: mount host file store
PASS: screenshot before
PASS: install fake dependencies
PASS: run install script
PASS: create user account
PASS: guix describe
PASS: hello not already built
PASS: guix build hello
PASS: hello indeed built
/gnu/store/59qdz41chhifidaq79iiiyx70m7lmyrp-debian-install-builder:1: FAIL guix install hello
/gnu/store/59qdz41chhifidaq79iiiyx70m7lmyrp-debian-install-builder:1: FAIL user profile created
/gnu/store/59qdz41chhifidaq79iiiyx70m7lmyrp-debian-install-builder:1: FAIL hello
PASS: guix install hello, unprivileged user
PASS: user hello
PASS: unprivileged user profile created
/gnu/store/59qdz41chhifidaq79iiiyx70m7lmyrp-debian-install-builder:1: FAIL store is read-only
PASS: screenshot after
# of expected passes      15
# of unexpected failures  4
--8<---------------cut here---------------end--------------->8---

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:02 GMT) Full text and rfc822 format available.

Message #232 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v5 00/14] Rootless guix-daemon
Date: Fri, 14 Mar 2025 18:47:57 +0100
Hello Guix!

Changes since v4:

  • Remove qualifiers such as “new” from the documentation
    and clarify that unprivileged guix-daemon is the option
    chosen by default in some cases (Simon, Maxim).

  • Change ‘deleteTmpDir’ to deal with the case where
    CAP_SYS_CHOWN is available but ‘--disable-chroot’ is used
    (Reepca).

  • Add ‘unshare’ call in the build process before ‘execve’
    to create new user and mount namespaces, thereby locking
    together all the previous mounts; check by calling
    ‘umount’ and ensuring that it returns EINVAL that mounts
    are indeed locked (Reepca).

  • In ‘guix-install.sh’, keep /var/guix/profiles/per-user/root
    root-owned (previously it was chowned to ‘guix-daemon’).

  • In ‘guix-install.sh’, start ‘gnu-store.mount’ explicitly
    since it is no longer a dependency of ‘guix-daemon.service’.

  • In ‘guix-daemon.service.in’, set
    ‘GUIX_DATABASE_DIRECTORY=/var/guix’ for forward compatibility
    (I’m thinking of eventually changing the default database
    location when not running as root).

With these changes, the ‘debian-install’ and ‘guix-daemon’
system tests both pass.

I think we’ve never been this close to completion.  :-)

Thoughts?

Thanks a lot for your feedback, comrades.

Ludo’.

Ludovic Courtès (14):
  daemon: Use ‘close_range’ where available.
  daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Remount root directory as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  daemon: Move comments where they belong.
  tests: Add missing derivation inputs.
  tests: Run in a chroot and unprivileged user namespaces.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.
  DRAFT gnu: guix: Update to 00562be.

 build-aux/test-env.in               |  16 +-
 config-daemon.ac                    |   5 +-
 doc/guix.texi                       | 102 +++++++++---
 etc/gnu-store.mount.in              |   3 +-
 etc/guix-daemon.service.in          |  22 ++-
 etc/guix-install.sh                 | 109 +++++++++---
 gnu/packages/package-management.scm |   6 +-
 guix/substitutes.scm                |   2 +-
 nix/libstore/build.cc               | 247 ++++++++++++++++++++-------
 nix/libstore/local-store.cc         |  26 ++-
 nix/libutil/util.cc                 |  23 ++-
 tests/derivations.scm               |  24 ++-
 tests/packages.scm                  |  13 +-
 tests/processes.scm                 |   9 +-
 tests/store.scm                     | 250 ++++++++++++++++++++++++----
 15 files changed, 675 insertions(+), 182 deletions(-)


base-commit: 519fc51b6ecfe9ac9f2fa2f4ae052ab1984eed22
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:03 GMT) Full text and rfc822 format available.

Message #235 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 02/14] daemon: Bind-mount /etc/nsswitch.conf & co. only if
 it exists.
Date: Fri, 14 Mar 2025 18:47:59 +0100
Those files may be missing in some contexts, for instance within the
build environment.

* nix/libstore/build.cc (DerivationGoal::runChild): Add /etc/resolv.conf
and related files to ‘ss’ only if they exist.

Change-Id: Ie19664a86c8101a1dc82cf39ad4b7abb10f8250a
---
 nix/libstore/build.cc | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34..8ca5e5b732 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2093,10 +2093,11 @@ void DerivationGoal::runChild()
                network, so give them access to /etc/resolv.conf and so
                on. */
             if (fixedOutput) {
-                ss.push_back("/etc/resolv.conf");
-                ss.push_back("/etc/nsswitch.conf");
-                ss.push_back("/etc/services");
-                ss.push_back("/etc/hosts");
+		auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf",
+			       "/etc/services", "/etc/hosts" };
+		for (auto & file: files) {
+		    if (pathExists(file)) ss.push_back(file);
+		}
             }
 
             for (auto & i : ss) dirsInChroot[i] = i;
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:04 GMT) Full text and rfc822 format available.

Message #238 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 01/14] daemon: Use ‘close_range’ where available.
Date: Fri, 14 Mar 2025 18:47:58 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc3..4e949bc88a 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b..eb2d16e1cc 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:05 GMT) Full text and rfc822 format available.

Message #241 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 05/14] daemon: Remount root directory as read-only.
Date: Fri, 14 Mar 2025 18:48:02 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Bind-mount the store
and /tmp under ‘chrootRootDir’ to themselves as read-write.
Remount / as read-only.

Change-Id: I79565094c8ec8448401897c720aad75304fd1948
---
 nix/libstore/build.cc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 3861a1ffd9..c8b778362a 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2091,6 +2091,18 @@ void DerivationGoal::runChild()
 
             for (auto & i : ss) dirsInChroot[i] = i;
 
+	    /* Make new mounts for the store and for /tmp.  That way, when
+	       'chrootRootDir' is made read-only below, these two mounts will
+	       remain writable (the store needs to be writable so derivation
+	       outputs can be written to it, and /tmp is writable by
+	       convention).  */
+	    auto chrootStoreDir = chrootRootDir + settings.nixStore;
+	    if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir);
+	    auto chrootTmpDir = chrootRootDir + "/tmp";
+	    if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir);
+
             /* Bind-mount all the directories from the "host"
                filesystem that we want in the chroot
                environment. */
@@ -2164,6 +2176,10 @@ void DerivationGoal::runChild()
 
             if (rmdir("real-root") == -1)
                 throw SysError("cannot remove real-root directory");
+
+	    /* 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);
         }
 #endif
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:07 GMT) Full text and rfc822 format available.

Message #244 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v5 04/14] daemon: Remount inputs as read-only.
Date: Fri, 14 Mar 2025 18:48:01 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 193b279b88..3861a1ffd9 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2107,8 +2107,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:08 GMT) Full text and rfc822 format available.

Message #247 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v5 03/14] daemon: Bind-mount all the inputs,
 not just directories.
Date: Fri, 14 Mar 2025 18:48:00 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.
Special-case symlinks.
(DerivationGoal)[regularInputPaths]: Remove.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 39 ++++++++++++++-------------------------
 1 file changed, 14 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 8ca5e5b732..193b279b88 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -659,9 +659,6 @@ private:
     /* RAII object to delete the chroot directory. */
     std::shared_ptr<AutoDelete> autoDelChroot;
 
-    /* All inputs that are regular files. */
-    PathSet regularInputPaths;
-
     /* Whether this is a fixed-output derivation. */
     bool fixedOutput;
 
@@ -1850,9 +1847,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1858,22 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
+	    struct stat st;
             if (lstat(i->c_str(), &st))
                 throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
 
-                regularInputPaths.insert(*i);
-            }
+	    if (S_ISLNK(st.st_mode)) {
+		/* Since bind-mounts follow symlinks, thus representing their
+		   target and not the symlink itself, special-case
+		   symlinks. XXX: When running unprivileged, TARGET can be
+		   deleted by the build process.  Use 'open_tree' & co. when
+		   it's more widely available.  */
+                Path target = chrootRootDir + *i;
+		if (symlink(readLink(*i).c_str(), target.c_str()) == -1)
+		    throw SysError(format("failed to create symlink '%1%' to '%2%'") % target % readLink(*i));
+	    }
+	    else
+		dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:50:08 GMT) Full text and rfc822 format available.

Message #250 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 07/14] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Fri, 14 Mar 2025 18:48:04 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 83e6c3e16e..f6540c2117 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, maxim.cournoyer <at> gmail.com, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:02 GMT) Full text and rfc822 format available.

Message #253 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v5 06/14] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Fri, 14 Mar 2025 18:48:03 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

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.

Reviewed-by: Reepca Russelstein <reepca <at> russelstein.xyz>
---
 doc/guix.texi               | 102 +++++++++++++++++------
 guix/substitutes.scm        |   2 +-
 nix/libstore/build.cc       | 156 +++++++++++++++++++++++++++++++-----
 nix/libstore/local-store.cc |  18 +++--
 4 files changed, 225 insertions(+), 53 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index d109877a32..66d0e42112 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -877,6 +877,7 @@ Setting Up the Daemon
 @section Setting Up the Daemon
 
 @cindex daemon
+@cindex build daemon
 During installation, the @dfn{build daemon} that must be running
 to use Guix has already been set up and you can run @command{guix}
 commands in your terminal program, @pxref{Getting Started}:
@@ -921,20 +922,38 @@ Build Environment Setup
 @cindex build environment
 In a standard multi-user setup, Guix and its daemon---the
 @command{guix-daemon} program---are installed by the system
-administrator; @file{/gnu/store} is owned by @code{root} and
-@command{guix-daemon} runs as @code{root}.  Unprivileged users may use
-Guix tools to build packages or otherwise access the store, and the
-daemon will do it on their behalf, ensuring that the store is kept in a
-consistent state, and allowing built packages to be shared among users.
+administrator.  Unprivileged users may use Guix tools to build packages
+or otherwise access the store, and the daemon will do it on their
+behalf, ensuring that the store is kept in a consistent state, and
+allowing built packages to be shared among users.
+
+There are currently two ways to set up and run the build daemon:
+
+@enumerate
+@item
+running @command{guix-daemon} as ``root'', letting it run build
+processes as unprivileged users taken from a pool of build users---this
+is the historical approach;
+
+@item
+running @command{guix-daemon} as a separate unprivileged user, relying
+on Linux's @dfn{unprivileged user namespace} functionality to set up
+isolated environments---this is the option chosen when installing Guix
+on a systemd-based distribution with the installation script
+(@pxref{Binary Installation}).
+@end enumerate
+
+The sections below describe each of these two configurations in more
+detail and summarize the kind of build isolation they provide.
+
+@unnumberedsubsubsec Daemon Running as Root
 
 @cindex build users
 When @command{guix-daemon} runs as @code{root}, you may not want package
 build processes themselves to run as @code{root} too, for obvious
 security reasons.  To avoid that, a special pool of @dfn{build users}
 should be created for use by build processes started by the daemon.
-These build users need not have a shell and a home directory: they will
-just be used when the daemon drops @code{root} privileges in build
-processes.  Having several such users allows the daemon to launch
+Having several such users allows the daemon to launch
 distinct build processes under separate UIDs, which guarantees that they
 do not interfere with each other---an essential feature since builds are
 regarded as pure functions (@pxref{Introduction}).
@@ -977,11 +996,45 @@ Build Environment Setup
 # guix-daemon --build-users-group=guixbuild
 @end example
 
+In this setup, @file{/gnu/store} is owned by @code{root}.
+
+@unnumberedsubsubsec Daemon Running Without Privileges
+
+@cindex rootless build daemon
+@cindex unprivileged build daemon
+@cindex build daemon, unprivileged
+The second and preferred option is to run @command{guix-daemon}
+@emph{as an unprivileged user}.  It has the advantage of reducing the
+harm that can be done should a build process manage to exploit a
+vulnerability in the daemon.  This option requires the user of Linux's
+unprivileged user namespace mechanism; today it is available and enabled
+by most GNU/Linux distributions but can still be disabled.  The
+installation script automatically determines whether this option is
+available on your system (@pxref{Binary Installation}).
+
+When using this option, you only need to create one user account, and
+@command{guix-daemon} will run with the authority of that account:
+
+@example
+# groupadd --system guix-daemon
+# useradd -g guix-daemon -G guix-daemon              \
+          -d /var/empty -s $(which nologin)          \
+          -c "Guix daemon privilege separation user" \
+          --system guix-daemon
+@end example
+
+In this configuration, @file{/gnu/store} is owned by the
+@code{guix-daemon} user.
+
+@unnumberedsubsubsec The Isolated Build Environment
+
 @cindex chroot
-@noindent
-This way, the daemon starts build processes in a chroot, under one of
-the @code{guixbuilder} users.  On GNU/Linux, by default, the chroot
-environment contains nothing but:
+@cindex build environment isolation
+@cindex isolated build environment
+@cindex hermetic build environment
+In both cases, the daemon starts build processes without privileges in
+an @emph{isolated} or @emph{hermetic} build environment---a ``chroot''.
+On GNU/Linux, by default, the build environment contains nothing but:
 
 @c Keep this list in sync with libstore/build.cc! -----------------------
 @itemize
@@ -1015,7 +1068,7 @@ Build Environment Setup
 @file{/homeless-shelter}.  This helps to highlight inappropriate uses of
 @env{HOME} in the build scripts of packages.
 
-All this usually enough to ensure details of the environment do not
+All this is usually enough to ensure details of the environment do not
 influence build processes.  In some exceptional cases where more control
 is needed---typically over the date, kernel, or CPU---you can resort to
 a virtual build machine (@pxref{build-vm, virtual build machines}).
@@ -1035,14 +1088,6 @@ Build Environment Setup
 for fixed-output derivations (@pxref{Derivations}) or for substitutes
 (@pxref{Substitutes}).
 
-If you are installing Guix as an unprivileged user, it is still possible
-to run @command{guix-daemon} provided you pass @option{--disable-chroot}.
-However, build processes will not be isolated from one another, and not
-from the rest of the system.  Thus, build processes may interfere with
-each other, and may access programs, libraries, and other files
-available on the system---making it much harder to view them as
-@emph{pure} functions.
-
 
 @node Daemon Offload Setup
 @subsection Using the Offload Facility
@@ -1567,10 +1612,17 @@ Invoking guix-daemon
 @item --disable-chroot
 Disable chroot builds.
 
-Using this option is not recommended since, again, it would allow build
-processes to gain access to undeclared dependencies.  It is necessary,
-though, when @command{guix-daemon} is running under an unprivileged user
-account.
+@quotation Warning
+Using this option is not recommended since it allows build processes to
+gain access to undeclared dependencies, to interfere with one another,
+and more generally to do anything that can be done with the authority of
+the daemon---which includes at least the ability to tamper with any file
+in the store!
+
+You may find it necessary, though, when support for Linux unprivileged
+user namespaces is missing (@pxref{Build Environment Setup}).  Use at
+your own risk!
+@end quotation
 
 @item --log-compression=@var{type}
 Compress build logs according to @var{type}, one of @code{gzip},
diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index 7ca55788d5..86b9f5472a 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -79,7 +79,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c8b778362a..76f75e00df 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -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,34 @@ 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");
+	}
+
+	if ((flags & CLONE_NEWUSER) != 0) {
+	     /* Initialize the UID/GID mapping of the child process.  */
+	     initializeUserNamespace(pid);
+	     writeFull(readiness.writeSide, (unsigned char*)"go\n", 3);
+	}
     } else
 #endif
     {
@@ -2013,23 +2055,34 @@ void DerivationGoal::runChild()
 
         _writeToStderr = 0;
 
+	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);
+	     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 +2233,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
 
@@ -2476,8 +2550,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 +2818,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());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbce..83e6c3e16e 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -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);
+	}
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:02 GMT) Full text and rfc822 format available.

Message #256 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 08/14] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Fri, 14 Mar 2025 18:48:05 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a..35d9c8cd56 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 76f75e00df..07c8ad7e1d 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2070,6 +2073,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:03 GMT) Full text and rfc822 format available.

Message #259 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 09/14] daemon: Move comments where they belong.
Date: Fri, 14 Mar 2025 18:48:06 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Shuffle
comments for clarity.

Change-Id: I6557c103ade4a3ab046354548ea193c68f8c9c05
---
 nix/libstore/build.cc | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 07c8ad7e1d..37c3d3bf1e 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1870,18 +1870,19 @@ void DerivationGoal::startBuilder()
         }
         dirsInChroot[tmpDirInSandbox] = tmpDir;
 
-        /* Make the closure of the inputs available in the chroot,
-           rather than the whole store.  This prevents any access
-           to undeclared dependencies.  !!! As an extra security
-           precaution, make the fake store only writable by the
-           build user. */
+	/* Create the fake store.  */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
-            throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
+	     /* As an extra security precaution, make the fake store only
+		writable by the build user.  */
+	     throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
+        /* Make the closure of the inputs available in the chroot, rather than
+           the whole store.  This prevents any access to undeclared
+           dependencies. */
         foreach (PathSet::iterator, i, inputPaths) {
 	    struct stat st;
             if (lstat(i->c_str(), &st))
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:03 GMT) Full text and rfc822 format available.

Message #262 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 10/14] tests: Add missing derivation inputs.
Date: Fri, 14 Mar 2025 18:48:07 +0100
These missing inputs go unnoticed when running ‘guix-daemon
--disable-chroot’ but are immediately visible otherwise.

* tests/derivations.scm ("fixed-output derivation"): Add %BASH to #:sources.
("fixed-output derivation: output paths are equal"):
("fixed-output derivation, recursive"):
("derivation with a fixed-output input"):
("derivation with duplicate fixed-output inputs"):
("derivation with equivalent fixed-output inputs"):
("build derivation with coreutils"): Likewise.
* tests/packages.scm (bootstrap-binary): New procedure.
("package-source-derivation, origin, sha512"): Use it instead of
‘search-bootstrap-binary’ and add BASH to #:sources.
("package-source-derivation, origin, sha3-512"): Likewise.

Change-Id: I4c9087df23c47729a3aff15e9e1435b7266e36e2
---
 tests/derivations.scm | 24 +++++++++++++++---------
 tests/packages.scm    | 13 +++++++++----
 2 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/tests/derivations.scm b/tests/derivations.scm
index 72ea9aa9cc..f30f05474e 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -443,7 +443,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                      (string-append
                                       "fixed-" (symbol->string hash-algorithm))
                                      %bash `(,builder)
-                                     #:sources `(,builder) ;optional
+                                     #:sources (list %bash builder)
                                      #:hash hash
                                      #:hash-algo hash-algorithm)))
            (build-derivations %store (list drv))
@@ -462,9 +462,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv1       (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (drv2       (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (succeeded? (build-derivations %store (list drv1 drv2))))
     (and succeeded?
@@ -477,7 +479,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv        (derivation %store "fixed-rec"
                                  %bash `(,builder)
-                                 #:sources (list builder)
+                                 #:sources (list %bash builder)
                                  #:hash (base32 "0sg9f58l1jj88w6pdrfdpj5x9b1zrwszk84j81zvby36q9whhhqa")
                                  #:hash-algo 'sha256
                                  #:recursive? #t))
@@ -511,9 +513,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed-out  (derivation->output-path fixed1))
          (builder3   (add-text-to-store
@@ -548,9 +552,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (builder3   (add-text-to-store %store "builder.sh"
                                         "echo fake builder"))
@@ -580,21 +586,21 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                       '()))
          (hash     (gcrypt:sha256 (string->utf8 "hello")))
          (drv1     (derivation %store "fixed" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:hash hash #:hash-algo 'sha256))
          (drv2     (derivation %store "fixed" %bash (list builder2)
-                               #:sources (list builder2)
+                               #:sources (list %bash builder2)
                                #:hash hash #:hash-algo 'sha256))
          (drv3a    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv1))))
          (drv3b    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv2))))
          (drv4     (derivation %store "fixed-user-user" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:inputs (list (derivation-input drv3a '("one"))
                                               (derivation-input drv3b '("two"))))))
     (match (derivation-inputs drv4)
@@ -878,7 +884,7 @@ (define %coreutils
                                     ,(string-append
                                       (derivation->output-path %coreutils)
                                       "/bin")))
-                      #:sources (list builder)
+                      #:sources (list %bash builder)
                       #:inputs (list (derivation-input %coreutils))))
          (succeeded?
           (build-derivations %store (list drv))))
diff --git a/tests/packages.scm b/tests/packages.scm
index 50c1cab915..f56c63128d 100644
--- a/tests/packages.scm
+++ b/tests/packages.scm
@@ -80,6 +80,11 @@ (define %store
 ;; When grafting, do not add dependency on 'glibc-utf8-locales'.
 (%graft-with-utf8-locale? #f)
 
+(define (bootstrap-binary name)
+  (let ((bin (search-bootstrap-binary name (%current-system))))
+    (and %store
+         (add-to-store %store name #t "sha256" bin))))
+
 
 (test-begin "packages")
 
@@ -609,14 +614,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha512"
   "hello"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
@@ -635,14 +640,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha3-512"
   "hello, sha3"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello, sha3 > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha3-512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:04 GMT) Full text and rfc822 format available.

Message #265 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 12/14] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Fri, 14 Mar 2025 18:48:09 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(Environment): Add ‘GUIX_STATE_DIRECTORY’.
(Before, User, AmbientCapabilities, PrivateMounts, BindPaths): New fields.
* etc/gnu-store.mount.in (Before): Remove.
(WantedBy): Change to ‘multi-user.target’.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/gnu-store.mount.in     |  3 +--
 etc/guix-daemon.service.in | 22 ++++++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/etc/gnu-store.mount.in b/etc/gnu-store.mount.in
index c94f2db72b..f9918c9e52 100644
--- a/etc/gnu-store.mount.in
+++ b/etc/gnu-store.mount.in
@@ -2,10 +2,9 @@
 Description=Read-only @storedir@ for GNU Guix
 DefaultDependencies=no
 ConditionPathExists=@storedir@
-Before=guix-daemon.service
 
 [Install]
-WantedBy=guix-daemon.service
+WantedBy=multi-user.target
 
 [Mount]
 What=@storedir@
diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1..c4faf1bcfe 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -5,11 +5,29 @@
 [Unit]
 Description=Build daemon for GNU Guix
 
+# Start before 'gnu-store.mount' to get a writable view of the store.
+Before=gnu-store.mount
+
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
-Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+Environment='GUIX_STATE_DIRECTORY=@localstatedir@/guix' 'GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Bind-mount the store read-write in a private namespace, to counter the
+# effect of 'gnu-store.mount'.
+PrivateMounts=true
+BindPaths=@storedir@
+
+# Provide the CAP_CHOWN capability so that guix-daemon cran create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:04 GMT) Full text and rfc822 format available.

Message #268 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 11/14] tests: Run in a chroot and unprivileged user
 namespaces.
Date: Fri, 14 Mar 2025 18:48:08 +0100
* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking and warn in that case.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("symlink is symlink")
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write")
("build root cannot be made world-readable")
("/tmp, store, and /dev/{null,full} are writable")
("network is unreachable"): New tests.
* tests/processes.scm ("client + lock"): Skip when
‘unprivileged-user-namespace-supported?’ returns true.

Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84
---
 build-aux/test-env.in |  16 ++-
 tests/processes.scm   |   9 +-
 tests/store.scm       | 250 ++++++++++++++++++++++++++++++++++++------
 3 files changed, 237 insertions(+), 38 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da58..a3f225582d 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,22 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+	echo "unprivileged user namespaces not supported; \
+running 'guix-daemon $extra_options'" >&2
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/processes.scm b/tests/processes.scm
index ba518f2d9e..a72ba16f58 100644
--- a/tests/processes.scm
+++ b/tests/processes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2018 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2019 Mathieu Othacehe <m.othacehe <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -25,6 +25,8 @@ (define-module (test-processes)
   #:use-module (guix gexp)
   #:use-module ((guix utils) #:select (call-with-temporary-directory))
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix tests)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-64)
@@ -84,6 +86,11 @@ (define-syntax-rule (test-assert* description exp)
       (and (kill (process-id daemon) 0)
            (string-suffix? "guix-daemon" (first (process-command daemon)))))))
 
+(when (unprivileged-user-namespace-supported?)
+  ;; The test below assumes the build process can communicate with the outside
+  ;; world via the TOKEN1 and TOKEN2 files, which is impossible when
+  ;; guix-daemon is set up to build in separate namespaces.
+  (test-skip 1))
 (test-assert* "client + lock"
   (with-store store
     (call-with-temporary-directory
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f43..aa2477ef75 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,191 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(test-assert "symlink is symlink"
+  (let* ((a (add-text-to-store %store "hello.txt" (random-text)))
+         (b (build-expression->derivation
+             %store "symlink"
+             '(symlink (assoc-ref %build-inputs "a") %output)
+             #:inputs `(("a" ,a))))
+         (c (build-expression->derivation
+             %store "symlink-reference"
+             `(call-with-output-file %output
+                (lambda (port)
+                  ;; Check that B is indeed visible as a symlink.  This should
+                  ;; always be the case, both in the '--disable-chroot' and in
+                  ;; the user namespace setups.
+                  (pk 'stat (lstat (assoc-ref %build-inputs "b")))
+                  (display (readlink (assoc-ref %build-inputs "b"))
+                           port)))
+             #:inputs `(("b" ,b)))))
+    (and (build-derivations %store (list c))
+         (string=? (call-with-input-file (derivation->output-path c)
+                     get-string-all)
+                   a))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-write-to-input"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "build root cannot be made world-readable"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-make-root-world-readable"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((guile (string-append (assoc-ref %guile-build-info
+                                                         'bindir)
+                                              "/guile")))
+                    (catch 'system-error
+                      (lambda ()
+                        (chmod "/" #o777))
+                      (lambda args
+                        (format #t "failed to make root writable: ~a~%"
+                                (strerror (system-error-errno args)))
+                        (format #t "attempting read-write remount~%")
+                        (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
+                        (chmod "/" #o777)))
+                    (copy-file guile "/guile")
+                    (chmod "/guile" #o6755)
+                    ;; At this point, there's a world-readable setuid 'guile'
+                    ;; binary in the store that remains visible until this
+                    ;; build completes.
+                    (list #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "/tmp, store, and /dev/{null,full} are writable"
+  ;; All of /tmp and all of the store must be writable (the store is writable
+  ;; so that derivation outputs can be written to it, but in practice it's
+  ;; always been wide open).  Things like /dev/null must be writable too.
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-tmp-and-store-are-writable"
+                #~(begin
+                    (mkdir "/tmp/something")
+                    (mkdir (in-vicinity (getenv "NIX_STORE")
+                                        "some-other-thing"))
+                    (call-with-output-file "/dev/null"
+                      (lambda (port)
+                        (display "Welcome to the void." port)))
+                    (catch 'system-error
+                      (lambda ()
+                        (call-with-output-file "/dev/full"
+                          (lambda (port)
+                            (display "No space left!" port)))
+                        (error "Should have thrown!"))
+                      (lambda args
+                        (unless (= ENOSPC (system-error-errno args))
+                          (apply throw args))))
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "network is unreachable"
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-network-unreachable"
+                #~(let ((check-connection-failure
+                         (lambda (address expected-code)
+                           (let ((s (socket AF_INET SOCK_STREAM 0)))
+                             (catch 'system-error
+                               (lambda ()
+                                 (connect s AF_INET (inet-pton AF_INET address) 80))
+                               (lambda args
+                                 (let ((errno (system-error-errno args)))
+                                   (unless (= expected-code errno)
+                                     (error "wrong error code"
+                                            errno (strerror errno))))))))))
+                    (check-connection-failure "127.0.0.1" ECONNREFUSED)
+                    (check-connection-failure "9.9.9.9" ENETUNREACH)
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1522,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:05 GMT) Full text and rfc822 format available.

Message #271 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 13/14] guix-install.sh: Support the unprivileged daemon
 where possible.
Date: Fri, 14 Mar 2025 18:48:10 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 109 ++++++++++++++++++++++++++++++++++----------
 1 file changed, 84 insertions(+), 25 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index 8887204df4..eb1093c577 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -414,6 +414,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg_info "Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg_info "Linking the root user's profile"
@@ -441,38 +446,80 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg_info "user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg_pass "user added <$user>"
+    fi
+}
+
+can_install_unprivileged_daemon()
+{ # Return true if we can install guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
+	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg_info "group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg_pass "group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg_info "group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg_info "user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg_pass "user added <guixbuilder${i}>"
-        fi
-    done
+    if can_install_unprivileged_daemon
+    then
+	if getent group guix-daemon > /dev/null; then
+	    _msg_info "group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg_pass "group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon /gnu /var/guix
+	chown -R root:root /var/guix/profiles/per-user/root
+
+	# The unprivileged daemon cannot create the log directory by itself.
+	mkdir /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg_info "group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg_pass "group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -487,6 +534,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg_info "remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -529,11 +584,11 @@ sys_enable_guix_daemon()
 
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
-              # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      install_unit gnu-store.mount
 
               systemctl daemon-reload &&
-                  systemctl start  guix-daemon; } &&
+                  systemctl start guix-daemon &&
+	          systemctl start gnu-store.mount; } &&
                 _msg_pass "enabled Guix daemon via systemd"
             ;;
         sysv-init)
@@ -654,6 +709,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg_pass "Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg_info "Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 14 Mar 2025 17:51:05 GMT) Full text and rfc822 format available.

Message #274 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v5 14/14] DRAFT gnu: guix: Update to 00562be.
Date: Fri, 14 Mar 2025 18:48:11 +0100
DRAFT: Temporary commit.

* gnu/packages/package-management.scm (guix): Update to 00562be.

Change-Id: I34ef62c3b12391b145916bd6f44f4da3b497754e
---
 gnu/packages/package-management.scm | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/gnu/packages/package-management.scm b/gnu/packages/package-management.scm
index b0e8ad0d2a..6a48216961 100644
--- a/gnu/packages/package-management.scm
+++ b/gnu/packages/package-management.scm
@@ -179,8 +179,8 @@ (define-public guix
   ;; Note: the 'update-guix-package.scm' script expects this definition to
   ;; start precisely like this.
   (let ((version "1.4.0")
-        (commit "5058b40aba825ab6e7b9e518dd1147d1e35fd7de")
-        (revision 34))
+        (commit "00562be83ffe965836a8a23674d379bb9b45dfc9")
+        (revision 35))
     (package
       (name "guix")
 
@@ -196,7 +196,7 @@ (define-public guix
                       (commit commit)))
                 (sha256
                  (base32
-                  "04vk4lslcd6h22yj5pxvb1pdyyxd8421gjfyvyb1bl3xn7c77246"))
+                  "1ixrs1hlipv81y90q60v6rhjjg7sz3f0rgpq201lvgvbl9pl19i9"))
                 (file-name (string-append "guix-" version "-checkout"))))
       (build-system gnu-build-system)
       (arguments
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 15 Mar 2025 23:46:04 GMT) Full text and rfc822 format available.

Message #277 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [PATCH v5 00/14] Rootless guix-daemon
Date: Sat, 15 Mar 2025 18:44:17 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

>   • In ‘guix-daemon.service.in’, set
>     ‘GUIX_DATABASE_DIRECTORY=/var/guix’ for forward compatibility
>     (I’m thinking of eventually changing the default database
>     location when not running as root).

Did you intend GUIX_STATE_DIRECTORY here, or GUIX_DATABASE_DIRECTORY in
the patch?  GUIX_STATE_DIRECTORY is what's in the patch.

> @@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
>  
>  void closeMostFDs(const set<int> & exceptions)
>  {
> -    int maxFD = 0;
> -    maxFD = sysconf(_SC_OPEN_MAX);
> -    for (int fd = 0; fd < maxFD; ++fd)
> -        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
> -            && exceptions.find(fd) == exceptions.end())
> -            close(fd); /* ignore result */
> +#ifdef HAVE_CLOSE_RANGE
> +    if (exceptions.empty())
> +	 close_range(3, ~0U, 0);
> +    else
> +#endif
> +    {
> +	 int maxFD = 0;
> +	 maxFD = sysconf(_SC_OPEN_MAX);
> +	 for (int fd = 0; fd < maxFD; ++fd)
> +	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
> +		  && exceptions.find(fd) == exceptions.end())
> +		   close(fd); /* ignore result */
> +    }
>  }

(in patch 01/14, in nix/libutil/util.cc)

Minor note: this could be implemented solely in terms of close_range, by
sorting the exceptions and iterating over the gaps between them.  It's
fine as it is though.

> +The second and preferred option is to run @command{guix-daemon}
> +@emph{as an unprivileged user}.  It has the advantage of reducing the
> +harm that can be done should a build process manage to exploit a
> +vulnerability in the daemon.  This option requires the user of Linux's
> +unprivileged user namespace mechanism; today it is available and enabled
> +by most GNU/Linux distributions but can still be disabled.  The
> +installation script automatically determines whether this option is
> +available on your system (@pxref{Binary Installation}).

(in patch 06/14, in doc/guix.texi)

In the third sentence: s/requires the user/requires the use/

> +When using this option, you only need to create one user account, and
> +@command{guix-daemon} will run with the authority of that account:
> +
> +@example
> +# groupadd --system guix-daemon
> +# useradd -g guix-daemon -G guix-daemon              \
> +          -d /var/empty -s $(which nologin)          \
> +          -c "Guix daemon privilege separation user" \
> +          --system guix-daemon
> +@end example
> +
> +In this configuration, @file{/gnu/store} is owned by the
> +@code{guix-daemon} user.

It may be somewhat confusing to the reader looking at this page in
isolation whether they still need to create this account after running
the installation script, since it was mentioned immediately before this.

> @@ -1567,10 +1612,17 @@ Invoking guix-daemon
>  @item --disable-chroot
>  Disable chroot builds.
> 
> -Using this option is not recommended since, again, it would allow build
> -processes to gain access to undeclared dependencies.  It is necessary,
> -though, when @command{guix-daemon} is running under an unprivileged user
> -account.
> +@quotation Warning
> +Using this option is not recommended since it allows build processes to
> +gain access to undeclared dependencies, to interfere with one another,
> +and more generally to do anything that can be done with the authority of
> +the daemon---which includes at least the ability to tamper with any file
> +in the store!
> +
> +You may find it necessary, though, when support for Linux unprivileged
> +user namespaces is missing (@pxref{Build Environment Setup}).  Use at
> +your own risk!
> +@end quotation

Note that the "do anything that can be done with the authority of the
daemon" part is only true in the case where the builder and the daemon
run under the same user.  For example, on Hurd, we use --disable-chroot
but still use the separate builder users.  More generally,
--disable-chroot allows any user that can start builds to gain the
privileges of the build user their build runs as.  While this allows for
the output of any build run as that user to be controlled, to my
knowledge it can't change the contents of existing outputs (unless they
can be gc'ed and rebuilt, of course).

Perhaps this would be a good place to recommend setting the permissions
of LOCALSTATEDIR/guix/daemon-socket to allow only trusted users to
access it?


> diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
> index c8b778362a..76f75e00df 100644
> --- a/nix/libstore/build.cc
> +++ b/nix/libstore/build.cc
> @@ -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();

(in nix/libstore/build.cc)

This is inside of DerivationGoal.  We found out while hunting that
deadlock bug that it doesn't always get disposed of in a timely fashion.
So we probably shouldn't rely on the AutoCloseFD type of
readiness.readSide and readiness.writeSide to close them in a timely
fashion, opting instead to explicitly close the end we use immediately
after we're done with it.  It would also probably be wise to follow the
usual practice of the reader closing the write end before reading, and
the writer closing the read end before writing, lest any
sudden-process-death cause indefinite hangs.

On a slightly-related note, while investigating what we currently do for
the other file-descriptor-bearing objects in DerivationGoal, I noticed
that we appear to never explicitly close builderOut.readSide in the
child (we do implicitly close it eventually with closeMostFDs).
Probably not a problem in practice, since the parent never writes to the
pipe, and if the parent process gets suddenly wiped out, the possibility
of the child blocking indefinitely is probably the least of our worries,
but maybe it would be good to close it in commonChildInit.

> @@ -102,10 +102,22 @@ then
>      rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
>      mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
>  
> +    # If unprivileged user namespaces are not supported, pass
> +    # '--disable-chroot'.
> +    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
> +       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
> +	extra_options=""
> +    else
> +	extra_options="--disable-chroot"
> +	echo "unprivileged user namespaces not supported; \
> +running 'guix-daemon $extra_options'" >&2
> +    fi
> +
>      # Launch the daemon without chroot support because is may be
>      # unavailable, for instance if we're not running as root.
>      "@abs_top_builddir@/pre-inst-env"				\
> -	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
> +	"@abs_top_builddir@/guix-daemon"			\
> +        $extra_options						\
>  	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &

(in patch 11/14, in build-aux/test-env.in)

I think this test for /proc/sys/kernel/unprivileged_userns_clone will
only work on linux, no?  On Hurd it will see that the file doesn't exist
and assume that means it shouldn't pass --disable-chroot.

Maybe also require that something like /proc/self/ns/user exists?  I see
in (gnu build linux-container) this is what we do for
'user-namespace-supported?'.  Perhaps
'unprivileged-user-namespace-supported?' should also be updated to only
return true if user namespaces are supported at all?  Otherwise many of
the test skips in this patch are going to do the opposite of what they
should on Hurd.

Also, perhaps the "Launch the daemon without chroot support because is
..."  comment should be updated (also s/is/it/ there) or removed.

> +(unless (unprivileged-user-namespace-supported?)
> +  (test-skip 1))
> +(test-assert "build root cannot be made world-readable"
> +  (let ((drv
> +         (run-with-store %store
> +           (gexp->derivation
> +            "attempt-to-make-root-world-readable"
> +            (with-imported-modules (source-module-closure
> +                                    '((guix build syscalls)))
> +              #~(begin
> +                  (use-modules (guix build syscalls))
> +
> +                  (let ((guile (string-append (assoc-ref %guile-build-info
> +                                                         'bindir)
> +                                              "/guile")))
> +                    (catch 'system-error
> +                      (lambda ()
> +                        (chmod "/" #o777))
> +                      (lambda args
> +                        (format #t "failed to make root writable: ~a~%"
> +                                (strerror (system-error-errno args)))
> +                        (format #t "attempting read-write remount~%")
> +                        (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
> +                        (chmod "/" #o777)))
> +                    (copy-file guile "/guile")
> +                    (chmod "/guile" #o6755)
> +                    ;; At this point, there's a world-readable setuid 'guile'
> +                    ;; binary in the store that remains visible until this
> +                    ;; build completes.
> +                    (list #$output))))))))
> +    (guard (c ((store-protocol-error? c) #t))
> +      (build-derivations %store (list drv))
> +      #f)))

(in tests/store.scm)

It may be good to forego actually creating the setuid guile here, since
it's not part of what's actually being tested, just a looming threat to
motivate what's being tested.  In the event that this test fails
someday, let's be kind to our testers and leave them with only a
world-readable setuid empty file or file containing a frowny face or
something.

... actually, on closer inspection, won't this test never fail?  The
gexp doesn't actually create #$output, so the build will always fail,
and the store-protocol-error will always be signaled, no?

> +# Provide the CAP_CHOWN capability so that guix-daemon cran create and chown
> +# /var/guix/profiles/per-user/$USER and also chown failed build directories
> +# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
> +# capabilities before executing build processes so they don't inherit them.
> +AmbientCapabilities=CAP_CHOWN

(in patch 12/14, in etc/guix-daemon.service.in)

s/cran create/can create/

Also, I'm curious how this interacts with the changes to the installer
script.  What happens if the installer detects that the rootless daemon
isn't supported, but the guix that it's installing only has this version
of guix-daemon.service?  I haven't checked, but if the answer is "the
service is broken", perhaps we should have two variants of the service
file, and sys_enable_guix_daemon decides which one to symlink
/etc/systemd/system/guix-daemon.service to?

> +can_install_unprivileged_daemon()
> +{ # Return true if we can install guix-daemon running without privileges.
> +    [ "$INIT_SYS" = systemd ] && \
> +	grep -q "User=guix-daemon" \
> +	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
> +	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
> +		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
> +}

(in patch 13/14, in etc/guix-install.sh)

I vaguely recall hearing that systemd only supports linux, so it might
not be strictly necessary to check whether user namespaces are supported
at all here like it was in test-env.  If we get support for the rootless
daemon working for the other init systems a check like that may be
necessary, though.

It does indeed look like it's going to be necessary to make some changes
to make the script able to support installing a newer guix to a
systemd-using system that doesn't support unprivileged user namespaces.

Perhaps a test case should be added for this system configuration?


We're getting close indeed!

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:04:03 GMT) Full text and rfc822 format available.

Message #280 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 01/16] daemon: Use ‘close_range’ where available.
Date: Mon, 17 Mar 2025 18:02:44 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc3..4e949bc88a 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b..eb2d16e1cc 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:04:04 GMT) Full text and rfc822 format available.

Message #283 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v6 00/16] Rootless guix-daemon
Date: Mon, 17 Mar 2025 18:02:43 +0100
Hello,

This new version addresses Reepca’s latest comments¹:

  • Close the read end of ‘logPipe’ in ‘commonChildInit’.

  • Explicitly close the ‘readiness’ pipe.

  • Fix ‘--disable-chroot’ warning in the manual that was misleading.

  • Have ‘test-env’ check whether user namespaces are supported at all,
    which fixes non-Linux support (where it would previously fail to
    pass ‘--disable-chroot’.)

  • Change ‘unprivileged-user-namespace-supported?’ similarly.

  • Fix “build root cannot be made world-readable” test, which could
    not possibly fail and was exposing users unnecessarily.

  • Change ‘guix-install.sh’ on systemd machines: warn when unprivileged
    user namespaces are disabled, attempt to enable them, and error out
    if we failed to enable them.

Hopefully I didn’t forget anything.

I checked that the “debian-install” and “guix-daemon” system tests
still pass.

Thanks,
Ludo’.

¹ https://issues.guix.gnu.org/75810#91

Ludovic Courtès (16):
  daemon: Use ‘close_range’ where available.
  daemon: Close the read end of the logging pipe.
  daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Remount root directory as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  daemon: Move comments where they belong.
  linux-container: ‘unprivileged-user-namespace-supported?’ returns #f
    on non-Linux.
  tests: Add missing derivation inputs.
  tests: Run in a chroot and unprivileged user namespaces.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.
  DRAFT gnu: guix: Update to 07c5b1b

 build-aux/test-env.in               |  18 +-
 config-daemon.ac                    |   5 +-
 doc/guix.texi                       | 102 ++++++++---
 etc/gnu-store.mount.in              |   3 +-
 etc/guix-daemon.service.in          |  22 ++-
 etc/guix-install.sh                 | 124 +++++++++++---
 gnu/build/linux-container.scm       |   4 +-
 gnu/packages/package-management.scm |   6 +-
 guix/substitutes.scm                |   2 +-
 nix/libstore/build.cc               | 251 +++++++++++++++++++++-------
 nix/libstore/local-store.cc         |  26 ++-
 nix/libutil/util.cc                 |  26 ++-
 tests/derivations.scm               |  24 ++-
 tests/packages.scm                  |  13 +-
 tests/processes.scm                 |   9 +-
 tests/store.scm                     | 247 +++++++++++++++++++++++----
 16 files changed, 698 insertions(+), 184 deletions(-)


base-commit: 0c497c87ac47206b3e8c6dfa2e1e9b5f3e452292
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:04 GMT) Full text and rfc822 format available.

Message #286 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 03/16] daemon: Bind-mount /etc/nsswitch.conf & co. only if
 it exists.
Date: Mon, 17 Mar 2025 18:02:46 +0100
Those files may be missing in some contexts, for instance within the
build environment.

* nix/libstore/build.cc (DerivationGoal::runChild): Add /etc/resolv.conf
and related files to ‘ss’ only if they exist.

Change-Id: Ie19664a86c8101a1dc82cf39ad4b7abb10f8250a
---
 nix/libstore/build.cc | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34..8ca5e5b732 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2093,10 +2093,11 @@ void DerivationGoal::runChild()
                network, so give them access to /etc/resolv.conf and so
                on. */
             if (fixedOutput) {
-                ss.push_back("/etc/resolv.conf");
-                ss.push_back("/etc/nsswitch.conf");
-                ss.push_back("/etc/services");
-                ss.push_back("/etc/hosts");
+		auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf",
+			       "/etc/services", "/etc/hosts" };
+		for (auto & file: files) {
+		    if (pathExists(file)) ss.push_back(file);
+		}
             }
 
             for (auto & i : ss) dirsInChroot[i] = i;
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:05 GMT) Full text and rfc822 format available.

Message #289 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v6 02/16] daemon: Close the read end of the logging pipe.
Date: Mon, 17 Mar 2025 18:02:45 +0100
* nix/libutil/util.cc (commonChildInit): Close ‘logPipe.readSide’.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ia9e48d1afb85d7af52770e016f2b6832792044dd
---
 nix/libutil/util.cc | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index eb2d16e1cc..56f116046c 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -1279,6 +1279,9 @@ void commonChildInit(Pipe & logPipe)
     if (setsid() == -1)
         throw SysError(format("creating a new session"));
 
+    /* Close the read end so only the parent holds a reference to it.  */
+    logPipe.readSide.close();
+
     /* Dup the write side of the logger pipe into stderr. */
     if (dup2(logPipe.writeSide, STDERR_FILENO) == -1)
         throw SysError("cannot pipe standard error into log file");
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:07 GMT) Full text and rfc822 format available.

Message #292 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v6 04/16] daemon: Bind-mount all the inputs,
 not just directories.
Date: Mon, 17 Mar 2025 18:02:47 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.
Special-case symlinks.
(DerivationGoal)[regularInputPaths]: Remove.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 39 ++++++++++++++-------------------------
 1 file changed, 14 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 8ca5e5b732..193b279b88 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -659,9 +659,6 @@ private:
     /* RAII object to delete the chroot directory. */
     std::shared_ptr<AutoDelete> autoDelChroot;
 
-    /* All inputs that are regular files. */
-    PathSet regularInputPaths;
-
     /* Whether this is a fixed-output derivation. */
     bool fixedOutput;
 
@@ -1850,9 +1847,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1858,22 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
+	    struct stat st;
             if (lstat(i->c_str(), &st))
                 throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
 
-                regularInputPaths.insert(*i);
-            }
+	    if (S_ISLNK(st.st_mode)) {
+		/* Since bind-mounts follow symlinks, thus representing their
+		   target and not the symlink itself, special-case
+		   symlinks. XXX: When running unprivileged, TARGET can be
+		   deleted by the build process.  Use 'open_tree' & co. when
+		   it's more widely available.  */
+                Path target = chrootRootDir + *i;
+		if (symlink(readLink(*i).c_str(), target.c_str()) == -1)
+		    throw SysError(format("failed to create symlink '%1%' to '%2%'") % target % readLink(*i));
+	    }
+	    else
+		dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:09 GMT) Full text and rfc822 format available.

Message #295 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 06/16] daemon: Remount root directory as read-only.
Date: Mon, 17 Mar 2025 18:02:49 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Bind-mount the store
and /tmp under ‘chrootRootDir’ to themselves as read-write.
Remount / as read-only.

Change-Id: I79565094c8ec8448401897c720aad75304fd1948
---
 nix/libstore/build.cc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 3861a1ffd9..c8b778362a 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2091,6 +2091,18 @@ void DerivationGoal::runChild()
 
             for (auto & i : ss) dirsInChroot[i] = i;
 
+	    /* Make new mounts for the store and for /tmp.  That way, when
+	       'chrootRootDir' is made read-only below, these two mounts will
+	       remain writable (the store needs to be writable so derivation
+	       outputs can be written to it, and /tmp is writable by
+	       convention).  */
+	    auto chrootStoreDir = chrootRootDir + settings.nixStore;
+	    if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir);
+	    auto chrootTmpDir = chrootRootDir + "/tmp";
+	    if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir);
+
             /* Bind-mount all the directories from the "host"
                filesystem that we want in the chroot
                environment. */
@@ -2164,6 +2176,10 @@ void DerivationGoal::runChild()
 
             if (rmdir("real-root") == -1)
                 throw SysError("cannot remove real-root directory");
+
+	    /* 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);
         }
 #endif
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:11 GMT) Full text and rfc822 format available.

Message #298 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v6 05/16] daemon: Remount inputs as read-only.
Date: Mon, 17 Mar 2025 18:02:48 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 193b279b88..3861a1ffd9 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2107,8 +2107,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:13 GMT) Full text and rfc822 format available.

Message #301 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 08/16] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Mon, 17 Mar 2025 18:02:51 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 83e6c3e16e..f6540c2117 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, maxim.cournoyer <at> gmail.com, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:15 GMT) Full text and rfc822 format available.

Message #304 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v6 07/16] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Mon, 17 Mar 2025 18:02:50 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

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.

Reviewed-by: Reepca Russelstein <reepca <at> russelstein.xyz>
---
 doc/guix.texi               | 102 +++++++++++++++++------
 guix/substitutes.scm        |   2 +-
 nix/libstore/build.cc       | 160 +++++++++++++++++++++++++++++++-----
 nix/libstore/local-store.cc |  18 ++--
 4 files changed, 229 insertions(+), 53 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index d109877a32..87943afec7 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -877,6 +877,7 @@ Setting Up the Daemon
 @section Setting Up the Daemon
 
 @cindex daemon
+@cindex build daemon
 During installation, the @dfn{build daemon} that must be running
 to use Guix has already been set up and you can run @command{guix}
 commands in your terminal program, @pxref{Getting Started}:
@@ -921,20 +922,38 @@ Build Environment Setup
 @cindex build environment
 In a standard multi-user setup, Guix and its daemon---the
 @command{guix-daemon} program---are installed by the system
-administrator; @file{/gnu/store} is owned by @code{root} and
-@command{guix-daemon} runs as @code{root}.  Unprivileged users may use
-Guix tools to build packages or otherwise access the store, and the
-daemon will do it on their behalf, ensuring that the store is kept in a
-consistent state, and allowing built packages to be shared among users.
+administrator.  Unprivileged users may use Guix tools to build packages
+or otherwise access the store, and the daemon will do it on their
+behalf, ensuring that the store is kept in a consistent state, and
+allowing built packages to be shared among users.
+
+There are currently two ways to set up and run the build daemon:
+
+@enumerate
+@item
+running @command{guix-daemon} as ``root'', letting it run build
+processes as unprivileged users taken from a pool of build users---this
+is the historical approach;
+
+@item
+running @command{guix-daemon} as a separate unprivileged user, relying
+on Linux's @dfn{unprivileged user namespace} functionality to set up
+isolated environments---this is the option chosen when installing Guix
+on a systemd-based distribution with the installation script
+(@pxref{Binary Installation}).
+@end enumerate
+
+The sections below describe each of these two configurations in more
+detail and summarize the kind of build isolation they provide.
+
+@unnumberedsubsubsec Daemon Running as Root
 
 @cindex build users
 When @command{guix-daemon} runs as @code{root}, you may not want package
 build processes themselves to run as @code{root} too, for obvious
 security reasons.  To avoid that, a special pool of @dfn{build users}
 should be created for use by build processes started by the daemon.
-These build users need not have a shell and a home directory: they will
-just be used when the daemon drops @code{root} privileges in build
-processes.  Having several such users allows the daemon to launch
+Having several such users allows the daemon to launch
 distinct build processes under separate UIDs, which guarantees that they
 do not interfere with each other---an essential feature since builds are
 regarded as pure functions (@pxref{Introduction}).
@@ -977,11 +996,45 @@ Build Environment Setup
 # guix-daemon --build-users-group=guixbuild
 @end example
 
+In this setup, @file{/gnu/store} is owned by @code{root}.
+
+@unnumberedsubsubsec Daemon Running Without Privileges
+
+@cindex rootless build daemon
+@cindex unprivileged build daemon
+@cindex build daemon, unprivileged
+The second and preferred option is to run @command{guix-daemon}
+@emph{as an unprivileged user}.  It has the advantage of reducing the
+harm that can be done should a build process manage to exploit a
+vulnerability in the daemon.  This option requires the use of Linux's
+unprivileged user namespace mechanism; today it is available and enabled
+by most GNU/Linux distributions but can still be disabled.  The
+installation script automatically determines whether this option is
+available on your system (@pxref{Binary Installation}).
+
+When using this option, you only need to create one user account, and
+@command{guix-daemon} will run with the authority of that account:
+
+@example
+# groupadd --system guix-daemon
+# useradd -g guix-daemon -G guix-daemon              \
+          -d /var/empty -s $(which nologin)          \
+          -c "Guix daemon privilege separation user" \
+          --system guix-daemon
+@end example
+
+In this configuration, @file{/gnu/store} is owned by the
+@code{guix-daemon} user.
+
+@unnumberedsubsubsec The Isolated Build Environment
+
 @cindex chroot
-@noindent
-This way, the daemon starts build processes in a chroot, under one of
-the @code{guixbuilder} users.  On GNU/Linux, by default, the chroot
-environment contains nothing but:
+@cindex build environment isolation
+@cindex isolated build environment
+@cindex hermetic build environment
+In both cases, the daemon starts build processes without privileges in
+an @emph{isolated} or @emph{hermetic} build environment---a ``chroot''.
+On GNU/Linux, by default, the build environment contains nothing but:
 
 @c Keep this list in sync with libstore/build.cc! -----------------------
 @itemize
@@ -1015,7 +1068,7 @@ Build Environment Setup
 @file{/homeless-shelter}.  This helps to highlight inappropriate uses of
 @env{HOME} in the build scripts of packages.
 
-All this usually enough to ensure details of the environment do not
+All this is usually enough to ensure details of the environment do not
 influence build processes.  In some exceptional cases where more control
 is needed---typically over the date, kernel, or CPU---you can resort to
 a virtual build machine (@pxref{build-vm, virtual build machines}).
@@ -1035,14 +1088,6 @@ Build Environment Setup
 for fixed-output derivations (@pxref{Derivations}) or for substitutes
 (@pxref{Substitutes}).
 
-If you are installing Guix as an unprivileged user, it is still possible
-to run @command{guix-daemon} provided you pass @option{--disable-chroot}.
-However, build processes will not be isolated from one another, and not
-from the rest of the system.  Thus, build processes may interfere with
-each other, and may access programs, libraries, and other files
-available on the system---making it much harder to view them as
-@emph{pure} functions.
-
 
 @node Daemon Offload Setup
 @subsection Using the Offload Facility
@@ -1567,10 +1612,17 @@ Invoking guix-daemon
 @item --disable-chroot
 Disable chroot builds.
 
-Using this option is not recommended since, again, it would allow build
-processes to gain access to undeclared dependencies.  It is necessary,
-though, when @command{guix-daemon} is running under an unprivileged user
-account.
+@quotation Warning
+Using this option is not recommended since it allows build processes to
+gain access to undeclared dependencies, to interfere with one another,
+and more generally to do anything that can be done with the authority of
+build users or that of the daemon---which includes at least the ability
+to tamper with any file in the store!
+
+You may find it necessary, though, when support for Linux unprivileged
+user namespaces is missing (@pxref{Build Environment Setup}).  Use at
+your own risk!
+@end quotation
 
 @item --log-compression=@var{type}
 Compress build logs according to @var{type}, one of @code{gzip},
diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index 7ca55788d5..86b9f5472a 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -79,7 +79,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c8b778362a..9a8278cd08 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -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,36 @@ 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);
+	     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 +2237,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
 
@@ -2476,8 +2554,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 +2822,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());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbce..83e6c3e16e 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -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);
+	}
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:17 GMT) Full text and rfc822 format available.

Message #307 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 09/16] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Mon, 17 Mar 2025 18:02:52 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a..35d9c8cd56 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 9a8278cd08..51ac11e235 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2074,6 +2077,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:20 GMT) Full text and rfc822 format available.

Message #310 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 10/16] daemon: Move comments where they belong.
Date: Mon, 17 Mar 2025 18:02:53 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Shuffle
comments for clarity.

Change-Id: I6557c103ade4a3ab046354548ea193c68f8c9c05
---
 nix/libstore/build.cc | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 51ac11e235..54d2996dd1 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1870,18 +1870,19 @@ void DerivationGoal::startBuilder()
         }
         dirsInChroot[tmpDirInSandbox] = tmpDir;
 
-        /* Make the closure of the inputs available in the chroot,
-           rather than the whole store.  This prevents any access
-           to undeclared dependencies.  !!! As an extra security
-           precaution, make the fake store only writable by the
-           build user. */
+	/* Create the fake store.  */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
-            throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
+	     /* As an extra security precaution, make the fake store only
+		writable by the build user.  */
+	     throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
+        /* Make the closure of the inputs available in the chroot, rather than
+           the whole store.  This prevents any access to undeclared
+           dependencies. */
         foreach (PathSet::iterator, i, inputPaths) {
 	    struct stat st;
             if (lstat(i->c_str(), &st))
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:22 GMT) Full text and rfc822 format available.

Message #313 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v6 11/16] linux-container: ‘unprivileged-user-namespace-supported?’ returns #f on non-Linux.
Date: Mon, 17 Mar 2025 18:02:54 +0100
Previously this procedure would return #t on non-Linux systems.

* gnu/build/linux-container.scm (unprivileged-user-namespace-supported?):
When USERNS-FILE doesn’t exist, return (user-namespace-supported?).

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I92050338b8b68bc3bd87100317eba69fcdf14a0a
---
 gnu/build/linux-container.scm | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm
index 5c303da8c8..a5c5d8962e 100644
--- a/gnu/build/linux-container.scm
+++ b/gnu/build/linux-container.scm
@@ -1,6 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2015 David Thompson <davet <at> gnu.org>
-;;; Copyright © 2017-2019, 2022, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2017-2019, 2022-2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -44,7 +44,7 @@ (define (unprivileged-user-namespace-supported?)
   (let ((userns-file "/proc/sys/kernel/unprivileged_userns_clone"))
     (if (file-exists? userns-file)
         (eqv? #\1 (call-with-input-file userns-file read-char))
-        #t)))
+        (user-namespace-supported?))))
 
 (define (setgroups-supported?)
   "Return #t if the setgroups proc file, introduced in Linux-libre 3.19,
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:05:23 GMT) Full text and rfc822 format available.

Message #316 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 12/16] tests: Add missing derivation inputs.
Date: Mon, 17 Mar 2025 18:02:55 +0100
These missing inputs go unnoticed when running ‘guix-daemon
--disable-chroot’ but are immediately visible otherwise.

* tests/derivations.scm ("fixed-output derivation"): Add %BASH to #:sources.
("fixed-output derivation: output paths are equal"):
("fixed-output derivation, recursive"):
("derivation with a fixed-output input"):
("derivation with duplicate fixed-output inputs"):
("derivation with equivalent fixed-output inputs"):
("build derivation with coreutils"): Likewise.
* tests/packages.scm (bootstrap-binary): New procedure.
("package-source-derivation, origin, sha512"): Use it instead of
‘search-bootstrap-binary’ and add BASH to #:sources.
("package-source-derivation, origin, sha3-512"): Likewise.

Change-Id: I4c9087df23c47729a3aff15e9e1435b7266e36e2
---
 tests/derivations.scm | 24 +++++++++++++++---------
 tests/packages.scm    | 13 +++++++++----
 2 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/tests/derivations.scm b/tests/derivations.scm
index 72ea9aa9cc..f30f05474e 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -443,7 +443,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                      (string-append
                                       "fixed-" (symbol->string hash-algorithm))
                                      %bash `(,builder)
-                                     #:sources `(,builder) ;optional
+                                     #:sources (list %bash builder)
                                      #:hash hash
                                      #:hash-algo hash-algorithm)))
            (build-derivations %store (list drv))
@@ -462,9 +462,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv1       (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (drv2       (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (succeeded? (build-derivations %store (list drv1 drv2))))
     (and succeeded?
@@ -477,7 +479,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv        (derivation %store "fixed-rec"
                                  %bash `(,builder)
-                                 #:sources (list builder)
+                                 #:sources (list %bash builder)
                                  #:hash (base32 "0sg9f58l1jj88w6pdrfdpj5x9b1zrwszk84j81zvby36q9whhhqa")
                                  #:hash-algo 'sha256
                                  #:recursive? #t))
@@ -511,9 +513,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed-out  (derivation->output-path fixed1))
          (builder3   (add-text-to-store
@@ -548,9 +552,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (builder3   (add-text-to-store %store "builder.sh"
                                         "echo fake builder"))
@@ -580,21 +586,21 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                       '()))
          (hash     (gcrypt:sha256 (string->utf8 "hello")))
          (drv1     (derivation %store "fixed" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:hash hash #:hash-algo 'sha256))
          (drv2     (derivation %store "fixed" %bash (list builder2)
-                               #:sources (list builder2)
+                               #:sources (list %bash builder2)
                                #:hash hash #:hash-algo 'sha256))
          (drv3a    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv1))))
          (drv3b    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv2))))
          (drv4     (derivation %store "fixed-user-user" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:inputs (list (derivation-input drv3a '("one"))
                                               (derivation-input drv3b '("two"))))))
     (match (derivation-inputs drv4)
@@ -878,7 +884,7 @@ (define %coreutils
                                     ,(string-append
                                       (derivation->output-path %coreutils)
                                       "/bin")))
-                      #:sources (list builder)
+                      #:sources (list %bash builder)
                       #:inputs (list (derivation-input %coreutils))))
          (succeeded?
           (build-derivations %store (list drv))))
diff --git a/tests/packages.scm b/tests/packages.scm
index 50c1cab915..f56c63128d 100644
--- a/tests/packages.scm
+++ b/tests/packages.scm
@@ -80,6 +80,11 @@ (define %store
 ;; When grafting, do not add dependency on 'glibc-utf8-locales'.
 (%graft-with-utf8-locale? #f)
 
+(define (bootstrap-binary name)
+  (let ((bin (search-bootstrap-binary name (%current-system))))
+    (and %store
+         (add-to-store %store name #t "sha256" bin))))
+
 
 (test-begin "packages")
 
@@ -609,14 +614,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha512"
   "hello"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
@@ -635,14 +640,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha3-512"
   "hello, sha3"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello, sha3 > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha3-512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:06:06 GMT) Full text and rfc822 format available.

Message #319 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 15/16] guix-install.sh: Support the unprivileged daemon
 where possible.
Date: Mon, 17 Mar 2025 18:02:58 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 124 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 99 insertions(+), 25 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index 8887204df4..30e4bc4223 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -414,6 +414,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg_info "Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg_info "Linking the root user's profile"
@@ -441,38 +446,95 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg_info "user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg_pass "user added <$user>"
+    fi
+}
+
+install_unprivileged_daemon()
+{ # Return true when installing guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg_info "group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg_pass "group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg_info "group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg_info "user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg_pass "user added <guixbuilder${i}>"
-        fi
-    done
+    if install_unprivileged_daemon
+    then
+	_msg_info "installing guix-daemon to run as an unprivileged user"
+
+	# Installing guix-daemon to run as a non-root user requires
+	# unprivileged user namespaces.
+	if [ -f /proc/sys/kernel/unprivileged_userns_clone ] \
+	       && [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -ne 1 ]
+	then
+	    echo 1 > /proc/sys/kernel/unprivileged_userns_clone || \
+		_err "failed to enable unprivileged user namespaces"
+
+	    _msg_warn "Unprivileged user namespaces were disabled and have been enabled now."
+	    _msg_warn "This Linux feature is required by guix-daemon.  To enable it permanently, run:"
+	    _msg_warn '  echo 1 > /proc/sys/kernel/unprivileged_userns_clone'
+	    _msg_warn "from the relevant startup script."
+	fi
+
+
+	if getent group guix-daemon > /dev/null; then
+	    _msg_info "group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg_pass "group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon /gnu /var/guix
+	chown -R root:root /var/guix/profiles/per-user/root
+
+	# The unprivileged daemon cannot create the log directory by itself.
+	mkdir /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg_info "group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg_pass "group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -487,6 +549,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg_info "remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -529,11 +599,11 @@ sys_enable_guix_daemon()
 
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
-              # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      install_unit gnu-store.mount
 
               systemctl daemon-reload &&
-                  systemctl start  guix-daemon; } &&
+                  systemctl start guix-daemon &&
+	          systemctl start gnu-store.mount; } &&
                 _msg_pass "enabled Guix daemon via systemd"
             ;;
         sysv-init)
@@ -654,6 +724,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg_pass "Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg_info "Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:06:08 GMT) Full text and rfc822 format available.

Message #322 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 13/16] tests: Run in a chroot and unprivileged user
 namespaces.
Date: Mon, 17 Mar 2025 18:02:56 +0100
* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking and warn in that case.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("symlink is symlink")
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write")
("build root cannot be made world-readable")
("/tmp, store, and /dev/{null,full} are writable")
("network is unreachable"): New tests.
* tests/processes.scm ("client + lock"): Skip when
‘unprivileged-user-namespace-supported?’ returns true.

Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84
---
 build-aux/test-env.in |  18 ++-
 tests/processes.scm   |   9 +-
 tests/store.scm       | 247 ++++++++++++++++++++++++++++++++++++------
 3 files changed, 236 insertions(+), 38 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da58..86c2e585d7 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,24 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ -f /proc/self/ns/user ] \
+       && { [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; }
+    then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+	echo "unprivileged user namespaces not supported; \
+running 'guix-daemon $extra_options'" >&2
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/processes.scm b/tests/processes.scm
index ba518f2d9e..a72ba16f58 100644
--- a/tests/processes.scm
+++ b/tests/processes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2018 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2019 Mathieu Othacehe <m.othacehe <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -25,6 +25,8 @@ (define-module (test-processes)
   #:use-module (guix gexp)
   #:use-module ((guix utils) #:select (call-with-temporary-directory))
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix tests)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-64)
@@ -84,6 +86,11 @@ (define-syntax-rule (test-assert* description exp)
       (and (kill (process-id daemon) 0)
            (string-suffix? "guix-daemon" (first (process-command daemon)))))))
 
+(when (unprivileged-user-namespace-supported?)
+  ;; The test below assumes the build process can communicate with the outside
+  ;; world via the TOKEN1 and TOKEN2 files, which is impossible when
+  ;; guix-daemon is set up to build in separate namespaces.
+  (test-skip 1))
 (test-assert* "client + lock"
   (with-store store
     (call-with-temporary-directory
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f43..b1ddff2082 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,188 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(test-assert "symlink is symlink"
+  (let* ((a (add-text-to-store %store "hello.txt" (random-text)))
+         (b (build-expression->derivation
+             %store "symlink"
+             '(symlink (assoc-ref %build-inputs "a") %output)
+             #:inputs `(("a" ,a))))
+         (c (build-expression->derivation
+             %store "symlink-reference"
+             `(call-with-output-file %output
+                (lambda (port)
+                  ;; Check that B is indeed visible as a symlink.  This should
+                  ;; always be the case, both in the '--disable-chroot' and in
+                  ;; the user namespace setups.
+                  (pk 'stat (lstat (assoc-ref %build-inputs "b")))
+                  (display (readlink (assoc-ref %build-inputs "b"))
+                           port)))
+             #:inputs `(("b" ,b)))))
+    (and (build-derivations %store (list c))
+         (string=? (call-with-input-file (derivation->output-path c)
+                     get-string-all)
+                   a))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-write-to-input"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "build root cannot be made world-readable"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-make-root-world-readable"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (catch 'system-error
+                    (lambda ()
+                      (chmod "/" #o777))
+                    (lambda args
+                      (format #t "failed to make root writable: ~a~%"
+                              (strerror (system-error-errno args)))
+                      (format #t "attempting read-write remount~%")
+                      (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
+                      (chmod "/" #o777)))
+
+                  ;; At this point, the build process could create a
+                  ;; world-readable setuid binary under its root (so in the
+                  ;; store) that would remain visible until the build
+                  ;; completes.
+                  (mkdir #$output)))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "/tmp, store, and /dev/{null,full} are writable"
+  ;; All of /tmp and all of the store must be writable (the store is writable
+  ;; so that derivation outputs can be written to it, but in practice it's
+  ;; always been wide open).  Things like /dev/null must be writable too.
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-tmp-and-store-are-writable"
+                #~(begin
+                    (mkdir "/tmp/something")
+                    (mkdir (in-vicinity (getenv "NIX_STORE")
+                                        "some-other-thing"))
+                    (call-with-output-file "/dev/null"
+                      (lambda (port)
+                        (display "Welcome to the void." port)))
+                    (catch 'system-error
+                      (lambda ()
+                        (call-with-output-file "/dev/full"
+                          (lambda (port)
+                            (display "No space left!" port)))
+                        (error "Should have thrown!"))
+                      (lambda args
+                        (unless (= ENOSPC (system-error-errno args))
+                          (apply throw args))))
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "network is unreachable"
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-network-unreachable"
+                #~(let ((check-connection-failure
+                         (lambda (address expected-code)
+                           (let ((s (socket AF_INET SOCK_STREAM 0)))
+                             (catch 'system-error
+                               (lambda ()
+                                 (connect s AF_INET (inet-pton AF_INET address) 80))
+                               (lambda args
+                                 (let ((errno (system-error-errno args)))
+                                   (unless (= expected-code errno)
+                                     (error "wrong error code"
+                                            errno (strerror errno))))))))))
+                    (check-connection-failure "127.0.0.1" ECONNREFUSED)
+                    (check-connection-failure "9.9.9.9" ENETUNREACH)
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1519,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:06:09 GMT) Full text and rfc822 format available.

Message #325 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 16/16] DRAFT gnu: guix: Update to 07c5b1b
Date: Mon, 17 Mar 2025 18:02:59 +0100
DRAFT: Temporary commit.

* gnu/packages/package-management.scm (guix): Update to 07c5b1b.

Change-Id: Id7c3275da249075cdb23d7f4f63fd1bcf7dd933b
---
 gnu/packages/package-management.scm | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/gnu/packages/package-management.scm b/gnu/packages/package-management.scm
index a4a96878f7..11cfd10197 100644
--- a/gnu/packages/package-management.scm
+++ b/gnu/packages/package-management.scm
@@ -179,8 +179,8 @@ (define-public guix
   ;; Note: the 'update-guix-package.scm' script expects this definition to
   ;; start precisely like this.
   (let ((version "1.4.0")
-        (commit "5058b40aba825ab6e7b9e518dd1147d1e35fd7de")
-        (revision 34))
+        (commit "07c5b1b1ef05c002ad7092976e67eceb45f0c5da")
+        (revision 35))
     (package
       (name "guix")
 
@@ -196,7 +196,7 @@ (define-public guix
                       (commit commit)))
                 (sha256
                  (base32
-                  "04vk4lslcd6h22yj5pxvb1pdyyxd8421gjfyvyb1bl3xn7c77246"))
+                  "0hl692xzb8jylc8rwwvmgbdv08dnx35dx116vsw1s4c0ph8fr50a"))
                 (file-name (string-append "guix-" version "-checkout"))))
       (build-system gnu-build-system)
       (arguments
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Mon, 17 Mar 2025 17:07:02 GMT) Full text and rfc822 format available.

Message #328 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v6 14/16] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Mon, 17 Mar 2025 18:02:57 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(Environment): Add ‘GUIX_STATE_DIRECTORY’.
(Before, User, AmbientCapabilities, PrivateMounts, BindPaths): New fields.
* etc/gnu-store.mount.in (Before): Remove.
(WantedBy): Change to ‘multi-user.target’.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/gnu-store.mount.in     |  3 +--
 etc/guix-daemon.service.in | 22 ++++++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/etc/gnu-store.mount.in b/etc/gnu-store.mount.in
index c94f2db72b..f9918c9e52 100644
--- a/etc/gnu-store.mount.in
+++ b/etc/gnu-store.mount.in
@@ -2,10 +2,9 @@
 Description=Read-only @storedir@ for GNU Guix
 DefaultDependencies=no
 ConditionPathExists=@storedir@
-Before=guix-daemon.service
 
 [Install]
-WantedBy=guix-daemon.service
+WantedBy=multi-user.target
 
 [Mount]
 What=@storedir@
diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1..6a5ef97f9b 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -5,11 +5,29 @@
 [Unit]
 Description=Build daemon for GNU Guix
 
+# Start before 'gnu-store.mount' to get a writable view of the store.
+Before=gnu-store.mount
+
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
-Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+Environment='GUIX_STATE_DIRECTORY=@localstatedir@/guix' 'GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Bind-mount the store read-write in a private namespace, to counter the
+# effect of 'gnu-store.mount'.
+PrivateMounts=true
+BindPaths=@storedir@
+
+# Provide the CAP_CHOWN capability so that guix-daemon can create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 18 Mar 2025 03:09:02 GMT) Full text and rfc822 format available.

Message #331 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [PATCH v6 00/16] Rootless guix-daemon
Date: Mon, 17 Mar 2025 22:07:45 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

> @@ -2013,23 +2057,36 @@ 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);
> +	     if (strcmp(str, "go\n") != 0)
> +		  throw Error("failed to initialize process in unprivileged user namespace");
> +	}
> +
>          restoreAffinity();

(in patch 7/16, in nix/libstore/build.cc)

Strictly speaking we should check whether the fds are >= 0, not > 0,
since 0 is technically a valid file descriptor, and we use -1 to
indicate the absence of a file descriptor.

Also, readiness.readSide isn't explicitly closed in the child after
we're done with it.

> +
> +	# The unprivileged daemon cannot create the log directory by itself.
> +	mkdir /var/log/guix
> +	chown guix-daemon:guix-daemon /var/log/guix
> +	chmod 755 /var/log/guix

(in patch 15/16, in etc/guix-install.sh)

Should this be 'mkdir -p' or some other conditional creation?  If I
understand correctly this will fail when overwriting an existing install
using GUIX_ALLOW_OVERWRITE.

Concerning guix-install.sh, to be clear, is the intent to specifically
not support installing the rootful daemon on systemd systems?

For my two cents, I do think that it's still a tradeoff - not just
because of the reliance on different kernel mechanisms for security, but
also because the rootless daemon currently causes visible changes to the
build environment (EROFS on /, and nothing owned by root, for example).
Which one should we consider the "canonical" build environment going
forward?


I decided to do some searching for container escapes / vulnerabilities
online, just to be extra careful, and found one that relies on the
container entry point being run as the user that owns the program that
execs the container entry point: CVE-2019-5736.  It exploits the fact
that /proc/self/exe, despite being displayed like a symlink in the
output of ls, does not actually act like a symlink, and indeed acts more
like a hardlink that readlink happens to have some associated data for.

The demo, modified for guix circumstances, would go something like this:

1. A derivation is created whose builder is /proc/self/exe, and whose
   LD_PRELOAD environment variable points into a malicious store item
   for one of its shared libraries - for example, libc.
2. The daemon reads this in, and, to my knowledge, does no verification
   of the builder string.  Note that this aspect isn't actually
   necessary, as the builder could also be a symlink to /proc/self/exe
   from the store.
3. The daemon sets up the build environment, and execs /proc/self/exe.
4. An attacker-controlled load-time function gets run.
5. It opens /proc/self/exe, initially read-only because it can't be
   opened writable while a process is executing it.  It then execs
   another attacker-controlled process, which inherits the open file
   descriptor and subsequently opens it via /proc/self/fd/<fd>, this
   time read-write (it can do this because it owns the file, and even if
   it's not writable, a quick fchmod will fix that, and the filesystem
   it was originally opened from isn't read-only, because guix-daemon
   starts before gnu-store.mount bind-mounts /gnu/store to itself prior
   to making it read-only).  It then overwrites the resulting file
   descriptor with whatever contents it wants.
6. The next time guix-daemon is started outside the container, it runs
   attacker-controlled code.


There are several points at which that particular attack could be
stopped, and I'd like to try to stop it at as many of them as possible.
A good start would be canonicalizing the builder prior to executing it
and then checking to make sure it is in the store.  A more general
solution could look like writing out and then executing a tiny binary,
something like /tmp/runbuilder, that does nothing but unlink itself and
then exec the actual program.

Here's a writeup of the CVE in question:
https://unit42.paloaltonetworks.com/breaking-docker-via-runc-explaining-cve-2019-5736/

Aside from all that, it looks good to me.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 18 Mar 2025 09:36:03 GMT) Full text and rfc822 format available.

Message #334 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v5 00/14] Rootless guix-daemon
Date: Tue, 18 Mar 2025 10:34:49 +0100
Hi!

(Oops, forgot to hit “send” on this one…)

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> Ludovic Courtès <ludo <at> gnu.org> writes:
>
>>   • In ‘guix-daemon.service.in’, set
>>     ‘GUIX_DATABASE_DIRECTORY=/var/guix’ for forward compatibility
>>     (I’m thinking of eventually changing the default database
>>     location when not running as root).
>
> Did you intend GUIX_STATE_DIRECTORY here, or GUIX_DATABASE_DIRECTORY in
> the patch?  GUIX_STATE_DIRECTORY is what's in the patch.

Sorry, I meant GUIX_STATE_DIRECTORY.

> Minor note: this could be implemented solely in terms of close_range, by
> sorting the exceptions and iterating over the gaps between them.  It's
> fine as it is though.

Yes.  In practice ‘exceptions’ is always empty in this code, so we
should eventually remove the argument.

> (in patch 06/14, in doc/guix.texi)
>
> In the third sentence: s/requires the user/requires the use/

Noted.

>> +When using this option, you only need to create one user account, and
>> +@command{guix-daemon} will run with the authority of that account:

[...]

> It may be somewhat confusing to the reader looking at this page in
> isolation whether they still need to create this account after running
> the installation script, since it was mentioned immediately before this.

Note that this doesn’t change the situation compared to ‘master’: it
describes what guix-daemon expects, without regard for the installation
method.  I’ll see how this can be clarified though.

>> +@quotation Warning
>> +Using this option is not recommended since it allows build processes to
>> +gain access to undeclared dependencies, to interfere with one another,
>> +and more generally to do anything that can be done with the authority of
>> +the daemon---which includes at least the ability to tamper with any file
>> +in the store!
>> +
>> +You may find it necessary, though, when support for Linux unprivileged
>> +user namespaces is missing (@pxref{Build Environment Setup}).  Use at
>> +your own risk!
>> +@end quotation
>
> Note that the "do anything that can be done with the authority of the
> daemon" part is only true in the case where the builder and the daemon
> run under the same user.  For example, on Hurd, we use --disable-chroot
> but still use the separate builder users.

Oh right.

[...]

> Perhaps this would be a good place to recommend setting the permissions
> of LOCALSTATEDIR/guix/daemon-socket to allow only trusted users to
> access it?

I don’t think so; we’re really just describing ‘--disable-chroot’ here.

>> +    /* 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();
>
> (in nix/libstore/build.cc)
>
> This is inside of DerivationGoal.  We found out while hunting that
> deadlock bug that it doesn't always get disposed of in a timely fashion.
> So we probably shouldn't rely on the AutoCloseFD type of
> readiness.readSide and readiness.writeSide to close them in a timely
> fashion, opting instead to explicitly close the end we use immediately
> after we're done with it.

Yes, agreed.

> On a slightly-related note, while investigating what we currently do for
> the other file-descriptor-bearing objects in DerivationGoal, I noticed
> that we appear to never explicitly close builderOut.readSide in the
> child (we do implicitly close it eventually with closeMostFDs).
> Probably not a problem in practice, since the parent never writes to the
> pipe, and if the parent process gets suddenly wiped out, the possibility
> of the child blocking indefinitely is probably the least of our worries,
> but maybe it would be good to close it in commonChildInit.

Agreed.

>> +    if [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
>> +       || [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; then
>> +	extra_options=""
>> +    else
>> +	extra_options="--disable-chroot"

[...]

> I think this test for /proc/sys/kernel/unprivileged_userns_clone will
> only work on linux, no?  On Hurd it will see that the file doesn't exist
> and assume that means it shouldn't pass --disable-chroot.

Oops yes.

> Maybe also require that something like /proc/self/ns/user exists?  I see
> in (gnu build linux-container) this is what we do for
> 'user-namespace-supported?'.  Perhaps
> 'unprivileged-user-namespace-supported?' should also be updated to only
> return true if user namespaces are supported at all?  Otherwise many of
> the test skips in this patch are going to do the opposite of what they
> should on Hurd.

OK.

> Also, perhaps the "Launch the daemon without chroot support because is
> ..."  comment should be updated (also s/is/it/ there) or removed.

OK.

>> +(unless (unprivileged-user-namespace-supported?)
>> +  (test-skip 1))
>> +(test-assert "build root cannot be made world-readable"

[...]

> It may be good to forego actually creating the setuid guile here, since
> it's not part of what's actually being tested, just a looming threat to
> motivate what's being tested.  In the event that this test fails
> someday, let's be kind to our testers and leave them with only a
> world-readable setuid empty file or file containing a frowny face or
> something.
>
> ... actually, on closer inspection, won't this test never fail?  The
> gexp doesn't actually create #$output, so the build will always fail,
> and the store-protocol-error will always be signaled, no?

Oops!  Done.

> (in patch 12/14, in etc/guix-daemon.service.in)
>
> s/cran create/can create/

Done.

> Also, I'm curious how this interacts with the changes to the installer
> script.  What happens if the installer detects that the rootless daemon
> isn't supported, but the guix that it's installing only has this version
> of guix-daemon.service?  I haven't checked, but if the answer is "the
> service is broken", perhaps we should have two variants of the service
> file, and sys_enable_guix_daemon decides which one to symlink
> /etc/systemd/system/guix-daemon.service to?

The assumption is indeed that unprivileged user namespaces are
supported.

I have reasons to believe that this assumption holds these days, which
is why I didn’t bother with the fallback you suggest.

>> +can_install_unprivileged_daemon()
>> +{ # Return true if we can install guix-daemon running without privileges.
>> +    [ "$INIT_SYS" = systemd ] && \
>> +	grep -q "User=guix-daemon" \
>> +	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service \
>> +	&& ([ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
>> +		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ])
>> +}
>
> (in patch 13/14, in etc/guix-install.sh)
>
> I vaguely recall hearing that systemd only supports linux, so it might
> not be strictly necessary to check whether user namespaces are supported
> at all here like it was in test-env.  If we get support for the rootless
> daemon working for the other init systems a check like that may be
> necessary, though.

Right, systemd is Linux-only, but you’re right that this should be
changed like ./test-env for other systems.

> It does indeed look like it's going to be necessary to make some changes
> to make the script able to support installing a newer guix to a
> systemd-using system that doesn't support unprivileged user namespaces.

A system running systemd definitely supports unprivileged user
namespaces; it is still possible to disable it manually, but I’m willing
to assume that it’s uncommon.

I’m sending v6 with these changes.

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 18 Mar 2025 14:01:02 GMT) Full text and rfc822 format available.

Message #337 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Tue, 18 Mar 2025 15:00:16 +0100
[Message part 1 (text/plain, inline)]
Hi!

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> (in patch 7/16, in nix/libstore/build.cc)
>
> Strictly speaking we should check whether the fds are >= 0, not > 0,
> since 0 is technically a valid file descriptor, and we use -1 to
> indicate the absence of a file descriptor.
>
> Also, readiness.readSide isn't explicitly closed in the child after
> we're done with it.

Right, fixed.

>> +	# The unprivileged daemon cannot create the log directory by itself.
>> +	mkdir /var/log/guix
>> +	chown guix-daemon:guix-daemon /var/log/guix
>> +	chmod 755 /var/log/guix
>
> (in patch 15/16, in etc/guix-install.sh)
>
> Should this be 'mkdir -p' or some other conditional creation?  If I
> understand correctly this will fail when overwriting an existing install
> using GUIX_ALLOW_OVERWRITE.

Correct, fixed as well.

(I’ve updated the branch on Codeberg.)

> Concerning guix-install.sh, to be clear, is the intent to specifically
> not support installing the rootful daemon on systemd systems?
>
> For my two cents, I do think that it's still a tradeoff - not just
> because of the reliance on different kernel mechanisms for security, but
> also because the rootless daemon currently causes visible changes to the
> build environment (EROFS on /, and nothing owned by root, for example).
> Which one should we consider the "canonical" build environment going
> forward?

The way I see it, we would gradually move to the non-root daemon:

  • First step here is to enable it by default on systemd distros.

  • Second step would be to allow Guix System users to migrate to
    non-root daemon, keeping the default unchanged.

  • Third step (a year later maybe?) would be to default to non-root
    daemon on Guix System and on remaining distros (though for these it
    might be trickier because we probably cannot rely on CAP_SYS_CHOWN,
    not as easily as with systemd at least).

The visible changes in the build environment are unfortunate; I’m hoping
they won’t have any practical impact, not any more than the other
parameters that may change currently (build UID, binfmt_misc, file
system, etc.)  We could test this hypothesis by rebuilding at least the
entire set of packages up to ‘hello’.  (I tried doing it just now in a
Debian VM but failed since the main partition cannot easily be extended;
it’ll be easier to do with Guix System.)

> The demo, modified for guix circumstances, would go something like this:
>
> 1. A derivation is created whose builder is /proc/self/exe, and whose
>    LD_PRELOAD environment variable points into a malicious store item
>    for one of its shared libraries - for example, libc.
> 2. The daemon reads this in, and, to my knowledge, does no verification
>    of the builder string.  Note that this aspect isn't actually
>    necessary, as the builder could also be a symlink to /proc/self/exe
>    from the store.
> 3. The daemon sets up the build environment, and execs /proc/self/exe.

Then there are 3 possibilities:

  1. If /proc/self/exe points to (say) /usr/bin/guix-daemon, outside the
     store, execve fails with ENOENT because that file is not mounted in
     the chroot.

  2. If /proc/self/exe points to a guix-daemon file inside the store:

     2a. If the store item containing guix-daemon is not an input of the
         derivation, execv fails with ENOENT.

     2b. If the store item containing guix-daemon is an input of the
         derivation, then it’s been remounted read-only and attempts to
         write to it fail with EROFS.

Here’s a test that fails both “rootfull” and “rootless”:

--8<---------------cut here---------------start------------->8---
(let* ((builder (add-file-tree-to-store %store
                                        `("builder" symlink "/proc/self/exe")))
       (drv (derivation %store "attempt-to-run-guix-daemon" builder '()
                        #:env-vars
                        '(("LD_PRELOAD" . "attacker-controlled.so")))))
  (guard (c ((store-protocol-error? c) c))
    (build-derivations %store (list drv))
    #f))
--8<---------------cut here---------------end--------------->8---

> There are several points at which that particular attack could be
> stopped, and I'd like to try to stop it at as many of them as possible.
> A good start would be canonicalizing the builder prior to executing it
> and then checking to make sure it is in the store.  A more general
> solution could look like writing out and then executing a tiny binary,
> something like /tmp/runbuilder, that does nothing but unlink itself and
> then exec the actual program.

If the above is correct, we’re already safe against this particular
attack.

Canonicalizing the builder cannot hurt (it’s useful in the
‘--disable-chroot’ case though mostly to prevent programming errors
rather than from a security perspective since there are many other
issues in that case), apart from adding more code to an already long
function.

For reference, the extra check I tried but that I’m inclined to not
include since it cannot catch anything new:

[Message part 2 (text/x-patch, inline)]
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 7484ea8fcf..970107c265 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2374,6 +2374,15 @@ 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).  */
+	    drv.builder = canonPath(drv.builder, true);
+	    printMsg(lvlError, format("builder is `%1%'") % drv.builder);
+	    if (!isInStore(drv.builder))
+		throw Error(format("derivation builder '%1%' is outside the store") % drv.builder);
 	}
 
         /* Fill in the arguments. */
[Message part 3 (text/plain, inline)]
Let me know what you think and I’ll send v7 accordingly.

Ludo’.

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Tue, 18 Mar 2025 23:21:03 GMT) Full text and rfc822 format available.

Message #340 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Tue, 18 Mar 2025 18:19:04 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

>> For my two cents, I do think that it's still a tradeoff - not just
>> because of the reliance on different kernel mechanisms for security, but
>> also because the rootless daemon currently causes visible changes to the
>> build environment (EROFS on /, and nothing owned by root, for example).
>> Which one should we consider the "canonical" build environment going
>> forward?
>
> The way I see it, we would gradually move to the non-root daemon:
>
>   • First step here is to enable it by default on systemd distros.
>
>   • Second step would be to allow Guix System users to migrate to
>     non-root daemon, keeping the default unchanged.
>
>   • Third step (a year later maybe?) would be to default to non-root
>     daemon on Guix System and on remaining distros (though for these it
>     might be trickier because we probably cannot rely on CAP_SYS_CHOWN,
>     not as easily as with systemd at least).
>
> The visible changes in the build environment are unfortunate; I’m hoping
> they won’t have any practical impact, not any more than the other
> parameters that may change currently (build UID, binfmt_misc, file
> system, etc.)  We could test this hypothesis by rebuilding at least the
> entire set of packages up to ‘hello’.  (I tried doing it just now in a
> Debian VM but failed since the main partition cannot easily be extended;
> it’ll be easier to do with Guix System.)
>

For what it's worth, the visible changes could be avoided with
subordinate ids, as I wrote in
https://debbugs.gnu.org/cgi/bugreport.cgi?bug=75810#86.

We could try it as-is and see how it goes, and if there are problems
with reproducibility add on using subordinate ids.  I would expect it to
be a much smaller change than the root->rootless transition.

Hopefully it works well enough as-is that offload systems could be set
up without any special permissions.

> Then there are 3 possibilities:
>
>   1. If /proc/self/exe points to (say) /usr/bin/guix-daemon, outside the
>      store, execve fails with ENOENT because that file is not mounted in
>      the chroot.

No, like I wrote, /proc/self/exe, despite being reported as a symlink by
stat, does not follow the usual symlink semantics.  This is much like
how the files in /proc/self/fd work, e.g.:

scheme@(guile-user)> (open-file "/tmp/freshfile" "w+")
$1 = #<input-output: /tmp/freshfile 13>
scheme@(guile-user)> (delete-file "/tmp/freshfile")
scheme@(guile-user)> (stat:type (lstat "/proc/self/fd/13"))
$3 = symlink
scheme@(guile-user)> (readlink "/proc/self/fd/13")
$4 = "/tmp/freshfile (deleted)"
scheme@(guile-user)> (open-file "/proc/self/fd/13" "w+")
$5 = #<input-output: /proc/self/fd/13 14>

Here is a test program to demonstrate this (it's unfortunately rather
tricky to demonstrate using usual command line tools):

--8<---------------cut here---------------start------------->8---
/* Test program to demonstrate that /proc/self/exe does not behave like a
   symlink, and a process can exec /proc/self/exe even if there is no other
   filename by which the currently-executing program can be reached.

   Compile with -static (may require a 'guix shell gcc-toolchain glibc:static')
   and run. */


#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <string.h>
#include <unistd.h>
#include <sched.h>
#include <sys/param.h>
#include <sys/mount.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <errno.h>
#include <signal.h>

#define die(msg, status) \
  do \
    { \
      perror(msg); \
      exit(status); \
    } while(0); \
  
#define pivot_root(new_root, put_old) (syscall(SYS_pivot_root, new_root,put_old))

int child_main(void * arg)
{
  int *pipefds = (int *) arg;
  char *argv[2] = {"???", NULL};
  int saved_errno;
  char c;
  if(close(pipefds[1]))
    die("Child closing write end of pipe", 20);

  if(read(pipefds[0], &c, 1 * sizeof(char)) < (1 * sizeof(char)))
    die("Reading from pipe", 21);

  if(c != 'y')
    die("Parent setup failed", 22);

  if(close(pipefds[0]))
    die("Child closing read end of pipe", 23);

  if(mkdir("/tmp/test-chroot", 0700) && errno != EEXIST)
    die("mkdir(\"/tmp/test-chroot\")", 24);

  if(mount(0, "/", 0, MS_REC|MS_PRIVATE, 0))
    die("making / private", 25);

  if(mount("/tmp/test-chroot", "/tmp/test-chroot", 0, MS_BIND, 0))
    die("making /tmp/test-chroot its own filesystem", 26);

  if(mkdir("/tmp/test-chroot/proc", 0700) && errno != EEXIST)
    die("mkdir(\"/tmp/test-chroot/proc\")", 27);

  if(mount("none", "/tmp/test-chroot/proc", "proc", 0, 0))
    die("mount procfs", 28);

  if(chdir("/tmp/test-chroot"))
    die("chdir to /tmp/test-chroot", 29);

  if(mkdir("real-root", 0))
    die("mkdir(\"real-root\")", 30);

  if(pivot_root(".", "real-root"))
    die("pivot_root", 31);

  if(chroot("."))
    die("chroot", 32);

  if(umount2("real-root", MNT_DETACH))
    die("unmounting real root", 33);

  if(rmdir("real-root"))
    die("removing real root directory", 34);

  if(execve("/proc/self/exe", argv, environ))
    {
      saved_errno = errno;
      fprintf(stderr, "execve errno: %d\n", saved_errno);
      errno = saved_errno;
      die("execve", 35);
    }
}



int main(int argc, char **argv)
{
  pid_t child_pid = -1;
  char stack[32 * 1024];
  int flags;
  int result;
  int status;
  FILE * f;
  char strbuf[512];
  int j;
  int pipefds[2];
  if(getenv("TEST_PROGRAM_MAGIC_ENV_VAR"))
    {
      fprintf(stderr, "Self-exec'ed!\n");
      fprintf(stderr, "Arguments: \n");
      for(j = 0; j < argc; j++) fprintf(stderr, "%s\n", argv[j]);
      j = readlink("/proc/self/exe", strbuf, sizeof(strbuf) - 1);
      if(j >= 0)
        {
          strbuf[j] = '\0';
          fprintf(stderr, "/proc/self/exe readlink result: %s\n", strbuf);
          f = fopen(strbuf, "r");
          if(f)
            {
              fprintf(stderr, "... and that file exists!\n");
              fclose(f);
            }
          else
            {
              perror("fopen /proc/self/exe readlink result error");
              fprintf(stderr, "... and that file does not exist.\n");
            }
          f = fopen("/proc/self/exe", "r");
          if(f)
            {
              fprintf(stderr, "/proc/self/exe can be opened\n");
              fclose(f);
            }
          else
            {
              perror("fopen /proc/self/exe error");
              fprintf(stderr, "/proc/self/exe cannot be opened\n");
            }
        }
      return 0;
    }
  setenv("TEST_PROGRAM_MAGIC_ENV_VAR", "1", 1);
  
  if(pipe(pipefds))
    die("pipe()", 1);

  flags = CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS | CLONE_NEWUSER;
  flags = flags | SIGCHLD; 
  child_pid = clone(child_main,
                    (char *)(((uintptr_t)stack + sizeof(stack) - 8) & ~((uintptr_t)0xf)),
                    flags,
                    (void *) pipefds);
  if(child_pid == -1)
    die("clone()", 1);

  if(close(pipefds[0]))
    die("parent closing read pipe end", 2);

  if(snprintf(strbuf, sizeof(strbuf), "/proc/%d/uid_map", child_pid)
     >= sizeof(strbuf))
    die("uid_map snprintf", 3);

  f = fopen(strbuf, "w");
  if(!f) die("uid_map fopen", 4);

  if(snprintf(strbuf, sizeof(strbuf), "%u %u 1", geteuid(), geteuid())
     >= sizeof(strbuf))
    die("uid_map contents snprintf", 5);

  if(fwrite(strbuf, strlen(strbuf) * sizeof(char), 1, f) < 1)
    die("uid_map fwrite", 6);

  if(fclose(f) == EOF)
    die("uid_map fclose", 7);

  if(snprintf(strbuf, sizeof(strbuf), "/proc/%d/setgroups", child_pid)
     >= sizeof(strbuf))
    die("setgroups snprintf", 8);

  f = fopen(strbuf, "w");
  if(!f) die("setgroups fopen", 9);

  if(fwrite("deny", 4 * sizeof(char), 1, f) < 1)
    die("setgroups fwrite", 10);

  if(fclose(f) == EOF)
    die("setgroups fclose", 11);

  if(snprintf(strbuf, sizeof(strbuf), "/proc/%d/gid_map", child_pid)
     >= sizeof(strbuf))
    die("gid_map snprintf", 12);

  f = fopen(strbuf, "w");
  if(!f) die("gid_map fopen", 13);

  if(snprintf(strbuf, sizeof(strbuf), "%u %u 1", getegid(), getegid())
     >= sizeof(strbuf))
    die("gid_map contents snprintf", 14);

  if(fwrite(strbuf, strlen(strbuf) * sizeof(char), 1, f) < 1)
    die("gid_map fwrite", 15);

  if(fclose(f) == EOF)
    die("gid_map fclose", 16);

  if(write(pipefds[1], "y", sizeof("y") - (1 * sizeof(char))) < (1 * sizeof(char)))
    die("writing to pipe", 17);

  if(close(pipefds[1]))
    die("parent closing write pipe end", 18)

  while (1)
    {
      result = waitpid(child_pid, &status, 0);
      if(result == child_pid)
        return WEXITSTATUS(status);
      if(result == -1 && errno != EINTR)
        die("waitpid", 19);
    }
}
--8<---------------cut here---------------end--------------->8---

Here's what it looks like when I run it:

--8<---------------cut here---------------start------------->8---
$ /tmp/test-program
Self-exec'ed!
Arguments: 
???
/proc/self/exe readlink result: /tmp/test-program
fopen /proc/self/exe readlink result error: No such file or directory
... and that file does not exist.
/proc/self/exe can be opened
--8<---------------cut here---------------end--------------->8---

> Here’s a test that fails both “rootfull” and “rootless”:
>
> (let* ((builder (add-file-tree-to-store %store
>                                         `("builder" symlink "/proc/self/exe")))
>        (drv (derivation %store "attempt-to-run-guix-daemon" builder '()
>                         #:env-vars
>                         '(("LD_PRELOAD" . "attacker-controlled.so")))))
>   (guard (c ((store-protocol-error? c) c))
>     (build-derivations %store (list drv))
>     #f))

From 'man 2 execve':

       ENOENT The file pathname or a script *or ELF interpreter* does not exist.

(emphasis mine).  The dynamic linker registered in guix-daemon's binary
is not likely to exist in the container in this test, but an attacker
could easily make it so as long as it's in the store.

> diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
> index 7484ea8fcf..970107c265 100644
> --- a/nix/libstore/build.cc
> +++ b/nix/libstore/build.cc
> @@ -2374,6 +2374,15 @@ 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).  */
> +	    drv.builder = canonPath(drv.builder, true);
> +	    printMsg(lvlError, format("builder is `%1%'") % drv.builder);
> +	    if (!isInStore(drv.builder))
> +		throw Error(format("derivation builder '%1%' is outside the store") % drv.builder);
>  	}
>  
>          /* Fill in the arguments. */

Note that we should still supply the original name or basename as
argv[0].

While ensuring that what actually gets execve'd is in the store suffices
to eliminate the vulnerability, it may be "conceptually purer" to
require that the links pointing to it are all in the store as well.  For
example, while a builder that is a symlink pointing to /proc/self/exe
wouldn't be able to modify the daemon binary, it's still a piece of
basically "undefined behavior" as far as the build environment is
concerned, which could be closed up.  But that can come later just as
well.



One more consideration I noticed when looking at v6's patch 14/16 (the
guix-daemon.service one): we don't do anything to set the gid.  I know
on guix system we usually use both dedicated privilege separation users
and groups for services.  Should we use a dedicated group for
guix-daemon as well?  Note that currently the chroot directories have
0750 permissions, so it's very important that their group not be
accessible to others.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:02 GMT) Full text and rfc822 format available.

Message #343 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 03/16] daemon: Bind-mount /etc/nsswitch.conf & co. only if
 it exists.
Date: Thu, 20 Mar 2025 21:54:36 +0100
Those files may be missing in some contexts, for instance within the
build environment.

* nix/libstore/build.cc (DerivationGoal::runChild): Add /etc/resolv.conf
and related files to ‘ss’ only if they exist.

Change-Id: Ie19664a86c8101a1dc82cf39ad4b7abb10f8250a
---
 nix/libstore/build.cc | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34..8ca5e5b732 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2093,10 +2093,11 @@ void DerivationGoal::runChild()
                network, so give them access to /etc/resolv.conf and so
                on. */
             if (fixedOutput) {
-                ss.push_back("/etc/resolv.conf");
-                ss.push_back("/etc/nsswitch.conf");
-                ss.push_back("/etc/services");
-                ss.push_back("/etc/hosts");
+		auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf",
+			       "/etc/services", "/etc/hosts" };
+		for (auto & file: files) {
+		    if (pathExists(file)) ss.push_back(file);
+		}
             }
 
             for (auto & i : ss) dirsInChroot[i] = i;
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:03 GMT) Full text and rfc822 format available.

Message #346 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 01/16] daemon: Use ‘close_range’ where available.
Date: Thu, 20 Mar 2025 21:54:34 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc3..4e949bc88a 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b..eb2d16e1cc 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:05 GMT) Full text and rfc822 format available.

Message #349 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v7 00/16] Rootless guix-daemon
Date: Thu, 20 Mar 2025 21:54:33 +0100
Hello,

Changes compared to v6 (all suggested by Reepca):

  • Canonicalize the builder’s file name before ‘execve’
    and add the “builder is outside the store” test in
    ‘tests/derivations.scm’ (though the test would already
    succeed before due to the ELF interpreter being unavailable
    in the chroot).

  • Explicitly close both ends of the ‘readiness’ pipe.

  • Use ‘mkdir -p’ to create /var/log/guix in ‘guix-install.sh’.

Thoughts?

Ludo’.

Ludovic Courtès (16):
  daemon: Use ‘close_range’ where available.
  daemon: Close the read end of the logging pipe.
  daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Remount root directory as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  daemon: Move comments where they belong.
  linux-container: ‘unprivileged-user-namespace-supported?’ returns #f
    on non-Linux.
  tests: Add missing derivation inputs.
  tests: Run in a chroot and unprivileged user namespaces.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.
  DRAFT gnu: guix: Update to f447941.

 build-aux/test-env.in               |  18 +-
 config-daemon.ac                    |   5 +-
 doc/guix.texi                       | 102 ++++++++---
 etc/gnu-store.mount.in              |   3 +-
 etc/guix-daemon.service.in          |  22 ++-
 etc/guix-install.sh                 | 124 ++++++++++---
 gnu/build/linux-container.scm       |   4 +-
 gnu/packages/package-management.scm |   6 +-
 guix/substitutes.scm                |   2 +-
 nix/libstore/build.cc               | 271 ++++++++++++++++++++++------
 nix/libstore/local-store.cc         |  26 ++-
 nix/libutil/util.cc                 |  26 ++-
 tests/derivations.scm               |  34 +++-
 tests/packages.scm                  |  13 +-
 tests/processes.scm                 |   9 +-
 tests/store.scm                     | 247 +++++++++++++++++++++----
 16 files changed, 726 insertions(+), 186 deletions(-)


base-commit: cbd2db98954739db1cdda208e1667c5d50976bf1
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:07 GMT) Full text and rfc822 format available.

Message #352 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 06/16] daemon: Remount root directory as read-only.
Date: Thu, 20 Mar 2025 21:54:39 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Bind-mount the store
and /tmp under ‘chrootRootDir’ to themselves as read-write.
Remount / as read-only.

Change-Id: I79565094c8ec8448401897c720aad75304fd1948
---
 nix/libstore/build.cc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 3861a1ffd9..c8b778362a 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2091,6 +2091,18 @@ void DerivationGoal::runChild()
 
             for (auto & i : ss) dirsInChroot[i] = i;
 
+	    /* Make new mounts for the store and for /tmp.  That way, when
+	       'chrootRootDir' is made read-only below, these two mounts will
+	       remain writable (the store needs to be writable so derivation
+	       outputs can be written to it, and /tmp is writable by
+	       convention).  */
+	    auto chrootStoreDir = chrootRootDir + settings.nixStore;
+	    if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir);
+	    auto chrootTmpDir = chrootRootDir + "/tmp";
+	    if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir);
+
             /* Bind-mount all the directories from the "host"
                filesystem that we want in the chroot
                environment. */
@@ -2164,6 +2176,10 @@ void DerivationGoal::runChild()
 
             if (rmdir("real-root") == -1)
                 throw SysError("cannot remove real-root directory");
+
+	    /* 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);
         }
 #endif
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:09 GMT) Full text and rfc822 format available.

Message #355 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v7 02/16] daemon: Close the read end of the logging pipe.
Date: Thu, 20 Mar 2025 21:54:35 +0100
* nix/libutil/util.cc (commonChildInit): Close ‘logPipe.readSide’.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ia9e48d1afb85d7af52770e016f2b6832792044dd
---
 nix/libutil/util.cc | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index eb2d16e1cc..56f116046c 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -1279,6 +1279,9 @@ void commonChildInit(Pipe & logPipe)
     if (setsid() == -1)
         throw SysError(format("creating a new session"));
 
+    /* Close the read end so only the parent holds a reference to it.  */
+    logPipe.readSide.close();
+
     /* Dup the write side of the logger pipe into stderr. */
     if (dup2(logPipe.writeSide, STDERR_FILENO) == -1)
         throw SysError("cannot pipe standard error into log file");
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:10 GMT) Full text and rfc822 format available.

Message #358 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v7 04/16] daemon: Bind-mount all the inputs,
 not just directories.
Date: Thu, 20 Mar 2025 21:54:37 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.
Special-case symlinks.
(DerivationGoal)[regularInputPaths]: Remove.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 39 ++++++++++++++-------------------------
 1 file changed, 14 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 8ca5e5b732..193b279b88 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -659,9 +659,6 @@ private:
     /* RAII object to delete the chroot directory. */
     std::shared_ptr<AutoDelete> autoDelChroot;
 
-    /* All inputs that are regular files. */
-    PathSet regularInputPaths;
-
     /* Whether this is a fixed-output derivation. */
     bool fixedOutput;
 
@@ -1850,9 +1847,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1858,22 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
+	    struct stat st;
             if (lstat(i->c_str(), &st))
                 throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
 
-                regularInputPaths.insert(*i);
-            }
+	    if (S_ISLNK(st.st_mode)) {
+		/* Since bind-mounts follow symlinks, thus representing their
+		   target and not the symlink itself, special-case
+		   symlinks. XXX: When running unprivileged, TARGET can be
+		   deleted by the build process.  Use 'open_tree' & co. when
+		   it's more widely available.  */
+                Path target = chrootRootDir + *i;
+		if (symlink(readLink(*i).c_str(), target.c_str()) == -1)
+		    throw SysError(format("failed to create symlink '%1%' to '%2%'") % target % readLink(*i));
+	    }
+	    else
+		dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:57:11 GMT) Full text and rfc822 format available.

Message #361 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v7 05/16] daemon: Remount inputs as read-only.
Date: Thu, 20 Mar 2025 21:54:38 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 193b279b88..3861a1ffd9 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2107,8 +2107,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:03 GMT) Full text and rfc822 format available.

Message #364 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 09/16] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Thu, 20 Mar 2025 21:54:42 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a..35d9c8cd56 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index e6cd45aba4..fa0f293aac 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2075,6 +2078,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:04 GMT) Full text and rfc822 format available.

Message #367 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 12/16] tests: Add missing derivation inputs.
Date: Thu, 20 Mar 2025 21:54:45 +0100
These missing inputs go unnoticed when running ‘guix-daemon
--disable-chroot’ but are immediately visible otherwise.

* tests/derivations.scm ("fixed-output derivation"): Add %BASH to #:sources.
("fixed-output derivation: output paths are equal"):
("fixed-output derivation, recursive"):
("derivation with a fixed-output input"):
("derivation with duplicate fixed-output inputs"):
("derivation with equivalent fixed-output inputs"):
("build derivation with coreutils"): Likewise.
* tests/packages.scm (bootstrap-binary): New procedure.
("package-source-derivation, origin, sha512"): Use it instead of
‘search-bootstrap-binary’ and add BASH to #:sources.
("package-source-derivation, origin, sha3-512"): Likewise.

Change-Id: I4c9087df23c47729a3aff15e9e1435b7266e36e2
---
 tests/derivations.scm | 24 +++++++++++++++---------
 tests/packages.scm    | 13 +++++++++----
 2 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/tests/derivations.scm b/tests/derivations.scm
index 9ea8b4a300..b364110de0 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -443,7 +443,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                      (string-append
                                       "fixed-" (symbol->string hash-algorithm))
                                      %bash `(,builder)
-                                     #:sources `(,builder) ;optional
+                                     #:sources (list %bash builder)
                                      #:hash hash
                                      #:hash-algo hash-algorithm)))
            (build-derivations %store (list drv))
@@ -462,9 +462,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv1       (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (drv2       (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (succeeded? (build-derivations %store (list drv1 drv2))))
     (and succeeded?
@@ -477,7 +479,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv        (derivation %store "fixed-rec"
                                  %bash `(,builder)
-                                 #:sources (list builder)
+                                 #:sources (list %bash builder)
                                  #:hash (base32 "0sg9f58l1jj88w6pdrfdpj5x9b1zrwszk84j81zvby36q9whhhqa")
                                  #:hash-algo 'sha256
                                  #:recursive? #t))
@@ -511,9 +513,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed-out  (derivation->output-path fixed1))
          (builder3   (add-text-to-store
@@ -548,9 +552,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (builder3   (add-text-to-store %store "builder.sh"
                                         "echo fake builder"))
@@ -580,21 +586,21 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                       '()))
          (hash     (gcrypt:sha256 (string->utf8 "hello")))
          (drv1     (derivation %store "fixed" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:hash hash #:hash-algo 'sha256))
          (drv2     (derivation %store "fixed" %bash (list builder2)
-                               #:sources (list builder2)
+                               #:sources (list %bash builder2)
                                #:hash hash #:hash-algo 'sha256))
          (drv3a    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv1))))
          (drv3b    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv2))))
          (drv4     (derivation %store "fixed-user-user" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:inputs (list (derivation-input drv3a '("one"))
                                               (derivation-input drv3b '("two"))))))
     (match (derivation-inputs drv4)
@@ -888,7 +894,7 @@ (define %coreutils
                                     ,(string-append
                                       (derivation->output-path %coreutils)
                                       "/bin")))
-                      #:sources (list builder)
+                      #:sources (list %bash builder)
                       #:inputs (list (derivation-input %coreutils))))
          (succeeded?
           (build-derivations %store (list drv))))
diff --git a/tests/packages.scm b/tests/packages.scm
index 50c1cab915..f56c63128d 100644
--- a/tests/packages.scm
+++ b/tests/packages.scm
@@ -80,6 +80,11 @@ (define %store
 ;; When grafting, do not add dependency on 'glibc-utf8-locales'.
 (%graft-with-utf8-locale? #f)
 
+(define (bootstrap-binary name)
+  (let ((bin (search-bootstrap-binary name (%current-system))))
+    (and %store
+         (add-to-store %store name #t "sha256" bin))))
+
 
 (test-begin "packages")
 
@@ -609,14 +614,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha512"
   "hello"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
@@ -635,14 +640,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha3-512"
   "hello, sha3"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello, sha3 > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha3-512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:04 GMT) Full text and rfc822 format available.

Message #370 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v7 11/16] linux-container: ‘unprivileged-user-namespace-supported?’ returns #f on non-Linux.
Date: Thu, 20 Mar 2025 21:54:44 +0100
Previously this procedure would return #t on non-Linux systems.

* gnu/build/linux-container.scm (unprivileged-user-namespace-supported?):
When USERNS-FILE doesn’t exist, return (user-namespace-supported?).

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I92050338b8b68bc3bd87100317eba69fcdf14a0a
---
 gnu/build/linux-container.scm | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm
index 5c303da8c8..a5c5d8962e 100644
--- a/gnu/build/linux-container.scm
+++ b/gnu/build/linux-container.scm
@@ -1,6 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2015 David Thompson <davet <at> gnu.org>
-;;; Copyright © 2017-2019, 2022, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2017-2019, 2022-2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -44,7 +44,7 @@ (define (unprivileged-user-namespace-supported?)
   (let ((userns-file "/proc/sys/kernel/unprivileged_userns_clone"))
     (if (file-exists? userns-file)
         (eqv? #\1 (call-with-input-file userns-file read-char))
-        #t)))
+        (user-namespace-supported?))))
 
 (define (setgroups-supported?)
   "Return #t if the setgroups proc file, introduced in Linux-libre 3.19,
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:05 GMT) Full text and rfc822 format available.

Message #373 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 14/16] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Thu, 20 Mar 2025 21:54:47 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(Environment): Add ‘GUIX_STATE_DIRECTORY’.
(Before, User, AmbientCapabilities, PrivateMounts, BindPaths): New fields.
* etc/gnu-store.mount.in (Before): Remove.
(WantedBy): Change to ‘multi-user.target’.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/gnu-store.mount.in     |  3 +--
 etc/guix-daemon.service.in | 22 ++++++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/etc/gnu-store.mount.in b/etc/gnu-store.mount.in
index c94f2db72b..f9918c9e52 100644
--- a/etc/gnu-store.mount.in
+++ b/etc/gnu-store.mount.in
@@ -2,10 +2,9 @@
 Description=Read-only @storedir@ for GNU Guix
 DefaultDependencies=no
 ConditionPathExists=@storedir@
-Before=guix-daemon.service
 
 [Install]
-WantedBy=guix-daemon.service
+WantedBy=multi-user.target
 
 [Mount]
 What=@storedir@
diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1..6a5ef97f9b 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -5,11 +5,29 @@
 [Unit]
 Description=Build daemon for GNU Guix
 
+# Start before 'gnu-store.mount' to get a writable view of the store.
+Before=gnu-store.mount
+
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
-Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+Environment='GUIX_STATE_DIRECTORY=@localstatedir@/guix' 'GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Bind-mount the store read-write in a private namespace, to counter the
+# effect of 'gnu-store.mount'.
+PrivateMounts=true
+BindPaths=@storedir@
+
+# Provide the CAP_CHOWN capability so that guix-daemon can create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, maxim.cournoyer <at> gmail.com, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:06 GMT) Full text and rfc822 format available.

Message #376 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v7 07/16] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Thu, 20 Mar 2025 21:54:40 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

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 <at> russelstein.xyz>
---
 doc/guix.texi               | 102 +++++++++++++++-----
 guix/substitutes.scm        |   2 +-
 nix/libstore/build.cc       | 180 +++++++++++++++++++++++++++++++-----
 nix/libstore/local-store.cc |  18 +++-
 tests/derivations.scm       |  10 ++
 5 files changed, 257 insertions(+), 55 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index fe43ed2504..ab9e21e42e 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -877,6 +877,7 @@ Setting Up the Daemon
 @section Setting Up the Daemon
 
 @cindex daemon
+@cindex build daemon
 During installation, the @dfn{build daemon} that must be running
 to use Guix has already been set up and you can run @command{guix}
 commands in your terminal program, @pxref{Getting Started}:
@@ -921,20 +922,38 @@ Build Environment Setup
 @cindex build environment
 In a standard multi-user setup, Guix and its daemon---the
 @command{guix-daemon} program---are installed by the system
-administrator; @file{/gnu/store} is owned by @code{root} and
-@command{guix-daemon} runs as @code{root}.  Unprivileged users may use
-Guix tools to build packages or otherwise access the store, and the
-daemon will do it on their behalf, ensuring that the store is kept in a
-consistent state, and allowing built packages to be shared among users.
+administrator.  Unprivileged users may use Guix tools to build packages
+or otherwise access the store, and the daemon will do it on their
+behalf, ensuring that the store is kept in a consistent state, and
+allowing built packages to be shared among users.
+
+There are currently two ways to set up and run the build daemon:
+
+@enumerate
+@item
+running @command{guix-daemon} as ``root'', letting it run build
+processes as unprivileged users taken from a pool of build users---this
+is the historical approach;
+
+@item
+running @command{guix-daemon} as a separate unprivileged user, relying
+on Linux's @dfn{unprivileged user namespace} functionality to set up
+isolated environments---this is the option chosen when installing Guix
+on a systemd-based distribution with the installation script
+(@pxref{Binary Installation}).
+@end enumerate
+
+The sections below describe each of these two configurations in more
+detail and summarize the kind of build isolation they provide.
+
+@unnumberedsubsubsec Daemon Running as Root
 
 @cindex build users
 When @command{guix-daemon} runs as @code{root}, you may not want package
 build processes themselves to run as @code{root} too, for obvious
 security reasons.  To avoid that, a special pool of @dfn{build users}
 should be created for use by build processes started by the daemon.
-These build users need not have a shell and a home directory: they will
-just be used when the daemon drops @code{root} privileges in build
-processes.  Having several such users allows the daemon to launch
+Having several such users allows the daemon to launch
 distinct build processes under separate UIDs, which guarantees that they
 do not interfere with each other---an essential feature since builds are
 regarded as pure functions (@pxref{Introduction}).
@@ -977,11 +996,45 @@ Build Environment Setup
 # guix-daemon --build-users-group=guixbuild
 @end example
 
+In this setup, @file{/gnu/store} is owned by @code{root}.
+
+@unnumberedsubsubsec Daemon Running Without Privileges
+
+@cindex rootless build daemon
+@cindex unprivileged build daemon
+@cindex build daemon, unprivileged
+The second and preferred option is to run @command{guix-daemon}
+@emph{as an unprivileged user}.  It has the advantage of reducing the
+harm that can be done should a build process manage to exploit a
+vulnerability in the daemon.  This option requires the use of Linux's
+unprivileged user namespace mechanism; today it is available and enabled
+by most GNU/Linux distributions but can still be disabled.  The
+installation script automatically determines whether this option is
+available on your system (@pxref{Binary Installation}).
+
+When using this option, you only need to create one user account, and
+@command{guix-daemon} will run with the authority of that account:
+
+@example
+# groupadd --system guix-daemon
+# useradd -g guix-daemon -G guix-daemon              \
+          -d /var/empty -s $(which nologin)          \
+          -c "Guix daemon privilege separation user" \
+          --system guix-daemon
+@end example
+
+In this configuration, @file{/gnu/store} is owned by the
+@code{guix-daemon} user.
+
+@unnumberedsubsubsec The Isolated Build Environment
+
 @cindex chroot
-@noindent
-This way, the daemon starts build processes in a chroot, under one of
-the @code{guixbuilder} users.  On GNU/Linux, by default, the chroot
-environment contains nothing but:
+@cindex build environment isolation
+@cindex isolated build environment
+@cindex hermetic build environment
+In both cases, the daemon starts build processes without privileges in
+an @emph{isolated} or @emph{hermetic} build environment---a ``chroot''.
+On GNU/Linux, by default, the build environment contains nothing but:
 
 @c Keep this list in sync with libstore/build.cc! -----------------------
 @itemize
@@ -1015,7 +1068,7 @@ Build Environment Setup
 @file{/homeless-shelter}.  This helps to highlight inappropriate uses of
 @env{HOME} in the build scripts of packages.
 
-All this usually enough to ensure details of the environment do not
+All this is usually enough to ensure details of the environment do not
 influence build processes.  In some exceptional cases where more control
 is needed---typically over the date, kernel, or CPU---you can resort to
 a virtual build machine (@pxref{build-vm, virtual build machines}).
@@ -1035,14 +1088,6 @@ Build Environment Setup
 for fixed-output derivations (@pxref{Derivations}) or for substitutes
 (@pxref{Substitutes}).
 
-If you are installing Guix as an unprivileged user, it is still possible
-to run @command{guix-daemon} provided you pass @option{--disable-chroot}.
-However, build processes will not be isolated from one another, and not
-from the rest of the system.  Thus, build processes may interfere with
-each other, and may access programs, libraries, and other files
-available on the system---making it much harder to view them as
-@emph{pure} functions.
-
 
 @node Daemon Offload Setup
 @subsection Using the Offload Facility
@@ -1567,10 +1612,17 @@ Invoking guix-daemon
 @item --disable-chroot
 Disable chroot builds.
 
-Using this option is not recommended since, again, it would allow build
-processes to gain access to undeclared dependencies.  It is necessary,
-though, when @command{guix-daemon} is running under an unprivileged user
-account.
+@quotation Warning
+Using this option is not recommended since it allows build processes to
+gain access to undeclared dependencies, to interfere with one another,
+and more generally to do anything that can be done with the authority of
+build users or that of the daemon---which includes at least the ability
+to tamper with any file in the store!
+
+You may find it necessary, though, when support for Linux unprivileged
+user namespaces is missing (@pxref{Build Environment Setup}).  Use at
+your own risk!
+@end quotation
 
 @item --log-compression=@var{type}
 Compress build logs according to @var{type}, one of @code{gzip},
diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index 7ca55788d5..86b9f5472a 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -79,7 +79,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c8b778362a..e6cd45aba4 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -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,25 @@ 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);
+	}
 
         /* 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 +2570,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 +2838,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());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbce..83e6c3e16e 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -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);
+	}
+    }
 }
 
 
diff --git a/tests/derivations.scm b/tests/derivations.scm
index 72ea9aa9cc..9ea8b4a300 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -858,6 +858,16 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (call-with-input-file (derivation->output-path drv)
            get-string-all))))
 
+(test-assert "builder is outside the store"
+  (let* ((builder (add-file-tree-to-store %store
+                                          `("builder" symlink "/proc/self/exe")))
+         (drv (derivation %store "attempt-to-run-guix-daemon" builder '()
+                          #:env-vars
+                          '(("LD_PRELOAD" . "attacker-controlled.so")))))
+    (guard (c ((store-protocol-error? c) c))
+      (build-derivations %store (list drv))
+      #f)))
+
 
 (define %coreutils
   (false-if-exception
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:06 GMT) Full text and rfc822 format available.

Message #379 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 08/16] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Thu, 20 Mar 2025 21:54:41 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 83e6c3e16e..f6540c2117 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:07 GMT) Full text and rfc822 format available.

Message #382 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 10/16] daemon: Move comments where they belong.
Date: Thu, 20 Mar 2025 21:54:43 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Shuffle
comments for clarity.

Change-Id: I6557c103ade4a3ab046354548ea193c68f8c9c05
---
 nix/libstore/build.cc | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index fa0f293aac..1733322316 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1870,18 +1870,19 @@ void DerivationGoal::startBuilder()
         }
         dirsInChroot[tmpDirInSandbox] = tmpDir;
 
-        /* Make the closure of the inputs available in the chroot,
-           rather than the whole store.  This prevents any access
-           to undeclared dependencies.  !!! As an extra security
-           precaution, make the fake store only writable by the
-           build user. */
+	/* Create the fake store.  */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
-            throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
+	     /* As an extra security precaution, make the fake store only
+		writable by the build user.  */
+	     throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
+        /* Make the closure of the inputs available in the chroot, rather than
+           the whole store.  This prevents any access to undeclared
+           dependencies. */
         foreach (PathSet::iterator, i, inputPaths) {
 	    struct stat st;
             if (lstat(i->c_str(), &st))
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:07 GMT) Full text and rfc822 format available.

Message #385 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 13/16] tests: Run in a chroot and unprivileged user
 namespaces.
Date: Thu, 20 Mar 2025 21:54:46 +0100
* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking and warn in that case.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("symlink is symlink")
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write")
("build root cannot be made world-readable")
("/tmp, store, and /dev/{null,full} are writable")
("network is unreachable"): New tests.
* tests/processes.scm ("client + lock"): Skip when
‘unprivileged-user-namespace-supported?’ returns true.

Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84
---
 build-aux/test-env.in |  18 ++-
 tests/processes.scm   |   9 +-
 tests/store.scm       | 247 ++++++++++++++++++++++++++++++++++++------
 3 files changed, 236 insertions(+), 38 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da58..86c2e585d7 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,24 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ -f /proc/self/ns/user ] \
+       && { [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; }
+    then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+	echo "unprivileged user namespaces not supported; \
+running 'guix-daemon $extra_options'" >&2
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/processes.scm b/tests/processes.scm
index ba518f2d9e..a72ba16f58 100644
--- a/tests/processes.scm
+++ b/tests/processes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2018 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2019 Mathieu Othacehe <m.othacehe <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -25,6 +25,8 @@ (define-module (test-processes)
   #:use-module (guix gexp)
   #:use-module ((guix utils) #:select (call-with-temporary-directory))
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix tests)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-64)
@@ -84,6 +86,11 @@ (define-syntax-rule (test-assert* description exp)
       (and (kill (process-id daemon) 0)
            (string-suffix? "guix-daemon" (first (process-command daemon)))))))
 
+(when (unprivileged-user-namespace-supported?)
+  ;; The test below assumes the build process can communicate with the outside
+  ;; world via the TOKEN1 and TOKEN2 files, which is impossible when
+  ;; guix-daemon is set up to build in separate namespaces.
+  (test-skip 1))
 (test-assert* "client + lock"
   (with-store store
     (call-with-temporary-directory
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f43..b1ddff2082 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,188 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(test-assert "symlink is symlink"
+  (let* ((a (add-text-to-store %store "hello.txt" (random-text)))
+         (b (build-expression->derivation
+             %store "symlink"
+             '(symlink (assoc-ref %build-inputs "a") %output)
+             #:inputs `(("a" ,a))))
+         (c (build-expression->derivation
+             %store "symlink-reference"
+             `(call-with-output-file %output
+                (lambda (port)
+                  ;; Check that B is indeed visible as a symlink.  This should
+                  ;; always be the case, both in the '--disable-chroot' and in
+                  ;; the user namespace setups.
+                  (pk 'stat (lstat (assoc-ref %build-inputs "b")))
+                  (display (readlink (assoc-ref %build-inputs "b"))
+                           port)))
+             #:inputs `(("b" ,b)))))
+    (and (build-derivations %store (list c))
+         (string=? (call-with-input-file (derivation->output-path c)
+                     get-string-all)
+                   a))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-write-to-input"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "build root cannot be made world-readable"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-make-root-world-readable"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (catch 'system-error
+                    (lambda ()
+                      (chmod "/" #o777))
+                    (lambda args
+                      (format #t "failed to make root writable: ~a~%"
+                              (strerror (system-error-errno args)))
+                      (format #t "attempting read-write remount~%")
+                      (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
+                      (chmod "/" #o777)))
+
+                  ;; At this point, the build process could create a
+                  ;; world-readable setuid binary under its root (so in the
+                  ;; store) that would remain visible until the build
+                  ;; completes.
+                  (mkdir #$output)))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "/tmp, store, and /dev/{null,full} are writable"
+  ;; All of /tmp and all of the store must be writable (the store is writable
+  ;; so that derivation outputs can be written to it, but in practice it's
+  ;; always been wide open).  Things like /dev/null must be writable too.
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-tmp-and-store-are-writable"
+                #~(begin
+                    (mkdir "/tmp/something")
+                    (mkdir (in-vicinity (getenv "NIX_STORE")
+                                        "some-other-thing"))
+                    (call-with-output-file "/dev/null"
+                      (lambda (port)
+                        (display "Welcome to the void." port)))
+                    (catch 'system-error
+                      (lambda ()
+                        (call-with-output-file "/dev/full"
+                          (lambda (port)
+                            (display "No space left!" port)))
+                        (error "Should have thrown!"))
+                      (lambda args
+                        (unless (= ENOSPC (system-error-errno args))
+                          (apply throw args))))
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "network is unreachable"
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-network-unreachable"
+                #~(let ((check-connection-failure
+                         (lambda (address expected-code)
+                           (let ((s (socket AF_INET SOCK_STREAM 0)))
+                             (catch 'system-error
+                               (lambda ()
+                                 (connect s AF_INET (inet-pton AF_INET address) 80))
+                               (lambda args
+                                 (let ((errno (system-error-errno args)))
+                                   (unless (= expected-code errno)
+                                     (error "wrong error code"
+                                            errno (strerror errno))))))))))
+                    (check-connection-failure "127.0.0.1" ECONNREFUSED)
+                    (check-connection-failure "9.9.9.9" ENETUNREACH)
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1519,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:08 GMT) Full text and rfc822 format available.

Message #388 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 15/16] guix-install.sh: Support the unprivileged daemon
 where possible.
Date: Thu, 20 Mar 2025 21:54:48 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 124 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 99 insertions(+), 25 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index 8887204df4..b5d833cd64 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -414,6 +414,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg_info "Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg_info "Linking the root user's profile"
@@ -441,38 +446,95 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg_info "user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg_pass "user added <$user>"
+    fi
+}
+
+install_unprivileged_daemon()
+{ # Return true when installing guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg_info "group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg_pass "group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg_info "group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg_info "user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg_pass "user added <guixbuilder${i}>"
-        fi
-    done
+    if install_unprivileged_daemon
+    then
+	_msg_info "installing guix-daemon to run as an unprivileged user"
+
+	# Installing guix-daemon to run as a non-root user requires
+	# unprivileged user namespaces.
+	if [ -f /proc/sys/kernel/unprivileged_userns_clone ] \
+	       && [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -ne 1 ]
+	then
+	    echo 1 > /proc/sys/kernel/unprivileged_userns_clone || \
+		_err "failed to enable unprivileged user namespaces"
+
+	    _msg_warn "Unprivileged user namespaces were disabled and have been enabled now."
+	    _msg_warn "This Linux feature is required by guix-daemon.  To enable it permanently, run:"
+	    _msg_warn '  echo 1 > /proc/sys/kernel/unprivileged_userns_clone'
+	    _msg_warn "from the relevant startup script."
+	fi
+
+
+	if getent group guix-daemon > /dev/null; then
+	    _msg_info "group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg_pass "group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon /gnu /var/guix
+	chown -R root:root /var/guix/profiles/per-user/root
+
+	# The unprivileged daemon cannot create the log directory by itself.
+	mkdir -p /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg_info "group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg_pass "group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -487,6 +549,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg_info "remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -529,11 +599,11 @@ sys_enable_guix_daemon()
 
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
-              # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      install_unit gnu-store.mount
 
               systemctl daemon-reload &&
-                  systemctl start  guix-daemon; } &&
+                  systemctl start guix-daemon &&
+	          systemctl start gnu-store.mount; } &&
                 _msg_pass "enabled Guix daemon via systemd"
             ;;
         sysv-init)
@@ -654,6 +724,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg_pass "Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg_info "Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:58:08 GMT) Full text and rfc822 format available.

Message #391 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v7 16/16] DRAFT gnu: guix: Update to f447941.
Date: Thu, 20 Mar 2025 21:54:49 +0100
DRAFT: Temporary commit.

* gnu/packages/package-management.scm (guix): Update to f447941.

Change-Id: I16b10e721b98e8721bf206c3b3824407147d9649
---
 gnu/packages/package-management.scm | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/gnu/packages/package-management.scm b/gnu/packages/package-management.scm
index a4a96878f7..a5d5083993 100644
--- a/gnu/packages/package-management.scm
+++ b/gnu/packages/package-management.scm
@@ -179,8 +179,8 @@ (define-public guix
   ;; Note: the 'update-guix-package.scm' script expects this definition to
   ;; start precisely like this.
   (let ((version "1.4.0")
-        (commit "5058b40aba825ab6e7b9e518dd1147d1e35fd7de")
-        (revision 34))
+        (commit "f447941a9c03769bfd17d3193a5aaad32342da53")
+        (revision 35))
     (package
       (name "guix")
 
@@ -196,7 +196,7 @@ (define-public guix
                       (commit commit)))
                 (sha256
                  (base32
-                  "04vk4lslcd6h22yj5pxvb1pdyyxd8421gjfyvyb1bl3xn7c77246"))
+                  "10id738y2cpg74jjfz8i2k8phw5lgbz91zx0nl0109z0ag0har34"))
                 (file-name (string-append "guix-" version "-checkout"))))
       (build-system gnu-build-system)
       (arguments
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Thu, 20 Mar 2025 20:59:02 GMT) Full text and rfc822 format available.

Message #394 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Thu, 20 Mar 2025 21:57:44 +0100
Hi,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

>> The visible changes in the build environment are unfortunate; I’m hoping
>> they won’t have any practical impact, not any more than the other
>> parameters that may change currently (build UID, binfmt_misc, file
>> system, etc.)  We could test this hypothesis by rebuilding at least the
>> entire set of packages up to ‘hello’.  (I tried doing it just now in a
>> Debian VM but failed since the main partition cannot easily be extended;
>> it’ll be easier to do with Guix System.)
>>
>
> For what it's worth, the visible changes could be avoided with
> subordinate ids, as I wrote in
> https://debbugs.gnu.org/cgi/bugreport.cgi?bug=75810#86.

Yes.  The newuidmap/newgidmap mechanism looks like a hack to me so I’m
reluctant, but if we have to, we could take that route.

> We could try it as-is and see how it goes, and if there are problems
> with reproducibility add on using subordinate ids.  I would expect it to
> be a much smaller change than the root->rootless transition.

Yes.

>>   1. If /proc/self/exe points to (say) /usr/bin/guix-daemon, outside the
>>      store, execve fails with ENOENT because that file is not mounted in
>>      the chroot.
>
> No, like I wrote, /proc/self/exe, despite being reported as a symlink by
> stat, does not follow the usual symlink semantics.  This is much like
> how the files in /proc/self/fd work, e.g.:

Oh right; apologies, and thanks for taking the time to (re)explain and
to come up with a reproducer.

> From 'man 2 execve':
>
>        ENOENT The file pathname or a script *or ELF interpreter* does not exist.
>
> (emphasis mine).  The dynamic linker registered in guix-daemon's binary
> is not likely to exist in the container in this test, but an attacker
> could easily make it so as long as it's in the store.

Yes.  And it wouldn’t be so hard when one is running guix-daemon from
the store (from the ‘guix’ package or from ‘guix pull’).

>> +	    drv.builder = canonPath(drv.builder, true);
>> +	    printMsg(lvlError, format("builder is `%1%'") % drv.builder);
>> +	    if (!isInStore(drv.builder))
>> +		throw Error(format("derivation builder '%1%' is outside the store") % drv.builder);
>>  	}
>>  
>>          /* Fill in the arguments. */
>
> Note that we should still supply the original name or basename as
> argv[0].

Right, noted!

> While ensuring that what actually gets execve'd is in the store suffices
> to eliminate the vulnerability, it may be "conceptually purer" to
> require that the links pointing to it are all in the store as well.  For
> example, while a builder that is a symlink pointing to /proc/self/exe
> wouldn't be able to modify the daemon binary, it's still a piece of
> basically "undefined behavior" as far as the build environment is
> concerned, which could be closed up.  But that can come later just as
> well.

Yes.  But in practice, “normal” symlinks (i.e., not /proc/self/exe) will
lead ‘canonPath’ to throw if one component is outside the store, since
‘canonPath’ operates within the chroot.

> One more consideration I noticed when looking at v6's patch 14/16 (the
> guix-daemon.service one): we don't do anything to set the gid.  I know
> on guix system we usually use both dedicated privilege separation users
> and groups for services.  Should we use a dedicated group for
> guix-daemon as well?  Note that currently the chroot directories have
> 0750 permissions, so it's very important that their group not be
> accessible to others.

Quoth
<https://www.freedesktop.org/software/systemd/man/latest/systemd.exec.html#User=>:

  User=, Group=
    Set the UNIX user or group that the processes are executed as,
    respectively. […] If no group is set, the default group of the user
    is used.

Since we don’t have ‘Group=’ in the .service file, the daemon runs as
the group of ‘guix-daemon’, also called ‘guix-daemon’ (created by
‘guix-install.sh’).

I confirmed in a VM that the process is indeed running as
guix-daemon:guix-daemon.

I’ve just sent v7 with the ‘canonPath’ change discussed above.

Thank you!

Ludo’.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Fri, 21 Mar 2025 20:07:02 GMT) Full text and rfc822 format available.

Message #397 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Fri, 21 Mar 2025 14:21:06 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

>> While ensuring that what actually gets execve'd is in the store suffices
>> to eliminate the vulnerability, it may be "conceptually purer" to
>> require that the links pointing to it are all in the store as well.  For
>> example, while a builder that is a symlink pointing to /proc/self/exe
>> wouldn't be able to modify the daemon binary, it's still a piece of
>> basically "undefined behavior" as far as the build environment is
>> concerned, which could be closed up.  But that can come later just as
>> well.
>
> Yes.  But in practice, “normal” symlinks (i.e., not /proc/self/exe) will
> lead ‘canonPath’ to throw if one component is outside the store, since
> ‘canonPath’ operates within the chroot.

Unless the component actually exists and is outside of the store.  If we
just rely on canonPath throwing an exception to be safe, then if there
ever arose a situation where a non-symlink executable existed outside of
the store, it would still be possible to convince the daemon to execute
it.

For example, suppose Linus Torvalds wakes up one day and decides "you
know what, it really is wrong that we're lying about /proc/self/exe
being a symlink" and changes it so that lstat reports a regular file (or
a special file, really anything other than a symlink) that happens to be
a valid argument to readlink.  Or suppose in the interest of
backwards-compatibility he instead adds /proc/self/exe-hardlink.
canonPath won't throw if a symlink points to these hypothetical files,
and the daemon would execute them.  A similar situation could also
happen if an executable showed up in /dev somehow.

I mention this because I see that patch 07/16 of v7 has left out the
isInStore check, and I think it should remain.


> Since we don’t have ‘Group=’ in the .service file, the daemon runs as
> the group of ‘guix-daemon’, also called ‘guix-daemon’ (created by
> ‘guix-install.sh’).
>
> I confirmed in a VM that the process is indeed running as
> guix-daemon:guix-daemon.

Great.


While researching container escape vulnerabilities, I recently came
across CAP_DAC_READ_SEARCH and open_by_handle_at, which is a system call
so insanely powerful it is outright banned in all but the root user
namespace.  Or at least, it was.  10 months ago, in commit
620c266f394932e5decc4b34683a75dfc59dc2f4 of
https://github.com/torvalds/linux, the requirements were relaxed so
that, in certain cases, processes in non-root user namespaces could use
open_by_handle_at.

The consequences of this for same-user containers are not clear to me
yet, as I haven't studied the kernel source enough to know what exactly
that commit message means by "privileges over the filesystem" or
"privileges over a subtree".  I also haven't been able to test this
behavior yet, because my kernel is actually too old (I do my rebases and
upgrades rather less regularly than is recommended).  I'll try to look
into this more once I update my system (and man-pages!), but figured I
should mention it, because aside from that, and the aforementioned
isInStore check, I can't think of any remaining concerns.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 22 Mar 2025 15:59:01 GMT) Full text and rfc822 format available.

Message #400 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Sat, 22 Mar 2025 16:57:45 +0100
[Message part 1 (text/plain, inline)]
Hi Reepca,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> Ludovic Courtès <ludo <at> gnu.org> writes:
>
>>> While ensuring that what actually gets execve'd is in the store suffices
>>> to eliminate the vulnerability, it may be "conceptually purer" to
>>> require that the links pointing to it are all in the store as well.  For
>>> example, while a builder that is a symlink pointing to /proc/self/exe
>>> wouldn't be able to modify the daemon binary, it's still a piece of
>>> basically "undefined behavior" as far as the build environment is
>>> concerned, which could be closed up.  But that can come later just as
>>> well.
>>
>> Yes.  But in practice, “normal” symlinks (i.e., not /proc/self/exe) will
>> lead ‘canonPath’ to throw if one component is outside the store, since
>> ‘canonPath’ operates within the chroot.
>
> Unless the component actually exists and is outside of the store.  If we
> just rely on canonPath throwing an exception to be safe, then if there
> ever arose a situation where a non-symlink executable existed outside of
> the store, it would still be possible to convince the daemon to execute
> it.

[...]

> I mention this because I see that patch 07/16 of v7 has left out the
> isInStore check, and I think it should remain.

Hmm right (I was very much assuming that /proc/self/exe was the only
non-store executable, but better be safe than sorry).  Re-adding this:

[Message part 2 (text/x-patch, inline)]
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 1733322316..d0fcc99854 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2390,6 +2390,9 @@ void DerivationGoal::runChild()
 	       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. */
[Message part 3 (text/plain, inline)]
> While researching container escape vulnerabilities, I recently came
> across CAP_DAC_READ_SEARCH and open_by_handle_at, which is a system call
> so insanely powerful it is outright banned in all but the root user
> namespace.  Or at least, it was.  10 months ago, in commit
> 620c266f394932e5decc4b34683a75dfc59dc2f4 of
> https://github.com/torvalds/linux, the requirements were relaxed so
> that, in certain cases, processes in non-root user namespaces could use
> open_by_handle_at.

The way ‘open_by_handle_at’ is documented (“half” of ‘openat’) does not
make it immediately obvious to me what makes it “powerful”.  I see the
risk of a confused deputy problem though because of the ‘mount_id’
argument in addition to ‘handle’.  Is that what you have in mind?

> The consequences of this for same-user containers are not clear to me
> yet, as I haven't studied the kernel source enough to know what exactly
> that commit message means by "privileges over the filesystem" or
> "privileges over a subtree".  I also haven't been able to test this
> behavior yet, because my kernel is actually too old (I do my rebases and
> upgrades rather less regularly than is recommended).  I'll try to look
> into this more once I update my system (and man-pages!), but figured I
> should mention it, because aside from that, and the aforementioned
> isInStore check, I can't think of any remaining concerns.

Alright.  I’ll send v8 with the change above.

Thanks again!

Ludo’.

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sat, 22 Mar 2025 21:49:02 GMT) Full text and rfc822 format available.

Message #403 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Sat, 22 Mar 2025 16:48:02 -0500
[Message part 1 (text/plain, inline)]
Ludovic Courtès <ludo <at> gnu.org> writes:

>> While researching container escape vulnerabilities, I recently came
>> across CAP_DAC_READ_SEARCH and open_by_handle_at, which is a system call
>> so insanely powerful it is outright banned in all but the root user
>> namespace.  Or at least, it was.  10 months ago, in commit
>> 620c266f394932e5decc4b34683a75dfc59dc2f4 of
>> https://github.com/torvalds/linux, the requirements were relaxed so
>> that, in certain cases, processes in non-root user namespaces could use
>> open_by_handle_at.
>
> The way ‘open_by_handle_at’ is documented (“half” of ‘openat’) does not
> make it immediately obvious to me what makes it “powerful”.  I see the
> risk of a confused deputy problem though because of the ‘mount_id’
> argument in addition to ‘handle’.  Is that what you have in mind?

The handle is a purely user-space sequence of bytes, and is not
namespaced whatsoever.  In other words, the first "half" (that is,
name_to_handle_at) is completely optional, as long as you have a good
idea of what sort of handle values to try.  This means that, if a
process has this capability in the root user namespace, they can
potentially access every file of any filesystem that has at least one
file visible to them.  Note that "filesystem" here is not the same thing
as "mount point", so this means that if you have a bind mount from the
root filesystem in the container (or the root filesystem itself in the
container is on the out-of-container root filesystem), a process in the
container but with CAP_DAC_READ_SEARCH in the root user namespace could
access *every file on the real root filesystem*.  This is how an exploit
for Docker named "shocker" worked
(http://stealth.openwall.net/xSports/shocker.c), caused by Docker
leaving CAP_DAC_READ_SEARCH available by default in privileged
containers.

I of course hope that the kernel's relaxing of the rules to also allow
open_by_handle_at in some situations in non-root user namespaces has
been carefully thought through to not open any holes like this, but it
would be good to keep an eye on it regardless.

- reepca
[signature.asc (application/pgp-signature, inline)]

Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:06 GMT) Full text and rfc822 format available.

Message #406 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 00/16] Rootless guix-daemon
Date: Sun, 23 Mar 2025 15:24:53 +0100
Hello,

Changes since v7, both in the patch entitled “daemon: Allow
running as non-root […]”:

  • Check “isInStore(drv.builder)” before calling ‘execve’, as
    suggested by Reepca.

  • Add comment in “builder is outside the store” test in
    ‘tests/derivations.scm’.

I believe these were the last outstanding issues.  I’ll merge it
in the coming days if there are no objections.

Thanks,
Ludo’.

Ludovic Courtès (16):
  daemon: Use ‘close_range’ where available.
  daemon: Close the read end of the logging pipe.
  daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
  daemon: Bind-mount all the inputs, not just directories.
  daemon: Remount inputs as read-only.
  daemon: Remount root directory as read-only.
  daemon: Allow running as non-root with unprivileged user namespaces.
  daemon: Create /var/guix/profiles/per-user unconditionally.
  daemon: Drop Linux ambient capabilities before executing builder.
  daemon: Move comments where they belong.
  linux-container: ‘unprivileged-user-namespace-supported?’ returns #f
    on non-Linux.
  tests: Add missing derivation inputs.
  tests: Run in a chroot and unprivileged user namespaces.
  etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  guix-install.sh: Support the unprivileged daemon where possible.
  DRAFT gnu: guix: Update to c9c7f87.

 build-aux/test-env.in               |  18 +-
 config-daemon.ac                    |   5 +-
 doc/guix.texi                       | 102 ++++++++---
 etc/gnu-store.mount.in              |   3 +-
 etc/guix-daemon.service.in          |  22 ++-
 etc/guix-install.sh                 | 124 ++++++++++---
 gnu/build/linux-container.scm       |   4 +-
 gnu/packages/package-management.scm |   6 +-
 guix/substitutes.scm                |   2 +-
 nix/libstore/build.cc               | 274 ++++++++++++++++++++++------
 nix/libstore/local-store.cc         |  26 ++-
 nix/libutil/util.cc                 |  26 ++-
 tests/derivations.scm               |  38 +++-
 tests/packages.scm                  |  13 +-
 tests/processes.scm                 |   9 +-
 tests/store.scm                     | 247 +++++++++++++++++++++----
 16 files changed, 733 insertions(+), 186 deletions(-)


base-commit: efac1498c15198afc4f9a2bc700408bde1b3b3ed
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:09 GMT) Full text and rfc822 format available.

Message #409 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 01/16] daemon: Use ‘close_range’ where available.
Date: Sun, 23 Mar 2025 15:24:54 +0100
* nix/libutil/util.cc (closeMostFDs) [HAVE_CLOSE_RANGE]: Use
‘close_range’ when ‘exceptions’ is empty.
* config-daemon.ac: Check for <linux/close_range.h> and the
‘close_range’ symbol.

Change-Id: I12fa3bde58b003fcce5ea5a1fee1dcf9a92c0359
---
 config-daemon.ac    |  5 +++--
 nix/libutil/util.cc | 23 +++++++++++++++++------
 2 files changed, 20 insertions(+), 8 deletions(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 6731c68bc3..4e949bc88a 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -78,7 +78,8 @@ if test "x$guix_build_daemon" = "xyes"; then
 
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
-  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h])
+  AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
+    linux/close_range.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
@@ -95,7 +96,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl strsignal: for error reporting.
   dnl statx: fine-grain 'stat' call, new in glibc 2.28.
   AC_CHECK_FUNCS([lutimes lchown posix_fallocate sched_setaffinity \
-     statvfs nanosleep strsignal statx])
+     statvfs nanosleep strsignal statx close_range])
 
   dnl Check for <locale>.
   AC_LANG_PUSH(C++)
diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index 3206dea11b..eb2d16e1cc 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -23,6 +23,10 @@
 #include <sys/prctl.h>
 #endif
 
+#ifdef HAVE_LINUX_CLOSE_RANGE_H
+# include <linux/close_range.h>
+#endif
+
 
 extern char * * environ;
 
@@ -1087,12 +1091,19 @@ string runProgram(Path program, bool searchPath, const Strings & args)
 
 void closeMostFDs(const set<int> & exceptions)
 {
-    int maxFD = 0;
-    maxFD = sysconf(_SC_OPEN_MAX);
-    for (int fd = 0; fd < maxFD; ++fd)
-        if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
-            && exceptions.find(fd) == exceptions.end())
-            close(fd); /* ignore result */
+#ifdef HAVE_CLOSE_RANGE
+    if (exceptions.empty())
+	 close_range(3, ~0U, 0);
+    else
+#endif
+    {
+	 int maxFD = 0;
+	 maxFD = sysconf(_SC_OPEN_MAX);
+	 for (int fd = 0; fd < maxFD; ++fd)
+	      if (fd != STDIN_FILENO && fd != STDOUT_FILENO && fd != STDERR_FILENO
+		  && exceptions.find(fd) == exceptions.end())
+		   close(fd); /* ignore result */
+    }
 }
 
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:11 GMT) Full text and rfc822 format available.

Message #412 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 03/16] daemon: Bind-mount /etc/nsswitch.conf & co. only if
 it exists.
Date: Sun, 23 Mar 2025 15:24:56 +0100
Those files may be missing in some contexts, for instance within the
build environment.

* nix/libstore/build.cc (DerivationGoal::runChild): Add /etc/resolv.conf
and related files to ‘ss’ only if they exist.

Change-Id: Ie19664a86c8101a1dc82cf39ad4b7abb10f8250a
---
 nix/libstore/build.cc | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index edd01bab34..8ca5e5b732 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2093,10 +2093,11 @@ void DerivationGoal::runChild()
                network, so give them access to /etc/resolv.conf and so
                on. */
             if (fixedOutput) {
-                ss.push_back("/etc/resolv.conf");
-                ss.push_back("/etc/nsswitch.conf");
-                ss.push_back("/etc/services");
-                ss.push_back("/etc/hosts");
+		auto files = { "/etc/resolv.conf", "/etc/nsswitch.conf",
+			       "/etc/services", "/etc/hosts" };
+		for (auto & file: files) {
+		    if (pathExists(file)) ss.push_back(file);
+		}
             }
 
             for (auto & i : ss) dirsInChroot[i] = i;
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:12 GMT) Full text and rfc822 format available.

Message #415 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v8 02/16] daemon: Close the read end of the logging pipe.
Date: Sun, 23 Mar 2025 15:24:55 +0100
* nix/libutil/util.cc (commonChildInit): Close ‘logPipe.readSide’.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ia9e48d1afb85d7af52770e016f2b6832792044dd
---
 nix/libutil/util.cc | 3 +++
 1 file changed, 3 insertions(+)

diff --git a/nix/libutil/util.cc b/nix/libutil/util.cc
index eb2d16e1cc..56f116046c 100644
--- a/nix/libutil/util.cc
+++ b/nix/libutil/util.cc
@@ -1279,6 +1279,9 @@ void commonChildInit(Pipe & logPipe)
     if (setsid() == -1)
         throw SysError(format("creating a new session"));
 
+    /* Close the read end so only the parent holds a reference to it.  */
+    logPipe.readSide.close();
+
     /* Dup the write side of the logger pipe into stderr. */
     if (dup2(logPipe.writeSide, STDERR_FILENO) == -1)
         throw SysError("cannot pipe standard error into log file");
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:13 GMT) Full text and rfc822 format available.

Message #418 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 06/16] daemon: Remount root directory as read-only.
Date: Sun, 23 Mar 2025 15:24:59 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Bind-mount the store
and /tmp under ‘chrootRootDir’ to themselves as read-write.
Remount / as read-only.

Change-Id: I79565094c8ec8448401897c720aad75304fd1948
---
 nix/libstore/build.cc | 16 ++++++++++++++++
 1 file changed, 16 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 3861a1ffd9..c8b778362a 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2091,6 +2091,18 @@ void DerivationGoal::runChild()
 
             for (auto & i : ss) dirsInChroot[i] = i;
 
+	    /* Make new mounts for the store and for /tmp.  That way, when
+	       'chrootRootDir' is made read-only below, these two mounts will
+	       remain writable (the store needs to be writable so derivation
+	       outputs can be written to it, and /tmp is writable by
+	       convention).  */
+	    auto chrootStoreDir = chrootRootDir + settings.nixStore;
+	    if (mount(chrootStoreDir.c_str(), chrootStoreDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of store '%1%' failed") % chrootStoreDir);
+	    auto chrootTmpDir = chrootRootDir + "/tmp";
+	    if (mount(chrootTmpDir.c_str(), chrootTmpDir.c_str(), 0, MS_BIND, 0) == -1)
+                throw SysError(format("read-write mount of temporary directory '%1%' failed") % chrootTmpDir);
+
             /* Bind-mount all the directories from the "host"
                filesystem that we want in the chroot
                environment. */
@@ -2164,6 +2176,10 @@ void DerivationGoal::runChild()
 
             if (rmdir("real-root") == -1)
                 throw SysError("cannot remove real-root directory");
+
+	    /* 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);
         }
 #endif
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:14 GMT) Full text and rfc822 format available.

Message #421 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v8 04/16] daemon: Bind-mount all the inputs,
 not just directories.
Date: Sun, 23 Mar 2025 15:24:57 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.
Special-case symlinks.
(DerivationGoal)[regularInputPaths]: Remove.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
 nix/libstore/build.cc | 39 ++++++++++++++-------------------------
 1 file changed, 14 insertions(+), 25 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 8ca5e5b732..193b279b88 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -659,9 +659,6 @@ private:
     /* RAII object to delete the chroot directory. */
     std::shared_ptr<AutoDelete> autoDelChroot;
 
-    /* All inputs that are regular files. */
-    PathSet regularInputPaths;
-
     /* Whether this is a fixed-output derivation. */
     bool fixedOutput;
 
@@ -1850,9 +1847,7 @@ void DerivationGoal::startBuilder()
 
         /* Make the closure of the inputs available in the chroot,
            rather than the whole store.  This prevents any access
-           to undeclared dependencies.  Directories are bind-mounted,
-           while other inputs are hard-linked (since only directories
-           can be bind-mounted).  !!! As an extra security
+           to undeclared dependencies.  !!! As an extra security
            precaution, make the fake store only writable by the
            build user. */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
@@ -1863,28 +1858,22 @@ void DerivationGoal::startBuilder()
             throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
         foreach (PathSet::iterator, i, inputPaths) {
-            struct stat st;
+	    struct stat st;
             if (lstat(i->c_str(), &st))
                 throw SysError(format("getting attributes of path `%1%'") % *i);
-            if (S_ISDIR(st.st_mode))
-                dirsInChroot[*i] = *i;
-            else {
-                Path p = chrootRootDir + *i;
-                if (link(i->c_str(), p.c_str()) == -1) {
-                    /* Hard-linking fails if we exceed the maximum
-                       link count on a file (e.g. 32000 of ext3),
-                       which is quite possible after a `nix-store
-                       --optimise'. */
-                    if (errno != EMLINK)
-                        throw SysError(format("linking `%1%' to `%2%'") % p % *i);
-                    StringSink sink;
-                    dumpPath(*i, sink);
-                    StringSource source(sink.s);
-                    restorePath(p, source);
-                }
 
-                regularInputPaths.insert(*i);
-            }
+	    if (S_ISLNK(st.st_mode)) {
+		/* Since bind-mounts follow symlinks, thus representing their
+		   target and not the symlink itself, special-case
+		   symlinks. XXX: When running unprivileged, TARGET can be
+		   deleted by the build process.  Use 'open_tree' & co. when
+		   it's more widely available.  */
+                Path target = chrootRootDir + *i;
+		if (symlink(readLink(*i).c_str(), target.c_str()) == -1)
+		    throw SysError(format("failed to create symlink '%1%' to '%2%'") % target % readLink(*i));
+	    }
+	    else
+		dirsInChroot[*i] = *i;
         }
 
         /* If we're repairing, checking or rebuilding part of a
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:15 GMT) Full text and rfc822 format available.

Message #424 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v8 05/16] daemon: Remount inputs as read-only.
Date: Sun, 23 Mar 2025 15:24:58 +0100
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: Ib7201bcf4363be566f205d23d17fe2f55d3ad666
---
 nix/libstore/build.cc | 7 +++++++
 1 file changed, 7 insertions(+)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 193b279b88..3861a1ffd9 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -2107,8 +2107,15 @@ void DerivationGoal::runChild()
                     createDirs(dirOf(target));
                     writeFile(target, "");
                 }
+
+		/* Extra flags passed with MS_BIND are ignored, hence the
+		   extra MS_REMOUNT.  */
                 if (mount(source.c_str(), target.c_str(), "", MS_BIND, 0) == -1)
                     throw SysError(format("bind mount from `%1%' to `%2%' failed") % source % target);
+		if (source.compare(0, settings.nixStore.length(), settings.nixStore) == 0) {
+		     if (mount(source.c_str(), target.c_str(), "", MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1)
+			  throw SysError(format("read-only remount of `%1%' failed") % target);
+		}
             }
 
             /* Bind a new instance of procfs on /proc to reflect our
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:16 GMT) Full text and rfc822 format available.

Message #427 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 08/16] daemon: Create /var/guix/profiles/per-user
 unconditionally.
Date: Sun, 23 Mar 2025 15:25:01 +0100
* nix/libstore/local-store.cc (LocalStore::LocalStore): Create
‘perUserDir’ unconditionally.

Change-Id: I5188320f9630a81d16f79212d0fffabd55d94abe
---
 nix/libstore/local-store.cc | 8 ++++----
 1 file changed, 4 insertions(+), 4 deletions(-)

diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 83e6c3e16e..f6540c2117 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -79,12 +79,12 @@ LocalStore::LocalStore(bool reserveSpace)
         createSymlink(profilesDir, gcRootsDir + "/profiles");
     }
 
-    /* Optionally, create directories and set permissions for a
-       multi-user install. */
+    Path perUserDir = profilesDir + "/per-user";
+    createDirs(perUserDir);
+
+    /* Optionally, set permissions for a multi-user install.  */
     if (getuid() == 0 && settings.buildUsersGroup != "") {
 
-        Path perUserDir = profilesDir + "/per-user";
-        createDirs(perUserDir);
         if (chmod(perUserDir.c_str(), 0755) == -1)
             throw SysError(format("could not set permissions on '%1%' to 755")
                            % perUserDir);
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:17 GMT) Full text and rfc822 format available.

Message #430 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 09/16] daemon: Drop Linux ambient capabilities before
 executing builder.
Date: Sun, 23 Mar 2025 15:25:02 +0100
* config-daemon.ac: Check for <sys/prctl.h>.
* nix/libstore/build.cc (DerivationGoal::runChild): When ‘useChroot’ is
true, call ‘prctl’ to drop all ambient capabilities.

Change-Id: If34637fc508e5fb6d278167f5df7802fc595284f
---
 config-daemon.ac      | 2 +-
 nix/libstore/build.cc | 9 +++++++++
 2 files changed, 10 insertions(+), 1 deletion(-)

diff --git a/config-daemon.ac b/config-daemon.ac
index 4e949bc88a..35d9c8cd56 100644
--- a/config-daemon.ac
+++ b/config-daemon.ac
@@ -79,7 +79,7 @@ if test "x$guix_build_daemon" = "xyes"; then
   dnl Chroot support.
   AC_CHECK_FUNCS([chroot unshare])
   AC_CHECK_HEADERS([sched.h sys/param.h sys/mount.h sys/syscall.h \
-    linux/close_range.h])
+    linux/close_range.h sys/prctl.h])
 
   if test "x$ac_cv_func_chroot" != "xyes"; then
     AC_MSG_ERROR(['chroot' function missing, bailing out])
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index 2261ef3c2a..e6a139eb0d 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -50,6 +50,9 @@
 #if HAVE_SCHED_H
 #include <sched.h>
 #endif
+#if HAVE_SYS_PRCTL_H
+#include <sys/prctl.h>
+#endif
 
 
 #define CHROOT_ENABLED HAVE_CHROOT && HAVE_SYS_MOUNT_H && defined(MS_BIND) && defined(MS_PRIVATE)
@@ -2075,6 +2078,12 @@ void DerivationGoal::runChild()
 
 #if CHROOT_ENABLED
         if (useChroot) {
+# if HAVE_SYS_PRCTL_H
+	    /* Drop ambient capabilities such as CAP_CHOWN that might have
+	       been granted when starting guix-daemon.  */
+	    prctl(PR_CAP_AMBIENT, PR_CAP_AMBIENT_CLEAR_ALL, 0, 0, 0);
+# endif
+
 	    if (!fixedOutput) {
 		/* Initialise the loopback interface. */
 		AutoCloseFD fd(socket(PF_INET, SOCK_DGRAM, IPPROTO_IP));
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:17 GMT) Full text and rfc822 format available.

Message #433 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 10/16] daemon: Move comments where they belong.
Date: Sun, 23 Mar 2025 15:25:03 +0100
* nix/libstore/build.cc (DerivationGoal::startBuilder): Shuffle
comments for clarity.

Change-Id: I6557c103ade4a3ab046354548ea193c68f8c9c05
---
 nix/libstore/build.cc | 13 +++++++------
 1 file changed, 7 insertions(+), 6 deletions(-)

diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index e6a139eb0d..d0fcc99854 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -1870,18 +1870,19 @@ void DerivationGoal::startBuilder()
         }
         dirsInChroot[tmpDirInSandbox] = tmpDir;
 
-        /* Make the closure of the inputs available in the chroot,
-           rather than the whole store.  This prevents any access
-           to undeclared dependencies.  !!! As an extra security
-           precaution, make the fake store only writable by the
-           build user. */
+	/* Create the fake store.  */
         Path chrootStoreDir = chrootRootDir + settings.nixStore;
         createDirs(chrootStoreDir);
         chmod_(chrootStoreDir, 01775);
 
         if (buildUser.enabled() && chown(chrootStoreDir.c_str(), 0, buildUser.getGID()) == -1)
-            throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
+	     /* As an extra security precaution, make the fake store only
+		writable by the build user.  */
+	     throw SysError(format("cannot change ownership of ‘%1%’") % chrootStoreDir);
 
+        /* Make the closure of the inputs available in the chroot, rather than
+           the whole store.  This prevents any access to undeclared
+           dependencies. */
         foreach (PathSet::iterator, i, inputPaths) {
 	    struct stat st;
             if (lstat(i->c_str(), &st))
-- 
2.48.1





Information forwarded to guix <at> cbaines.net, dev <at> jpoiret.xyz, ludo <at> gnu.org, othacehe <at> gnu.org, maxim.cournoyer <at> gmail.com, zimon.toutoune <at> gmail.com, me <at> tobias.gr, guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:26:17 GMT) Full text and rfc822 format available.

Message #436 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v8 07/16] daemon: Allow running as non-root with unprivileged
 user namespaces.
Date: Sun, 23 Mar 2025 15:25:00 +0100
From: Ludovic Courtès <ludovic.courtes <at> inria.fr>

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 <at> russelstein.xyz>
---
 doc/guix.texi               | 102 +++++++++++++++-----
 guix/substitutes.scm        |   2 +-
 nix/libstore/build.cc       | 183 +++++++++++++++++++++++++++++++-----
 nix/libstore/local-store.cc |  18 +++-
 tests/derivations.scm       |  14 +++
 5 files changed, 264 insertions(+), 55 deletions(-)

diff --git a/doc/guix.texi b/doc/guix.texi
index 0adc6f8c65..ec5d41bbf0 100644
--- a/doc/guix.texi
+++ b/doc/guix.texi
@@ -877,6 +877,7 @@ Setting Up the Daemon
 @section Setting Up the Daemon
 
 @cindex daemon
+@cindex build daemon
 During installation, the @dfn{build daemon} that must be running
 to use Guix has already been set up and you can run @command{guix}
 commands in your terminal program, @pxref{Getting Started}:
@@ -921,20 +922,38 @@ Build Environment Setup
 @cindex build environment
 In a standard multi-user setup, Guix and its daemon---the
 @command{guix-daemon} program---are installed by the system
-administrator; @file{/gnu/store} is owned by @code{root} and
-@command{guix-daemon} runs as @code{root}.  Unprivileged users may use
-Guix tools to build packages or otherwise access the store, and the
-daemon will do it on their behalf, ensuring that the store is kept in a
-consistent state, and allowing built packages to be shared among users.
+administrator.  Unprivileged users may use Guix tools to build packages
+or otherwise access the store, and the daemon will do it on their
+behalf, ensuring that the store is kept in a consistent state, and
+allowing built packages to be shared among users.
+
+There are currently two ways to set up and run the build daemon:
+
+@enumerate
+@item
+running @command{guix-daemon} as ``root'', letting it run build
+processes as unprivileged users taken from a pool of build users---this
+is the historical approach;
+
+@item
+running @command{guix-daemon} as a separate unprivileged user, relying
+on Linux's @dfn{unprivileged user namespace} functionality to set up
+isolated environments---this is the option chosen when installing Guix
+on a systemd-based distribution with the installation script
+(@pxref{Binary Installation}).
+@end enumerate
+
+The sections below describe each of these two configurations in more
+detail and summarize the kind of build isolation they provide.
+
+@unnumberedsubsubsec Daemon Running as Root
 
 @cindex build users
 When @command{guix-daemon} runs as @code{root}, you may not want package
 build processes themselves to run as @code{root} too, for obvious
 security reasons.  To avoid that, a special pool of @dfn{build users}
 should be created for use by build processes started by the daemon.
-These build users need not have a shell and a home directory: they will
-just be used when the daemon drops @code{root} privileges in build
-processes.  Having several such users allows the daemon to launch
+Having several such users allows the daemon to launch
 distinct build processes under separate UIDs, which guarantees that they
 do not interfere with each other---an essential feature since builds are
 regarded as pure functions (@pxref{Introduction}).
@@ -977,11 +996,45 @@ Build Environment Setup
 # guix-daemon --build-users-group=guixbuild
 @end example
 
+In this setup, @file{/gnu/store} is owned by @code{root}.
+
+@unnumberedsubsubsec Daemon Running Without Privileges
+
+@cindex rootless build daemon
+@cindex unprivileged build daemon
+@cindex build daemon, unprivileged
+The second and preferred option is to run @command{guix-daemon}
+@emph{as an unprivileged user}.  It has the advantage of reducing the
+harm that can be done should a build process manage to exploit a
+vulnerability in the daemon.  This option requires the use of Linux's
+unprivileged user namespace mechanism; today it is available and enabled
+by most GNU/Linux distributions but can still be disabled.  The
+installation script automatically determines whether this option is
+available on your system (@pxref{Binary Installation}).
+
+When using this option, you only need to create one user account, and
+@command{guix-daemon} will run with the authority of that account:
+
+@example
+# groupadd --system guix-daemon
+# useradd -g guix-daemon -G guix-daemon              \
+          -d /var/empty -s $(which nologin)          \
+          -c "Guix daemon privilege separation user" \
+          --system guix-daemon
+@end example
+
+In this configuration, @file{/gnu/store} is owned by the
+@code{guix-daemon} user.
+
+@unnumberedsubsubsec The Isolated Build Environment
+
 @cindex chroot
-@noindent
-This way, the daemon starts build processes in a chroot, under one of
-the @code{guixbuilder} users.  On GNU/Linux, by default, the chroot
-environment contains nothing but:
+@cindex build environment isolation
+@cindex isolated build environment
+@cindex hermetic build environment
+In both cases, the daemon starts build processes without privileges in
+an @emph{isolated} or @emph{hermetic} build environment---a ``chroot''.
+On GNU/Linux, by default, the build environment contains nothing but:
 
 @c Keep this list in sync with libstore/build.cc! -----------------------
 @itemize
@@ -1015,7 +1068,7 @@ Build Environment Setup
 @file{/homeless-shelter}.  This helps to highlight inappropriate uses of
 @env{HOME} in the build scripts of packages.
 
-All this usually enough to ensure details of the environment do not
+All this is usually enough to ensure details of the environment do not
 influence build processes.  In some exceptional cases where more control
 is needed---typically over the date, kernel, or CPU---you can resort to
 a virtual build machine (@pxref{build-vm, virtual build machines}).
@@ -1035,14 +1088,6 @@ Build Environment Setup
 for fixed-output derivations (@pxref{Derivations}) or for substitutes
 (@pxref{Substitutes}).
 
-If you are installing Guix as an unprivileged user, it is still possible
-to run @command{guix-daemon} provided you pass @option{--disable-chroot}.
-However, build processes will not be isolated from one another, and not
-from the rest of the system.  Thus, build processes may interfere with
-each other, and may access programs, libraries, and other files
-available on the system---making it much harder to view them as
-@emph{pure} functions.
-
 
 @node Daemon Offload Setup
 @subsection Using the Offload Facility
@@ -1567,10 +1612,17 @@ Invoking guix-daemon
 @item --disable-chroot
 Disable chroot builds.
 
-Using this option is not recommended since, again, it would allow build
-processes to gain access to undeclared dependencies.  It is necessary,
-though, when @command{guix-daemon} is running under an unprivileged user
-account.
+@quotation Warning
+Using this option is not recommended since it allows build processes to
+gain access to undeclared dependencies, to interfere with one another,
+and more generally to do anything that can be done with the authority of
+build users or that of the daemon---which includes at least the ability
+to tamper with any file in the store!
+
+You may find it necessary, though, when support for Linux unprivileged
+user namespaces is missing (@pxref{Build Environment Setup}).  Use at
+your own risk!
+@end quotation
 
 @item --log-compression=@var{type}
 Compress build logs according to @var{type}, one of @code{gzip},
diff --git a/guix/substitutes.scm b/guix/substitutes.scm
index 7ca55788d5..86b9f5472a 100644
--- a/guix/substitutes.scm
+++ b/guix/substitutes.scm
@@ -79,7 +79,7 @@ (define %narinfo-cache-directory
   ;; time, 'guix substitute' is called by guix-daemon as root and stores its
   ;; cached data in /var/guix/….  However, when invoked from 'guix challenge'
   ;; as a user, it stores its cache in ~/.cache.
-  (if (zero? (getuid))
+  (if (getenv "_NIX_OPTIONS")                     ;invoked by guix-daemon
       (or (and=> (getenv "XDG_CACHE_HOME")
                  (cut string-append <> "/guix/substitute"))
           (string-append %state-directory "/substitute/cache"))
diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc
index c8b778362a..2261ef3c2a 100644
--- a/nix/libstore/build.cc
+++ b/nix/libstore/build.cc
@@ -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());
 		}
             }
diff --git a/nix/libstore/local-store.cc b/nix/libstore/local-store.cc
index 0883a4bbce..83e6c3e16e 100644
--- a/nix/libstore/local-store.cc
+++ b/nix/libstore/local-store.cc
@@ -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);
+	}
+    }
 }
 
 
diff --git a/tests/derivations.scm b/tests/derivations.scm
index 72ea9aa9cc..b0b151dd33 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -858,6 +858,20 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (call-with-input-file (derivation->output-path drv)
            get-string-all))))
 
+(test-assert "builder is outside the store"
+  ;; Ensure that attempts to build derivations whose builder is outside the
+  ;; store are rejected.  This is a protection against attacks similar to
+  ;; CVE-2019-5736, which abuse the fact that /proc/self/exe can be opened
+  ;; even when it presents itself as a symlink to a file not in the chroot.
+  (let* ((builder (add-file-tree-to-store %store
+                                          `("builder" symlink "/proc/self/exe")))
+         (drv (derivation %store "attempt-to-run-guix-daemon" builder '()
+                          #:env-vars
+                          '(("LD_PRELOAD" . "attacker-controlled.so")))))
+    (guard (c ((store-protocol-error? c) c))
+      (build-derivations %store (list drv))
+      #f)))
+
 
 (define %coreutils
   (false-if-exception
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:28:03 GMT) Full text and rfc822 format available.

Message #439 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>,
 Reepca Russelstein <reepca <at> russelstein.xyz>
Subject: [PATCH v8 11/16] linux-container: ‘unprivileged-user-namespace-supported?’ returns #f on non-Linux.
Date: Sun, 23 Mar 2025 15:25:04 +0100
Previously this procedure would return #t on non-Linux systems.

* gnu/build/linux-container.scm (unprivileged-user-namespace-supported?):
When USERNS-FILE doesn’t exist, return (user-namespace-supported?).

Reported-by: Reepca Russelstein <reepca <at> russelstein.xyz>
Change-Id: I92050338b8b68bc3bd87100317eba69fcdf14a0a
---
 gnu/build/linux-container.scm | 4 ++--
 1 file changed, 2 insertions(+), 2 deletions(-)

diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm
index 5c303da8c8..a5c5d8962e 100644
--- a/gnu/build/linux-container.scm
+++ b/gnu/build/linux-container.scm
@@ -1,6 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2015 David Thompson <davet <at> gnu.org>
-;;; Copyright © 2017-2019, 2022, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2017-2019, 2022-2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -44,7 +44,7 @@ (define (unprivileged-user-namespace-supported?)
   (let ((userns-file "/proc/sys/kernel/unprivileged_userns_clone"))
     (if (file-exists? userns-file)
         (eqv? #\1 (call-with-input-file userns-file read-char))
-        #t)))
+        (user-namespace-supported?))))
 
 (define (setgroups-supported?)
   "Return #t if the setgroups proc file, introduced in Linux-libre 3.19,
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:28:04 GMT) Full text and rfc822 format available.

Message #442 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 12/16] tests: Add missing derivation inputs.
Date: Sun, 23 Mar 2025 15:25:05 +0100
These missing inputs go unnoticed when running ‘guix-daemon
--disable-chroot’ but are immediately visible otherwise.

* tests/derivations.scm ("fixed-output derivation"): Add %BASH to #:sources.
("fixed-output derivation: output paths are equal"):
("fixed-output derivation, recursive"):
("derivation with a fixed-output input"):
("derivation with duplicate fixed-output inputs"):
("derivation with equivalent fixed-output inputs"):
("build derivation with coreutils"): Likewise.
* tests/packages.scm (bootstrap-binary): New procedure.
("package-source-derivation, origin, sha512"): Use it instead of
‘search-bootstrap-binary’ and add BASH to #:sources.
("package-source-derivation, origin, sha3-512"): Likewise.

Change-Id: I4c9087df23c47729a3aff15e9e1435b7266e36e2
---
 tests/derivations.scm | 24 +++++++++++++++---------
 tests/packages.scm    | 13 +++++++++----
 2 files changed, 24 insertions(+), 13 deletions(-)

diff --git a/tests/derivations.scm b/tests/derivations.scm
index b0b151dd33..4dc7bcf1c5 100644
--- a/tests/derivations.scm
+++ b/tests/derivations.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2024 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -443,7 +443,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                      (string-append
                                       "fixed-" (symbol->string hash-algorithm))
                                      %bash `(,builder)
-                                     #:sources `(,builder) ;optional
+                                     #:sources (list %bash builder)
                                      #:hash hash
                                      #:hash-algo hash-algorithm)))
            (build-derivations %store (list drv))
@@ -462,9 +462,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv1       (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (drv2       (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (succeeded? (build-derivations %store (list drv1 drv2))))
     (and succeeded?
@@ -477,7 +479,7 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (drv        (derivation %store "fixed-rec"
                                  %bash `(,builder)
-                                 #:sources (list builder)
+                                 #:sources (list %bash builder)
                                  #:hash (base32 "0sg9f58l1jj88w6pdrfdpj5x9b1zrwszk84j81zvby36q9whhhqa")
                                  #:hash-algo 'sha256
                                  #:recursive? #t))
@@ -511,9 +513,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed-out  (derivation->output-path fixed1))
          (builder3   (add-text-to-store
@@ -548,9 +552,11 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
          (hash       (gcrypt:sha256 (string->utf8 "hello")))
          (fixed1     (derivation %store "fixed"
                                  %bash `(,builder1)
+                                 #:sources (list %bash builder1)
                                  #:hash hash #:hash-algo 'sha256))
          (fixed2     (derivation %store "fixed"
                                  %bash `(,builder2)
+                                 #:sources (list %bash builder2)
                                  #:hash hash #:hash-algo 'sha256))
          (builder3   (add-text-to-store %store "builder.sh"
                                         "echo fake builder"))
@@ -580,21 +586,21 @@ (define* (directory-contents dir #:optional (slurp get-bytevector-all))
                                       '()))
          (hash     (gcrypt:sha256 (string->utf8 "hello")))
          (drv1     (derivation %store "fixed" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:hash hash #:hash-algo 'sha256))
          (drv2     (derivation %store "fixed" %bash (list builder2)
-                               #:sources (list builder2)
+                               #:sources (list %bash builder2)
                                #:hash hash #:hash-algo 'sha256))
          (drv3a    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv1))))
          (drv3b    (derivation %store "fixed-user" %bash (list builder3)
                                #:outputs '("one" "two")
-                               #:sources (list builder3)
+                               #:sources (list %bash builder3)
                                #:inputs (list (derivation-input drv2))))
          (drv4     (derivation %store "fixed-user-user" %bash (list builder1)
-                               #:sources (list builder1)
+                               #:sources (list %bash builder1)
                                #:inputs (list (derivation-input drv3a '("one"))
                                               (derivation-input drv3b '("two"))))))
     (match (derivation-inputs drv4)
@@ -892,7 +898,7 @@ (define %coreutils
                                     ,(string-append
                                       (derivation->output-path %coreutils)
                                       "/bin")))
-                      #:sources (list builder)
+                      #:sources (list %bash builder)
                       #:inputs (list (derivation-input %coreutils))))
          (succeeded?
           (build-derivations %store (list drv))))
diff --git a/tests/packages.scm b/tests/packages.scm
index 50c1cab915..f56c63128d 100644
--- a/tests/packages.scm
+++ b/tests/packages.scm
@@ -80,6 +80,11 @@ (define %store
 ;; When grafting, do not add dependency on 'glibc-utf8-locales'.
 (%graft-with-utf8-locale? #f)
 
+(define (bootstrap-binary name)
+  (let ((bin (search-bootstrap-binary name (%current-system))))
+    (and %store
+         (add-to-store %store name #t "sha256" bin))))
+
 
 (test-begin "packages")
 
@@ -609,14 +614,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha512"
   "hello"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
@@ -635,14 +640,14 @@ (define %store
 
 (test-equal "package-source-derivation, origin, sha3-512"
   "hello, sha3"
-  (let* ((bash    (search-bootstrap-binary "bash" (%current-system)))
+  (let* ((bash    (bootstrap-binary "bash"))
          (builder (add-text-to-store %store "my-fixed-builder.sh"
                                      "echo -n hello, sha3 > $out" '()))
          (method  (lambda* (url hash-algo hash #:optional name
                                 #:rest rest)
                     (and (eq? hash-algo 'sha3-512)
                          (raw-derivation name bash (list builder)
-                                         #:sources (list builder)
+                                         #:sources (list bash builder)
                                          #:hash hash
                                          #:hash-algo hash-algo))))
          (source  (origin
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:28:06 GMT) Full text and rfc822 format available.

Message #445 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 13/16] tests: Run in a chroot and unprivileged user
 namespaces.
Date: Sun, 23 Mar 2025 15:25:06 +0100
* build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged
user namespace support is lacking and warn in that case.
* tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’
rather than a shared file as a source of entropy.
("symlink is symlink")
("isolated environment", "inputs are read-only")
("inputs cannot be remounted read-write")
("build root cannot be made world-readable")
("/tmp, store, and /dev/{null,full} are writable")
("network is unreachable"): New tests.
* tests/processes.scm ("client + lock"): Skip when
‘unprivileged-user-namespace-supported?’ returns true.

Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84
---
 build-aux/test-env.in |  18 ++-
 tests/processes.scm   |   9 +-
 tests/store.scm       | 247 ++++++++++++++++++++++++++++++++++++------
 3 files changed, 236 insertions(+), 38 deletions(-)

diff --git a/build-aux/test-env.in b/build-aux/test-env.in
index 9caa29da58..86c2e585d7 100644
--- a/build-aux/test-env.in
+++ b/build-aux/test-env.in
@@ -1,7 +1,7 @@
 #!/bin/sh
 
 # GNU Guix --- Functional package management for GNU
-# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès <ludo <at> gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo <at> gnu.org>
 #
 # This file is part of GNU Guix.
 #
@@ -102,10 +102,24 @@ then
     rm -rf "$GUIX_STATE_DIRECTORY/daemon-socket"
     mkdir -m 0700 "$GUIX_STATE_DIRECTORY/daemon-socket"
 
+    # If unprivileged user namespaces are not supported, pass
+    # '--disable-chroot'.
+    if [ -f /proc/self/ns/user ] \
+       && { [ ! -f /proc/sys/kernel/unprivileged_userns_clone ] \
+		|| [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -eq 1 ]; }
+    then
+	extra_options=""
+    else
+	extra_options="--disable-chroot"
+	echo "unprivileged user namespaces not supported; \
+running 'guix-daemon $extra_options'" >&2
+    fi
+
     # Launch the daemon without chroot support because is may be
     # unavailable, for instance if we're not running as root.
     "@abs_top_builddir@/pre-inst-env"				\
-	"@abs_top_builddir@/guix-daemon" --disable-chroot	\
+	"@abs_top_builddir@/guix-daemon"			\
+        $extra_options						\
 	--substitute-urls="$GUIX_BINARY_SUBSTITUTE_URL" &
 
     daemon_pid=$!
diff --git a/tests/processes.scm b/tests/processes.scm
index ba518f2d9e..a72ba16f58 100644
--- a/tests/processes.scm
+++ b/tests/processes.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2018 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;; Copyright © 2019 Mathieu Othacehe <m.othacehe <at> gmail.com>
 ;;;
 ;;; This file is part of GNU Guix.
@@ -25,6 +25,8 @@ (define-module (test-processes)
   #:use-module (guix gexp)
   #:use-module ((guix utils) #:select (call-with-temporary-directory))
   #:use-module (gnu packages bootstrap)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix tests)
   #:use-module (srfi srfi-1)
   #:use-module (srfi srfi-64)
@@ -84,6 +86,11 @@ (define-syntax-rule (test-assert* description exp)
       (and (kill (process-id daemon) 0)
            (string-suffix? "guix-daemon" (first (process-command daemon)))))))
 
+(when (unprivileged-user-namespace-supported?)
+  ;; The test below assumes the build process can communicate with the outside
+  ;; world via the TOKEN1 and TOKEN2 files, which is impossible when
+  ;; guix-daemon is set up to build in separate namespaces.
+  (test-skip 1))
 (test-assert* "client + lock"
   (with-store store
     (call-with-temporary-directory
diff --git a/tests/store.scm b/tests/store.scm
index 45948f4f43..b1ddff2082 100644
--- a/tests/store.scm
+++ b/tests/store.scm
@@ -1,5 +1,5 @@
 ;;; GNU Guix --- Functional package management for GNU
-;;; Copyright © 2012-2021, 2023 Ludovic Courtès <ludo <at> gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo <at> gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -28,8 +28,12 @@ (define-module (test-store)
   #:use-module (guix base32)
   #:use-module (guix packages)
   #:use-module (guix derivations)
+  #:use-module ((guix modules)
+                #:select (source-module-closure))
   #:use-module (guix serialization)
   #:use-module (guix build utils)
+  #:use-module ((gnu build linux-container)
+                #:select (unprivileged-user-namespace-supported?))
   #:use-module (guix gexp)
   #:use-module (gnu packages)
   #:use-module (gnu packages bootstrap)
@@ -391,6 +395,188 @@ (define %shell
          (equal? (valid-derivers %store o)
                  (list (derivation-file-name d))))))
 
+(test-assert "symlink is symlink"
+  (let* ((a (add-text-to-store %store "hello.txt" (random-text)))
+         (b (build-expression->derivation
+             %store "symlink"
+             '(symlink (assoc-ref %build-inputs "a") %output)
+             #:inputs `(("a" ,a))))
+         (c (build-expression->derivation
+             %store "symlink-reference"
+             `(call-with-output-file %output
+                (lambda (port)
+                  ;; Check that B is indeed visible as a symlink.  This should
+                  ;; always be the case, both in the '--disable-chroot' and in
+                  ;; the user namespace setups.
+                  (pk 'stat (lstat (assoc-ref %build-inputs "b")))
+                  (display (readlink (assoc-ref %build-inputs "b"))
+                           port)))
+             #:inputs `(("b" ,b)))))
+    (and (build-derivations %store (list c))
+         (string=? (call-with-input-file (derivation->output-path c)
+                     get-string-all)
+                   a))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "isolated environment"
+  (string-join (append
+                '("PID: 1" "UID: 30001")
+                (delete-duplicates
+                 (sort (list "/dev" "/tmp" "/proc" "/etc"
+                             (match (string-tokenize (%store-prefix)
+                                                     (char-set-complement
+                                                      (char-set #\/)))
+                               ((top _ ...) (string-append "/" top))))
+                       string<?))
+                '("/etc/group" "/etc/hosts" "/etc/passwd")))
+  (let* ((b (add-text-to-store %store "build.sh"
+                               "echo -n PID: $$ UID: $UID /* /etc/* > $out"))
+         (s (add-to-store %store "bash" #t "sha256"
+                          (search-bootstrap-binary "bash"
+                                                   (%current-system))))
+         (d (derivation %store "the-thing"
+                        s `("-e" ,b)
+                        #:env-vars `(("foo" . ,(random-text)))
+                        #:sources (list b s)))
+         (o (derivation->output-path d)))
+    (and (build-derivations %store (list d))
+         (call-with-input-file o get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-equal "inputs are read-only"
+  "All good!"
+  (let* ((input (plain-file (string-append "might-be-tampered-with-"
+                                           (number->string
+                                            (car (gettimeofday))
+                                            16))
+                            "All good!"))
+         (drv
+          (run-with-store %store
+            (gexp->derivation
+             "attempt-to-write-to-input"
+             (with-imported-modules (source-module-closure
+                                     '((guix build syscalls)))
+               #~(begin
+                   (use-modules (guix build syscalls))
+
+                   (let ((input #$input))
+                     (chmod input #o666)
+                     (call-with-output-file input
+                       (lambda (port)
+                         (display "BAD!" port)))
+                     (mkdir #$output))))))))
+    (and (guard (c ((store-protocol-error? c) #t))
+           (build-derivations %store (list drv)))
+         (call-with-input-file (run-with-store %store
+                                 (lower-object input))
+           get-string-all))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "inputs cannot be remounted read-write"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-remount-input-read-write"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (let ((input #$(plain-file "input-that-might-be-tampered-with"
+                                             "All good!")))
+                    (mount "none" input "none" (logior MS_BIND MS_REMOUNT))
+                    (call-with-output-file input
+                      (lambda (port)
+                        (display "BAD!" port)))
+                    (mkdir #$output))))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "build root cannot be made world-readable"
+  (let ((drv
+         (run-with-store %store
+           (gexp->derivation
+            "attempt-to-make-root-world-readable"
+            (with-imported-modules (source-module-closure
+                                    '((guix build syscalls)))
+              #~(begin
+                  (use-modules (guix build syscalls))
+
+                  (catch 'system-error
+                    (lambda ()
+                      (chmod "/" #o777))
+                    (lambda args
+                      (format #t "failed to make root writable: ~a~%"
+                              (strerror (system-error-errno args)))
+                      (format #t "attempting read-write remount~%")
+                      (mount "none" "/" "/" (logior MS_BIND MS_REMOUNT))
+                      (chmod "/" #o777)))
+
+                  ;; At this point, the build process could create a
+                  ;; world-readable setuid binary under its root (so in the
+                  ;; store) that would remain visible until the build
+                  ;; completes.
+                  (mkdir #$output)))))))
+    (guard (c ((store-protocol-error? c) #t))
+      (build-derivations %store (list drv))
+      #f)))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "/tmp, store, and /dev/{null,full} are writable"
+  ;; All of /tmp and all of the store must be writable (the store is writable
+  ;; so that derivation outputs can be written to it, but in practice it's
+  ;; always been wide open).  Things like /dev/null must be writable too.
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-tmp-and-store-are-writable"
+                #~(begin
+                    (mkdir "/tmp/something")
+                    (mkdir (in-vicinity (getenv "NIX_STORE")
+                                        "some-other-thing"))
+                    (call-with-output-file "/dev/null"
+                      (lambda (port)
+                        (display "Welcome to the void." port)))
+                    (catch 'system-error
+                      (lambda ()
+                        (call-with-output-file "/dev/full"
+                          (lambda (port)
+                            (display "No space left!" port)))
+                        (error "Should have thrown!"))
+                      (lambda args
+                        (unless (= ENOSPC (system-error-errno args))
+                          (apply throw args))))
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
+(unless (unprivileged-user-namespace-supported?)
+  (test-skip 1))
+(test-assert "network is unreachable"
+  (let ((drv (run-with-store %store
+               (gexp->derivation
+                "check-network-unreachable"
+                #~(let ((check-connection-failure
+                         (lambda (address expected-code)
+                           (let ((s (socket AF_INET SOCK_STREAM 0)))
+                             (catch 'system-error
+                               (lambda ()
+                                 (connect s AF_INET (inet-pton AF_INET address) 80))
+                               (lambda args
+                                 (let ((errno (system-error-errno args)))
+                                   (unless (= expected-code errno)
+                                     (error "wrong error code"
+                                            errno (strerror errno))))))))))
+                    (check-connection-failure "127.0.0.1" ECONNREFUSED)
+                    (check-connection-failure "9.9.9.9" ENETUNREACH)
+                    (mkdir #$output))))))
+    (build-derivations %store (list drv))))
+
 (test-equal "with-build-handler"
   'success
   (let* ((b  (add-text-to-store %store "build" "echo $foo > $out" '()))
@@ -1333,40 +1519,31 @@ (define %shell
 
 (test-assert "build-things, check mode"
   (with-store store
-    (call-with-temporary-output-file
-     (lambda (entropy entropy-port)
-       (write (random-text) entropy-port)
-       (force-output entropy-port)
-       (let* ((drv  (build-expression->derivation
-                     store "non-deterministic"
-                     `(begin
-                        (use-modules (rnrs io ports))
-                        (let ((out (assoc-ref %outputs "out")))
-                          (call-with-output-file out
-                            (lambda (port)
-                              ;; Rely on the fact that tests do not use the
-                              ;; chroot, and thus ENTROPY is readable.
-                              (display (call-with-input-file ,entropy
-                                         get-string-all)
-                                       port)))
-                          #t))
-                     #:guile-for-build
-                     (package-derivation store %bootstrap-guile (%current-system))))
-              (file (derivation->output-path drv)))
-         (and (build-things store (list (derivation-file-name drv)))
-              (begin
-                (write (random-text) entropy-port)
-                (force-output entropy-port)
-                (guard (c ((store-protocol-error? c)
-                           (pk 'determinism-exception c)
-                           (and (not (zero? (store-protocol-error-status c)))
-                                (string-contains (store-protocol-error-message c)
-                                                 "deterministic"))))
-                  ;; This one will produce a different result.  Since we're in
-                  ;; 'check' mode, this must fail.
-                  (build-things store (list (derivation-file-name drv))
-                                (build-mode check))
-                  #f))))))))
+    (let* ((drv  (build-expression->derivation
+                  store "non-deterministic"
+                  `(begin
+                     (use-modules (rnrs io ports))
+                     (let ((out (assoc-ref %outputs "out")))
+                       (call-with-output-file out
+                         (lambda (port)
+                           (let ((now (gettimeofday)))
+                             (display (+ (car now) (cdr now)) port))))
+                       #t))
+                  #:guile-for-build
+                  (package-derivation store %bootstrap-guile (%current-system))))
+           (file (derivation->output-path drv)))
+      (and (build-things store (list (derivation-file-name drv)))
+           (begin
+             (guard (c ((store-protocol-error? c)
+                        (pk 'determinism-exception c)
+                        (and (not (zero? (store-protocol-error-status c)))
+                             (string-contains (store-protocol-error-message c)
+                                              "deterministic"))))
+               ;; This one will produce a different result.  Since we're in
+               ;; 'check' mode, this must fail.
+               (build-things store (list (derivation-file-name drv))
+                             (build-mode check))
+               #f))))))
 
 (test-assert "build-succeeded trace in check mode"
   (string-contains
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:28:07 GMT) Full text and rfc822 format available.

Message #448 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 14/16] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
Date: Sun, 23 Mar 2025 15:25:07 +0100
* etc/guix-daemon.service.in (ExecStart): Remove ‘--build-users-group’.
(Environment): Add ‘GUIX_STATE_DIRECTORY’.
(Before, User, AmbientCapabilities, PrivateMounts, BindPaths): New fields.
* etc/gnu-store.mount.in (Before): Remove.
(WantedBy): Change to ‘multi-user.target’.

Change-Id: Id826b8ab535844b6024d777f6bd15fd49db6d65e
---
 etc/gnu-store.mount.in     |  3 +--
 etc/guix-daemon.service.in | 22 ++++++++++++++++++++--
 2 files changed, 21 insertions(+), 4 deletions(-)

diff --git a/etc/gnu-store.mount.in b/etc/gnu-store.mount.in
index c94f2db72b..f9918c9e52 100644
--- a/etc/gnu-store.mount.in
+++ b/etc/gnu-store.mount.in
@@ -2,10 +2,9 @@
 Description=Read-only @storedir@ for GNU Guix
 DefaultDependencies=no
 ConditionPathExists=@storedir@
-Before=guix-daemon.service
 
 [Install]
-WantedBy=guix-daemon.service
+WantedBy=multi-user.target
 
 [Mount]
 What=@storedir@
diff --git a/etc/guix-daemon.service.in b/etc/guix-daemon.service.in
index 5c43d9b7f1..6a5ef97f9b 100644
--- a/etc/guix-daemon.service.in
+++ b/etc/guix-daemon.service.in
@@ -5,11 +5,29 @@
 [Unit]
 Description=Build daemon for GNU Guix
 
+# Start before 'gnu-store.mount' to get a writable view of the store.
+Before=gnu-store.mount
+
 [Service]
 ExecStart=@localstatedir@/guix/profiles/per-user/root/current-guix/bin/guix-daemon \
-    --build-users-group=guixbuild --discover=no \
+    --discover=no \
     --substitute-urls='@GUIX_SUBSTITUTE_URLS@'
-Environment='GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+Environment='GUIX_STATE_DIRECTORY=@localstatedir@/guix' 'GUIX_LOCPATH=@localstatedir@/guix/profiles/per-user/root/guix-profile/lib/locale' LC_ALL=en_US.utf8
+
+# Run under a dedicated unprivileged user account.
+User=guix-daemon
+
+# Bind-mount the store read-write in a private namespace, to counter the
+# effect of 'gnu-store.mount'.
+PrivateMounts=true
+BindPaths=@storedir@
+
+# Provide the CAP_CHOWN capability so that guix-daemon can create and chown
+# /var/guix/profiles/per-user/$USER and also chown failed build directories
+# when using '--keep-failed'.  Note that guix-daemon explicitly drops ambient
+# capabilities before executing build processes so they don't inherit them.
+AmbientCapabilities=CAP_CHOWN
+
 StandardOutput=journal
 StandardError=journal
 
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:28:09 GMT) Full text and rfc822 format available.

Message #451 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 16/16] DRAFT gnu: guix: Update to c9c7f87.
Date: Sun, 23 Mar 2025 15:25:09 +0100
DRAFT: Temporary commit.

* gnu/packages/package-management.scm (guix): Update to c9c7f87.

Change-Id: Iec7bdb412f5a2779d90a8c95582d2e59bd178b35
---
 gnu/packages/package-management.scm | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/gnu/packages/package-management.scm b/gnu/packages/package-management.scm
index 099c9ea67d..af3673469f 100644
--- a/gnu/packages/package-management.scm
+++ b/gnu/packages/package-management.scm
@@ -179,8 +179,8 @@ (define-public guix
   ;; Note: the 'update-guix-package.scm' script expects this definition to
   ;; start precisely like this.
   (let ((version "1.4.0")
-        (commit "5058b40aba825ab6e7b9e518dd1147d1e35fd7de")
-        (revision 34))
+        (commit "c9c7f8753008d12bcd71b7f0fbe2132bdbcb4f5a")
+        (revision 35))
     (package
       (name "guix")
 
@@ -196,7 +196,7 @@ (define-public guix
                       (commit commit)))
                 (sha256
                  (base32
-                  "04vk4lslcd6h22yj5pxvb1pdyyxd8421gjfyvyb1bl3xn7c77246"))
+                  "1l03pgc3587cpjqnfqw4l3bqhxlqn26i63jcxvgi43dfans7krgz"))
                 (file-name (string-append "guix-" version "-checkout"))))
       (build-system gnu-build-system)
       (arguments
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:28:11 GMT) Full text and rfc822 format available.

Message #454 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: 75810 <at> debbugs.gnu.org
Cc: Ludovic Courtès <ludo <at> gnu.org>
Subject: [PATCH v8 15/16] guix-install.sh: Support the unprivileged daemon
 where possible.
Date: Sun, 23 Mar 2025 15:25:08 +0100
* etc/guix-install.sh (create_account): New function.
(sys_create_build_user): Use it.  When ‘guix-daemon.service’ contains
“User=guix-daemon” only create the ‘guix-daemon’ user and group.
(sys_delete_build_user): Delete the ‘guix-daemon’ user and group.
(can_install_unprivileged_daemon): New function.
(sys_create_store): When installing the unprivileged daemon, change
ownership of /gnu and /var/guix, and create /var/log/guix.
(sys_authorize_build_farms): When the ‘guix-daemon’ account exists,
change ownership of /etc/guix.

Change-Id: I73e573f1cc5c0cb3794aaaa6b576616b66e0c5e9
---
 etc/guix-install.sh | 124 +++++++++++++++++++++++++++++++++++---------
 1 file changed, 99 insertions(+), 25 deletions(-)

diff --git a/etc/guix-install.sh b/etc/guix-install.sh
index 8887204df4..b5d833cd64 100755
--- a/etc/guix-install.sh
+++ b/etc/guix-install.sh
@@ -414,6 +414,11 @@ sys_create_store()
     cd "$tmp_path"
     _msg_info "Installing /var/guix and /gnu..."
     # Strip (skip) the leading ‘.’ component, which fails on read-only ‘/’.
+    #
+    # TODO: Eventually extract with ‘--owner=guix-daemon’ when installing
+    # and unprivileged guix-daemon service; for now, this script may install
+    # from both an old release that does not support unprivileged guix-daemon
+    # and a new release that does, so ‘chown -R’ later if needed.
     tar --extract --strip-components=1 --file "$pkg" -C /
 
     _msg_info "Linking the root user's profile"
@@ -441,38 +446,95 @@ sys_delete_store()
     rm -rf ~root/.config/guix
 }
 
+create_account()
+{
+    local user="$1"
+    local group="$2"
+    local supplementary_groups="$3"
+    local comment="$4"
+
+    if id "$user" &>/dev/null; then
+	_msg_info "user '$user' is already in the system, reset"
+	usermod -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" "$user"
+    else
+	useradd -g "$group" -G "$supplementary_groups"	\
+		-d /var/empty -s "$(which nologin)"	\
+		-c "$comment" --system "$user"
+	_msg_pass "user added <$user>"
+    fi
+}
+
+install_unprivileged_daemon()
+{ # Return true when installing guix-daemon running without privileges.
+    [ "$INIT_SYS" = systemd ] && \
+	grep -q "User=guix-daemon" \
+	     ~root/.config/guix/current/lib/systemd/system/guix-daemon.service
+}
+
 sys_create_build_user()
 { # Create the group and user accounts for build users.
 
     _debug "--- [ ${FUNCNAME[0]} ] ---"
 
-    if getent group guixbuild > /dev/null; then
-        _msg_info "group guixbuild exists"
-    else
-        groupadd --system guixbuild
-        _msg_pass "group <guixbuild> created"
-    fi
-
     if getent group kvm > /dev/null; then
         _msg_info "group kvm exists and build users will be added to it"
         local KVMGROUP=,kvm
     fi
 
-    for i in $(seq -w 1 10); do
-        if id "guixbuilder${i}" &>/dev/null; then
-            _msg_info "user is already in the system, reset"
-            usermod -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i"             \
-                    "guixbuilder${i}";
-        else
-            useradd -g guixbuild -G guixbuild"$KVMGROUP"     \
-                    -d /var/empty -s "$(which nologin)" \
-                    -c "Guix build user $i" --system    \
-                    "guixbuilder${i}";
-            _msg_pass "user added <guixbuilder${i}>"
-        fi
-    done
+    if install_unprivileged_daemon
+    then
+	_msg_info "installing guix-daemon to run as an unprivileged user"
+
+	# Installing guix-daemon to run as a non-root user requires
+	# unprivileged user namespaces.
+	if [ -f /proc/sys/kernel/unprivileged_userns_clone ] \
+	       && [ "$(cat /proc/sys/kernel/unprivileged_userns_clone)" -ne 1 ]
+	then
+	    echo 1 > /proc/sys/kernel/unprivileged_userns_clone || \
+		_err "failed to enable unprivileged user namespaces"
+
+	    _msg_warn "Unprivileged user namespaces were disabled and have been enabled now."
+	    _msg_warn "This Linux feature is required by guix-daemon.  To enable it permanently, run:"
+	    _msg_warn '  echo 1 > /proc/sys/kernel/unprivileged_userns_clone'
+	    _msg_warn "from the relevant startup script."
+	fi
+
+
+	if getent group guix-daemon > /dev/null; then
+	    _msg_info "group guix-daemon exists"
+	else
+	    groupadd --system guix-daemon
+	    _msg_pass "group guix-daemon created"
+	fi
+
+	create_account guix-daemon guix-daemon		\
+		       guix-daemon$KVMGROUP		\
+		       "Unprivileged Guix Daemon User"
+
+	# ‘tar xf’ creates root:root files.  Change that.
+	chown -R guix-daemon:guix-daemon /gnu /var/guix
+	chown -R root:root /var/guix/profiles/per-user/root
+
+	# The unprivileged daemon cannot create the log directory by itself.
+	mkdir -p /var/log/guix
+	chown guix-daemon:guix-daemon /var/log/guix
+	chmod 755 /var/log/guix
+    else
+	if getent group guixbuild > /dev/null; then
+            _msg_info "group guixbuild exists"
+	else
+            groupadd --system guixbuild
+            _msg_pass "group <guixbuild> created"
+	fi
+
+	for i in $(seq -w 1 10); do
+	    create_account "guixbuilder${i}" "guixbuild"	\
+	                   "guixbuild${KVMGROUP}"		\
+			   "Guix build user $i"
+	done
+    fi
 }
 
 sys_delete_build_user()
@@ -487,6 +549,14 @@ sys_delete_build_user()
     if getent group guixbuild &>/dev/null; then
         groupdel -f guixbuild
     fi
+
+    _msg_info "remove guix-daemon user"
+    if id guix-daemon &>/dev/null; then
+	userdel -f guix-daemon
+    fi
+    if getent group guix-daemon &>/dev/null; then
+	groupdel -f guix-daemon
+    fi
 }
 
 sys_enable_guix_daemon()
@@ -529,11 +599,11 @@ sys_enable_guix_daemon()
 
               # Install after guix-daemon.service to avoid a harmless warning.
               # systemd .mount units must be named after the target directory.
-              # Here we assume a hard-coded name of /gnu/store.
-              install_unit gnu-store.mount
+	      install_unit gnu-store.mount
 
               systemctl daemon-reload &&
-                  systemctl start  guix-daemon; } &&
+                  systemctl start guix-daemon &&
+	          systemctl start gnu-store.mount; } &&
                 _msg_pass "enabled Guix daemon via systemd"
             ;;
         sysv-init)
@@ -654,6 +724,10 @@ project's build farms?"; then
 		&& guix archive --authorize < "$key" \
 		&& _msg_pass "Authorized public key for $host"
 	done
+	if id guix-daemon &>/dev/null; then
+	    # /etc/guix/acl must be readable by the unprivileged guix-daemon.
+	    chown -R guix-daemon:guix-daemon /etc/guix
+	fi
     else
         _msg_info "Skipped authorizing build farm public keys"
     fi
-- 
2.48.1





Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 14:31:05 GMT) Full text and rfc822 format available.

Message #457 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810 <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v6 00/16] Rootless guix-daemon
Date: Sun, 23 Mar 2025 15:30:29 +0100
Hello,

Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> The handle is a purely user-space sequence of bytes, and is not
> namespaced whatsoever.  In other words, the first "half" (that is,
> name_to_handle_at) is completely optional, as long as you have a good
> idea of what sort of handle values to try.  This means that, if a
> process has this capability in the root user namespace, they can
> potentially access every file of any filesystem that has at least one
> file visible to them.  Note that "filesystem" here is not the same thing
> as "mount point", so this means that if you have a bind mount from the
> root filesystem in the container (or the root filesystem itself in the
> container is on the out-of-container root filesystem), a process in the
> container but with CAP_DAC_READ_SEARCH in the root user namespace could
> access *every file on the real root filesystem*.  This is how an exploit
> for Docker named "shocker" worked
> (http://stealth.openwall.net/xSports/shocker.c), caused by Docker
> leaving CAP_DAC_READ_SEARCH available by default in privileged
> containers.

Ouch. I think the conceptual quagmire stemming from the accumulation of
features retrofitted on the otherwise simpler 1970 Unix model doesn’t
help: mount points, file systems, namespaces,
shared/locked/private/slave subtrees, “capabilities”, ACLs, etc.
It’s intractable.

> I of course hope that the kernel's relaxing of the rules to also allow
> open_by_handle_at in some situations in non-root user namespaces has
> been carefully thought through to not open any holes like this, but it
> would be good to keep an eye on it regardless.

Indeed. :-/

Ludo’.

PS: Just sent v8.




Information forwarded to guix-patches <at> gnu.org:
bug#75810; Package guix-patches. (Sun, 23 Mar 2025 21:46:02 GMT) Full text and rfc822 format available.

Message #460 received at 75810 <at> debbugs.gnu.org (full text, mbox):

From: Reepca Russelstein <reepca <at> russelstein.xyz>
To: Ludovic Courtès <ludo <at> gnu.org>
Cc: Ludovic Courtès <ludovic.courtes <at> inria.fr>,
 75810 <at> debbugs.gnu.org
Subject: Re: [PATCH v8 07/16] daemon: Allow running as non-root with
 unprivileged user namespaces.
Date: Sun, 23 Mar 2025 16:44:44 -0500
[Message part 1 (text/plain, inline)]
Looks good to me.

- reepca
[signature.asc (application/pgp-signature, inline)]

Reply sent to Ludovic Courtès <ludo <at> gnu.org>:
You have taken responsibility. (Wed, 26 Mar 2025 17:08:04 GMT) Full text and rfc822 format available.

Notification sent to Ludovic Courtès <ludo <at> gnu.org>:
bug acknowledged by developer. (Wed, 26 Mar 2025 17:08:05 GMT) Full text and rfc822 format available.

Message #465 received at 75810-done <at> debbugs.gnu.org (full text, mbox):

From: Ludovic Courtès <ludo <at> gnu.org>
To: Reepca Russelstein <reepca <at> russelstein.xyz>
Cc: 75810-done <at> debbugs.gnu.org
Subject: Re: [bug#75810] [PATCH v8 07/16] daemon: Allow running as non-root
 with unprivileged user namespaces.
Date: Wed, 26 Mar 2025 18:07:13 +0100
Reepca Russelstein <reepca <at> russelstein.xyz> skribis:

> Looks good to me.

Pushed as a9239a769c5611f12061c9a895e4e218b3445e2f.

  a9239a769c * guix-install.sh: Support the unprivileged daemon where possible.
  107eb8ee8f * etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
  2f65438eba * tests: Run in a chroot and unprivileged user namespaces.
  f854095b6f * tests: Add missing derivation inputs.
  29164192e9 * linux-container: ‘unprivileged-user-namespace-supported?’ returns #f on non-Linux.
  bdd7b9a45d * daemon: Move comments where they belong.
  0163c732a1 * daemon: Drop Linux ambient capabilities before executing builder.
  a3d6f5ae70 * daemon: Create /var/guix/profiles/per-user unconditionally.
  ae18b3d9e6 * daemon: Allow running as non-root with unprivileged user namespaces.
  40f69b586a * daemon: Remount root directory as read-only.
  93474f9288 * daemon: Remount inputs as read-only.
  550ca89744 * daemon: Bind-mount all the inputs, not just directories.
  5c0b93b244 * daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
  7bad04fac0 * daemon: Close the read end of the logging pipe.
  f03e6eff2f * daemon: Use ‘close_range’ where available.

Thanks a *lot*, Reepca.

If you liked this patch series, surely you’ll enjoy this followup:

  https://issues.guix.gnu.org/77288

:-)

Ludo’.




bug archived. Request was from Debbugs Internal Request <help-debbugs <at> gnu.org> to internal_control <at> debbugs.gnu.org. (Thu, 24 Apr 2025 11:25:48 GMT) Full text and rfc822 format available.

This bug report was last modified 55 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.