diff --git a/container.h b/container.h index dede696..63b5a8c 100644 --- a/container.h +++ b/container.h @@ -18,6 +18,9 @@ #include #include +#define PROCD_NOAFILE_DIR "/tmp/.ujail" +#define PROCD_NOAFILE PROCD_NOAFILE_DIR "/noafile" + static inline bool is_container() { struct stat s; int r = stat("/.dockerenv", &s); diff --git a/initd/early.c b/initd/early.c index aa164d7..dae6068 100644 --- a/initd/early.c +++ b/initd/early.c @@ -52,6 +52,25 @@ early_console(const char *dev) fcntl(STDERR_FILENO, F_SETFL, fcntl(STDERR_FILENO, F_GETFL) | O_NONBLOCK); } +static void +early_noafile(void) +{ + int fd; + + mkdir(PROCD_NOAFILE_DIR, 0000); + if (mount("tmpfs", PROCD_NOAFILE_DIR, "tmpfs", + MS_NOSUID | MS_NODEV | MS_NOEXEC, "size=4k,mode=000")) + return; + + fd = creat(PROCD_NOAFILE, 0000); + if (fd >= 0) + close(fd); + + if (mount(NULL, PROCD_NOAFILE_DIR, NULL, + MS_REMOUNT | MS_RDONLY | MS_NOSUID | MS_NODEV | MS_NOEXEC, NULL)) + ERROR("failed to lock %s read-only: %m\n", PROCD_NOAFILE_DIR); +} + static void early_mounts(void) { @@ -73,6 +92,7 @@ early_mounts(void) early_console("/dev/console"); mount("tmpfs", "/tmp", "tmpfs", MS_NOSUID | MS_NODEV | MS_NOATIME, "mode=01777"); + early_noafile(); mkdir("/tmp/shm", 01777); mkdir("/tmp/run", 0755); diff --git a/jail/cgroups.c b/jail/cgroups.c index c2f93ea..198b5a2 100644 --- a/jail/cgroups.c +++ b/jail/cgroups.c @@ -96,6 +96,8 @@ void cgroups_free(void) free(valp); } free(cgroup_path); + cgroup_path = NULL; + initialized = false; } } diff --git a/jail/elf.c b/jail/elf.c index 978fc6e..d343b15 100644 --- a/jail/elf.c +++ b/jail/elf.c @@ -14,11 +14,14 @@ #define _GNU_SOURCE #include +#include #include #include #include #include #include +#include +#include #include @@ -29,6 +32,29 @@ struct avl_tree libraries; static LIST_HEAD(library_paths); +/* DT_RPATH search dirs: transitive, inherited by dependencies-of-dependencies. */ +static LIST_HEAD(rpath_scope); + +/* DT_RUNPATH search dirs: NON-transitive (applies only to the defining + * object's direct DT_NEEDED entries), so it's reset and restored on every + * scan call regardless of whether that object defines its own. + */ +static LIST_HEAD(runpath_scope); + +/* Trailing-slash-stripped length of `path`, keeping the one slash of root + * ("/"), so stored dirs are canonical and path_exists_in() can dedup by + * plain length+content comparison (e.g. "/foo" == "/foo/"). + */ +static size_t normalized_len(const char *path) +{ + size_t len = strlen(path); + + while (len > 1 && path[len - 1] == '/') + len--; + + return len; +} + static void alloc_library_path(const char *path) { struct stat s; @@ -37,16 +63,145 @@ static void alloc_library_path(const char *path) struct library_path *p; char *_path; + size_t len = normalized_len(path); p = calloc_a(sizeof(*p), - &_path, strlen(path) + 1); + &_path, len + 1); if (!p) return; - p->path = strcpy(_path, path); + p->path = _path; + memcpy(p->path, path, len); + p->path[len] = '\0'; list_add_tail(&p->list, &library_paths); - DEBUG("adding ld.so path %s\n", path); + DEBUG("adding ld.so path %s\n", p->path); +} + +static bool path_exists_in(struct list_head *list, const char *path) +{ + struct library_path *p; + size_t len = normalized_len(path); + + list_for_each_entry(p, list, list) + if (strlen(p->path) == len && !memcmp(p->path, path, len)) + return true; + + return false; +} + +/* + * If `path` exists and isn't already in `target` or `own`, append it to + * `own` (list_add_tail, not list_add: preserves left-to-right RPATH/RUNPATH + * order once `own` is later spliced onto the front of `target`). + */ +static void alloc_library_path_front(struct list_head *own, struct list_head *target, const char *path) +{ + struct stat s; + struct library_path *p; + char *_path; + size_t len; + + if (stat(path, &s)) + return; + + if (path_exists_in(target, path) || path_exists_in(own, path)) + return; + + len = normalized_len(path); + + p = calloc_a(sizeof(*p), + &_path, len + 1); + if (!p) + return; + + p->path = _path; + memcpy(p->path, path, len); + p->path[len] = '\0'; + + list_add_tail(&p->list, own); + DEBUG("adding rpath/runpath dir %s\n", p->path); +} + +/* + * Expand an RPATH/RUNPATH string ($ORIGIN, empty-token==cwd, ":"-separated) + * and prepend its directories onto `target`, preserving left-to-right order. + * Returns a boundary marker usable with free_scope_dirs() to later remove + * exactly the entries added here. + */ +static struct list_head *add_rpath_dirs(struct list_head *target, const char *rpath, const char *binpath) +{ + struct list_head *boundary = target->next; + LIST_HEAD(own); + char origin_dir[PATH_MAX] = ""; + char cwd[PATH_MAX]; + bool have_cwd = false; + char *copy, *rest, *tok; + char *slash; + + if (!rpath || !*rpath) + return boundary; + + snprintf(origin_dir, sizeof(origin_dir), "%s", binpath); + slash = strrchr(origin_dir, '/'); + if (slash) + *slash = '\0'; + else + origin_dir[0] = '\0'; + + copy = strdup(rpath); + if (!copy) + return boundary; + + rest = copy; + while ((tok = strsep(&rest, ":")) != NULL) { + char resolved[PATH_MAX]; + size_t originlen = 0; + + if (!*tok) { + if (!have_cwd) { + if (!getcwd(cwd, sizeof(cwd))) + continue; + have_cwd = true; + } + alloc_library_path_front(&own, target, cwd); + continue; + } + + if (!strncmp(tok, "$ORIGIN", 7) && + (tok[7] == '\0' || tok[7] == '/')) + originlen = 7; + else if (!strncmp(tok, "${ORIGIN}", 9) && + (tok[9] == '\0' || tok[9] == '/')) + originlen = 9; + + if (originlen) { + snprintf(resolved, sizeof(resolved), "%s%s", origin_dir, tok + originlen); + alloc_library_path_front(&own, target, resolved); + } else { + alloc_library_path_front(&own, target, tok); + } + } + + free(copy); + + list_splice(&own, target); + + return boundary; +} + +/* + * Free every entry that was added to `target` in front of `boundary` + * (i.e. everything a matching add_rpath_dirs() call spliced in), restoring + * `target` to exactly the state it had before that call. + */ +static void free_scope_dirs(struct list_head *target, struct list_head *boundary) +{ + while (target->next != boundary) { + struct library_path *p = list_entry(target->next, struct library_path, list); + list_del(&p->list); + free(p); + } } /* @@ -79,12 +234,30 @@ int lib_open(char **fullpath, const char *file) *fullpath = NULL; + list_for_each_entry(p, &runpath_scope, list) { + snprintf(path, sizeof(path), "%s/%s", p->path, file); + fd = open(path, O_RDONLY|O_CLOEXEC); + if (fd >= 0) { + *fullpath = strdup(path); + return fd; + } + } + + list_for_each_entry(p, &rpath_scope, list) { + snprintf(path, sizeof(path), "%s/%s", p->path, file); + fd = open(path, O_RDONLY|O_CLOEXEC); + if (fd >= 0) { + *fullpath = strdup(path); + return fd; + } + } + list_for_each_entry(p, &library_paths, list) { snprintf(path, sizeof(path), "%s/%s", p->path, file); fd = open(path, O_RDONLY|O_CLOEXEC); if (fd >= 0) { *fullpath = strdup(path); - break; + return fd; } } @@ -102,16 +275,28 @@ const char* find_lib(const char *file) return l->path; } -static int elf64_find_section(const char *map, unsigned int type, unsigned long *offset, unsigned long *size, unsigned long *vaddr) +static int elf64_find_section(const char *map, unsigned long map_size, unsigned int type, unsigned long *offset, unsigned long *size, unsigned long *vaddr) { Elf64_Ehdr *e; Elf64_Phdr *ph; - int i; + unsigned long phoff, phnum, i; + + if (map_size < sizeof(Elf64_Ehdr)) + return -1; e = (Elf64_Ehdr *) map; - ph = (Elf64_Phdr *) (map + e->e_phoff); + phoff = e->e_phoff; + phnum = e->e_phnum; + + if (e->e_phentsize != sizeof(Elf64_Phdr)) + return -1; - for (i = 0; i < e->e_phnum; i++) { + if (phoff >= map_size || phnum > (map_size - phoff) / sizeof(Elf64_Phdr)) + return -1; + + ph = (Elf64_Phdr *) (map + phoff); + + for (i = 0; i < phnum; i++) { if (ph[i].p_type == type) { *offset = ph[i].p_offset; if (size) @@ -125,16 +310,28 @@ static int elf64_find_section(const char *map, unsigned int type, unsigned long return -1; } -static int elf32_find_section(const char *map, unsigned int type, unsigned long *offset, unsigned long *size, unsigned long *vaddr) +static int elf32_find_section(const char *map, unsigned long map_size, unsigned int type, unsigned long *offset, unsigned long *size, unsigned long *vaddr) { Elf32_Ehdr *e; Elf32_Phdr *ph; - int i; + unsigned long phoff, phnum, i; + + if (map_size < sizeof(Elf32_Ehdr)) + return -1; e = (Elf32_Ehdr *) map; - ph = (Elf32_Phdr *) (map + e->e_phoff); + phoff = e->e_phoff; + phnum = e->e_phnum; - for (i = 0; i < e->e_phnum; i++) { + if (e->e_phentsize != sizeof(Elf32_Phdr)) + return -1; + + if (phoff >= map_size || phnum > (map_size - phoff) / sizeof(Elf32_Phdr)) + return -1; + + ph = (Elf32_Phdr *) (map + phoff); + + for (i = 0; i < phnum; i++) { if (ph[i].p_type == type) { *offset = ph[i].p_offset; if (size) @@ -148,114 +345,301 @@ static int elf32_find_section(const char *map, unsigned int type, unsigned long return -1; } -static int elf_find_section(const char *map, unsigned int type, unsigned long *offset, unsigned long *size, unsigned long *vaddr) +static int elf_find_section(const char *map, unsigned long map_size, unsigned int type, unsigned long *offset, unsigned long *size, unsigned long *vaddr) { - int clazz = map[EI_CLASS]; + int clazz; + + if (map_size < 1 + EI_CLASS) + return -1; + + clazz = map[EI_CLASS]; if (clazz == ELFCLASS32) - return elf32_find_section(map, type, offset, size, vaddr); + return elf32_find_section(map, map_size, type, offset, size, vaddr); else if (clazz == ELFCLASS64) - return elf64_find_section(map, type, offset, size, vaddr); + return elf64_find_section(map, map_size, type, offset, size, vaddr); ERROR("unknown elf format %d\n", clazz); return -1; } -static int elf32_scan_dynamic(const char *map, unsigned long dyn_offset, unsigned long dyn_size, long load_offset) +/* + * Resolve a DT_* string-table offset into a NUL-terminated C string, + * rejecting anything (bad DT_STRSZ/d_val) that would read outside the + * mapped file. + */ +static const char *dyn_str(const char *map, unsigned long map_size, + const char *strtab, unsigned long str_size, + unsigned long d_val) { - Elf32_Dyn *dynamic = (Elf32_Dyn *) (map + dyn_offset); - const char *strtab = NULL; + unsigned long tab_off; + const char *s, *tab_end; - while ((void *) dynamic < (void *) (map + dyn_offset + dyn_size)) { - Elf32_Dyn *curr = dynamic; + if (!strtab) + return NULL; - dynamic++; - if (curr->d_tag != DT_STRTAB) - continue; + if (strtab < map) + return NULL; + + tab_off = (unsigned long) (strtab - map); + if (tab_off > map_size) + return NULL; + + if (str_size > map_size - tab_off) + str_size = map_size - tab_off; + + if (d_val >= str_size) + return NULL; - strtab = map + (curr->d_un.d_ptr - load_offset); - break; + s = strtab + d_val; + tab_end = strtab + str_size; + if (!memchr(s, '\0', (size_t) (tab_end - s))) + return NULL; + + return s; +} + +/* + * DT_RPATH/DT_RUNPATH are read and pushed onto their scope lists before the + * DT_NEEDED pass below, so this object's own dependencies resolve through + * them too, as a real runtime linker would. + */ +static int elf32_scan_dynamic(const char *map, unsigned long map_size, unsigned long dyn_offset, unsigned long dyn_size, long load_offset, const char *path) +{ + Elf32_Dyn *dyn_arr = (Elf32_Dyn *) (map + dyn_offset); + unsigned long dyn_count, di; + const char *strtab = NULL; + unsigned long str_size = 0; + unsigned long rpath_val = 0, runpath_val = 0; + bool have_rpath = false, have_runpath = false; + const char *rpath, *runpath; + struct list_head *rpath_boundary = NULL; + struct library_path *p, *ptmp; + int ret = 0; + + /* + * A well-formed PT_DYNAMIC segment is a whole array of Elf32_Dyn + * entries (ELF spec); reject anything else outright instead of + * silently truncating, and iterate by validated index rather than + * pointer comparison so a partial trailing entry can never be + * dereferenced past the segment/map bounds. + */ + if (dyn_size % sizeof(Elf32_Dyn) != 0) { + ERROR("PT_DYNAMIC size is not a multiple of Elf32_Dyn in %s\n", path); + return -1; + } + dyn_count = dyn_size / sizeof(Elf32_Dyn); + + for (di = 0; di < dyn_count; di++) { + Elf32_Dyn *curr = &dyn_arr[di]; + unsigned long strtab_off; + + switch (curr->d_tag) { + case DT_STRTAB: + strtab_off = (unsigned long) curr->d_un.d_ptr - load_offset; + if (strtab_off >= map_size) + continue; + strtab = map + strtab_off; + break; + case DT_STRSZ: + str_size = curr->d_un.d_val; + break; + case DT_RPATH: + rpath_val = curr->d_un.d_val; + have_rpath = true; + break; + case DT_RUNPATH: + runpath_val = curr->d_un.d_val; + have_runpath = true; + break; + default: + break; + } } if (!strtab) return -1; - dynamic = (Elf32_Dyn *) (map + dyn_offset); - while ((void *) dynamic < (void *) (map + dyn_offset + dyn_size)) { - Elf32_Dyn *curr = dynamic; + /* DT_RUNPATH tag presence alone suppresses DT_RPATH (ld.so(8)), + * regardless of whether DT_RUNPATH's value itself resolves. */ + rpath = (have_rpath && !have_runpath) ? dyn_str(map, map_size, strtab, str_size, rpath_val) : NULL; + runpath = have_runpath ? dyn_str(map, map_size, strtab, str_size, runpath_val) : NULL; + + LIST_HEAD(saved_runpath); + list_splice_init(&runpath_scope, &saved_runpath); + if (runpath) + add_rpath_dirs(&runpath_scope, runpath, path); + + if (rpath) + rpath_boundary = add_rpath_dirs(&rpath_scope, rpath, path); + + for (di = 0; di < dyn_count; di++) { + Elf32_Dyn *curr = &dyn_arr[di]; + const char *needed; - dynamic++; if (curr->d_tag != DT_NEEDED) continue; - if (add_path_and_deps(&strtab[curr->d_un.d_val], 1, -1, 1)) - return -1; + needed = dyn_str(map, map_size, strtab, str_size, curr->d_un.d_val); + if (!needed) { + ERROR("corrupt DT_NEEDED entry in %s\n", path); + ret = -1; + break; + } + + if (add_path_and_deps(needed, 1, -1, 1)) { + ret = -1; + break; + } } - return 0; + if (rpath) + free_scope_dirs(&rpath_scope, rpath_boundary); + + list_for_each_entry_safe(p, ptmp, &runpath_scope, list) + free(p); + INIT_LIST_HEAD(&runpath_scope); + list_splice_init(&saved_runpath, &runpath_scope); + + return ret; } -static int elf64_scan_dynamic(const char *map, unsigned long dyn_offset, unsigned long dyn_size, long load_offset) +static int elf64_scan_dynamic(const char *map, unsigned long map_size, unsigned long dyn_offset, unsigned long dyn_size, long load_offset, const char *path) { - Elf64_Dyn *dynamic = (Elf64_Dyn *) (map + dyn_offset); + Elf64_Dyn *dyn_arr = (Elf64_Dyn *) (map + dyn_offset); + unsigned long dyn_count, di; const char *strtab = NULL; + unsigned long str_size = 0; + unsigned long rpath_val = 0, runpath_val = 0; + bool have_rpath = false, have_runpath = false; + const char *rpath, *runpath; + struct list_head *rpath_boundary = NULL; + struct library_path *p, *ptmp; + int ret = 0; + + /* + * A well-formed PT_DYNAMIC segment is a whole array of Elf64_Dyn + * entries (ELF spec); reject anything else outright instead of + * silently truncating, and iterate by validated index rather than + * pointer comparison so a partial trailing entry can never be + * dereferenced past the segment/map bounds. + */ + if (dyn_size % sizeof(Elf64_Dyn) != 0) { + ERROR("PT_DYNAMIC size is not a multiple of Elf64_Dyn in %s\n", path); + return -1; + } + dyn_count = dyn_size / sizeof(Elf64_Dyn); - while ((void *) dynamic < (void *) (map + dyn_offset + dyn_size)) { - Elf64_Dyn *curr = dynamic; - - dynamic++; - if (curr->d_tag != DT_STRTAB) - continue; + for (di = 0; di < dyn_count; di++) { + Elf64_Dyn *curr = &dyn_arr[di]; + unsigned long strtab_off; - strtab = map + (curr->d_un.d_ptr - load_offset); - break; + switch (curr->d_tag) { + case DT_STRTAB: + strtab_off = (unsigned long) curr->d_un.d_ptr - load_offset; + if (strtab_off >= map_size) + continue; + strtab = map + strtab_off; + break; + case DT_STRSZ: + str_size = curr->d_un.d_val; + break; + case DT_RPATH: + rpath_val = curr->d_un.d_val; + have_rpath = true; + break; + case DT_RUNPATH: + runpath_val = curr->d_un.d_val; + have_runpath = true; + break; + default: + break; + } } if (!strtab) return -1; - dynamic = (Elf64_Dyn *) (map + dyn_offset); - while ((void *) dynamic < (void *) (map + dyn_offset + dyn_size)) { - Elf64_Dyn *curr = dynamic; + /* DT_RUNPATH tag presence alone suppresses DT_RPATH (ld.so(8)), + * regardless of whether DT_RUNPATH's value itself resolves. */ + rpath = (have_rpath && !have_runpath) ? dyn_str(map, map_size, strtab, str_size, rpath_val) : NULL; + runpath = have_runpath ? dyn_str(map, map_size, strtab, str_size, runpath_val) : NULL; + + LIST_HEAD(saved_runpath); + list_splice_init(&runpath_scope, &saved_runpath); + if (runpath) + add_rpath_dirs(&runpath_scope, runpath, path); + + if (rpath) + rpath_boundary = add_rpath_dirs(&rpath_scope, rpath, path); + + for (di = 0; di < dyn_count; di++) { + Elf64_Dyn *curr = &dyn_arr[di]; + const char *needed; - dynamic++; if (curr->d_tag != DT_NEEDED) continue; - if (add_path_and_deps(&strtab[curr->d_un.d_val], 1, -1, 1)) - return -1; + needed = dyn_str(map, map_size, strtab, str_size, curr->d_un.d_val); + if (!needed) { + ERROR("corrupt DT_NEEDED entry in %s\n", path); + ret = -1; + break; + } + + if (add_path_and_deps(needed, 1, -1, 1)) { + ret = -1; + break; + } } - return 0; + if (rpath) + free_scope_dirs(&rpath_scope, rpath_boundary); + + list_for_each_entry_safe(p, ptmp, &runpath_scope, list) + free(p); + INIT_LIST_HEAD(&runpath_scope); + list_splice_init(&saved_runpath, &runpath_scope); + + return ret; } -int elf_load_deps(const char *path, const char *map) +int elf_load_deps(const char *path, const char *map, unsigned long map_size) { unsigned long dyn_offset, dyn_size; unsigned long load_offset, load_vaddr; unsigned long interp_offset; - if (elf_find_section(map, PT_INTERP, &interp_offset, NULL, NULL) == 0) { - add_path_and_deps(map+interp_offset, 1, -1, 0); + if (elf_find_section(map, map_size, PT_INTERP, &interp_offset, NULL, NULL) == 0) { + if (interp_offset < map_size && + memchr(map + interp_offset, '\0', map_size - interp_offset)) + add_path_and_deps(map+interp_offset, 1, -1, 0); + else + ERROR("corrupt PT_INTERP entry in %s\n", path); } - if (elf_find_section(map, PT_LOAD, &load_offset, NULL, &load_vaddr)) { + if (elf_find_section(map, map_size, PT_LOAD, &load_offset, NULL, &load_vaddr)) { DEBUG("failed to load the .load section from %s\n", path); return 0; } - if (elf_find_section(map, PT_DYNAMIC, &dyn_offset, &dyn_size, NULL)) { + if (elf_find_section(map, map_size, PT_DYNAMIC, &dyn_offset, &dyn_size, NULL)) { DEBUG("failed to load the .dynamic section from %s\n", path); return 0; } + if (dyn_offset >= map_size || dyn_size > map_size - dyn_offset) { + ERROR("PT_DYNAMIC out of bounds in %s\n", path); + return -1; + } + int clazz = map[EI_CLASS]; if (clazz == ELFCLASS32) - return elf32_scan_dynamic(map, dyn_offset, dyn_size, load_vaddr - load_offset); + return elf32_scan_dynamic(map, map_size, dyn_offset, dyn_size, load_vaddr - load_offset, path); else if (clazz == ELFCLASS64) - return elf64_scan_dynamic(map, dyn_offset, dyn_size, load_vaddr - load_offset); + return elf64_scan_dynamic(map, map_size, dyn_offset, dyn_size, load_vaddr - load_offset, path); ERROR("unknown elf format %d\n", clazz); return -1; @@ -324,6 +708,15 @@ void free_library_search(void) list_for_each_entry_safe(p, ptmp, &library_paths, list) free(p); + /* rpath_scope/runpath_scope should already be empty here (every + * scoped push in elf32/64_scan_dynamic has a matching pop); free + * defensively in case that ever stops holding. */ + list_for_each_entry_safe(p, ptmp, &rpath_scope, list) + free(p); + + list_for_each_entry_safe(p, ptmp, &runpath_scope, list) + free(p); + avl_remove_all_elements(&libraries, l, avl, tmp) free(l); } diff --git a/jail/elf.h b/jail/elf.h index 11fd7e0..046b377 100644 --- a/jail/elf.h +++ b/jail/elf.h @@ -30,7 +30,7 @@ struct library_path { extern struct avl_tree libraries; void alloc_library(const char *path, const char *name); -int elf_load_deps(const char *path, const char *map); +int elf_load_deps(const char *path, const char *map, unsigned long map_size); const char* find_lib(const char *file); void init_library_search(void); int lib_open(char **fullpath, const char *file); diff --git a/jail/fs.c b/jail/fs.c index d0ca2b6..1969425 100644 --- a/jail/fs.c +++ b/jail/fs.c @@ -18,6 +18,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,63 @@ #define UJAIL_NOAFILE "/tmp/.ujailnoafile" +/* + * mnt_already_visible() requires a new mount's atime class to match + * the host's existing instance or the kernel refuses it with EPERM + * ("Mount too revealing"). Detect the mountpoint's actual atime class + * instead of hardcoding one, so the new mount can never conflict. + */ +unsigned long detect_atime_flag(const char *mountpoint) +{ + FILE *f; + char *line = NULL; + size_t linelen = 0; + unsigned long ret = MS_RELATIME; /* kernel default if nothing else is known */ + size_t mplen = strlen(mountpoint); + + f = fopen("/proc/self/mountinfo", "r"); + if (!f) + return ret; + + while (getline(&line, &linelen, f) != -1) { + /* mountinfo(5): field 5 is the mountpoint, field 6 its options */ + char *saveptr = NULL; + char *field; + int idx = 0; + char *mp_field = NULL, *opts_field = NULL; + + for (field = strtok_r(line, " \t\n", &saveptr); field; + field = strtok_r(NULL, " \t\n", &saveptr), idx++) { + if (idx == 4) + mp_field = field; + else if (idx == 5) { + opts_field = field; + break; + } + } + + if (!mp_field || !opts_field) + continue; + + if (strlen(mp_field) != mplen || strcmp(mp_field, mountpoint)) + continue; + + /* last matching entry wins: it's the topmost/currently-effective one */ + if (strstr(opts_field, "noatime")) + ret = MS_NOATIME; + else if (strstr(opts_field, "relatime")) + ret = MS_RELATIME; + else + /* strictatime is the absence of noatime/relatime, not a token */ + ret = MS_STRICTATIME; + } + + free(line); + fclose(f); + + return ret; +} + struct mount { struct avl_node avl; const char *source; @@ -51,10 +109,132 @@ struct mount { const char *optstr; int error; bool inner; + int source_fd; }; +/* open_tree()/move_mount()/mount_setattr() have no glibc wrappers yet; + * struct ujail_mount_attr is declared in fs.h */ +int sys_open_tree(int dfd, const char *path, unsigned flags) +{ + return syscall(SYS_open_tree, dfd, path, flags); +} + +static int sys_move_mount(int from_dfd, const char *from_path, int to_dfd, const char *to_path, unsigned flags) +{ + return syscall(SYS_move_mount, from_dfd, from_path, to_dfd, to_path, flags); +} + +int sys_mount_setattr(int dfd, const char *path, unsigned flags, struct ujail_mount_attr *attr, size_t size) +{ + return syscall(SYS_mount_setattr, dfd, path, flags, attr, size); +} + struct avl_tree mounts; +/* same masking as do_mount()'s is_mask branch, applied immediately + * against an absolute path instead of queued through jail_root. + * + * UJAIL_NOAFILE lives under the pre-pivot root, gone by the time this + * runs; JAIL_NOAFILE, a locked bind of procd's read-only noafile + * queued for deferred-userns jails, is used instead. */ +int mask_path_now(const char *path) +{ + struct stat s; + + if (stat(path, &s)) + return 0; /* doesn't exist, nothing to mask */ + + if (S_ISDIR(s.st_mode)) { + if (mount("none", path, "tmpfs", MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_RELATIME, "size=0,mode=000")) + return -1; + } else { + if (mount(JAIL_NOAFILE, path, "bind", MS_BIND, NULL)) + return -1; + if (mount(JAIL_NOAFILE, path, "bind", MS_REMOUNT | MS_BIND | MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_RELATIME, NULL)) + return -1; + } + + DEBUG("masked path %s\n", path); + return 0; +} + +static void mountinfo_unescape(char *s) +{ + char *r = s, *w = s; + + while (*r) { + if (r[0] == '\\' && r[1] >= '0' && r[1] <= '7' && + r[2] >= '0' && r[2] <= '7' && r[3] >= '0' && r[3] <= '7') { + *w++ = (char)(((r[1] - '0') << 6) | ((r[2] - '0') << 3) | (r[3] - '0')); + r += 4; + } else { + *w++ = *r++; + } + } + *w = '\0'; +} + +static unsigned long mountinfo_current_flags(const char *path) +{ + unsigned long flags = MS_RELATIME; + bool found = false; + FILE *f; + char *line = NULL; + size_t linecap = 0; + + f = fopen("/proc/self/mountinfo", "re"); + if (!f) + return flags; + + while (getline(&line, &linecap, f) >= 0) { + char *mp, *opts, *save = NULL, *optsave = NULL; + char *tok; + unsigned long this_flags; + + strtok_r(line, " ", &save); + strtok_r(NULL, " ", &save); + strtok_r(NULL, " ", &save); + strtok_r(NULL, " ", &save); + mp = strtok_r(NULL, " ", &save); + opts = strtok_r(NULL, " ", &save); + if (!mp || !opts) + continue; + mountinfo_unescape(mp); + if (strcmp(mp, path)) + continue; + + this_flags = 0; + for (tok = strtok_r(opts, ",", &optsave); tok; + tok = strtok_r(NULL, ",", &optsave)) { + if (!strcmp(tok, "ro")) + this_flags |= MS_RDONLY; + else if (!strcmp(tok, "nosuid")) + this_flags |= MS_NOSUID; + else if (!strcmp(tok, "nodev")) + this_flags |= MS_NODEV; + else if (!strcmp(tok, "noexec")) + this_flags |= MS_NOEXEC; + else if (!strcmp(tok, "noatime")) + this_flags |= MS_NOATIME; + else if (!strcmp(tok, "relatime")) + this_flags |= MS_RELATIME; + else if (!strcmp(tok, "nodiratime")) + this_flags |= MS_NODIRATIME; + } + + /* last match wins: it's the topmost/effective entry */ + flags = this_flags; + found = true; + } + free(line); + fclose(f); + + if (!found) + return MS_RELATIME; + + return flags; +} + static int do_mount(const char *root, const char *orig_source, const char *target, const char *filesystemtype, unsigned long orig_mountflags, unsigned long propflags, const char *optstr, int error, bool inner) { @@ -86,14 +266,14 @@ static int do_mount(const char *root, const char *orig_source, const char *targe return 0; /* doesn't exists, nothing to mask */ if (S_ISDIR(s.st_mode)) {/* use empty 0-sized tmpfs for directories */ - if (mount("none", new, "tmpfs", MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_NOATIME, "size=0,mode=000")) + if (mount("none", new, "tmpfs", MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_RELATIME, "size=0,mode=000")) return error; } else { /* mount-bind 0-sized file having mode 000 */ if (mount(UJAIL_NOAFILE, new, "bind", MS_BIND, NULL)) return error; - if (mount(UJAIL_NOAFILE, new, "bind", MS_REMOUNT | MS_BIND | MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_NOATIME, NULL)) + if (mount(UJAIL_NOAFILE, new, "bind", MS_REMOUNT | MS_BIND | MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV | MS_RELATIME, NULL)) return error; } @@ -132,6 +312,49 @@ static int do_mount(const char *root, const char *orig_source, const char *targe const char *hack_fstype = ((!filesystemtype || strcmp(filesystemtype, "cgroup"))?filesystemtype:"cgroup2"); if (mount(source?:(is_bind?new:NULL), new, hack_fstype?:"none", mountflags, optstr)) { + int mount_errno = errno; + + if ((mountflags & MS_REMOUNT) && mount_errno == EPERM) { + /* Not a heuristic: re-read the kernel-enforced flags from + * mountinfo and only proceed once the security-relevant + * ones (ro/nosuid/nodev/noexec) are confirmed in effect. */ + unsigned long retry_flags = mountflags | mountinfo_current_flags(new); + + if (retry_flags != mountflags && + !mount(source?:(is_bind?new:NULL), new, hack_fstype?:"none", retry_flags, optstr)) + goto mount_ok; + + unsigned long lockable_flags = MS_RDONLY | MS_NOSUID | MS_NODEV | MS_NOEXEC; + unsigned long wanted = orig_mountflags & lockable_flags; + unsigned long got = mountinfo_current_flags(new) & lockable_flags; + + if ((wanted & ~got) == 0) { + WARNING("remount(%s) to apply flags failed: %s (tolerated - " + "the flags actually in effect already satisfy the " + "request; expected under CLONE_NEWUSER for host-owned " + "bind sources)\n", new, strerror(mount_errno)); + goto mount_ok; + } + + if (error) { + errno = mount_errno; + ERROR("failed to enforce mount restrictions on %s %s: %m " + "(missing flags: %s%s%s%s)\n", source, new, + (wanted & ~got & MS_RDONLY) ? "ro " : "", + (wanted & ~got & MS_NOSUID) ? "nosuid " : "", + (wanted & ~got & MS_NODEV) ? "nodev " : "", + (wanted & ~got & MS_NOEXEC) ? "noexec " : ""); + ret = error; + goto free_source_out; + } + + WARNING("remount(%s) to apply mount restrictions failed and could not " + "be verified in effect; continuing best-effort since " + "this mount was not marked as critical\n", new); + goto mount_ok; + } + + errno = mount_errno; if (error) ERROR("failed to mount %s %s: %m\n", source, new); @@ -139,6 +362,7 @@ static int do_mount(const char *root, const char *orig_source, const char *targe goto free_source_out; } +mount_ok: DEBUG("mount %s%s %s (%s)\n", (mountflags & MS_BIND)?"-B ":"", source, new, (mountflags & MS_RDONLY)?"ro":"rw"); @@ -187,6 +411,7 @@ static int _add_mount(const char *source, const char *target, const char *filesy m->propflags = propflags; m->error = error; m->inner = inner; + m->source_fd = -1; avl_insert(&mounts, &m->avl); DEBUG("adding mount %s %s bind(%d) ro(%d) err(%d)\n", (m->source == (void*)(-1))?"mask":m->source, m->target, @@ -222,6 +447,28 @@ int add_mount_bind(const char *path, int readonly, int error) return _add_mount_bind(path, path, readonly, error); } +int add_mount_fd(int fd, const char *target, int error) +{ + struct mount *m; + + if (avl_find(&mounts, target)) + return 1; + + m = calloc(1, sizeof(struct mount)); + if (!m) + return ENOMEM; + + m->avl.key = m->target = strdup(target); + m->mountflags = MS_BIND; + m->error = error; + m->source_fd = fd; + + avl_insert(&mounts, &m->avl); + DEBUG("adding mount fd:%d %s bind(1) ro(?) err(%d)\n", fd, target, error != 0); + + return 0; +} + enum { OCI_MOUNT_SOURCE, OCI_MOUNT_DESTINATION, @@ -386,6 +633,20 @@ static int parseOCImountopts(struct blob_attr *msg, unsigned long *mount_flags, return 0; } +static bool is_proc_or_sys_path(const char *path) +{ + if (!strcmp(path, "/proc") || !strcmp(path, "/sys")) + return true; + + if (!strncmp(path, "/proc/", 6)) + return true; + + if (!strncmp(path, "/sys/", 5)) + return true; + + return false; +} + int parseOCImount(struct blob_attr *msg) { struct blob_attr *tb[__OCI_MOUNT_MAX]; @@ -405,6 +666,18 @@ int parseOCImount(struct blob_attr *msg) return ret; } + if (is_proc_or_sys_path(blobmsg_get_string(tb[OCI_MOUNT_DESTINATION])) && + ((mount_flags & MS_BIND) || + (tb[OCI_MOUNT_TYPE] && !strcmp(blobmsg_get_string(tb[OCI_MOUNT_TYPE]), "bind"))) && + !(mount_flags & MS_RDONLY)) { + ERROR("OCI mount config requests a writable bind mount onto %s; " + "refusing to allow write access to /proc or /sys\n", + blobmsg_get_string(tb[OCI_MOUNT_DESTINATION])); + if (mount_data) + free(mount_data); + return EPERM; + } + ret = add_mount(tb[OCI_MOUNT_SOURCE] ? blobmsg_get_string(tb[OCI_MOUNT_SOURCE]) : NULL, blobmsg_get_string(tb[OCI_MOUNT_DESTINATION]), tb[OCI_MOUNT_TYPE] ? blobmsg_get_string(tb[OCI_MOUNT_TYPE]) : NULL, @@ -427,6 +700,47 @@ static void build_noafile(void) { return; } +static int do_mount_fd(const char *root, int fd, const char *target, int error) +{ + char new[PATH_MAX]; + struct stat s; + + snprintf(new, sizeof(new), "%s%s", root, target); + + if (fstat(fd, &s)) { + if (error) + ERROR("fstat(fd:%d) failed: %m\n", fd); + close(fd); + return error; + } + + if (S_ISDIR(s.st_mode)) { + mkdir_p(new, 0755); + } else { + mkdir_p(dirname(new), 0755); + snprintf(new, sizeof(new), "%s%s", root, target); + int cfd = open(new, O_CREAT|O_WRONLY|O_TRUNC|O_EXCL, 0644); + if (cfd >= 0) + close(cfd); + if (error && cfd < 0 && errno != EEXIST) { + ERROR("failed to create mount target %s: %m\n", new); + close(fd); + return errno; + } + } + + if (sys_move_mount(fd, "", AT_FDCWD, new, MOVE_MOUNT_F_EMPTY_PATH)) { + if (error) + ERROR("move_mount() to %s failed: %m\n", new); + close(fd); + return error; + } + + close(fd); + DEBUG("move_mount fd to %s\n", new); + return 0; +} + int mount_all(const char *jailroot) { struct library *l; struct mount *m; @@ -436,10 +750,16 @@ int mount_all(const char *jailroot) { avl_for_each_element(&libraries, l, avl) add_mount_bind(l->path, 1, -1); - avl_for_each_element(&mounts, m, avl) + avl_for_each_element(&mounts, m, avl) { + if (m->source_fd >= 0) { + if (do_mount_fd(jailroot, m->source_fd, m->target, m->error)) + return -1; + continue; + } if (do_mount(jailroot, m->source, m->target, m->filesystemtype, m->mountflags, m->propflags, m->optstr, m->error, m->inner)) return -1; + } return 0; } @@ -495,6 +815,7 @@ int add_2paths_and_deps(const char *path, const char *path2, int readonly, int e } char *map = NULL; + char *fullpath = NULL; int fd, ret = -1; if (path[0] == '/') { if (avl_find(&mounts, path2)) @@ -506,14 +827,11 @@ int add_2paths_and_deps(const char *path, const char *path2, int readonly, int e } else { if (avl_find(&libraries, path)) return 0; - char *fullpath; fd = lib_open(&fullpath, path); if (fd < 0) return error; - if (fullpath) { + if (fullpath) alloc_library(fullpath, path); - free(fullpath); - } } struct stat s; @@ -547,7 +865,10 @@ int add_2paths_and_deps(const char *path, const char *path2, int readonly, int e } if (map[0] == ELFMAG0 && map[1] == ELFMAG1 && map[2] == ELFMAG2 && map[3] == ELFMAG3) { - ret = elf_load_deps(path, map); + /* Pass the resolved fullpath, not the bare soname, so + * elf_load_deps() can expand a $ORIGIN-relative DT_RPATH/ + * DT_RUNPATH in this object against its real directory. */ + ret = elf_load_deps(fullpath ? fullpath : path, map, s.st_size); goto out; } @@ -558,6 +879,7 @@ int add_2paths_and_deps(const char *path, const char *path2, int readonly, int e close(fd); if (map) munmap(map, s.st_size); + free(fullpath); return ret; } diff --git a/jail/fs.h b/jail/fs.h index 541030f..9b3ee8b 100644 --- a/jail/fs.h +++ b/jail/fs.h @@ -13,16 +13,50 @@ #ifndef _JAIL_FS_H_ #define _JAIL_FS_H_ +#include #include #include +#include "../container.h" + +#define JAIL_NOAFILE "/dev/.ujailnoafile" + int add_mount(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, unsigned long propflags, const char *optstr, int error); int add_mount_inner(const char *source, const char *target, const char *filesystemtype, unsigned long mountflags, unsigned long propflags, const char *optstr, int error); int add_mount_bind(const char *path, int readonly, int error); +int add_mount_fd(int fd, const char *target, int error); +int mask_path_now(const char *path); + +/* open_tree()/mount_setattr() wrappers - no glibc wrappers yet. + * Fields must match the kernel's struct mount_attr layout exactly + * (__u64, not unsigned long - many OpenWrt targets are 32-bit). */ +#include +struct ujail_mount_attr { + uint64_t attr_set, attr_clr, propagation, userns_fd; +}; +int sys_open_tree(int dfd, const char *path, unsigned flags); +int sys_mount_setattr(int dfd, const char *path, unsigned flags, struct ujail_mount_attr *attr, size_t size); + +#ifndef OPEN_TREE_CLONE +#define OPEN_TREE_CLONE 1 +#endif +#ifndef OPEN_TREE_CLOEXEC +#define OPEN_TREE_CLOEXEC O_CLOEXEC +#endif +#ifndef MOUNT_ATTR_RDONLY +#define MOUNT_ATTR_RDONLY 0x00000001 +#endif +#ifndef MOVE_MOUNT_F_EMPTY_PATH +#define MOVE_MOUNT_F_EMPTY_PATH 0x00000004 +#endif +#ifndef AT_EMPTY_PATH +#define AT_EMPTY_PATH 0x1000 +#endif int parseOCImount(struct blob_attr *msg); int add_2paths_and_deps(const char *path, const char *path2, int readonly, int error, int lib); +unsigned long detect_atime_flag(const char *mountpoint); static inline int add_path_and_deps(const char *path, int readonly, int error, int lib) { diff --git a/jail/jail.c b/jail/jail.c index eaaf017..22419ef 100644 --- a/jail/jail.c +++ b/jail/jail.c @@ -151,6 +151,8 @@ static struct { bool set_oom_score_adj; struct mknod_args **devices; char *ocibundle; + char **oci_deferred_masked; + char **oci_deferred_readonly; bool immediately; struct blob_attr *annotations; int term_timeout; @@ -283,6 +285,19 @@ static void free_opts(bool parent) { free_hooklist(opts.hooks.startContainer); free_hooklist(opts.hooks.poststart); free_hooklist(opts.hooks.poststop); + + if (opts.oci_deferred_masked) { + char **p; + for (p = opts.oci_deferred_masked; *p; p++) + free(*p); + free(opts.oci_deferred_masked); + } + if (opts.oci_deferred_readonly) { + char **p; + for (p = opts.oci_deferred_readonly; *p; p++) + free(*p); + free(opts.oci_deferred_readonly); + } } static int mount_overlay(char *jail_root, char *overlaydir) { @@ -611,6 +626,11 @@ static int create_devices(void) if (strncmp(path, "/dev", 4)) return EPERM; + if (opts.setns.user != -1) { + ++cur; + continue; + } + /* make sure parent folder exists */ tmp = strrchr(path, '/'); if (!tmp) @@ -679,9 +699,173 @@ static int create_devices(void) } static char jail_root[] = "/tmp/ujail-XXXXXX"; +/* Handshake for the deferred CLONE_NEWUSER creation in enter_userns(). */ +int userns_pipe[4]; + +/* single-byte read()/write(), retrying on EINTR */ +static ssize_t xread_byte(int fd, char *buf) +{ + ssize_t n; + + do { + n = read(fd, buf, 1); + } while (n < 0 && errno == EINTR); + + return n; +} + +static ssize_t xwrite_byte(int fd, char byte) +{ + ssize_t n; + + do { + n = write(fd, &byte, 1); + } while (n < 0 && errno == EINTR); + + return n; +} + static char tmpovdir[] = "/tmp/ujail-overlay-XXXXXX"; static mode_t old_umask; static void enter_jail_fs(void); + +static size_t path_depth(const char *path) +{ + size_t depth = 0; + + for (; *path; path++) + if (*path == '/') + depth++; + + return depth; +} + +static void mountinfo_unescape(char *s) +{ + char *r = s, *w = s; + + while (*r) { + if (r[0] == '\\' && r[1] >= '0' && r[1] <= '7' && + r[2] >= '0' && r[2] <= '7' && r[3] >= '0' && r[3] <= '7') { + *w++ = (char)(((r[1] - '0') << 6) | ((r[2] - '0') << 3) | (r[3] - '0')); + r += 4; + } else { + *w++ = *r++; + } + } + *w = '\0'; +} + +static int mountinfo_detach_children(const char *prefix) +{ + size_t prefixlen = strlen(prefix); + int pass; + + for (pass = 0; pass < 16; pass++) { + FILE *f; + char *line = NULL; + size_t linecap = 0; + char *paths[128]; + size_t n = 0, dropped = 0, i, j; + bool progress = false; + + f = fopen("/proc/self/mountinfo", "re"); + if (!f) { + ERROR("mountinfo_detach_children(%s): fopen(/proc/self/mountinfo) " + "failed: %m\n", prefix); + return -1; + } + + while (getline(&line, &linecap, f) >= 0) { + char *mp, *save = NULL; + + strtok_r(line, " ", &save); + strtok_r(NULL, " ", &save); + strtok_r(NULL, " ", &save); + strtok_r(NULL, " ", &save); + mp = strtok_r(NULL, " ", &save); + if (!mp) + continue; + mountinfo_unescape(mp); + + if (strncmp(mp, prefix, prefixlen) || mp[prefixlen] != '/') + continue; + + if (n < ARRAY_SIZE(paths)) { + char *dup = strdup(mp); + + if (dup) + paths[n++] = dup; + else + dropped++; + } else { + dropped++; + } + } + free(line); + fclose(f); + + if (dropped) + WARNING("mountinfo_detach_children(%s): %zu nested mount(s) " + "exceeded the %zu-entry per-pass limit or could not " + "be recorded (out of memory); retrying over further " + "passes\n", prefix, dropped, ARRAY_SIZE(paths)); + + if (n == 0 && dropped == 0) + return 0; + + for (i = 0; i < n; i++) { + size_t deepest = i; + + for (j = i + 1; j < n; j++) + if (path_depth(paths[j]) > path_depth(paths[deepest])) + deepest = j; + if (deepest != i) { + char *tmp = paths[i]; + paths[i] = paths[deepest]; + paths[deepest] = tmp; + } + } + + for (i = 0; i < n; i++) { + if (!umount2(paths[i], MNT_DETACH)) + progress = true; + free(paths[i]); + } + + if (!progress && !dropped) { + ERROR("mountinfo_detach_children(%s): %zu nested mount(s) could " + "not be detached; refusing to continue\n", prefix, n); + return -1; + } + } + + ERROR("mountinfo_detach_children(%s): nested mounts remained attached " + "after %d passes; refusing to continue\n", prefix, pass); + return -1; +} + +/* + * Make the mount namespace private and detach inherited /proc,/sys + * children before build_jail_fs() mounts its own. Must run before + * setns_open(CLONE_NEWUSER) joins an external userns and drops + * privilege; see the call site in exec_jail(). + */ +static int isolate_mountns_and_detach_inherited(void) +{ + if (mount("none", "/", "none", MS_REC|MS_PRIVATE, NULL)) { + ERROR("private mount failed %m\n"); + return -1; + } + + if ((opts.procfs || opts.ocibundle) && mountinfo_detach_children("/proc")) + return -1; + if ((opts.sysfs || opts.ocibundle) && mountinfo_detach_children("/sys")) + return -1; + + return 0; +} + static int build_jail_fs(void) { char *overlaydir = NULL; @@ -699,12 +883,6 @@ static int build_jail_fs(void) return -1; } - /* oldroot can't be MS_SHARED else pivot_root() fails */ - if (mount("none", "/", "none", MS_REC|MS_PRIVATE, NULL)) { - ERROR("private mount failed %m\n"); - return -1; - } - if (opts.extroot) { if (mount(opts.extroot, jail_root, "bind", MS_BIND, NULL)) { ERROR("extroot mount failed %m\n"); @@ -748,8 +926,172 @@ static int build_jail_fs(void) return -1; } - if (mount_all(jail_root)) { + { + /* fds stay open until mount_all() performs the /proc/self/fd/N + * binds below; closing early would drop or swap the source */ + int *held_fds = NULL; + size_t n_devices = 0, n_custom = 0, i; + int fail = 0; + + if (opts.setns.user != -1) { + struct mknod_args *curdef; + + if (opts.devices) { + struct mknod_args **cur; + + for (cur = opts.devices; *cur; cur++) + n_custom++; + } + + for (curdef = default_devices; curdef->path; curdef++) + n_devices++; + + n_devices += n_custom; + + held_fds = malloc(n_devices * sizeof(int)); + if (!held_fds) { + ERROR("out of memory validating devices\n"); + return -1; + } + for (i = 0; i < n_devices; i++) + held_fds[i] = -1; + + if (opts.devices) { + struct mknod_args **cur; + + for (i = 0, cur = opts.devices; *cur && !fail; cur++, i++) { + struct stat st; + + if (strncmp((*cur)->path, "/dev", 4)) { + ERROR("custom device %s is outside of /dev; " + "refusing to bind-mount it\n", + (*cur)->path); + fail = 1; + break; + } + + held_fds[i] = open((*cur)->path, O_PATH | O_CLOEXEC); + if (held_fds[i] < 0) { + ERROR("custom device %s requested but not found " + "on the host; it cannot be created under " + "CLONE_NEWUSER (no privilege to mknod)\n", + (*cur)->path); + fail = 1; + break; + } + + if (fstat(held_fds[i], &st)) { + ERROR("custom device %s: fstat() failed: %m\n", + (*cur)->path); + fail = 1; + break; + } + + if (((*cur)->mode & S_IFMT) && + (st.st_mode & S_IFMT) != ((*cur)->mode & S_IFMT)) { + ERROR("custom device %s exists on the host but " + "is not the requested node type; its " + "major:minor/mode/owner cannot be enforced " + "under CLONE_NEWUSER\n", + (*cur)->path); + fail = 1; + break; + } + + if (((*cur)->mode & S_IFMT) == S_IFCHR || + ((*cur)->mode & S_IFMT) == S_IFBLK) { + if ((*cur)->dev && st.st_rdev != (*cur)->dev) { + ERROR("custom device %s exists on the host " + "but its major:minor (%u:%u) does not " + "match the requested %u:%u; refusing " + "to bind-mount a different device than " + "configured\n", (*cur)->path, + major(st.st_rdev), minor(st.st_rdev), + major((*cur)->dev), minor((*cur)->dev)); + fail = 1; + break; + } + } + + { + int tree; + struct ujail_mount_attr attr = { .attr_set = MOUNT_ATTR_RDONLY }; + + tree = sys_open_tree(held_fds[i], "", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_EMPTY_PATH); + if (tree < 0) { + ERROR("open_tree() on custom device %s failed: %m\n", (*cur)->path); + fail = 1; + break; + } + close(held_fds[i]); + held_fds[i] = tree; + + if (sys_mount_setattr(tree, "", AT_EMPTY_PATH, &attr, sizeof(attr))) { + ERROR("mount_setattr() on custom device %s failed: %m\n", (*cur)->path); + fail = 1; + break; + } + } + if (add_mount_fd(held_fds[i], (*cur)->path, -1)) { + ERROR("could not queue bind-mount for mandatory " + "custom device %s; refusing to start with " + "a requested device missing\n", + (*cur)->path); + fail = 1; + break; + } + } + } + + if (!fail) { + size_t j = 0; + + for (curdef = default_devices; curdef->path; curdef++, j++) { + int tree; + struct ujail_mount_attr attr = { .attr_set = MOUNT_ATTR_RDONLY }; + + held_fds[n_custom + j] = open(curdef->path, O_PATH | O_CLOEXEC); + if (held_fds[n_custom + j] < 0) { + WARNING("could not open default device %s; " + "it will be unavailable in the jail\n", + curdef->path); + continue; + } + + tree = sys_open_tree(held_fds[n_custom + j], "", OPEN_TREE_CLONE | OPEN_TREE_CLOEXEC | AT_EMPTY_PATH); + if (tree < 0) { + WARNING("open_tree() on default device %s failed; " + "it will be unavailable in the jail\n", curdef->path); + continue; + } + close(held_fds[n_custom + j]); + held_fds[n_custom + j] = tree; + + if (sys_mount_setattr(tree, "", AT_EMPTY_PATH, &attr, sizeof(attr))) { + WARNING("mount_setattr() on default device %s failed; " + "it will be unavailable in the jail\n", curdef->path); + continue; + } + + if (add_mount_fd(held_fds[n_custom + j], curdef->path, 0)) + WARNING("could not queue bind-mount for default " + "device %s; it will be unavailable in " + "the jail\n", curdef->path); + } + } + } + + if (!fail && mount_all(jail_root)) { ERROR("mount_all() failed\n"); + fail = 1; + } + + for (i = 0; i < n_devices; i++) + if (held_fds && held_fds[i] >= 0) + close(held_fds[i]); + free(held_fds); + + if (fail) return -1; } @@ -794,6 +1136,9 @@ static void free_and_exit(int ret) } static void post_jail_fs(void); +static void enter_userns(void); +static void remask_after_unshare(void); +static void remount_proc_sys_after_unshare(void); static void enter_jail_fs(void) { char dirbuf[sizeof(jail_root) + 4]; @@ -833,6 +1178,67 @@ static void enter_jail_fs(void) mount(NULL, "/", "bind", MS_REMOUNT | MS_BIND | MS_RDONLY, 0); umask(old_umask); + enter_userns(); +} + +/* + * Create our own CLONE_NEWUSER here, after /proc and /sys are already + * mounted, so the PID namespace stays owned by the initial userns + * throughout mount setup. See the comment in exec_jail() for why. + */ +static void enter_userns(void) +{ + char buf[1]; + + if (!((opts.namespace & CLONE_NEWUSER) && opts.setns.user == -1)) { + post_jail_fs(); + return; + } + + if (unshare(CLONE_NEWUSER)) { + ERROR("unshare(CLONE_NEWUSER) failed: %m\n"); + free_and_exit(-1); + } + + buf[0] = 'i'; + if (xwrite_byte(userns_pipe[1], buf[0]) < 1) { + ERROR("can't write to parent\n"); + free_and_exit(-1); + } + close(userns_pipe[1]); + + if (xread_byte(userns_pipe[2], buf) < 1) { + ERROR("can't read from parent\n"); + free_and_exit(-1); + } + close(userns_pipe[2]); + if (buf[0] != 'O') { + ERROR("parent had an error, child exiting\n"); + free_and_exit(-1); + } + + if ((opts.namespace & CLONE_NEWNS) && unshare(CLONE_NEWNS)) { + ERROR("unshare(CLONE_NEWNS) failed: %m\n"); + free_and_exit(-1); + } + if (opts.namespace & CLONE_NEWNS) { + remask_after_unshare(); + remount_proc_sys_after_unshare(); + } + + if (setregid(0, 0) < 0) { + ERROR("setgid\n"); + free_and_exit(-1); + } + if (setreuid(0, 0) < 0) { + ERROR("setuid\n"); + free_and_exit(-1); + } + if (setgroups(0, NULL) < 0) { + ERROR("setgroups\n"); + free_and_exit(-1); + } + post_jail_fs(); } @@ -845,10 +1251,10 @@ static int write_uid_gid_map(pid_t child_pid, bool gidmap, char *mapstr) child_pid, gidmap?"gid_map":"uid_map") < 0) return -1; - if ((map_file = open(map_path, O_WRONLY)) < 0) + if ((map_file = open(map_path, O_WRONLY | O_CLOEXEC)) < 0) return -1; - if (dprintf(map_file, "%s", mapstr)) { + if (dprintf(map_file, "%s", mapstr) < 0) { close(map_file); return -1; } @@ -866,7 +1272,7 @@ static int write_single_uid_gid_map(pid_t child_pid, bool gidmap, int id) child_pid, gidmap?"gid_map":"uid_map") < 0) return -1; - if ((map_file = open(map_path, O_WRONLY)) < 0) + if ((map_file = open(map_path, O_WRONLY | O_CLOEXEC)) < 0) return -1; if (dprintf(map_file, map_format, 0, id, 1) < 0) { @@ -888,7 +1294,7 @@ static int write_setgroups(pid_t child_pid, bool allow) return -1; } - if ((setgroups_file = open(setgroups_path, O_WRONLY)) < 0) { + if ((setgroups_file = open(setgroups_path, O_WRONLY | O_CLOEXEC)) < 0) { return -1; } @@ -903,30 +1309,85 @@ static int write_setgroups(pid_t child_pid, bool allow) static void get_jail_user(int *user, int *user_gid, int *gr_gid) { - struct passwd *p = NULL; - struct group *g = NULL; + struct passwd pwd, *p = NULL; + struct group grp, *g = NULL; + char *buf = NULL; + size_t bufsize; + long sc; + int ret = 0, attempt; if (opts.user) { - p = getpwnam(opts.user); - if (!p) { - ERROR("failed to get uid/gid for user %s: %d (%s)\n", - opts.user, errno, strerror(errno)); + sc = sysconf(_SC_GETPW_R_SIZE_MAX); + bufsize = (sc > 0) ? (size_t)sc : 16384; + + for (attempt = 0; attempt < 8; attempt++) { + buf = malloc(bufsize); + if (!buf) { + ERROR("out of memory resolving user %s\n", opts.user); + free_and_exit(EXIT_FAILURE); + } + + ret = getpwnam_r(opts.user, &pwd, buf, bufsize, &p); + if (ret != ERANGE) + break; + + free(buf); + buf = NULL; + bufsize *= 2; + } + + if (ret || !p) { + free(buf); + if (!ret) + ERROR("failed to get uid/gid for user %s: " + "no such user\n", opts.user); + else + ERROR("failed to get uid/gid for user %s: %d (%s)\n", + opts.user, ret, strerror(ret)); free_and_exit(EXIT_FAILURE); } + *user = p->pw_uid; *user_gid = p->pw_gid; + free(buf); } else { *user = -1; *user_gid = -1; } if (opts.group) { - g = getgrnam(opts.group); - if (!g) { - ERROR("failed to get gid for group %s: %m\n", opts.group); + sc = sysconf(_SC_GETGR_R_SIZE_MAX); + bufsize = (sc > 0) ? (size_t)sc : 16384; + + for (attempt = 0; attempt < 8; attempt++) { + buf = malloc(bufsize); + if (!buf) { + ERROR("out of memory resolving group %s\n", opts.group); + free_and_exit(EXIT_FAILURE); + } + + ret = getgrnam_r(opts.group, &grp, buf, bufsize, &g); + if (ret != ERANGE) + break; + + free(buf); + buf = NULL; + bufsize *= 2; + } + + if (ret || !g) { + free(buf); + if (!ret) + ERROR("failed to get gid for group %s: " + "no such group\n", opts.group); + else + ERROR("failed to get gid for group %s: %d (%s)\n", + opts.group, ret, strerror(ret)); free_and_exit(EXIT_FAILURE); } + *gr_gid = g->gr_gid; + free(buf); } else { *gr_gid = -1; } @@ -950,6 +1411,229 @@ static void set_jail_user(int pw_uid, int user_gid, int gr_gid) } } +static const char *proc_mask_critical[] = { + "/proc/kcore", + "/proc/sysrq-trigger", + NULL, +}; + +static const char *proc_mask_optional[] = { + "/proc/latency_stats", + "/proc/timer_list", + "/proc/timer_stats", + "/proc/sched_debug", + "/proc/scsi", + NULL, +}; + +static const char *sys_mask_critical[] = { + "/sys/firmware", + NULL, +}; + +static int mask_default_paths(const char **paths, bool critical) +{ + const char **p; + + for (p = paths; *p; p++) { + if (add_mount((void *)(-1), *p, NULL, 0, 0, NULL, critical ? -1 : 0)) { + if (critical) { + ERROR("failed to mask sensitive path %s\n", *p); + return -1; + } + WARNING("could not mask optional path %s; it may not " + "exist on this kernel/config\n", *p); + } + } + + return 0; +} + +/* deferred like mask_path_now(): locking a readonlyPath in phase 1 + * would stay locked past the jail's own unshare(CLONE_NEWNS). */ +static int remount_readonly_now(const char *path) +{ + struct stat s; + + if (stat(path, &s)) + return 0; /* doesn't exist, nothing to restrict */ + + if (mount(path, path, "bind", MS_BIND | MS_REC, NULL)) + return -1; + if (mount(path, path, "bind", MS_REMOUNT | MS_BIND | MS_RDONLY | MS_REC, NULL)) + return -1; + + DEBUG("read-only path %s\n", path); + return 0; +} + +/* re-apply masks inside the container's own mntns: the first pass is + * locked once copied in after unshare(CLONE_NEWNS); best-effort */ +static void remask_after_unshare(void) +{ + const char **p; + char **dp; + + if (!opts.ocibundle) { + if (opts.procfs) { + for (p = proc_mask_critical; *p; p++) + mask_path_now(*p); + for (p = proc_mask_optional; *p; p++) + mask_path_now(*p); + } + + if (opts.sysfs) { + for (p = sys_mask_critical; *p; p++) + mask_path_now(*p); + } + } + + /* same reason as the default masks above: locked in phase 1, an + * OCI bundle's own masks would outlive its unshare(CLONE_NEWNS). */ + if (opts.oci_deferred_masked) + for (dp = opts.oci_deferred_masked; *dp; dp++) + mask_path_now(*dp); + + if (opts.oci_deferred_readonly) + for (dp = opts.oci_deferred_readonly; *dp; dp++) + remount_readonly_now(*dp); +} + +/* /proc/sys is locked read-only for every procfs jail via a self-bind, + * separately from the mask_default_paths() lists above; a deferred- + * userns jail skips it in phase 1 for the same reason it skips the + * masks (see the call site in post_main()), so re-apply it here. */ +static void remount_proc_sys_after_unshare(void) +{ + struct stat s; + + if (!(opts.procfs || opts.ocibundle)) + return; + if (stat("/proc/sys", &s)) + return; + + /* the MS_MOVE below needs a mountpoint to move from, or it's a + * silent EINVAL. */ + if (opts.namespace & CLONE_NEWNET) + mount("/proc/sys/net", "/proc/self/net", "bind", MS_BIND, NULL); + + if (mount("/proc/sys", "/proc/sys", "bind", MS_BIND, NULL)) + return; + if (mount("/proc/sys", "/proc/sys", "bind", MS_REMOUNT | MS_BIND | MS_RDONLY, NULL)) + WARNING("could not remount /proc/sys read-only\n"); + + if (opts.namespace & CLONE_NEWNET) + mount("/proc/self/net", "/proc/sys/net", "bind", MS_MOVE, NULL); +} + +static bool resolve_jail_user_gids(int primary_gid) +{ + gid_t *groups = NULL; + int ngroups = 16; + int ret = -1; + int attempt, i, n; + + if (!opts.user || primary_gid == -1) + return false; + + for (attempt = 0; attempt < 16; attempt++) { + gid_t *tmp = realloc(groups, ngroups * sizeof(gid_t)); + if (!tmp) { + free(groups); + ERROR("out of memory resolving groups for %s\n", opts.user); + return false; + } + groups = tmp; + + ret = getgrouplist(opts.user, primary_gid, groups, &ngroups); + if (ret >= 0) + break; + } + + if (ret < 0) { + free(groups); + ERROR("could not resolve groups for %s\n", opts.user); + return false; + } + + for (n = 0, i = 0; i < ret; i++) { + if ((int)groups[i] == primary_gid) + continue; + groups[n++] = groups[i]; + } + + opts.additional_gids = groups; + opts.num_additional_gids = n; + + return true; +} + +/* deterministic: parent and child (across fork) compute the same ids */ +static int *compute_inner_gids(int primary_gid) +{ + size_t i; + int *inner_id; + int next_id = 1; + + inner_id = calloc(opts.num_additional_gids ?: 1, sizeof(int)); + if (!inner_id) + return NULL; + + for (i = 0; i < opts.num_additional_gids; i++) { + int gid = (int)opts.additional_gids[i]; + int candidate = gid; + bool used; + + if (gid == 0) { + do { + used = (candidate == 0 || candidate == primary_gid); + for (size_t j = 0; !used && j < opts.num_additional_gids; j++) + if ((int)opts.additional_gids[j] == candidate) + used = true; + for (size_t j = 0; !used && j < i; j++) + if (inner_id[j] == candidate) + used = true; + if (used) + candidate = next_id++; + } while (used); + } + + inner_id[i] = candidate; + } + + return inner_id; +} + +static char *build_group_gidmap(int primary_gid) +{ + size_t i, len = 0, pos = 0; + char *map; + int *inner_id; + + inner_id = compute_inner_gids(primary_gid); + if (!inner_id) + return NULL; + + len += snprintf(NULL, 0, "%d %d %d\n", 0, primary_gid, 1); + for (i = 0; i < opts.num_additional_gids; i++) + len += snprintf(NULL, 0, "%d %d %d\n", + inner_id[i], opts.additional_gids[i], 1); + + map = malloc(len + 1); + if (!map) { + free(inner_id); + return NULL; + } + + pos += snprintf(&map[pos], len + 1 - pos, "%d %d %d\n", 0, primary_gid, 1); + for (i = 0; i < opts.num_additional_gids; i++) + pos += snprintf(&map[pos], len + 1 - pos, "%d %d %d\n", + inner_id[i], opts.additional_gids[i], 1); + + free(inner_id); + return map; +} + static int apply_rlimits(void) { int resource; @@ -1183,12 +1867,38 @@ static int exec_jail(void *arg) close(pipes[0]); close(pipes[3]); - setns_open(CLONE_NEWUSER); + if ((opts.namespace & CLONE_NEWUSER) && opts.setns.user == -1) { + /* CLONE_NEWUSER is deferred to enter_userns(); keep our + * ends of the handshake open, close only the parent's. */ + close(userns_pipe[0]); + close(userns_pipe[3]); + } else { + /* Not deferring anything: this handshake isn't used at all. */ + close(userns_pipe[0]); + close(userns_pipe[1]); + close(userns_pipe[2]); + close(userns_pipe[3]); + } + setns_open(CLONE_NEWNET); setns_open(CLONE_NEWNS); setns_open(CLONE_NEWIPC); setns_open(CLONE_NEWUTS); + /* + * Must run before setns_open(CLONE_NEWUSER) below: joining an + * external userns drops privilege immediately, and our own userns + * is deferred to enter_userns(), so this always runs privileged. + */ + if ((opts.namespace & CLONE_NEWNS) && + ((opts.namespace & CLONE_NEWUSER) || opts.setns.user != -1) && + isolate_mountns_and_detach_inherited()) { + ERROR("failed to detach inherited mounts\n"); + return EXIT_FAILURE; + } + + setns_open(CLONE_NEWUSER); + buf[0] = 'i'; if (write(pipes[1], buf, 1) < 1) { ERROR("can't write to parent\n"); @@ -1204,12 +1914,24 @@ static int exec_jail(void *arg) return EXIT_FAILURE; } + if (opts.setns.user != -1 && (opts.namespace & CLONE_NEWNS) && + unshare(CLONE_NEWNS)) { + ERROR("unshare(CLONE_NEWNS) failed: %m\n"); + return EXIT_FAILURE; + } + if (opts.namespace & CLONE_NEWCGROUP) unshare(CLONE_NEWCGROUP); setns_open(CLONE_NEWCGROUP); - if ((opts.namespace & CLONE_NEWUSER) || (opts.setns.user != -1)) { + /* + * A join of an existing userns (opts.setns.user) can become root + * right away. Our own CLONE_NEWUSER is not created here: doing so + * before /proc,/sys are mounted ties the PID namespace to it, + * which fails mnt_already_visible() on hosts with locked /proc. + */ + if (opts.setns.user != -1) { if (setregid(0, 0) < 0) { ERROR("setgid\n"); free_and_exit(EXIT_FAILURE); @@ -1219,8 +1941,14 @@ static int exec_jail(void *arg) free_and_exit(EXIT_FAILURE); } if (setgroups(0, NULL) < 0) { - ERROR("setgroups\n"); - free_and_exit(EXIT_FAILURE); + if (errno != EPERM) { + ERROR("setgroups\n"); + free_and_exit(EXIT_FAILURE); + } + WARNING("setgroups(0, NULL) denied by the joined " + "userns (setgroups=deny is permanent once a " + "gid_map is written); continuing without " + "dropping supplementary groups\n"); } } @@ -1242,8 +1970,12 @@ static void pre_exec_jail(struct uloop_timeout *t) if ((opts.namespace & CLONE_NEWNS) && build_jail_fs()) { ERROR("failed to build jail fs\n"); free_and_exit(EXIT_FAILURE); - } else { - run_hooks(opts.hooks.createContainer, post_jail_fs); + } else if (!(opts.namespace & CLONE_NEWNS)) { + /* + * No mount namespace to build (plain "-f"): build_jail_fs() + * is skipped, so reach enter_userns() directly here instead. + */ + run_hooks(opts.hooks.createContainer, enter_userns); } } @@ -1285,13 +2017,53 @@ static void post_start_hook(void) free_and_exit(EXIT_FAILURE); /* use either cmdline-supplied user/group or uid/gid from OCI spec */ - get_jail_user(&pw_uid, &pw_gid, &gr_gid); - set_jail_user(opts.pw_uid?:pw_uid, opts.pw_gid?:pw_gid, opts.gr_gid?:gr_gid); + if (opts.ocibundle || opts.uidmap || !(opts.namespace & CLONE_NEWUSER)) { + get_jail_user(&pw_uid, &pw_gid, &gr_gid); + set_jail_user(opts.pw_uid?:pw_uid, opts.pw_gid?:pw_gid, opts.gr_gid?:gr_gid); + } else if (opts.user || opts.group || opts.pw_uid != -1 || opts.pw_gid != -1 || opts.gr_gid != -1) { + WARNING("user/group identity switch (-U/-G) is not re-applied under " + "CLONE_NEWUSER without an OCI bundle or explicit uidmap; the " + "process already has the correct identity via the uid_map\n"); + } - if (opts.additional_gids && - (setgroups(opts.num_additional_gids, opts.additional_gids) < 0)) { - ERROR("setgroups failed: %m\n"); - free_and_exit(EXIT_FAILURE); + if (opts.additional_gids) { + bool default_own_userns_map = !opts.uidmap && + (opts.namespace & CLONE_NEWUSER) && opts.setns.user == -1; + + if (default_own_userns_map) { + bool has_gr = (opts.gr_gid != -1); + int primary_gid = has_gr ? opts.gr_gid : + ((opts.pw_uid != -1) ? opts.pw_gid : 65534); + int *inner_id = compute_inner_gids(primary_gid); + + if (inner_id) { + gid_t *mapped = calloc(opts.num_additional_gids ?: 1, sizeof(gid_t)); + size_t i; + + if (mapped) { + for (i = 0; i < opts.num_additional_gids; i++) + mapped[i] = (gid_t)inner_id[i]; + if (setgroups(opts.num_additional_gids, mapped) < 0) { + ERROR("setgroups failed: %m\n"); + free(mapped); + free(inner_id); + free_and_exit(EXIT_FAILURE); + } + free(mapped); + } else { + ERROR("out of memory computing setgroups() list\n"); + free(inner_id); + free_and_exit(EXIT_FAILURE); + } + free(inner_id); + } else { + ERROR("out of memory computing setgroups() list\n"); + free_and_exit(EXIT_FAILURE); + } + } else if (setgroups(opts.num_additional_gids, opts.additional_gids) < 0) { + ERROR("setgroups failed: %m\n"); + free_and_exit(EXIT_FAILURE); + } } if (opts.set_umask) @@ -2207,6 +2979,30 @@ static const struct blobmsg_policy oci_linux_policy[] = { [OCI_LINUX_ROOTFSPROPAGATION] = { "rootfsPropagation", BLOBMSG_TYPE_STRING }, }; +static int append_deferred_path(char ***list, const char *path) +{ + size_t n = 0; + char **newlist; + + if (*list) + while ((*list)[n]) + n++; + + newlist = realloc(*list, (n + 2) * sizeof(char *)); + if (!newlist) + return ENOMEM; + + newlist[n] = strdup(path); + if (!newlist[n]) { + *list = newlist; + return ENOMEM; + } + newlist[n + 1] = NULL; + *list = newlist; + + return 0; +} + static int parseOCIlinux(struct blob_attr *msg) { struct blob_attr *tb[__OCI_LINUX_MAX]; @@ -2238,19 +3034,35 @@ static int parseOCIlinux(struct blob_attr *msg) return res; } - if (tb[OCI_LINUX_READONLYPATHS]) { - blobmsg_for_each_attr(cur, tb[OCI_LINUX_READONLYPATHS], rem) { - res = add_mount(NULL, blobmsg_get_string(cur), NULL, MS_BIND | MS_REC | MS_RDONLY, 0, NULL, 0); - if (res) - return res; + { + bool defer_userns = (opts.namespace & CLONE_NEWUSER) && opts.setns.user == -1; + + if (tb[OCI_LINUX_READONLYPATHS]) { + blobmsg_for_each_attr(cur, tb[OCI_LINUX_READONLYPATHS], rem) { + if (defer_userns) { + res = append_deferred_path(&opts.oci_deferred_readonly, blobmsg_get_string(cur)); + if (res) + return res; + continue; + } + res = add_mount(NULL, blobmsg_get_string(cur), NULL, MS_BIND | MS_REC | MS_RDONLY, 0, NULL, 0); + if (res) + return res; + } } - } - if (tb[OCI_LINUX_MASKEDPATHS]) { - blobmsg_for_each_attr(cur, tb[OCI_LINUX_MASKEDPATHS], rem) { - res = add_mount((void *)(-1), blobmsg_get_string(cur), NULL, 0, 0, NULL, 0); - if (res) - return res; + if (tb[OCI_LINUX_MASKEDPATHS]) { + blobmsg_for_each_attr(cur, tb[OCI_LINUX_MASKEDPATHS], rem) { + if (defer_userns) { + res = append_deferred_path(&opts.oci_deferred_masked, blobmsg_get_string(cur)); + if (res) + return res; + continue; + } + res = add_mount((void *)(-1), blobmsg_get_string(cur), NULL, 0, 0, NULL, 0); + if (res) + return res; + } } } @@ -2830,6 +3642,12 @@ int main(int argc, char **argv) } + if (!opts.extroot && (opts.user || opts.group)) { + int precheck_uid, precheck_gid, precheck_grgid; + + get_jail_user(&precheck_uid, &precheck_gid, &precheck_grgid); + } + if (opts.tmpoverlaysize && strlen(opts.tmpoverlaysize) > 8) { ERROR("size parameter too long: \"%s\"\n", opts.tmpoverlaysize); ret=-1; @@ -2910,8 +3728,18 @@ int main(int argc, char **argv) for (size_t s = optind; s < argc; s++) opts.jail_argv[s - optind] = strdup(argv[s]); - if (opts.namespace & CLONE_NEWUSER) + if (opts.namespace & CLONE_NEWUSER) { get_jail_user(&opts.pw_uid, &opts.pw_gid, &opts.gr_gid); + + if (!opts.uidmap && opts.user) { + int primary_gid = (opts.gr_gid != -1) ? opts.gr_gid : opts.pw_gid; + + if (!resolve_jail_user_gids(primary_gid)) + WARNING("could not resolve supplementary groups for " + "user %s; jail will start with no supplementary " + "groups\n", opts.user); + } + } } if (!opts.extroot) { @@ -2956,6 +3784,9 @@ static void post_main(struct uloop_timeout *t) if (pipe(&pipes[0]) < 0 || pipe(&pipes[2]) < 0) free_and_exit(-1); + if (pipe2(&userns_pipe[0], O_CLOEXEC) < 0 || pipe2(&userns_pipe[2], O_CLOEXEC) < 0) + free_and_exit(-1); + if (has_namespaces()) { if (opts.namespace & CLONE_NEWNS) { if (!opts.extroot && (opts.user || opts.group)) { @@ -2986,10 +3817,23 @@ static void post_main(struct uloop_timeout *t) /* default mounts */ add_mount(NULL, "/dev", "tmpfs", MS_NOATIME | MS_NOEXEC | MS_NOSUID, 0, "size=1M", -1); add_mount("shm", "/dev/shm", "tmpfs", MS_NOSUID | MS_NOEXEC | MS_NODEV, 0, "mode=1777", -1); - add_mount(NULL, "/dev/pts", "devpts", MS_NOATIME | MS_NOEXEC | MS_NOSUID, 0, "newinstance,ptmxmode=0666,mode=0620,gid=5", 0); + { + const char *ptsopts = (opts.namespace & CLONE_NEWUSER) ? + "newinstance,ptmxmode=0666,mode=0620,gid=0" : + "newinstance,ptmxmode=0666,mode=0620,gid=5"; + + add_mount(NULL, "/dev/pts", "devpts", MS_NOATIME | MS_NOEXEC | MS_NOSUID, 0, ptsopts, 0); + } + + bool defer_userns = (opts.namespace & CLONE_NEWUSER) && opts.setns.user == -1; + + if (defer_userns) + add_mount(PROCD_NOAFILE, JAIL_NOAFILE, NULL, + MS_BIND | MS_RDONLY | MS_NOSUID | MS_NOEXEC | MS_NODEV, 0, NULL, -1); if (opts.procfs || opts.ocibundle) { - add_mount("proc", "/proc", "proc", MS_NOATIME | MS_NODEV | MS_NOEXEC | MS_NOSUID, 0, NULL, -1); + add_mount("proc", "/proc", "proc", + detect_atime_flag("/proc") | MS_NODEV | MS_NOEXEC | MS_NOSUID, 0, NULL, -1); /* * hack to make /proc/sys/net read-write while the rest of /proc/sys is read-only @@ -3006,15 +3850,34 @@ static void post_main(struct uloop_timeout *t) * succeeds as the bind-mount of /proc/self/net is performed first and then * move-mount of /proc/sys/net follows because 'e' preceeds 'y' in the ASCII * table (and in the alphabet). + * + * A jail that defers its own CLONE_NEWUSER applies this (and all other + * default masking below) itself in phase 2, once it regains its own + * mount namespace: a mount locked here, while still privileged, can + * never be replaced by that same, now less-privileged phase, since a + * locked mount stays present underneath anything mounted over it and + * still counts against the kernel's mount-visibility check for any + * nested runtime's own /proc mount. */ - if (!add_mount(NULL, "/proc/sys", NULL, MS_BIND | MS_RDONLY, 0, NULL, -1)) + if (!defer_userns && + !add_mount(NULL, "/proc/sys", NULL, MS_BIND | MS_RDONLY, 0, NULL, -1)) if (opts.namespace & CLONE_NEWNET) if (!add_mount_inner("/proc/self/net", "/proc/sys/net", NULL, MS_MOVE, 0, NULL, -1)) add_mount_inner("/proc/sys/net", "/proc/self/net", NULL, MS_BIND, 0, NULL, -1); } if (opts.sysfs || opts.ocibundle) - add_mount("sysfs", "/sys", "sysfs", MS_RELATIME | MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RDONLY, 0, NULL, -1); + add_mount("sysfs", "/sys", "sysfs", + detect_atime_flag("/sys") | MS_NODEV | MS_NOEXEC | MS_NOSUID | MS_RDONLY, 0, NULL, -1); + + if (!opts.ocibundle && !defer_userns) { + if (opts.procfs && + (mask_default_paths(proc_mask_critical, true) || + mask_default_paths(proc_mask_optional, false))) + free_and_exit(-1); + if (opts.sysfs && mask_default_paths(sys_mask_critical, true)) + free_and_exit(-1); + } } @@ -3052,7 +3915,11 @@ static void post_main(struct uloop_timeout *t) } } - jail_process.pid = clone(exec_jail, child_stack + STACK_SIZE, SIGCHLD | (opts.namespace & (~CLONE_NEWCGROUP)), NULL); + /* + * CLONE_NEWUSER is excluded here; the child creates its own + * later, in enter_userns(). See exec_jail() for why. + */ + jail_process.pid = clone(exec_jail, child_stack + STACK_SIZE, SIGCHLD | (opts.namespace & (~(CLONE_NEWCGROUP | CLONE_NEWUSER))), NULL); } else { jail_process.pid = fork(); } @@ -3094,6 +3961,8 @@ static void post_main(struct uloop_timeout *t) close(opts.setns.cgroup); close(pipes[1]); close(pipes[2]); + close(userns_pipe[1]); + close(userns_pipe[2]); if (read(pipes[0], sig_buf, 1) < 1) { ERROR("can't read from child\n"); free_and_exit(-1); @@ -3104,27 +3973,6 @@ static void post_main(struct uloop_timeout *t) if (opts.ocibundle) cgroups_apply(jail_process.pid); - if (opts.namespace & CLONE_NEWUSER) { - if (write_setgroups(jail_process.pid, true)) { - ERROR("can't write setgroups\n"); - free_and_exit(-1); - } - if (!opts.uidmap) { - bool has_gr = (opts.gr_gid != -1); - if (opts.pw_uid != -1) { - write_single_uid_gid_map(jail_process.pid, 0, opts.pw_uid); - write_single_uid_gid_map(jail_process.pid, 1, has_gr?opts.gr_gid:opts.pw_gid); - } else { - write_single_uid_gid_map(jail_process.pid, 0, 65534); - write_single_uid_gid_map(jail_process.pid, 1, has_gr?opts.gr_gid:65534); - } - } else { - write_uid_gid_map(jail_process.pid, 0, opts.uidmap); - if (opts.gidmap) - write_uid_gid_map(jail_process.pid, 1, opts.gidmap); - } - } - if (opts.namespace & CLONE_NEWNET) jail_network_start(parent_ctx, opts.name, jail_process.pid); @@ -3153,6 +4001,97 @@ static void post_create_runtime(void) free_and_exit(-1); } + /* + * Wait for the child to reach enter_userns() and create its own + * userns before writing its uid/gid maps; see that function. + */ + if ((opts.namespace & CLONE_NEWUSER) && opts.setns.user == -1) { + char ubuf[1]; + + if (xread_byte(userns_pipe[0], ubuf) < 1) { + ERROR("can't read from child\n"); + free_and_exit(-1); + } + close(userns_pipe[0]); + + if (write_setgroups(jail_process.pid, true)) { + ERROR("can't write setgroups\n"); + free_and_exit(-1); + } + if (!opts.uidmap) { + bool has_gr = (opts.gr_gid != -1); + int primary_gid = has_gr ? opts.gr_gid : + ((opts.pw_uid != -1) ? opts.pw_gid : 65534); + int target_uid = (opts.pw_uid != -1) ? opts.pw_uid : 65534; + + if (write_single_uid_gid_map(jail_process.pid, 0, target_uid)) { + ERROR("failed to map uid %d into the jail's user " + "namespace (mapping a uid other than your own " + "requires CAP_SETUID, typically real root; use " + "-U with your own account, run as a privileged " + "user, or supply an explicit --uidmap)\n", + target_uid); + free_and_exit(-1); + } + + if (opts.additional_gids && opts.num_additional_gids) { + char *gidmap = build_group_gidmap(primary_gid); + + if (gidmap) { + if (write_uid_gid_map(jail_process.pid, 1, gidmap)) { + ERROR("failed to map supplementary groups for " + "%s into the jail's user namespace " + "(mapping a gid other than your own " + "requires CAP_SETGID, typically real " + "root)\n", opts.user); + free(gidmap); + free_and_exit(-1); + } + free(gidmap); + } else { + WARNING("failed to build supplementary group map for " + "%s; falling back to primary gid only\n", opts.user); + if (write_single_uid_gid_map(jail_process.pid, 1, primary_gid)) { + ERROR("failed to map gid %d into the jail's " + "user namespace (mapping a gid other " + "than your own requires CAP_SETGID, " + "typically real root)\n", primary_gid); + free_and_exit(-1); + } + } + } else { + if (write_single_uid_gid_map(jail_process.pid, 1, primary_gid)) { + ERROR("failed to map gid %d into the jail's user " + "namespace (mapping a gid other than your " + "own requires CAP_SETGID, typically real " + "root)\n", primary_gid); + free_and_exit(-1); + } + } + } else { + if (write_uid_gid_map(jail_process.pid, 0, opts.uidmap)) { + ERROR("failed to write uidmap (check --uidmap values " + "and privileges)\n"); + free_and_exit(-1); + } + if (opts.gidmap && write_uid_gid_map(jail_process.pid, 1, opts.gidmap)) { + ERROR("failed to write gidmap (check --gidmap values " + "and privileges)\n"); + free_and_exit(-1); + } + } + + ubuf[0] = 'O'; + if (xwrite_byte(userns_pipe[3], ubuf[0]) < 0) { + ERROR("can't write to child\n"); + free_and_exit(-1); + } + close(userns_pipe[3]); + } else { + close(userns_pipe[0]); + close(userns_pipe[3]); + } + jail_oci_state = OCI_STATE_CREATED; if (opts.ocibundle && !opts.immediately) uloop_run(); /* wait for 'start' command via ubus */