From faa3281ce8f09a1712898d6603e0746e63c6e2f0 Mon Sep 17 00:00:00 2001 From: Aleksa Sarai Date: Mon, 18 Jul 2016 23:44:11 +1000 Subject: [PATCH] nsenter: major cleanup Removed a lot of clutter, improved the style of the code, removed unnecessary complexity. In addition, made errors unique by making bail() exit with a unique error code. Most of this code comes from the current state of the rootless containers branch. Signed-off-by: Aleksa Sarai --- libcontainer/nsenter/nsexec.c | 657 +++++++++++++++++----------------- 1 file changed, 323 insertions(+), 334 deletions(-) diff --git a/libcontainer/nsenter/nsexec.c b/libcontainer/nsenter/nsexec.c index cb7a66299..58d824a63 100644 --- a/libcontainer/nsenter/nsexec.c +++ b/libcontainer/nsenter/nsexec.c @@ -2,269 +2,263 @@ #include #include #include -#include -#include -#include +#include #include #include #include +#include +#include #include #include #include #include -#include -#include -#include #include -#include +#include +#include +#include +#include + +#include +#include #include -// All arguments should be above the stack because it grows down +#define SYNC_VAL 0x42 +#define JUMP_VAL 0x43 + +/* Assume the stack grows down, so arguments should be above it. */ struct clone_arg { /* * Reserve some space for clone() to locate arguments * and retcode in this place */ - char stack[4096] __attribute__((aligned(16))); - char stack_ptr[0]; + char stack[4096] __attribute__ ((aligned(16))); + char stack_ptr[0]; jmp_buf *env; }; struct nsenter_config { uint32_t cloneflags; - char *uidmap; - int uidmap_len; - char *gidmap; - int gidmap_len; - uint8_t is_setgroup; - int consolefd; + char *uidmap; + int uidmap_len; + char *gidmap; + int gidmap_len; + uint8_t is_setgroup; + int consolefd; }; -// list of known message types we want to send to bootstrap program -// These are defined in libcontainer/message_linux.go -#define INIT_MSG 62000 -#define CLONE_FLAGS_ATTR 27281 -#define CONSOLE_PATH_ATTR 27282 -#define NS_PATHS_ATTR 27283 -#define UIDMAP_ATTR 27284 -#define GIDMAP_ATTR 27285 -#define SETGROUP_ATTR 27286 +/* + * List of netlink message types sent to us as part of bootstrapping the init. + * These constants are defined in libcontainer/message_linux.go. + */ +#define INIT_MSG 62000 +#define CLONE_FLAGS_ATTR 27281 +#define CONSOLE_PATH_ATTR 27282 +#define NS_PATHS_ATTR 27283 +#define UIDMAP_ATTR 27284 +#define GIDMAP_ATTR 27285 +#define SETGROUP_ATTR 27286 -// Use raw setns syscall for versions of glibc that don't include it -// (namely glibc-2.12) +/* + * Use the raw syscall for versions of glibc which don't include a function for + * it, namely (glibc 2.12). + */ #if __GLIBC__ == 2 && __GLIBC_MINOR__ < 14 - #define _GNU_SOURCE - #include "syscall.h" - #if defined(__NR_setns) && !defined(SYS_setns) - #define SYS_setns __NR_setns - #endif +# define _GNU_SOURCE +# include "syscall.h" +# if !defined(SYS_setns) && defined(__NR_setns) +# define SYS_setns __NR_setns +# endif - #ifdef SYS_setns - int setns(int fd, int nstype) - { - return syscall(SYS_setns, fd, nstype); - } - #endif +#ifndef SYS_setns +# error "setns(2) syscall not supported by glibc version" #endif -#define pr_perror(fmt, ...) \ - fprintf(stderr, "nsenter: " fmt ": %m\n", ##__VA_ARGS__) - -static int child_func(void *_arg) +int setns(int fd, int nstype) { - struct clone_arg *arg = (struct clone_arg *)_arg; - longjmp(*arg->env, 1); + return syscall(SYS_setns, fd, nstype); +} +#endif + +/* TODO(cyphar): Fix this so it correctly deals with syncT. */ +#define bail(fmt, ...) \ + do { \ + fprintf(stderr, "nsenter: " fmt ": %m\n", ##__VA_ARGS__); \ + exit(__COUNTER__ + 1); \ + } while(0) + +static int child_func(void *arg) +{ + struct clone_arg *ca = (struct clone_arg *)arg; + longjmp(*ca->env, JUMP_VAL); } -static int clone_parent(jmp_buf *env, int flags) __attribute__((noinline)); +static int clone_parent(jmp_buf *env, int flags) __attribute__ ((noinline)); static int clone_parent(jmp_buf *env, int flags) { - struct clone_arg ca; - int child; + int child; + struct clone_arg ca = { + .env = env, + }; - ca.env = env; - child = clone(child_func, ca.stack_ptr, CLONE_PARENT | SIGCHLD | flags, - &ca); - // On old kernels, CLONE_PARENT cannot work with CLONE_NEWPID, - // unshare before clone to workaround this. - if (child == -1 && errno == EINVAL) { - if (unshare(flags)) { - pr_perror("Unable to unshare namespaces"); - return -1; - } - child = clone(child_func, ca.stack_ptr, SIGCHLD | CLONE_PARENT, - &ca); + child = clone(child_func, ca.stack_ptr, CLONE_PARENT | SIGCHLD | flags, &ca); + + /* + * On old kernels, CLONE_PARENT didn't work with CLONE_NEWPID, so we have + * to unshare(2) before clone(2) in order to do this. This was fixed in + * upstream commit 1f7f4dde5c945f41a7abc2285be43d918029ecc5, and was + * introduced by by 40a0d32d1eaffe6aac7324ca92604b6b3977eb0e. + * + * As far as we're aware, the last mainline kernel which had this bug was + * Linux 3.12. However, we cannot comment on which kernels the broken patch + * was backported to. + */ + if (errno == EINVAL) { + if (unshare(flags) < 0) + bail("unable to unshare namespaces"); + child = clone(child_func, ca.stack_ptr, SIGCHLD | CLONE_PARENT, &ca); } + return child; } -// get init pipe from the parent. It's used to read bootstrap data, and to -// write pid to after nsexec finishes setting up the environment. -static int get_init_pipe() +/* + * Gets the init pipe fd from the environment, which is used to read the + * bootstrap data and tell the parent what the new pid is after we finish + * setting up the environment. + */ +static int initpipe(void) { - char buf[PATH_MAX]; - char *initpipe; - int pipenum = -1; + int pipenum; + char *initpipe, *endptr; initpipe = getenv("_LIBCONTAINER_INITPIPE"); - if (initpipe == NULL) { + if (initpipe == NULL || *initpipe == '\0') return -1; - } - pipenum = atoi(initpipe); - snprintf(buf, sizeof(buf), "%d", pipenum); - if (strcmp(initpipe, buf)) { - pr_perror("Unable to parse _LIBCONTAINER_INITPIPE"); - exit(1); - } + errno = 0; + pipenum = strtol(initpipe, &endptr, 10); + if (errno != 0 || *endptr != '\0') + bail("unable to parse _LIBCONTAINER_INITPIPE"); return pipenum; } -// num_namespaces returns the number of additional namespaces to setns. The -// argument is a comma-separated string of namespace paths. -static int num_namespaces(char *nspaths) -{ - int i; - int size = 0; - - for (i = 0; nspaths[i]; i++) { - if (nspaths[i] == ',') { - size += 1; - } - } - - return size + 1; -} - static uint32_t readint32(char *buf) { - return *(uint32_t *)buf; + return *(uint32_t *) buf; } static uint8_t readint8(char *buf) { - return *(uint8_t *)buf; + return *(uint8_t *) buf; } -static void update_process_idmap(char *pathfmt, int pid, char *map, int map_len) +static int write_file(char *data, size_t data_len, char *pathfmt, ...) { - char buf[PATH_MAX]; - int len; - int fd; + int fd, len, ret = 0; + char path[PATH_MAX]; - len = snprintf(buf, sizeof(buf), pathfmt, pid); - if (len < 0) { - pr_perror("failed to construct '%s' for %d", pathfmt, pid); - exit(1); + va_list ap; + va_start(ap, pathfmt); + len = vsnprintf(path, PATH_MAX, pathfmt, ap); + va_end(ap); + if (len < 0) + return -1; + + fd = open(path, O_RDWR); + if (fd < 0) { + ret = -1; + goto out; } - fd = open(buf, O_RDWR); - if (fd == -1) { - pr_perror("failed to open %s", buf); - exit(1); - } - - len = write(fd, map, map_len); - if (len == -1) { - pr_perror("failed to write to %s", buf); - close(fd); - exit(1); - } else if (len != map_len) { - pr_perror("Failed to write data to %s (%d/%d)", - buf, len, map_len); - close(fd); - exit(1); + len = write(fd, data, data_len); + if (len != data_len) { + ret = -1; + goto out; } +out: close(fd); + return ret; } -static void update_process_uidmap(int pid, char *map, int map_len) +#define SETGROUPS_ALLOW "allow" +#define SETGROUPS_DENY "deny" + +/* This *must* be called before we touch gid_map. */ +static void update_setgroups(int pid, bool setgroup) { - if ((map == NULL) || (map_len <= 0)) { + char *policy; + + if (setgroup) + policy = SETGROUPS_ALLOW; + else + policy = SETGROUPS_DENY; + + if (write_file(policy, strlen(policy), "/proc/%d/setgroups", pid) < 0) { + /* + * If the kernel is too old to support /proc/pid/setgroups, + * open(2) or write(2) will return ENOENT. This is fine. + */ + if (errno != ENOENT) + bail("failed to write '%s' to /proc/%d/setgroups", policy, pid); + } +} + +static void update_uidmap(int pid, char *map, int map_len) +{ + if (map == NULL || map_len <= 0) return; - } - update_process_idmap("/proc/%d/uid_map", pid, map, map_len); + if (write_file(map, map_len, "/proc/%d/uid_map", pid) < 0) + bail("failed to update /proc/%d/uid_map", pid); } -static void update_process_gidmap(int pid, uint8_t is_setgroup, char *map, int map_len) +static void update_gidmap(int pid, char *map, int map_len) { - if ((map == NULL) || (map_len <= 0)) { + if (map == NULL || map_len <= 0) return; - } - if (is_setgroup == 1) { - int fd; - int len; - char buf[PATH_MAX]; - - len = snprintf(buf, sizeof(buf), "/proc/%d/setgroups", pid); - if (len < 0) { - pr_perror("failed to get setgroups path for %d", pid); - exit(1); - } - - fd = open(buf, O_RDWR); - if (fd == -1) { - pr_perror("failed to open %s", buf); - exit(1); - } - if (write(fd, "allow", 5) != 5) { - // If the kernel is too old to support - // /proc/PID/setgroups, write will return - // ENOENT; this is OK. - if (errno != ENOENT) { - pr_perror("failed to write allow to %s", buf); - close(fd); - exit(1); - } - } - close(fd); - } - - update_process_idmap("/proc/%d/gid_map", pid, map, map_len); + if (write_file(map, map_len, "/proc/%d/gid_map", pid) < 0) + bail("failed to update /proc/%d/gid_map", pid); } +#define JSON_MAX 4096 -static void start_child(int pipenum, jmp_buf *env, int syncpipe[2], - struct nsenter_config *config) +static void start_child(int pipenum, jmp_buf *env, int syncpipe[2], struct nsenter_config *config) { - int len; - int childpid; - char buf[PATH_MAX]; - uint8_t syncbyte = 1; + int len, childpid; + char buf[JSON_MAX]; + uint8_t syncval; - // We must fork to actually enter the PID namespace, use CLONE_PARENT - // so the child can have the right parent, and we don't need to forward - // the child's exit code or resend its death signal. + /* + * We must fork to actually enter the PID namespace, and use + * CLONE_PARENT so that the child init can have the right parent + * (the bootstrap process). Also so we don't need to forward the + * child's exit code or resend its death signal. + */ childpid = clone_parent(env, config->cloneflags); - if (childpid < 0) { - pr_perror("Unable to fork"); - exit(1); - } + if (childpid < 0) + bail("unable to fork"); - // update uid_map and gid_map for the child process if they - // were provided - update_process_uidmap(childpid, config->uidmap, config->uidmap_len); - update_process_gidmap(childpid, config->is_setgroup, config->gidmap, config->gidmap_len); + /* Update setgroups, uid_map and gid_map for the process if provided. */ + if (config->is_setgroup) + update_setgroups(childpid, true); + update_uidmap(childpid, config->uidmap, config->uidmap_len); + update_gidmap(childpid, config->gidmap, config->gidmap_len); - // Send the sync signal to the child + /* Send the sync signal to the child. */ close(syncpipe[0]); - syncbyte = 1; - if (write(syncpipe[1], &syncbyte, 1) != 1) { - pr_perror("failed to write sync byte to child"); - exit(1); - } + syncval = SYNC_VAL; + if (write(syncpipe[1], &syncval, sizeof(syncval)) != sizeof(syncval)) + bail("failed to write sync byte to child"); - // Send the child pid back to our parent - len = snprintf(buf, sizeof(buf), "{ \"pid\" : %d }\n", childpid); - if ((len < 0) || (write(pipenum, buf, len) != len)) { - pr_perror("Unable to send a child pid"); + /* Send the child pid back to our parent */ + len = snprintf(buf, JSON_MAX, "{\"pid\": %d}\n", childpid); + if (len < 0 || write(pipenum, buf, len) != len) { kill(childpid, SIGKILL); - exit(1); + bail("unable to send a child pid"); } exit(0); @@ -272,81 +266,98 @@ static void start_child(int pipenum, jmp_buf *env, int syncpipe[2], static struct nsenter_config process_nl_attributes(int pipenum, char *data, int data_size) { - struct nsenter_config config = {0}; - struct nlattr *nlattr; - int payload_len; - int start = 0; + char *current = data; + struct nsenter_config config = { 0 }; config.consolefd = -1; - while (start < data_size) { - nlattr = (struct nlattr *)(data + start); - start += NLA_HDRLEN; - payload_len = nlattr->nla_len - NLA_HDRLEN; + while (current < data + data_size) { + struct nlattr *nlattr = (struct nlattr *)current; + size_t payload_len = nlattr->nla_len - NLA_HDRLEN; - if (nlattr->nla_type == CLONE_FLAGS_ATTR) { - config.cloneflags = readint32(data + start); - } else if (nlattr->nla_type == CONSOLE_PATH_ATTR) { - // get the console path before setns because it may - // change mnt namespace - config.consolefd = open(data + start, O_RDWR); - if (config.consolefd < 0) { - pr_perror("Failed to open console %s", - data + start); - exit(1); + /* Advance to payload. */ + current += NLA_HDRLEN; + + /* Handle payload. */ + switch (nlattr->nla_type) { + case CLONE_FLAGS_ATTR: + config.cloneflags = readint32(current); + break; + case CONSOLE_PATH_ATTR: + /* + * The context in which this is done (before or after we + * join the other namespaces) will affect how the path + * resolution of the console works. This order is not + * decided here, but rather in container_linux.go. We just + * follow the order given by the netlink message. + */ + config.consolefd = open(current, O_RDWR); + if (config.consolefd < 0) + bail("failed to open console %s", current); + break; + case NS_PATHS_ATTR:{ + /* + * Open each namespace path and setns it in the + * order provided to us. We currently don't have + * any context for what kind of namespace we're + * joining, so just blindly do it. + */ + char *saveptr = NULL; + char *ns = strtok_r(current, ",", &saveptr); + int *fds = NULL, num = 0, i; + char **paths = NULL; + + if (!ns || !strlen(current)) + bail("ns paths are empty"); + + /* + * We have to open the file descriptors first, since after + * we join the mnt namespace we might no longer be able to + * access the paths. + */ + do { + int fd; + + /* Resize fds. */ + num++; + fds = realloc(fds, num * sizeof(int)); + paths = realloc(paths, num * sizeof(char *)); + + fd = open(ns, O_RDONLY); + if (fd < 0) + bail("failed to open %s", ns); + + fds[num - 1] = fd; + paths[num - 1] = ns; + } while ((ns = strtok_r(NULL, ",", &saveptr)) != NULL); + + for (i = 0; i < num; i++) { + int fd = fds[i]; + char *path = paths[i]; + + if (setns(fd, 0) < 0) + bail("failed to setns to %s", path); + + close(fd); + } + + break; } - } else if (nlattr->nla_type == NS_PATHS_ATTR) { - // if custom namespaces are required, open all - // descriptors and perform setns on them - int i, j; - int nslen = num_namespaces(data + start); - int fds[nslen]; - char *nslist[nslen]; - char *ns; - char *saveptr = NULL; - - for (i = 0; i < nslen; i++) { - char *str = NULL; - - if (i == 0) { - str = data + start; - } - ns = strtok_r(str, ",", &saveptr); - if (ns == NULL) { - break; - } - fds[i] = open(ns, O_RDONLY); - if (fds[i] == -1) { - for (j = 0; j < i; j++) { - close(fds[j]); - } - pr_perror("Failed to open %s", ns); - exit(1); - } - nslist[i] = ns; - } - - for (i = 0; i < nslen; i++) { - if (setns(fds[i], 0) != 0) { - pr_perror("Failed to setns to %s", nslist[i]); - exit(1); - } - close(fds[i]); - } - } else if (nlattr->nla_type == UIDMAP_ATTR) { - config.uidmap = data + start; + case UIDMAP_ATTR: + config.uidmap = current; config.uidmap_len = payload_len; - } else if (nlattr->nla_type == GIDMAP_ATTR) { - config.gidmap = data + start; + break; + case GIDMAP_ATTR: + config.gidmap = current; config.gidmap_len = payload_len; - } else if (nlattr->nla_type == SETGROUP_ATTR) { - config.is_setgroup = readint8(data + start); - } else { - pr_perror("Unknown netlink message type %d", - nlattr->nla_type); - exit(1); + break; + case SETGROUP_ATTR: + config.is_setgroup = readint8(current); + break; + default: + bail("unknown netlink message type %d", nlattr->nla_type); } - start += NLA_ALIGN(payload_len); + current += NLA_ALIGN(payload_len); } return config; @@ -356,116 +367,94 @@ void nsexec(void) { int pipenum; - // If we don't have init pipe, then just return to the go routine, - // we'll only have init pipe for start or exec - pipenum = get_init_pipe(); - if (pipenum == -1) { + /* + * If we don't have an init pipe, just return to the go routine. + * We'll only get an init pipe for start or exec. + */ + pipenum = initpipe(); + if (pipenum == -1) return; - } - // Retrieve the netlink header - struct nlmsghdr nl_msg_hdr; - int len; + /* Retrieve the netlink header. */ + int len; + struct nlmsghdr hdr; - if ((len = read(pipenum, &nl_msg_hdr, NLMSG_HDRLEN)) != NLMSG_HDRLEN) { - pr_perror("Invalid netlink header length %d", len); - exit(1); - } + len = read(pipenum, &hdr, NLMSG_HDRLEN); + if (len != NLMSG_HDRLEN) + bail("invalid netlink header length %d", len); - if (nl_msg_hdr.nlmsg_type == NLMSG_ERROR) { - pr_perror("Failed to read netlink message"); - exit(1); - } + if (hdr.nlmsg_type == NLMSG_ERROR) + bail("failed to read netlink message"); - if (nl_msg_hdr.nlmsg_type != INIT_MSG) { - pr_perror("Unexpected msg type %d", nl_msg_hdr.nlmsg_type); - exit(1); - } + if (hdr.nlmsg_type != INIT_MSG) + bail("unexpected msg type %d", hdr.nlmsg_type); - // Retrieve data - int nl_total_size = NLMSG_PAYLOAD(&nl_msg_hdr, 0); - char data[nl_total_size]; + /* Retrieve data. */ + size_t size = NLMSG_PAYLOAD(&hdr, 0); + char *data = alloca(size); - if ((len = read(pipenum, data, nl_total_size)) != nl_total_size) { - pr_perror("Failed to read netlink payload, %d != %d", len, - nl_total_size); - exit(1); - } + len = read(pipenum, data, size); + if (len != size) + bail("failed to read netlink payload, %d != %lu", len, size); - jmp_buf env; - int syncpipe[2] = {-1, -1}; - struct nsenter_config config = process_nl_attributes(pipenum, - data, nl_total_size); + jmp_buf env; + int syncpipe[2]; + struct nsenter_config config = process_nl_attributes(pipenum, data, size); - // required clone_flags to be passed - if (config.cloneflags == -1) { - pr_perror("Missing clone_flags"); - exit(1); - } - // prepare sync pipe between parent and child. We need this to let the - // child know that the parent has finished setting up - if (pipe(syncpipe) != 0) { - pr_perror("Failed to setup sync pipe between parent and child"); - exit(1); - } + /* clone(2) flags are mandatory. */ + if (config.cloneflags == -1) + bail("missing clone_flags"); - if (setjmp(env) == 1) { - // Child + /* Pipe so we can tell the child when we've finished setting up. */ + if (pipe(syncpipe) < 0) + bail("failed to setup sync pipe between parent and child"); + + /* Set up the jump point. */ + if (setjmp(env) == JUMP_VAL) { + /* + * We're inside the child now, having jumped from the + * start_child() code after forking in the parent. + */ uint8_t s = 0; - int consolefd = config.consolefd; + int consolefd = config.consolefd; - // close the writing side of pipe + /* Close the writing side of pipe. */ close(syncpipe[1]); - // sync with parent - if ((read(syncpipe[0], &s, 1) != 1) || (s != 1)) { - pr_perror("Failed to read sync byte from parent"); - exit(1); - } + /* Sync with parent. */ + if (read(syncpipe[0], &s, sizeof(s)) != sizeof(s) || s != SYNC_VAL) + bail("failed to read sync byte from parent"); - if (setsid() == -1) { - pr_perror("setsid failed"); - exit(1); - } + if (setsid() < 0) + bail("setsid failed"); - if (setuid(0) == -1) { - pr_perror("setuid failed"); - exit(1); - } + if (setuid(0) < 0) + bail("setuid failed"); - if (setgid(0) == -1) { - pr_perror("setgid failed"); - exit(1); - } - - if (setgroups(0, NULL) == -1) { - pr_perror("setgroups failed"); - exit(1); - } + if (setgid(0) < 0) + bail("setgid failed"); + + if (setgroups(0, NULL) < 0) + bail("setgroups failed"); if (consolefd != -1) { - if (ioctl(consolefd, TIOCSCTTY, 0) == -1) { - pr_perror("ioctl TIOCSCTTY failed"); - exit(1); - } - if (dup3(consolefd, STDIN_FILENO, 0) != STDIN_FILENO) { - pr_perror("Failed to dup stdin"); - exit(1); - } - if (dup3(consolefd, STDOUT_FILENO, 0) != STDOUT_FILENO) { - pr_perror("Failed to dup stdout"); - exit(1); - } - if (dup3(consolefd, STDERR_FILENO, 0) != STDERR_FILENO) { - pr_perror("Failed to dup stderr"); - exit(1); - } + if (ioctl(consolefd, TIOCSCTTY, 0) < 0) + bail("ioctl TIOCSCTTY failed"); + if (dup3(consolefd, STDIN_FILENO, 0) != STDIN_FILENO) + bail("failed to dup stdin"); + if (dup3(consolefd, STDOUT_FILENO, 0) != STDOUT_FILENO) + bail("failed to dup stdout"); + if (dup3(consolefd, STDERR_FILENO, 0) != STDERR_FILENO) + bail("failed to dup stderr"); } - // Finish executing, let the Go runtime take over. + /* Finish executing, let the Go runtime take over. */ return; } - // Parent + /* Run the parent code. */ start_child(pipenum, &env, syncpipe, &config); + + /* Should never be reached. */ + bail("should never be reached"); }