[PATCH 0/6] Rootless guix-daemon

  • Open
  • quality assurance status badge
Details
4 participants
  • Janneke Nieuwenhuizen
  • Ludovic Courtès
  • Noé Lopez
  • Reepca Russelstein
Owner
unassigned
Submitted by
Ludovic Courtès
Severity
normal
L
L
Ludovic Courtès wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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
J
J
Janneke Nieuwenhuizen wrote 5 days 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
L
L
Ludovic Courtès wrote 5 days 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’.
R
R
Reepca Russelstein wrote 4 days 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-----

L
L
Ludovic Courtès wrote 2 days 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’.
N
N
Noé Lopez wrote 2 days 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é
L
L
Ludovic Courtès wrote 2 days 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’.
R
R
Reepca Russelstein wrote 17 hours 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-----

?
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