git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file src/disk.c

#define _XOPEN_SOURCE 700
#define _FILE_OFFSET_BITS 64
#include "config.h"
#include "disk.h"

#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <linux/fs.h>
#include <openssl/evp.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/file.h>
#include <sys/ioctl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <sys/sysmacros.h>
#include <time.h>
#include <unistd.h>

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;
}