#define _GNU_SOURCE #include "run.h" #include "config.h" #include "install.h" #include "source.h" #include "state.h" #include #include #include #include #include #include #include #include #include #include #include #include #undef strchr #undef memchr #define RUN_VIEW_LIMIT 32 struct run_view { char *public_path, *private_path; }; struct run_choice { const char *digest; const char *command; char *relative; int private_path; int auto_view; struct run_view views[RUN_VIEW_LIMIT]; size_t view_count; char *private_bins[RUN_VIEW_LIMIT]; size_t private_bin_count; }; static int valid_name(const char *name); static int view_object(int root, const char *path, struct stat *st); static int private_bin_order(const void *left, const void *right) { return strcmp(*(const char *const *)left, *(const char *const *)right); } static int collect_private_bins(int files, struct run_choice *choice) { static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"}; static const char prefix[] = "usr/lib/holy/private/"; struct stat st; FILE *stream = NULL; char *line = NULL; size_t capacity = 0, number = 0; ssize_t length; int copy, ok = 0; if (fstat(files, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > 16 * 1024 * 1024 || lseek(files, 0, SEEK_SET) < 0) return 0; copy = dup(files); if (copy < 0) return 0; stream = fdopen(copy, "r"); if (!stream) { close(copy); return 0; } while ((length = getline(&line, &capacity, stream)) >= 0) { char **fields = NULL, *error = NULL; size_t count = 0, i, j; const char *path, *artifact, *suffix; ++number; if (memchr(line, 0, (size_t)length) || !holy_lex(line, (size_t)length, &fields, &count, "installed/files", number, &error)) { free(error); holy_tokens_free(fields, count); goto done; } free(error); if (!count || !strcmp(fields[0], "dir")) { holy_tokens_free(fields, count); continue; } if (count != 12 && count != 13) { holy_tokens_free(fields, count); goto done; } path = fields[1]; if (strncmp(path, prefix, sizeof prefix - 1)) { holy_tokens_free(fields, count); continue; } artifact = path + sizeof prefix - 1; suffix = strchr(artifact, '/'); if (!suffix || suffix == artifact) { holy_tokens_free(fields, count); goto done; } ++suffix; for (i = 0; i < sizeof dirs / sizeof dirs[0]; ++i) { char *bin; if (strncmp(suffix, dirs[i], strlen(dirs[i])) || !valid_name(suffix + strlen(dirs[i]))) continue; bin = strndup(path, (size_t)(suffix - path) + strlen(dirs[i]) - 1); if (!bin) { holy_tokens_free(fields, count); goto done; } for (j = 0; j < choice->private_bin_count; ++j) if (!strcmp(choice->private_bins[j], bin)) break; if (j == choice->private_bin_count) { if (j == RUN_VIEW_LIMIT) { free(bin); holy_tokens_free(fields, count); goto done; } choice->private_bins[choice->private_bin_count++] = bin; } else free(bin); break; } holy_tokens_free(fields, count); } ok = !ferror(stream) && st.st_size == ftello(stream); if (ok) qsort(choice->private_bins, choice->private_bin_count, sizeof *choice->private_bins, private_bin_order); done: free(line); fclose(stream); return ok; } static char *join(const char *left, const char *right) { size_t a = strlen(left), b = strlen(right); char *result; if (a > (size_t)-1 - b - 1) return NULL; result = malloc(a + b + 1); if (result) { memcpy(result, left, a); memcpy(result + a, right, b + 1); } return result; } static char *join_root(const char *root, const char *relative) { char *prefix, *path; if (!strcmp(root, "/")) return join(root, relative); prefix = join(root, "/"); if (!prefix) return NULL; path = join(prefix, relative); free(prefix); return path; } static int valid_name(const char *name) { return name && *name && strcmp(name, ".") && strcmp(name, "..") && !strchr(name, '/'); } static int valid_public_view(const char *path) { static const char *const roots[] = {"/usr/bin/", "/usr/sbin/", "/bin/", "/sbin/", "/usr/lib/"}; const char *p; size_t i; if (!path) return 0; if (!strcmp(path, "/app")) return 1; if (!strncmp(path, "/app/", 5)) { p = path + 5; if (!*p) return 0; goto components; } for (i = 0; i < sizeof roots / sizeof roots[0]; ++i) if (!strncmp(path, roots[i], strlen(roots[i]))) break; if (i == sizeof roots / sizeof roots[0]) return 0; p = path + strlen(roots[i]); components: while (*p) { const char *end = strchr(p, '/'); size_t n = end ? (size_t)(end - p) : strlen(p); if (!n || (n == 1 && *p == '.') || (n == 2 && !memcmp(p, "..", 2))) return 0; if (!end) return 1; p = end + 1; } return 0; } static int add_view(struct run_choice *choice, const char *spec) { const char *equals = strchr(spec, '='); struct run_view *view; size_t i; if (!equals || choice->view_count == RUN_VIEW_LIMIT) return 0; view = &choice->views[choice->view_count]; view->public_path = strndup(spec, (size_t)(equals - spec)); view->private_path = strdup(equals + 1); if (!view->public_path || !view->private_path || !valid_public_view(view->public_path) || strncmp(view->private_path, "/usr/lib/holy/private/", 22) || !valid_public_view(view->private_path)) return 0; for (i = 0; i < choice->view_count; ++i) if (!strcmp(choice->views[i].public_path, view->public_path)) return 0; ++choice->view_count; return 1; } static int collect_auto_views(int files, int root, struct run_choice *choice) { static const char prefix[] = "usr/lib/holy/private/"; struct stat st; FILE *stream = NULL; char *line = NULL; size_t capacity = 0, number = 0; ssize_t length; int copy = -1, rootfd = -1, result = 1; size_t explicit_views = choice->view_count; if (fstat(files, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 || st.st_size > 16 * 1024 * 1024 || lseek(files, 0, SEEK_SET) < 0) return 1; copy = dup(files); rootfd = openat(root, ".", O_PATH | O_DIRECTORY | O_CLOEXEC); if (copy < 0 || rootfd < 0 || !(stream = fdopen(copy, "r"))) goto done; copy = -1; result = 0; while ((length = getline(&line, &capacity, stream)) >= 0) { char **fields = NULL, *error = NULL; size_t count = 0, i; const char *slash, *suffix; char *public_path = NULL, *private_path = NULL, *spec = NULL; struct stat source_state, target_state; int source = -1, target = -1; ++number; if (memchr(line, 0, (size_t)length) || !holy_lex(line, (size_t)length, &fields, &count, "installed/files", number, &error)) { free(error); holy_tokens_free(fields, count); result = 1; goto done; } free(error); if (!count || strcmp(fields[0], "file") || count != 12 || strncmp(fields[1], prefix, sizeof prefix - 1)) { holy_tokens_free(fields, count); continue; } slash = strchr(fields[1] + sizeof prefix - 1, '/'); suffix = slash ? slash + 1 : NULL; if (!suffix || !*suffix) { holy_tokens_free(fields, count); result = 1; goto done; } public_path = join("/", suffix); private_path = join("/", fields[1]); if (!public_path || !private_path) { result = 1; goto row_done; } if (!valid_public_view(public_path)) goto row_done; for (i = 0; i < choice->view_count; ++i) if (!strcmp(choice->views[i].public_path, public_path)) break; if (i < choice->view_count) { if (i >= explicit_views && strcmp(choice->views[i].private_path, private_path)) result = 3; goto row_done; } source = view_object(rootfd, private_path, &source_state); if (source < 0) { result = 6; goto row_done; } target = view_object(rootfd, public_path, &target_state); if (target < 0) { if (errno != ENOENT) result = 6; goto row_done; } if (!S_ISREG(source_state.st_mode) || !S_ISREG(target_state.st_mode)) { result = 6; goto row_done; } spec = malloc(strlen(public_path) + strlen(private_path) + 2); if (!spec) { result = 1; goto row_done; } sprintf(spec, "%s=%s", public_path, private_path); if (!add_view(choice, spec)) { result = choice->view_count == RUN_VIEW_LIMIT ? 6 : 1; goto row_done; } row_done: if (source >= 0) close(source); if (target >= 0) close(target); free(public_path); free(private_path); free(spec); holy_tokens_free(fields, count); if (result) goto done; } if (ferror(stream) || st.st_size != ftello(stream)) result = 1; done: free(line); if (stream) fclose(stream); if (copy >= 0) close(copy); if (rootfd >= 0) close(rootfd); return result; } static int select_path(void *context, int root, int instance, const char *digest) { static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"}; static const char prefix[] = "usr/lib/holy/private/"; struct run_choice *choice = context; const char *name = choice->command; char *private_base = NULL, *public_path = NULL; size_t i, count = sizeof dirs / sizeof dirs[0]; int files = -1, status = 0, owned = 0; if (strcmp(digest, choice->digest)) return 0; files = openat(instance, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (files < 0) return 1; private_base = join("usr/lib/holy/private/", digest); if (private_base) { char *next = join(private_base, "/"); free(private_base); private_base = next; } if (!private_base) { status = 1; goto done; } if (name[0] == '/') { const char *relative = name + 1; /* an absolute command may name a private file the manifest owns, which is how a package with a private tree starts its own entry point */ if (!strncmp(relative, prefix, sizeof prefix - 1)) { const char *inside = relative + sizeof prefix - 1; const char *slash = strchr(inside, '/'); if (!slash || slash == inside) { status = 2; goto done; } for (i = 0; i < count; ++i) if (!strncmp(slash + 1, dirs[i], strlen(dirs[i])) && valid_name(slash + 1 + strlen(dirs[i]))) break; if (i == count) { status = 2; goto done; } owned = 1; } else { for (i = 0; i < count; ++i) if (!strncmp(relative, dirs[i], strlen(dirs[i])) && valid_name(relative + strlen(dirs[i]))) break; if (i == count) { status = 2; goto done; } } public_path = strdup(relative); } else { if (!valid_name(name)) { status = 2; goto done; } } if (name[0] == '/' && !public_path) { status = 1; goto done; } for (i = 0; i < (public_path ? (owned ? 1 : 2) : count * 2); ++i) { char *candidate, *base; int private_candidate = public_path ? i == 0 && !owned : i < count; const char *suffix = public_path ? public_path : dirs[i % count]; base = private_candidate ? private_base : ""; candidate = join(base, suffix); if (candidate && !public_path) { char *next = join(candidate, name); free(candidate); candidate = next; } if (!candidate) { status = 1; goto done; } status = holy_install_manifest_executable(files, candidate); if (status > 0) { if (holy_install_check_manifest(files, root) != 1) { free(candidate); status = 4; goto done; } choice->relative = candidate; choice->private_path = private_candidate; if (choice->auto_view && (status = collect_auto_views(files, root, choice))) goto done; for (i = 0; i < choice->view_count; ++i) { int owned = holy_install_manifest_owns(files, choice->views[i].private_path + 1); if (owned != 1 && owned != 2) { status = owned < 0 ? 1 : 6; goto done; } } if (!collect_private_bins(files, choice)) { status = 1; goto done; } status = 0; goto done; } free(candidate); if (status < 0) { status = 1; goto done; } } status = 6; done: free(private_base); free(public_path); close(files); return status; } static int write_kernel_file(const char *path, const char *value) { size_t length = strlen(value), offset = 0; int fd = open(path, O_WRONLY | O_CLOEXEC); if (fd < 0) return 0; while (offset < length) { ssize_t written = write(fd, value + offset, length - offset); if (written < 0 && errno == EINTR) continue; if (written <= 0) { close(fd); return 0; } offset += (size_t)written; } return !close(fd); } static int enter_view_namespace(void) { char mapping[80]; uid_t uid = geteuid(); gid_t gid = getegid(); if (unshare(CLONE_NEWUSER | CLONE_NEWNS)) return 0; snprintf(mapping, sizeof mapping, "%lu %lu 1\n", (unsigned long)uid, (unsigned long)uid); if (!write_kernel_file("/proc/self/uid_map", mapping) || !write_kernel_file("/proc/self/setgroups", "deny\n")) return 0; snprintf(mapping, sizeof mapping, "%lu %lu 1\n", (unsigned long)gid, (unsigned long)gid); return write_kernel_file("/proc/self/gid_map", mapping) && !mount(NULL, "/", NULL, MS_REC | MS_PRIVATE, NULL); } static int view_object(int root, const char *path, struct stat *st) { struct open_how how = {0}; int fd; how.flags = O_PATH | O_CLOEXEC; how.resolve = RESOLVE_BENEATH | RESOLVE_NO_SYMLINKS; fd = (int)syscall(SYS_openat2, root, path + 1, &how, sizeof how); if (fd < 0) return -1; if (fstat(fd, st) || (!S_ISREG(st->st_mode) && !S_ISDIR(st->st_mode))) { close(fd); errno = EINVAL; return -1; } return fd; } static int apply_views(const char *root, const struct run_choice *choice) { size_t i; int rootfd = open(root, O_PATH | O_DIRECTORY | O_CLOEXEC); if (rootfd < 0) return 6; for (i = 0; i < choice->view_count; ++i) { struct stat original, replacement; int source = view_object(rootfd, choice->views[i].private_path, &replacement); int target = view_object(rootfd, choice->views[i].public_path, &original); char source_descriptor[64], target_descriptor[64]; if (source < 0 || target < 0 || S_ISDIR(original.st_mode) != S_ISDIR(replacement.st_mode) || (S_ISREG(replacement.st_mode) && strncmp(choice->views[i].public_path, "/usr/lib/", 9) && !(replacement.st_mode & 0111))) { if (source >= 0) close(source); if (target >= 0) close(target); close(rootfd); return 6; } snprintf(source_descriptor, sizeof source_descriptor, "/proc/self/fd/%d", source); snprintf(target_descriptor, sizeof target_descriptor, "/proc/self/fd/%d", target); if (mount(source_descriptor, target_descriptor, NULL, MS_BIND, NULL)) { perror("holypkg: view bind mount"); close(source); close(target); close(rootfd); return 6; } close(source); close(target); } close(rootfd); return 0; } static int private_path_env(const char *root, const struct run_choice *choice) { const char *old = getenv("PATH"); char *value; size_t i, length = 0, old_length; char *cursor; int ok; if (!choice->private_bin_count) return 1; if (!old || !*old) old = "/usr/bin:/bin"; if (strchr(root, ':')) return 0; old_length = strlen(old); for (i = 0; i < choice->private_bin_count; ++i) { size_t a = strlen(root), b = strlen(choice->private_bins[i]), n; if (strchr(choice->private_bins[i], ':') || a > SIZE_MAX - b - 2) return 0; n = a + b + 2; if (n > SIZE_MAX - length) return 0; length += n; } if (length > SIZE_MAX - old_length - 1) return 0; value = malloc(length + old_length + 1); if (!value) return 0; cursor = value; for (i = 0; i < choice->private_bin_count; ++i) { size_t a = strlen(root), b = strlen(choice->private_bins[i]); memcpy(cursor, root, a); cursor += a; if (cursor[-1] != '/') *cursor++ = '/'; memcpy(cursor, choice->private_bins[i], b); cursor += b; *cursor++ = ':'; } memcpy(cursor, old, old_length + 1); ok = !setenv("PATH", value, 1); free(value); return ok; } int holy_run(int argc, char **argv) { const char *root = "/", *arch = NULL, *libc = NULL, *separator; char source_id[65], digest[65], *alias = NULL, *name = NULL, *canonical = NULL; char *path = NULL; struct run_choice choice = {0}; unsigned long long generation; int i, command = -1, result = 2; if (argc < 5 || !(separator = strchr(argv[2], ':')) || separator == argv[2] || !separator[1] || strchr(separator + 1, ':')) goto done; alias = strndup(argv[2], (size_t)(separator - argv[2])); name = strdup(separator + 1); if (!alias || !name) { result = 1; goto done; } for (i = 3; i < argc; ++i) { if (!strcmp(argv[i], "--")) { command = i + 1; break; } if (!strcmp(argv[i], "--root") && i + 1 < argc && !strcmp(root, "/")) root = argv[++i]; else if (!strcmp(argv[i], "--arch") && i + 1 < argc && !arch) arch = argv[++i]; else if (!strcmp(argv[i], "--libc") && i + 1 < argc && !libc) libc = argv[++i]; else if (!strcmp(argv[i], "--auto-view") && !choice.auto_view) choice.auto_view = 1; else if (!strcmp(argv[i], "--view") && i + 1 < argc && add_view(&choice, argv[i + 1])) ++i; else goto done; } if (command < 0 || command == argc || !argv[command][0]) goto done; canonical = realpath(root, NULL); if (!canonical) { result = 6; goto done; } if (!strcmp(alias, "local")) strcpy(source_id, "-"); else { result = holy_source_known_id(canonical, alias, source_id); if (result) goto done; } result = holy_state_find_slot(canonical, source_id, name, arch, libc, digest); if (result) goto done; choice.digest = digest; choice.command = argv[command]; result = holy_state_visit(canonical, select_path, &choice, &generation); if (result) goto done; if (!choice.relative) { result = 6; goto done; } path = join_root(choice.view_count ? "/" : canonical, choice.relative); if (!path) { result = 1; goto done; } if (!private_path_env(choice.view_count ? "/" : canonical, &choice)) { result = 1; goto done; } if (choice.view_count) { if (!enter_view_namespace()) { fputs("holypkg: user or mount namespace unavailable for path view\n", stderr); result = 6; goto done; } result = apply_views(canonical, &choice); if (result) goto done; if (chroot(canonical) || chdir("/")) { perror("holypkg: view target root"); result = 6; goto done; } } execv(path, argv + command); perror("holypkg: run"); result = 1; done: if (result == 2) fputs("usage: holypkg run SOURCE:PACKAGE [--root DIRECTORY] [--arch ARCH] [--libc LIBC] [--auto-view] [--view PUBLIC=PRIVATE ...] -- COMMAND [ARGS...]\n", stderr); else if (result == 6 && !choice.relative) fputs("holypkg: package command unavailable\n", stderr); else if (result == 4) fputs("holypkg: installed payload changed\n", stderr); else if (result == 3) fputs("holypkg: private path view is ambiguous; select --view explicitly\n", stderr); free(alias); free(name); free(canonical); free(choice.relative); free(path); for (i = 0; i < RUN_VIEW_LIMIT; ++i) { free(choice.views[i].public_path); free(choice.views[i].private_path); free(choice.private_bins[i]); } return result; }