summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLudovic Courtès <ludo@gnu.org>2025-04-08 14:03:48 +0200
committerLudovic Courtès <ludo@gnu.org>2025-05-05 14:34:00 +0200
commita57ed987ffd1452ba5a4d70feb54893e99b8e076 (patch)
tree7813ca0a8b517650db72af51a5920bf3ee187806
parente1a0171a56602ecba193975ea2438329abb51c94 (diff)
linux-container: Lock mounts by default.
This makes it impossible to unmount or remount things from within ‘call-with-container’. * gnu/build/linux-container.scm (initialize-user-namespace): Add #:host-uid and #:host-gid. and honor them. (run-container): Add #:lock-mounts?. Honor it by calling ‘unshare’ followed by ‘initialize-user-namespace’. (call-with-container): Add #:lock-mounts? and pass it down. (container-excursion): Get the user namespace owning the PID namespace and join it, then join the remaining namespaces. * tests/containers.scm ("call-with-container, mnt namespace, locked mounts"): New test. ("container-excursion"): Pass #:lock-mounts? #f. Change-Id: I13be982aef99e68a653d472f0e595c81cfcfa392
-rw-r--r--gnu/build/linux-container.scm111
-rw-r--r--tests/containers.scm33
2 files changed, 103 insertions, 41 deletions
diff --git a/gnu/build/linux-container.scm b/gnu/build/linux-container.scm
index 345ce2de08a..51f04bc249b 100644
--- a/gnu/build/linux-container.scm
+++ b/gnu/build/linux-container.scm
@@ -189,7 +189,10 @@ for the process."
189 (remount-read-only "/")))) 189 (remount-read-only "/"))))
190 190
191(define* (initialize-user-namespace pid host-uids 191(define* (initialize-user-namespace pid host-uids
192 #:key (guest-uid 0) (guest-gid 0)) 192 #:key
193 (host-uid (getuid))
194 (host-gid (getgid))
195 (guest-uid 0) (guest-gid 0))
193 "Configure the user namespace for PID. HOST-UIDS specifies the number of 196 "Configure the user namespace for PID. HOST-UIDS specifies the number of
194host user identifiers to map into the user namespace. GUEST-UID and GUEST-GID 197host user identifiers to map into the user namespace. GUEST-UID and GUEST-GID
195specify the first UID (respectively GID) that host UIDs (respectively GIDs) 198specify the first UID (respectively GID) that host UIDs (respectively GIDs)
@@ -200,24 +203,21 @@ map to in the namespace."
200 (define (scope file) 203 (define (scope file)
201 (string-append proc-dir file)) 204 (string-append proc-dir file))
202 205
203 (let ((uid (getuid)) 206 ;; Only root can write to the gid map without first disabling the
204 (gid (getgid))) 207 ;; setgroups syscall.
205 208 (unless (and (zero? host-uid) (zero? host-gid))
206 ;; Only root can write to the gid map without first disabling the 209 (call-with-output-file (scope "/setgroups")
207 ;; setgroups syscall.
208 (unless (and (zero? uid) (zero? gid))
209 (call-with-output-file (scope "/setgroups")
210 (lambda (port)
211 (display "deny" port))))
212
213 ;; Map the user/group that created the container to the root user
214 ;; within the container.
215 (call-with-output-file (scope "/uid_map")
216 (lambda (port)
217 (format port "~d ~d ~d" guest-uid uid host-uids)))
218 (call-with-output-file (scope "/gid_map")
219 (lambda (port) 210 (lambda (port)
220 (format port "~d ~d ~d" guest-gid gid host-uids))))) 211 (display "deny" port))))
212
213 ;; Map the user/group that created the container to the root user
214 ;; within the container.
215 (call-with-output-file (scope "/uid_map")
216 (lambda (port)
217 (format port "~d ~d ~d" guest-uid host-uid host-uids)))
218 (call-with-output-file (scope "/gid_map")
219 (lambda (port)
220 (format port "~d ~d ~d" guest-gid host-gid host-uids))))
221 221
222(define (namespaces->bit-mask namespaces) 222(define (namespaces->bit-mask namespaces)
223 "Return the number suitable for the 'flags' argument of 'clone' that 223 "Return the number suitable for the 'flags' argument of 'clone' that
@@ -238,12 +238,14 @@ corresponds to the symbols in NAMESPACES."
238 #:key (guest-uid 0) (guest-gid 0) 238 #:key (guest-uid 0) (guest-gid 0)
239 (populate-file-system (const #t)) 239 (populate-file-system (const #t))
240 (loopback-network? #t) 240 (loopback-network? #t)
241 (lock-mounts? #t)
241 writable-root?) 242 writable-root?)
242 "Run THUNK in a new container process and return its PID. ROOT specifies 243 "Run THUNK in a new container process and return its PID. ROOT specifies
243the root directory for the container. MOUNTS is a list of <file-system> 244the root directory for the container. MOUNTS is a list of <file-system>
244objects that specify file systems to mount inside the container. NAMESPACES 245objects that specify file systems to mount inside the container. NAMESPACES
245is a list of symbols that correspond to the possible Linux namespaces: mnt, 246is a list of symbols that correspond to the possible Linux namespaces: mnt,
246ipc, uts, user, and net. 247ipc, uts, user, and net. When LOCK-MOUNTS? is true, arrange so that none of
248MOUNTS can be unmounted or remounted individually from within THUNK.
247 249
248When LOOPBACK-NETWORK? is true and 'net is amount NAMESPACES, set up the 250When LOOPBACK-NETWORK? is true and 'net is amount NAMESPACES, set up the
249loopback device (\"lo\") and a minimal /etc/hosts. 251loopback device (\"lo\") and a minimal /etc/hosts.
@@ -303,6 +305,28 @@ that host UIDs (respectively GIDs) map to in the namespace."
303 ;; cannot be 'read' so they shouldn't be written as is. 305 ;; cannot be 'read' so they shouldn't be written as is.
304 (write args child) 306 (write args child)
305 (primitive-exit 3)))) 307 (primitive-exit 3))))
308
309 (when (and lock-mounts?
310 (memq 'mnt namespaces)
311 (memq 'user namespaces))
312 ;; Create a new mount namespace owned by a new user
313 ;; namespace to "lock" together previous mounts, such that
314 ;; they cannot be unmounted or remounted separately--see
315 ;; mount_namespaces(7).
316 ;;
317 ;; Note: at this point, the process is single-threaded (no
318 ;; GC mark threads, no finalization thread, etc.) which is
319 ;; why unshare(CLONE_NEWUSER) can be used.
320 (let ((uid (getuid)) (gid (getgid)))
321 (unshare (logior CLONE_NEWUSER CLONE_NEWNS))
322 (when (file-exists? "/proc/self")
323 (initialize-user-namespace (getpid)
324 host-uids
325 #:host-uid uid
326 #:host-gid gid
327 #:guest-uid guest-uid
328 #:guest-gid guest-gid))))
329
306 ;; TODO: Manage capabilities. 330 ;; TODO: Manage capabilities.
307 (write 'ready child) 331 (write 'ready child)
308 (close-port child) 332 (close-port child)
@@ -365,6 +389,7 @@ if there are no child processes left."
365 389
366(define* (call-with-container mounts thunk #:key (namespaces %namespaces) 390(define* (call-with-container mounts thunk #:key (namespaces %namespaces)
367 (host-uids 1) (guest-uid 0) (guest-gid 0) 391 (host-uids 1) (guest-uid 0) (guest-gid 0)
392 (lock-mounts? #t)
368 (relayed-signals (list SIGINT SIGTERM)) 393 (relayed-signals (list SIGINT SIGTERM))
369 (child-is-pid1? #t) 394 (child-is-pid1? #t)
370 (populate-file-system (const #t)) 395 (populate-file-system (const #t))
@@ -449,6 +474,7 @@ load path must be adjusted as needed."
449 (call-with-temporary-directory 474 (call-with-temporary-directory
450 (lambda (root) 475 (lambda (root)
451 (let ((pid (run-container root mounts namespaces host-uids thunk* 476 (let ((pid (run-container root mounts namespaces host-uids thunk*
477 #:lock-mounts? lock-mounts?
452 #:guest-uid guest-uid 478 #:guest-uid guest-uid
453 #:guest-gid guest-gid 479 #:guest-gid guest-gid
454 #:populate-file-system populate-file-system 480 #:populate-file-system populate-file-system
@@ -469,24 +495,35 @@ return the exit status, an integer as returned by 'waitpid'."
469 (0 495 (0
470 (call-with-clean-exit 496 (call-with-clean-exit
471 (lambda () 497 (lambda ()
472 (for-each (lambda (ns) 498 ;; First, determine the user namespace that owns the pid namespace and
473 (let ((source (namespace-file (getpid) ns)) 499 ;; join that user namespace (the assumption is that it also owns all
474 (target (namespace-file pid ns))) 500 ;; the other namespaces). It's important that the user namespace is
475 ;; Joining the namespace that the process already 501 ;; joined first, so that the user will have the privileges to join the
476 ;; belongs to would throw an error so avoid that. 502 ;; other namespaces.
477 ;; XXX: This /proc interface leads to TOCTTOU. 503 (let* ((pid-ns (open-fdes (namespace-file pid "pid")
478 (unless (string=? (readlink source) (readlink target)) 504 (logior O_CLOEXEC O_RDONLY)))
479 (call-with-input-file source 505 (user-ns (get-user-ns pid-ns)))
480 (lambda (current-ns-port) 506 (close-fdes pid-ns)
481 (call-with-input-file target 507 (unless (equal? (stat user-ns)
482 (lambda (new-ns-port) 508 (stat (namespace-file (getpid) "user")))
483 (setns (fileno new-ns-port) 0)))))))) 509 (setns user-ns 0))
484 ;; It's important that the user namespace is joined first, 510 (close-fdes user-ns)
485 ;; so that the user will have the privileges to join the 511
486 ;; other namespaces. Furthermore, it's important that the 512 ;; Then join all the remaining namespaces.
487 ;; mount namespace is joined last, otherwise the /proc mount 513 (for-each (lambda (ns)
488 ;; point would no longer be accessible. 514 (let ((source (namespace-file (getpid) ns))
489 '("user" "ipc" "uts" "net" "pid" "mnt")) 515 (target (namespace-file pid ns)))
516 ;; Joining the namespace that the process already
517 ;; belongs to would throw an error so avoid that.
518 ;; XXX: This /proc interface leads to TOCTTOU.
519 (unless (string=? (readlink source) (readlink target))
520 (call-with-input-file target
521 (lambda (new-ns-port)
522 (setns (fileno new-ns-port) 0))))))
523 ;; It's important that the mount namespace is joined last,
524 ;; otherwise the /proc mount point would no longer be
525 ;; accessible.
526 '("ipc" "uts" "net" "pid" "mnt")))
490 (purify-environment) 527 (purify-environment)
491 (chdir "/") 528 (chdir "/")
492 529
diff --git a/tests/containers.scm b/tests/containers.scm
index 1e915d517e8..6edea9631dc 100644
--- a/tests/containers.scm
+++ b/tests/containers.scm
@@ -1,6 +1,6 @@
1;;; GNU Guix --- Functional package management for GNU 1;;; GNU Guix --- Functional package management for GNU
2;;; Copyright © 2015 David Thompson <davet@gnu.org> 2;;; Copyright © 2015 David Thompson <davet@gnu.org>
3;;; Copyright © 2016, 2017, 2019, 2023 Ludovic Courtès <ludo@gnu.org> 3;;; Copyright © 2016-2017, 2019, 2023, 2025 Ludovic Courtès <ludo@gnu.org>
4;;; 4;;;
5;;; This file is part of GNU Guix. 5;;; This file is part of GNU Guix.
6;;; 6;;;
@@ -111,6 +111,26 @@
111 #:namespaces '(user mnt)))) 111 #:namespaces '(user mnt))))
112 112
113(skip-if-unsupported) 113(skip-if-unsupported)
114(test-equal "call-with-container, mnt namespace, locked mounts"
115 EINVAL
116 ;; umount(2) fails with EINVAL when targeting a mount point that is
117 ;; "locked".
118 (status:exit-val
119 (call-with-container (list (file-system
120 (device "none")
121 (mount-point "/testing")
122 (type "tmpfs")
123 (check? #f)))
124 (lambda ()
125 (primitive-exit (catch 'system-error
126 (lambda ()
127 (umount "/testing")
128 0)
129 (lambda args
130 (system-error-errno args)))))
131 #:namespaces '(user mnt))))
132
133(skip-if-unsupported)
114(test-equal "call-with-container, mnt namespace, wrong bind mount" 134(test-equal "call-with-container, mnt namespace, wrong bind mount"
115 `(system-error ,ENOENT) 135 `(system-error ,ENOENT)
116 ;; An exception should be raised; see <http://bugs.gnu.org/23306>. 136 ;; An exception should be raised; see <http://bugs.gnu.org/23306>.
@@ -169,7 +189,8 @@
169 #:namespaces '(user mnt)))) 189 #:namespaces '(user mnt))))
170 190
171(skip-if-unsupported) 191(skip-if-unsupported)
172(test-assert "container-excursion" 192(test-equal "container-excursion"
193 0
173 (call-with-temporary-directory 194 (call-with-temporary-directory
174 (lambda (root) 195 (lambda (root)
175 ;; Two pipes: One for the container to signal that the test can begin, 196 ;; Two pipes: One for the container to signal that the test can begin,
@@ -193,7 +214,11 @@
193 (readlink (string-append "/proc/" pid "/ns/" ns))) 214 (readlink (string-append "/proc/" pid "/ns/" ns)))
194 '("user" "ipc" "uts" "net" "pid" "mnt")))) 215 '("user" "ipc" "uts" "net" "pid" "mnt"))))
195 216
196 (let* ((pid (run-container root '() %namespaces 1 container)) 217 (let* ((pid (run-container root '() %namespaces 1 container
218 ;; Do not lock mounts so the user namespace
219 ;; appears to be the same seen from inside
220 ;; and from outside.
221 #:lock-mounts? #f))
197 (container-namespaces (namespaces pid)) 222 (container-namespaces (namespaces pid))
198 (result 223 (result
199 (begin 224 (begin
@@ -213,7 +238,7 @@
213 (write 'done end-out) 238 (write 'done end-out)
214 (close end-out) 239 (close end-out)
215 (waitpid pid) 240 (waitpid pid)
216 (zero? result))))))) 241 result))))))
217 242
218(skip-if-unsupported) 243(skip-if-unsupported)
219(test-equal "container-excursion, same namespaces" 244(test-equal "container-excursion, same namespaces"