[PATCH 0/6] Rootless guix-daemon

  • Open
  • quality assurance status badge
Details
7 participants
  • Janneke Nieuwenhuizen
  • Ludovic Courtès
  • Ludovic Courtès
  • Maxim Cournoyer
  • Noé Lopez
  • Reepca Russelstein
  • Simon Tournier
Owner
unassigned
Submitted by
Ludovic Courtès
Severity
normal
Blocked by

Debbugs page

Ludovic Courtès wrote 1 months ago
(address . guix-patches@gnu.org)(name . Ludovic Courtès)(address . ludovic.courtes@inria.fr)
cover.1737738362.git.ludo@gnu.org
From: Ludovic Courtès <ludovic.courtes@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’.


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
Ludovic Courtès wrote 1 months ago
[PATCH 1/6] daemon: Allow running as non-root with unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludovic.courtes@inria.fr)
41862c6aa51aa70c69a348635eb03a5ca8069695.1737738362.git.ludo@gnu.org
From: Ludovic Courtès <ludovic.courtes@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(-)

Toggle diff (271 lines)
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@gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
;;; Copyright © 2020 Christopher Baines <mail@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
Ludovic Courtès wrote 1 months ago
[PATCH 2/6] DRAFT tests: Run in a chroot and unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
6fb69ee481d628317ed09c6e762f38489bab0ea7.1737738362.git.ludo@gnu.org
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(-)

Toggle diff (156 lines)
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@gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo@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@gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo@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
Ludovic Courtès wrote 1 months ago
[PATCH 3/6] daemon: Create /var/guix/profiles/per-user unconditionally.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
d68dadd3e97944bc3339402d3be9fe8899b8c1dc.1737738362.git.ludo@gnu.org
* 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(-)

Toggle diff (23 lines)
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
Ludovic Courtès wrote 1 months ago
[PATCH 4/6] daemon: Drop Linux ambient capabilities before executing builder.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
d0fe57ac1b0f14d2fcfb01b9c9de80905cee73e1.1737738362.git.ludo@gnu.org
* 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(-)

Toggle diff (42 lines)
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
Ludovic Courtès wrote 1 months ago
[PATCH 5/6] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
713be4d9b744226c4922f3eec66adfb3547c95bc.1737738362.git.ludo@gnu.org
* 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(-)

Toggle diff (27 lines)
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
Ludovic Courtès wrote 1 months ago
[PATCH 6/6] guix-install.sh: Support the unprivileged daemon where possible.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
2c04ad0cb868e4f41047f971c05915c5419729bd.1737738362.git.ludo@gnu.org
* 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(-)

Toggle diff (165 lines)
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
Janneke Nieuwenhuizen wrote 1 months ago
Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
87ikq49fxx.fsf@gnu.org
Ludovic Courtès writes:

Hello!

Toggle quote (2 lines)
> 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...

[..]

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

Yay, awesome!

Toggle quote (2 lines)
> There’s a couple of cases where root is/was still necessary:

[..]

Toggle quote (8 lines)
> 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@gnu.org> | GNU LilyPond https://LilyPond.org
Freelance IT https://www.JoyOfSource.com| Avatar® https://AvatarAcademy.com
Ludovic Courtès wrote 1 months ago
(name . Janneke Nieuwenhuizen)(address . janneke@gnu.org)(address . 75810@debbugs.gnu.org)
87y0yzdffb.fsf@gnu.org
Hello,

Janneke Nieuwenhuizen <janneke@gnu.org> skribis:

Toggle quote (11 lines)
>> 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.

Toggle quote (3 lines)
> 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?

Toggle quote (8 lines)
> 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’.
Reepca Russelstein wrote 1 months ago
(address . 75810@debbugs.gnu.org)(address . ludo@gnu.org)
87r04qe7dj.fsf@russelstein.xyz
Toggle quote (9 lines)
> 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:

Toggle snippet (35 lines)
(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


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


Toggle snippet (17 lines)
user@debian:~$ guix build --no-grafts hello
/gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1
user@debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
Hello, world!
user@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@debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
GOOOOOD BYYEEEEEE
user@debian:~$

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.

Toggle quote (21 lines)
> 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.

Toggle quote (8 lines)
> 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.

Toggle quote (6 lines)
> 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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAmeVhCkXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJynXgf+JjAkPl4ovl0cvNj774zFtcoa
iXEzBEt9UX6Yu48Ja6f5OhqyuNd7ZxkZMCSz2ZrnOEABBk+hzTsRvg1VX1RFBqxo
jOyXWtPZYtSzFQdPL/CM5GD4oO8gW0QNf1/dn5cJDR1c4To6MSAt4v6CxLBspUZw
2DHKhGJwrKtFLWIR/6iFmmMzmn19npgFRjcL55Sb8qs691jvV1LmHJ4wN2E6p8M+
BtbWWulOGKClud2frYdI9zJp51iKIAm0V7xX6dnKhyz55OimlEv2vUHqktBOGehU
ymEqXwTf8ickK3XDPoYlRjcmv6BzuuQ4AR4I7ud8aHPF9rYSodXV64jNmnJ16w==
=3cRE
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 1 months ago
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
87bjvshrk0.fsf@gnu.org
Hello Reepca,

Reepca Russelstein <reepca@russelstein.xyz> skribis:

Toggle quote (3 lines)
> user@debian:~$ /gnu/store/8bjy9g0cssjrw9ljz2r8ww1sma95isfj-hello-2.12.1/bin/hello
> GOOOOOD BYYEEEEEE

This particular issue is fixed with read-only mounts:
Toggle diff (12 lines)
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.

Toggle quote (4 lines)
> 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 …’):

Toggle snippet (20 lines)
(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.

Toggle quote (21 lines)
>> 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.

[...]

Toggle quote (3 lines)
> 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:

Toggle snippet (16 lines)
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.

Toggle quote (8 lines)
> 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.

Toggle quote (15 lines)
> 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?

Toggle quote (3 lines)
> 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.

Toggle quote (10 lines)
> 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’.

Toggle quote (7 lines)
> 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’.
Noé Lopez wrote 1 months ago
Re: [PATCH 0/6] Rootless guix-daemon
(address . 75810@debbugs.gnu.org)
87ed0ox6wj.fsf@xn--no-cja.eu
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é
Ludovic Courtès wrote 1 months ago
(name . Noé Lopez)(address . noe@xn--no-cja.eu)
87r04nhpzy.fsf@gnu.org
Hi,

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

Toggle quote (3 lines)
> 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.

Toggle quote (6 lines)
> 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’.
Reepca Russelstein wrote 1 months ago
Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
87msfadpmi.fsf@russelstein.xyz
Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (29 lines)
> Hello Reepca,
>
> Reepca Russelstein <reepca@russelstein.xyz> skribis:
>
>> user@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.

Toggle quote (43 lines)
>> 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.


Toggle quote (28 lines)
> - 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.

Toggle quote (29 lines)
>> 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.

Toggle quote (21 lines)
>> 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.

Toggle quote (13 lines)
>> 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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAmeZ3gYXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJzAkQf/VZjUTwuxVy5Aid8p4p+ovhT0
0tkp7zheyG8TojG/YSgBhjF4YXfA5vAymdMjCMFCmt6J3gIlOgGjgbDyVylvzFxG
KnE5nYXnujP2XKJ61pbWKVrAP2Lqdz7gGq+EKu9dCsHBDkPQkWo0idoSW6oIdXSF
EISvUGtZ5wrzm6uAl5D0YINqw/aAEbharanfZYin2eRIy7hH5k598Wca7hgBC9e0
fDy+dBn7vME3bUzitXHpvdZVsgHOSDpKogacsIJRbsAqEVPYXNU/tN5xTjomb5dM
ycF4epwPKqDcy+/rWX3N3vP266E5vxUL3SvQ0ujYlafrC+OiEICySu4BLTPoSA==
=6lw/
-----END PGP SIGNATURE-----

Reepca Russelstein wrote 1 months ago
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
875xluehn7.fsf@russelstein.xyz
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:

Toggle snippet (29 lines)
(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

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

Toggle snippet (35 lines)
user@debian:~$ guix build --derivations --no-grafts -f /tmp/mal-test3.scm
/gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv
user@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@debian:~$
user@debian:~$ ls /gnu/store/qx5m1iq72628qy90wpwczypzfc28ss57-sneakysneaky-0.drv.chroot
dev etc gnu guile proc tmp
user@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@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@debian:~$


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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAmedUCwXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJyBtQgAmXPI8k1Os1tVCb6Py8Rn++1Q
tPR0TL9wItX0hN+RQTHNpIsPE6hbgc2FgKQ3zbIUk2D4VGn8YA1qh9nFt/UJMuoK
QGHbE+6ctuADAbU4KubRR5N/NxyP+IupG7zttEtiO6rOGfSeoVaugdsjoLE5ZUx1
MX9qr5asjfx8SO8oEkafM7bQpDDNJrTSj8OWrQ5KhJclIwMdaXVFGScJUU4igvZX
LfL4Bo8tnK2V6urP87xEDindHRBUsFLvI//TTUuZzl1qyYmB6+AFtRTfpr/zIQH5
m2gOdoXi3mbdOnNwD7q+BCfC7Nh+a/1mmfJrP/mRHfosYen4Jl8wnjWkFgucNQ==
=jv04
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 3 weeks ago
[PATCH v2 0/9] Rootless daemon
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
cover.1739448513.git.ludo@gnu.org
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 1/9] daemon: Use ‘close_range ’ where available.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
bd91ab23e03ab06c821e9e41b69c0f87c7945a85.1739448513.git.ludo@gnu.org
* 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(-)

Toggle diff (66 lines)
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 2/9] daemon: Bind-mount all the inputs, not just directories.
(address . 75810@debbugs.gnu.org)
1a481f7f9df95b1c76e69c5e923fa30098e33cbe.1739448513.git.ludo@gnu.org
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.

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

Toggle diff (47 lines)
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 6/9] daemon: Create /var/guix/profiles/per-user unconditionally.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
d6e5382ab3c33c467cb753969ca574561e90d8de.1739448513.git.ludo@gnu.org
* 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(-)

Toggle diff (23 lines)
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 7/9] daemon: Drop Linux ambient capabilities before executing builder.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
06b47bf0dcee78ac73405afe118f7ed7f0a374fa.1739448513.git.ludo@gnu.org
* 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(-)

Toggle diff (42 lines)
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 5/9] DRAFT tests: Run in a chroot and unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
913c41825df54b9c7d1fbad1fb1e7e9f48bd0d13.1739448513.git.ludo@gnu.org
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(-)

Toggle diff (213 lines)
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@gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo@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@gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo@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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 4/9] daemon: Allow running as non-root with unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludovic.courtes@inria.fr)
eb3f91a7e866f9a1565afd094a158269f548b89e.1739448513.git.ludo@gnu.org
From: Ludovic Courtès <ludovic.courtes@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(-)

Toggle diff (289 lines)
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@gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
;;; Copyright © 2020 Christopher Baines <mail@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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 9/9] guix-install.sh: Support the unprivileged daemon where possible.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
107f6e7972e083aa645e8a1bc121750c4d94fc1f.1739448513.git.ludo@gnu.org
* 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(-)

Toggle diff (165 lines)
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 3/9] daemon: Remount inputs as read-only.
(address . 75810@debbugs.gnu.org)
15b9f858aa6c636cd02801c35bbcc467a184d1b0.1739448513.git.ludo@gnu.org
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

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

Toggle diff (22 lines)
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
Ludovic Courtès wrote 3 weeks ago
[PATCH v2 8/9] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
a706daddbcc0193cda052bd3e0c76c6cf355a9bf.1739448513.git.ludo@gnu.org
* 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(-)

Toggle diff (27 lines)
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
Ludovic Courtès wrote 3 weeks ago
Re: [bug#75810] [PATCH 0/6] Rootless guix-daemon
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
877c5u2ct5.fsf@gnu.org
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:

Toggle quote (8 lines)
> #~(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’.
Ludovic Courtès wrote 3 weeks ago
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
87v7tcy2hb.fsf@gnu.org
Ludovic Courtès <ludo@gnu.org> skribis:

Toggle quote (3 lines)
> 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’.
Reepca Russelstein wrote 3 weeks ago
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
87jz9sc72o.fsf@russelstein.xyz
Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (28 lines)
> 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.

Toggle quote (7 lines)
> 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.


Toggle quote (29 lines)
> @@ -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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAmev8jAXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJxhVQgAiklMsjAnY7wew/w14L0WhFXC
+esDHM9NfYrtJtozzjTuliqmRahG6G+y05Fi6PGRd7swJ6G2AjgpkM8MRVAt9wwC
2xigqEVy9y/kwGavnOjH2Dmapbek2nBXwjW1fAUgTZHEpFBiEOFHRRL34dEXJsJc
CA++5/rZxhA5BnBhgh+pxAshKgq/cfljq5kulelV6rHZlfOHKk8SpVQcXEf6tvue
jItLW8g9OirAjPPbI5hzE8Qy+fCw9GbAOE2zj34zqFVWkHTQmUkrpu5OOOR7rhUv
LDwM5Tw9jx0pBMmoxq5rfuD6CVdNLUj/H14zTJoaXfw3fGwrTo1GQGiOo8b3ew==
=AduA
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 3 weeks ago
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
87o6z2vqnt.fsf@gnu.org
Hi,

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

Reepca Russelstein <reepca@russelstein.xyz> skribis:

Toggle quote (8 lines)
> 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
does work as expected: / is read-only, /tmp and /gnu/store are
read-write, individual inputs in /gnu/store are read-only.

Toggle quote (5 lines)
> 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.

Toggle quote (3 lines)
> 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.

Toggle quote (11 lines)
> 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’?

Toggle quote (6 lines)
> 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.

Toggle quote (5 lines)
> 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’.
Reepca Russelstein wrote 3 weeks ago
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
87cyfid33v.fsf@russelstein.xyz
Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (9 lines)
>> 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.

Toggle quote (14 lines)
>> 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:

Toggle snippet (16 lines)
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());
}

Toggle quote (13 lines)
>> 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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAmexUAQXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJxqywf+O7JKSO8MJLvUU9VnXB+4ectV
Cew3MzUpCTI5hXGzZ+F0kL3APataYHG72g1Vmgtf1Q2tJVqG9NKFrMzKTI4qKXv9
+Vg/2/UOiDr3X1ACA7zMgz/hKZ6ZVwk13JE0S8JaBjiJKvYc+4FKKDd9G0ERdq6Y
RGxs2DsCDaHLefWEuzNASboh+AnOTqVI35HhwWDYibfXNmqMLT5TKCuph5jThPrO
Jdl8vzlaf9kbe3qI38/RWwkH3fF8N0pjNSAEcATB1YIFZJuTiyG2o/rInj5RgEce
t3PwYfuZ9JtRzU3JWYcaF+8mbiBHft4qsyTwzWxna4YZRgO1GxZSakwH+JGsAg==
=fmOO
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 3 weeks ago
Remounting the store read-write for guix-daemon
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
87h64sldip.fsf_-_@gnu.org
Hi,

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

Toggle quote (4 lines)
> 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’.

Ludovic Courtès wrote 2 weeks ago
[PATCH v3 01/11] daemon: Use ‘close_range ’ where available.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
47cd29cc645e87a85536da2d6edc676744c58cd5.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (66 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 00/11] Rootless guix-daemon
(address . 75810@debbugs.gnu.org)
cover.1740142328.git.ludo@gnu.org
Hello!

Here’s an updated version, addressing most issues brought up
by Reepca, also available from
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 07/11] daemon: Create /var/guix/profiles/per-user unconditionally.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
42766ef6f8486fd4e25a5f211883934a7bdb5256.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (23 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 02/11] daemon: Bind-mount all the inputs, not just directories.
(address . 75810@debbugs.gnu.org)
9819a4edaacbd5ed8d56094d6bb602f90f535be6.1740142328.git.ludo@gnu.org
* nix/libstore/build.cc (DerivationGoal::startBuilder): Add all of
‘inputPaths’ to ‘dirsInChroot’ instead of hard-linking regular files.

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

Toggle diff (47 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 04/11] daemon: Remount root directory as read-only.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
6272a7109a276d4ffa5cdd8b218b3233aaade5f5.1740142328.git.ludo@gnu.org
* 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(+)

Toggle diff (36 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 03/11] daemon: Remount inputs as read-only.
(address . 75810@debbugs.gnu.org)
e62cb42e65d821ec41f8394a49c28da693d78d77.1740142328.git.ludo@gnu.org
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

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

Toggle diff (22 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 08/11] daemon: Drop Linux ambient capabilities before executing builder.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
41e73aaabf721c22bfde3b0369a8e0d1a5694671.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (42 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 06/11] tests: Run in a chroot and unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
b97bc2f5d1a408c0f1007166f8654c03f11ca2c4.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (307 lines)
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@gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo@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@gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2019 Mathieu Othacehe <m.othacehe@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@gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo@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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 09/11] daemon: Move comments where they belong.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
ec4bc30e61d57621813fe5a4a3ec6c4036a805eb.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (32 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 10/11] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
32992bd69bfc6c3ff386c67ccfc2edeeb9fe7fd4.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (53 lines)
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
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 11/11] guix-install.sh: Support the unprivileged daemon where possible.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
bd7e1c6c599a505c4be0ee001f27bb6d9a973b5a.1740142328.git.ludo@gnu.org
* 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(-)

Toggle diff (159 lines)
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
Ludovic Courtès wrote 2 weeks ago
Re: [bug#75810] Remounting the store read-write for guix-daemon
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
87ikp38mxp.fsf@gnu.org
Ludovic Courtès <ludovic.courtes@inria.fr> skribis:

Toggle quote (7 lines)
> 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’.
Ludovic Courtès wrote 2 weeks ago
[PATCH v3 05/11] daemon: Allow running as non-root with unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludovic.courtes@inria.fr)
1f4adc1c09dde70b193e1571b250e6152f0b4ca2.1740142328.git.ludo@gnu.org
From: Ludovic Courtès <ludovic.courtes@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(-)

Toggle diff (303 lines)
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@gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
;;; Copyright © 2020 Christopher Baines <mail@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
Simon Tournier wrote 2 weeks ago
Re: [bug#75810] [PATCH v3 00/11] Rootless guix-daemon
87jz9jrzfo.fsf@gmail.com
Hi Ludo,

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

Toggle quote (7 lines)
> 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@gnu.org>
Fri, 24 Jan 2025 20:20:42 +0100
id:87ikq49fxx.fsf@gnu.org
Reepca Russelstein wrote 2 weeks ago
Re: [PATCH v3 00/11] Rootless guix-daemon
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
877c5idit1.fsf@russelstein.xyz
Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (16 lines)
> 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?

Toggle quote (7 lines)
>
> • ‘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.

Toggle quote (7 lines)
> 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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAme5AIoXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJxgYwgAhCMl0Qtku5lc4mEsPXe8b9pV
+yE3q1BYWa3Wbo4Cy9M5gYUG9l7R5jk1wRD7g7BZJAcN7IwbUwBzXpHVaRgBn81P
ZrduzqK4I42w5PxSc8VU274uWVrHAHu8P6kvV+EKlQL1ooiMi7DhTytFNXlaiPEI
nAgZJKiKxEfkvgTiAWDySuM7EDQ+CZ5CDn7k9bOsYJjeDqCW7CSIt3jhNz/Sltg8
3pijCCCa6sFSdxUVJd9wPPK+AulN3oMHz0JfWKPotsjKM2nSt+jwWXZIZwUvOyVo
2VNQ+gOqA8acA4vKFMcm6fZ2YNko0y/TJOn/vcKXmxU4lvzJPdmaBFZvztpsEQ==
=zez6
-----END PGP SIGNATURE-----

Ludovic Courtès wrote 2 weeks ago
Re: [bug#75810] [PATCH v3 00/11] Rootless guix-daemon
(address . 75810@debbugs.gnu.org)(name . Reepca Russelstein)(address . reepca@russelstein.xyz)
87bjut6h0d.fsf@gnu.org
Ludovic Courtès <ludo@gnu.org> skribis:

Toggle quote (4 lines)
> Next up:
>
> • automating ‘guix-install.sh’ VM tests;

Ludovic Courtès wrote 2 weeks ago
(name . Simon Tournier)(address . zimon.toutoune@gmail.com)
87y0xx528u.fsf@gnu.org
Hi,

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

Toggle quote (5 lines)
> 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.

Toggle quote (4 lines)
> 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.)

Toggle quote (4 lines)
>>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.

Toggle quote (4 lines)
> 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’).

Toggle quote (2 lines)
> If yes, cool! It’ll be a booster for cluster sysadmins. :-)

Ludovic Courtès wrote 1 weeks ago
(name . Reepca Russelstein)(address . reepca@russelstein.xyz)(address . 75810@debbugs.gnu.org)
87wmdatner.fsf@gnu.org
Hi,

Reepca Russelstein <reepca@russelstein.xyz> skribis:

Toggle quote (8 lines)
>> • 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.

[...]

Toggle quote (9 lines)
> 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.

Toggle quote (5 lines)
> 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?

Toggle quote (11 lines)
>> 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’.
Ludovic Courtès wrote 1 weeks ago
control message for bug #75810
(address . control@debbugs.gnu.org)
87mse6tjpv.fsf@gnu.org
block 75810 by 76376
quit
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 01/14] daemon: Use ‘close_range ’ where available.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
a6cdda9b1569fa4211e08164af40f1f1ede5ac53.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (66 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 00/14] Rootless guix-daemon
(address . 75810@debbugs.gnu.org)
cover.1740752774.git.ludo@gnu.org
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:


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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 02/14] daemon: Bind-mount /etc/nsswitch.conf & co. only if it exists.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
9c8ee0e95ead78ad6d34459f998097413e9f9bf8.1740752774.git.ludo@gnu.org
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(-)

Toggle diff (22 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 03/14] daemon: Bind-mount all the inputs, not just directories.
(address . 75810@debbugs.gnu.org)
99aa9bb6ad2104a891536b2cc94db64cbe7ad0ba.1740752774.git.ludo@gnu.org
* 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@russelstein.xyz>
Change-Id: I070987f92d73f187f7826a975bee9ee309d67f56
---
nix/libstore/build.cc | 39 ++++++++++++++-------------------------
1 file changed, 14 insertions(+), 25 deletions(-)

Toggle diff (69 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 07/14] daemon: Create /var/guix/profiles/per-user unconditionally.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
1ffc921092a6ef697f1ee1ded2c256afbd985723.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (23 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 05/14] daemon: Remount root directory as read-only.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
f3474c5bb726b06503c9ccc1758019ba327b21cd.1740752774.git.ludo@gnu.org
* 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(+)

Toggle diff (36 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 09/14] daemon: Move comments where they belong.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
7f3478a0fd9d7e26d5c5abda46c2821900c8bff6.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (32 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 08/14] daemon: Drop Linux ambient capabilities before executing builder.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
ed036f6e200cadf86df275a91b768693c6579b06.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (42 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 10/14] tests: Add missing derivation inputs.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
f32d600f0b63651c64531ff5df0dbedd4152bcc4.1740752774.git.ludo@gnu.org
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(-)

Toggle diff (153 lines)
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@gnu.org>
+;;; Copyright © 2012-2025 Ludovic Courtès <ludo@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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 14/14] DRAFT gexp: No symlinks for ‘imported-files/derivation’.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
4f4dfe6ff53cb4ff0832a0f02640735c0e3fa52a.1740752774.git.ludo@gnu.org

* 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(-)

Toggle diff (18 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 12/14] etc: systemd services: Run ‘guix-daemon’ as an unprivileged user.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
ad43a913694738760d5e4f77af620af19d228801.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (53 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 04/14] daemon: Remount inputs as read-only.
(address . 75810@debbugs.gnu.org)
2c64a81a3bcd1ea6e79d24dea82ea6bf28af6aac.1740752774.git.ludo@gnu.org
* nix/libstore/build.cc (DerivationGoal::runChild): Remount ‘target’ as
read-only.

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

Toggle diff (22 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 11/14] tests: Run in a chroot and unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
fd153e1ba1a543e35561649d7b3e9d472d4f9ef5.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (353 lines)
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@gnu.org>
+# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès <ludo@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@gnu.org>
+;;; Copyright © 2018, 2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2019 Mathieu Othacehe <m.othacehe@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@gnu.org>
+;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès <ludo@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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 13/14] guix-install.sh: Support the unprivileged daemon where possible.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludo@gnu.org)
26dd92508755f40dbca161e81f801b3c212e1c86.1740752774.git.ludo@gnu.org
* 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(-)

Toggle diff (157 lines)
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
Ludovic Courtès wrote 1 weeks ago
[PATCH v4 06/14] daemon: Allow running as non-root with unprivileged user namespaces.
(address . 75810@debbugs.gnu.org)(name . Ludovic Courtès)(address . ludovic.courtes@inria.fr)
10c042b5dfa44688c5e4aa0e72bd4f7d1ebf6eb5.1740752774.git.ludo@gnu.org
From: Ludovic Courtès <ludovic.courtes@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(-)

Toggle diff (432 lines)
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@gnu.org>
+;;; Copyright © 2013-2021, 2023-2025 Ludovic Courtès <ludo@gnu.org>
;;; Copyright © 2014 Nikita Karetnikov <nikita@karetnikov.org>
;;; Copyright © 2018 Kyle Meyer <kyle@kyleam.com>
;;; Copyright © 2020 Christopher Baines <mail@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
Simon Tournier wrote 1 weeks ago
87senyggjy.fsf@gmail.com
Hi,

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

Toggle quote (8 lines)
> * 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 +++++++++++++++++++-------

[...]

Toggle quote (7 lines)
> 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

[...]

Toggle quote (17 lines)
> +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.

Toggle quote (7 lines)
> +@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”.

Toggle quote (10 lines)
> +@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 …

Toggle quote (9 lines)
> -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
Reepca Russelstein wrote 1 weeks ago
Re: [PATCH v4 00/14] Rootless guix-daemon
(name . Ludovic Courtès)(address . ludo@gnu.org)(address . 75810@debbugs.gnu.org)
8734fw7t93.fsf@russelstein.xyz
Ludovic Courtès <ludo@gnu.org> writes:

Toggle quote (8 lines)
> 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:

Toggle quote (48 lines)
> @@ -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.


Toggle quote (4 lines)
> 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
-----BEGIN PGP SIGNATURE-----

iQFLBAEBCAA1FiEEdNapMPRLm4SepVYGwWaqSV9/GJwFAmfDESkXHHJlZXBjYUBy
dXNzZWxzdGVpbi54eXoACgkQwWaqSV9/GJzjmQgAon0XOWbS7jwlulptpAiXIkXA
V1q/837h5N/qLH6goZ7YAoikyQ/DuooCmU7Y8PrIF80y1y6Vi7mPD32Cx9/pKsXG
ktHTM9skGqvntzozqUHHzjzZm8V5zNSANdnBb+luZvnKsZHQdxh0dzN5hokK8y1x
7CIj1pGS0sSb6lhiFm1RSuCw3FegxltZeKPb+OJrjfSXZWtN14nvQqdRTLLX01zj
6Yh8k5tJetYs4FWOwjsa8BzsqIGnoOtjb11KOiBfsHcgSoWgGRyGEDhYbnLbXpw4
0wG03p8ble2YGqMbRiJ2zOSt+cVcdpf3OQ7Mgd34XL8WVSCxBG1hf5YoJrim2A==
=DRJH
-----END PGP SIGNATURE-----

Maxim Cournoyer wrote 7 days ago
Re: [bug#75810] [PATCH v4 06/14] daemon: Allow running as non-root with unprivileged user namespaces.
(name . Ludovic Courtès)(address . ludo@gnu.org)
87senwezge.fsf@gmail.com
Hi Ludo,

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

Toggle quote (26 lines)
> From: Ludovic Courtès <ludovic.courtes@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!

[...]

Toggle quote (14 lines)
> +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.

Toggle quote (31 lines)
> +
> +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.

[...]

Toggle quote (40 lines)
> 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.

[...]

Toggle quote (25 lines)
> #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@gmail>

--
Thanks,
Maxim
Ludovic Courtès wrote 5 days ago
(name . Simon Tournier)(address . zimon.toutoune@gmail.com)
87zfi2dogt.fsf@gnu.org
Hi,

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

Toggle quote (21 lines)
>> +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.

Toggle quote (9 lines)
>> +@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.

Toggle quote (7 lines)
>> 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’.
Ludovic Courtès wrote 5 days ago
(name . Maxim Cournoyer)(address . maxim.cournoyer@gmail.com)
87o6yido2w.fsf@gnu.org
Hello,

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

Toggle quote (4 lines)
> 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.

Toggle quote (7 lines)
>> - 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.

Toggle quote (28 lines)
>> #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’.
Maxim Cournoyer wrote 5 days ago
(name . Ludovic Courtès)(address . ludo@gnu.org)
87plixej5m.fsf@gmail.com
Hi Ludovic,

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

[...]

Toggle quote (14 lines)
>>> - 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@gmail>

--
Thanks,
Maxim
?
Your comment

Commenting via the web interface is currently disabled.

To comment on this conversation send an email to 75810@debbugs.gnu.org

To respond to this issue using the mumi CLI, first switch to it
mumi current 75810
Then, you may apply the latest patchset in this issue (with sign off)
mumi am -- -s
Or, compose a reply to this issue
mumi compose
Or, send patches to this issue
mumi send-email *.patch
You may also tag this issue. See list of standard tags. For example, to set the confirmed and easy tags
mumi command -t +confirmed -t +easy
Or, remove the moreinfo tag and set the help tag
mumi command -t -moreinfo -t +help