build: linux-container: Fix run-container.
[jackhill/guix/guix.git] / gnu / build / linux-container.scm
index 91996d0..2d4de78 100644 (file)
@@ -1,5 +1,6 @@
 ;;; GNU Guix --- Functional package management for GNU
 ;;; Copyright © 2015 David Thompson <davet@gnu.org>
+;;; Copyright © 2017, 2018, 2019 Ludovic Courtès <ludo@gnu.org>
 ;;;
 ;;; This file is part of GNU Guix.
 ;;;
@@ -21,9 +22,9 @@
   #:use-module (ice-9 match)
   #:use-module (ice-9 rdelim)
   #:use-module (srfi srfi-98)
-  #:use-module (guix utils)
   #:use-module (guix build utils)
   #:use-module (guix build syscalls)
+  #:use-module (gnu system file-systems)          ;<file-system>
   #:use-module ((gnu build file-systems) #:select (mount-file-system))
   #:export (user-namespace-supported?
             unprivileged-user-namespace-supported?
@@ -31,7 +32,8 @@
             %namespaces
             run-container
             call-with-container
-            container-excursion))
+            container-excursion
+            container-excursion*))
 
 (define (user-namespace-supported?)
   "Return #t if user namespaces are supported on this system."
@@ -58,9 +60,14 @@ exists."
     (const #t)
     (lambda ()
       (thunk)
-      (primitive-exit 0))
+
+      ;; XXX: Somehow we sometimes get EBADF from write(2) or close(2) upon
+      ;; exit (coming from fd finalizers) when used by the Shepherd.  To work
+      ;; around that, exit forcefully so fd finalizers don't have a chance to
+      ;; run and fail.
+      (primitive-_exit 0))
     (lambda ()
-      (primitive-exit 1))))
+      (primitive-_exit 1))))
 
 (define (purify-environment)
   "Unset all environment variables."
@@ -72,8 +79,9 @@ exists."
 ;; specification:
 ;; https://raw.githubusercontent.com/docker/libcontainer/master/SPEC.md
 (define* (mount-file-systems root mounts #:key mount-/sys? mount-/proc?)
-  "Mount the essential file systems and the those in the MOUNTS list relative
-to ROOT, then make ROOT the new root directory for the process."
+  "Mount the essential file systems and the those in MOUNTS, a list of
+<file-system> objects, relative to ROOT; then make ROOT the new root directory
+for the process."
   (define (scope dir)
     (string-append root dir))
 
@@ -122,17 +130,28 @@ to ROOT, then make ROOT the new root directory for the process."
               "/dev/random"
               "/dev/urandom"
               "/dev/tty"
-              "/dev/ptmx"
               "/dev/fuse"))
 
+  ;; Mount a new devpts instance on /dev/pts.
+  (when (file-exists? "/dev/ptmx")
+    (mount* "none" (scope "/dev/pts") "devpts" 0
+            "newinstance,mode=0620")
+    (symlink "/dev/pts/ptmx" (scope "/dev/ptmx")))
+
   ;; Setup the container's /dev/console by bind mounting the pseudo-terminal
-  ;; associated with standard input.
-  (let ((in      (current-input-port))
-        (console (scope "/dev/console")))
-    (when (isatty? in)
+  ;; associated with standard input when there is one.
+  (let* ((in      (current-input-port))
+         (tty     (catch 'system-error
+                    (lambda ()
+                      ;; This call throws if IN does not correspond to a tty.
+                      ;; This is more reliable than 'isatty?'.
+                      (ttyname in))
+                    (const #f)))
+         (console (scope "/dev/console")))
+    (when tty
       (touch console)
       (chmod console #o600)
-      (bind-mount (ttyname in) console)))
+      (bind-mount tty console)))
 
   ;; Setup standard input/output/error.
   (symlink "/proc/self/fd"   (scope "/dev/fd"))
@@ -141,8 +160,8 @@ to ROOT, then make ROOT the new root directory for the process."
   (symlink "/proc/self/fd/2" (scope "/dev/stderr"))
 
   ;; Mount user-specified file systems.
-  (for-each (lambda (spec)
-              (mount-file-system spec #:root root))
+  (for-each (lambda (file-system)
+              (mount-file-system file-system #:root root))
             mounts)
 
   ;; Jail the process inside the container's root file system.
@@ -153,9 +172,12 @@ to ROOT, then make ROOT the new root directory for the process."
     (umount "real-root" MNT_DETACH)
     (rmdir "real-root")))
 
-(define (initialize-user-namespace pid host-uids)
+(define* (initialize-user-namespace pid host-uids
+                                    #:key (guest-uid 0) (guest-gid 0))
   "Configure the user namespace for PID.  HOST-UIDS specifies the number of
-host user identifiers to map into the user namespace."
+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."
   (define proc-dir
     (string-append "/proc/" (number->string pid)))
 
@@ -176,10 +198,10 @@ host user identifiers to map into the user namespace."
     ;; within the container.
     (call-with-output-file (scope "/uid_map")
       (lambda (port)
-        (format port "0 ~d ~d" uid host-uids)))
+        (format port "~d ~d ~d" guest-uid uid host-uids)))
     (call-with-output-file (scope "/gid_map")
       (lambda (port)
-        (format port "0 ~d ~d" gid host-uids)))))
+        (format port "~d ~d ~d" guest-gid gid host-uids)))))
 
 (define (namespaces->bit-mask namespaces)
   "Return the number suitable for the 'flags' argument of 'clone' that
@@ -195,13 +217,17 @@ corresponds to the symbols in NAMESPACES."
                ('net  CLONE_NEWNET))
               namespaces)))
 
-(define (run-container root mounts namespaces host-uids thunk)
+(define* (run-container root mounts namespaces host-uids thunk
+                        #:key (guest-uid 0) (guest-gid 0))
   "Run THUNK in a new container process and return its PID.  ROOT specifies
-the root directory for the container.  MOUNTS is a list of file system specs
-that specify the mapping of host file systems into the container.  NAMESPACES
+the root directory for the container.  MOUNTS is a list of <file-system>
+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.  HOST-UIDS specifies the number of
-host user identifiers to map into the user namespace."
+ipc, uts, user, and net.
+
+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."
   ;; The parent process must initialize the user namespace for the child
   ;; before it can boot.  To negotiate this, a pipe is used such that the
   ;; child process blocks until the parent writes to it.
@@ -217,7 +243,8 @@ host user identifiers to map into the user namespace."
              (match (read child)
                ('ready
                 (purify-environment)
-                (when (memq 'mnt namespaces)
+                (when (and (memq 'mnt namespaces)
+                           (not (string=? root "/")))
                   (catch #t
                     (lambda ()
                       (mount-file-systems root mounts
@@ -226,6 +253,8 @@ host user identifiers to map into the user namespace."
                                                                namespaces)))
                     (lambda args
                       ;; Forward the exception to the parent process.
+                      ;; FIXME: SRFI-35 conditions and non-trivial objects
+                      ;; cannot be 'read' so they shouldn't be written as is.
                       (write args child)
                       (primitive-exit 3))))
                 ;; TODO: Manage capabilities.
@@ -237,7 +266,9 @@ host user identifiers to map into the user namespace."
          (pid
           (close-port child)
           (when (memq 'user namespaces)
-            (initialize-user-namespace pid host-uids))
+            (initialize-user-namespace pid host-uids
+                                       #:guest-uid guest-uid
+                                       #:guest-gid guest-gid))
           ;; TODO: Initialize cgroups.
           (write 'ready parent)
           (newline parent)
@@ -253,30 +284,55 @@ host user identifiers to map into the user namespace."
               (_                                  ;unexpected termination
                #f)))))))))
 
+;; FIXME: This is copied from (guix utils), which we cannot use because it
+;; would pull (guix config) and all.
+(define (call-with-temporary-directory proc)
+  "Call PROC with a name of a temporary directory; close the directory and
+delete it when leaving the dynamic extent of this call."
+  (let* ((directory (or (getenv "TMPDIR") "/tmp"))
+         (template  (string-append directory "/guix-directory.XXXXXX"))
+         (tmp-dir   (mkdtemp! template)))
+    (dynamic-wind
+      (const #t)
+      (lambda ()
+        (proc tmp-dir))
+      (lambda ()
+        (false-if-exception (delete-file-recursively tmp-dir))))))
+
 (define* (call-with-container mounts thunk #:key (namespaces %namespaces)
-                              (host-uids 1))
-  "Run THUNK in a new container process and return its exit status.
-MOUNTS is a list of file system specs that specify the mapping of host file
-systems into the container.  NAMESPACES is a list of symbols corresponding to
+                              (host-uids 1) (guest-uid 0) (guest-gid 0)
+                              (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.
+MOUNTS is a list of <file-system> objects that specify file systems to mount
+inside the container.  NAMESPACES is a list of symbols corresponding to
 the identifiers for Linux namespaces: mnt, ipc, uts, pid, user, and net.  By
-default, all namespaces are used.  HOST-UIDS is the number of host user
-identifiers to map into the container's user namespace, if there is one.  By
-default, only a single uid/gid, that of the current user, is mapped into the
-container.  The host user that creates the container is the root user (uid/gid
-0) within the container.  Only root can map more than a single uid/gid.
+default, all namespaces are used.
+
+HOST-UIDS is the number of host user identifiers to map into the container's
+user namespace, if there is one.  By default, only a single uid/gid, that of
+the current user, is mapped into the container.  The host user that creates
+the container is the root user (uid/gid 0) within the container.  Only root
+can map more than a single uid/gid.
+
+GUEST-UID and GUEST-GID specify the first UID (respectively GID) that host
+UIDs (respectively GIDs) map to in the namespace.
 
 Note that if THUNK needs to load any additional Guile modules, the relevant
 module files must be present in one of the mappings in MOUNTS and the Guile
 load path must be adjusted as needed."
   (call-with-temporary-directory
    (lambda (root)
-     (let ((pid (run-container root mounts namespaces host-uids thunk)))
+     (let ((pid (run-container root mounts namespaces host-uids thunk
+                               #:guest-uid guest-uid
+                               #:guest-gid guest-gid)))
        ;; Catch SIGINT and kill the container process.
        (sigaction SIGINT
          (lambda (signum)
            (false-if-exception
             (kill pid SIGKILL))))
 
+       (process-spawned-hook pid)
        (match (waitpid pid)
          ((_ . status) status))))))
 
@@ -291,15 +347,17 @@ return the exit status."
      (call-with-clean-exit
       (lambda ()
         (for-each (lambda (ns)
-                    (call-with-input-file (namespace-file (getpid) ns)
-                      (lambda (current-ns-port)
-                        (call-with-input-file (namespace-file pid ns)
-                          (lambda (new-ns-port)
-                            ;; Joining the namespace that the process
-                            ;; already belongs to would throw an error.
-                            (unless (= (port->fdes current-ns-port)
-                                       (port->fdes new-ns-port))
-                              (setns (port->fdes new-ns-port) 0)))))))
+                    (let ((source (namespace-file (getpid) ns))
+                          (target (namespace-file pid ns)))
+                      ;; Joining the namespace that the process already
+                      ;; belongs to would throw an error so avoid that.
+                      ;; XXX: This /proc interface leads to TOCTTOU.
+                      (unless (string=? (readlink source) (readlink target))
+                        (call-with-input-file source
+                          (lambda (current-ns-port)
+                            (call-with-input-file target
+                              (lambda (new-ns-port)
+                                (setns (fileno new-ns-port) 0))))))))
                   ;; It's important that the user namespace is joined first,
                   ;; so that the user will have the privileges to join the
                   ;; other namespaces.  Furthermore, it's important that the
@@ -313,3 +371,22 @@ return the exit status."
      (match (waitpid pid)
        ((_ . status)
         (status:exit-val status))))))
+
+(define (container-excursion* pid thunk)
+  "Like 'container-excursion', but return the return value of THUNK."
+  (match (pipe)
+    ((in . out)
+     (match (container-excursion pid
+              (lambda ()
+                (close-port in)
+                (write (thunk) out)
+                (close-port out)))
+       (0
+        (close-port out)
+        (let ((result (read in)))
+          (close-port in)
+          result))
+       (_                                         ;maybe PID died already
+        (close-port out)
+        (close-port in)
+        #f)))))