X-Git-Url: http://www.chiark.greenend.org.uk/ucgi/~ianmdlvl/git?a=blobdiff_plain;f=src%2Fnspawn%2Fnspawn.c;h=edad1cbf423b9ef9ce9eb847d7de47d7d589158a;hb=4d9f07b492ba1707d4a92cd937b87b8baf827f7d;hp=19fb086e7ab8ef2c9a67278f5329d17f10c80dd8;hpb=d6797c920e9eb70f46a893c00fdd9ecb86d15f84;p=elogind.git diff --git a/src/nspawn/nspawn.c b/src/nspawn/nspawn.c index 19fb086e7..edad1cbf4 100644 --- a/src/nspawn/nspawn.c +++ b/src/nspawn/nspawn.c @@ -88,6 +88,8 @@ #include "blkid-util.h" #include "gpt.h" #include "siphash24.h" +#include "copy.h" +#include "base-filesystem.h" #ifdef HAVE_SECCOMP #include "seccomp-util.h" @@ -105,6 +107,12 @@ typedef enum LinkJournal { LINK_GUEST } LinkJournal; +typedef enum Volatile { + VOLATILE_NO, + VOLATILE_YES, + VOLATILE_STATE, +} Volatile; + static char *arg_directory = NULL; static char *arg_user = NULL; static sd_id128_t arg_uuid = {}; @@ -145,6 +153,7 @@ static uint64_t arg_retain = (1ULL << CAP_MKNOD); static char **arg_bind = NULL; static char **arg_bind_ro = NULL; +static char **arg_tmpfs = NULL; static char **arg_setenv = NULL; static bool arg_quiet = false; static bool arg_share_system = false; @@ -156,6 +165,7 @@ static bool arg_network_veth = false; static const char *arg_network_bridge = NULL; static unsigned long arg_personality = 0xffffffffLU; static const char *arg_image = NULL; +static Volatile arg_volatile = VOLATILE_NO; static int help(void) { @@ -199,11 +209,13 @@ static int help(void) { " --bind=PATH[:PATH] Bind mount a file or directory from the host into\n" " the container\n" " --bind-ro=PATH[:PATH] Similar, but creates a read-only bind mount\n" + " --tmpfs=PATH:[OPTIONS] Mount an empty tmpfs to the specified directory\n" " --setenv=NAME=VALUE Pass an environment variable to PID 1\n" " --share-system Share system namespaces with host\n" " --register=BOOLEAN Register container as machine\n" " --keep-unit Do not register a scope for the machine, reuse\n" - " the service unit nspawn is running in\n", + " the service unit nspawn is running in\n" + " --volatile[=MODE] Run the system in volatile mode\n", program_invocation_short_name); return 0; @@ -221,6 +233,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_LINK_JOURNAL, ARG_BIND, ARG_BIND_RO, + ARG_TMPFS, ARG_SETENV, ARG_SHARE_SYSTEM, ARG_REGISTER, @@ -230,6 +243,7 @@ static int parse_argv(int argc, char *argv[]) { ARG_NETWORK_VETH, ARG_NETWORK_BRIDGE, ARG_PERSONALITY, + ARG_VOLATILE, }; static const struct option options[] = { @@ -246,6 +260,7 @@ static int parse_argv(int argc, char *argv[]) { { "link-journal", required_argument, NULL, ARG_LINK_JOURNAL }, { "bind", required_argument, NULL, ARG_BIND }, { "bind-ro", required_argument, NULL, ARG_BIND_RO }, + { "tmpfs", required_argument, NULL, ARG_TMPFS }, { "machine", required_argument, NULL, 'M' }, { "slice", required_argument, NULL, 'S' }, { "setenv", required_argument, NULL, ARG_SETENV }, @@ -261,6 +276,7 @@ static int parse_argv(int argc, char *argv[]) { { "network-bridge", required_argument, NULL, ARG_NETWORK_BRIDGE }, { "personality", required_argument, NULL, ARG_PERSONALITY }, { "image", required_argument, NULL, 'i' }, + { "volatile", optional_argument, NULL, ARG_VOLATILE }, {} }; @@ -468,6 +484,42 @@ static int parse_argv(int argc, char *argv[]) { break; } + case ARG_TMPFS: { + _cleanup_free_ char *a = NULL, *b = NULL; + char *e; + + e = strchr(optarg, ':'); + if (e) { + a = strndup(optarg, e - optarg); + b = strdup(e + 1); + } else { + a = strdup(optarg); + b = strdup("mode=0755"); + } + + if (!a || !b) + return log_oom(); + + if (!path_is_absolute(a)) { + log_error("Invalid tmpfs specification: %s", optarg); + return -EINVAL; + } + + r = strv_push(&arg_tmpfs, a); + if (r < 0) + return log_oom(); + + a = NULL; + + r = strv_push(&arg_tmpfs, b); + if (r < 0) + return log_oom(); + + b = NULL; + + break; + } + case ARG_SETENV: { char **n; @@ -517,6 +569,25 @@ static int parse_argv(int argc, char *argv[]) { break; + case ARG_VOLATILE: + + if (!optarg) + arg_volatile = VOLATILE_YES; + else { + r = parse_boolean(optarg); + if (r < 0) { + if (streq(optarg, "state")) + arg_volatile = VOLATILE_STATE; + else { + log_error("Failed to parse --volatile= argument: %s", optarg); + return r; + } + } else + arg_volatile = r ? VOLATILE_YES : VOLATILE_NO; + } + + break; + case '?': return -EINVAL; @@ -543,6 +614,11 @@ static int parse_argv(int argc, char *argv[]) { return -EINVAL; } + if (arg_volatile != VOLATILE_NO && arg_read_only) { + log_error("Cannot combine --read-only with --volatile. Note that --volatile already implies a read-only base hierarchy."); + return -EINVAL; + } + arg_retain = (arg_retain | plus | (arg_private_network ? 1ULL << CAP_NET_ADMIN : 0)) & ~minus; return 1; @@ -560,17 +636,17 @@ static int mount_all(const char *dest) { } MountPoint; static const MountPoint mount_table[] = { - { "proc", "/proc", "proc", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, true }, - { "/proc/sys", "/proc/sys", NULL, NULL, MS_BIND, true }, /* Bind mount first */ - { NULL, "/proc/sys", NULL, NULL, MS_BIND|MS_RDONLY|MS_REMOUNT, true }, /* Then, make it r/o */ - { "sysfs", "/sys", "sysfs", NULL, MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, true }, - { "tmpfs", "/dev", "tmpfs", "mode=755", MS_NOSUID|MS_STRICTATIME, true }, + { "proc", "/proc", "proc", NULL, MS_NOSUID|MS_NOEXEC|MS_NODEV, true }, + { "/proc/sys", "/proc/sys", NULL, NULL, MS_BIND, true }, /* Bind mount first */ + { NULL, "/proc/sys", NULL, NULL, MS_BIND|MS_RDONLY|MS_REMOUNT, true }, /* Then, make it r/o */ + { "sysfs", "/sys", "sysfs", NULL, MS_RDONLY|MS_NOSUID|MS_NOEXEC|MS_NODEV, true }, + { "tmpfs", "/dev", "tmpfs", "mode=755", MS_NOSUID|MS_STRICTATIME, true }, { "devpts", "/dev/pts", "devpts","newinstance,ptmxmode=0666,mode=620,gid=" STRINGIFY(TTY_GID), MS_NOSUID|MS_NOEXEC, true }, - { "tmpfs", "/dev/shm", "tmpfs", "mode=1777", MS_NOSUID|MS_NODEV|MS_STRICTATIME, true }, - { "tmpfs", "/run", "tmpfs", "mode=755", MS_NOSUID|MS_NODEV|MS_STRICTATIME, true }, + { "tmpfs", "/dev/shm", "tmpfs", "mode=1777", MS_NOSUID|MS_NODEV|MS_STRICTATIME, true }, + { "tmpfs", "/run", "tmpfs", "mode=755", MS_NOSUID|MS_NODEV|MS_STRICTATIME, true }, #ifdef HAVE_SELINUX - { "/sys/fs/selinux", "/sys/fs/selinux", NULL, NULL, MS_BIND, false }, /* Bind mount first */ - { NULL, "/sys/fs/selinux", NULL, NULL, MS_BIND|MS_RDONLY|MS_REMOUNT, false }, /* Then, make it r/o */ + { "/sys/fs/selinux", "/sys/fs/selinux", NULL, NULL, MS_BIND, false }, /* Bind mount first */ + { NULL, "/sys/fs/selinux", NULL, NULL, MS_BIND|MS_RDONLY|MS_REMOUNT, false }, /* Then, make it r/o */ #endif }; @@ -639,7 +715,7 @@ static int mount_binds(const char *dest, char **l, bool ro) { char **x, **y; STRV_FOREACH_PAIR(x, y, l) { - char *where; + _cleanup_free_ char *where = NULL; struct stat source_st, dest_st; int r; @@ -648,12 +724,14 @@ static int mount_binds(const char *dest, char **l, bool ro) { return -errno; } - where = strappenda(dest, *y); + where = strappend(dest, *y); + if (!where) + return log_oom(); + r = stat(where, &dest_st); if (r == 0) { if ((source_st.st_mode & S_IFMT) != (dest_st.st_mode & S_IFMT)) { - log_error("The file types of %s and %s do not match. Refusing bind mount", - *x, where); + log_error("The file types of %s and %s do not match. Refusing bind mount", *x, where); return -EINVAL; } } else if (errno == ENOENT) { @@ -666,8 +744,9 @@ static int mount_binds(const char *dest, char **l, bool ro) { log_error("Failed to bind mount %s: %m", *x); return -errno; } + /* Create the mount point, but be conservative -- refuse to create block - * and char devices. */ + * and char devices. */ if (S_ISDIR(source_st.st_mode)) mkdir_label(where, 0755); else if (S_ISFIFO(source_st.st_mode)) @@ -698,6 +777,27 @@ static int mount_binds(const char *dest, char **l, bool ro) { return 0; } +static int mount_tmpfs(const char *dest) { + char **i, **o; + + STRV_FOREACH_PAIR(i, o, arg_tmpfs) { + _cleanup_free_ char *where = NULL; + + where = strappend(dest, *i); + if (!where) + return log_oom(); + + mkdir_label(where, 0755); + + if (mount("tmpfs", where, "tmpfs", MS_NODEV|MS_STRICTATIME, *o) < 0) { + log_error("tmpfs mount to %s failed: %m", where); + return -errno; + } + } + + return 0; +} + static int setup_timezone(const char *dest) { _cleanup_free_ char *where = NULL, *p = NULL, *q = NULL, *check = NULL, *what = NULL; char *z, *y; @@ -730,7 +830,6 @@ static int setup_timezone(const char *dest) { if (!y) y = path_startswith(q, "/usr/share/zoneinfo/"); - /* Already pointing to the right place? Then do nothing .. */ if (y && streq(y, z)) return 0; @@ -749,7 +848,9 @@ static int setup_timezone(const char *dest) { if (!what) return log_oom(); + mkdir_parents(where, 0755); unlink(where); + if (symlink(what, where) < 0) { log_error("Failed to correct timezone of container: %m"); return 0; @@ -759,7 +860,7 @@ static int setup_timezone(const char *dest) { } static int setup_resolv_conf(const char *dest) { - char _cleanup_free_ *where = NULL; + _cleanup_free_ char *where = NULL; assert(dest); @@ -773,11 +874,105 @@ static int setup_resolv_conf(const char *dest) { /* We don't really care for the results of this really. If it * fails, it fails, but meh... */ - copy_file("/etc/resolv.conf", where, O_TRUNC|O_NOFOLLOW); + mkdir_parents(where, 0755); + copy_file("/etc/resolv.conf", where, O_TRUNC|O_NOFOLLOW, 0644); + + return 0; +} + +static int setup_volatile_state(const char *directory) { + const char *p; + int r; + + assert(directory); + + if (arg_volatile != VOLATILE_STATE) + return 0; + + /* --volatile=state means we simply overmount /var + with a tmpfs, and the rest read-only. */ + + r = bind_remount_recursive(directory, true); + if (r < 0) { + log_error("Failed to remount %s read-only: %s", directory, strerror(-r)); + return r; + } + + p = strappenda(directory, "/var"); + mkdir(p, 0755); + + if (mount("tmpfs", p, "tmpfs", MS_STRICTATIME, "mode=755") < 0) { + log_error("Failed to mount tmpfs to /var: %m"); + return -errno; + } return 0; } +static int setup_volatile(const char *directory) { + bool tmpfs_mounted = false, bind_mounted = false; + char template[] = "/tmp/nspawn-volatile-XXXXXX"; + const char *f, *t; + int r; + + assert(directory); + + if (arg_volatile != VOLATILE_YES) + return 0; + + /* --volatile=yes means we mount a tmpfs to the root dir, and + the original /usr to use inside it, and that read-only. */ + + if (!mkdtemp(template)) { + log_error("Failed to create temporary directory: %m"); + return -errno; + } + + if (mount("tmpfs", template, "tmpfs", MS_STRICTATIME, "mode=755") < 0) { + log_error("Failed to mount tmpfs for root directory: %m"); + r = -errno; + goto fail; + } + + tmpfs_mounted = true; + + f = strappenda(directory, "/usr"); + t = strappenda(template, "/usr"); + + mkdir(t, 0755); + if (mount(f, t, "bind", MS_BIND|MS_REC, NULL) < 0) { + log_error("Failed to create /usr bind mount: %m"); + r = -errno; + goto fail; + } + + bind_mounted = true; + + r = bind_remount_recursive(t, true); + if (r < 0) { + log_error("Failed to remount %s read-only: %s", t, strerror(-r)); + goto fail; + } + + if (mount(template, directory, NULL, MS_MOVE, NULL) < 0) { + log_error("Failed to move root mount: %m"); + r = -errno; + goto fail; + } + + rmdir(template); + + return 0; + +fail: + if (bind_mounted) + umount(t); + if (tmpfs_mounted) + umount(template); + rmdir(template); + return r; +} + static char* id128_format_as_uuid(sd_id128_t id, char s[37]) { snprintf(s, 37, @@ -1798,22 +1993,25 @@ static int setup_macvlan(pid_t pid) { return 0; } -static int audit_still_doesnt_work_in_containers(void) { +static int setup_seccomp(void) { #ifdef HAVE_SECCOMP + static const int blacklist[] = { + SCMP_SYS(kexec_load), + SCMP_SYS(open_by_handle_at), + SCMP_SYS(init_module), + SCMP_SYS(finit_module), + SCMP_SYS(delete_module), + SCMP_SYS(iopl), + SCMP_SYS(ioperm), + SCMP_SYS(swapon), + SCMP_SYS(swapoff), + }; + scmp_filter_ctx seccomp; + unsigned i; int r; - /* - Audit is broken in containers, much of the userspace audit - hookup will fail if running inside a container. We don't - care and just turn off creation of audit sockets. - - This will make socket(AF_NETLINK, *, NETLINK_AUDIT) fail - with EAFNOSUPPORT which audit userspace uses as indication - that audit is disabled in the kernel. - */ - seccomp = seccomp_init(SCMP_ACT_ALLOW); if (!seccomp) return log_oom(); @@ -1824,6 +2022,26 @@ static int audit_still_doesnt_work_in_containers(void) { goto finish; } + for (i = 0; i < ELEMENTSOF(blacklist); i++) { + r = seccomp_rule_add(seccomp, SCMP_ACT_ERRNO(EPERM), blacklist[i], 0); + if (r == -EFAULT) + continue; /* unknown syscall */ + if (r < 0) { + log_error("Failed to block syscall: %s", strerror(-r)); + goto finish; + } + } + + /* + Audit is broken in containers, much of the userspace audit + hookup will fail if running inside a container. We don't + care and just turn off creation of audit sockets. + + This will make socket(AF_NETLINK, *, NETLINK_AUDIT) fail + with EAFNOSUPPORT which audit userspace uses as indication + that audit is disabled in the kernel. + */ + r = seccomp_rule_add( seccomp, SCMP_ACT_ERRNO(EAFNOSUPPORT), @@ -2579,20 +2797,31 @@ static int change_uid_gid(char **_home) { } /* - * Return 0 in case the container is being rebooted, has been shut - * down or exited successfully. On failures a negative value is - * returned. + * Return values: + * < 0 : wait_for_terminate() failed to get the state of the + * container, the container was terminated by a signal, or + * failed for an unknown reason. No change is made to the + * container argument. + * > 0 : The program executed in the container terminated with an + * error. The exit code of the program executed in the + * container is returned. No change is made to the container + * argument. + * 0 : The container is being rebooted, has been shut down or exited + * successfully. The container argument has been set to either + * CONTAINER_TERMINATED or CONTAINER_REBOOTED. * - * The status of the container "CONTAINER_TERMINATED" or - * "CONTAINER_REBOOTED" will be saved in the container argument + * That is, success is indicated by a return value of zero, and an + * error is indicated by a non-zero value. */ static int wait_for_container(pid_t pid, ContainerStatus *container) { int r; siginfo_t status; r = wait_for_terminate(pid, &status); - if (r < 0) + if (r < 0) { + log_warning("Failed to wait for container: %s", strerror(-r)); return r; + } switch (status.si_code) { case CLD_EXITED: @@ -2606,7 +2835,6 @@ static int wait_for_container(pid_t pid, ContainerStatus *container) { } else { log_error("Container %s failed with error code %i.", arg_machine, status.si_status); - r = -1; } break; @@ -2734,7 +2962,7 @@ int main(int argc, char *argv[]) { if (arg_boot) { if (path_is_os_tree(arg_directory) <= 0) { - log_error("Directory %s doesn't look like an OS root directory (/etc/os-release is missing). Refusing.", arg_directory); + log_error("Directory %s doesn't look like an OS root directory (os-release file is missing). Refusing.", arg_directory); goto finish; } } else { @@ -2769,7 +2997,11 @@ int main(int argc, char *argv[]) { goto finish; } - r = dissect_image(image_fd, &root_device, &root_device_rw, &home_device, &home_device_rw, &srv_device, &srv_device_rw, &secondary); + r = dissect_image(image_fd, + &root_device, &root_device_rw, + &home_device, &home_device_rw, + &srv_device, &srv_device_rw, + &secondary); if (r < 0) goto finish; } @@ -2787,7 +3019,8 @@ int main(int argc, char *argv[]) { } if (!arg_quiet) - log_info("Spawning container %s on %s. Press ^] three times within 1s to abort execution.", arg_machine, arg_image ? arg_image : arg_directory); + log_info("Spawning container %s on %s.\nPress ^] three times within 1s to kill container.", + arg_machine, arg_image ? arg_image : arg_directory); if (unlockpt(master) < 0) { log_error("Failed to unlock tty: %m"); @@ -2948,6 +3181,17 @@ int main(int argc, char *argv[]) { goto child_fail; } + r = setup_volatile(arg_directory); + if (r < 0) + goto child_fail; + + if (setup_volatile_state(arg_directory) < 0) + goto child_fail; + + r = base_filesystem_create(arg_directory); + if (r < 0) + goto child_fail; + if (arg_read_only) { k = bind_remount_recursive(arg_directory, true); if (k < 0) { @@ -2967,7 +3211,7 @@ int main(int argc, char *argv[]) { dev_setup(arg_directory); - if (audit_still_doesnt_work_in_containers() < 0) + if (setup_seccomp() < 0) goto child_fail; if (setup_dev_console(arg_directory, console) < 0) @@ -2996,6 +3240,9 @@ int main(int argc, char *argv[]) { if (mount_binds(arg_directory, arg_bind_ro, true) < 0) goto child_fail; + if (mount_tmpfs(arg_directory) < 0) + goto child_fail; + if (setup_kdbus(arg_directory, kdbus_domain) < 0) goto child_fail; @@ -3161,75 +3408,93 @@ int main(int argc, char *argv[]) { * join its cgroup which might limit what it can do */ r = eventfd_child_succeeded(eventfds[1]); eventfds[1] = safe_close(eventfds[1]); - if (r < 0) - goto check_container_status; - r = register_machine(pid); - if (r < 0) - goto finish; + if (r >= 0) { + r = register_machine(pid); + if (r < 0) + goto finish; - r = move_network_interfaces(pid); - if (r < 0) - goto finish; + r = move_network_interfaces(pid); + if (r < 0) + goto finish; - r = setup_veth(pid, veth_name); - if (r < 0) - goto finish; + r = setup_veth(pid, veth_name); + if (r < 0) + goto finish; - r = setup_bridge(veth_name); - if (r < 0) - goto finish; + r = setup_bridge(veth_name); + if (r < 0) + goto finish; - r = setup_macvlan(pid); - if (r < 0) - goto finish; + r = setup_macvlan(pid); + if (r < 0) + goto finish; - /* Block SIGCHLD here, before notifying child. - * process_pty() will handle it with the other signals. */ - r = sigprocmask(SIG_BLOCK, &mask_chld, NULL); - if (r < 0) - goto finish; + /* Block SIGCHLD here, before notifying child. + * process_pty() will handle it with the other signals. */ + r = sigprocmask(SIG_BLOCK, &mask_chld, NULL); + if (r < 0) + goto finish; - /* Reset signal to default */ - r = default_signals(SIGCHLD, -1); - if (r < 0) - goto finish; + /* Reset signal to default */ + r = default_signals(SIGCHLD, -1); + if (r < 0) + goto finish; - /* Notify the child that the parent is ready with all - * its setup, and that the child can now hand over - * control to the code to run inside the container. */ - r = eventfd_send_state(eventfds[0], - EVENTFD_PARENT_SUCCEEDED); - eventfds[0] = safe_close(eventfds[0]); - if (r < 0) - goto finish; + /* Notify the child that the parent is ready with all + * its setup, and that the child can now hand over + * control to the code to run inside the container. */ + r = eventfd_send_state(eventfds[0], EVENTFD_PARENT_SUCCEEDED); + eventfds[0] = safe_close(eventfds[0]); + if (r < 0) + goto finish; - k = process_pty(master, &mask, arg_boot ? pid : 0, SIGRTMIN+3); - if (k < 0) { - r = EXIT_FAILURE; - break; - } + k = process_pty(master, &mask, arg_boot ? pid : 0, SIGRTMIN+3); + if (k < 0) { + r = EXIT_FAILURE; + break; + } - if (!arg_quiet) - putc('\n', stdout); + if (!arg_quiet) + putc('\n', stdout); - /* Kill if it is not dead yet anyway */ - terminate_machine(pid); + /* Kill if it is not dead yet anyway */ + terminate_machine(pid); + } -check_container_status: - /* Redundant, but better safe than sorry */ + /* Normally redundant, but better safe than sorry */ kill(pid, SIGKILL); r = wait_for_container(pid, &container_status); pid = 0; if (r < 0) { + /* We failed to wait for the container, or the + * container exited abnormally */ r = EXIT_FAILURE; break; - } else if (container_status == CONTAINER_TERMINATED) + } else if (r > 0 || container_status == CONTAINER_TERMINATED) + /* The container exited with a non-zero + * status, or with zero status and no reboot + * was requested. */ break; /* CONTAINER_REBOOTED, loop again */ + + if (arg_keep_unit) { + /* Special handling if we are running as a + * service: instead of simply restarting the + * machine we want to restart the entire + * service, so let's inform systemd about this + * with the special exit code 133. The service + * file uses RestartForceExitStatus=133 so + * that this results in a full nspawn + * restart. This is necessary since we might + * have cgroup parameters set we want to have + * flushed out. */ + r = 133; + break; + } } finish: @@ -3246,6 +3511,7 @@ finish: strv_free(arg_network_macvlan); strv_free(arg_bind); strv_free(arg_bind_ro); + strv_free(arg_tmpfs); return r; }