diff --git a/libcontainer/nsenter/nsexec.c b/libcontainer/nsenter/nsexec.c index ed6ee814d..607d495a2 100644 --- a/libcontainer/nsenter/nsexec.c +++ b/libcontainer/nsenter/nsexec.c @@ -420,6 +420,14 @@ void nl_free(struct nlconfig_t *config) free(config->data); } +struct namespace_t { + int fd; + char type[PATH_MAX]; + char path[PATH_MAX]; +}; + +typedef int nsset_t; + static struct nstype_t { int type; char *name; @@ -451,35 +459,28 @@ static int nstype(char *name) bail("unknown namespace type %s", name); } -void join_namespaces(char *nslist) +static nsset_t __open_namespaces(char *nsspec, struct namespace_t **ns_list, size_t *ns_len) { - int num = 0, i; - char *saveptr = NULL; - char *namespace = strtok_r(nslist, ",", &saveptr); - struct namespace_t { - int fd; - char type[PATH_MAX]; - char path[PATH_MAX]; - } *namespaces = NULL; + int len = 0; + nsset_t ns_to_join = 0; + char *namespace, *saveptr = NULL; + struct namespace_t *namespaces = NULL; - if (!namespace || !strlen(namespace) || !strlen(nslist)) + namespace = strtok_r(nsspec, ",", &saveptr); + + if (!namespace || !strlen(namespace) || !strlen(nsspec)) 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; char *path; struct namespace_t *ns; /* Resize the namespace array. */ - namespaces = realloc(namespaces, ++num * sizeof(struct namespace_t)); + namespaces = realloc(namespaces, ++len * sizeof(struct namespace_t)); if (!namespaces) bail("failed to reallocate namespace array"); - ns = &namespaces[num - 1]; + ns = &namespaces[len - 1]; /* Split 'ns:path'. */ path = strstr(namespace, ":"); @@ -495,22 +496,43 @@ void join_namespaces(char *nslist) strncpy(ns->type, namespace, PATH_MAX - 1); strncpy(ns->path, path, PATH_MAX - 1); ns->path[PATH_MAX - 1] = '\0'; + + ns_to_join |= nstype(ns->type); } while ((namespace = strtok_r(NULL, ",", &saveptr)) != NULL); - /* - * The ordering in which we join namespaces is important. We should - * always join the user namespace *first*. This is all guaranteed - * from the container_linux.go side of this, so we're just going to - * follow the order given to us. - */ + *ns_list = namespaces; + *ns_len = len; + return ns_to_join; +} - for (i = 0; i < num; i++) { - struct namespace_t *ns = &namespaces[i]; +/* + * Try to join all namespaces that are in the "allow" nsset, and return the + * set we were able to successfully join. If a permission error is returned + * from nsset(2), the namespace is skipped (non-permission errors are fatal). + */ +static nsset_t __join_namespaces(nsset_t allow, struct namespace_t *ns_list, size_t ns_len) +{ + nsset_t joined = 0; + + for (size_t i = 0; i < ns_len; i++) { + struct namespace_t *ns = &ns_list[i]; int type = nstype(ns->type); + int err, saved_errno; - write_log(DEBUG, "setns(%#x) into %s namespace (with path %s)", type, ns->type, ns->path); - if (setns(ns->fd, type) < 0) + if (!(type & allow)) + continue; + + err = setns(ns->fd, type); + saved_errno = errno; + write_log(DEBUG, "setns(%#x) into %s namespace (with path %s): %s", + type, ns->type, ns->path, strerror(errno)); + if (err < 0) { + /* Skip permission errors. */ + if (saved_errno == EPERM) + continue; bail("failed to setns into %s namespace", ns->type); + } + joined |= type; /* * If we change user namespaces, make sure we switch to root in the @@ -524,9 +546,95 @@ void join_namespaces(char *nslist) } close(ns->fd); + ns->fd = -1; + } + return joined; +} + +static char *strappend(char *dst, char *src) +{ + if (!dst) + return strdup(src); + + size_t len = strlen(dst) + strlen(src) + 1; + dst = realloc(dst, len); + strncat(dst, src, len); + return dst; +} + +static char *nsset_to_str(nsset_t nsset) +{ + char *str = NULL; + for (struct nstype_t * ns = all_ns_types; ns->name != NULL; ns++) { + if (ns->type & nsset) { + if (str) + str = strappend(str, ", "); + str = strappend(str, ns->name); + } + } + return str ? : strdup(""); +} + +static void __close_namespaces(nsset_t to_join, nsset_t joined, struct namespace_t *ns_list, size_t ns_len) +{ + /* We expect to have joined every namespace. */ + nsset_t failed_to_join = to_join & ~joined; + + /* Double-check that we used up (and thus joined) all of the nsfds. */ + for (size_t i = 0; i < ns_len; i++) { + struct namespace_t *ns = &ns_list[i]; + int type = nstype(ns->type); + + if (ns->fd < 0) + continue; + + failed_to_join |= type; + write_log(FATAL, "failed to setns(%#x) into %s namespace (with path %s): %s", + type, ns->type, ns->path, strerror(EPERM)); + close(ns->fd); + ns->fd = -1; } - free(namespaces); + /* Make sure we joined the namespaces we planned to. */ + if (failed_to_join) + bail("failed to join {%s} namespaces: %s", nsset_to_str(failed_to_join), strerror(EPERM)); + + free(ns_list); +} + +void join_namespaces(char *nsspec) +{ + nsset_t to_join = 0, joined = 0; + struct namespace_t *ns_list; + size_t ns_len; + + /* + * We have to open the file descriptors first, since after we join the + * mnt or user namespaces we might no longer be able to access the + * paths. + */ + to_join = __open_namespaces(nsspec, &ns_list, &ns_len); + + /* + * We first try to join all non-userns namespaces to join any namespaces + * that we might not be able to join once we switch credentials to the + * container's userns. We then join the user namespace, and then try to + * join any remaining namespaces (this last step is needed for rootless + * containers -- we don't get setns(2) permissions until we join the userns + * and get CAP_SYS_ADMIN). + * + * Splitting the joins this way is necessary for containers that are + * configured to join some externally-created namespace but are also + * configured to join an unrelated user namespace. + * + * This is similar to what nsenter(1) seems to do in practice. + */ + joined |= __join_namespaces(to_join & ~(joined | CLONE_NEWUSER), ns_list, ns_len); + joined |= __join_namespaces(CLONE_NEWUSER, ns_list, ns_len); + joined |= __join_namespaces(to_join & ~(joined | CLONE_NEWUSER), ns_list, ns_len); + + /* Verify that we joined all of the namespaces. */ + __close_namespaces(to_join, joined, ns_list, ns_len); } static inline int sane_kill(pid_t pid, int signum)