summaryrefslogtreecommitdiff
path: root/gnu
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 /gnu
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
Diffstat (limited to 'gnu')
-rw-r--r--gnu/build/linux-container.scm111
1 files changed, 74 insertions, 37 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