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

file src/cache.c

#define _POSIX_C_SOURCE 200809L
#include "cache.h"
#include "deps.h"
#include "fetch.h"
#include "package.h"
#include "scan.h"
#include "stage.h"
#include "verify.h"
#include "provides.h"
#include "state.h"

#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <unistd.h>

static int cache_directory(const char *root_path, int create)
{
    static const char *const parts[] = {
        "var", "cache", "holypkg", "objects", "sha256"
    };
    struct stat st;
    size_t i;
    int current = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (current < 0) return -1;
    for (i = 0; i <= sizeof parts / sizeof *parts; ++i) {
        int next;
        if (fstat(current, &st) ||
            (st.st_uid != geteuid() && st.st_uid != 0) ||
            (st.st_mode & 0022)) {
            fprintf(stderr, "holypkg: unsafe cache directory owner or mode\n");
            close(current);
            return -1;
        }
        if (i == sizeof parts / sizeof *parts) return current;
        if (create) {
            if (mkdirat(current, parts[i], 0700)) {
                if (errno != EEXIST) { close(current); return -1; }
            } else if (fsync(current)) { close(current); return -1; }
        }
        next = openat(current, parts[i], O_RDONLY | O_DIRECTORY |
                      O_NOFOLLOW | O_CLOEXEC);
        close(current);
        if (next < 0) return -1;
        current = next;
    }
    return -1;
}

static int unavailable_dir(int cache, int create)
{
    struct stat st;
    int dir;
    if (create && mkdirat(cache, ".unavailable", 0700) && errno != EEXIST) return -1;
    dir = openat(cache, ".unavailable", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    if (dir < 0) return -1;
    if (fstat(dir, &st) || (st.st_uid != geteuid() && st.st_uid != 0) ||
        (st.st_mode & 0022)) { close(dir); errno = EPERM; return -1; }
    return dir;
}

static int clear_unavailable(int cache, const char *digest)
{
    int dir = unavailable_dir(cache, 0), ok;
    if (dir < 0) return errno == ENOENT;
    ok = !unlinkat(dir, digest, 0) || errno == ENOENT;
    if (ok) ok = !fsync(dir);
    close(dir);
    return ok;
}

static int mark_unavailable(int cache, const char *digest)
{
    char record[120];
    size_t length = (size_t)snprintf(record, sizeof record,
                                    "format holy-cache-unavailable-1\nartifact %s\n", digest);
    struct stat st;
    int dir = unavailable_dir(cache, 1), fd, ok = 0;
    if (dir < 0 || length >= sizeof record) return 0;
    fd = openat(dir, digest, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
    if (fd < 0 && errno == EEXIST) {
        char existing[120];
        fd = openat(dir, digest, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
        if (fd >= 0 && !fstat(fd, &st) && S_ISREG(st.st_mode) &&
            st.st_size == (off_t)length && !(st.st_mode & 0022) &&
            (st.st_uid == geteuid() || st.st_uid == 0) &&
            pread(fd, existing, length, 0) == (ssize_t)length &&
            !memcmp(existing, record, length)) ok = 1;
    } else if (fd >= 0) {
        ok = write(fd, record, length) == (ssize_t)length && !fsync(fd);
    }
    if (fd >= 0) close(fd);
    if (ok) ok = !fsync(dir);
    close(dir);
    return ok;
}

int holy_cache_stage_local_digest(const char *source, const char *root_path,
                                   char output[65])
{
    struct holy_package_identity identity = {0};
    struct stat st;
    char name[70];
    char *snapshot = holy_stage_local(source, "holy-cache");
    int current = -1, ok = 0;
    if (output) output[0] = 0;
    if (!snapshot || !holy_verify_with_output(snapshot, 0) ||
        !holy_scan_local_with_output(snapshot, 0) ||
        !holy_deps_local_with_output(snapshot, 0) ||
        !holy_provides_local(snapshot, 0) ||
        !holy_package_identity(snapshot, &identity)) goto done;
    current = cache_directory(root_path, 1);
    if (current < 0 || flock(current, LOCK_EX)) goto done;
    if (!holy_fetch_at(snapshot, current, identity.digest, name) ||
        fstatat(current, name, &st, AT_SYMLINK_NOFOLLOW) ||
        !S_ISREG(st.st_mode) || st.st_uid != geteuid() ||
        (st.st_mode & 0022) || !clear_unavailable(current, identity.digest)) goto done;
    printf("%s/var/cache/holypkg/objects/sha256/%s\n", root_path, name);
    if (output) memcpy(output, identity.digest, 65);
    ok = 1;
done:
    if (!ok) fprintf(stderr, "holypkg: cache stage failed; inspect target cache\n");
    if (current >= 0) close(current);
    holy_package_identity_free(&identity);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    return ok;
}

int holy_cache_stage_local(const char *source, const char *root_path)
{
    return holy_cache_stage_local_digest(source, root_path, NULL);
}

char *holy_cache_snapshot(const char *digest, const char *root_path)
{
    struct holy_package_identity identity = {0};
    struct stat st;
    char name[70], *snapshot = NULL;
    size_t i;
    int dir = -1, fd = -1, ok = 0;
    if (!digest || strlen(digest) != 64) goto done;
    for (i = 0; i < 64; ++i)
        if (!((digest[i] >= '0' && digest[i] <= '9') ||
              (digest[i] >= 'a' && digest[i] <= 'f'))) goto done;
    snprintf(name, sizeof name, "%s.holy", digest);
    dir = cache_directory(root_path, 0);
    if (dir < 0) goto done;
    fd = openat(dir, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
    if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
        (st.st_mode & 0022)) goto done;
    snapshot = holy_stage_fd(fd, "holy-cache-verify");
    if (!snapshot || !holy_verify_with_output(snapshot, 0) ||
        !holy_scan_local_with_output(snapshot, 0) ||
        !holy_deps_local_with_output(snapshot, 0) ||
        !holy_provides_local(snapshot, 0) ||
        !holy_package_identity(snapshot, &identity) ||
        strcmp(identity.digest, digest)) goto done;
    ok = 1;
done:
    if (!ok) fprintf(stderr, "holypkg: cache object verification failed\n");
    holy_package_identity_free(&identity);
    if (fd >= 0) close(fd);
    if (dir >= 0) close(dir);
    if (!ok && snapshot) { unlink(snapshot); free(snapshot); snapshot = NULL; }
    return snapshot;
}

static int verify_object(const char *digest, const char *root_path, int emit)
{
    struct holy_package_identity identity = {0};
    char *snapshot = holy_cache_snapshot(digest, root_path);
    int ok = snapshot != NULL;
    if (ok && emit) {
        ok = holy_package_identity(snapshot, &identity);
        if (ok) printf("verified %s %s %s %s\n", identity.digest, identity.name,
                       identity.arch, identity.libc);
    }
    holy_package_identity_free(&identity);
    if (snapshot) { unlink(snapshot); free(snapshot); }
    return ok;
}

int holy_cache_verify(const char *digest, const char *root_path)
{
    return verify_object(digest, root_path, 1);
}

int holy_cache_object(const char *digest, const char *root_path)
{
    return verify_object(digest, root_path, 0);
}

static int cache_name(const char *name)
{
    size_t i;
    if (strlen(name) != 69 || strcmp(name + 64, ".holy")) return 0;
    for (i = 0; i < 64; ++i)
        if (!((name[i] >= '0' && name[i] <= '9') ||
              (name[i] >= 'a' && name[i] <= 'f'))) return 0;
    return 1;
}

static int unavailable_name(const char *name)
{
    char object[70];
    if (strlen(name) != 64) return 0;
    snprintf(object, sizeof object, "%s.holy", name);
    return cache_name(object);
}

static int name_order(const void *left, const void *right)
{
    const char *const *a = left, *const *b = right;
    return strcmp(*a, *b);
}

static int cache_names(int dir, char ***output, size_t *count, int unavailable)
{
    DIR *list = NULL;
    struct dirent *entry;
    char **names = NULL;
    size_t used = 0;
    int ok = 0;
    *output = NULL; *count = 0;
    list = fdopendir(dup(dir));
    if (!list) return 0;
    errno = 0;
    while ((entry = readdir(list))) {
        struct stat st;
        char **grown;
        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..") ||
            (!unavailable && !strcmp(entry->d_name, ".unavailable"))) continue;
        if (!(unavailable ? unavailable_name(entry->d_name) : cache_name(entry->d_name)) ||
            used == 100000 ||
            fstatat(dir, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) ||
            !S_ISREG(st.st_mode) || (st.st_mode & 0022)) goto done;
        grown = realloc(names, (used + 1) * sizeof *grown);
        if (!grown) goto done;
        names = grown;
        names[used] = strdup(entry->d_name);
        if (!names[used]) goto done;
        ++used;
        errno = 0;
    }
    if (errno) goto done;
    qsort(names, used, sizeof *names, name_order);
    *output = names; *count = used; names = NULL; used = 0;
    ok = 1;
done:
    while (used) free(names[--used]);
    free(names);
    closedir(list);
    return ok;
}

static int transaction_refs(int dir, const char *digest, unsigned depth)
{
    DIR *list;
    struct dirent *entry;
    int result = 0;
    if (depth > 4 || (list = fdopendir(dup(dir))) == NULL) return -1;
    errno = 0;
    while ((entry = readdir(list))) {
        struct stat st;
        int fd;
        if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
        if (strstr(entry->d_name, digest)) { result = 1; break; }
        if (fstatat(dir, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) ||
            (st.st_mode & 0022)) { result = -1; break; }
        if (S_ISDIR(st.st_mode)) {
            fd = openat(dir, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
            if (fd < 0) { result = -1; break; }
            result = transaction_refs(fd, digest, depth + 1);
            close(fd);
        } else if (S_ISREG(st.st_mode) && st.st_size <= 4 * 1024 * 1024) {
            char buffer[4096 + 63];
            ssize_t got;
            size_t carry = 0, i;
            fd = openat(dir, entry->d_name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
            if (fd < 0) { result = -1; break; }
            while ((got = read(fd, buffer + carry, 4096)) > 0) {
                size_t length = carry + (size_t)got;
                for (i = 0; i + 64 <= length; ++i)
                    if (!memcmp(buffer + i, digest, 64)) { result = 1; break; }
                if (result) break;
                carry = length < 63 ? length : 63;
                memmove(buffer, buffer + length - carry, carry);
            }
            if (got < 0) result = -1;
            close(fd);
        } else result = -1;
        if (result) break;
        errno = 0;
    }
    if (!entry && errno) result = -1;
    closedir(list);
    return result;
}

int holy_cache_list(const char *root_path)
{
    char **names = NULL;
    size_t count = 0, i;
    unsigned long long generation;
    int status = 1, state = -1, installed = -1, cache = -1, unavailable = -1, result = 1;
    state = holy_state_lock(root_path, 0, &generation, &status);
    if (state < 0) return status;
    installed = openat(state, "installed", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    cache = cache_directory(root_path, 0);
    if (installed < 0 || cache < 0 || flock(cache, LOCK_SH) ||
        !cache_names(cache, &names, &count, 0)) goto done;
    for (i = 0; i < count; ++i) {
        struct stat st, used;
        char digest[65];
        memcpy(digest, names[i], 64); digest[64] = 0;
        if (fstatat(cache, names[i], &st, AT_SYMLINK_NOFOLLOW) || !S_ISREG(st.st_mode)) goto done;
        if (!fstatat(installed, digest, &used, AT_SYMLINK_NOFOLLOW)) {
            if (!S_ISDIR(used.st_mode)) goto done;
            printf("cache %s size %ju installed\n", digest, (uintmax_t)st.st_size);
        } else if (errno == ENOENT)
            printf("cache %s size %ju retained\n", digest, (uintmax_t)st.st_size);
        else goto done;
    }
    for (i = 0; i < count; ++i) free(names[i]);
    free(names); names = NULL; count = 0;
    unavailable = unavailable_dir(cache, 0);
    if (unavailable >= 0) {
        if (!cache_names(unavailable, &names, &count, 1)) goto done;
        for (i = 0; i < count; ++i) {
            struct stat st;
            char object[70];
            snprintf(object, sizeof object, "%s.holy", names[i]);
            if (fstatat(cache, object, &st, AT_SYMLINK_NOFOLLOW) == 0) continue;
            if (errno != ENOENT) goto done;
            printf("cache %s unavailable\n", names[i]);
        }
    } else if (errno != ENOENT) goto done;
    result = ferror(stdout) ? 1 : 0;
done:
    for (i = 0; i < count; ++i) free(names[i]);
    free(names);
    if (unavailable >= 0) close(unavailable);
    if (cache >= 0) close(cache);
    if (installed >= 0) close(installed);
    if (state >= 0) close(state);
    return result;
}

int holy_cache_clean(const char *digest, const char *root_path, int yes,
                     int accept_unavailable)
{
    char name[70];
    struct stat st, installed_st;
    unsigned long long generation;
    int status = 1, state = -1, installed = -1, transactions = -1;
    int cache = -1, result = 1, refs, installed_ref = 0;
    if (accept_unavailable && !yes) return 2;
    if (!digest || strlen(digest) != 64) return 2;
    snprintf(name, sizeof name, "%s.holy", digest);
    if (!cache_name(name)) return 2;
    state = holy_state_lock(root_path, yes, &generation, &status);
    if (state < 0) return status;
    installed = openat(state, "installed", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    transactions = openat(state, "transactions", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
    cache = cache_directory(root_path, 0);
    if (installed < 0 || transactions < 0 || cache < 0 ||
        flock(cache, yes ? LOCK_EX : LOCK_SH)) goto done;
    if (fstatat(cache, name, &st, AT_SYMLINK_NOFOLLOW)) {
        result = errno == ENOENT ? 6 : 1; goto done;
    }
    if (!S_ISREG(st.st_mode) || (st.st_mode & 0022)) goto done;
    if (!fstatat(installed, digest, &installed_st, AT_SYMLINK_NOFOLLOW)) {
        if (!S_ISDIR(installed_st.st_mode)) goto done;
        installed_ref = 1;
    } else if (errno != ENOENT) goto done;
    refs = transaction_refs(transactions, digest, 0);
    if (refs < 0) goto done;
    if (!yes) {
        printf("cache-clean-plan %s size %ju generation %llu installed %d transactions %d read-only\n",
               digest, (uintmax_t)st.st_size, generation, installed_ref, refs);
        result = 0; goto done;
    }
    if ((installed_ref || refs) && !accept_unavailable) {
        fprintf(stderr, "holypkg: decision-required cache object %s installed=%d transactions=%d; use --accept-unavailable after review\n",
                digest, installed_ref, refs);
        result = 3; goto done;
    }
    if (!mark_unavailable(cache, digest)) goto done;
    if (unlinkat(cache, name, 0) || fsync(cache)) goto done;
    printf("cache-cleaned %s unavailable\n", digest);
    result = 0;
done:
    if (cache >= 0) close(cache);
    if (transactions >= 0) close(transactions);
    if (installed >= 0) close(installed);
    if (state >= 0) close(state);
    return result;
}