#define _XOPEN_SOURCE 700 #define _FILE_OFFSET_BITS 64 #include "config.h" #include "disk.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include enum { sector = 512, sample = 1048576, esp_start = 4096, esp_sectors = 524288, root_start = 528384 }; struct disk_plan { char *image; uintmax_t device, inode, size, root_sectors; char head[65], tail[65]; int block; uintmax_t rdev; char serial[128]; char tools[4][65]; }; static const char *const block_tools[] = { "/usr/bin/sfdisk", "/usr/bin/mkfs.fat", "/usr/bin/mke2fs", "/usr/bin/limine" }; static const char *const block_tool_keys[] = { "sfdisk-sha256", "mkfs-fat-sha256", "mke2fs-sha256", "limine-sha256" }; static const char *value(const struct holy_config *c, const char *section, const char *key) { size_t i; for (i = 0; i < c->count; ++i) if (!strcmp(c->entries[i].section, section) && !strcmp(c->entries[i].key, key)) return c->entries[i].values[0]; return NULL; } static int number(const char *text, uintmax_t *out) { char *end; if (!text || !*text || *text == '-' || *text == '+') return 0; errno = 0; *out = strtoumax(text, &end, 10); return !errno && !*end; } static int hash_region(int fd, off_t offset, char out[65]) { unsigned char data[16384], digest[32]; static const char digits[] = "0123456789abcdef"; EVP_MD_CTX *ctx = EVP_MD_CTX_new(); size_t total = 0, i; unsigned length = 0; int ok = ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1; while (ok && total < sample) { ssize_t got = pread(fd, data, sizeof data, offset + (off_t)total); if (got < 0 && errno == EINTR) continue; if (got <= 0) { ok = 0; break; } ok = EVP_DigestUpdate(ctx, data, (size_t)got) == 1; total += (size_t)got; } if (ok) ok = EVP_DigestFinal_ex(ctx, digest, &length) == 1 && length == 32; EVP_MD_CTX_free(ctx); if (!ok) return 0; for (i = 0; i < 32; ++i) { out[2*i] = digits[digest[i] >> 4]; out[2*i+1] = digits[digest[i] & 15]; } out[64] = 0; return 1; } static int hash_span(int fd, uintmax_t offset, uintmax_t size, char out[65]) { unsigned char data[1048576], digest[32]; static const char digits[] = "0123456789abcdef"; EVP_MD_CTX *ctx = EVP_MD_CTX_new(); unsigned length = 0; size_t i; int ok = ctx && EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) == 1; while (ok && size) { size_t want = size < sizeof data ? (size_t)size : sizeof data; ssize_t got = pread(fd, data, want, (off_t)offset); if (got < 0 && errno == EINTR) continue; if (got <= 0) { ok = 0; break; } ok = EVP_DigestUpdate(ctx, data, (size_t)got) == 1; offset += (uintmax_t)got; size -= (uintmax_t)got; } if (ok) ok = EVP_DigestFinal_ex(ctx, digest, &length) == 1 && length == 32; EVP_MD_CTX_free(ctx); if (!ok) return 0; for (i = 0; i < 32; ++i) { out[2*i] = digits[digest[i] >> 4]; out[2*i+1] = digits[digest[i] & 15]; } out[64] = 0; return 1; } static int image_facts(int fd, struct disk_plan *p) { struct stat st; if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < (off_t)(1ULL << 30) || st.st_size % sector) return 0; p->device = (uintmax_t)st.st_dev; p->inode = (uintmax_t)st.st_ino; p->size = (uintmax_t)st.st_size; p->root_sectors = ((p->size / sector - root_start - 34) / 2048) * 2048; return p->root_sectors && hash_region(fd, 0, p->head) && hash_region(fd, st.st_size - sample, p->tail); } static int sysfs_serial(dev_t device, char out[128]) { char path[128]; const char *suffixes[] = { "/serial", "/device/serial" }; size_t i; for (i = 0; i < sizeof suffixes / sizeof *suffixes; ++i) { FILE *input; size_t length, j; int complete; int n = snprintf(path, sizeof path, "/sys/dev/block/%u:%u%s", major(device), minor(device), suffixes[i]); if (n < 0 || (size_t)n >= sizeof path) return 0; input = fopen(path, "r"); if (!input) continue; if (!fgets(out, 128, input)) { fclose(input); continue; } complete = strchr(out, '\n') != NULL || feof(input); fclose(input); if (!complete) return 0; length = strlen(out); while (length && (out[length - 1] == '\n' || out[length - 1] == '\r')) out[--length] = 0; if (length) { for (j = 0; j < length; ++j) if ((unsigned char)out[j] < 32 || (unsigned char)out[j] > 126) return 0; return 1; } } return 0; } static int block_facts(int fd, struct disk_plan *p) { struct stat st; unsigned long long bytes; int logical; char partition[128]; int n; if (fstat(fd, &st) || !S_ISBLK(st.st_mode) || ioctl(fd, (int)BLKGETSIZE64, &bytes) || ioctl(fd, BLKSSZGET, &logical) || logical != sector || bytes < (1ULL << 30) || bytes > INT64_MAX || bytes % sector) return 0; n = snprintf(partition, sizeof partition, "/sys/dev/block/%u:%u/partition", major(st.st_rdev), minor(st.st_rdev)); if (n < 0 || (size_t)n >= sizeof partition || !access(partition, F_OK)) return 0; p->device = (uintmax_t)st.st_dev; p->inode = (uintmax_t)st.st_ino; p->rdev = (uintmax_t)st.st_rdev; p->size = (uintmax_t)bytes; p->root_sectors = ((p->size / sector - root_start - 34) / 2048) * 2048; return p->root_sectors && sysfs_serial(st.st_rdev, p->serial) && hash_region(fd, 0, p->head) && hash_region(fd, (off_t)(p->size - sample), p->tail); } static int tool_hash(const char *path, char out[65]) { struct stat st; int fd = open(path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); int ok = fd >= 0 && !fstat(fd, &st) && S_ISREG(st.st_mode) && st.st_size > 0 && (uintmax_t)st.st_size <= SIZE_MAX && hash_span(fd, 0, (uintmax_t)st.st_size, out); if (fd >= 0) close(fd); return ok; } static int quote(FILE *f, const char *s) { const unsigned char *p = (const unsigned char *)s; if (fputc('"', f) == EOF) return 0; for (; *p; ++p) { if (*p == '"' || *p == '\\') { if (fputc('\\', f) == EOF || fputc(*p, f) == EOF) return 0; } else if (*p < 32 || *p == 127) { if (fprintf(f, "\\x%02x", *p) < 0) return 0; } else if (fputc(*p, f) == EOF) return 0; } return fputc('"', f) != EOF; } static int sync_parent(const char *path) { char *copy = strdup(path), *slash; int fd, ok; if (!copy) return 0; slash = strrchr(copy, '/'); if (slash) { if (slash == copy) slash[1] = 0; else *slash = 0; } else { free(copy); copy = strdup("."); if (!copy) return 0; } fd = open(copy, O_RDONLY | O_DIRECTORY | O_CLOEXEC); free(copy); if (fd < 0) return 0; ok = !fsync(fd); if (close(fd)) ok = 0; return ok; } static int write_plan(const char *path, const struct disk_plan *p) { int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); FILE *f; int ok; if (fd < 0) { perror("holyinstall: plan"); return 1; } f = fdopen(fd, "w"); if (!f) { close(fd); unlink(path); return 1; } ok = fprintf(f, "[disk-plan]\nformat %d\nimage ", p->block ? 2 : 1) >= 0 && quote(f, p->image) && fprintf(f, "\ndevice %" PRIuMAX "\ninode %" PRIuMAX "\nsize %" PRIuMAX "\nroot-sectors %" PRIuMAX "\nhead-sha256 %s\ntail-sha256 %s\n", p->device, p->inode, p->size, p->root_sectors, p->head, p->tail) >= 0; if (ok && p->block) { size_t i; ok = fputs("kind block\nserial ", f) >= 0 && quote(f, p->serial) && fprintf(f, "\nrdev %" PRIuMAX "\n", p->rdev) >= 0; for (i = 0; ok && i < 4; ++i) ok = fprintf(f, "%s %s\n", block_tool_keys[i], p->tools[i]) >= 0; } if (fflush(f) || fsync(fd)) ok = 0; if (fclose(f)) ok = 0; if (ok) ok = sync_parent(path); if (!ok) { unlink(path); return 1; } return 0; } static int read_plan(const char *path, struct disk_plan *p) { struct holy_config c = {0}; const char *s; char *error = NULL; size_t i; int ok = holy_config_load_plan(path, &c, &error); if (!ok) { fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid plan"); free(error); return 2; } s = value(&c, "disk-plan", "format"); p->block = s && !strcmp(s, "2"); ok = s && (!strcmp(s, "1") || p->block) && c.count == (size_t)(p->block ? 15 : 8); for (i = 0; i < c.count; ++i) if (strcmp(c.entries[i].section, "disk-plan")) ok = 0; s = value(&c, "disk-plan", "image"); if (ok && s) p->image = strdup(s); else ok = 0; ok = ok && p->image && p->image[0] == '/'; ok = ok && number(value(&c, "disk-plan", "device"), &p->device) && number(value(&c, "disk-plan", "inode"), &p->inode) && number(value(&c, "disk-plan", "size"), &p->size) && number(value(&c, "disk-plan", "root-sectors"), &p->root_sectors); s = value(&c, "disk-plan", "head-sha256"); if (ok && s && strlen(s) == 64) memcpy(p->head, s, 65); else ok = 0; s = value(&c, "disk-plan", "tail-sha256"); if (ok && s && strlen(s) == 64) memcpy(p->tail, s, 65); else ok = 0; for (i = 0; ok && i < 64; ++i) if (!((p->head[i] >= '0' && p->head[i] <= '9') || (p->head[i] >= 'a' && p->head[i] <= 'f')) || !((p->tail[i] >= '0' && p->tail[i] <= '9') || (p->tail[i] >= 'a' && p->tail[i] <= 'f'))) ok = 0; if (ok && p->block) { s = value(&c, "disk-plan", "kind"); ok = s && !strcmp(s, "block") && !strncmp(p->image, "/dev/", 5) && number(value(&c, "disk-plan", "rdev"), &p->rdev); s = value(&c, "disk-plan", "serial"); if (!s || !*s || strlen(s) >= sizeof p->serial) ok = 0; else { strcpy(p->serial, s); for (i = 0; i < strlen(s); ++i) if ((unsigned char)s[i] < 32 || (unsigned char)s[i] > 126) ok = 0; } for (i = 0; i < 4; ++i) { size_t j; s = value(&c, "disk-plan", block_tool_keys[i]); if (!s || strlen(s) != 64) { ok = 0; continue; } memcpy(p->tools[i], s, 65); for (j = 0; j < 64; ++j) if (!((s[j] >= '0' && s[j] <= '9') || (s[j] >= 'a' && s[j] <= 'f'))) ok = 0; } } if (ok) ok = p->size >= (1ULL << 30) && p->size % sector == 0 && p->root_sectors == ((p->size / sector - root_start - 34) / 2048) * 2048; holy_config_free(&c); if (!ok) { fputs("holyinstall: invalid disk plan\n", stderr); return 2; } return 0; } static int child(const char *program, char *const args[], const char *input, int image_fd) { pid_t pid; int status, pipefd[2] = {-1, -1}; if (input && pipe(pipefd)) return 1; pid = fork(); if (pid < 0) { if (pipefd[0] >= 0) { close(pipefd[0]); close(pipefd[1]); } return 1; } if (!pid) { if (image_fd >= 0 && (dup2(image_fd, 9) < 0 || fcntl(9, F_SETFD, 0) < 0)) _exit(127); if (input && (dup2(pipefd[0], 0) < 0)) _exit(127); if (pipefd[0] >= 0) close(pipefd[0]); if (pipefd[1] >= 0) close(pipefd[1]); execv(program, args); _exit(127); } if (input) { size_t len = strlen(input), used = 0; close(pipefd[0]); while (used < len) { ssize_t n = write(pipefd[1], input + used, len - used); if (n < 0 && errno == EINTR) continue; if (n <= 0) break; used += (size_t)n; } close(pipefd[1]); if (used != len) { waitpid(pid, &status, 0); return 1; } } while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) return 1; return WIFEXITED(status) ? WEXITSTATUS(status) : 1; } static int loop_status(const char *image) { int pipefd[2], status; pid_t pid; char byte; ssize_t got; char *const args[] = {"losetup", "-j", (char *)image, NULL}; if (access("/sbin/losetup", X_OK) || pipe(pipefd)) return -1; pid = fork(); if (pid < 0) { close(pipefd[0]); close(pipefd[1]); return -1; } if (!pid) { close(pipefd[0]); if (dup2(pipefd[1], STDOUT_FILENO) < 0) _exit(127); close(pipefd[1]); execv("/sbin/losetup", args); _exit(127); } close(pipefd[1]); do { got = read(pipefd[0], &byte, 1); } while (got < 0 && errno == EINTR); close(pipefd[0]); while (waitpid(pid, &status, 0) < 0) if (errno != EINTR) return -1; if (got < 0 || !WIFEXITED(status) || WEXITSTATUS(status)) return -1; return got ? 1 : 0; } static int journal(const char *path, const char *stage, int first) { int fd = open(path, O_WRONLY | O_APPEND | O_NOFOLLOW | O_CLOEXEC | (first ? O_CREAT | O_EXCL : 0), 0600); size_t len = strlen(stage); int ok; if (fd < 0) return 0; ok = write(fd, stage, len) == (ssize_t)len && write(fd, "\n", 1) == 1 && !fsync(fd); if (close(fd)) ok = 0; if (ok && first) ok = sync_parent(path); return ok; } static int disk_plan(const char *config_path, const char *plan_path) { struct holy_config c = {0}; struct disk_plan p = {0}; const char *image, *device, *layout; char *error = NULL; int fd, rc = 1; if (!holy_config_load(config_path, &c, &error)) { fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid config"); free(error); return 2; } image = value(&c, "disk", "image"); device = value(&c, "disk", "device"); layout = value(&c, "disk", "layout"); if (!!image == !!device || !layout || strcmp(layout, "gpt-ext4")) { rc = 2; goto done; } p.block = device != NULL; p.image = realpath(p.block ? device : image, NULL); if (!p.image) { perror("holyinstall: disk path"); rc = 6; goto done; } if (p.block && strncmp(p.image, "/dev/", 5)) { rc = 2; goto done; } fd = open(p.image, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || !(p.block ? block_facts(fd, &p) : image_facts(fd, &p))) { fputs(p.block ? "holyinstall: device requires 512-byte sectors, a serial and at least 1 GiB\n" : "holyinstall: image must be a regular file of at least 1 GiB\n", stderr); if (fd >= 0) close(fd); rc = 6; goto done; } close(fd); if (!p.block && loop_status(p.image)) { fputs("holyinstall: image is attached to a loop device or losetup is unavailable\n", stderr); rc = 6; goto done; } if (p.block) { size_t i; for (i = 0; i < 4; ++i) if (!tool_hash(block_tools[i], p.tools[i])) { fprintf(stderr, "holyinstall: missing tool %s\n", block_tools[i]); rc = 6; goto done; } } printf("disk %s %s\nidentity %" PRIuMAX ":%" PRIuMAX " size %" PRIuMAX "\nGPT: BIOS 2048+2048; ESP %d+%d FAT32; root %d+%" PRIuMAX " ext4\nall existing data on this %s will be destroyed\n", p.block ? "device" : "image", p.image, p.device, p.inode, p.size, esp_start, esp_sectors, root_start, p.root_sectors, p.block ? "device" : "image"); if (p.block) printf("serial %s\nrdev %" PRIuMAX "\n", p.serial, p.rdev); rc = write_plan(plan_path, &p); done: free(p.image); holy_config_free(&c); return rc; } static int disk_show(const char *plan_path) { struct disk_plan p = {0}; int rc = read_plan(plan_path, &p); if (!rc) printf("disk %s %s\nidentity %" PRIuMAX ":%" PRIuMAX " size %" PRIuMAX "\nGPT: BIOS 2048+2048; ESP %d+%d FAT32; " "root %d+%" PRIuMAX " ext4\nhead-sha256 %s\n" "tail-sha256 %s\nall existing data on this %s will be destroyed\n", p.block ? "device" : "image", p.image, p.device, p.inode, p.size, esp_start, esp_sectors, root_start, p.root_sectors, p.head, p.tail, p.block ? "device" : "image"); if (!rc && p.block) { size_t i; printf("serial %s\nrdev %" PRIuMAX "\n", p.serial, p.rdev); for (i = 0; i < 4; ++i) printf("%s %s\n", block_tool_keys[i], p.tools[i]); } free(p.image); return rc; } static char *partition_path(const char *disk, unsigned index) { size_t length = strlen(disk); int suffix = length && disk[length - 1] >= '0' && disk[length - 1] <= '9'; char *path = malloc(length + (size_t)suffix + 2); if (path) snprintf(path, length + (size_t)suffix + 2, "%s%s%u", disk, suffix ? "p" : "", index); return path; } static int sysfs_number(const char *directory, const char *name, uintmax_t *number_out) { char *path; FILE *input; char text[64]; size_t length = strlen(directory) + strlen(name) + 2; int ok; path = malloc(length); if (!path) return 0; snprintf(path, length, "%s/%s", directory, name); input = fopen(path, "r"); free(path); if (!input) return 0; ok = fgets(text, sizeof text, input) && (strchr(text, '\n') || feof(input)); fclose(input); if (!ok) return 0; text[strcspn(text, "\r\n")] = 0; return number(text, number_out); } static int partition_matches(const char *path, dev_t disk_device, unsigned index, uintmax_t start, uintmax_t sectors) { struct stat st; char disk_link[128], part_link[128], *disk_sys, *part_sys, *slash; uintmax_t actual_index, actual_start, actual_sectors; int n, ok = 0; if (stat(path, &st) || !S_ISBLK(st.st_mode)) return 0; n = snprintf(disk_link, sizeof disk_link, "/sys/dev/block/%u:%u", major(disk_device), minor(disk_device)); if (n < 0 || (size_t)n >= sizeof disk_link) return 0; n = snprintf(part_link, sizeof part_link, "/sys/dev/block/%u:%u", major(st.st_rdev), minor(st.st_rdev)); if (n < 0 || (size_t)n >= sizeof part_link) return 0; disk_sys = realpath(disk_link, NULL); part_sys = realpath(part_link, NULL); if (!disk_sys || !part_sys) goto done; ok = sysfs_number(part_sys, "partition", &actual_index) && sysfs_number(part_sys, "start", &actual_start) && sysfs_number(part_sys, "size", &actual_sectors) && actual_index == index && actual_start == start && actual_sectors == sectors; slash = strrchr(part_sys, '/'); if (!slash) ok = 0; else { *slash = 0; if (strcmp(part_sys, disk_sys)) ok = 0; } done: free(disk_sys); free(part_sys); return ok; } static int wait_partition(const char *path, dev_t disk_device, unsigned index, uintmax_t start, uintmax_t sectors) { const struct timespec delay = {0, 100000000}; unsigned attempt; for (attempt = 0; attempt < 300; ++attempt) { if (partition_matches(path, disk_device, index, start, sectors)) return 1; nanosleep(&delay, NULL); } return 0; } static int disk_apply_block(const char *plan_path, const char *confirm, const struct disk_plan *expected) { struct disk_plan actual = {0}; char *journal_path = NULL, *script = NULL, *esp = NULL, *root = NULL; int fd = -1, rc = 1, n; size_t i; char *const partition[] = {"sfdisk", "--no-reread", "--no-tell-kernel", "/proc/self/fd/9", NULL}; char *const verify[] = {"sfdisk", "--verify", "/proc/self/fd/9", NULL}; char *fat[] = {"mkfs.fat", "-F", "32", "-s", "4", "-n", "HOLYBOOT", NULL, NULL}; char *ext[] = {"mke2fs", "-q", "-t", "ext4", "-F", "-b", "4096", NULL, NULL}; char *const boot[] = {"limine", "bios-install", "/proc/self/fd/9", "1", NULL}; if (strcmp(confirm, expected->image)) { fputs("holyinstall: confirmation must match device path\n", stderr); return 3; } fd = open(expected->image, O_RDWR | O_EXCL | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || flock(fd, LOCK_EX | LOCK_NB) || !block_facts(fd, &actual)) { fputs("holyinstall: device is busy, changed or unavailable\n", stderr); rc = 6; goto done; } if (actual.device != expected->device || actual.inode != expected->inode || actual.rdev != expected->rdev || actual.size != expected->size || strcmp(actual.serial, expected->serial) || strcmp(actual.head, expected->head) || strcmp(actual.tail, expected->tail)) { fputs("holyinstall: device changed since plan\n", stderr); rc = 3; goto done; } for (i = 0; i < 4; ++i) { char hash[65]; if (!tool_hash(block_tools[i], hash) || strcmp(hash, expected->tools[i])) { fprintf(stderr, "holyinstall: tool changed since plan: %s\n", block_tools[i]); rc = 3; goto done; } } journal_path = malloc(strlen(plan_path) + 9); if (!journal_path) goto done; sprintf(journal_path, "%s.journal", plan_path); if (!access(journal_path, F_OK)) { fputs("holyinstall: disk journal exists; inspect before recovery\n", stderr); rc = 5; goto done; } esp = partition_path(expected->image, 2); root = partition_path(expected->image, 3); if (!esp || !root) goto done; fat[7] = esp; ext[7] = root; n = snprintf(NULL, 0, "label: gpt\nunit: sectors\nfirst-lba: 2048\n" "start=2048, size=2048, type=21686148-6449-6E6F-744E-656564454649\n" "start=4096, size=524288, type=U\n" "start=528384, size=%" PRIuMAX ", type=L\n", expected->root_sectors); if (n < 0) goto done; script = malloc((size_t)n + 1); if (!script) goto done; snprintf(script, (size_t)n + 1, "label: gpt\nunit: sectors\nfirst-lba: 2048\n" "start=2048, size=2048, type=21686148-6449-6E6F-744E-656564454649\n" "start=4096, size=524288, type=U\n" "start=528384, size=%" PRIuMAX ", type=L\n", expected->root_sectors); if (!journal(journal_path, "prepared", 1)) goto done; rc = 5; if (!journal(journal_path, "partitioning", 0) || child(block_tools[0], partition, script, fd) || fsync(fd) || ioctl(fd, BLKRRPART) || child(block_tools[0], verify, NULL, fd) || !wait_partition(esp, (dev_t)expected->rdev, 2, esp_start, esp_sectors) || !wait_partition(root, (dev_t)expected->rdev, 3, root_start, expected->root_sectors) || !journal(journal_path, "partitioned", 0)) goto done; close(fd); fd = -1; if (!journal(journal_path, "formatting-esp", 0) || child(block_tools[1], fat, NULL, -1) || !journal(journal_path, "formatted-esp", 0) || !journal(journal_path, "formatting-root", 0) || child(block_tools[2], ext, NULL, -1) || !journal(journal_path, "formatted-root", 0)) goto done; fd = open(expected->image, O_RDWR | O_EXCL | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || !journal(journal_path, "installing-bios", 0) || child(block_tools[3], boot, NULL, fd) || fsync(fd) || !journal(journal_path, "committed", 0)) goto done; puts("block device prepared with GPT, FAT32, ext4 and Limine BIOS stage"); rc = 0; done: if (rc == 5) fputs("holyinstall: device mutation may be partial; inspect journal and device\n", stderr); if (fd >= 0) close(fd); free(journal_path); free(script); free(esp); free(root); return rc; } static int disk_apply(const char *plan_path, const char *confirm) { struct disk_plan p = {0}, actual = {0}; char *journal_path = NULL, *script = NULL; char root_blocks[32], offset[64]; int fd = -1, rc = read_plan(plan_path, &p), n; char *const partition[] = {"sfdisk", "--no-reread", "--no-tell-kernel", "/proc/self/fd/9", NULL}; char *const verify[] = {"sfdisk", "--verify", "/proc/self/fd/9", NULL}; char *const fat[] = {"mkfs.fat", "-F", "32", "-s", "4", "--offset=4096", "-n", "HOLYBOOT", "/proc/self/fd/9", "262144", NULL}; char *const ext[] = {"mke2fs", "-q", "-t", "ext4", "-F", "-b", "4096", "-E", offset, "/proc/self/fd/9", root_blocks, NULL}; if (rc) goto done; if (p.block) { rc = disk_apply_block(plan_path, confirm, &p); goto done; } if (strcmp(confirm, p.image)) { fputs("holyinstall: confirmation must match image path\n", stderr); rc = 3; goto done; } fd = open(p.image, O_RDWR | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || flock(fd, LOCK_EX | LOCK_NB) || !image_facts(fd, &actual)) { rc = 6; goto done; } if (actual.device != p.device || actual.inode != p.inode || actual.size != p.size || strcmp(actual.head, p.head) || strcmp(actual.tail, p.tail)) { fputs("holyinstall: image changed since plan\n", stderr); rc = 3; goto done; } if (loop_status(p.image)) { fputs("holyinstall: image is attached to a loop device or losetup is unavailable\n", stderr); rc = 6; goto done; } journal_path = malloc(strlen(plan_path) + 9); if (!journal_path) { rc = 1; goto done; } sprintf(journal_path, "%s.journal", plan_path); if (access(journal_path, F_OK) == 0) { fputs("holyinstall: disk journal exists; inspect before recovery\n", stderr); rc = 5; goto done; } if (access("/usr/sbin/sfdisk", X_OK) || access("/sbin/mkfs.fat", X_OK) || access("/sbin/mke2fs", X_OK)) { fputs("holyinstall: sfdisk, mkfs.fat and mke2fs are required\n", stderr); rc = 6; goto done; } n = snprintf(NULL, 0, "label: gpt\nunit: sectors\nfirst-lba: 2048\n" "start=2048, size=2048, type=21686148-6449-6E6F-744E-656564454649\n" "start=4096, size=524288, type=U\n" "start=528384, size=%" PRIuMAX ", type=L\n", p.root_sectors); script = malloc((size_t)n + 1); if (!script) { rc = 1; goto done; } snprintf(script, (size_t)n + 1, "label: gpt\nunit: sectors\nfirst-lba: 2048\n" "start=2048, size=2048, type=21686148-6449-6E6F-744E-656564454649\n" "start=4096, size=524288, type=U\n" "start=528384, size=%" PRIuMAX ", type=L\n", p.root_sectors); snprintf(root_blocks, sizeof root_blocks, "%" PRIuMAX, p.root_sectors / 8); snprintf(offset, sizeof offset, "offset=%" PRIuMAX, (uintmax_t)root_start * sector); if (!journal(journal_path, "prepared", 1)) { rc = 1; goto done; } rc = 5; if (!journal(journal_path, "partitioning", 0) || child("/usr/sbin/sfdisk", partition, script, fd)) goto done; if (child("/usr/sbin/sfdisk", verify, NULL, fd)) goto done; if (!journal(journal_path, "partitioned", 0) || !journal(journal_path, "formatting-esp", 0) || child("/sbin/mkfs.fat", fat, NULL, fd)) goto done; if (!journal(journal_path, "formatted-esp", 0) || !journal(journal_path, "formatting-root", 0) || child("/sbin/mke2fs", ext, NULL, fd)) goto done; if (fsync(fd) || !journal(journal_path, "committed", 0)) goto done; puts("disk image prepared; mount and package installation remain separate steps"); rc = 0; done: if (rc == 5) fputs("holyinstall: disk mutation may be partial; inspect journal and image\n", stderr); if (fd >= 0) close(fd); free(script); free(journal_path); free(p.image); return rc; } struct final_file { char *path; uintmax_t device, inode, size; char digest[65]; }; struct final_plan { struct final_file disk, esp, root, limine; uintmax_t root_sectors; }; static int digest_text(const char *s) { size_t i; if (!s || 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 void final_free(struct final_plan *p) { free(p->disk.path); free(p->esp.path); free(p->root.path); free(p->limine.path); } static int final_facts(int fd, struct final_file *f) { struct stat st; if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0) return 0; f->device = (uintmax_t)st.st_dev; f->inode = (uintmax_t)st.st_ino; f->size = (uintmax_t)st.st_size; return hash_span(fd, 0, f->size, f->digest); } static int final_open(const char *path, struct final_file *f) { int fd; f->path = realpath(path, NULL); if (!f->path) return -1; fd = open(f->path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || !final_facts(fd, f)) { if (fd >= 0) close(fd); return -1; } close(fd); return 0; } static int final_entry(FILE *out, const char *name, const struct final_file *f) { return fprintf(out, "%s ", name) >= 0 && quote(out, f->path) && fprintf(out, "\n%s-device %" PRIuMAX "\n%s-inode %" PRIuMAX "\n%s-size %" PRIuMAX "\n%s-sha256 %s\n", name, f->device, name, f->inode, name, f->size, name, f->digest) >= 0; } static int final_write(const char *path, const struct final_plan *p) { int fd = open(path, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600); FILE *out; int ok; if (fd < 0) { perror("holyinstall: final plan"); return 1; } out = fdopen(fd, "w"); if (!out) { close(fd); unlink(path); return 1; } ok = fputs("[disk-finalize-plan]\nformat 1\n", out) >= 0 && final_entry(out, "disk", &p->disk) && final_entry(out, "esp", &p->esp) && final_entry(out, "root", &p->root) && final_entry(out, "limine", &p->limine) && fprintf(out, "root-sectors %" PRIuMAX "\n", p->root_sectors) >= 0; if (fflush(out) || fsync(fd)) ok = 0; if (fclose(out)) ok = 0; if (ok) ok = sync_parent(path); if (!ok) { unlink(path); return 1; } return 0; } static int final_read_file(const struct holy_config *c, const char *name, struct final_file *f) { const char *s; char key[64]; snprintf(key, sizeof key, "%s", name); s = value(c, "disk-finalize-plan", key); if (!s || s[0] != '/') return 0; f->path = strdup(s); if (!f->path) return 0; snprintf(key, sizeof key, "%s-device", name); if (!number(value(c, "disk-finalize-plan", key), &f->device)) return 0; snprintf(key, sizeof key, "%s-inode", name); if (!number(value(c, "disk-finalize-plan", key), &f->inode)) return 0; snprintf(key, sizeof key, "%s-size", name); if (!number(value(c, "disk-finalize-plan", key), &f->size)) return 0; snprintf(key, sizeof key, "%s-sha256", name); s = value(c, "disk-finalize-plan", key); if (!digest_text(s)) return 0; memcpy(f->digest, s, 65); return 1; } static int final_read(const char *path, struct final_plan *p) { struct holy_config c = {0}; char *error = NULL; const char *s; size_t i; int ok = holy_config_load_plan(path, &c, &error); if (!ok) { fprintf(stderr, "holyinstall: %s\n", error ? error : "invalid plan"); free(error); return 2; } ok = c.count == 22; for (i = 0; i < c.count; ++i) if (strcmp(c.entries[i].section, "disk-finalize-plan")) ok = 0; s = value(&c, "disk-finalize-plan", "format"); ok = ok && s && !strcmp(s, "1") && final_read_file(&c, "disk", &p->disk) && final_read_file(&c, "esp", &p->esp) && final_read_file(&c, "root", &p->root) && final_read_file(&c, "limine", &p->limine) && number(value(&c, "disk-finalize-plan", "root-sectors"), &p->root_sectors); if (ok) ok = p->disk.size >= (1ULL << 30) && p->disk.size % sector == 0 && p->root_sectors == ((p->disk.size / sector - root_start - 34) / 2048) * 2048 && p->esp.size == (uintmax_t)esp_sectors * sector && p->root.size == p->root_sectors * sector && p->limine.size > 0 && !strcmp(p->limine.path, "/usr/bin/limine"); holy_config_free(&c); if (!ok) { fputs("holyinstall: invalid disk finalize plan\n", stderr); return 2; } return 0; } static int final_match(int fd, const struct final_file *expected) { struct final_file actual = {0}; return final_facts(fd, &actual) && actual.device == expected->device && actual.inode == expected->inode && actual.size == expected->size && !strcmp(actual.digest, expected->digest); } static int final_copy(int source, int target, uintmax_t offset, uintmax_t size) { unsigned char buffer[1048576]; uintmax_t pos = 0; while (pos < size) { size_t want = size - pos < sizeof buffer ? (size_t)(size - pos) : sizeof buffer; ssize_t got = pread(source, buffer, want, (off_t)pos); size_t used = 0; if (got < 0 && errno == EINTR) continue; if (got <= 0) return 0; while (used < (size_t)got) { ssize_t n = pwrite(target, buffer + used, (size_t)got - used, (off_t)(offset + pos + used)); if (n < 0 && errno == EINTR) continue; if (n <= 0) return 0; used += (size_t)n; } pos += (uintmax_t)got; } return 1; } static uint64_t little64(const unsigned char *p) { unsigned i; uint64_t value = 0; for (i = 0; i < 8; ++i) value |= (uint64_t)p[i] << (i * 8); return value; } static int final_layout(int fd, uintmax_t root_sectors) { unsigned char header[512], entries[384]; uint64_t start[3] = {2048, esp_start, root_start}; uint64_t count[3] = {2048, esp_sectors, root_sectors}; size_t i; if (pread(fd, header, sizeof header, sector) != (ssize_t)sizeof header || memcmp(header, "EFI PART", 8) || little64(header + 72) != 2 || header[84] != 128 || header[85] || header[86] || header[87] || pread(fd, entries, sizeof entries, sector * 2) != (ssize_t)sizeof entries) return 0; for (i = 0; i < 3; ++i) if (little64(entries + i * 128 + 32) != start[i] || little64(entries + i * 128 + 40) != start[i] + count[i] - 1) return 0; return 1; } static int final_plan_make(const char *disk_plan_path, const char *esp, const char *root, const char *output) { struct disk_plan base = {0}, actual = {0}; struct final_plan p = {0}; char *journal_path = NULL; FILE *journal_file = NULL; char last[64] = {0}, line[64]; int fd = -1, rc = read_plan(disk_plan_path, &base); char *const verify[] = {"sfdisk", "--verify", "/proc/self/fd/9", NULL}; if (rc) goto done; journal_path = malloc(strlen(disk_plan_path) + 9); if (!journal_path) { rc = 1; goto done; } sprintf(journal_path, "%s.journal", disk_plan_path); { struct stat st; int journal_fd = open(journal_path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK); if (journal_fd < 0) { rc = 6; goto done; } if (fstat(journal_fd, &st) || !S_ISREG(st.st_mode) || st.st_size > 4096) { close(journal_fd); rc = 2; goto done; } journal_file = fdopen(journal_fd, "r"); if (!journal_file) { close(journal_fd); rc = 1; goto done; } } while (fgets(line, sizeof line, journal_file)) { if (!strchr(line, '\n')) { rc = 2; goto done; } memcpy(last, line, strlen(line) + 1); } if (strcmp(last, "committed\n")) { rc = 5; goto done; } fd = open(base.image, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (fd < 0 || !image_facts(fd, &actual) || actual.device != base.device || actual.inode != base.inode || actual.size != base.size || actual.root_sectors != base.root_sectors || loop_status(base.image) || !final_layout(fd, base.root_sectors) || child("/usr/sbin/sfdisk", verify, NULL, fd)) { rc = 6; goto done; } p.root_sectors = base.root_sectors; if (final_open(base.image, &p.disk) || final_open(esp, &p.esp) || final_open(root, &p.root) || final_open("/usr/bin/limine", &p.limine)) { rc = 6; goto done; } if (p.esp.size != (uintmax_t)esp_sectors * sector || p.root.size != p.root_sectors * sector) { rc = 2; goto done; } printf("disk %s\nesp %s sha256 %s\nroot %s sha256 %s\nlimine %s sha256 %s\n", p.disk.path, p.esp.path, p.esp.digest, p.root.path, p.root.digest, p.limine.path, p.limine.digest); rc = final_write(output, &p); done: if (journal_file) fclose(journal_file); if (fd >= 0) close(fd); free(journal_path); free(base.image); final_free(&p); return rc; } static int final_apply(const char *path, const char *confirm) { struct final_plan p = {0}; int disk = -1, esp = -1, root = -1, limine = -1; int rc = final_read(path, &p); char *journal_path = NULL; char check[65]; char *const install[] = {"limine", "bios-install", "/proc/self/fd/9", "1", NULL}; char *const verify[] = {"sfdisk", "--verify", "/proc/self/fd/9", NULL}; if (rc) goto done; if (strcmp(confirm, p.disk.path)) { rc = 3; goto done; } disk = open(p.disk.path, O_RDWR | O_NOFOLLOW | O_CLOEXEC); esp = open(p.esp.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); root = open(p.root.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); limine = open(p.limine.path, O_RDONLY | O_NOFOLLOW | O_CLOEXEC); if (disk < 0 || esp < 0 || root < 0 || limine < 0 || flock(disk, LOCK_EX | LOCK_NB)) { rc = 6; goto done; } if (!final_match(disk, &p.disk) || !final_match(esp, &p.esp) || !final_match(root, &p.root) || !final_match(limine, &p.limine)) { fputs("holyinstall: disk or source image changed since plan\n", stderr); rc = 3; goto done; } if (loop_status(p.disk.path)) { rc = 6; goto done; } if (access("/usr/sbin/sfdisk", X_OK) || access(p.limine.path, X_OK)) { fputs("holyinstall: sfdisk and Limine are required\n", stderr); rc = 6; goto done; } journal_path = malloc(strlen(path) + 9); if (!journal_path) { rc = 1; goto done; } sprintf(journal_path, "%s.journal", path); if (!journal(journal_path, "prepared", 1)) { rc = errno == EEXIST ? 5 : 1; goto done; } rc = 5; if (!journal(journal_path, "copying-esp", 0) || !final_copy(esp, disk, (uintmax_t)esp_start * sector, p.esp.size) || fsync(disk) || !hash_span(disk, (uintmax_t)esp_start * sector, p.esp.size, check) || strcmp(check, p.esp.digest) || !journal(journal_path, "copied-esp", 0)) goto done; if (!journal(journal_path, "copying-root", 0) || !final_copy(root, disk, (uintmax_t)root_start * sector, p.root.size) || fsync(disk) || !hash_span(disk, (uintmax_t)root_start * sector, p.root.size, check) || strcmp(check, p.root.digest) || !journal(journal_path, "copied-root", 0)) goto done; if (!journal(journal_path, "installing-limine", 0) || child("/usr/bin/limine", install, NULL, disk) || child("/usr/sbin/sfdisk", verify, NULL, disk) || fsync(disk) || !journal(journal_path, "committed", 0)) goto done; puts("disk image finalized"); rc = 0; done: if (rc == 5) fputs("holyinstall: disk finalize may be partial; inspect journal and image\n", stderr); if (disk >= 0) close(disk); if (esp >= 0) close(esp); if (root >= 0) close(root); if (limine >= 0) close(limine); free(journal_path); final_free(&p); return rc; } int holy_disk_main(int argc, char **argv) { if (argc == 3 && !strcmp(argv[0], "show") && !strcmp(argv[1], "--plan")) return disk_show(argv[2]); if (argc == 9 && !strcmp(argv[0], "finalize-plan") && !strcmp(argv[1], "--disk-plan") && !strcmp(argv[3], "--esp") && !strcmp(argv[5], "--root-image") && !strcmp(argv[7], "--output")) return final_plan_make(argv[2], argv[4], argv[6], argv[8]); if (argc == 5 && !strcmp(argv[0], "finalize-apply") && !strcmp(argv[1], "--plan") && !strcmp(argv[3], "--confirm")) return final_apply(argv[2], argv[4]); if (argc == 5 && !strcmp(argv[0], "plan") && !strcmp(argv[1], "--config") && !strcmp(argv[3], "--output")) return disk_plan(argv[2], argv[4]); if (argc == 5 && !strcmp(argv[0], "apply") && !strcmp(argv[1], "--plan") && !strcmp(argv[3], "--confirm")) return disk_apply(argv[2], argv[4]); fputs("usage: holyinstall disk show --plan PLAN | " "holyinstall disk plan --config FILE --output NEW_PLAN | " "holyinstall disk apply --plan PLAN --confirm IMAGE | " "holyinstall disk finalize-plan --disk-plan PLAN --esp FILE " "--root-image FILE --output NEW_PLAN | " "holyinstall disk finalize-apply --plan PLAN --confirm IMAGE\n", stderr); return 2; }