#define _XOPEN_SOURCE 700 #include "config.h" #include "disk.h" #include #include #include #include #include #include #include #include #include #include #include struct install_input { char *root; char **artifacts; size_t count; char **accepted_arch; size_t accept_count; char **accepted_privileged; size_t privileged_count; char **sources; size_t source_count; struct stat root_stat; char hash[65]; }; static int digest_valid(const char *s) { size_t i; if (strlen(s) != 64) return 0; for (i = 0; i < 64; ++i) if (!((s[i] >= '0' && s[i] <= '9') || (s[i] >= 'a' && s[i] <= 'f'))) return 0; return 1; } static char *source_binding(const char *artifact, const char *source) { char *binding; if (!digest_valid(artifact) || !digest_valid(source)) return NULL; binding = malloc(130); if (binding) snprintf(binding, 130, "%s=%s", artifact, source); return binding; } static int hex_digest(const unsigned char *bytes, char output[65]) { static const char digits[] = "0123456789abcdef"; size_t i; for (i = 0; i < 32; ++i) { output[i * 2] = digits[bytes[i] >> 4]; output[i * 2 + 1] = digits[bytes[i] & 15]; } output[64] = 0; return 1; } static int hash_string(EVP_MD_CTX *ctx, const char *s) { size_t size = strlen(s), i; unsigned char length[8]; if ((unsigned long long)size != size) return 0; for (i = 0; i < sizeof length; ++i) length[i] = (unsigned char)((unsigned long long)size >> (i * 8)); return EVP_DigestUpdate(ctx, length, sizeof length) == 1 && EVP_DigestUpdate(ctx, s, size) == 1; } static int config_hash(const struct holy_config *config, char output[65]) { EVP_MD_CTX *ctx = EVP_MD_CTX_new(); unsigned char bytes[32]; unsigned length; size_t i, j; int ok = ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1; for (i = 0; i < config->count && ok; ++i) { const struct holy_entry *e = &config->entries[i]; ok = hash_string(ctx, e->section) && hash_string(ctx, e->key); for (j = 0; j < e->count && ok; ++j) ok = hash_string(ctx, e->values[j]); } if (ok) ok = EVP_DigestFinal_ex(ctx, bytes, &length) == 1 && length == 32; EVP_MD_CTX_free(ctx); return ok && hex_digest(bytes, output); } static const char *field(const struct holy_config *config, const char *section, const char *key) { size_t i; for (i = 0; i < config->count; ++i) if (!strcmp(config->entries[i].section, section) && !strcmp(config->entries[i].key, key)) return config->entries[i].values[0]; return NULL; } static int load_input(const char *path, struct holy_config *config, struct install_input *input) { const char *root; char *error = NULL; size_t i, j; if (!holy_config_load(path, config, &error)) { fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid config"); free(error); return 2; } root = field(config, "install", "root"); if (!root) { fputs("holyinstall: [install] root is required\n", stderr); return 2; } input->root = realpath(root, NULL); if (!input->root || stat(input->root, &input->root_stat) || !S_ISDIR(input->root_stat.st_mode)) { fputs("holyinstall: target root must be an existing directory\n", stderr); return 6; } if (!strcmp(input->root, "/")) { fputs("holyinstall: host / is not an installation target\n", stderr); return 2; } input->artifacts = calloc(config->count ? config->count : 1, sizeof *input->artifacts); if (!input->artifacts) return 1; for (i = 0; i < config->count; ++i) { const struct holy_entry *e = &config->entries[i]; if (strcmp(e->section, "install") || strcmp(e->key, "artifact")) continue; if (input->count >= 1024) { fputs("holyinstall: at most 1024 artifacts are supported\n", stderr); return 2; } if (!digest_valid(e->values[0])) { fprintf(stderr, "holyinstall: invalid artifact at %s:%zu\n", e->file, e->line); return 2; } for (j = 0; j < input->count; ++j) if (!strcmp(input->artifacts[j], e->values[0])) { fprintf(stderr, "holyinstall: duplicate artifact at %s:%zu\n", e->file, e->line); return 2; } input->artifacts[input->count] = strdup(e->values[0]); if (!input->artifacts[input->count]) return 1; ++input->count; } if (!input->count) { fputs("holyinstall: [install] needs artifact entries\n", stderr); return 2; } input->accepted_arch = calloc(config->count ? config->count : 1, sizeof *input->accepted_arch); input->accepted_privileged = calloc(config->count ? config->count : 1, sizeof *input->accepted_privileged); if (!input->accepted_arch || !input->accepted_privileged) return 1; for (i = 0; i < config->count; ++i) { const struct holy_entry *e = &config->entries[i]; char ***accepted; size_t *accepted_count; if (strcmp(e->section, "install")) continue; if (!strcmp(e->key, "accept-arch")) { accepted = &input->accepted_arch; accepted_count = &input->accept_count; } else if (!strcmp(e->key, "accept-privileged")) { accepted = &input->accepted_privileged; accepted_count = &input->privileged_count; } else continue; if (!digest_valid(e->values[0])) { fprintf(stderr, "holyinstall: invalid %s at %s:%zu\n", e->key, e->file, e->line); return 2; } for (j = 0; j < input->count; ++j) if (!strcmp(input->artifacts[j], e->values[0])) break; if (j == input->count) { fprintf(stderr, "holyinstall: %s needs a selected artifact at %s:%zu\n", e->key, e->file, e->line); return 2; } for (j = 0; j < *accepted_count; ++j) if (!strcmp((*accepted)[j], e->values[0])) break; if (j != *accepted_count) { fprintf(stderr, "holyinstall: duplicate %s at %s:%zu\n", e->key, e->file, e->line); return 2; } (*accepted)[*accepted_count] = strdup(e->values[0]); if (!(*accepted)[*accepted_count]) return 1; ++*accepted_count; } input->sources = calloc(config->count ? config->count : 1, sizeof *input->sources); if (!input->sources) return 1; for (i = 0; i < config->count; ++i) { const struct holy_entry *e = &config->entries[i]; char *binding; if (strcmp(e->section, "install") || strcmp(e->key, "source")) continue; for (j = 0; j < input->count; ++j) if (!strcmp(input->artifacts[j], e->values[0])) break; if (j == input->count || !(binding = source_binding(e->values[0], e->values[1]))) { fprintf(stderr, "holyinstall: invalid source binding at %s:%zu\n", e->file, e->line); return 2; } for (j = 0; j < input->source_count; ++j) if (!strncmp(input->sources[j], binding, 65)) break; if (j != input->source_count) { free(binding); fprintf(stderr, "holyinstall: duplicate source binding at %s:%zu\n", e->file, e->line); return 2; } input->sources[input->source_count++] = binding; } if (!config_hash(config, input->hash)) return 1; return 0; } static int run_package_manager(const char *binary, const struct install_input *input, const char *approved, char **output) { char **args; size_t i, count = input->count + (input->accept_count + input->privileged_count + input->source_count) * 2 + (approved ? 7 : 6), used = 0, capacity = 0; char *buffer = NULL; int pipefd[2] = {-1, -1}, status, rc = 1; pid_t child; args = calloc(count, sizeof *args); if (!args) return 1; args[0] = (char *)binary; args[1] = "db"; args[2] = approved ? "apply-set" : "plan-set"; i = 3; if (approved) args[i++] = (char *)approved; for (size_t j = 0; j < input->count; ++j) args[i++] = (char *)input->artifacts[j]; for (size_t j = 0; j < input->accept_count; ++j) { args[i++] = "--accept-arch"; args[i++] = input->accepted_arch[j]; } for (size_t j = 0; j < input->privileged_count; ++j) { args[i++] = "--accept-privileged"; args[i++] = input->accepted_privileged[j]; } for (size_t j = 0; j < input->source_count; ++j) { args[i++] = "--source"; args[i++] = input->sources[j]; } args[i++] = "--root"; args[i++] = input->root; if (pipe(pipefd)) goto done; child = fork(); if (child < 0) goto done; if (!child) { close(pipefd[0]); if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(1); close(pipefd[1]); execv(binary, args); _exit(127); } close(pipefd[1]); pipefd[1] = -1; for (;;) { ssize_t got; char *next; if (used == capacity) { size_t new_capacity = capacity ? capacity * 2 : 4096; if (new_capacity > 16 * 1024 * 1024) { rc = 1; break; } next = realloc(buffer, new_capacity + 1); if (!next) { rc = 1; break; } buffer = next; capacity = new_capacity; } got = read(pipefd[0], buffer + used, capacity - used); if (got < 0 && errno == EINTR) continue; if (got < 0) { rc = 1; break; } if (!got) { rc = 0; break; } used += (size_t)got; } close(pipefd[0]); pipefd[0] = -1; while (waitpid(child, &status, 0) < 0) if (errno != EINTR) goto done; if (rc || !WIFEXITED(status)) { rc = 1; goto done; } rc = WEXITSTATUS(status); if (rc) goto done; if (!buffer) { rc = 1; goto done; } buffer[used] = 0; if (memchr(buffer, 0, used)) { rc = 1; goto done; } *output = buffer; buffer = NULL; done: if (pipefd[0] >= 0) close(pipefd[0]); if (pipefd[1] >= 0) close(pipefd[1]); free(buffer); free(args); return rc; } static int set_hash(const char *output, char hash[65]) { const char *marker = " sha256 ", *line = output, *end; int found = 0; while (*line) { end = strchr(line, '\n'); if (!end) return 0; if (!strncmp(line, "plan-set ", 9)) { const char *at = strstr(line, marker); if (!at || at >= end || end - at != 8 + 64 + 10 || memcmp(at + 8 + 64, " read-only", 10)) return 0; memcpy(hash, at + 8, 64); hash[64] = 0; if (!digest_valid(hash) || ++found != 1) return 0; } line = end + 1; } return found == 1; } static int quote(FILE *stream, const char *s) { const unsigned char *p = (const unsigned char *)s; if (fputc('"', stream) == EOF) return 0; for (; *p; ++p) { if (*p == '"' || *p == '\\') { if (fputc('\\', stream) == EOF) return 0; if (fputc(*p, stream) == EOF) return 0; } else if (*p < 32 || *p == 127) { if (fprintf(stream, "\\x%02x", *p) < 0) return 0; } else if (fputc(*p, stream) == EOF) return 0; } return fputc('"', stream) != EOF; } static int sync_parent(const char *path) { const char *slash = strrchr(path, '/'); char *directory = slash ? strndup(path, (size_t)(slash - path)) : strdup("."); int fd, ok; if (!directory) return 0; if (!*directory) { free(directory); directory = strdup("/"); } if (!directory) return 0; fd = open(directory, O_RDONLY | O_DIRECTORY | O_CLOEXEC); free(directory); if (fd < 0) return 0; ok = !fsync(fd); if (close(fd)) ok = 0; return ok; } static int write_plan(const char *path, const struct install_input *input, const char *hash) { int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); FILE *stream; size_t i; int ok = 1; if (fd < 0) { perror("holyinstall: plan"); return 1; } stream = fdopen(fd, "w"); if (!stream) { close(fd); unlink(path); return 1; } if (fprintf(stream, "[install-plan]\nformat holy-install-plan-%d\nroot ", input->source_count ? 4 : input->privileged_count ? 3 : input->accept_count ? 2 : 1) < 0 || !quote(stream, input->root) || fprintf(stream, "\ndevice %ju\ninode %ju\nconfig-sha256 %s\nset-sha256 %s\n", (uintmax_t)input->root_stat.st_dev, (uintmax_t)input->root_stat.st_ino, input->hash, hash) < 0) ok = 0; for (i = 0; i < input->count && ok; ++i) if (fprintf(stream, "artifact %s\n", input->artifacts[i]) < 0) ok = 0; for (i = 0; i < input->accept_count && ok; ++i) if (fprintf(stream, "accept-arch %s\n", input->accepted_arch[i]) < 0) ok = 0; for (i = 0; i < input->privileged_count && ok; ++i) if (fprintf(stream, "accept-privileged %s\n", input->accepted_privileged[i]) < 0) ok = 0; for (i = 0; i < input->source_count && ok; ++i) if (fprintf(stream, "source %.64s %s\n", input->sources[i], input->sources[i] + 65) < 0) ok = 0; if (fflush(stream) || fsync(fd)) ok = 0; if (fclose(stream)) ok = 0; if (ok) ok = sync_parent(path); if (!ok) unlink(path); return ok ? 0 : 1; } static int check_plan(const char *path, struct install_input *input, char hash[65]) { int fd = open(path, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC); FILE *stream; struct stat plan_stat; char *line = NULL, *resolved = NULL; size_t capacity = 0, row = 0; ssize_t length; int rc = 2, version = 0, phase = 0; if (fd < 0) { perror("holyinstall: plan"); return 2; } if (fstat(fd, &plan_stat) || !S_ISREG(plan_stat.st_mode) || plan_stat.st_size < 0 || plan_stat.st_size > 2 * 1024 * 1024) { close(fd); fputs("holyinstall: invalid plan file\n", stderr); return 2; } stream = fdopen(fd, "r"); if (!stream) { close(fd); return 1; } while ((length = getline(&line, &capacity, stream)) >= 0) { char **v = NULL, *error = NULL; char expected[64]; size_t count = 0; ++row; if (length > 1024 * 1024 || memchr(line, 0, (size_t)length) || !holy_lex(line, (size_t)length, &v, &count, path, row, &error)) { free(error); holy_tokens_free(v, count); goto done; } if (row == 1) { if (count != 1 || strcmp(v[0], "[install-plan]")) { holy_tokens_free(v, count); goto done; } } else if (count != 2 && !(count == 3 && !strcmp(v[0], "source"))) { holy_tokens_free(v, count); goto done; } else if (row == 2) { if (strcmp(v[0], "format") || (strcmp(v[1], "holy-install-plan-1") && strcmp(v[1], "holy-install-plan-2") && strcmp(v[1], "holy-install-plan-3") && strcmp(v[1], "holy-install-plan-4"))) { holy_tokens_free(v, count); goto done; } version = !strcmp(v[1], "holy-install-plan-4") ? 4 : !strcmp(v[1], "holy-install-plan-3") ? 3 : !strcmp(v[1], "holy-install-plan-2") ? 2 : 1; } else if (row == 3) { if (strcmp(v[0], "root") || !(input->root = strdup(v[1])) || !(resolved = realpath(input->root, NULL)) || strcmp(resolved, input->root) || !strcmp(input->root, "/") || stat(input->root, &input->root_stat) || !S_ISDIR(input->root_stat.st_mode)) { holy_tokens_free(v, count); goto done; } } else if (row == 4 || row == 5) { snprintf(expected, sizeof expected, "%ju", row == 4 ? (uintmax_t)input->root_stat.st_dev : (uintmax_t)input->root_stat.st_ino); if (strcmp(v[0], row == 4 ? "device" : "inode") || strcmp(v[1], expected)) { holy_tokens_free(v, count); goto done; } } else if (row == 6) { if (strcmp(v[0], "config-sha256") || !digest_valid(v[1])) { holy_tokens_free(v, count); goto done; } memcpy(input->hash, v[1], 65); } else if (row == 7) { if (strcmp(v[0], "set-sha256") || !digest_valid(v[1])) { holy_tokens_free(v, count); goto done; } memcpy(hash, v[1], 65); } else if (!strcmp(v[0], "accept-arch") || !strcmp(v[0], "accept-privileged")) { char **next; char ***accepted; size_t *accepted_count; size_t i; int privileged = !strcmp(v[0], "accept-privileged"); if (privileged) { if (version < 3 || phase >= 3) { holy_tokens_free(v, count); goto done; } phase = 2; accepted = &input->accepted_privileged; accepted_count = &input->privileged_count; } else { if (phase >= 2 || version < 2) { holy_tokens_free(v, count); goto done; } phase = 1; accepted = &input->accepted_arch; accepted_count = &input->accept_count; } if (!digest_valid(v[1]) || *accepted_count >= input->count) { holy_tokens_free(v, count); goto done; } for (i = 0; i < input->count; ++i) if (!strcmp(v[1], input->artifacts[i])) break; if (i == input->count) { holy_tokens_free(v, count); goto done; } for (i = 0; i < *accepted_count; ++i) if (!strcmp(v[1], (*accepted)[i])) break; if (i != *accepted_count || !(next = realloc(*accepted, (*accepted_count + 1) * sizeof *next))) { holy_tokens_free(v, count); goto done; } *accepted = next; (*accepted)[*accepted_count] = strdup(v[1]); if (!(*accepted)[*accepted_count]) { holy_tokens_free(v, count); goto done; } ++*accepted_count; } else if (!strcmp(v[0], "source")) { char *binding, **next; size_t i; if (version != 4 || count != 3 || !(binding = source_binding(v[1], v[2]))) { holy_tokens_free(v, count); goto done; } phase = 3; for (i = 0; i < input->count; ++i) if (!strcmp(input->artifacts[i], v[1])) break; if (i == input->count || input->source_count >= input->count) { free(binding); holy_tokens_free(v, count); goto done; } for (i = 0; i < input->source_count; ++i) if (!strncmp(input->sources[i], binding, 65)) break; if (i != input->source_count || !(next = realloc(input->sources, (input->source_count + 1) * sizeof *next))) { free(binding); holy_tokens_free(v, count); goto done; } input->sources = next; input->sources[input->source_count++] = binding; } else { char **next; size_t i; if (phase || strcmp(v[0], "artifact") || !digest_valid(v[1]) || input->count >= 1024) { holy_tokens_free(v, count); goto done; } for (i = 0; i < input->count; ++i) if (!strcmp(v[1], input->artifacts[i])) break; if (i != input->count || input->count == (size_t)-1 / sizeof *next || !(next = realloc(input->artifacts, (input->count + 1) * sizeof *next))) { holy_tokens_free(v, count); goto done; } input->artifacts = next; input->artifacts[input->count] = strdup(v[1]); if (!input->artifacts[input->count]) { holy_tokens_free(v, count); goto done; } ++input->count; } holy_tokens_free(v, count); } if (ferror(stream) || row != 7 + input->count + input->accept_count + input->privileged_count + input->source_count || !input->count || (version == 1 && input->accept_count) || (version == 2 && (!input->accept_count || input->privileged_count)) || (version == 3 && !input->privileged_count) || (version == 4 && !input->source_count)) goto done; rc = 0; done: free(line); free(resolved); fclose(stream); if (rc) fputs("holyinstall: plan inputs changed or invalid\n", stderr); return rc; } struct menu_state { char *root; char *disk_image; char **artifacts; size_t count; char **accepted_arch; size_t accept_count; char **accepted_privileged; size_t privileged_count; char **sources; size_t source_count; }; static void free_input(struct install_input *input) { size_t i; for (i = 0; i < input->count; ++i) free(input->artifacts[i]); free(input->artifacts); for (i = 0; i < input->accept_count; ++i) free(input->accepted_arch[i]); free(input->accepted_arch); for (i = 0; i < input->privileged_count; ++i) free(input->accepted_privileged[i]); free(input->accepted_privileged); for (i = 0; i < input->source_count; ++i) free(input->sources[i]); free(input->sources); free(input->root); memset(input, 0, sizeof *input); } static void free_menu(struct menu_state *menu) { size_t i; for (i = 0; i < menu->count; ++i) free(menu->artifacts[i]); free(menu->artifacts); for (i = 0; i < menu->accept_count; ++i) free(menu->accepted_arch[i]); free(menu->accepted_arch); for (i = 0; i < menu->privileged_count; ++i) free(menu->accepted_privileged[i]); free(menu->accepted_privileged); for (i = 0; i < menu->source_count; ++i) free(menu->sources[i]); free(menu->sources); free(menu->root); free(menu->disk_image); } static int menu_load(const char *path, struct menu_state *menu) { struct holy_config config = {0}; char *error = NULL; size_t i; if (access(path, F_OK) && errno == ENOENT) return 0; if (!holy_config_load(path, &config, &error)) { fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid config"); free(error); return 2; } for (i = 0; i < config.count; ++i) { const struct holy_entry *e = &config.entries[i]; char **next; if (strcmp(e->section, "install") && strcmp(e->section, "disk")) { fputs("holyinstall: text menu accepts [install] and [disk] only\n", stderr); holy_config_free(&config); return 2; } if (!strcmp(e->section, "disk")) { if (!strcmp(e->key, "image")) { menu->disk_image = strdup(e->values[0]); if (!menu->disk_image) { holy_config_free(&config); return 1; } } else if (strcmp(e->key, "layout") || strcmp(e->values[0], "gpt-ext4")) { holy_config_free(&config); return 2; } continue; } if (!strcmp(e->key, "root")) { menu->root = strdup(e->values[0]); if (!menu->root) { holy_config_free(&config); return 1; } } else if (!strcmp(e->key, "artifact")) { next = realloc(menu->artifacts, (menu->count + 1) * sizeof *next); if (!next) { holy_config_free(&config); return 1; } menu->artifacts = next; menu->artifacts[menu->count] = strdup(e->values[0]); if (!menu->artifacts[menu->count]) { holy_config_free(&config); return 1; } ++menu->count; } } if (menu->disk_image && !field(&config, "disk", "layout")) { fputs("holyinstall: [disk] layout is required\n", stderr); holy_config_free(&config); return 2; } if (!menu->disk_image && field(&config, "disk", "layout")) { fputs("holyinstall: [disk] image is required\n", stderr); holy_config_free(&config); return 2; } for (i = 0; i < config.count; ++i) { const struct holy_entry *e = &config.entries[i]; size_t j; char **next; char ***accepted; size_t *accepted_count; if (strcmp(e->section, "install")) continue; if (!strcmp(e->key, "source")) { char *binding = source_binding(e->values[0], e->values[1]); for (j = 0; j < menu->count; ++j) if (!strcmp(e->values[0], menu->artifacts[j])) break; if (!binding || j == menu->count) { free(binding); holy_config_free(&config); return 2; } for (j = 0; j < menu->source_count; ++j) if (!strncmp(menu->sources[j], binding, 65)) break; if (j != menu->source_count || !(next = realloc(menu->sources, (menu->source_count + 1) * sizeof *next))) { free(binding); holy_config_free(&config); return 2; } menu->sources = next; menu->sources[menu->source_count++] = binding; continue; } if (!strcmp(e->key, "accept-arch")) { accepted = &menu->accepted_arch; accepted_count = &menu->accept_count; } else if (!strcmp(e->key, "accept-privileged")) { accepted = &menu->accepted_privileged; accepted_count = &menu->privileged_count; } else continue; for (j = 0; j < menu->count; ++j) if (!strcmp(e->values[0], menu->artifacts[j])) break; if (!digest_valid(e->values[0]) || j == menu->count) { fprintf(stderr, "holyinstall: %s needs a selected artifact\n", e->key); holy_config_free(&config); return 2; } for (j = 0; j < *accepted_count; ++j) if (!strcmp(e->values[0], (*accepted)[j])) break; if (j != *accepted_count) { fprintf(stderr, "holyinstall: duplicate %s\n", e->key); holy_config_free(&config); return 2; } next = realloc(*accepted, (*accepted_count + 1) * sizeof *next); if (!next) { holy_config_free(&config); return 1; } *accepted = next; (*accepted)[*accepted_count] = strdup(e->values[0]); if (!(*accepted)[*accepted_count]) { holy_config_free(&config); return 1; } ++*accepted_count; } holy_config_free(&config); return 0; } static int menu_write(FILE *stream, const struct menu_state *menu) { size_t i; if (fputs("[install]\n", stream) == EOF) return 0; if (menu->root && (fputs("root ", stream) == EOF || !quote(stream, menu->root) || fputc('\n', stream) == EOF)) return 0; for (i = 0; i < menu->count; ++i) if (fprintf(stream, "artifact %s\n", menu->artifacts[i]) < 0) return 0; for (i = 0; i < menu->accept_count; ++i) if (fprintf(stream, "accept-arch %s\n", menu->accepted_arch[i]) < 0) return 0; for (i = 0; i < menu->privileged_count; ++i) if (fprintf(stream, "accept-privileged %s\n", menu->accepted_privileged[i]) < 0) return 0; for (i = 0; i < menu->source_count; ++i) if (fprintf(stream, "source %.64s %s\n", menu->sources[i], menu->sources[i] + 65) < 0) return 0; if (menu->disk_image && (fputs("[disk]\nimage ", stream) == EOF || !quote(stream, menu->disk_image) || fputs("\nlayout gpt-ext4\n", stream) == EOF)) return 0; return 1; } static int menu_config_file(const char *path, const struct menu_state *menu) { size_t size = strlen(path); char *temporary; FILE *stream; int fd, ok; if (size > (size_t)-1 - 12) return 0; temporary = malloc(size + 12); if (!temporary) return 0; snprintf(temporary, size + 12, "%s.XXXXXX", path); fd = mkstemp(temporary); if (fd < 0) { free(temporary); return 0; } stream = fdopen(fd, "w"); if (!stream) { close(fd); unlink(temporary); free(temporary); return 0; } ok = menu_write(stream, menu); if (fflush(stream) || fsync(fd)) ok = 0; if (fclose(stream)) ok = 0; if (ok) ok = !rename(temporary, path) && sync_parent(path); if (!ok) unlink(temporary); free(temporary); return ok; } static int menu_line(const char *prompt, char **answer) { size_t capacity = 0; ssize_t n; fputs(prompt, stdout); if (fflush(stdout)) return 0; n = getline(answer, &capacity, stdin); if (n < 0) return 0; if (n && (*answer)[n - 1] == '\n') (*answer)[n - 1] = 0; return 1; } static void menu_path(const char *path) { const unsigned char *p = (const unsigned char *)path; for (; *p; ++p) if (*p < 32 || *p == 127) printf("\\x%02x", *p); else putchar(*p); } static int menu_packages(struct menu_state *menu, int *dirty) { for (;;) { char *answer = NULL; size_t i; int found = 0; puts("Packages: first entry is the explicit root package"); for (i = 0; i < menu->count; ++i) { size_t j, k, s; for (j = 0; j < menu->accept_count; ++j) if (!strcmp(menu->artifacts[i], menu->accepted_arch[j])) break; for (k = 0; k < menu->privileged_count; ++k) if (!strcmp(menu->artifacts[i], menu->accepted_privileged[k])) break; for (s = 0; s < menu->source_count; ++s) if (!strncmp(menu->artifacts[i], menu->sources[s], 64)) break; printf("%zu %s%s%s%s\n", i + 1, menu->artifacts[i], j < menu->accept_count ? " [arch override]" : "", k < menu->privileged_count ? " [setuid approved]" : "", s < menu->source_count ? " [source assigned]" : ""); } if (!menu_line("a add, d delete, x arch override, p setuid approval, s source, b back > ", &answer)) { free(answer); return 0; } if (!strcmp(answer, "b")) { free(answer); return 1; } if (!strcmp(answer, "a")) { char *digest = NULL, **next; free(answer); if (!menu_line("SHA-256 > ", &digest)) { free(digest); return 0; } if (!digest_valid(digest) || menu->count >= 1024) { puts("Invalid SHA-256 or package limit reached"); free(digest); continue; } for (i = 0; i < menu->count; ++i) if (!strcmp(menu->artifacts[i], digest)) { found = 1; break; } if (found) { puts("Already selected"); free(digest); continue; } next = realloc(menu->artifacts, (menu->count + 1) * sizeof *next); if (!next) { free(digest); return 0; } menu->artifacts = next; menu->artifacts[menu->count++] = digest; *dirty = 1; continue; } if (!strcmp(answer, "d") || !strcmp(answer, "x") || !strcmp(answer, "p") || !strcmp(answer, "s")) { char *number = NULL, *end; unsigned long long index; int remove = !strcmp(answer, "d"); int source_choice = !strcmp(answer, "s"); char ***accepted = !strcmp(answer, "p") ? &menu->accepted_privileged : &menu->accepted_arch; size_t *accepted_count = !strcmp(answer, "p") ? &menu->privileged_count : &menu->accept_count; free(answer); if (!menu_line("Number > ", &number)) { free(number); return 0; } errno = 0; index = strtoull(number, &end, 10); if (errno || !number[0] || *end || !index || index > menu->count) { puts("Invalid number"); free(number); continue; } if (source_choice) { char *id = NULL, *binding = NULL; size_t j; free(number); if (!menu_line("Source ID SHA-256 (empty clears) > ", &id)) { free(id); return 0; } if (*id && !(binding = source_binding(menu->artifacts[index - 1], id))) { puts("Invalid source ID"); free(id); continue; } free(id); for (j = 0; j < menu->source_count; ++j) if (!strncmp(menu->sources[j], menu->artifacts[index - 1], 64)) break; if (j < menu->source_count) { free(menu->sources[j]); for (++j; j < menu->source_count; ++j) menu->sources[j - 1] = menu->sources[j]; --menu->source_count; } if (binding) { char **next = realloc(menu->sources, (menu->source_count + 1) * sizeof *next); if (!next) { free(binding); return 0; } menu->sources = next; menu->sources[menu->source_count++] = binding; } *dirty = 1; continue; } for (i = 0; i < *accepted_count; ++i) if (!strcmp((*accepted)[i], menu->artifacts[index - 1])) break; if (i < *accepted_count) { free((*accepted)[i]); for (++i; i < *accepted_count; ++i) (*accepted)[i - 1] = (*accepted)[i]; --*accepted_count; } else if (!remove) { char **next = realloc(*accepted, (*accepted_count + 1) * sizeof *next); if (!next) { free(number); return 0; } *accepted = next; (*accepted)[*accepted_count] = strdup(menu->artifacts[index - 1]); if (!(*accepted)[*accepted_count]) { free(number); return 0; } ++*accepted_count; } if (remove) { for (i = 0; i < menu->source_count; ++i) if (!strncmp(menu->sources[i], menu->artifacts[index - 1], 64)) break; if (i < menu->source_count) { free(menu->sources[i]); for (++i; i < menu->source_count; ++i) menu->sources[i - 1] = menu->sources[i]; --menu->source_count; } char **other = menu->accepted_privileged; size_t *other_count = &menu->privileged_count; if (accepted == &menu->accepted_privileged) { other = menu->accepted_arch; other_count = &menu->accept_count; } for (i = 0; i < *other_count; ++i) if (!strcmp(other[i], menu->artifacts[index - 1])) break; if (i < *other_count) { free(other[i]); for (++i; i < *other_count; ++i) other[i - 1] = other[i]; --*other_count; } free(menu->artifacts[index - 1]); for (i = (size_t)index; i < menu->count; ++i) menu->artifacts[i - 1] = menu->artifacts[i]; --menu->count; } *dirty = 1; free(number); continue; } puts("Choose a, d, x, p, s or b"); free(answer); } } static int menu_prepare(const char *plan_path, const char *binary, const struct menu_state *menu, int save_plan) { struct holy_config config = {0}; struct install_input input = {0}; char temporary[] = "/tmp/holyinstall-menu-XXXXXX"; char *output = NULL, hash[65]; FILE *stream; int fd, rc, ok; fd = mkstemp(temporary); if (fd < 0) return 1; stream = fdopen(fd, "w"); if (!stream) { close(fd); unlink(temporary); return 1; } ok = menu_write(stream, menu); if (fflush(stream) || fsync(fd)) ok = 0; if (fclose(stream)) ok = 0; if (!ok) { unlink(temporary); return 1; } rc = load_input(temporary, &config, &input); unlink(temporary); if (rc) goto done; rc = run_package_manager(binary, &input, NULL, &output); if (rc) goto done; if (!set_hash(output, hash)) { rc = 1; goto done; } if (fputs(output, stdout) == EOF) { rc = 1; goto done; } if (save_plan) { rc = write_plan(plan_path, &input, hash); if (!rc) printf("holyinstall plan %s set %s\n", plan_path, hash); } done: free(output); free_input(&input); holy_config_free(&config); return rc; } static int menu_disk_plan(const char *plan_path, const struct menu_state *menu) { char temporary[] = "/tmp/holyinstall-disk-menu-XXXXXX"; char *args[] = {"plan", "--config", temporary, "--output", (char *)plan_path}; FILE *stream; int fd, ok, rc; if (!menu->disk_image) { puts("Select a disk image first"); return 3; } fd = mkstemp(temporary); if (fd < 0) return 1; stream = fdopen(fd, "w"); if (!stream) { close(fd); unlink(temporary); return 1; } ok = menu_write(stream, menu); if (fflush(stream) || fsync(fd)) ok = 0; if (fclose(stream)) ok = 0; if (!ok) { unlink(temporary); return 1; } rc = holy_disk_main(5, args); unlink(temporary); return rc; } static int menu_disk_apply(const char *plan_path, const struct menu_state *menu) { char *show[] = {"show", "--plan", (char *)plan_path}; char *apply[] = {"apply", "--plan", (char *)plan_path, "--confirm", NULL}; char *answer = NULL, *canonical = NULL; int rc; if (!menu->disk_image) { puts("Select a disk image first"); return 3; } rc = holy_disk_main(3, show); if (rc) return rc; canonical = realpath(menu->disk_image, NULL); if (!canonical) return 6; if (!menu_line("Type the exact disk image path to erase > ", &answer)) { free(canonical); free(answer); return 3; } if (strcmp(answer, canonical)) { puts("Confirmation does not match the selected image"); free(canonical); free(answer); return 3; } apply[4] = answer; rc = holy_disk_main(5, apply); free(canonical); free(answer); return rc; } static int menu_run(const char *config_path, const char *plan_path, const char *binary) { struct menu_state menu = {0}; char *disk_plan_path; int rc, prepared = 0, dirty = access(config_path, F_OK) != 0; if (!isatty(STDIN_FILENO) || !isatty(STDOUT_FILENO)) { fputs("holyinstall: menu requires a terminal\n", stderr); return 3; } if (strlen(plan_path) > (size_t)-1 - 6) return 2; disk_plan_path = malloc(strlen(plan_path) + 6); if (!disk_plan_path) return 1; sprintf(disk_plan_path, "%s.disk", plan_path); rc = menu_load(config_path, &menu); if (rc) goto done; for (;;) { char *answer = NULL; fputs("\nHoly installer: prepared root package stage\n1 Target root: ", stdout); menu_path(menu.root ? menu.root : "unset"); printf("\n2 Packages: %zu\nDisk image: ", menu.count); menu_path(menu.disk_image ? menu.disk_image : "unset"); printf("\nConfig: %s\n" "3 Preview packages\n4 Save config\n5 Prepare package plan\n" "6 Install prepared packages\n7 Abort\n" "8 Select disk image\n9 Prepare disk plan\n10 Apply disk plan\n", dirty ? "modified" : "saved"); if (!menu_line("Choice > ", &answer)) { free(answer); rc = 0; break; } if (!strcmp(answer, "1")) { char *root = NULL; free(answer); if (!menu_line("Target root > ", &root)) { free(root); rc = 0; break; } if (root[0]) { free(menu.root); menu.root = root; dirty = 1; prepared = 0; } else free(root); continue; } if (!strcmp(answer, "2")) { int changed = 0; free(answer); if (!menu_packages(&menu, &changed)) { rc = 0; break; } if (changed) { dirty = 1; prepared = 0; } continue; } if (!strcmp(answer, "3")) { free(answer); rc = menu_prepare(plan_path, binary, &menu, 0); if (rc) printf("Preview failed (status %d)\n", rc); continue; } if (!strcmp(answer, "4")) { free(answer); if (!menu_config_file(config_path, &menu)) puts("Save failed"); else { dirty = 0; puts("Config saved"); } continue; } if (!strcmp(answer, "5")) { free(answer); rc = menu_prepare(plan_path, binary, &menu, 1); prepared = rc == 0; if (rc) printf("Plan failed (status %d)\n", rc); continue; } if (!strcmp(answer, "6")) { struct install_input input = {0}; char hash[65], *confirm = NULL, *output = NULL; free(answer); if (!prepared) { puts("Prepare and review a plan first"); continue; } rc = check_plan(plan_path, &input, hash); if (rc) { free_input(&input); printf("Plan failed (status %d)\n", rc); prepared = 0; continue; } fputs("Root ", stdout); menu_path(input.root); printf("\nSet %s\nArtifacts %zu\nArchitecture overrides %zu\nSetuid approvals %zu\n", hash, input.count, input.accept_count, input.privileged_count); for (size_t j = 0; j < input.accept_count; ++j) printf("accept-arch %s\n", input.accepted_arch[j]); for (size_t j = 0; j < input.privileged_count; ++j) printf("accept-privileged %s\n", input.accepted_privileged[j]); if (!menu_line("Type yes to install > ", &confirm)) { free(confirm); free_input(&input); rc = 0; break; } if (!strcmp(confirm, "yes")) { rc = run_package_manager(binary, &input, hash, &output); if (!rc) { fputs(output, stdout); puts("Package transaction complete"); } else printf("Install failed (status %d)\n", rc); prepared = 0; } free(confirm); free(output); free_input(&input); continue; } if (!strcmp(answer, "7")) { free(answer); rc = 0; break; } if (!strcmp(answer, "8")) { char *image = NULL; free(answer); if (!menu_line("Disk image (blank clears selection) > ", &image)) { free(image); rc = 0; break; } free(menu.disk_image); menu.disk_image = *image ? image : NULL; if (!*image) free(image); dirty = 1; prepared = 0; continue; } if (!strcmp(answer, "9")) { free(answer); rc = menu_disk_plan(disk_plan_path, &menu); if (rc) printf("Disk plan failed (status %d)\n", rc); else printf("Disk plan %s ready for review\n", disk_plan_path); continue; } if (!strcmp(answer, "10")) { free(answer); rc = menu_disk_apply(disk_plan_path, &menu); if (rc) printf("Disk apply failed (status %d)\n", rc); else puts("Disk image prepared"); continue; } puts("Choose 1 through 10, or 7 to abort"); free(answer); } done: free_menu(&menu); free(disk_plan_path); return rc; } int main(int argc, char **argv) { const char *config_path = NULL, *plan_path = NULL; const char *binary = "/usr/bin/holypkg"; struct holy_config config = {0}; struct install_input input = {0}; char hash[65], *output = NULL; int i, apply = 0, menu = 0, rc; if (argc > 1 && !strcmp(argv[1], "disk")) return holy_disk_main(argc - 2, argv + 2); for (i = 1; i < argc; ++i) { if (!strcmp(argv[i], "--config") && ++i < argc && !config_path) config_path = argv[i]; else if (!strcmp(argv[i], "--plan") && ++i < argc && !plan_path && !apply) plan_path = argv[i]; else if (!strcmp(argv[i], "--apply") && ++i < argc && !plan_path) { plan_path = argv[i]; apply = 1; } else if (!strcmp(argv[i], "--menu") && !menu) { menu = 1; } else if (!strcmp(argv[i], "--holypkg") && ++i < argc) binary = argv[i]; else goto usage; } if (!plan_path || (apply ? config_path != NULL || menu : config_path == NULL)) goto usage; if (menu) return menu_run(config_path, plan_path, binary); if (apply) { rc = check_plan(plan_path, &input, hash); if (rc) goto done; rc = run_package_manager(binary, &input, hash, &output); if (!rc && fputs(output, stdout) == EOF) rc = 1; } else { rc = load_input(config_path, &config, &input); if (rc) goto done; rc = run_package_manager(binary, &input, NULL, &output); if (rc) goto done; if (!set_hash(output, hash)) { rc = 1; goto done; } if (fputs(output, stdout) == EOF) { rc = 1; goto done; } rc = write_plan(plan_path, &input, hash); if (!rc) printf("holyinstall plan %s set %s\n", plan_path, hash); } done: free(output); free_input(&input); holy_config_free(&config); return rc; usage: fputs("usage: holyinstall [--menu] --config FILE --plan NEW_FILE [--holypkg FILE] | --apply PLAN [--holypkg FILE]\n", stderr); return 2; }