diff options
| author | Ludovic Courtès <ludo@gnu.org> | 2025-04-08 14:03:48 +0200 |
|---|---|---|
| committer | Ludovic Courtès <ludo@gnu.org> | 2025-05-05 14:34:00 +0200 |
| commit | a57ed987ffd1452ba5a4d70feb54893e99b8e076 (patch) | |
| tree | 7813ca0a8b517650db72af51a5920bf3ee187806 | |
| parent | e1a0171a56602ecba193975ea2438329abb51c94 (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.scm | 111 | ||||
| -rw-r--r-- | tests/containers.scm | 33 |
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 |
| 194 | host user identifiers to map into the user namespace. GUEST-UID and GUEST-GID | 197 | host user identifiers to map into the user namespace. GUEST-UID and GUEST-GID |
| 195 | specify the first UID (respectively GID) that host UIDs (respectively GIDs) | 198 | specify 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 |
| 243 | the root directory for the container. MOUNTS is a list of <file-system> | 244 | the root directory for the container. MOUNTS is a list of <file-system> |
| 244 | objects that specify file systems to mount inside the container. NAMESPACES | 245 | objects that specify file systems to mount inside the container. NAMESPACES |
| 245 | is a list of symbols that correspond to the possible Linux namespaces: mnt, | 246 | is a list of symbols that correspond to the possible Linux namespaces: mnt, |
| 246 | ipc, uts, user, and net. | 247 | ipc, uts, user, and net. When LOCK-MOUNTS? is true, arrange so that none of |
| 248 | MOUNTS can be unmounted or remounted individually from within THUNK. | ||
| 247 | 249 | ||
| 248 | When LOOPBACK-NETWORK? is true and 'net is amount NAMESPACES, set up the | 250 | When LOOPBACK-NETWORK? is true and 'net is amount NAMESPACES, set up the |
| 249 | loopback device (\"lo\") and a minimal /etc/hosts. | 251 | loopback 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" |
