From patchwork Fri Feb 21 13:05:54 2025 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 8bit X-Patchwork-Submitter: =?utf-8?q?Ludovic_Court=C3=A8s?= X-Patchwork-Id: 38907 Return-Path: X-Original-To: patchwork@mira.cbaines.net Delivered-To: patchwork@mira.cbaines.net Received: by mira.cbaines.net (Postfix, from userid 113) id 23F8727BBEA; Fri, 21 Feb 2025 13:09:20 +0000 (GMT) X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on mira.cbaines.net X-Spam-Level: X-Spam-Status: No, score=-7.6 required=5.0 tests=BAYES_00,DKIMWL_WL_HIGH, DKIM_SIGNED,DKIM_VALID,MAILING_LIST_MULTI,RCVD_IN_DNSWL_BLOCKED, RCVD_IN_VALIDITY_CERTIFIED,RCVD_IN_VALIDITY_RPBL,RCVD_IN_VALIDITY_SAFE, SPF_HELO_PASS,URIBL_BLOCKED autolearn=ham autolearn_force=no version=3.4.6 Received: from lists.gnu.org (lists.gnu.org [209.51.188.17]) by mira.cbaines.net (Postfix) with ESMTPS id 2725027BBE2 for ; Fri, 21 Feb 2025 13:09:19 +0000 (GMT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1tlSmA-0005Ny-SE; Fri, 21 Feb 2025 08:09:06 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]) by lists.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tlSm8-0005NR-W6 for guix-patches@gnu.org; Fri, 21 Feb 2025 08:09:06 -0500 Received: from debbugs.gnu.org ([2001:470:142:5::43]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_128_GCM_SHA256:128) (Exim 4.90_1) (envelope-from ) id 1tlSm8-0004r8-Jw for guix-patches@gnu.org; Fri, 21 Feb 2025 08:09:04 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=debbugs.gnu.org; s=debbugs-gnu-org; h=MIME-Version:References:In-Reply-To:Date:From:To:Subject; bh=x4qLkh9hJTZ4TkZolyQcxkgbw8cbkZm5cnTUmctXty8=; b=KfLSAc7FtenCCx5DBHAkr6OauhzC5fQRXeBJBh4fL5Cr97RdASnvyjJfgfZ8vBA0CsH8+NMNCKyKWueF19kJX9Pe2GQ5cWPn/o87F/1r528jRJgN8HQQTMzusf+SjYdsEBCIl/9eXifbuWqRf/O2t+LZsc0WMmiY2HT4rfqCKGGg6breJzYlKZELznIliPYgfzQ7rZuxtpVmVRLB/60fjvtpagX6EwtDHXDWkJqCtI1JXx5CouvAaLLAF0wKxtFDev0D/fDkp0v4AOv4tioC+6/M4GulL4KAVR3oGYTweVW7H5UvefPga5YID+7T2wsKMQ1/UbAstEr5FLdG3gHoFw==; Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1tlSm8-0005mu-8I for guix-patches@gnu.org; Fri, 21 Feb 2025 08:09:04 -0500 X-Loop: help-debbugs@gnu.org Subject: [bug#75810] [PATCH v3 06/11] tests: Run in a chroot and unprivileged user namespaces. Resent-From: Ludovic =?utf-8?q?Court=C3=A8s?= Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Fri, 21 Feb 2025 13:09:03 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 75810 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: 75810@debbugs.gnu.org Cc: Ludovic =?utf-8?q?Court=C3=A8s?= Received: via spool by 75810-submit@debbugs.gnu.org id=B75810.174014333122118 (code B ref 75810); Fri, 21 Feb 2025 13:09:03 +0000 Received: (at 75810) by debbugs.gnu.org; 21 Feb 2025 13:08:51 +0000 Received: from localhost ([127.0.0.1]:56691 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1tlSlt-0005kT-17 for submit@debbugs.gnu.org; Fri, 21 Feb 2025 08:08:50 -0500 Received: from eggs.gnu.org ([2001:470:142:3::10]:60044) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from ) id 1tlSju-0005OH-5o for 75810@debbugs.gnu.org; Fri, 21 Feb 2025 08:07:01 -0500 Received: from fencepost.gnu.org ([2001:470:142:3::e]) by eggs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.90_1) (envelope-from ) id 1tlSjo-0004Z7-PQ; Fri, 21 Feb 2025 08:06:40 -0500 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=gnu.org; s=fencepost-gnu-org; h=MIME-Version:References:In-Reply-To:Date:Subject:To: From; bh=x4qLkh9hJTZ4TkZolyQcxkgbw8cbkZm5cnTUmctXty8=; b=ktDtittoYnlV9w3J0Qp3 wT+5TAOUna0q4SyqB2cco3M8nNwxjh6mda4Hvn0HHhuecw9xSxnQiW5jBY6BF8X4UCVT+LVzzXnFd cY2wagPXDspLQ5gRnJrEl1J/d9dAUrymx5ywGUlctAOZvFme4+x/nwCL1QAOWAwrFo8Wfhjs3Lt58 ypTJ7FaGOu42hdXhZlD9vPmZ+ZT+0+BsnhNxguByjci86/xOswSsl/lVOijFUGkC4QGFW3kP/0IwD pEI01vjKLjZzkwB1sdwhX7oIGyW9Hi9Mw//4CXeLCqXKgEWSfDSlJ/oPSzAX+4/5mC9k6tlO1G1kn X5+qtEolwvFMeg==; From: Ludovic =?utf-8?q?Court=C3=A8s?= Date: Fri, 21 Feb 2025 14:05:54 +0100 Message-ID: X-Mailer: git-send-email 2.48.1 In-Reply-To: References: MIME-Version: 1.0 X-BeenThere: debbugs-submit@debbugs.gnu.org X-Mailman-Version: 2.1.18 Precedence: list X-BeenThere: guix-patches@gnu.org List-Id: List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , Errors-To: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org Sender: guix-patches-bounces+patchwork=mira.cbaines.net@gnu.org X-getmail-retrieved-from-mailbox: Patches * build-aux/test-env.in: Pass ‘--disable-chroot’ only when unprivileged user namespace support is lacking. * tests/store.scm ("build-things, check mode"): Use ‘gettimeofday’ rather than a shared file as a source of entropy. ("isolated environment", "inputs are read-only") ("inputs cannot be remounted read-write") ("build root cannot be made world-readable") ("/tmp, store, and /dev/{null,full} are writable"): New tests. * tests/processes.scm ("client + lock"): Skip when ‘unprivileged-user-namespace-supported?’ returns true. Change-Id: I3b3c3ebdf6db5fd36ee70251d07b893c17ca1b84 --- build-aux/test-env.in | 14 ++- tests/processes.scm | 9 +- tests/store.scm | 206 +++++++++++++++++++++++++++++++++++------- 3 files changed, 191 insertions(+), 38 deletions(-) diff --git a/build-aux/test-env.in b/build-aux/test-env.in index 9caa29da581..5626152b346 100644 --- a/build-aux/test-env.in +++ b/build-aux/test-env.in @@ -1,7 +1,7 @@ #!/bin/sh # GNU Guix --- Functional package management for GNU -# Copyright © 2012, 2013, 2014, 2015, 2016, 2017, 2018, 2019, 2021 Ludovic Courtès +# Copyright © 2012-2019, 2021, 2025 Ludovic Courtès # # 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 +;;; Copyright © 2018, 2025 Ludovic Courtès ;;; Copyright © 2019 Mathieu Othacehe ;;; ;;; 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 +;;; Copyright © 2012-2021, 2023, 2025 Ludovic Courtès ;;; ;;; 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 $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