From patchwork Tue Mar 11 12:41:50 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: 40084 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 C744127BBE2; Tue, 11 Mar 2025 12:43:30 +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=unavailable 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 1AEED27BBE9 for ; Tue, 11 Mar 2025 12:43:30 +0000 (GMT) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1trywz-0002uX-4K; Tue, 11 Mar 2025 08:43:13 -0400 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 1trywr-0002qL-9E for guix-patches@gnu.org; Tue, 11 Mar 2025 08:43:08 -0400 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 1trywo-0005DS-D6 for guix-patches@gnu.org; Tue, 11 Mar 2025 08:43:03 -0400 DKIM-Signature: v=1; a=rsa-sha256; q=dns/txt; c=relaxed/relaxed; d=debbugs.gnu.org; s=debbugs-gnu-org; h=MIME-Version:Date:References:In-Reply-To:From:To:Subject; bh=bRQKi1Tkiq1NRlXwH8yPFRMk2w4iz6R8CwGIX0J1a9Y=; b=Q3exYBsuU4sBgcD6b4BtzKNrGX2/8j9Beax7gJ4+S8rJdr5ClYrNRT3Qct5mXonhr8VjMkzf1Cl4Bqo20NcIeXXLNcg4pvmoMOuZYu2pyjkQgcqftJPNnnUtK3vZ6qsiNuFcl7KkMXpiAEZnYtONLcsJb1c8hdDNePEKmuj0O8C3wt2ng5pookLiD3cej3v0tvw/na/QwjYDqM7trGfnAfO44NHY/y3X2NGIoo3RrdpndxVOSkEDNcWPns9J3ZQzAowl3HA5MllTHfgSC66QprowE/U2Z2Sy9EyK+vfON+3mYsQpcCuWpe73ZsKNFkJ2fE1VfOzMxUjXQJrK1kQ7JQ==; Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1trywo-0005NI-4n for guix-patches@gnu.org; Tue, 11 Mar 2025 08:43:02 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#75810] Locked mounts Resent-From: Ludovic =?utf-8?q?Court=C3=A8s?= Original-Sender: "Debbugs-submit" Resent-CC: guix-patches@gnu.org Resent-Date: Tue, 11 Mar 2025 12:43:02 +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: Reepca Russelstein Cc: 75810@debbugs.gnu.org Received: via spool by 75810-submit@debbugs.gnu.org id=B75810.174169693420592 (code B ref 75810); Tue, 11 Mar 2025 12:43:02 +0000 Received: (at 75810) by debbugs.gnu.org; 11 Mar 2025 12:42:14 +0000 Received: from localhost ([127.0.0.1]:42107 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1tryvy-0005M0-Je for submit@debbugs.gnu.org; Tue, 11 Mar 2025 08:42:14 -0400 Received: from hera.aquilenet.fr ([185.233.100.1]:36596) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from ) id 1tryvr-0005LN-5L for 75810@debbugs.gnu.org; Tue, 11 Mar 2025 08:42:07 -0400 Received: from localhost (localhost [127.0.0.1]) by hera.aquilenet.fr (Postfix) with ESMTP id 7D4C035A; Tue, 11 Mar 2025 13:41:55 +0100 (CET) Authentication-Results: hera.aquilenet.fr; none X-Virus-Scanned: Debian amavis at hera.aquilenet.fr Received: from hera.aquilenet.fr ([127.0.0.1]) by localhost (hera.aquilenet.fr [127.0.0.1]) (amavis, port 10024) with ESMTP id uHLQ3eloNKrI; Tue, 11 Mar 2025 13:41:53 +0100 (CET) Received: from ribbon (91-160-117-201.subs.proxad.net [91.160.117.201]) by hera.aquilenet.fr (Postfix) with ESMTPSA id 57DF22B0; Tue, 11 Mar 2025 13:41:53 +0100 (CET) From: Ludovic =?utf-8?q?Court=C3=A8s?= In-Reply-To: <87jz9sc72o.fsf@russelstein.xyz> (Reepca Russelstein's message of "Fri, 14 Feb 2025 19:47:27 -0600") References: <87r04qe7dj.fsf@russelstein.xyz> <87bjvshrk0.fsf@gnu.org> <875xluehn7.fsf@russelstein.xyz> <877c5u2ct5.fsf@gnu.org> <87jz9sc72o.fsf@russelstein.xyz> Date: Tue, 11 Mar 2025 13:41:50 +0100 Message-ID: <87y0xbivsh.fsf_-_@gnu.org> User-Agent: Gnus/5.13 (Gnus v5.13) MIME-Version: 1.0 X-Rspamd-Server: hera X-Rspamd-Queue-Id: 7D4C035A X-Spamd-Result: default: False [-4.84 / 15.00]; BAYES_HAM(-3.00)[100.00%]; NEURAL_HAM(-2.99)[-0.998]; R_MIXED_CHARSET(1.25)[subject]; MIME_GOOD(-0.10)[multipart/mixed,text/plain,text/x-patch]; MIME_TRACE(0.00)[0:+,1:+,2:+,3:+]; RCVD_COUNT_TWO(0.00)[2]; FROM_EQ_ENVFROM(0.00)[]; RCPT_COUNT_TWO(0.00)[2]; ARC_NA(0.00)[]; HAS_ATTACHMENT(0.00)[]; RCVD_VIA_SMTP_AUTH(0.00)[]; RCVD_TLS_ALL(0.00)[]; FROM_HAS_DN(0.00)[]; TO_MATCH_ENVRCPT_ALL(0.00)[]; TO_DN_SOME(0.00)[]; MID_RHS_MATCH_FROM(0.00)[] X-Spamd-Bar: ---- X-Rspamd-Action: no action 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 Hi Reepca, Reepca Russelstein skribis: > I still think it would be a good idea to call unshare to create an extra > user and mount namespace just before executing the builder in the > unprivileged case, just to be sure that the mount-locking behavior is > triggered in a way that is documented. For some reason, it’s not working as advertised: mounts are seemingly not locked together and umount(2) on one of them returns EPERM (instead of EINVAL). I suspect chroot, pivot_root, & co. spoil it all. Attached is a patch and test case. To be sure, I wrote a minimal C program: umount returns EINVAL as expected. However, when compiling it with -DWITH_CHROOT, unshare(2) fails with EPERM because “the caller's root directory does not match the root directory of the mount namespace in which it resides” (quoting unshare(2)). So I now get the idea of “locked mounts” but I’m at loss as to how this is supposed to interact with chroots. Thoughts? Ludo’. #define _GNU_SOURCE 1 #include #include #include #include #include #include #include #include #include #include #include #include #undef NDEBUG static char child_stack[8192]; static int sync_pipe[2]; static int child (void *arg) { close (sync_pipe[1]); char str[123]; read (sync_pipe[0], str, sizeof str); assert (strcmp (str, "go!\n") == 0); close (sync_pipe[0]); printf ("child process: %d\n", getpid ()); mkdir ("/tmp/t", 0700); errno = 0; mount ("none", "/tmp/t", "tmpfs", 0, NULL); assert_perror (errno); #ifdef WITH_CHROOT chroot ("/tmp"); assert_perror (errno); #endif unshare (CLONE_NEWNS | CLONE_NEWUSER); assert_perror (errno); #ifdef WITH_CHROOT umount ("/t"); #else umount ("/tmp/t"); #endif int err = errno; printf ("umount errno: %s\n", strerror (err)); assert (err == EINVAL); return EXIT_FAILURE; } static void initialize_namespace (pid_t pid) { close (sync_pipe[0]); char name[1024]; FILE *file; sprintf (name, "/proc/%d/uid_map", pid); file = fopen (name, "w"); fprintf (file, "42 %d 1", getuid ()); fclose (file); sprintf (name, "/proc/%d/setgroups", pid); file = fopen (name, "w"); fprintf (file, "deny"); fclose (file); sprintf (name, "/proc/%d/gid_map", pid); file = fopen (name, "w"); fprintf (file, "42 %d 1", getgid ()); fclose (file); errno = 0; write (sync_pipe[1], "go!\n", 5); assert_perror (errno); close (sync_pipe[1]); } int main () { errno = 0; pipe2 (sync_pipe, O_CLOEXEC); assert_perror (errno); int pid = clone (child, (char *) (((intptr_t) child_stack + sizeof child_stack - sizeof (void *)) & ~0xfULL), CLONE_NEWNS | CLONE_NEWUSER | SIGCHLD, NULL); assert_perror (errno); initialize_namespace (pid); return EXIT_SUCCESS; } /* unshare -mrf sh -c 'mount -t tmpfs none /tmp; exec unshare -mr strace -e umount2 umount /tmp' */ diff --git a/nix/libstore/build.cc b/nix/libstore/build.cc index 057a15ccd0..6a6a960a1c 100644 --- a/nix/libstore/build.cc +++ b/nix/libstore/build.cc @@ -2244,6 +2244,13 @@ void DerivationGoal::runChild() /* Remount root as read-only. */ if (mount("/", "/", 0, MS_BIND | MS_REMOUNT | MS_RDONLY, 0) == -1) throw SysError(format("read-only remount of build root '%1%' failed") % chrootRootDir); + + if (getuid() != 0) { + /* Create a new mount namespace to "lock" previous mounts. + See mount_namespaces(7). */ + if (unshare(CLONE_NEWNS | CLONE_NEWUSER) == -1) + throw SysError(format("creating new user and mount namespaces")); + } } #endif diff --git a/tests/store.scm b/tests/store.scm index c22739afe6..9da9345dd0 100644 --- a/tests/store.scm +++ b/tests/store.scm @@ -37,6 +37,8 @@ (define-module (test-store) #:use-module (guix gexp) #:use-module (gnu packages) #:use-module (gnu packages bootstrap) + #:use-module ((gnu packages make-bootstrap) + #:select (%guile-static-stripped)) #:use-module (ice-9 match) #:use-module (ice-9 regex) #:use-module (rnrs bytevectors) @@ -59,6 +61,8 @@ (define %shell (test-begin "store") +(test-skip 25) + (test-assert "open-connection with file:// URI" (let ((store (open-connection (string-append "file://" (%daemon-socket-uri))))) @@ -455,7 +459,7 @@ (define %shell (drv (run-with-store %store (gexp->derivation - "attempt-to-remount-input-read-write" + "attempt-to-write-to-input" (with-imported-modules (source-module-closure '((guix build syscalls))) #~(begin @@ -496,6 +500,58 @@ (define %shell (build-derivations %store (list drv)) #f))) +(let ((guile (with-external-store external-store + (and external-store + (run-with-store external-store + (mlet %store-monad ((drv (lower-object %guile-static-stripped))) + (mbegin %store-monad + (built-derivations (list drv)) + (return (derivation->output-path (pk 'GDRV drv)))))))))) + + (unless (and guile (unprivileged-user-namespace-supported?)) + (test-skip 1)) + (test-equal "input mount is locked" + EINVAL + ;; Check that mounts within the build environment are "locked" together and + ;; cannot be separated from within the build environment namespace--see + ;; mount_namespaces(7). + ;; + ;; Since guile-bootstrap@2.0 lacks 'umount', resort to the hack below to + ;; get a statically-linked Guile with 'umount'. + (let* ((guile (computed-file "guile-with-umount" + ;; The #:guile-for-build argument must be a + ;; derivation, hence this silly thing. + #~(symlink #$(local-file guile "guile-with-umount" + #:recursive? #t) + #$output) + #:guile %bootstrap-guile)) + (drv + (run-with-store %store + (mlet %store-monad ((guile (lower-object guile))) + (gexp->derivation + "attempt-to-unmount-input" + (with-imported-modules (source-module-closure + '((guix build syscalls))) + #~(begin + (use-modules (guix build syscalls)) + + (let ((input #$(plain-file "input-that-might-be-unmounted" + (random-text)))) + (catch 'system-error + (lambda () + ;; umount(2) returns EINVAL when the target is locked. + (umount input)) + (lambda args + (call-with-output-file #$output + (lambda (port) + (write (system-error-errno args) port)))))))) + #:guile-for-build guile))))) + (build-derivations %store (list (pk 'UMDRV drv))) + (call-with-input-file (derivation->output-path drv) + read)))) + +(test-skip 100) + (unless (unprivileged-user-namespace-supported?) (test-skip 1)) (test-assert "build root cannot be made world-readable"