From patchwork Tue Apr 8 12:24:43 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: 41448 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 2012C27BC4B; Tue, 8 Apr 2025 13:27:56 +0100 (BST) X-Spam-Checker-Version: SpamAssassin 3.4.6 (2021-04-09) on mira.cbaines.net X-Spam-Level: X-Spam-Status: No, score=-7.3 required=5.0 tests=BAYES_00,DKIM_INVALID, DKIM_SIGNED,MAILING_LIST_MULTI,RCVD_IN_DNSWL_BLOCKED,RCVD_IN_MSPIKE_H2, RCVD_IN_VALIDITY_CERTIFIED,RCVD_IN_VALIDITY_RPBL,RCVD_IN_VALIDITY_SAFE, SPF_HELO_PASS,URIBL_BLOCKED,URIBL_SBL_A 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 6897D27BC49 for ; Tue, 8 Apr 2025 13:27:55 +0100 (BST) Received: from localhost ([::1] helo=lists1p.gnu.org) by lists.gnu.org with esmtp (Exim 4.90_1) (envelope-from ) id 1u283K-00085y-PF; Tue, 08 Apr 2025 08:27:42 -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 1u282m-0007zw-MI for guix-patches@gnu.org; Tue, 08 Apr 2025 08:27: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 1u282j-00083G-Qg; Tue, 08 Apr 2025 08:27:07 -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:References:In-Reply-To:Date:From:To:Subject; bh=Ve3XKTREZQHeS4M1MJakiBFc6OQecMPwO3afqDj8jKk=; b=KIVWXj8ye9ukYQjSfAIUYYZe4c4hiAWn54V95LBik1HYrMQxDtEZP10Tqq11ggLRyJ05DYHDoLapzZY/ueVFwM2eS3F6aNkXDX8xNIni/2P2StlStCkDO9e1eu7ifqg4Q0qkl/Ij8t9+OOFa1USUe7j36TJ1JjzK2c8R9hahihNdJe+Km/TH7joDQ6jTBknc+Ksp/T5nfO2mKuk76uvKrEGV8tpdQjUj3jT1Maz78kMH7+lLbTHAjzBycQDe+EwZkNcY93jQTYYskXXQLtUvpKZkp/AroiWMtLDvknXbs1KNueUXBDHOyd3Iceg0V74MXPeXWCxDrSATjCvb6+HGmA==; Received: from Debian-debbugs by debbugs.gnu.org with local (Exim 4.84_2) (envelope-from ) id 1u282i-0001Pb-2v; Tue, 08 Apr 2025 08:27:04 -0400 X-Loop: help-debbugs@gnu.org Subject: [bug#77638] [PATCH 3/8] linux-container: Support having a read-only root file system. Resent-From: Ludovic =?utf-8?q?Court=C3=A8s?= Original-Sender: "Debbugs-submit" Resent-CC: andrew@trop.in, guix@cbaines.net, janneke@gnu.org, dev@jpoiret.xyz, ludo@gnu.org, othacehe@gnu.org, zimon.toutoune@gmail.com, tanguy@bioneland.org, me@tobias.gr, guix-patches@gnu.org Resent-Date: Tue, 08 Apr 2025 12:27:03 +0000 Resent-Message-ID: Resent-Sender: help-debbugs@gnu.org X-GNU-PR-Message: followup 77638 X-GNU-PR-Package: guix-patches X-GNU-PR-Keywords: patch To: 77638@debbugs.gnu.org Cc: Ludovic =?utf-8?q?Court=C3=A8s?= , Andrew Tropin , Christopher Baines , Janneke Nieuwenhuizen , Josselin Poiret , Ludovic =?utf-8?q?Court=C3=A8s?= , Mathieu Othacehe , Simon Tournier , Tanguy Le Carrour , Tobias Geerinckx-Rice X-Debbugs-Original-Xcc: Andrew Tropin , Christopher Baines , Janneke Nieuwenhuizen , Josselin Poiret , Ludovic =?utf-8?q?Court=C3=A8s?= , Mathieu Othacehe , Simon Tournier , Tanguy Le Carrour , Tobias Geerinckx-Rice Received: via spool by 77638-submit@debbugs.gnu.org id=B77638.17441151965238 (code B ref 77638); Tue, 08 Apr 2025 12:27:03 +0000 Received: (at 77638) by debbugs.gnu.org; 8 Apr 2025 12:26:36 +0000 Received: from localhost ([127.0.0.1]:59695 helo=debbugs.gnu.org) by debbugs.gnu.org with esmtp (Exim 4.84_2) (envelope-from ) id 1u282F-0001MF-CK for submit@debbugs.gnu.org; Tue, 08 Apr 2025 08:26:36 -0400 Received: from eggs.gnu.org ([2001:470:142:3::10]:35494) by debbugs.gnu.org with esmtps (TLS1.2:ECDHE_RSA_AES_256_GCM_SHA384:256) (Exim 4.84_2) (envelope-from ) id 1u281v-0001Iq-3u for 77638@debbugs.gnu.org; Tue, 08 Apr 2025 08:26:19 -0400 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 1u281o-0007nQ-Fk; Tue, 08 Apr 2025 08:26:09 -0400 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=Ve3XKTREZQHeS4M1MJakiBFc6OQecMPwO3afqDj8jKk=; b=XAXBmXxJEN2+3D8QNoao a19ikQ9rCc6Okm9Lhq3Tr64Yh/OmB5PC02woEW9jismYJX8QthSVOmrMJzR0ZFPY41anqbYOBh3wR 1P39sZgVTfd1evH8K6sDRZShK53lcA4t2+P+ikQMeb8BLhUyrB9bL0v1J8VnsZSC0EahcrAvMA5sK YnxkIl4wb0vUvYlVuLZqan55AEAE+F36gkNvOgSdB75HNz1haR8Qcqc+wsGD8Sie3JQstkw9QqM0r HUookT5ehCtVZhSd2MeGHsNoijx9Snc4qVIW2dfpPR/2Jp8mVMY3iGz67+RiV7ZQihzxDy0LSD2L3 BRpO46e2LkTEqQ==; From: Ludovic =?utf-8?q?Court=C3=A8s?= Date: Tue, 8 Apr 2025 14:24:43 +0200 Message-ID: <1a625b6062f6dbb5b73ae42eef277e21ee05f0bf.1744114408.git.ludo@gnu.org> X-Mailer: git-send-email 2.49.0 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 Until now, the read-only file system set up by ‘call-with-container’ would always be writable. With this change, it can be made read-only. With this patch, only ‘least-authority-wrapper’ switches to a read-only root file system. * gnu/build/linux-container.scm (remount-read-only): New procedure. (mount-file-systems): Add #:writable-root? and #:populate-file-system and honor them. (run-container): Likewise. (call-with-container): Likewise. * gnu/system/linux-container.scm (container-script): Pass #:writable-root? to ‘call-with-container’. (eval/container): Add #:populate-file-system and #:writable-root? and honor them. * guix/scripts/environment.scm (launch-environment/container): Pass #:writable-root? to ‘call-with-container’. * guix/scripts/home.scm (spawn-home-container): Likewise. * tests/containers.scm ("call-with-container, mnt namespace, read-only root") ("call-with-container, mnt namespace, writable root"): New tests. Change-Id: I603e2fd08851338b737bb16c8af3f765e2538906 --- gnu/build/linux-container.scm | 38 +++++++++++++++++++++++++++++----- gnu/system/linux-container.scm | 5 +++++ guix/scripts/environment.scm | 1 + guix/scripts/home.scm | 1 + tests/containers.scm | 26 +++++++++++++++++++++++ 5 files changed, 66 insertions(+), 5 deletions(-) diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm index a5c5d8962e..4dcdaa8f33 100644 --- a/gnu/build/linux-container.scm +++ b/gnu/build/linux-container.scm @@ -75,10 +75,16 @@ (define (purify-environment) (match (get-environment-variables) (((names . _) ...) names)))) +(define (remount-read-only mount-point) + (mount mount-point mount-point "none" + (logior MS_BIND MS_REMOUNT MS_RDONLY))) + ;; The container setup procedure closely resembles that of the Docker ;; specification: ;; https://raw.githubusercontent.com/docker/libcontainer/master/SPEC.md -(define* (mount-file-systems root mounts #:key mount-/sys? mount-/proc?) +(define* (mount-file-systems root mounts #:key mount-/sys? mount-/proc? + (populate-file-system (const #t)) + writable-root?) "Mount the essential file systems and the those in MOUNTS, a list of objects, relative to ROOT; then make ROOT the new root directory for the process." @@ -177,7 +183,10 @@ (define* (mount-file-systems root mounts #:key mount-/sys? mount-/proc?) (chdir "/") (umount "real-root" MNT_DETACH) (rmdir "real-root") - (chmod "/" #o755))) + (populate-file-system) + (chmod "/" #o755) + (unless writable-root? + (remount-read-only "/")))) (define* (initialize-user-namespace pid host-uids #:key (guest-uid 0) (guest-gid 0)) @@ -226,13 +235,19 @@ (define (namespaces->bit-mask namespaces) namespaces))) (define* (run-container root mounts namespaces host-uids thunk - #:key (guest-uid 0) (guest-gid 0)) + #:key (guest-uid 0) (guest-gid 0) + (populate-file-system (const #t)) + writable-root?) "Run THUNK in a new container process and return its PID. ROOT specifies the root directory for the container. MOUNTS is a list of objects that specify file systems to mount inside the container. NAMESPACES is a list of symbols that correspond to the possible Linux namespaces: mnt, ipc, uts, user, and net. +When WRITABLE-ROOT? is false, remount the container's root as read-only before +calling THUNK. Call POPULATE-FILE-SYSTEM before the root is (potentially) +made read-only. + HOST-UIDS specifies the number of host user identifiers to map into the user namespace. GUEST-UID and GUEST-GID specify the first UID (respectively GID) that host UIDs (respectively GIDs) map to in the namespace." @@ -258,7 +273,12 @@ (define* (run-container root mounts namespaces host-uids thunk (mount-file-systems root mounts #:mount-/proc? (memq 'pid namespaces) #:mount-/sys? (memq 'net - namespaces))) + namespaces) + #:populate-file-system + populate-file-system + #:writable-root? + (or writable-root? + (not (memq 'mnt namespaces))))) (lambda args ;; Forward the exception to the parent process. ;; FIXME: SRFI-35 conditions and non-trivial objects @@ -329,6 +349,8 @@ (define* (call-with-container mounts thunk #:key (namespaces %namespaces) (host-uids 1) (guest-uid 0) (guest-gid 0) (relayed-signals (list SIGINT SIGTERM)) (child-is-pid1? #t) + (populate-file-system (const #t)) + writable-root? (process-spawned-hook (const #t))) "Run THUNK in a new container process and return its exit status; call PROCESS-SPAWNED-HOOK with the PID of the new process that has been spawned. @@ -349,6 +371,10 @@ (define* (call-with-container mounts thunk #:key (namespaces %namespaces) RELAYED-SIGNALS is the list of signals that are \"relayed\" to the container process when caught by its parent. +When WRITABLE-ROOT? is false, remount the container's root as read-only before +calling THUNK. Call POPULATE-FILE-SYSTEM before the root is (potentially) +made read-only. + When CHILD-IS-PID1? is true, and if NAMESPACES contains 'pid', then the child process runs directly as PID 1. As such, it is responsible for (1) installing signal handlers and (2) reaping terminated processes by calling 'waitpid'. @@ -402,7 +428,9 @@ (define* (call-with-container mounts thunk #:key (namespaces %namespaces) (lambda (root) (let ((pid (run-container root mounts namespaces host-uids thunk* #:guest-uid guest-uid - #:guest-gid guest-gid))) + #:guest-gid guest-gid + #:populate-file-system populate-file-system + #:writable-root? writable-root?))) (install-signal-handlers pid) (process-spawned-hook pid) (match (waitpid pid) diff --git a/gnu/system/linux-container.scm b/gnu/system/linux-container.scm index 3622328500..e7cb90d091 100644 --- a/gnu/system/linux-container.scm +++ b/gnu/system/linux-container.scm @@ -312,12 +312,15 @@ (define* (container-script os #:key (mappings '()) shared-network?) #:namespaces (if #$shared-network? (delq 'net %namespaces) %namespaces) + #:writable-root? #t #:process-spawned-hook explain))))) (gexp->script "run-container" script))) (define* (eval/container exp #:key + (populate-file-system (const #t)) + writable-root? (mappings '()) (mounts '()) (namespaces %namespaces) @@ -367,6 +370,8 @@ (define* (eval/container exp (list "-c" (object->string (lowered-gexp-sexp lowered)))))) + #:writable-root? writable-root? + #:populate-file-system populate-file-system #:namespaces namespaces #:guest-uid guest-uid #:guest-gid guest-gid)))))) diff --git a/guix/scripts/environment.scm b/guix/scripts/environment.scm index 648a497743..4be9807163 100644 --- a/guix/scripts/environment.scm +++ b/guix/scripts/environment.scm @@ -959,6 +959,7 @@ (define* (launch-environment/container #:key command bash user user-mappings #:emulate-fhs? emulate-fhs?))) #:guest-uid uid #:guest-gid gid + #:writable-root? #t ;for backward compatibility #:namespaces (if network? (delq 'net %namespaces) ; share host network %namespaces))))))) diff --git a/guix/scripts/home.scm b/guix/scripts/home.scm index 56a4b7c7d4..7ce6217324 100644 --- a/guix/scripts/home.scm +++ b/guix/scripts/home.scm @@ -375,6 +375,7 @@ (define* (spawn-home-container home (type "tmpfs") (check? #f))) #:mappings (append network-mappings mappings) + #:writable-root? #t #:guest-uid uid #:guest-gid gid)) diff --git a/tests/containers.scm b/tests/containers.scm index 70d5ba2d30..1e915d517e 100644 --- a/tests/containers.scm +++ b/tests/containers.scm @@ -142,6 +142,32 @@ (define (skip-if-unsupported) (assert-exit (= #o755 (stat:perms (lstat "/"))))) #:namespaces '(user mnt)))) +(skip-if-unsupported) +(test-assert "call-with-container, mnt namespace, read-only root" + (zero? + (call-with-container '() + (lambda () + (assert-exit (and (file-is-directory? "/witness") + (catch 'system-error + (lambda () + (mkdir "/whatever") + #f) + (lambda args + (= (system-error-errno args) EROFS)))))) + #:populate-file-system (lambda () + (mkdir "/witness")) + #:namespaces '(user mnt)))) + +(skip-if-unsupported) +(test-assert "call-with-container, mnt namespace, writable root" + (zero? + (call-with-container '() + (lambda () + (mkdir "whatever") + (assert-exit (file-is-directory? "/whatever"))) + #:writable-root? #t + #:namespaces '(user mnt)))) + (skip-if-unsupported) (test-assert "container-excursion" (call-with-temporary-directory