_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/state.c
#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "state.h"
#include "stage.h"
#include "cache.h"
#include "preview.h"
#include "verify.h"
#include "extract.h"
#include "package.h"
#include "install.h"
#include "config.h"
#include "resolve.h"
#include "scan.h"
#include "deps.h"
#include "provides.h"
#include "source.h"
#include "change.h"
#include <archive.h>
#include <archive_entry.h>
#include <openssl/evp.h>
#include <dirent.h>
#include <elf.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <limits.h>
#include <linux/openat2.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/utsname.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <unistd.h>
#ifndef O_PATH
#define O_PATH 010000000
#endif
static int set_journal_present(int dir);
static int update_pending(int dir);
static int completed_update(int transactions, const char *name);
static int remove_record(int transactions, const char *digest,
unsigned long long generation, int broken, int create);
static int remove_workdir(int transactions, const char *digest,
unsigned long long generation, int broken);
static int commit_remove_record(int transactions, const char *digest,
unsigned long long generation, int broken);
static char *update_record(int dir, const char *name);
static int installed_fields(int item, const char *const *keys,
const char *const *values, size_t fields);
static int valid_owner_path(const char *path);
static int loader_directories(const struct holy_elf_info *elf, const char *consumer,
char ***directories, size_t *count);
static void free_loader_directories(char **directories, size_t count);
static int loader_file_match(const char *directory, const char *path,
const char *name);
static int write_all(int fd, const void *data, size_t length);
static int set_generation(int dir, unsigned long long generation);
static int native_architecture(const char *host, const char *target)
{
return !strcmp(target, "noarch") ||
(!strcmp(target, "x86_64") && !strcmp(host, "x86_64")) ||
(!strcmp(target, "x86") && !strcmp(host, "i686"));
}
static int architecture_valid(const char *text)
{
const char *space = strchr(text, ' ');
size_t length;
if (!space || space == text || (length = (size_t)(space - text)) > 64 ||
strspn(text, "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_.-") != length) return 0;
return !strcmp(space + 1, "x86") || !strcmp(space + 1, "x86_64");
}
static int state_architecture(const char *state, char output[96])
{
const char *start = strstr(state, "\narchitecture "), *end;
output[0] = 0;
if (!start) return 1;
start += 14;
end = strchr(start, '\n');
if (!end || end == start || end - start >= 96) return 0;
memcpy(output, start, (size_t)(end - start));
output[end - start] = 0;
return architecture_valid(output);
}
static int instance_architecture(int item, char output[96])
{
char state[1024];
ssize_t got;
int fd = openat(item, "state", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) return 0;
got = read(fd, state, sizeof state - 1);
close(fd);
if (got < 1 || got == (ssize_t)sizeof state - 1 || memchr(state, 0, (size_t)got)) return 0;
state[got] = 0;
return state_architecture(state, output);
}
static int instance_architecture_matches(int item, const char *expected)
{
char actual[96];
return instance_architecture(item, actual) && !strcmp(actual, expected);
}
static int safe_directory(int fd)
{
struct stat st;
return !fstat(fd, &st) && S_ISDIR(st.st_mode) &&
(st.st_uid == 0 || st.st_uid == geteuid()) &&
!(st.st_mode & 0022);
}
static int valid_digest(const char *digest)
{
size_t i;
if (!digest || strlen(digest) != 64) return 0;
for (i = 0; i < 64; ++i)
if (!((digest[i] >= '0' && digest[i] <= '9') ||
(digest[i] >= 'a' && digest[i] <= 'f'))) return 0;
return 1;
}
static int child_dir(int parent, const char *name, int create)
{
int fd;
if (create) {
if (mkdirat(parent, name, 0700)) {
if (errno != EEXIST) return -1;
} else if (fsync(parent)) return -1;
}
fd = openat(parent, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) return -1;
if (!safe_directory(fd)) { close(fd); errno = EPERM; return -1; }
return fd;
}
static DIR *directory_stream(int fd)
{
int copy = openat(fd, ".", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
DIR *stream;
if (copy < 0) return NULL;
stream = fdopendir(copy);
if (!stream) close(copy);
return stream;
}
static int state_dir_at(int root, int create)
{
static const char *const path[] = { "var", "lib", "holypkg" };
int fd = dup(root);
size_t i;
if (fd < 0) return -1;
if (!safe_directory(fd)) { close(fd); errno = EPERM; return -1; }
for (i = 0; i < sizeof path / sizeof *path; ++i) {
int next = child_dir(fd, path[i], create);
close(fd);
if (next < 0) return -1;
fd = next;
}
return fd;
}
static int state_dir(const char *root_path, int create)
{
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, create);
if (root >= 0) close(root);
return dir;
}
static int read_generation(int dir, unsigned long long *generation)
{
char buffer[32];
struct stat st;
ssize_t got;
size_t i;
unsigned long long n = 0;
int fd = openat(dir, "generation", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) return 0;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 2 ||
st.st_size > 21 || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) { close(fd); return 0; }
got = read(fd, buffer, sizeof buffer);
close(fd);
if (got != st.st_size || buffer[got - 1] != '\n' ||
(got > 2 && buffer[0] == '0')) return 0;
for (i = 0; i + 1 < (size_t)got; ++i) {
unsigned digit = (unsigned char)buffer[i] - '0';
if (digit > 9 || n > (ULLONG_MAX - digit) / 10) return 0;
n = n * 10 + digit;
}
*generation = n;
return 1;
}
static int state_layout(int dir, int create)
{
static const char *const names[] = { "installed", "transactions", "index" };
size_t i;
for (i = 0; i < sizeof names / sizeof *names; ++i) {
int child = child_dir(dir, names[i], create);
if (child < 0) return 0;
close(child);
}
return 1;
}
static int empty_child(int dir, const char *name)
{
int fd = child_dir(dir, name, 0);
DIR *entries;
struct dirent *entry;
int empty = 1;
if (fd < 0) return 0;
entries = fdopendir(fd);
if (!entries) { close(fd); return 0; }
errno = 0;
while ((entry = readdir(entries)) != NULL) {
if (!strcmp(name, "transactions") && completed_update(fd, entry->d_name)) {
errno = 0; continue;
}
if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) {
empty = 0;
break;
}
errno = 0;
}
if (!entry && errno) empty = 0;
if (closedir(entries)) empty = 0;
if (!empty) fprintf(stderr, "holypkg: unrecognized database entries in %s\n", name);
return empty;
}
static int instance_record_digest(int item, const char *name, char output[65])
{
unsigned char buffer[8192], digest[32];
unsigned int length;
size_t total = 0, i;
ssize_t got;
struct stat st;
EVP_MD_CTX *hash = EVP_MD_CTX_new();
int fd = openat(item, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
int ok = 0;
if (fd < 0 || !hash || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_mode & 0022) || (st.st_uid != 0 && st.st_uid != geteuid()) ||
(st.st_size < 1 && strcmp(name, "provides")) || st.st_size < 0 || st.st_size > 16 * 1024 * 1024 ||
EVP_DigestInit_ex(hash, EVP_sha256(), NULL) != 1) goto done;
while ((got = read(fd, buffer, sizeof buffer)) != 0) {
if (got < 0) { if (errno == EINTR) continue; goto done; }
total += (size_t)got;
if (total > 16 * 1024 * 1024 ||
EVP_DigestUpdate(hash, buffer, (size_t)got) != 1) goto done;
}
if (total != (size_t)st.st_size ||
EVP_DigestFinal_ex(hash, digest, &length) != 1 || length != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", digest[i]);
ok = 1;
done:
if (fd >= 0) close(fd);
EVP_MD_CTX_free(hash);
return ok;
}
static int config_state_valid(int item, const char *artifact)
{
char source[65], raw[65], installed[65], plan[65], actual[65];
char *record = update_record(item, "config-state");
int ok = 0;
if (!record) return 0;
if (sscanf(record,
"format holy-config-transform-1\nsource %64[0-9a-f]\nraw %64[0-9a-f]\ninstalled %64[0-9a-f]\nplan %64[0-9a-f]\n",
source, raw, installed, plan) != 4 ||
!valid_digest(source) || !valid_digest(raw) || !valid_digest(installed) ||
!valid_digest(plan) ||
strcmp(source, artifact) ||
!instance_record_digest(item, "package-files", actual) || strcmp(actual, raw) ||
!instance_record_digest(item, "files", actual) || strcmp(actual, installed)) goto done;
ok = 1;
done:
free(record);
return ok;
}
static int graph_digest(int item, char output[65])
{
return instance_record_digest(item, "graph", output);
}
static int instance_state_generation(int item, const char *digest,
unsigned long long *recorded)
{
char buffer[1024], prefix[960], graph[65], source[65], source_hash[65], claims[65], architecture[96], *end;
struct stat st;
unsigned long long generation;
ssize_t got;
size_t length;
const char *reason = "explicit";
int version, fd = openat(item, "state", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) return 0;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_mode & 0022) || (st.st_uid != 0 && st.st_uid != geteuid()) ||
st.st_size < 24 || st.st_size >= (off_t)sizeof buffer) {
close(fd);
return 0;
}
got = read(fd, buffer, sizeof buffer - 1);
close(fd);
if (got != st.st_size || buffer[got - 1] != '\n' || memchr(buffer, 0, (size_t)got)) return 0;
buffer[got] = '\0';
version = !strncmp(buffer, "format holy-instance-7\n", 23) ? 7 :
!strncmp(buffer, "format holy-instance-6\n", 23) ? 6 :
!strncmp(buffer, "format holy-instance-5\n", 23) ? 5 :
!strncmp(buffer, "format holy-instance-4\n", 23) ? 4 :
!strncmp(buffer, "format holy-instance-3\n", 23) ? 3 :
!strncmp(buffer, "format holy-instance-2\n", 23) ? 2 : 1;
if (strstr(buffer, "\nreason dependency\n")) reason = "dependency";
if (version < 3 && (!fstatat(item, "source", &st, AT_SYMLINK_NOFOLLOW) || errno != ENOENT)) return 0;
if (version < 4 && (!fstatat(item, "provides", &st, AT_SYMLINK_NOFOLLOW) || errno != ENOENT)) return 0;
if (version >= 4) {
if (!graph_digest(item, graph) || !instance_record_digest(item, "provides", claims)) return 0;
if (!fstatat(item, "source", &st, AT_SYMLINK_NOFOLLOW)) {
if (!holy_source_instance(item, source, source_hash)) return 0;
} else {
if (errno != ENOENT) return 0;
strcpy(source, "-"); strcpy(source_hash, "-");
}
if (!state_architecture(buffer, architecture) ||
(version == 5 || version == 7) != !!architecture[0]) return 0;
if (architecture[0]) {
const char *keys[] = {"arch"}, *values[] = {strchr(architecture, ' ') + 1};
if (installed_fields(item, keys, values, 1) != 1) return 0;
}
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-instance-%d\nsource-id %s\nsource-record %s\ndelivery local\nreason %s\nartifact %s\ngraph %s\nprovides %s\n%s%s%s%s%s%sgeneration ",
version, source, source_hash, reason, digest, graph, claims,
architecture[0] ? "architecture " : "", architecture, architecture[0] ? "\n" : "",
version >= 6 ? "privileged " : "", version >= 6 ? digest : "",
version >= 6 ? "\n" : "");
} else if (version == 3) {
if (!graph_digest(item, graph) || !holy_source_instance(item, source, source_hash)) return 0;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-instance-3\nsource-id %s\nsource-record %s\ndelivery local\nreason %s\nartifact %s\ngraph %s\ngeneration ",
source, source_hash, reason, digest, graph);
} else if (version == 2) {
if (!graph_digest(item, graph)) return 0;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-instance-2\nsource-id -\ndelivery local\nreason %s\nartifact %s\ngraph %s\ngeneration ",
reason, digest, graph);
} else {
if (!fstatat(item, "graph", &st, AT_SYMLINK_NOFOLLOW) || errno != ENOENT) return 0;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-instance-1\nsource-id -\ndelivery local\nreason explicit\nartifact %s\ngeneration ", digest);
}
if (length >= sizeof prefix || (size_t)got <= length + 1 ||
memcmp(buffer, prefix, length)) return 0;
buffer[got - 1] = '\0';
if (buffer[length] < '0' || buffer[length] > '9') return 0;
errno = 0;
generation = strtoull(buffer + length, &end, 10);
if (errno || *end || (buffer[length] == '0' && buffer[length + 1])) return 0;
if (recorded) *recorded = generation;
return 1;
}
static int installed_valid(int dir)
{
static const char *const required[] = { "meta", "files", "deps", "origin", "state", "graph", "source", "provides", "hooks", "transform", "hooks-state", "package-files", "config-state" };
int installed = child_dir(dir, "installed", 0), ok = 1;
DIR *list;
struct dirent *entry;
if (installed < 0) return 0;
list = directory_stream(installed);
if (!list) { close(installed); return 0; }
errno = 0;
while ((entry = readdir(list))) {
int item;
size_t i;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (!valid_digest(entry->d_name)) { ok = 0; break; }
item = child_dir(installed, entry->d_name, 0);
if (item < 0) { ok = 0; break; }
for (i = 0; i < 5; ++i) {
struct stat st;
if (fstatat(item, required[i], &st, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(st.st_mode) || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) { ok = 0; break; }
}
if (ok && !instance_state_generation(item, entry->d_name, NULL)) ok = 0;
if (ok) {
struct stat transformed, source;
int has_transform, has_source;
errno = 0;
has_transform = !fstatat(item, "config-state", &transformed, AT_SYMLINK_NOFOLLOW);
if (!has_transform && errno != ENOENT) ok = 0;
errno = 0;
has_source = !fstatat(item, "package-files", &source, AT_SYMLINK_NOFOLLOW);
if (!has_source && errno != ENOENT) ok = 0;
if (has_transform != has_source || (has_transform &&
(!S_ISREG(transformed.st_mode) || !S_ISREG(source.st_mode) ||
!config_state_valid(item, entry->d_name)))) ok = 0;
}
if (ok) {
DIR *members = fdopendir(dup(item));
struct dirent *member;
unsigned seen = 0;
if (!members) ok = 0;
else {
errno = 0;
while ((member = readdir(members))) {
if (!strcmp(member->d_name, ".") ||
!strcmp(member->d_name, "..")) continue;
for (i = 0; i < sizeof required / sizeof *required; ++i)
if (!strcmp(member->d_name, required[i])) break;
if (i == sizeof required / sizeof *required ||
(seen & (1u << i))) { ok = 0; break; }
seen |= 1u << i;
errno = 0;
}
if (!member && errno) ok = 0;
{
unsigned base = seen & ((1u << 8) - 1u);
if (base != (1u << 5) - 1u && base != (1u << 6) - 1u &&
base != (1u << 7) - 1u && base != ((1u << 6) - 1u + (1u << 7)) &&
base != (1u << 8) - 1u) ok = 0;
}
closedir(members);
}
}
if (ok && (fstatat(item, "hooks", &(struct stat){0}, AT_SYMLINK_NOFOLLOW) == 0 ||
fstatat(item, "hooks-state", &(struct stat){0}, AT_SYMLINK_NOFOLLOW) == 0)) {
char expected[96], completed[96], actual[96], digest[65];
struct stat hooks_stat, state_stat;
int fd;
if (fstatat(item, "hooks", &hooks_stat, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(hooks_stat.st_mode) || (hooks_stat.st_mode & 0022) ||
(hooks_stat.st_uid != 0 && hooks_stat.st_uid != geteuid()) ||
(fstatat(item, "hooks-state", &state_stat, AT_SYMLINK_NOFOLLOW) == 0) !=
(hooks_stat.st_size > 0)) ok = 0;
if (ok && hooks_stat.st_size > 0) {
if (!instance_record_digest(item, "hooks", digest)) ok = 0;
if (ok) {
int length = snprintf(expected, sizeof expected, "skipped sha256 %s\n", digest);
int completed_length = snprintf(completed, sizeof completed,
"completed sha256 %s\n", digest);
ssize_t got;
fd = openat(item, "hooks-state", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 || fstat(fd, &state_stat) || !S_ISREG(state_stat.st_mode) ||
(state_stat.st_mode & 0022) ||
(state_stat.st_uid != 0 && state_stat.st_uid != geteuid()) ||
length >= (int)sizeof expected ||
completed_length >= (int)sizeof completed) ok = 0;
else {
got = read(fd, actual, sizeof actual);
if ((got != length || memcmp(actual, expected, (size_t)length)) &&
(got != completed_length ||
memcmp(actual, completed, (size_t)completed_length))) ok = 0;
}
if (fd >= 0) close(fd);
}
}
}
if (ok && !fstatat(item, "transform", &(struct stat){0}, AT_SYMLINK_NOFOLLOW)) {
struct stat transform_stat;
if (fstatat(item, "transform", &transform_stat, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(transform_stat.st_mode) || (transform_stat.st_mode & 0022) ||
(transform_stat.st_uid != 0 && transform_stat.st_uid != geteuid())) ok = 0;
}
close(item);
if (!ok) break;
errno = 0;
}
if (!entry && errno) ok = 0;
closedir(list);
close(installed);
if (!ok) fprintf(stderr, "holypkg: invalid installed entries\n");
return ok;
}
static int hook_skipped(int item)
{
char prefix[8];
int fd = openat(item, "hooks-state", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
ssize_t got;
if (fd < 0) return 0;
got = read(fd, prefix, sizeof prefix);
close(fd);
return got == (ssize_t)sizeof prefix && !memcmp(prefix, "skipped ", sizeof prefix);
}
static int journal_exists(int dir)
{
int transactions = child_dir(dir, "transactions", 0);
struct stat st;
int present;
if (transactions < 0) return -1;
present = fstatat(transactions, "journal", &st, AT_SYMLINK_NOFOLLOW) == 0;
if (present && (!S_ISREG(st.st_mode) || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid()))) present = -1;
if (!present && errno != ENOENT) present = -1;
close(transactions);
return present;
}
static int journal_valid(int dir, unsigned long long generation,
unsigned long long *original,
char artifact[65], char plan_digest[65], int *removing)
{
int transactions = child_dir(dir, "transactions", 0), fd = -1, result = -1;
struct stat st;
char buffer[256], prefix[96], digest[65], plan[65];
ssize_t got;
size_t length;
unsigned long long recorded = 0;
int is_removing = 0, attempt;
if (transactions < 0) return -1;
fd = openat(transactions, "journal", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) { result = errno == ENOENT ? 0 : -1; goto done; }
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 140 ||
st.st_size > (off_t)sizeof buffer || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) goto done;
got = read(fd, buffer, sizeof buffer);
if (got != st.st_size) goto done;
for (attempt = 0; attempt < 6; ++attempt) {
if (attempt >= 3 && !generation) break;
recorded = generation - (unsigned long long)(attempt / 3);
is_removing = attempt % 3;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-journal-1\nstage %s\ngeneration %llu\nartifact ",
is_removing == 2 ? "repairing" : is_removing ? "removing" : "applying", recorded);
if (length >= sizeof prefix) goto done;
if (!memcmp(buffer, prefix, length)) break;
}
if (attempt == 6 || (attempt >= 3 && !generation)) goto done;
if (st.st_size != (off_t)(length + 65 + 5 + 65) ||
buffer[length + 64] != '\n' ||
memcmp(buffer + length + 65, "plan ", 5) ||
buffer[length + 65 + 5 + 64] != '\n') goto done;
memcpy(digest, buffer + length, 64);
digest[64] = '\0';
memcpy(plan, buffer + length + 70, 64);
plan[64] = '\0';
if (!valid_digest(digest) || !valid_digest(plan)) goto done;
if (original) *original = recorded;
if (artifact) memcpy(artifact, digest, 65);
if (plan_digest) memcpy(plan_digest, plan, 65);
if (removing) *removing = is_removing;
result = 1;
done:
if (fd >= 0) close(fd);
close(transactions);
return result;
}
static int transaction_pending(int dir, unsigned long long generation)
{
int transactions = child_dir(dir, "transactions", 0);
struct stat hook;
int hook_pending;
if (transactions < 0) return -1;
hook_pending = !fstatat(transactions, "hook-journal", &hook, AT_SYMLINK_NOFOLLOW);
if (!hook_pending && errno != ENOENT) hook_pending = -1;
if (hook_pending > 0 && (!S_ISREG(hook.st_mode) || (hook.st_mode & 0022) ||
(hook.st_uid != 0 && hook.st_uid != geteuid()))) hook_pending = -1;
close(transactions);
if (hook_pending) return hook_pending;
int result = update_pending(dir);
if (result) return result;
result = set_journal_present(dir);
return result ? result : journal_valid(dir, generation, NULL, NULL, NULL, NULL);
}
static int read_reservation(int transactions, const char *name,
unsigned long long generation, char digest[65],
char approved[65])
{
char buffer[256], prefix[96];
struct stat st;
ssize_t got;
size_t length, i;
int is_approved = 0;
int fd = openat(transactions, name, O_RDONLY | O_NOFOLLOW |
O_CLOEXEC | O_NONBLOCK);
if (fd < 0) return errno == ENOENT ? 0 : -1;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-reservation-1\nstage prepared\ngeneration %llu\nartifact ",
generation);
if (length >= sizeof prefix || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_size != (off_t)(length + 65) &&
st.st_size != (off_t)(length + 65 + 70)) || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) { close(fd); return -1; }
got = read(fd, buffer, sizeof buffer);
close(fd);
if (got != st.st_size || buffer[length + 64] != '\n') return -1;
if (memcmp(buffer, prefix, length)) {
is_approved = 1;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-reservation-1\nstage approved\ngeneration %llu\nartifact ",
generation);
if (length >= sizeof prefix || memcmp(buffer, prefix, length) ||
st.st_size != (off_t)(length + 65 + 70)) return -1;
}
for (i = 0; i < 64; ++i)
if (!((buffer[length + i] >= '0' && buffer[length + i] <= '9') ||
(buffer[length + i] >= 'a' && buffer[length + i] <= 'f')))
return -1;
memcpy(digest, buffer + length, 64);
digest[64] = '\0';
approved[0] = '\0';
if (is_approved != (st.st_size != (off_t)(length + 65))) return -1;
if (st.st_size != (off_t)(length + 65)) {
if (memcmp(buffer + length + 65, "plan ", 5) ||
buffer[length + 65 + 5 + 64] != '\n') return -1;
memcpy(approved, buffer + length + 70, 64);
approved[64] = '\0';
if (!valid_digest(approved)) return -1;
}
return 1;
}
static int pending_child(int dir, unsigned long long generation, char digest[65],
char approved[65])
{
int child = child_dir(dir, "transactions", 0), result = -1;
DIR *listing;
struct dirent *entry;
size_t count = 0;
if (child < 0) return -1;
listing = directory_stream(child);
if (!listing) { close(child); return -1; }
errno = 0;
while ((entry = readdir(listing))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
if (completed_update(child, entry->d_name)) { errno = 0; continue; }
if (strcmp(entry->d_name, "pending") || ++count > 1) break;
errno = 0;
}
if (!entry && !errno && count <= 1)
result = read_reservation(child, "pending", generation, digest, approved);
closedir(listing);
close(child);
if (result < 0) fprintf(stderr, "holypkg: unrecognized database entries in transactions\n");
return result;
}
int holy_state_init(const char *root_path)
{
char temp_name[43];
unsigned long long generation;
struct stat st;
int dir = state_dir(root_path, 1), temp = -1, ok = 0;
if (dir < 0) goto done;
if (flock(dir, LOCK_EX)) goto done;
if (!state_layout(dir, 1) ||
!empty_child(dir, "installed") ||
!empty_child(dir, "transactions") ||
!empty_child(dir, "index")) goto done;
if (fstatat(dir, "generation", &st, AT_SYMLINK_NOFOLLOW) == 0) {
if (!read_generation(dir, &generation)) goto done;
} else if (errno != ENOENT) goto done;
temp = holy_temporary_at(dir, temp_name);
if (temp < 0) goto done;
if (write(temp, "0\n", 2) != 2 || fsync(temp)) goto done;
if (linkat(dir, temp_name, dir, "generation", 0) && errno != EEXIST)
goto done;
if (fsync(dir) || !read_generation(dir, &generation)) goto done;
printf("generation %llu\n", generation);
ok = 1;
done:
if (!ok) fprintf(stderr, "holypkg: database init failed\n");
if (temp >= 0) { close(temp); unlinkat(dir, temp_name, 0); }
if (dir >= 0) close(dir);
return ok;
}
int holy_state_status(const char *root_path, int json)
{
unsigned long long generation;
char digest[65], approved[65];
int dir = state_dir(root_path, 0), pending, result = 1;
if (dir < 0) goto done;
if (flock(dir, LOCK_SH) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) goto done;
pending = transaction_pending(dir, generation);
if (pending < 0) goto done;
if (pending) {
if (json) printf("{\"schema\":\"holy-db-status-1\",\"type\":\"incomplete\",\"generation\":%llu}\n", generation);
else printf("generation %llu\nincomplete transaction; inspect journal\n", generation);
result = 5;
goto done;
}
if (!installed_valid(dir)) goto done;
pending = pending_child(dir, generation, digest, approved);
if (pending < 0) goto done;
if (json) {
printf("{\"schema\":\"holy-db-status-1\",\"type\":\"state\",\"generation\":%llu,\"pending\":",
generation);
if (pending) {
printf("{\"stage\":\"%s\",\"sha256\":\"%s\"",
approved[0] ? "approved" : "prepared", digest);
if (approved[0]) printf(",\"plan\":\"%s\"", approved);
putchar('}');
}
else fputs("null", stdout);
puts("}");
} else {
printf("generation %llu\n", generation);
if (pending) printf("pending %s%s%s\n", digest,
approved[0] ? " approved " : "", approved);
}
if (pending) {
result = 5;
} else result = 0;
done:
if (result == 1) {
fprintf(stderr, "holypkg: database status unavailable\n");
if (json) puts("{\"schema\":\"holy-db-status-1\",\"type\":\"error\",\"code\":\"invalid-state\"}");
}
if (dir >= 0) close(dir);
return result;
}
static int compare_instance_names(const void *a, const void *b)
{
return strcmp(*(const char *const *)a, *(const char *const *)b);
}
int holy_state_lock(const char *root_path, int exclusive,
unsigned long long *generation, int *status)
{
unsigned long long current;
char digest[65], approved[65];
int dir = state_dir(root_path, 0), pending;
*status = 1;
if (dir < 0 || flock(dir, exclusive ? LOCK_EX : LOCK_SH) ||
!state_layout(dir, 0) || !read_generation(dir, ¤t)) goto failed;
pending = transaction_pending(dir, current);
if (pending < 0) goto failed;
if (pending) { *status = 5; goto failed; }
pending = pending_child(dir, current, digest, approved);
if (pending < 0) goto failed;
if (pending) { *status = 5; goto failed; }
if (!installed_valid(dir)) goto failed;
*generation = current;
*status = 0;
return dir;
failed:
if (dir >= 0) close(dir);
return -1;
}
int holy_state_visit(const char *root_path, holy_instance_visit visit, void *context,
unsigned long long *generation)
{
int root = -1, dir = -1, installed = -1, result = 1, pending;
char **names = NULL, digest[65], approved[65];
size_t count = 0, i;
DIR *list = NULL;
struct dirent *entry;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0 || (dir = state_dir_at(root, 0)) < 0 || flock(dir, LOCK_SH) ||
!state_layout(dir, 0) || !read_generation(dir, generation)) goto done;
pending = transaction_pending(dir, *generation);
if (pending < 0) goto done;
if (pending) { result = 5; goto done; }
pending = pending_child(dir, *generation, digest, approved);
if (pending < 0) goto done;
if (pending) { result = 5; goto done; }
if (!installed_valid(dir) || (installed = child_dir(dir, "installed", 0)) < 0 ||
!(list = directory_stream(installed))) goto done;
errno = 0;
while ((entry = readdir(list))) {
char **grown;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (count >= SIZE_MAX / sizeof *names ||
!(grown = realloc(names, (count + 1) * sizeof *names))) goto done;
names = grown;
names[count] = strdup(entry->d_name);
if (!names[count]) goto done;
++count;
errno = 0;
}
if (errno) goto done;
if (count) qsort(names, count, sizeof *names, compare_instance_names);
for (i = 0; i < count; ++i) {
int item = child_dir(installed, names[i], 0), rc;
if (item < 0) goto done;
rc = visit(context, root, item, names[i]);
close(item);
if (rc) { result = rc; goto done; }
}
result = 0;
done:
for (i = 0; i < count; ++i) free(names[i]);
free(names);
if (list) closedir(list);
if (installed >= 0) close(installed);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_reserve(const char *digest, const char *root_path)
{
unsigned long long generation;
char existing[65], approved[65], temp_name[43] = {0}, record[192];
int dir = -1, transactions = -1, temp = -1, result = 1;
size_t length;
if (!valid_digest(digest)) {
fprintf(stderr, "holypkg: expected a lowercase SHA-256 digest\n");
return 2;
}
dir = state_dir(root_path, 0);
if (!holy_cache_object(digest, root_path)) { result = 6; goto done; }
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!installed_valid(dir) || !empty_child(dir, "index") ||
!read_generation(dir, &generation)) goto done;
result = update_pending(dir);
if (result) { result = result > 0 ? 5 : 1; goto done; }
result = 1;
result = pending_child(dir, generation, existing, approved);
if (result < 0) { result = 1; goto done; }
if (result) { result = 5; goto done; }
result = 1;
transactions = child_dir(dir, "transactions", 0);
if (transactions < 0) goto done;
length = (size_t)snprintf(record, sizeof record,
"format holy-reservation-1\nstage prepared\ngeneration %llu\nartifact %s\n",
generation, digest);
if (length >= sizeof record) goto done;
temp = holy_temporary_at(transactions, temp_name);
if (temp < 0 || write(temp, record, length) != (ssize_t)length || fsync(temp) ||
linkat(transactions, temp_name, transactions, "pending", 0)) goto done;
if (close(temp)) { temp = -1; goto done; }
temp = -1;
if (unlinkat(transactions, temp_name, 0) || fsync(transactions)) goto done;
printf("reserved %s generation %llu\n", digest, generation);
result = 0;
done:
if (result && result != 5) fprintf(stderr, "holypkg: reservation failed\n");
if (temp >= 0) { close(temp); unlinkat(transactions, temp_name, 0); }
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
return result;
}
int holy_state_cancel(const char *root_path)
{
unsigned long long generation;
char digest[65], approved[65];
int dir = state_dir(root_path, 0), transactions = -1, result = 1;
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!installed_valid(dir) || !empty_child(dir, "index") ||
!read_generation(dir, &generation)) goto done;
result = update_pending(dir);
if (result) { result = result > 0 ? 5 : 1; goto done; }
result = 1;
if (pending_child(dir, generation, digest, approved) != 1) goto done;
transactions = child_dir(dir, "transactions", 0);
if (transactions < 0 || unlinkat(transactions, "pending", 0) ||
fsync(transactions)) goto done;
printf("cancelled %s\n", digest);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: reservation cancellation failed\n");
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
return result;
}
static int temporary_name(const char *name)
{
size_t i;
if (strlen(name) != 42 || strncmp(name, ".holy-tmp-", 10)) return 0;
for (i = 10; i < 42; ++i)
if (!((name[i] >= '0' && name[i] <= '9') ||
(name[i] >= 'a' && name[i] <= 'f'))) return 0;
return 1;
}
int holy_state_recover(const char *root_path)
{
unsigned long long generation;
char temp_name[43] = {0}, pending_digest[65], temp_digest[65];
char pending_approved[65], temp_approved[65];
struct stat pending_st, temp_st;
DIR *listing = NULL;
struct dirent *entry;
int dir = state_dir(root_path, 0), transactions = -1;
int has_pending = 0, result = 1;
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) goto done;
result = transaction_pending(dir, generation);
if (result < 0) { result = 1; goto done; }
if (result) {
fprintf(stderr, "holypkg: incomplete file transaction requires manual inspection\n");
result = 5;
goto done;
}
result = 1;
if (!installed_valid(dir)) goto done;
transactions = child_dir(dir, "transactions", 0);
if (transactions < 0) goto done;
listing = directory_stream(transactions);
if (!listing) goto done;
errno = 0;
while ((entry = readdir(listing))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, ".."))
continue;
if (completed_update(transactions, entry->d_name)) { errno = 0; continue; }
if (!strcmp(entry->d_name, "pending")) {
if (has_pending++) goto done;
} else if (temporary_name(entry->d_name) && !temp_name[0]) {
memcpy(temp_name, entry->d_name, 43);
} else goto done;
errno = 0;
}
if (errno) goto done;
if (has_pending && (read_reservation(transactions, "pending", generation,
pending_digest, pending_approved) != 1 ||
fstatat(transactions, "pending", &pending_st,
AT_SYMLINK_NOFOLLOW))) goto done;
if (!temp_name[0]) { result = has_pending ? 5 : 0; goto done; }
if (read_reservation(transactions, temp_name, generation,
temp_digest, temp_approved) != 1 ||
fstatat(transactions, temp_name, &temp_st, AT_SYMLINK_NOFOLLOW)) goto done;
if (has_pending && (strcmp(pending_digest, temp_digest) ||
((pending_st.st_dev != temp_st.st_dev ||
pending_st.st_ino != temp_st.st_ino) &&
!(temp_approved[0] && !pending_approved[0])))) goto done;
if (unlinkat(transactions, temp_name, 0) || fsync(transactions)) goto done;
printf("recovered temporary reservation %s\n", temp_digest);
result = has_pending ? 5 : 0;
done:
if (result == 1) fprintf(stderr, "holypkg: reservation recovery failed\n");
if (listing) closedir(listing);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
return result;
}
int holy_state_preflight(const char *root_path, int json)
{
unsigned long long generation;
char digest[65], approved[65], *snapshot = NULL;
int dir = state_dir(root_path, 0), pending, result = 1;
int inspected = 0;
const char *code = "invalid-state";
if (dir < 0 || flock(dir, LOCK_SH) || !state_layout(dir, 0) ||
!installed_valid(dir) || !empty_child(dir, "index") ||
!read_generation(dir, &generation)) goto done;
result = update_pending(dir);
if (result) { code = "incomplete-transaction"; result = result > 0 ? 5 : 1; goto done; }
result = 1;
pending = pending_child(dir, generation, digest, approved);
if (pending < 0) goto done;
if (!pending) { result = 6; code = "unavailable-reservation"; goto done; }
snapshot = holy_cache_snapshot(digest, root_path);
if (!snapshot) { result = 6; code = "unavailable-artifact"; goto done; }
inspected = 1;
result = holy_preview_local_format(snapshot, root_path, json);
if (result == 0 || result == 3 || result == 4) {
if (json)
printf("{\"schema\":\"holy-preview-1\",\"type\":\"reservation\",\"generation\":%llu,\"artifact\":\"%s\"}\n",
generation, digest);
else printf("reservation generation %llu artifact %s\n", generation, digest);
}
done:
if (result == 6) fprintf(stderr, "holypkg: reservation preflight unavailable\n");
if (json && !inspected && result != 0)
printf("{\"schema\":\"holy-preview-1\",\"type\":\"error\",\"code\":\"%s\"}\n", code);
if (snapshot) { unlink(snapshot); free(snapshot); }
if (dir >= 0) close(dir);
return result;
}
struct plan_hash {
int completed;
int accepted_privileged;
EVP_MD_CTX *hash;
size_t count;
int dir;
int claim_error;
};
static int path_available(int dir, const char *path, int directory)
{
int installed = child_dir(dir, "installed", 0), result = -1;
DIR *list;
struct dirent *entry;
if (installed < 0) return -1;
list = directory_stream(installed);
if (!list) { close(installed); return -1; }
errno = 0;
while ((entry = readdir(list))) {
int item, files, kind;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
item = child_dir(installed, entry->d_name, 0);
if (item < 0) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
close(item);
if (files < 0) goto done;
kind = holy_install_manifest_owns(files, path);
close(files);
if (kind < 0) goto done;
if (kind && (kind == 1 || !directory)) {
fprintf(stderr, "holypkg: path already claimed by %s\n", entry->d_name);
result = 0;
goto done;
}
errno = 0;
}
if (!errno) result = 1;
done:
closedir(list);
close(installed);
return result;
}
static int hash_text(EVP_MD_CTX *hash, const char *text)
{
char length[32];
size_t size = strlen(text);
int written = snprintf(length, sizeof length, "%zu:", size);
return written > 0 && (size_t)written < sizeof length &&
EVP_DigestUpdate(hash, length, (size_t)written) == 1 &&
EVP_DigestUpdate(hash, text, size) == 1;
}
static int plan_entry(void *context, const struct holy_manifest_entry *entry)
{
struct plan_hash *plan = context;
char attributes[128];
int written, available;
if ((entry->link && (entry->mode != 0777 || entry->link[0] == '/')) ||
!entry->path[0] || entry->path[0] == '/' ||
!strcmp(entry->path, "var/lib/holypkg") ||
!strncmp(entry->path, "var/lib/holypkg/", 16) ||
!strcmp(entry->path, "var/cache/holypkg") ||
!strncmp(entry->path, "var/cache/holypkg/", 18) ||
((entry->mode & 07000) &&
(!plan->accepted_privileged || entry->directory || entry->link ||
entry->hardlink || (entry->mode & 03000))) ||
entry->uid != (long long)geteuid() ||
entry->gid != (long long)getegid()) {
fprintf(stderr, "holypkg: plan requires files or relative symlinks and local ownership; unsupported path: %s\n",
entry->path);
return 0;
}
available = plan->completed ? 1 : path_available(plan->dir, entry->path, entry->directory);
if (available != 1) {
plan->claim_error = available == 0 ? 4 : 1;
return 0;
}
written = snprintf(attributes, sizeof attributes, "%c:%o:%lld:%lld:%lld:",
entry->directory ? 'd' : entry->link ? 'l' : entry->hardlink ? 'h' : 'f', entry->mode,
entry->uid, entry->gid, entry->size);
if (written <= 0 || (size_t)written >= sizeof attributes ||
!hash_text(plan->hash, entry->path) ||
!hash_text(plan->hash, attributes) ||
(entry->link && !hash_text(plan->hash, entry->link)) ||
(entry->hardlink && !hash_text(plan->hash, entry->hardlink)) ||
(entry->group && !hash_text(plan->hash, entry->group)) ||
(!entry->directory && !entry->link &&
EVP_DigestUpdate(plan->hash, entry->hash, 32) != 1)) return 0;
++plan->count;
return 1;
}
static int recorded_transform(const char *snapshot)
{
struct archive *archive = archive_read_new();
struct archive_entry *entry;
int status, ok = 0, found = 0;
if (!archive) return 0;
if (archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, snapshot, 8192) != ARCHIVE_OK)
goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
if (!strcmp(archive_entry_pathname(entry), "HOLY/transform")) {
if (found++ || archive_entry_filetype(entry) != AE_IFREG) goto done;
}
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
}
ok = status == ARCHIVE_EOF && found == 1;
done:
archive_read_free(archive);
return ok;
}
static int same_root(const char *root_path, const struct stat *before)
{
struct stat after;
int fd = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int ok = fd >= 0 && !fstat(fd, &after) &&
before->st_dev == after.st_dev && before->st_ino == after.st_ino;
if (fd >= 0) close(fd);
return ok;
}
static int installed_fields(int item, const char *const *keys,
const char *const *values, size_t fields)
{
struct stat st;
char *line = NULL;
size_t capacity = 0, number = 0;
ssize_t length;
int fd = openat(item, "meta", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
unsigned seen = 0;
int matches = 1, result = -1;
FILE *input;
if (fd < 0) return -1;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 ||
st.st_size > 16 * 1024 * 1024) { close(fd); return -1; }
input = fdopen(fd, "r");
if (!input) { close(fd); return -1; }
while ((length = getline(&line, &capacity, input)) >= 0) {
char **v = NULL, *error = NULL;
size_t count = 0;
++number;
if (memchr(line, '\0', (size_t)length) ||
!holy_lex(line, (size_t)length, &v, &count, "installed/meta", number, &error)) {
free(error);
goto done;
}
if (count && count != 2) { holy_tokens_free(v, count); goto done; }
if (count) {
size_t i;
for (i = 0; i < fields; ++i) if (!strcmp(v[0], keys[i])) {
if (seen & (1u << i)) { holy_tokens_free(v, count); goto done; }
seen |= 1u << i;
if (strcmp(v[1], values[i])) matches = 0;
}
}
holy_tokens_free(v, count);
}
if (!ferror(input) && seen == (1u << fields) - 1u && ftello(input) == st.st_size) result = matches;
done:
free(line);
fclose(input);
return result;
}
static int installed_name(int item, const char *name)
{
const char *keys[] = {"name"}, *values[] = {name};
return installed_fields(item, keys, values, 1);
}
static int installed_source_id(int item, char source[65])
{
struct stat st;
char digest[65];
if (!fstatat(item, "source", &st, AT_SYMLINK_NOFOLLOW))
return holy_source_instance(item, source, digest);
if (errno != ENOENT) return 0;
strcpy(source, "-");
return 1;
}
int holy_state_find_slot(const char *root_path, const char *source_id,
const char *name, const char *arch, const char *libc,
char digest[65])
{
unsigned long long generation;
int database = -1, installed = -1, result = 1;
DIR *list = NULL;
struct dirent *entry;
size_t found = 0;
digest[0] = 0;
if ((!valid_digest(source_id) && strcmp(source_id, "-")) || !name || !*name ||
(arch && !*arch) || (libc && !*libc)) return 2;
database = holy_state_lock(root_path, 0, &generation, &result);
if (database < 0) return result;
result = 1;
installed = child_dir(database, "installed", 0);
list = installed < 0 ? NULL : directory_stream(installed);
if (!list) goto done;
errno = 0;
while ((entry = readdir(list))) {
const char *arch_key[] = {"arch"}, *libc_key[] = {"libc"};
const char *arch_value[] = {arch}, *libc_value[] = {libc};
char actual[65];
int item, match;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
item = child_dir(installed, entry->d_name, 0);
if (item < 0) goto done;
match = installed_name(item, name);
if (match > 0 && arch) match = installed_fields(item, arch_key, arch_value, 1);
if (match > 0 && libc) match = installed_fields(item, libc_key, libc_value, 1);
if (match > 0) match = installed_source_id(item, actual) ?
!strcmp(actual, source_id) : -1;
close(item);
if (match < 0) goto done;
if (match) {
if (!valid_digest(entry->d_name)) goto done;
++found;
if (found == 1) strcpy(digest, entry->d_name);
}
errno = 0;
}
if (errno) goto done;
result = found > 1 ? 3 : found ? 0 : 6;
done:
if (result) fprintf(stderr, "holypkg: installed source slot %s for %s\n",
result == 3 ? "requires arch/libc choice" : "unavailable", name);
if (list) closedir(list);
if (installed >= 0) close(installed);
if (database >= 0) close(database);
if (result) digest[0] = 0;
return result;
}
struct hook_step {
char *interpreter;
char *path;
char *body;
size_t length;
};
struct hook_plan {
struct hook_step steps[64];
size_t count;
char hash[65];
};
static void free_hook_plan(struct hook_plan *plan)
{
size_t i;
for (i = 0; i < plan->count; ++i) {
free(plan->steps[i].interpreter);
free(plan->steps[i].path);
free(plan->steps[i].body);
}
}
static int hook_body(int root, int files, const char *path, const char *expected,
char **body, size_t *length)
{
struct open_how how = {0};
struct stat st;
unsigned char digest[32];
unsigned int n;
char actual[65];
int fd = -1, ok = 0;
size_t used = 0, i;
if (!valid_owner_path(path) || holy_install_manifest_owns(files, path) != 1 ||
holy_install_check_path(files, root, path) != 1) return 0;
how.flags = O_RDONLY | O_CLOEXEC;
how.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS | RESOLVE_NO_SYMLINKS;
fd = (int)syscall(SYS_openat2, root, path, &how, sizeof how);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 ||
st.st_size > 1024 * 1024) goto done;
*body = malloc((size_t)st.st_size + 1);
if (!*body) goto done;
while (used < (size_t)st.st_size) {
ssize_t got = read(fd, *body + used, (size_t)st.st_size - used);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) goto done;
used += (size_t)got;
}
for (i = 0; i < used; ++i)
if (((unsigned char)(*body)[i] < 32 && (*body)[i] != '\n' && (*body)[i] != '\t') ||
(unsigned char)(*body)[i] == 127) goto done;
(*body)[used] = 0;
if (EVP_Digest(*body, used, digest, &n, EVP_sha256(), NULL) != 1 || n != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(actual + i * 2, 3, "%02x", digest[i]);
if (strcmp(actual, expected)) goto done;
*length = used;
ok = 1;
done:
if (!ok) { free(*body); *body = NULL; }
if (fd >= 0) close(fd);
return ok;
}
static int hook_interpreter_digest(int root, const char *path, char output[65])
{
struct open_how how = {0};
struct stat st;
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
unsigned char buffer[8192], digest[32];
unsigned int size;
ssize_t got;
int fd, ok = 0;
size_t i;
if (!ctx) return 0;
how.flags = O_RDONLY | O_CLOEXEC;
how.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS;
fd = (int)syscall(SYS_openat2, root, path, &how, sizeof how);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) || !(st.st_mode & 0111) ||
EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1) goto done;
for (;;) {
got = read(fd, buffer, sizeof buffer);
if (got < 0 && errno == EINTR) continue;
if (got < 0) goto done;
if (!got) break;
if (EVP_DigestUpdate(ctx, buffer, (size_t)got) != 1) goto done;
}
if (EVP_DigestFinal_ex(ctx, digest, &size) != 1 || size != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", digest[i]);
ok = 1;
done:
if (fd >= 0) close(fd);
EVP_MD_CTX_free(ctx);
return ok;
}
static int prepare_hook_plan(int root, int item, const char *artifact,
unsigned long long generation, struct hook_plan *plan)
{
char *hooks = update_record(item, "hooks"), *cursor;
char number[32], device[96];
struct stat st;
EVP_MD_CTX *hash = EVP_MD_CTX_new();
unsigned char bytes[32];
unsigned int n;
int files = -1, ok = 0;
size_t i, line = 0;
if (!hooks || !*hooks || !hash || fstat(root, &st)) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (files < 0) goto done;
snprintf(device, sizeof device, "%ju:%ju", (uintmax_t)st.st_dev, (uintmax_t)st.st_ino);
snprintf(number, sizeof number, "%llu", generation);
if (EVP_DigestInit_ex(hash, EVP_sha256(), NULL) != 1 ||
!hash_text(hash, "holy-hook-plan-1") || !hash_text(hash, device) ||
!hash_text(hash, number) || !hash_text(hash, artifact) || !hash_text(hash, hooks)) goto done;
for (cursor = hooks; *cursor; ) {
char *end = strchr(cursor, '\n'), **v = NULL, *error = NULL;
char interpreter_hash[65];
size_t count = 0;
struct hook_step *step;
if (!end || plan->count >= 64 ||
!holy_lex(cursor, (size_t)(end - cursor), &v, &count,
"installed/hooks", ++line, &error)) {
free(error); holy_tokens_free(v, count); goto done;
}
if (!count) { holy_tokens_free(v, count); cursor = end + 1; continue; }
if (count != 6 || strcmp(v[0], "hook") || strcmp(v[1], "postinstall") ||
v[2][0] != '/' || !valid_owner_path(v[2] + 1) ||
!valid_owner_path(v[3]) || strcmp(v[4], "sha256") ||
!valid_digest(v[5])) {
holy_tokens_free(v, count); goto done;
}
step = &plan->steps[plan->count];
step->interpreter = strdup(v[2]);
step->path = strdup(v[3]);
if (!step->interpreter || !step->path ||
!hook_interpreter_digest(root, step->interpreter, interpreter_hash) ||
!hook_body(root, files, step->path, v[5], &step->body, &step->length) ||
!hash_text(hash, v[2]) || !hash_text(hash, v[3]) ||
!hash_text(hash, v[5]) || !hash_text(hash, interpreter_hash) ||
!hash_text(hash, step->body)) {
free(step->interpreter); free(step->path); free(step->body);
memset(step, 0, sizeof *step);
holy_tokens_free(v, count); goto done;
}
++plan->count;
holy_tokens_free(v, count);
cursor = end + 1;
}
if (!plan->count || EVP_DigestFinal_ex(hash, bytes, &n) != 1 || n != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(plan->hash + i * 2, 3, "%02x", bytes[i]);
ok = 1;
done:
if (files >= 0) close(files);
EVP_MD_CTX_free(hash);
free(hooks);
return ok;
}
static int hook_journal_write(int transactions, unsigned long long generation,
const char *artifact, const char *plan,
const char *stage, size_t next)
{
char record[320], temporary[43] = {0};
int fd, ok = 0;
int length = snprintf(record, sizeof record,
"format holy-hook-journal-1\ngeneration %llu\nartifact %s\nplan %s\nstage %s\nnext %zu\n",
generation, artifact, plan, stage, next);
if (length < 0 || length >= (int)sizeof record) return 0;
fd = holy_temporary_at(transactions, temporary);
if (fd < 0) return 0;
if (write_all(fd, record, (size_t)length) && !fsync(fd) && !close(fd)) {
fd = -1;
if (!renameat(transactions, temporary, transactions, "hook-journal") &&
!fsync(transactions)) ok = 1;
}
if (fd >= 0) close(fd);
if (!ok) unlinkat(transactions, temporary, 0);
return ok;
}
static int hook_journal_read(int transactions, unsigned long long *generation,
char artifact[65], char plan[65],
char stage[8], size_t *next)
{
char *record = update_record(transactions, "hook-journal"), canonical[320];
unsigned long long saved;
unsigned long index;
char digest[65], checksum[65], phase[8], extra;
int n, result = 0;
if (!record || strlen(record) > 319 ||
sscanf(record, "format holy-hook-journal-1\ngeneration %llu\nartifact %64[0-9a-f]\nplan %64[0-9a-f]\nstage %7[a-z]\nnext %lu\n%c",
&saved, digest, checksum, phase, &index, &extra) != 5 ||
!valid_digest(digest) || !valid_digest(checksum) ||
(strcmp(phase, "ready") && strcmp(phase, "running"))) goto done;
n = snprintf(canonical, sizeof canonical,
"format holy-hook-journal-1\ngeneration %llu\nartifact %s\nplan %s\nstage %s\nnext %lu\n",
saved, digest, checksum, phase, index);
if (n < 0 || n >= (int)sizeof canonical || strcmp(record, canonical)) goto done;
*generation = saved; *next = (size_t)index;
strcpy(artifact, digest); strcpy(plan, checksum); strcpy(stage, phase);
result = 1;
done:
free(record);
return result;
}
static int hook_journal_clean_temps(int transactions)
{
DIR *list = directory_stream(transactions);
struct dirent *entry;
int ok = 1;
if (!list) return 0;
errno = 0;
while ((entry = readdir(list))) {
struct stat st;
if (!temporary_name(entry->d_name)) { errno = 0; continue; }
if (fstatat(transactions, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(st.st_mode) || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid()) ||
unlinkat(transactions, entry->d_name, 0)) { ok = 0; break; }
errno = 0;
}
if (!entry && errno) ok = 0;
closedir(list);
return ok && !fsync(transactions);
}
static int run_hook_step(int root, const struct hook_step *step, int *exit_status)
{
pid_t child;
int status;
char *script = malloc(strlen(step->path) + 2);
*exit_status = -1;
if (!script) return 0;
script[0] = '/';
strcpy(script + 1, step->path);
child = fork();
if (child < 0) { free(script); return 0; }
if (!child) {
char *const argv[] = {step->interpreter, script, NULL};
char *const env[] = {"PATH=/usr/bin:/bin", "HOME=/", "LANG=C", NULL};
if (fchdir(root) || chroot(".") || chdir("/")) _exit(127);
execve(step->interpreter, argv, env);
_exit(127);
}
free(script);
while (waitpid(child, &status, 0) < 0) if (errno != EINTR) return 0;
if (WIFEXITED(status)) *exit_status = WEXITSTATUS(status);
else if (WIFSIGNALED(status)) *exit_status = 128 + WTERMSIG(status);
return *exit_status == 0;
}
static int complete_hooks(int item, int transactions, int dir,
unsigned long long generation)
{
char hooks_hash[65], completed[96], existing[96];
struct stat st;
int fd, n;
if (!instance_record_digest(item, "hooks", hooks_hash)) return 0;
n = snprintf(completed, sizeof completed, "completed sha256 %s\n", hooks_hash);
if (n < 0 || n >= (int)sizeof completed) return 0;
fd = openat(transactions, "hook-state-new", O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd >= 0) {
int written = write_all(fd, completed, (size_t)n) && !fsync(fd);
if (close(fd)) written = 0;
if (!written || fsync(transactions)) return 0;
} else if (errno == EEXIST) {
fd = openat(transactions, "hook-state-new", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
st.st_size != n || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid()) ||
read(fd, existing, (size_t)n) != n || memcmp(existing, completed, (size_t)n)) {
if (fd >= 0) close(fd);
return 0;
}
close(fd);
} else return 0;
if (renameat(transactions, "hook-state-new", item, "hooks-state") ||
fsync(transactions) || fsync(item)) return 0;
if (!set_generation(dir, generation + 1) ||
unlinkat(transactions, "hook-journal", 0) || fsync(transactions)) return 0;
return 1;
}
int holy_state_configure(const char *digest, const char *approved,
const char *root_path, int retry)
{
struct hook_plan plan = {0};
unsigned long long generation = 0, saved_generation = 0;
char saved_artifact[65], saved_plan[65], stage[8];
int root = -1, dir = -1, installed = -1, item = -1, transactions = -1;
int result = 1;
size_t i, next = 0;
if (!valid_digest(digest) || (approved && !valid_digest(approved))) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) goto done;
if (!retry) {
dir = holy_state_lock(root_path, approved != NULL, &generation, &result);
if (dir < 0) goto done;
} else {
dir = state_dir_at(root, 0);
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!read_generation(dir, &generation) || !installed_valid(dir)) goto done;
}
result = 1;
installed = child_dir(dir, "installed", 0);
transactions = child_dir(dir, "transactions", 0);
item = installed < 0 ? -1 : child_dir(installed, digest, 0);
if (transactions < 0 || item < 0) { result = 6; goto done; }
if (retry) {
if (!hook_journal_read(transactions, &saved_generation, saved_artifact,
saved_plan, stage, &next) ||
(saved_generation != generation && saved_generation + 1 != generation) ||
strcmp(saved_artifact, digest)) {
result = 5; goto done;
}
if (!hook_journal_clean_temps(transactions)) { result = 5; goto done; }
if (saved_generation + 1 == generation) {
char *record = update_record(item, "hooks-state");
int completed = record && !strncmp(record, "completed sha256 ", 17) &&
!strcmp(stage, "ready");
free(record);
if (!completed || unlinkat(transactions, "hook-journal", 0) || fsync(transactions)) {
result = 5; goto done;
}
printf("configured %s generation %llu recovered\n", digest, generation);
result = 0; goto done;
}
} else if (!hook_skipped(item)) { result = 6; goto done; }
result = 6;
if (!prepare_hook_plan(root, item, digest, generation, &plan)) goto done;
if (retry) {
if (strcmp(saved_plan, plan.hash) || next > plan.count) { result = 5; goto done; }
} else if (!approved) {
printf("configure-plan generation %llu artifact %s hooks %zu sha256 %s read-only\n",
generation, digest, plan.count, plan.hash);
for (i = 0; i < plan.count; ++i) {
printf("hook %zu postinstall interpreter %s script /%s uid 0 root %s\n",
i, plan.steps[i].interpreter, plan.steps[i].path, root_path);
fwrite(plan.steps[i].body, 1, plan.steps[i].length, stdout);
if (!plan.steps[i].length || plan.steps[i].body[plan.steps[i].length - 1] != '\n') putchar('\n');
}
result = ferror(stdout) ? 1 : 0;
goto done;
} else if (strcmp(approved, plan.hash)) { result = 3; goto done; }
if (geteuid() != 0) { result = 6; goto done; }
if (!retry) {
if (!hook_journal_write(transactions, generation, digest, plan.hash, "ready", 0)) {
result = 1; goto done;
}
} else if (!strcmp(stage, "running")) {
fprintf(stderr, "holypkg: retrying hook %zu after unknown result by explicit request\n", next);
}
result = 5;
for (i = next; i < plan.count; ++i) {
int succeeded, exit_status;
if (!hook_journal_write(transactions, generation, digest, plan.hash, "running", i)) goto done;
if (flock(dir, LOCK_UN)) goto done;
succeeded = run_hook_step(root, &plan.steps[i], &exit_status);
if (flock(dir, LOCK_EX)) goto done;
{
unsigned long long current;
if (!read_generation(dir, ¤t) || current != generation ||
!installed_valid(dir)) goto done;
}
if (!succeeded) {
fprintf(stderr, "holypkg: hook %zu exit=%d, external effects unknown; explicit retry required\n",
i, exit_status);
goto done;
}
if (!hook_journal_write(transactions, generation, digest, plan.hash, "ready", i + 1)) goto done;
}
if (!complete_hooks(item, transactions, dir, generation)) goto done;
printf("configured %s generation %llu hooks %zu\n", digest, generation + 1, plan.count);
result = 0;
done:
if (result && result != 3 && result != 6 && result != 5)
fprintf(stderr, "holypkg: hook configuration failed (status %d)\n", result);
free_hook_plan(&plan);
if (item >= 0) close(item);
if (transactions >= 0) close(transactions);
if (installed >= 0) close(installed);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
static int same_slot(const struct holy_package_identity *a, const char *a_source,
const struct holy_package_identity *b, const char *b_source)
{
return !strcmp(a_source, b_source) && !strcmp(a->name, b->name) &&
!strcmp(a->os, b->os) && !strcmp(a->arch, b->arch) && !strcmp(a->libc, b->libc);
}
static int slot_available_except(int dir, const struct holy_package_identity *identity,
const char *source_id, const char *replaced)
{
const char *keys[] = {"name", "os", "arch", "libc"};
const char *values[] = {identity->name, identity->os, identity->arch, identity->libc};
int installed = child_dir(dir, "installed", 0), available = -1;
DIR *list;
struct dirent *entry;
if (installed < 0) return -1;
list = directory_stream(installed);
if (!list) { close(installed); return -1; }
errno = 0;
while ((entry = readdir(list))) {
int item, match;
char source[65];
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (replaced && !strcmp(entry->d_name, replaced)) { errno = 0; continue; }
if (!strcmp(entry->d_name, identity->digest)) { available = 0; goto done; }
item = child_dir(installed, entry->d_name, 0);
if (item < 0) goto done;
match = installed_fields(item, keys, values, 4);
if (match > 0) {
if (!installed_source_id(item, source)) match = -1;
else match = !strcmp(source_id, source);
}
close(item);
if (match < 0) goto done;
if (match) { available = 0; goto done; }
errno = 0;
}
if (!errno) available = 1;
done:
closedir(list);
close(installed);
return available;
}
static int slot_available(int dir, const struct holy_package_identity *identity,
const char *source_id)
{
return slot_available_except(dir, identity, source_id, NULL);
}
static int inspect_plan(const char *root_path, int root, int dir,
unsigned long long generation, const char *digest,
char output[65], size_t *paths, char **graph_output, size_t *graph_length)
{
struct holy_package_identity identity = {0};
struct plan_hash plan = {0};
struct holy_resolution resolution = {0};
char *graph = NULL;
size_t graph_size = 0;
char generation_text[32], root_id[128];
unsigned char checksum[32];
unsigned int checksum_size;
size_t i;
struct stat root_st;
struct utsname host;
int result = 1;
char *snapshot = NULL;
if (fstat(root, &root_st)) goto done;
snapshot = holy_cache_snapshot(digest, root_path);
if (!snapshot) { result = 6; goto done; }
result = holy_preview_local_format(snapshot, root_path, -1);
if (result) goto done;
result = 6;
if (!holy_extract_preflight(snapshot) || !recorded_transform(snapshot) ||
!holy_package_identity(snapshot, &identity) ||
strcmp(identity.os, "linux") ||
strcmp(identity.libc, "nolibc") || strcmp(identity.digest, digest)) goto done;
if (strcmp(identity.arch, "noarch") &&
(uname(&host) ||
!((!strcmp(identity.arch, "x86_64") && !strcmp(host.machine, "x86_64")) ||
(!strcmp(identity.arch, "x86") && !strcmp(host.machine, "i686"))))) {
fprintf(stderr, "holypkg: plan requires native host architecture for static executables\n");
goto done;
}
{
const char *inputs[] = {snapshot};
result = holy_resolve_collect(inputs, 1, NULL, &resolution);
if (result) goto done;
result = 1;
if (!holy_resolution_record(&resolution, &graph, &graph_size)) goto done;
}
plan.hash = EVP_MD_CTX_new();
plan.dir = dir;
if (!plan.hash) { result = 1; goto done; }
snprintf(generation_text, sizeof generation_text, "%llu", generation);
if (snprintf(root_id, sizeof root_id, "%ju:%ju",
(uintmax_t)root_st.st_dev, (uintmax_t)root_st.st_ino) >=
(int)sizeof root_id) { result = 1; goto done; }
result = 6;
if (EVP_DigestInit_ex(plan.hash, EVP_sha256(), NULL) != 1 ||
!hash_text(plan.hash, "holy-readonly-plan-2") ||
!hash_text(plan.hash, root_id) ||
!hash_text(plan.hash, generation_text) || !hash_text(plan.hash, digest) ||
!hash_text(plan.hash, graph) ||
!holy_verify_visit(snapshot, plan_entry, &plan)) {
if (plan.claim_error) result = plan.claim_error;
goto done;
}
if (!holy_install_preflight(snapshot, root, 0)) { result = 4; goto done; }
result = slot_available(dir, &identity, "-");
if (result < 0) { result = 1; goto done; }
if (!result) {
fprintf(stderr, "holypkg: installed package slot already active: %s\n", identity.name);
result = 4;
goto done;
}
if (EVP_DigestFinal_ex(plan.hash, checksum, &checksum_size) != 1 ||
checksum_size != sizeof checksum) { result = 1; goto done; }
if (!same_root(root_path, &root_st)) { result = 4; goto done; }
for (i = 0; i < sizeof checksum; ++i)
snprintf(output + i * 2, 3, "%02x", checksum[i]);
output[64] = '\0';
*paths = plan.count;
if (graph_output) {
*graph_output = graph;
*graph_length = graph_size;
graph = NULL;
}
result = 0;
done:
if (result) fprintf(stderr, "holypkg: cannot form install plan (status %d)\n", result);
free(graph);
holy_resolution_free(&resolution);
EVP_MD_CTX_free(plan.hash);
holy_package_identity_free(&identity);
if (snapshot) { unlink(snapshot); free(snapshot); }
return result;
}
int holy_state_plan(const char *root_path)
{
struct stat root_st;
unsigned long long generation;
char digest[65], hash[65], approved[65];
size_t paths;
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, 0), pending, result = 1;
if (root < 0 || dir < 0 || flock(dir, LOCK_SH) ||
!state_layout(dir, 0) || !installed_valid(dir) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) goto done;
result = update_pending(dir);
if (result) { result = result > 0 ? 5 : 1; goto done; }
result = 1;
pending = pending_child(dir, generation, digest, approved);
if (pending < 0) goto done;
if (!pending) { result = 6; goto done; }
result = inspect_plan(root_path, root, dir, generation, digest, hash, &paths, NULL, NULL);
if (result) goto done;
if (fstat(root, &root_st)) { result = 1; goto done; }
printf("plan root %ju:%ju generation %llu artifact %s paths %zu sha256 %s read-only\n",
(uintmax_t)root_st.st_dev, (uintmax_t)root_st.st_ino,
generation, digest, paths, hash);
done:
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_approve(const char *hash, const char *root_path)
{
unsigned long long generation;
char digest[65], approved[65], actual[65], record[256];
char temp_name[43] = {0};
size_t paths, length;
int root, dir = -1, transactions = -1, temp = -1, result = 1;
if (!valid_digest(hash)) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) goto done;
dir = state_dir_at(root, 0);
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!installed_valid(dir) || !empty_child(dir, "index") ||
!read_generation(dir, &generation)) goto done;
result = update_pending(dir);
if (result) { result = result > 0 ? 5 : 1; goto done; }
result = 1;
result = pending_child(dir, generation, digest, approved);
if (result < 0) { result = 1; goto done; }
if (!result) { result = 6; goto done; }
if (approved[0]) { result = 5; goto done; }
result = inspect_plan(root_path, root, dir, generation, digest, actual, &paths, NULL, NULL);
if (result) goto done;
if (strcmp(hash, actual)) { result = 3; goto done; }
result = 1;
transactions = child_dir(dir, "transactions", 0);
if (transactions < 0) goto done;
length = (size_t)snprintf(record, sizeof record,
"format holy-reservation-1\nstage approved\ngeneration %llu\nartifact %s\nplan %s\n",
generation, digest, actual);
if (length >= sizeof record) goto done;
temp = holy_temporary_at(transactions, temp_name);
if (temp < 0 || write(temp, record, length) != (ssize_t)length ||
fsync(temp)) goto done;
if (close(temp)) { temp = -1; goto done; }
temp = -1;
if (renameat(transactions, temp_name, transactions, "pending") ||
fsync(transactions)) goto done;
printf("approved %s generation %llu artifact %s\n", actual, generation, digest);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: plan approval failed (status %d)\n", result);
if (temp >= 0) close(temp);
if (temp_name[0] && transactions >= 0) unlinkat(transactions, temp_name, 0);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_recheck(const char *root_path)
{
unsigned long long generation;
char digest[65], approved[65], actual[65];
size_t paths;
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, 0), pending, result = 1;
if (dir < 0 || flock(dir, LOCK_SH) || !state_layout(dir, 0) ||
!installed_valid(dir) || !empty_child(dir, "index") ||
!read_generation(dir, &generation)) goto done;
result = update_pending(dir);
if (result) { result = result > 0 ? 5 : 1; goto done; }
result = 1;
pending = pending_child(dir, generation, digest, approved);
if (pending < 0) goto done;
if (!pending || !approved[0]) { result = 5; goto done; }
result = inspect_plan(root_path, root, dir, generation, digest, actual, &paths, NULL, NULL);
if (result) goto done;
if (strcmp(approved, actual)) { result = 3; goto done; }
printf("rechecked %s generation %llu artifact %s paths %zu\n",
approved, generation, digest, paths);
done:
if (result) fprintf(stderr, "holypkg: approved plan recheck failed (status %d)\n", result);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
static int write_all(int fd, const void *data, size_t length)
{
const char *p = data;
while (length) {
ssize_t sent = write(fd, p, length);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) return 0;
p += sent;
length -= (size_t)sent;
}
return 1;
}
static int record_file(int dir, const char *name, const void *data, size_t length)
{
int fd = openat(dir, name, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
int ok;
if (fd < 0) return 0;
ok = write_all(fd, data, length) && !fsync(fd);
if (close(fd)) ok = 0;
return ok && !fsync(dir);
}
static int instance_preflight(const char *snapshot)
{
static const char *const names[] = {
"HOLY/meta", "HOLY/files", "HOLY/deps", "HOLY/origin", "HOLY/provides"
};
struct archive *archive = archive_read_new();
struct archive_entry *entry;
int status, ok = 0;
unsigned seen = 0;
size_t i;
if (!archive) return 0;
if (archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, snapshot, 8192) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
for (i = 0; i < sizeof names / sizeof *names; ++i)
if (path && !strcmp(path, names[i])) break;
if (i < sizeof names / sizeof *names) {
if (seen & (1u << i) || archive_entry_filetype(entry) != AE_IFREG ||
archive_entry_size(entry) < 0 ||
archive_entry_size(entry) > 16 * 1024 * 1024) goto done;
seen |= 1u << i;
}
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
}
ok = status == ARCHIVE_EOF && seen == (1u << 5) - 1u;
done:
archive_read_free(archive);
return ok;
}
static char *read_hooks(const char *snapshot, size_t *length)
{
struct archive *archive = archive_read_new();
struct archive_entry *entry;
char *text = NULL;
int status;
*length = 0;
if (!archive || archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, snapshot, 8192) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
la_int64_t size = archive_entry_size(entry);
if (!path || strcmp(path, "HOLY/hooks")) {
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
continue;
}
if (text || archive_entry_filetype(entry) != AE_IFREG || size < 0 ||
size > 1024 * 1024) goto done;
text = malloc((size_t)size + 1);
if (!text) goto done;
{
size_t used = 0;
while (used < (size_t)size) {
la_ssize_t got = archive_read_data(archive, text + used, (size_t)size - used);
if (got <= 0) goto done;
used += (size_t)got;
}
}
{
size_t i;
for (i = 0; i < (size_t)size; ++i)
if (((unsigned char)text[i] < 32 && text[i] != '\n' && text[i] != '\t') ||
(unsigned char)text[i] == 127) goto done;
}
text[size] = 0;
*length = (size_t)size;
}
if (status == ARCHIVE_EOF && text) {
archive_read_free(archive);
return text;
}
done:
free(text);
if (archive) archive_read_free(archive);
return NULL;
}
static int save_instance(int installed, const char *digest, const char *snapshot,
unsigned long long generation, const char *graph, size_t graph_length,
const char *reason, const char *source_record,
const char *architecture, int privileged, int skip_hooks)
{
static const char *const names[] = { "meta", "files", "deps", "origin", "provides", "hooks", "transform" };
struct archive *archive = NULL;
struct archive_entry *entry;
char buffer[65536], state[1024], graph_hash[65], source[65], source_hash[65], claims[65];
int item = -1, status, ok = 0;
unsigned seen = 0;
size_t i;
if (architecture && !architecture_valid(architecture)) return 0;
if (mkdirat(installed, digest, 0700)) return 0;
if (fsync(installed)) return 0;
item = child_dir(installed, digest, 0);
if (item < 0) goto done;
archive = archive_read_new();
if (!archive || archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, snapshot, 8192) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
int fd;
la_ssize_t got;
la_int64_t count = 0;
for (i = 0; i < sizeof names / sizeof *names; ++i)
if (path && !strncmp(path, "HOLY/", 5) && !strcmp(path + 5, names[i])) break;
if (i == sizeof names / sizeof *names) {
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
continue;
}
if (seen & (1u << i) || archive_entry_filetype(entry) != AE_IFREG ||
archive_entry_size(entry) < 0 || archive_entry_size(entry) > 16 * 1024 * 1024)
goto done;
fd = openat(item, names[i], O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) goto done;
while ((got = archive_read_data(archive, buffer, sizeof buffer)) > 0) {
if (count > archive_entry_size(entry) - got ||
!write_all(fd, buffer, (size_t)got)) break;
count += got;
}
if (got != 0 || count != archive_entry_size(entry) || fsync(fd)) {
close(fd);
goto done;
}
if (close(fd)) goto done;
seen |= 1u << i;
}
if (status != ARCHIVE_EOF || seen != (1u << 7) - 1u) goto done;
{
char hooks_hash[65], hook_state[96];
struct stat hooks_stat;
int hooks = openat(item, "hooks", O_RDONLY | O_NOFOLLOW | O_CLOEXEC);
int has_hooks = hooks >= 0 && !fstat(hooks, &hooks_stat) && hooks_stat.st_size > 0;
if (hooks >= 0) close(hooks);
if (has_hooks != !!skip_hooks) goto done;
if (has_hooks) {
size_t n;
if (!instance_record_digest(item, "hooks", hooks_hash)) goto done;
n = (size_t)snprintf(hook_state, sizeof hook_state, "skipped sha256 %s\n", hooks_hash);
if (n >= sizeof hook_state || !record_file(item, "hooks-state", hook_state, n)) goto done;
}
}
if (!record_file(item, "graph", graph, graph_length) ||
!graph_digest(item, graph_hash)) goto done;
if (!instance_record_digest(item, "provides", claims)) goto done;
strcpy(source, "-"); strcpy(source_hash, "-");
if (source_record && (!record_file(item, "source", source_record, strlen(source_record)) ||
!holy_source_instance(item, source, source_hash))) goto done;
i = (size_t)snprintf(state, sizeof state,
"format holy-instance-%d\nsource-id %s\nsource-record %s\ndelivery local\nreason %s\nartifact %s\ngraph %s\nprovides %s\n%s%s%s%s%s%sgeneration %llu\n",
privileged ? architecture ? 7 : 6 : architecture ? 5 : 4,
source, source_hash, reason, digest, graph_hash, claims,
architecture ? "architecture " : "", architecture ? architecture : "", architecture ? "\n" : "",
privileged ? "privileged " : "", privileged ? digest : "", privileged ? "\n" : "",
generation + 1);
if (i >= sizeof state || !record_file(item, "state", state, i) || fsync(item)) goto done;
ok = 1;
done:
if (archive) archive_read_free(archive);
if (item >= 0) close(item);
return ok;
}
int holy_state_apply(const char *root_path)
{
char digest[65], approved[65], actual[65], journal[256], generation_record[32];
char temp_name[43] = {0};
char *snapshot = NULL, *graph = NULL;
size_t graph_length = 0;
unsigned long long generation;
struct stat st;
size_t paths, length;
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, 0);
int transactions = -1, installed = -1, temp = -1, journaled = 0, result = 1;
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!installed_valid(dir) || !empty_child(dir, "index") ||
!read_generation(dir, &generation) || generation == ULLONG_MAX) goto done;
result = update_pending(dir);
if (result) { result = result > 0 ? 5 : 1; goto done; }
result = 1;
result = pending_child(dir, generation, digest, approved);
if (result < 0) { result = 1; goto done; }
if (!result || !approved[0]) { result = 5; goto done; }
result = inspect_plan(root_path, root, dir, generation, digest, actual, &paths, &graph, &graph_length);
if (result) goto done;
if (strcmp(actual, approved)) { result = 3; goto done; }
snapshot = holy_cache_snapshot(digest, root_path);
if (!snapshot) { result = 6; goto done; }
if (!instance_preflight(snapshot)) { result = 6; goto done; }
if (!holy_install_preflight(snapshot, root, 0)) { result = 4; goto done; }
installed = child_dir(dir, "installed", 0);
transactions = child_dir(dir, "transactions", 0);
if (installed < 0 || transactions < 0 ||
!fstatat(installed, digest, &st, AT_SYMLINK_NOFOLLOW) ||
errno != ENOENT) { result = 4; goto done; }
length = (size_t)snprintf(journal, sizeof journal,
"format holy-journal-1\nstage applying\ngeneration %llu\nartifact %s\nplan %s\n",
generation, digest, approved);
if (length >= sizeof journal || !record_file(transactions, "journal", journal, length)) {
result = journal_exists(dir) ? 5 : 1;
goto done;
}
journaled = 1;
result = 5;
if (!holy_install_payload(snapshot, root, 0) ||
!save_instance(installed, digest, snapshot, generation, graph, graph_length, "explicit", NULL, NULL, 0, 0)) goto done;
length = (size_t)snprintf(generation_record, sizeof generation_record,
"%llu\n", generation + 1);
if (length >= sizeof generation_record) goto done;
temp = holy_temporary_at(dir, temp_name);
if (temp < 0 || !write_all(temp, generation_record, length) || fsync(temp)) goto done;
if (close(temp)) { temp = -1; goto done; }
temp = -1;
if (renameat(dir, temp_name, dir, "generation") || fsync(dir) ||
unlinkat(transactions, "pending", 0) || fsync(transactions) ||
unlinkat(transactions, "journal", 0) || fsync(transactions)) goto done;
printf("installed %s generation %llu paths %zu\n", digest, generation + 1, paths);
result = 0;
done:
free(graph);
if (result) fprintf(stderr, "holypkg: apply failed (status %d)%s\n", result,
journaled ? "; inspect incomplete transaction" : "");
if (temp >= 0) close(temp);
if (temp_name[0] && dir >= 0) unlinkat(dir, temp_name, 0);
if (snapshot) { unlink(snapshot); free(snapshot); }
if (installed >= 0) close(installed);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_abort_empty(const char *root_path)
{
unsigned long long generation, recorded;
char digest[65], plan[65], reserved[65], approved[65];
struct stat st, root_st;
char *snapshot = NULL;
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, 0);
int transactions = -1, installed = -1, result = 1, removing = 0;
if (dir < 0 || fstat(root, &root_st) || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) goto done;
result = journal_valid(dir, generation, &recorded, digest, plan, &removing);
if (result < 0) { result = 1; goto done; }
if (!result) { result = 5; goto done; }
result = 5;
if (removing || recorded != generation || !installed_valid(dir)) goto done;
transactions = child_dir(dir, "transactions", 0);
installed = child_dir(dir, "installed", 0);
if (transactions < 0 || installed < 0 ||
read_reservation(transactions, "pending", generation, reserved, approved) != 1 ||
strcmp(reserved, digest) || strcmp(approved, plan) ||
!fstatat(installed, digest, &st, AT_SYMLINK_NOFOLLOW) || errno != ENOENT) goto done;
snapshot = holy_cache_snapshot(digest, root_path);
if (!snapshot || !holy_install_preflight(snapshot, root, 0) ||
!same_root(root_path, &root_st)) goto done;
if (unlinkat(transactions, "journal", 0) || fsync(transactions)) goto done;
printf("aborted empty apply %s; approval retained\n", digest);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: cannot abort incomplete transaction without manual review\n");
if (snapshot) { unlink(snapshot); free(snapshot); }
if (installed >= 0) close(installed);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
struct check_finding {
char *path;
char *code;
char *target;
};
struct check_result {
char digest[65];
char architecture[96];
int intact;
struct check_finding *findings;
size_t count;
};
static void free_check_result(struct check_result *record)
{
size_t i;
for (i = 0; i < record->count; ++i) {
free(record->findings[i].path);
free(record->findings[i].code);
free(record->findings[i].target);
}
free(record->findings);
}
static int collect_finding(void *context, const char *path, const char *code,
const char *target)
{
struct check_result *record = context;
struct check_finding *grown;
char *copy, *code_copy, *target_copy = target ? strdup(target) : NULL;
if (record->count >= SIZE_MAX / sizeof *grown) return 0;
if (target && !target_copy) return 0;
copy = strdup(path);
if (!copy) { free(target_copy); return 0; }
code_copy = strdup(code);
if (!code_copy) { free(copy); free(target_copy); return 0; }
grown = realloc(record->findings, (record->count + 1) * sizeof *grown);
if (!grown) { free(copy); free(code_copy); free(target_copy); return 0; }
record->findings = grown;
grown[record->count].path = copy;
grown[record->count].code = code_copy;
grown[record->count++].target = target_copy;
return 1;
}
static int check_status(int checked, const struct check_result *record)
{
size_t i;
if (checked > 0) return 1;
if (checked < 0 || !record->count) return checked;
for (i = 0; i < record->count; ++i)
if (strcmp(record->findings[i].code, "unknown-interpreter") &&
strcmp(record->findings[i].code, "unavailable-path-resolution") &&
strcmp(record->findings[i].code, "unknown-loader-context")) return 0;
return 2;
}
static int check_unavailable(const struct check_result *record)
{
size_t i;
for (i = 0; i < record->count; ++i)
if (!strcmp(record->findings[i].code, "unavailable-path-resolution")) return 1;
return 0;
}
static void print_check_string(const char *value)
{
const unsigned char *p = (const unsigned char *)value;
putchar('"');
for (; *p; ++p) {
if (*p == '"' || *p == '\\') printf("\\%c", *p);
else if (*p < 32 || *p >= 127) printf("\\u%04x", (unsigned)*p);
else putchar(*p);
}
putchar('"');
}
static int print_check_result(const struct check_result *record,
unsigned long long generation)
{
size_t i, primary = 0;
if (record->intact == 0)
for (i = 0; i < record->count; ++i)
if (strcmp(record->findings[i].code, "unknown-interpreter") &&
strcmp(record->findings[i].code, "unavailable-path-resolution")) {
primary = i;
break;
}
printf("{\"schema\":\"holy-installed-check-1\",\"type\":\"artifact\",\"artifact\":\"%s\",\"state\":\"%s\",\"code\":",
record->digest, record->intact == 1 ? "pass" : record->intact == 2 ? "unknown" : "fail");
if (record->intact == 1 || !record->count) fputs("null", stdout);
else print_check_string(record->findings[primary].code);
printf(",\"generation\":%llu,\"findings\":[", generation);
for (i = 0; i < record->count; ++i) {
const struct check_finding *finding = &record->findings[i];
int unknown = !strcmp(finding->code, "unknown-interpreter") ||
!strcmp(finding->code, "unavailable-path-resolution") ||
!strcmp(finding->code, "unknown-loader-context") ||
!strcmp(finding->code, "skipped-hook");
printf("%s{\"code\":\"%s\",\"severity\":\"%s\",\"path\":",
i ? "," : "", finding->code, unknown ? "warning" : "error");
print_check_string(finding->path);
if (finding->target) {
fputs(",\"target\":", stdout);
print_check_string(finding->target);
}
fputs("}", stdout);
}
fputs("]", stdout);
if (record->architecture[0]) {
const char *space = strchr(record->architecture, ' ');
printf(",\"architecture\":{\"code\":\"accepted-arch-mismatch\",\"host\":\"%.*s\",\"target\":\"%s\",\"execution\":\"unverified\",\"scope\":\"artifact\"}",
(int)(space - record->architecture), record->architecture, space + 1);
}
puts("}");
return !ferror(stdout);
}
static int compare_check_result(const void *a, const void *b)
{
const struct check_result *left = a, *right = b;
return strcmp(left->digest, right->digest);
}
struct graph_elf {
struct holy_elf_info info;
int seen;
};
struct graph_soname {
const struct holy_elf_info *consumer;
const char *consumer_path;
const char *name;
int root, files;
int found, arch, versions, path_ok;
};
static int graph_alias_match(int root, int files, const char *directory,
const char *name, int regular)
{
struct open_how how = {0};
struct stat source, target;
size_t prefix = strlen(directory + 1), n = strlen(name);
char *path;
int alias, ok = 0;
if (prefix > SIZE_MAX - n - 2) return 0;
path = malloc(prefix + n + 2);
if (!path) return 0;
memcpy(path, directory + 1, prefix);
path[prefix] = '/';
memcpy(path + prefix + 1, name, n + 1);
if (holy_install_check_path(files, root, path) != 1) goto done;
how.flags = O_PATH | O_CLOEXEC;
how.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS;
alias = (int)syscall(SYS_openat2, root, path, &how, sizeof how);
if (alias < 0) goto done;
ok = !fstat(alias, &target) && !fstat(regular, &source) &&
S_ISREG(target.st_mode) && target.st_dev == source.st_dev &&
target.st_ino == source.st_ino;
close(alias);
done:
free(path);
return ok;
}
static int graph_consumer_elf(void *context, const char *path, int fd)
{
struct graph_elf *captured = context;
(void)path;
if (captured->seen || holy_elf_read_fd(fd, &captured->info)) return 0;
captured->seen = 1;
return 1;
}
static int graph_provider_elf(void *context, const char *path, int fd)
{
struct graph_soname *match = context;
struct holy_elf_info info = {0};
size_t i, j;
int status = holy_elf_read_fd(fd, &info), versions = 1;
if (status == 1) { holy_elf_free(&info); return 1; }
if (status) { holy_elf_free(&info); return 0; }
if (info.type != ET_DYN || (info.flags1 & DF_1_PIE) ||
!info.soname || strcmp(info.soname, match->name)) goto done;
match->found = 1;
if (!match->consumer) { match->arch = 1; match->versions = 1; goto done; }
if (info.elf_class != match->consumer->elf_class ||
info.machine != match->consumer->machine) goto done;
match->arch = 1;
for (i = 0; i < match->consumer->version_count; ++i) {
const struct holy_elf_version *want = &match->consumer->versions[i];
if (want->weak || strcmp(want->provider, match->name)) continue;
for (j = 0; j < info.defined_version_count; ++j)
if (!strcmp(info.defined_versions[j].name, want->name)) break;
if (j == info.defined_version_count) { versions = 0; break; }
}
if (versions) {
char **directories = NULL;
size_t count = 0, directory;
match->versions = 1;
if (loader_directories(match->consumer, match->consumer_path,
&directories, &count)) {
for (directory = 0; directory < count; ++directory) {
struct open_how how = {0};
char *alias;
int opened;
size_t a = strlen(directories[directory] + 1), b = strlen(match->name);
if (a > SIZE_MAX - b - 2) break;
alias = malloc(a + b + 2);
if (!alias) break;
memcpy(alias, directories[directory] + 1, a);
alias[a] = '/';
memcpy(alias + a + 1, match->name, b + 1);
how.flags = O_PATH | O_CLOEXEC;
how.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS;
opened = (int)syscall(SYS_openat2, match->root, alias, &how, sizeof how);
free(alias);
if (opened < 0) continue;
close(opened);
if (loader_file_match(directories[directory], path, match->name) ||
graph_alias_match(match->root, match->files,
directories[directory], match->name, fd))
match->path_ok = 1;
break;
}
}
free_loader_directories(directories, count);
}
done:
holy_elf_free(&info);
return 1;
}
static int check_soname_edge(int installed, int root, const char *consumer,
int provider, const char *path, const char *name,
const char **code)
{
struct graph_elf captured = {0};
struct graph_soname match = {0};
int item = -1, files = -1, provider_files = -1, status, result = -1;
size_t i;
*code = "broken-provider";
if (strcmp(path, "-")) {
item = child_dir(installed, consumer, 0);
files = item < 0 ? -1 : openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (files < 0) goto done;
status = holy_install_visit_regular(files, root, path, graph_consumer_elf, &captured);
if (status < 0) goto done;
if (!status || !captured.seen) { result = 0; goto done; }
for (i = 0; i < captured.info.needed_count; ++i)
if (!strcmp(captured.info.needed[i], name)) break;
if (i == captured.info.needed_count) goto done;
match.consumer = &captured.info;
match.consumer_path = path;
}
match.name = name;
provider_files = openat(provider, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (provider_files < 0) goto done;
match.root = root;
match.files = provider_files;
status = holy_install_visit_regular(provider_files, root, NULL, graph_provider_elf, &match);
if (status < 0) goto done;
if (!status) { result = 0; goto done; }
if (!match.found || !match.arch) {
*code = "missing-soname";
result = 0;
} else if (!match.versions) {
*code = "missing-symbol-version";
result = 0;
} else {
*code = "unknown-loader-context";
result = match.consumer && !match.path_ok ? 2 : 1;
}
done:
holy_elf_free(&captured.info);
if (provider_files >= 0) close(provider_files);
if (files >= 0) close(files);
if (item >= 0) close(item);
return result;
}
static int check_graph(int installed, int root, const char *digest,
struct check_result *record)
{
int item = child_dir(installed, digest, 0), fd, result = 1;
FILE *stream;
char *line = NULL;
size_t capacity = 0, number = 0;
ssize_t length;
if (item < 0) return -1;
fd = openat(item, "graph", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
close(item);
if (fd < 0) return errno == ENOENT ? 1 : -1;
stream = fdopen(fd, "r");
if (!stream) { close(fd); return -1; }
while ((length = getline(&line, &capacity, stream)) >= 0) {
char **v = NULL, *error = NULL;
size_t count = 0;
int provider, intact = 1, detailed = 0;
++number;
if (memchr(line, 0, (size_t)length) ||
!holy_lex(line, (size_t)length, &v, &count, "installed/graph", number, &error)) {
free(error); result = -1; break;
}
if (!count || strcmp(v[0], "edge")) { holy_tokens_free(v, count); continue; }
if (count != 7 || !valid_digest(v[1]) || !valid_digest(v[3])) {
holy_tokens_free(v, count); result = -1; break;
}
if (strcmp(v[1], digest)) { holy_tokens_free(v, count); continue; }
provider = child_dir(installed, v[3], 0);
if (provider < 0) intact = errno == ENOENT ? 0 : -1;
else if (!strcmp(v[5], "soname")) {
const char *code;
intact = check_soname_edge(installed, root, digest, provider,
v[4], v[6], &code);
if (intact == 2) {
if (record && !collect_finding(record, v[4], code, v[6])) result = -1;
else if (record && result == 1) result = 0;
} else if (!intact && record) {
if (!collect_finding(record, v[4], code, v[6])) result = -1;
else detailed = 1;
}
} else if (!strcmp(v[5], "interpreter") || !strcmp(v[5], "needed-path") ||
!strcmp(v[5], "shebang") || !strcmp(v[5], "path-alias")) {
int files = openat(provider, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
intact = files < 0 || v[6][0] != '/' ? -1 : holy_install_check_path(files, root, v[6] + 1);
if (files >= 0) close(files);
}
if (provider >= 0) close(provider);
if (intact < 0) result = -1;
else if (!intact) {
fprintf(stderr, "holypkg: broken-provider consumer=%s requirement=%s provider=%s target=%s\n",
digest, v[2], v[3], v[6]);
if (result == 1) result = 0;
if (record && !detailed &&
!collect_finding(record, v[6], "broken-provider", NULL)) result = -1;
}
holy_tokens_free(v, count);
if (result < 0) break;
}
if (ferror(stream)) result = -1;
free(line);
fclose(stream);
return result;
}
static int check_all(int installed, int root, unsigned long long generation, int json)
{
struct check_result *records = NULL;
DIR *list = NULL;
struct dirent *entry;
size_t count = 0, capacity = 0, i, passed = 0, failed = 0, unknown = 0;
int result = 1, unavailable = 0;
list = directory_stream(installed);
if (!list) return 1;
errno = 0;
while ((entry = readdir(list))) {
int item, files, status, skipped = 0;
struct check_result *grown;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (count == capacity) {
size_t next = capacity ? capacity * 2 : 8;
if (next < capacity || next > SIZE_MAX / sizeof *records) goto done;
grown = realloc(records, next * sizeof *records);
if (!grown) goto done;
records = grown;
capacity = next;
}
item = child_dir(installed, entry->d_name, 0);
if (item < 0) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
memset(&records[count], 0, sizeof records[count]);
if (files < 0 || !instance_architecture(item, records[count].architecture)) {
if (files >= 0) close(files);
close(item); goto done;
}
skipped = hook_skipped(item);
if (skipped && !collect_finding(&records[count], "HOLY/hooks", "skipped-hook", NULL)) {
close(files); close(item); goto done;
}
close(item);
memcpy(records[count].digest, entry->d_name, 65);
++count;
status = holy_install_check_report(files, root,
collect_finding, &records[count - 1]);
close(files);
if (status < 0) goto done;
if (skipped) status = 0;
{
int graph = check_graph(installed, root, entry->d_name, &records[count - 1]);
if (graph < 0) goto done;
if (!graph) status = 0;
}
records[count - 1].intact = check_status(status, &records[count - 1]);
if (check_unavailable(&records[count - 1])) unavailable = 1;
errno = 0;
}
if (errno) goto done;
if (count) qsort(records, count, sizeof *records, compare_check_result);
result = 0;
for (i = 0; i < count; ++i) {
int written;
if (records[i].intact == 1) ++passed;
else if (records[i].intact == 2) { ++unknown; result = 4; }
else { ++failed; result = 4; }
if (json)
written = print_check_result(&records[i], generation) ? 0 : -1;
else
written = printf("%s %s generation %llu\n",
records[i].intact == 1 ? "intact" :
records[i].intact == 2 ? "unknown" : "changed",
records[i].digest, generation);
if (written < 0) { result = 1; break; }
if (!json && records[i].architecture[0] &&
printf("accepted-arch-mismatch %s %s execution unverified scope artifact\n",
records[i].digest, records[i].architecture) < 0) { result = 1; break; }
if (!json) {
size_t j;
for (j = 0; j < records[i].count; ++j)
if (!strcmp(records[i].findings[j].code, "skipped-hook") &&
printf("skipped-hook %s installed-unconfigured\n", records[i].digest) < 0) {
result = 1; break;
}
if (result == 1) break;
}
}
if (result != 1) {
if (json) {
if (printf("{\"schema\":\"holy-installed-check-1\",\"type\":\"summary\",\"pass\":%zu,\"fail\":%zu,\"unknown\":%zu,\"coverage\":\"data-manifest-and-direct-shebang\"}\n",
passed, failed, unknown) < 0) result = 1;
} else if (count == 0 && puts("checked 0 installed packages") == EOF)
result = 1;
}
if (result == 4 && unavailable) result = 6;
done:
for (i = 0; i < count; ++i) free_check_result(&records[i]);
free(records);
closedir(list);
return result;
}
int holy_state_check(const char *digest, const char *root_path, int json)
{
struct check_result record = {0};
unsigned long long generation;
int root, dir = -1, installed = -1, item = -1, files = -1, result = 1, reported = 0;
int checked, all = !strcmp(digest, "--all");
if (!all && !valid_digest(digest)) {
if (json) puts("{\"schema\":\"holy-installed-check-1\",\"type\":\"error\",\"code\":\"invalid-argument\",\"status\":2}");
return 2;
}
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) goto done;
dir = state_dir_at(root, 0);
if (dir < 0 || flock(dir, LOCK_SH) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) goto done;
checked = transaction_pending(dir, generation);
if (checked < 0) goto done;
if (checked) { result = 5; goto done; }
if (!installed_valid(dir)) goto done;
installed = child_dir(dir, "installed", 0);
if (installed < 0) goto done;
if (all) {
result = check_all(installed, root, generation, json);
reported = result == 0 || result == 4 || result == 6;
goto done;
}
item = child_dir(installed, digest, 0);
if (item < 0) { result = 6; goto done; }
if (!instance_architecture(item, record.architecture)) goto done;
if (hook_skipped(item) &&
!collect_finding(&record, "HOLY/hooks", "skipped-hook", NULL)) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (files < 0) goto done;
checked = holy_install_check_report(files, root,
collect_finding, &record);
if (checked >= 0) {
int graph = check_graph(installed, root, digest, &record);
if (graph < 0) checked = -1;
else if (!graph) checked = 0;
}
if (checked > 0 && record.count) checked = 0;
checked = check_status(checked, &record);
result = checked > 0 && checked != 2 ? 0 : checked >= 0 ? 4 : 1;
if (result == 4 && check_unavailable(&record)) result = 6;
if (json && checked >= 0) {
memcpy(record.digest, digest, 65);
record.intact = checked;
if (!print_check_result(&record, generation)) { result = 1; goto done; }
printf("{\"schema\":\"holy-installed-check-1\",\"type\":\"summary\",\"pass\":%d,\"fail\":%d,\"unknown\":%d,\"coverage\":\"data-manifest-and-direct-shebang\"}\n",
checked == 1, checked == 0, checked == 2);
reported = 1;
} else if (checked >= 0 && !json)
printf("%s %s generation %llu\n",
checked == 1 ? "intact" : checked == 2 ? "unknown" : "changed", digest, generation);
if (!json && (result == 0 || result == 4) && record.architecture[0])
printf("accepted-arch-mismatch %s %s execution unverified scope artifact\n", digest, record.architecture);
if (!json && (result == 0 || result == 4) && record.count) {
size_t i;
for (i = 0; i < record.count; ++i)
if (!strcmp(record.findings[i].code, "skipped-hook"))
printf("skipped-hook %s installed-unconfigured\n", digest);
}
done:
free_check_result(&record);
if (result) fprintf(stderr, "holypkg: installed check failed (status %d)\n", result);
if (json && result != 0 && !reported) {
const char *code = result == 5 ? "incomplete-transaction" :
result == 6 ? "unavailable-instance" : "invalid-state";
printf("{\"schema\":\"holy-installed-check-1\",\"type\":\"error\",\"code\":\"%s\",\"status\":%d}\n",
code, result);
}
if (files >= 0) close(files);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
static int compare_installed_path(const void *left, const void *right)
{
const char *const *a = left, *const *b = right;
return strcmp(*a, *b);
}
int holy_state_files(const char *digest, const char *root_path)
{
unsigned long long generation;
char **paths = NULL, *line = NULL;
size_t count = 0, capacity = 0, length = 0, number = 0, i;
int database = -1, installed = -1, item = -1, fd = -1, result = 1;
struct stat st;
FILE *input = NULL;
ssize_t got;
if (!valid_digest(digest)) return 2;
database = holy_state_lock(root_path, 0, &generation, &result);
if (database < 0) return result;
result = 1;
if (!installed_valid(database) ||
(installed = child_dir(database, "installed", 0)) < 0 ||
(item = child_dir(installed, digest, 0)) < 0) {
if (item < 0 && installed >= 0 && errno == ENOENT) result = 6;
goto done;
}
fd = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
st.st_size < 0 || st.st_size > 16 * 1024 * 1024) goto done;
input = fdopen(fd, "r");
if (!input) goto done;
fd = -1;
while ((got = getline(&line, &length, input)) >= 0) {
char **fields = NULL, *error = NULL, *copy;
size_t fields_count = 0;
++number;
if (memchr(line, '\0', (size_t)got) ||
!holy_lex(line, (size_t)got, &fields, &fields_count,
"installed/files", number, &error)) {
free(error); holy_tokens_free(fields, fields_count); goto done;
}
if (fields_count) {
int link = !strcmp(fields[0], "symlink") ||
!strcmp(fields[0], "hardlink");
if ((link && fields_count != 13) ||
(!link && fields_count != 12) ||
(!link && strcmp(fields[0], "dir") && strcmp(fields[0], "file")) ||
!valid_owner_path(fields[1]) || count >= 100000) {
holy_tokens_free(fields, fields_count); goto done;
}
copy = strdup(fields[1]);
if (!copy) { holy_tokens_free(fields, fields_count); goto done; }
if (count == capacity) {
size_t next = capacity ? capacity * 2 : 32;
char **grown = realloc(paths, next * sizeof *grown);
if (!grown) { free(copy); holy_tokens_free(fields, fields_count); goto done; }
paths = grown; capacity = next;
}
paths[count++] = copy;
}
holy_tokens_free(fields, fields_count);
}
if (ferror(input) || ftello(input) != st.st_size) goto done;
qsort(paths, count, sizeof *paths, compare_installed_path);
for (i = 1; i < count; ++i) if (!strcmp(paths[i - 1], paths[i])) goto done;
for (i = 0; i < count; ++i) {
char *absolute = malloc(strlen(paths[i]) + 2);
if (!absolute) goto done;
absolute[0] = '/';
strcpy(absolute + 1, paths[i]);
print_check_string(absolute);
putchar('\n');
free(absolute);
}
if (ferror(stdout)) goto done;
result = 0;
done:
if (result) fprintf(stderr, "holypkg: installed file list failed (status %d)\n", result);
for (i = 0; i < count; ++i) free(paths[i]);
free(paths); free(line);
if (input) fclose(input);
if (fd >= 0) close(fd);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
if (database >= 0) close(database);
return result;
}
static int finish_remove_record(int dir, int installed, int item, int transactions,
const char *digest, unsigned long long generation,
int broken, int retired)
{
char generation_record[32], temp_name[43] = {0};
size_t length;
int temp = -1, work = -1, ok = 0;
struct stat old, observed;
work = remove_workdir(transactions, digest, generation, broken);
if (work < 0 || fstat(item, &old)) goto done;
if (!retired) {
if (!fstatat(work, "old-instance", &observed, AT_SYMLINK_NOFOLLOW) ||
errno != ENOENT || fsync(item) ||
renameat(installed, digest, work, "old-instance")) goto done;
}
if (fstatat(work, "old-instance", &observed, AT_SYMLINK_NOFOLLOW) ||
!S_ISDIR(observed.st_mode) || old.st_dev != observed.st_dev ||
old.st_ino != observed.st_ino || fsync(work) || fsync(installed) ||
fsync(transactions)) goto done;
length = (size_t)snprintf(generation_record, sizeof generation_record,
"%llu\n", generation + 1);
if (length >= sizeof generation_record) goto done;
temp = holy_temporary_at(dir, temp_name);
if (temp < 0 || !write_all(temp, generation_record, length) || fsync(temp)) goto done;
if (close(temp)) { temp = -1; goto done; }
temp = -1;
if (renameat(dir, temp_name, dir, "generation") || fsync(dir) ||
!commit_remove_record(transactions, digest, generation, broken) ||
unlinkat(transactions, "journal", 0) || fsync(transactions)) goto done;
ok = 1;
done:
if (work >= 0) close(work);
if (temp >= 0) close(temp);
if (temp_name[0]) unlinkat(dir, temp_name, 0);
return ok;
}
static int exclusive_claims(int installed, const char *digest, int files)
{
DIR *list = directory_stream(installed);
struct dirent *entry;
int result = -1;
if (!list) return -1;
errno = 0;
while ((entry = readdir(list))) {
int item, other, conflict;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..") ||
!strcmp(entry->d_name, digest)) continue;
item = child_dir(installed, entry->d_name, 0);
if (item < 0) goto done;
other = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
close(item);
if (other < 0) goto done;
conflict = holy_install_manifests_conflict(files, other);
close(other);
if (conflict < 0) goto done;
if (conflict) {
fprintf(stderr, "holypkg: conflicting installed ownership with %s\n", entry->d_name);
result = 0;
goto done;
}
errno = 0;
}
if (!errno) result = 1;
done:
closedir(list);
return result;
}
static int dependent_consumer(int installed, const char *digest)
{
DIR *list = directory_stream(installed);
struct dirent *entry;
int result = 0, found = 0;
if (!list) return -1;
errno = 0;
while ((entry = readdir(list))) {
FILE *stream;
char *line = NULL;
size_t capacity = 0, number = 0;
ssize_t length;
int item, fd;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
item = child_dir(installed, entry->d_name, 0);
if (item < 0) { result = -1; break; }
fd = openat(item, "graph", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
close(item);
if (fd < 0) {
if (errno == ENOENT) { errno = 0; continue; }
result = -1; break;
}
stream = fdopen(fd, "r");
if (!stream) { close(fd); result = -1; break; }
while ((length = getline(&line, &capacity, stream)) >= 0) {
char **v = NULL, *error = NULL;
size_t count = 0;
struct stat st;
++number;
if (memchr(line, 0, (size_t)length) ||
!holy_lex(line, (size_t)length, &v, &count, "installed/graph", number, &error)) {
free(error); result = -1; break;
}
if (count && !strcmp(v[0], "edge")) {
if (count != 7 || !valid_digest(v[1]) || !valid_digest(v[3])) result = -1;
else if (!strcmp(v[3], digest) && strcmp(v[1], digest)) {
if (!fstatat(installed, v[1], &st, AT_SYMLINK_NOFOLLOW)) {
if (!S_ISDIR(st.st_mode)) result = -1;
else {
fprintf(stderr, "holypkg: provider %s still required by %s requirement %s\n",
digest, v[1], v[2]);
found = 1;
}
} else if (errno != ENOENT) result = -1;
}
}
holy_tokens_free(v, count);
if (result) break;
}
if (ferror(stream)) result = -1;
free(line);
fclose(stream);
if (result) break;
errno = 0;
}
if (!entry && errno) result = -1;
closedir(list);
return result < 0 ? -1 : found;
}
int holy_state_remove(const char *digest, const char *root_path, int accept_broken)
{
unsigned long long generation;
char journal[256];
char reserved[65], approved[65];
size_t length;
int root, dir = -1, installed = -1, item = -1, files = -1;
int transactions = -1, journaled = 0, result = 1, pending, broken;
if (!valid_digest(digest)) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) goto done;
dir = state_dir_at(root, 0);
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation) ||
generation == ULLONG_MAX) goto done;
pending = transaction_pending(dir, generation);
if (pending < 0) goto done;
if (pending) { result = 5; goto done; }
if (!installed_valid(dir)) goto done;
pending = pending_child(dir, generation, reserved, approved);
if (pending < 0) goto done;
if (pending) { result = 5; goto done; }
installed = child_dir(dir, "installed", 0);
transactions = child_dir(dir, "transactions", 0);
if (installed < 0 || transactions < 0) goto done;
pending = dependent_consumer(installed, digest);
if (pending < 0) goto done;
if (pending && !accept_broken) { result = 3; goto done; }
if (pending) fprintf(stderr, "holypkg: accepted broken dependents for %s\n", digest);
broken = pending;
item = child_dir(installed, digest, 0);
if (item < 0) { result = 6; goto done; }
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (files < 0) goto done;
pending = holy_install_check_manifest(files, root);
if (pending != 1) { result = pending == 0 ? 4 : 1; goto done; }
pending = exclusive_claims(installed, digest, files);
if (pending != 1) { result = pending == 0 ? 4 : 1; goto done; }
length = (size_t)snprintf(journal, sizeof journal,
"format holy-journal-1\nstage removing\ngeneration %llu\nartifact %s\nplan %064d\n",
generation, digest, broken ? 1 : 0);
if (length >= sizeof journal || !record_file(transactions, "journal", journal, length)) {
result = journal_exists(dir) ? 5 : 1;
goto done;
}
journaled = 1;
result = 5;
if (!remove_record(transactions, digest, generation, broken, 1)) goto done;
if (!holy_install_remove_manifest(files, root)) goto done;
if (close(files)) { files = -1; goto done; }
files = -1;
if (!finish_remove_record(dir, installed, item, transactions, digest, generation,
broken, 0)) goto done;
printf("removed %s generation %llu\n", digest, generation + 1);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: remove failed (status %d)%s\n", result,
journaled ? "; inspect incomplete transaction" : "");
if (files >= 0) close(files);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_continue_remove(const char *root_path)
{
unsigned long long generation, recorded;
char digest[65], plan[65], reserved[65], approved[65];
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, 0);
int installed = -1, item = -1, transactions = -1, files = -1;
int result = 5, removing = 0, found, retired = 0, work = -1;
struct stat removed_st;
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation) ||
generation == ULLONG_MAX) { result = 1; goto done; }
found = journal_valid(dir, generation, &recorded, digest, plan, &removing);
if (found < 0) { result = 1; goto done; }
if (!found || removing != 1 ||
(strspn(plan, "0") != 64 && strcmp(plan,
"0000000000000000000000000000000000000000000000000000000000000001")) ||
!installed_valid(dir)) goto done;
transactions = child_dir(dir, "transactions", 0);
installed = child_dir(dir, "installed", 0);
if (transactions < 0 || installed < 0) { result = 1; goto done; }
if (read_reservation(transactions, "pending", generation, reserved, approved) != 0)
goto done;
if (recorded != generation) {
if (generation != recorded + 1 || !remove_record(transactions, digest, recorded,
plan[63] == '1', 0) || fstatat(installed, digest, &removed_st, AT_SYMLINK_NOFOLLOW) == 0 ||
errno != ENOENT || !commit_remove_record(transactions, digest, recorded,
plan[63] == '1') || unlinkat(transactions, "journal", 0) || fsync(transactions)) goto done;
printf("recovered removal %s generation %llu\n", digest, generation);
result = 0;
goto done;
}
item = child_dir(installed, digest, 0);
if (item < 0) {
if (errno != ENOENT || !remove_record(transactions, digest, generation,
plan[63] == '1', 0)) goto done;
work = remove_workdir(transactions, digest, generation, plan[63] == '1');
item = work < 0 ? -1 : child_dir(work, "old-instance", 0);
if (item < 0) goto done;
retired = 1;
}
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (files < 0 || exclusive_claims(installed, digest, files) != 1 ||
!remove_record(transactions, digest, generation, plan[63] == '1', 1) ||
!holy_install_finish_remove_manifest(files, root) ||
!finish_remove_record(dir, installed, item, transactions, digest, generation,
plan[63] == '1', retired)) goto done;
printf("recovered removal %s generation %llu\n", digest, generation + 1);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: removal recovery requires manual inspection (status %d)\n", result);
if (files >= 0) close(files);
if (item >= 0) close(item);
if (work >= 0) close(work);
if (installed >= 0) close(installed);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_finish_apply(const char *root_path)
{
unsigned long long generation, recorded, instance_generation;
char digest[65], plan[65], reserved[65], approved[65];
int root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
int dir = root < 0 ? -1 : state_dir_at(root, 0);
int transactions = -1, installed = -1, item = -1, files = -1;
int result = 5, removing = 0, found;
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) {
result = 1;
goto done;
}
found = journal_valid(dir, generation, &recorded, digest, plan, &removing);
if (found < 0) { result = 1; goto done; }
if (!found || removing || !generation || recorded != generation - 1 ||
!installed_valid(dir)) goto done;
transactions = child_dir(dir, "transactions", 0);
installed = child_dir(dir, "installed", 0);
if (transactions < 0 || installed < 0) { result = 1; goto done; }
found = read_reservation(transactions, "pending", recorded, reserved, approved);
if (found < 0 || (found && (strcmp(reserved, digest) || strcmp(approved, plan))))
goto done;
item = child_dir(installed, digest, 0);
if (item < 0 || !instance_state_generation(item, digest, &instance_generation) ||
instance_generation != generation) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (files < 0 || holy_install_check_manifest(files, root) != 1) goto done;
if (found && (unlinkat(transactions, "pending", 0) || fsync(transactions))) goto done;
if (unlinkat(transactions, "journal", 0) || fsync(transactions)) goto done;
printf("recovered install %s generation %llu\n", digest, generation);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: install recovery requires manual inspection (status %d)\n", result);
if (files >= 0) close(files);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
static int valid_owner_path(const char *path)
{
const char *part = path;
if (!*path) return 0;
while (*part) {
const char *end = strchr(part, '/');
size_t length = end ? (size_t)(end - part) : strlen(part), i;
if (!length || (length == 1 && part[0] == '.') ||
(length == 2 && part[0] == '.' && part[1] == '.')) return 0;
for (i = 0; i < length; ++i)
if ((unsigned char)part[i] < 32 ||
(unsigned char)part[i] == 127) return 0;
if (!end) break;
part = end + 1;
}
return 1;
}
static int compare_owner(const void *a, const void *b)
{
return strcmp((const char *)a, (const char *)b);
}
int holy_state_owner(const char *input, const char *root_path)
{
const char *path = *input == '/' ? input + 1 : input;
int root = -1, dir = -1, installed = -1, result = 1, found = 0;
unsigned long long generation;
char (*owners)[65] = NULL;
size_t count = 0, capacity = 0, i;
DIR *list = NULL;
struct dirent *entry;
if (!valid_owner_path(path)) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) goto done;
dir = state_dir_at(root, 0);
if (dir < 0 || flock(dir, LOCK_SH) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) goto done;
found = transaction_pending(dir, generation);
if (found < 0) goto done;
if (found) { result = 5; goto done; }
if (!installed_valid(dir)) goto done;
found = 0;
installed = child_dir(dir, "installed", 0);
if (installed < 0) goto done;
list = directory_stream(installed);
if (!list) goto done;
errno = 0;
while ((entry = readdir(list))) {
int item, files, kind;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
item = child_dir(installed, entry->d_name, 0);
if (item < 0) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
close(item);
if (files < 0) goto done;
kind = holy_install_manifest_owns(files, path);
close(files);
if (kind < 0) goto done;
if (kind) {
char (*grown)[65];
if (found && (found == 1 || kind == 1)) {
fprintf(stderr, "holypkg: conflicting installed owners for %s\n", path);
result = 4;
goto done;
}
if (count == capacity) {
size_t next = capacity ? capacity * 2 : 4;
if (next < capacity || next > SIZE_MAX / sizeof *owners) goto done;
grown = realloc(owners, next * sizeof *owners);
if (!grown) goto done;
owners = grown;
capacity = next;
}
memcpy(owners[count], entry->d_name, 65);
++count;
found = kind;
}
errno = 0;
}
if (errno) goto done;
result = found ? 0 : 6;
if (!result) {
qsort(owners, count, sizeof *owners, compare_owner);
for (i = 0; i < count; ++i)
if (printf("%s %s %s\n", owners[i],
found == 1 ? "file" : "directory", path) < 0) {
result = 1;
break;
}
}
done:
free(owners);
if (list) closedir(list);
if (installed >= 0) close(installed);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
struct set_item {
char *snapshot, *source_record;
char source_id[65];
char architecture[96];
struct holy_package_identity identity;
int reused;
int privileged;
int skipped_hooks;
};
struct privileged_scan {
char *first;
unsigned mode;
size_t count;
int unsupported;
};
static int scan_privileged(void *context, const struct holy_manifest_entry *entry)
{
struct privileged_scan *scan = context;
if (!(entry->mode & 07000)) return 1;
if (entry->directory || entry->link || entry->hardlink || entry->group ||
(entry->mode & 03000) || !(entry->mode & 0111)) {
scan->unsupported = 1;
return 0;
}
if (!scan->first) {
scan->first = strdup(entry->path);
if (!scan->first) return 0;
scan->mode = entry->mode;
}
++scan->count;
return 1;
}
struct set_claim {
char *path;
unsigned mode;
long long uid, gid;
int directory;
};
struct install_set {
struct holy_resolution resolution;
struct set_item *items;
size_t count, paths;
struct set_claim *claims;
size_t claim_count;
char *graph;
size_t graph_length;
char **bindings;
size_t binding_count;
char hash[65];
char host[65];
char catalog_index[65];
};
struct set_journal {
unsigned long long generation;
char hash[65], root[65];
char catalog_index[65];
char *choice;
char **digests, **bindings;
size_t count, binding_count;
char **accepted_arch, host[65];
size_t accepted_count;
char **accepted_privileged;
size_t privileged_count;
char **skipped_hooks;
size_t skipped_count;
};
static void free_set(struct install_set *set)
{
size_t i;
for (i = 0; i < set->count; ++i) {
if (set->items[i].snapshot) unlink(set->items[i].snapshot);
free(set->items[i].snapshot);
free(set->items[i].source_record);
holy_package_identity_free(&set->items[i].identity);
}
for (i = 0; i < set->claim_count; ++i) free(set->claims[i].path);
free(set->claims);
free(set->items);
free(set->graph);
for (i = 0; i < set->binding_count; ++i) free(set->bindings[i]);
free(set->bindings);
holy_resolution_free(&set->resolution);
memset(set, 0, sizeof *set);
}
static int set_claim(void *context, const struct holy_manifest_entry *entry)
{
struct install_set *set = context;
struct set_claim *claims;
size_t length = strlen(entry->path);
if (set->claim_count >= 1000000) return 0;
claims = realloc(set->claims, (set->claim_count + 1) * sizeof *claims);
if (!claims) return 0;
set->claims = claims;
if (length && entry->path[length - 1] == '/') --length;
claims = &set->claims[set->claim_count];
claims->path = strndup(entry->path, length);
if (!claims->path) return 0;
claims->directory = entry->directory;
claims->mode = entry->mode;
claims->uid = entry->uid;
claims->gid = entry->gid;
++set->claim_count;
return 1;
}
static int claim_order(const void *left, const void *right)
{
return strcmp(((const struct set_claim *)left)->path,
((const struct set_claim *)right)->path);
}
static int set_claims_valid(struct install_set *set)
{
size_t i;
if (set->claim_count) qsort(set->claims, set->claim_count,
sizeof *set->claims, claim_order);
for (i = 1; i < set->claim_count; ++i) {
const struct set_claim *a = &set->claims[i - 1], *b = &set->claims[i];
if (!strcmp(a->path, b->path) &&
(!a->directory || !b->directory || a->mode != b->mode ||
a->uid != b->uid || a->gid != b->gid)) {
fprintf(stderr, "holypkg: selected packages claim the same path: %s\n", a->path);
return 0;
}
}
return 1;
}
static int loader_directories(const struct holy_elf_info *elf, const char *consumer,
char ***directories, size_t *count)
{
const char *path = elf->runpath ? elf->runpath : elf->rpath;
const char *start, *end;
char **list = NULL;
size_t length, used = 0, capacity = 0;
*directories = NULL;
*count = 0;
if (!path || !*path) return 0;
for (start = path; ; start = end + 1) {
char *directory;
end = strchr(start, ':');
length = end ? (size_t)(end - start) : strlen(start);
if (length < 2 || start[length - 1] == '/') goto fail;
if (start[0] == '/') {
if (memchr(start, '$', length)) goto fail;
directory = strndup(start, length);
if (!directory) goto fail;
if (!valid_owner_path(directory + 1)) { free(directory); goto fail; }
} else if (length >= 7 && !memcmp(start, "$ORIGIN", 7) &&
(length == 7 || start[7] == '/') && consumer && *consumer) {
char *relative;
const char *suffix = start + 7;
size_t prefix = 0, k;
for (k = 0; consumer[k]; ++k) if (consumer[k] == '/') prefix = k + 1;
if (!prefix) goto fail;
if (length == 7) relative = strndup(consumer, prefix - 1);
else {
char *tail = strndup(suffix + 1, length - 8);
relative = tail ? holy_relative_link_path(consumer, strlen(consumer),
tail, "") : NULL;
free(tail);
}
if (!relative || !valid_owner_path(relative)) { free(relative); goto fail; }
directory = malloc(strlen(relative) + 2);
if (!directory) { free(relative); goto fail; }
directory[0] = '/';
strcpy(directory + 1, relative);
free(relative);
} else goto fail;
if (used == capacity) {
size_t next = capacity ? capacity * 2 : 4;
char **grown;
if (next < capacity || next > 1024) { free(directory); goto fail; }
grown = realloc(list, next * sizeof *list);
if (!grown) { free(directory); goto fail; }
list = grown; capacity = next;
}
list[used++] = directory;
if (!end) break;
}
*directories = list;
*count = used;
return 1;
fail:
while (used) free(list[--used]);
free(list);
return 0;
}
static void free_loader_directories(char **directories, size_t count)
{
while (count) free(directories[--count]);
free(directories);
}
static int loader_file_match(const char *directory, const char *path,
const char *name)
{
size_t length = strlen(directory + 1);
return !strncmp(path, directory + 1, length) && path[length] == '/' &&
!strcmp(path + length + 1, name);
}
static int scan_loader_alias(const struct holy_scan_result *scan,
const char *directory, const char *name,
const char *regular)
{
size_t prefix = strlen(directory + 1), n = strlen(name), hop, i;
char *current;
if (prefix > SIZE_MAX - n - 2) return 0;
current = malloc(prefix + n + 2);
if (!current) return 0;
memcpy(current, directory + 1, prefix);
current[prefix] = '/';
memcpy(current + prefix + 1, name, n + 1);
for (hop = 0; hop < 16; ++hop) {
const char *target = NULL;
char *next;
if (!strcmp(current, regular)) { free(current); return 1; }
for (i = 0; i < scan->symlink_count; ++i)
if (!strcmp(scan->symlinks[i].path, current)) {
target = scan->symlinks[i].target; break;
}
if (!target) break;
next = holy_relative_link_path(current, strlen(current), target, "");
if (!next) break;
free(current);
current = next;
}
free(current);
return 0;
}
static int explicit_elf_paths(const char *snapshot, int allow_soname)
{
struct holy_scan_result scan = {0};
size_t i, j;
int result = 6;
if (!holy_scan_collect(snapshot, &scan)) return 6;
result = 0;
for (i = 0; i < scan.count; ++i) {
const struct holy_scanned_file *file = &scan.files[i];
if (file->elf.interpreter && file->elf.interpreter[0] != '/') { result = 3; break; }
for (j = 0; j < file->elf.needed_count; ++j) {
char **directories = NULL;
size_t directory_count = 0;
int known = allow_soname && loader_directories(&file->elf, file->path,
&directories, &directory_count);
free_loader_directories(directories, directory_count);
if (file->elf.needed[j][0] != '/' && !known) {
fprintf(stderr, "holypkg: unknown-loader-search consumer=%s requirement=%s\n",
file->path, file->elf.needed[j]);
result = 3;
break;
}
}
if (result) break;
}
for (i = 0; !result && i < scan.script_count; ++i)
if (scan.scripts[i].kind != 1) {
fprintf(stderr, "holypkg: script-interpreter-decision consumer=%s interpreter=%s\n",
scan.scripts[i].path, scan.scripts[i].interpreter);
result = 3;
}
holy_scan_free(&scan);
return result;
}
static int selected_soname_paths(const struct holy_resolution *resolution,
const char *const *digests,
const char *const *snapshots, size_t count, int root,
const struct set_claim *claims, size_t claim_count)
{
size_t i, j, k;
for (i = 0; i < resolution->edge_count; ++i) {
const struct holy_resolved_edge *edge = &resolution->edges[i];
struct holy_scan_result consumer = {0}, provider = {0};
const struct holy_scanned_file *file = NULL;
char **directories = NULL;
size_t directory_count = 0, d;
int found = 0, ok = 0;
if (strcmp(edge->kind, "soname") || !strcmp(edge->path, "-")) continue;
for (j = 0; j < count; ++j)
if (!strcmp(digests[j], edge->consumer)) break;
if (j == count || !holy_scan_collect(snapshots[j], &consumer)) goto edge_done;
for (k = 0; k < consumer.count; ++k)
if (!strcmp(consumer.files[k].path, edge->path)) {
file = &consumer.files[k]; break;
}
if (!file || !loader_directories(&file->elf, file->path,
&directories, &directory_count)) goto edge_done;
for (j = 0; j < count; ++j)
if (!strcmp(digests[j], edge->provider)) break;
if (j == count || !holy_scan_collect(snapshots[j], &provider)) goto edge_done;
for (d = 0; d < directory_count; ++d) {
struct open_how how = {0};
char *alias;
size_t a = strlen(directories[d] + 1), b = strlen(edge->target);
int opened, occupied = 0;
if (a > SIZE_MAX - b - 2) break;
alias = malloc(a + b + 2);
if (!alias) break;
memcpy(alias, directories[d] + 1, a);
alias[a] = '/';
memcpy(alias + a + 1, edge->target, b + 1);
for (k = 0; k < provider.count; ++k) {
const struct holy_scanned_file *candidate = &provider.files[k];
if (candidate->elf.type == ET_DYN && !(candidate->elf.flags1 & DF_1_PIE) &&
candidate->elf.soname && !strcmp(candidate->elf.soname, edge->target) &&
candidate->elf.elf_class == file->elf.elf_class &&
candidate->elf.machine == file->elf.machine &&
!strcmp(candidate->runtime, file->runtime) &&
scan_loader_alias(&provider, directories[d], edge->target,
candidate->path)) { found = 1; break; }
}
if (found) { free(alias); break; }
{
struct set_claim key = {0};
key.path = alias;
occupied = bsearch(&key, claims, claim_count,
sizeof *claims, claim_order) != NULL;
}
how.flags = O_PATH | O_CLOEXEC;
how.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS;
if (!occupied) {
opened = (int)syscall(SYS_openat2, root, alias, &how, sizeof how);
if (opened >= 0) { occupied = 1; close(opened); }
else if (errno != ENOENT) occupied = 1;
}
free(alias);
if (occupied) break;
}
ok = found;
edge_done:
free_loader_directories(directories, directory_count);
holy_scan_free(&consumer);
holy_scan_free(&provider);
if (!ok) {
fprintf(stderr, "holypkg: unknown-loader-search consumer=%s requirement=%s provider=%s\n",
edge->path, edge->target, edge->provider);
return 0;
}
}
return 1;
}
static int set_soname_paths(const struct install_set *set, int root)
{
const char **digests = calloc(set->count, sizeof *digests);
const char **snapshots = calloc(set->count, sizeof *snapshots);
size_t i;
int ok = 0;
if (!digests || !snapshots) goto done;
for (i = 0; i < set->count; ++i) {
digests[i] = set->items[i].identity.digest;
snapshots[i] = set->items[i].snapshot;
}
ok = selected_soname_paths(&set->resolution, digests, snapshots, set->count,
root, set->claims, set->claim_count);
done:
free(digests); free(snapshots);
return ok;
}
static int instance_matches_snapshot(int item, const char *snapshot);
static int reuse_instance(int dir, int root, struct set_item *candidate,
unsigned long long generation, int completed,
const char *graph, EVP_MD_CTX *hash)
{
int installed = child_dir(dir, "installed", 0), item = -1, files = -1, fd = -1;
int result = -1;
unsigned long long recorded;
char state[640], prefix[80], *line = NULL;
size_t capacity = 0, old_count = 0, new_count = 0;
ssize_t got;
FILE *stream = NULL;
const char *part;
if (installed < 0) goto done;
item = child_dir(installed, candidate->identity.digest, 0);
if (item < 0) { if (errno == ENOENT) result = 0; goto done; }
if (!instance_state_generation(item, candidate->identity.digest, &recorded)) goto done;
if (recorded > generation) {
if (completed && recorded == generation + 1) result = 0;
goto done;
}
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (files < 0 || !instance_matches_snapshot(item, candidate->snapshot) ||
exclusive_claims(installed, candidate->identity.digest, files) != 1 ||
holy_install_check_manifest(files, root) != 1 ||
check_graph(installed, root, candidate->identity.digest, NULL) != 1) goto done;
fd = openat(item, "graph", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || !(stream = fdopen(fd, "r"))) goto done;
fd = -1;
snprintf(prefix, sizeof prefix, "edge \"%s\" ", candidate->identity.digest);
for (part = graph; *part; ) {
if (!strncmp(part, prefix, strlen(prefix))) ++new_count;
part = strchr(part, '\n');
if (!part) goto done;
++part;
}
while ((got = getline(&line, &capacity, stream)) >= 0) {
const char *match;
if (strncmp(line, prefix, strlen(prefix))) continue;
++old_count;
match = strstr(graph, line);
if (!match || (match != graph && match[-1] != '\n')) goto done;
}
if (ferror(stream) || old_count != new_count) goto done;
fd = openat(item, "state", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || (got = read(fd, state, sizeof state - 1)) <= 0) goto done;
state[got] = 0;
if (!hash_text(hash, "reuse-installed") || !hash_text(hash, state)) goto done;
{
char marker[96];
snprintf(marker, sizeof marker, "\nprivileged %s\n", candidate->identity.digest);
candidate->privileged = strstr(state, marker) != NULL;
}
if (!installed_source_id(item, candidate->source_id)) goto done;
candidate->reused = 1;
result = 1;
done:
if (stream) fclose(stream);
free(line);
if (fd >= 0) close(fd);
if (files >= 0) close(files);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
return result;
}
struct installed_candidates {
int installed;
char **digests;
size_t count;
const char *root;
};
struct named_claim { const char *name; int matched; };
static int match_named_claim(void *opaque, const char *kind, const char *name,
const char *arch, const char *libc, const char *version, const char *evidence)
{
struct named_claim *claim = opaque;
(void)arch; (void)libc; (void)version; (void)evidence;
if (!strcmp(kind, "package") && !strcmp(name, claim->name)) claim->matched = 1;
return 1;
}
static int installed_package_claim(struct installed_candidates *catalog, int item,
const char *digest, const char *name)
{
struct named_claim claim = {name, 0};
int fd, ok, matches = installed_name(item, name);
if (matches) return matches;
fd = openat(item, "provides", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd >= 0) {
ok = holy_provides_visit_fd(fd, match_named_claim, &claim);
close(fd);
} else {
char *snapshot;
if (errno != ENOENT) return -1;
snapshot = holy_cache_snapshot(digest, catalog->root);
if (!snapshot) {
fprintf(stderr, "holypkg: legacy capability metadata needs cached artifact %s\n", digest);
return -1;
}
ok = holy_provides_visit(snapshot, match_named_claim, &claim);
unlink(snapshot); free(snapshot);
}
return ok ? claim.matched : -1;
}
static int installed_command_claim(int item, const char *name)
{
static const char *const dirs[] = {"usr/bin/", "bin/", "usr/sbin/", "sbin/"};
size_t i, length = strlen(name);
int files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
int result = 0;
if (files < 0) return -1;
for (i = 0; i < sizeof dirs / sizeof *dirs; ++i) {
size_t prefix = strlen(dirs[i]);
char *path;
if (length > SIZE_MAX - prefix - 1) { result = -1; break; }
path = malloc(prefix + length + 1);
if (!path) { result = -1; break; }
memcpy(path, dirs[i], prefix);
memcpy(path + prefix, name, length + 1);
result = holy_install_manifest_executable(files, path);
free(path);
if (result) break;
}
close(files);
return result;
}
static int installed_soname_claim(struct installed_candidates *catalog,
int item, const char *digest, const char *name,
const char *arch, const char *libc)
{
struct holy_scan_result scan = {0};
const char *keys[] = {"arch", "libc"}, *values[] = {arch, libc};
char *snapshot;
size_t i, fields = 0;
int result = -1;
if (strcmp(arch, "any")) { keys[fields] = "arch"; values[fields++] = arch; }
if (strcmp(libc, "any")) { keys[fields] = "libc"; values[fields++] = libc; }
if (fields && (result = installed_fields(item, keys, values, fields)) <= 0) return result;
snapshot = holy_cache_snapshot(digest, catalog->root);
if (!snapshot) return -1;
if (!holy_scan_collect(snapshot, &scan)) goto done;
result = 0;
for (i = 0; i < scan.count; ++i) {
const struct holy_scanned_file *file = &scan.files[i];
if (file->elf.type == ET_DYN && !(file->elf.flags1 & DF_1_PIE) &&
file->elf.soname && !strcmp(file->elf.soname, name) &&
(!strcmp(arch, "any") || !strcmp(arch, holy_elf_machine(&file->elf))) &&
(!strcmp(libc, "any") || !strcmp(libc, file->runtime))) {
result = 1;
break;
}
}
done:
holy_scan_free(&scan);
unlink(snapshot); free(snapshot);
return result;
}
enum installed_query { QUERY_PACKAGE, QUERY_PATH, QUERY_COMMAND, QUERY_SONAME };
static int add_installed_candidates(struct installed_candidates *catalog,
enum installed_query query, const char *needle,
const char *arch, const char *libc)
{
DIR *list = directory_stream(catalog->installed);
struct dirent *entry;
int ok = 0;
if (!list) return 0;
errno = 0;
while ((entry = readdir(list))) {
int item, matches;
size_t i;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
for (i = 0; i < catalog->count; ++i)
if (!strcmp(entry->d_name, catalog->digests[i])) break;
if (i != catalog->count) { errno = 0; continue; }
item = child_dir(catalog->installed, entry->d_name, 0);
if (item < 0) goto done;
if (query == QUERY_PACKAGE)
matches = installed_package_claim(catalog, item, entry->d_name, needle);
else if (query == QUERY_COMMAND)
matches = installed_command_claim(item, needle);
else if (query == QUERY_SONAME)
matches = installed_soname_claim(catalog, item, entry->d_name, needle, arch, libc);
else {
int files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
matches = files < 0 ? -1 : holy_install_manifest_owns(files, needle);
if (files >= 0) close(files);
}
close(item);
if (matches < 0) goto done;
if (matches) {
if (catalog->count == 10000 ||
!(catalog->digests[catalog->count] = strdup(entry->d_name))) goto done;
++catalog->count;
}
errno = 0;
}
ok = errno == 0;
done:
closedir(list);
return ok;
}
static int installed_or_provider(void *context, const char *name,
const char *relation, const char *version)
{
(void)relation; (void)version;
return add_installed_candidates(context, QUERY_PACKAGE, name, NULL, NULL);
}
static int installed_requirement(void *context, const char *id,
const char *consumer, const char *kind, const char *name,
const char *arch, const char *libc, const char *relation,
const char *version, const char *original, const char *evidence)
{
(void)id; (void)consumer; (void)relation;
(void)version; (void)original; (void)evidence;
if (!strcmp(kind, "package"))
return add_installed_candidates(context, QUERY_PACKAGE, name, NULL, NULL);
if (!strcmp(kind, "package-or"))
return holy_package_or_each(name, installed_or_provider, context);
if (!strcmp(kind, "file") && name[0] == '/')
return add_installed_candidates(context, QUERY_PATH, name + 1, NULL, NULL);
if (!strcmp(kind, "command"))
return add_installed_candidates(context, QUERY_COMMAND, name, NULL, NULL);
if (!strcmp(kind, "soname") && !strcmp(relation, "any"))
return add_installed_candidates(context, QUERY_SONAME, name, arch, libc);
return 1;
}
static int installed_link_step(int root, const char *path, size_t *alias_length,
char **target)
{
const char *end = path;
struct open_how how = {0};
*target = NULL;
how.flags = O_PATH | O_NOFOLLOW | O_CLOEXEC;
how.resolve = RESOLVE_IN_ROOT | RESOLVE_NO_MAGICLINKS;
while (*end) {
char *prefix;
struct stat st;
int fd;
ssize_t length;
end = strchr(end, '/');
if (!end) end = path + strlen(path);
prefix = strndup(path, (size_t)(end - path));
if (!prefix) return 1;
fd = (int)syscall(SYS_openat2, root, prefix, &how, sizeof how);
free(prefix);
if (fd < 0) return errno == ENOENT ? 0 : errno == ENOSYS ? 6 : 1;
if (fstat(fd, &st)) { close(fd); return 1; }
if (S_ISLNK(st.st_mode)) {
*target = malloc(65537);
if (!*target) { close(fd); return 1; }
length = readlinkat(fd, "", *target, 65536);
close(fd);
if (length <= 0 || length == 65536) {
free(*target); *target = NULL; return 3;
}
(*target)[length] = 0;
*alias_length = (size_t)(end - path);
return 2;
}
close(fd);
if (!*end) break;
++end;
}
return 0;
}
static int candidate_link_step(const struct holy_scan_result *scans, size_t count,
const char *path, size_t *alias_length,
const char **target)
{
size_t i, j;
*alias_length = 0;
*target = NULL;
for (i = 0; i < count; ++i) for (j = 0; j < scans[i].symlink_count; ++j) {
const struct holy_scanned_symlink *link = &scans[i].symlinks[j];
size_t length = strlen(link->path);
if (strncmp(path, link->path, length) ||
(path[length] && path[length] != '/')) continue;
if (length > *alias_length) {
*alias_length = length;
*target = link->target;
} else if (length == *alias_length && strcmp(*target, link->target)) {
return 3;
}
}
return *target ? 2 : 0;
}
static int installed_script_candidates(struct installed_candidates *catalog,
int root, const char *interpreter,
const struct holy_scan_result *initial,
size_t initial_count)
{
char *path = strdup(interpreter + 1), *visited[16] = {0};
size_t hop, i;
int result = 1;
if (!path) return 1;
for (hop = 0; hop < 16; ++hop) {
size_t alias_length = 0;
char *target = NULL, *next;
const char *candidate_target = NULL;
size_t candidate_length = 0;
int step, candidate_step;
for (i = 0; i < hop; ++i) if (!strcmp(visited[i], path)) {
result = 3; goto done;
}
visited[hop] = strdup(path);
if (!visited[hop]) goto done;
step = installed_link_step(root, path, &alias_length, &target);
if (step == 6 || step == 3 || step == 1) { result = step; goto done; }
candidate_step = candidate_link_step(initial, initial_count, path,
&candidate_length, &candidate_target);
if (candidate_step == 3) { free(target); result = 3; goto done; }
if (step == 0 && candidate_step == 2) {
next = holy_relative_link_path(path, candidate_length,
candidate_target, path + candidate_length);
if (!next) { result = 3; goto done; }
free(path);
path = next;
continue;
}
if (step == 0) {
result = add_installed_candidates(catalog, QUERY_PATH, path, NULL, NULL) ? 0 : 1;
break;
}
next = holy_relative_link_path(path, alias_length, target,
path + alias_length);
free(target);
if (!next) { result = 3; goto done; }
path[alias_length] = 0;
if (!add_installed_candidates(catalog, QUERY_PATH, path, NULL, NULL)) {
free(next); goto done;
}
free(path);
path = next;
}
if (hop == 16) result = 3;
done:
for (i = 0; i < 16; ++i) free(visited[i]);
free(path);
return result;
}
static int discover_installed(const char *root_path, int dir,
const char *const *digests, size_t *count, char ***output)
{
struct installed_candidates catalog = {-1, NULL, 0, root_path};
struct holy_scan_result *initial = NULL;
size_t i, j, k, initial_count = *count;
int root = -1, result = 1;
catalog.digests = calloc(10000, sizeof *catalog.digests);
if (!catalog.digests) return 1;
catalog.installed = child_dir(dir, "installed", 0);
if (catalog.installed < 0) goto done;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0) goto done;
initial = calloc(initial_count, sizeof *initial);
if (!initial) goto done;
for (i = 0; i < *count; ++i) {
char *snapshot;
catalog.digests[i] = strdup(digests[i]);
if (!catalog.digests[i]) goto done;
++catalog.count;
snapshot = holy_cache_snapshot(catalog.digests[i], root_path);
if (!snapshot) { result = 6; goto done; }
j = holy_scan_collect(snapshot, &initial[i]);
unlink(snapshot); free(snapshot);
if (!j) { result = 6; goto done; }
}
for (i = 0; i < catalog.count; ++i) {
struct holy_scan_result extra = {0};
struct holy_scan_result *scan = i < initial_count ? &initial[i] : &extra;
char *snapshot = holy_cache_snapshot(catalog.digests[i], root_path);
int ok;
if (!snapshot) { result = 6; goto done; }
ok = holy_deps_visit(snapshot, installed_requirement, &catalog) &&
(i < initial_count || holy_scan_collect(snapshot, scan));
unlink(snapshot);
free(snapshot);
for (j = 0; ok && j < scan->count; ++j) {
const struct holy_elf_info *elf = &scan->files[j].elf;
if (elf->interpreter && elf->interpreter[0] == '/')
ok = add_installed_candidates(&catalog, QUERY_PATH, elf->interpreter + 1, NULL, NULL);
for (k = 0; ok && k < elf->needed_count; ++k)
if (elf->needed[k][0] == '/')
ok = add_installed_candidates(&catalog, QUERY_PATH, elf->needed[k] + 1, NULL, NULL);
else
ok = add_installed_candidates(&catalog, QUERY_SONAME, elf->needed[k],
holy_elf_machine(elf), scan->files[j].runtime);
}
for (j = 0; ok && j < scan->script_count; ++j)
if (scan->scripts[j].kind == 1) {
int status = installed_script_candidates(&catalog, root,
scan->scripts[j].interpreter,
initial, initial_count);
if (status) { result = status; ok = 0; }
}
if (i >= initial_count) holy_scan_free(&extra);
if (!ok) { if (result == 1) result = 6; goto done; }
}
result = 0;
done:
if (initial) {
for (i = 0; i < initial_count; ++i) holy_scan_free(&initial[i]);
free(initial);
}
if (root >= 0) close(root);
if (catalog.installed >= 0) close(catalog.installed);
*count = catalog.count;
*output = catalog.digests;
return result;
}
static int binding_valid(const char *binding)
{
return strlen(binding) == 129 && binding[64] == '=' &&
strspn(binding, "0123456789abcdef") == 64 && valid_digest(binding + 65);
}
static int instance_source_matches(int instance, const char *record)
{
char id[65], actual[65], expected[65];
unsigned char bytes[32];
unsigned int size;
size_t i;
struct stat st;
if (!record) return fstatat(instance, "source", &st, AT_SYMLINK_NOFOLLOW) && errno == ENOENT;
if (!holy_source_instance(instance, id, actual) ||
EVP_Digest(record, strlen(record), bytes, &size, EVP_sha256(), NULL) != 1 || size != 32) return 0;
for (i = 0; i < 32; ++i) snprintf(expected + i * 2, 3, "%02x", bytes[i]);
return !strcmp(expected, actual);
}
static int build_set(const char *root_path, int root, int dir,
unsigned long long generation, const char *const *digests,
size_t count, const char *choice, int completed,
const char *const *bindings, size_t binding_count,
const char *default_source_id,
const char *catalog_index,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged, size_t privileged_count,
const char *const *skipped_hooks, size_t skipped_count,
struct install_set *set)
{
char **snapshots = NULL;
char **candidates = NULL;
struct plan_hash plan = {0};
struct stat st;
struct utsname host;
char number[128];
unsigned char digest[32];
unsigned int length;
size_t i, j, initial_count = count;
int result = 1;
if (!count || count > 10000 || binding_count > 10000 ||
accepted_count > 10000 || privileged_count > 10000 || skipped_count > 10000) return 2;
if ((default_source_id && !valid_digest(default_source_id)) ||
(catalog_index && (!valid_digest(catalog_index) ||
(!default_source_id && !completed)))) return 2;
for (i = 0; i < accepted_count; ++i) {
if (!valid_digest(accepted_arch[i])) return 2;
for (j = 0; j < i; ++j) if (!strcmp(accepted_arch[i], accepted_arch[j])) return 2;
}
for (i = 0; i < privileged_count; ++i) {
if (!valid_digest(accepted_privileged[i])) return 2;
for (j = 0; j < i; ++j)
if (!strcmp(accepted_privileged[i], accepted_privileged[j])) return 2;
}
for (i = 0; i < skipped_count; ++i) {
if (!valid_digest(skipped_hooks[i])) return 2;
for (j = 0; j < i; ++j) if (!strcmp(skipped_hooks[i], skipped_hooks[j])) return 2;
}
for (i = 0; i < count; ++i) if (!valid_digest(digests[i])) return 2;
for (i = 0; i < binding_count; ++i) {
if (!binding_valid(bindings[i])) return 2;
for (j = 0; j < i; ++j) if (!strncmp(bindings[i], bindings[j], 64)) return 2;
for (j = 0; j < count; ++j)
if (!strncmp(bindings[i], digests[j], 64)) break;
if (j == count) return 2;
}
if (!completed) {
result = discover_installed(root_path, dir, digests, &count, &candidates);
if (result) goto done;
digests = (const char *const *)candidates;
result = 1;
}
snapshots = calloc(count, sizeof *snapshots);
if (!snapshots || fstat(root, &st) || uname(&host)) goto done;
if (strlen(host.machine) >= sizeof set->host) goto done;
strcpy(set->host, host.machine);
for (i = 0; i < count; ++i) {
snapshots[i] = holy_cache_snapshot(digests[i], root_path);
if (!snapshots[i]) { result = 6; goto done; }
}
result = holy_resolve_collect((const char *const *)snapshots, count, choice,
&set->resolution);
if (result) goto done;
for (i = 0; i < accepted_count; ++i) {
for (j = 0; j < set->resolution.artifact_count; ++j)
if (!strcmp(accepted_arch[i], set->resolution.artifacts[j])) break;
if (j == set->resolution.artifact_count) { result = 3; goto done; }
}
for (i = 0; i < privileged_count; ++i) {
for (j = 0; j < set->resolution.artifact_count; ++j)
if (!strcmp(accepted_privileged[i], set->resolution.artifacts[j])) break;
if (j == set->resolution.artifact_count) { result = 3; goto done; }
}
for (i = 0; i < skipped_count; ++i) {
for (j = 0; j < set->resolution.artifact_count; ++j)
if (!strcmp(skipped_hooks[i], set->resolution.artifacts[j])) break;
if (j == set->resolution.artifact_count) { result = 3; goto done; }
}
/* a caller binding is a decision about one artifact, so it must land in the set.
a source binding instead offers every staged candidate, and the resolver may
drop one, so those are not checked here. */
if (!catalog_index)
for (i = 0; i < binding_count; ++i) {
for (j = 0; j < set->resolution.artifact_count; ++j)
if (!strncmp(bindings[i], set->resolution.artifacts[j], 64)) break;
if (j == set->resolution.artifact_count) { result = 3; goto done; }
}
result = 1;
set->items = calloc(set->resolution.artifact_count, sizeof *set->items);
if (!set->items || !holy_resolution_record(&set->resolution, &set->graph,
&set->graph_length)) goto done;
set->count = set->resolution.artifact_count;
plan.hash = EVP_MD_CTX_new();
plan.dir = dir;
plan.completed = completed;
if (!plan.hash || EVP_DigestInit_ex(plan.hash, EVP_sha256(), NULL) != 1 ||
!hash_text(plan.hash, "holy-set-plan-1") ||
!hash_text(plan.hash, choice ? choice : "-") ||
!hash_text(plan.hash, set->graph)) goto done;
if (catalog_index) {
memcpy(set->catalog_index, catalog_index, 65);
if (!hash_text(plan.hash, "catalog-index") ||
!hash_text(plan.hash, catalog_index)) goto done;
}
snprintf(number, sizeof number, "%ju:%ju", (uintmax_t)st.st_dev, (uintmax_t)st.st_ino);
if (!hash_text(plan.hash, number)) goto done;
snprintf(number, sizeof number, "%llu", generation);
if (!hash_text(plan.hash, number)) goto done;
for (i = 0; i < set->count; ++i) {
struct set_item *item = &set->items[i];
for (j = 0; j < count; ++j)
if (!strcmp(digests[j], set->resolution.artifacts[i])) break;
if (j == count) goto done;
item->snapshot = snapshots[j];
snapshots[j] = NULL;
result = 6;
if (!holy_package_identity(item->snapshot, &item->identity) ||
strcmp(item->identity.digest, set->resolution.artifacts[i]) ||
strcmp(item->identity.os, "linux") ||
(strcmp(item->identity.libc, "nolibc") && strcmp(item->identity.libc, "glibc") &&
strcmp(item->identity.libc, "musl")) ||
!recorded_transform(item->snapshot) || !instance_preflight(item->snapshot)) goto done;
strcpy(item->source_id, "-");
result = explicit_elf_paths(item->snapshot, 1);
if (result) goto done;
result = reuse_instance(dir, root, item, generation, completed, set->graph, plan.hash);
if (result < 0) { result = 4; goto done; }
{
size_t hook_length;
char *hooks = read_hooks(item->snapshot, &hook_length);
int accepted = 0;
if (!hooks) { result = 6; goto done; }
for (j = 0; j < skipped_count; ++j)
if (!strcmp(skipped_hooks[j], item->identity.digest)) accepted = 1;
if (item->reused) result = accepted ? 3 : 0;
else if (accepted && !hook_length) result = 2;
else if (hook_length && !accepted) {
fprintf(stderr, "holypkg: decision-required hooks artifact=%s; --skip-hooks %s records installed-unconfigured\n",
item->identity.digest, item->identity.digest);
fwrite(hooks, 1, hook_length, stderr);
if (hooks[hook_length - 1] != '\n') fputc('\n', stderr);
result = 3;
} else {
item->skipped_hooks = accepted;
result = 0;
if (accepted && (!hash_text(plan.hash, "skipped-hooks") ||
!hash_text(plan.hash, item->identity.digest) ||
!hash_text(plan.hash, hooks))) result = 1;
}
free(hooks);
if (result) goto done;
}
{
struct privileged_scan scan = {0};
int accepted = 0, scanned = holy_verify_visit(item->snapshot, scan_privileged, &scan);
for (j = 0; j < privileged_count; ++j)
if (!strcmp(accepted_privileged[j], item->identity.digest)) accepted = 1;
if (!scanned) {
result = scan.unsupported ? 6 : 1;
free(scan.first); goto done;
}
if (item->reused) {
result = accepted ? 3 : item->privileged != !!scan.count ? 4 : 0;
} else if (accepted && !scan.count) result = 2;
else if (scan.count && !accepted) {
fprintf(stderr, "holypkg: decision-required privileged artifact=%s path=%s mode=%04o; --accept-privileged %s permits setuid placement\n",
item->identity.digest, scan.first, scan.mode, item->identity.digest);
result = 3;
} else {
item->privileged = accepted;
result = 0;
if (accepted && (!hash_text(plan.hash, "accepted-privileged") ||
!hash_text(plan.hash, item->identity.digest))) result = 1;
}
free(scan.first);
if (result) goto done;
}
for (j = 0; j < accepted_count; ++j)
if (!strcmp(accepted_arch[j], item->identity.digest)) break;
if (j < accepted_count) {
if (item->reused) { result = 3; goto done; }
if (native_architecture(host.machine, item->identity.arch)) { result = 2; goto done; }
snprintf(item->architecture, sizeof item->architecture, "%s %s", host.machine, item->identity.arch);
if (!architecture_valid(item->architecture)) { result = 2; goto done; }
if (!hash_text(plan.hash, "accepted-architecture") ||
!hash_text(plan.hash, item->identity.digest) ||
!hash_text(plan.hash, item->architecture)) { result = 1; goto done; }
} else if (item->reused) {
int installed = child_dir(dir, "installed", 0);
int existing = installed < 0 ? -1 : child_dir(installed, item->identity.digest, 0);
int valid = existing >= 0 && instance_architecture(existing, item->architecture);
if (existing >= 0) close(existing);
if (installed >= 0) close(installed);
if (!valid) { result = 4; goto done; }
}
if (!native_architecture(host.machine, item->identity.arch)) {
char expected[96];
snprintf(expected, sizeof expected, "%s %s", host.machine, item->identity.arch);
if (strcmp(expected, item->architecture)) {
fprintf(stderr, "holypkg: decision-required architecture artifact=%s host=%s target=%s; --accept-arch %s permits placement without proving execution\n",
item->identity.digest, host.machine, item->identity.arch, item->identity.digest);
result = 3; goto done;
}
}
for (j = 0; j < binding_count; ++j) if (!strncmp(bindings[j], item->identity.digest, 64)) {
char registry[65];
if (item->reused) { result = 3; goto done; }
result = holy_source_record(dir, bindings[j] + 65, &item->source_record, registry);
if (result) goto done;
memcpy(item->source_id, bindings[j] + 65, 65);
if (!hash_text(plan.hash, "source-binding") || !hash_text(plan.hash, registry) ||
!hash_text(plan.hash, item->source_record)) { result = 1; goto done; }
break;
}
if (default_source_id && j == binding_count && !item->reused) {
char registry[65];
for (j = 0; j < initial_count; ++j)
if (!strcmp(digests[j], item->identity.digest)) break;
if (j == initial_count) { result = 6; goto done; }
result = holy_source_record(dir, default_source_id,
&item->source_record, registry);
if (result) goto done;
memcpy(item->source_id, default_source_id, 65);
if (!hash_text(plan.hash, "source-binding") || !hash_text(plan.hash, registry) ||
!hash_text(plan.hash, item->source_record)) { result = 1; goto done; }
}
if (item->reused && !strcmp(item->identity.digest, set->resolution.root)) {
result = 3; goto done;
}
result = holy_preview_resolved(item->snapshot, root_path, completed || item->reused,
item->privileged, item->skipped_hooks || item->reused);
if (result) goto done;
for (j = 0; j < i; ++j)
if (same_slot(&set->items[j].identity, set->items[j].source_id,
&item->identity, item->source_id)) {
result = 4; goto done;
}
if (!completed && !item->reused) {
result = slot_available(dir, &item->identity, item->source_id);
if (result != 1) { result = result < 0 ? 1 : 4; goto done; }
if (!holy_install_preflight(item->snapshot, root, item->privileged)) { result = 4; goto done; }
}
result = 6;
plan.completed = completed || item->reused;
plan.accepted_privileged = item->privileged;
if (!hash_text(plan.hash, item->identity.digest) ||
!holy_verify_visit(item->snapshot, plan_entry, &plan)) {
if (plan.claim_error) result = plan.claim_error;
goto done;
}
result = 1;
if (!holy_verify_visit(item->snapshot, set_claim, set)) goto done;
}
if (!set_claims_valid(set)) { result = 4; goto done; }
if (!set_soname_paths(set, root)) { result = 3; goto done; }
if (!same_root(root_path, &st)) { result = 4; goto done; }
set->bindings = calloc(set->count, sizeof *set->bindings);
if (!set->bindings) goto done;
for (i = 0; i < set->count; ++i) if (set->items[i].source_record) {
char *binding = malloc(130);
if (!binding) goto done;
snprintf(binding, 130, "%s=%s", set->items[i].identity.digest,
set->items[i].source_id);
set->bindings[set->binding_count++] = binding;
}
if (EVP_DigestFinal_ex(plan.hash, digest, &length) != 1 || length != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(set->hash + i * 2, 3, "%02x", digest[i]);
set->paths = plan.count;
result = 0;
done:
if (snapshots) for (i = 0; i < count; ++i) {
if (snapshots[i]) unlink(snapshots[i]);
free(snapshots[i]);
}
free(snapshots);
if (candidates) {
for (i = 0; i < count; ++i) free(candidates[i]);
free(candidates);
}
EVP_MD_CTX_free(plan.hash);
return result;
}
static void free_set_journal(struct set_journal *journal)
{
size_t i;
for (i = 0; i < journal->count; ++i) free(journal->digests[i]);
free(journal->digests);
for (i = 0; i < journal->binding_count; ++i) free(journal->bindings[i]);
free(journal->bindings);
free(journal->choice);
for (i = 0; i < journal->accepted_count; ++i) free(journal->accepted_arch[i]);
free(journal->accepted_arch);
for (i = 0; i < journal->privileged_count; ++i) free(journal->accepted_privileged[i]);
free(journal->accepted_privileged);
for (i = 0; i < journal->skipped_count; ++i) free(journal->skipped_hooks[i]);
free(journal->skipped_hooks);
memset(journal, 0, sizeof *journal);
}
static int set_choice_valid(const char *choice)
{
const char *equal;
size_t i;
if (!strcmp(choice, "-")) return 1;
equal = strchr(choice, '=');
if (!equal || equal == choice || !valid_digest(equal + 1) ||
(size_t)(equal - choice) > 65536) return 0;
for (i = 0; choice + i < equal; ++i)
if (!((choice[i] >= 'a' && choice[i] <= 'z') ||
(choice[i] >= 'A' && choice[i] <= 'Z') ||
(choice[i] >= '0' && choice[i] <= '9') ||
choice[i] == '-' || choice[i] == '_' || choice[i] == '.')) return 0;
return 1;
}
static int read_set_journal(int dir, struct set_journal *journal)
{
int transactions = child_dir(dir, "transactions", 0), fd = -1, result = -1, version = 1;
struct stat st;
FILE *stream = NULL;
char *line = NULL;
size_t capacity = 0, number = 0, bytes = 0, roots = 0;
ssize_t got;
if (transactions < 0) return -1;
{
DIR *list = directory_stream(transactions);
struct dirent *entry;
int valid = 1;
if (!list) goto done;
errno = 0;
while ((entry = readdir(list))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (completed_update(transactions, entry->d_name)) { errno = 0; continue; }
if (strcmp(entry->d_name, "set-journal")) { valid = 0; break; }
errno = 0;
}
if (!entry && errno) valid = 0;
closedir(list);
if (!valid) {
struct stat other;
if (fstatat(transactions, "set-journal", &other, AT_SYMLINK_NOFOLLOW) &&
errno == ENOENT) result = 0;
goto done;
}
}
fd = openat(transactions, "set-journal", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) { result = errno == ENOENT ? 0 : -1; goto done; }
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 1 ||
st.st_size > 4 * 1024 * 1024 || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) goto done;
stream = fdopen(fd, "r");
if (!stream) goto done;
fd = -1;
while ((got = getline(&line, &capacity, stream)) >= 0) {
char *end;
bytes += (size_t)got;
if (bytes > 4 * 1024 * 1024 || !got || line[got - 1] != '\n' ||
memchr(line, 0, (size_t)got)) goto done;
line[got - 1] = 0;
if (number == 0) {
if (!strcmp(line, "format holy-set-journal-6")) version = 6;
else if (!strcmp(line, "format holy-set-journal-5")) version = 5;
else if (!strcmp(line, "format holy-set-journal-4")) version = 4;
else if (!strcmp(line, "format holy-set-journal-3")) version = 3;
else if (!strcmp(line, "format holy-set-journal-2")) version = 2;
else if (strcmp(line, "format holy-set-journal-1")) goto done;
} else if (number == 1) {
if (strncmp(line, "generation ", 11) || line[11] < '0' || line[11] > '9' ||
(line[11] == '0' && line[12])) goto done;
errno = 0;
journal->generation = strtoull(line + 11, &end, 10);
if (errno || *end || journal->generation == ULLONG_MAX) goto done;
} else if (number == 2) {
if (strncmp(line, "plan ", 5) || !valid_digest(line + 5)) goto done;
memcpy(journal->hash, line + 5, 65);
} else if (number == 3) {
if (strncmp(line, "root ", 5) || !valid_digest(line + 5)) goto done;
memcpy(journal->root, line + 5, 65);
} else if (number == 4) {
if (strncmp(line, "choice ", 7) || !set_choice_valid(line + 7)) goto done;
journal->choice = strdup(line + 7);
if (!journal->choice) goto done;
} else if (version >= 3 && number == 5) {
char architecture[96];
if (strncmp(line, "host ", 5) || strlen(line + 5) > 64) goto done;
snprintf(architecture, sizeof architecture, "%s x86", line + 5);
if (!architecture_valid(architecture)) goto done;
strcpy(journal->host, line + 5);
} else if ((version == 5 || version == 6) && number == 6 &&
!strncmp(line, "catalog-index ", 14)) {
if (strncmp(line, "catalog-index ", 14) ||
!valid_digest(line + 14)) goto done;
memcpy(journal->catalog_index, line + 14, 65);
} else if (version >= 3 && !strncmp(line, "accept-arch ", 12)) {
char **next;
size_t i;
if (!valid_digest(line + 12) || journal->accepted_count >= journal->count) goto done;
for (i = 0; i < journal->count; ++i) if (!strcmp(line + 12, journal->digests[i])) break;
if (i == journal->count) goto done;
for (i = 0; i < journal->accepted_count; ++i)
if (!strcmp(line + 12, journal->accepted_arch[i])) goto done;
next = realloc(journal->accepted_arch, (journal->accepted_count + 1) * sizeof *next);
if (!next) goto done;
journal->accepted_arch = next;
next[journal->accepted_count] = strdup(line + 12);
if (!next[journal->accepted_count]) goto done;
++journal->accepted_count;
} else if (version >= 4 && !strncmp(line, "accept-privileged ", 18)) {
char **next;
size_t i;
if (!valid_digest(line + 18) || journal->privileged_count >= journal->count) goto done;
for (i = 0; i < journal->count; ++i)
if (!strcmp(line + 18, journal->digests[i])) break;
if (i == journal->count) goto done;
for (i = 0; i < journal->privileged_count; ++i)
if (!strcmp(line + 18, journal->accepted_privileged[i])) goto done;
next = realloc(journal->accepted_privileged,
(journal->privileged_count + 1) * sizeof *next);
if (!next) goto done;
journal->accepted_privileged = next;
next[journal->privileged_count] = strdup(line + 18);
if (!next[journal->privileged_count]) goto done;
++journal->privileged_count;
} else if (version == 6 && !strncmp(line, "skip-hooks ", 11)) {
char **next;
size_t i;
if (!valid_digest(line + 11) || journal->skipped_count >= journal->count) goto done;
for (i = 0; i < journal->count; ++i)
if (!strcmp(line + 11, journal->digests[i])) break;
if (i == journal->count) goto done;
for (i = 0; i < journal->skipped_count; ++i)
if (!strcmp(line + 11, journal->skipped_hooks[i])) goto done;
next = realloc(journal->skipped_hooks, (journal->skipped_count + 1) * sizeof *next);
if (!next) goto done;
journal->skipped_hooks = next;
next[journal->skipped_count] = strdup(line + 11);
if (!next[journal->skipped_count]) goto done;
++journal->skipped_count;
} else if (version >= 2 && !strncmp(line, "binding ", 8)) {
char **next;
size_t i;
if (journal->accepted_count || journal->privileged_count || journal->skipped_count ||
!binding_valid(line + 8) || journal->binding_count >= journal->count) goto done;
for (i = 0; i < journal->count; ++i)
if (!strncmp(line + 8, journal->digests[i], 64)) break;
if (i == journal->count) goto done;
for (i = 0; i < journal->binding_count; ++i)
if (!strncmp(line + 8, journal->bindings[i], 64)) goto done;
next = realloc(journal->bindings, (journal->binding_count + 1) * sizeof *next);
if (!next) goto done;
journal->bindings = next;
next[journal->binding_count] = strdup(line + 8);
if (!next[journal->binding_count]) goto done;
++journal->binding_count;
} else {
char **next;
if (journal->binding_count || journal->accepted_count || journal->privileged_count ||
journal->skipped_count ||
strncmp(line, "artifact ", 9) || !valid_digest(line + 9) ||
journal->count >= 10000 ||
(journal->count && strcmp(journal->digests[journal->count - 1], line + 9) >= 0))
goto done;
next = realloc(journal->digests, (journal->count + 1) * sizeof *next);
if (!next) goto done;
journal->digests = next;
next[journal->count] = strdup(line + 9);
if (!next[journal->count]) goto done;
++journal->count;
if (!strcmp(line + 9, journal->root)) ++roots;
}
++number;
}
if (!ferror(stream) && bytes == (size_t)st.st_size && number >= 6 && roots == 1 &&
(version == 1 || (version == 2 && journal->binding_count) ||
(version == 3 && journal->accepted_count) ||
(version == 4 && journal->privileged_count) ||
(version == 5 && journal->catalog_index[0] && journal->binding_count) ||
(version == 6 && journal->skipped_count))) result = 1;
done:
free(line);
if (stream) fclose(stream);
if (fd >= 0) close(fd);
close(transactions);
if (result != 1) free_set_journal(journal);
return result;
}
static int set_journal_present(int dir)
{
struct set_journal journal = {0};
unsigned long long generation;
int result = read_set_journal(dir, &journal);
if (result == 1 && (!read_generation(dir, &generation) ||
(generation != journal.generation && generation != journal.generation + 1))) result = -1;
free_set_journal(&journal);
return result;
}
static int write_set_journal(int transactions, unsigned long long generation,
const struct install_set *set, const char *choice,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged, size_t privileged_count,
const char *const *skipped_hooks, size_t skipped_count)
{
char *record = NULL;
size_t length = 0, i;
FILE *stream = open_memstream(&record, &length);
int ok = 1;
if (!stream) return 0;
if (fprintf(stream, "format holy-set-journal-%d\ngeneration %llu\nplan %s\nroot %s\nchoice %s\n",
skipped_count ? 6 : set->catalog_index[0] ? 5 : privileged_count ? 4 :
accepted_count ? 3 : set->binding_count ? 2 : 1,
generation, set->hash, set->resolution.root, choice ? choice : "-") < 0) ok = 0;
if ((accepted_count || privileged_count || skipped_count || set->catalog_index[0]) &&
fprintf(stream, "host %s\n", set->host) < 0) ok = 0;
if (set->catalog_index[0] &&
fprintf(stream, "catalog-index %s\n", set->catalog_index) < 0) ok = 0;
for (i = 0; i < set->count && ok; ++i)
if (fprintf(stream, "artifact %s\n", set->items[i].identity.digest) < 0) ok = 0;
for (i = 0; i < set->binding_count && ok; ++i)
if (fprintf(stream, "binding %s\n", set->bindings[i]) < 0) ok = 0;
for (i = 0; i < accepted_count && ok; ++i)
if (fprintf(stream, "accept-arch %s\n", accepted_arch[i]) < 0) ok = 0;
for (i = 0; i < privileged_count && ok; ++i)
if (fprintf(stream, "accept-privileged %s\n", accepted_privileged[i]) < 0) ok = 0;
for (i = 0; i < skipped_count && ok; ++i)
if (fprintf(stream, "skip-hooks %s\n", skipped_hooks[i]) < 0) ok = 0;
if (fclose(stream)) ok = 0;
if (ok) ok = record_file(transactions, "set-journal", record, length);
free(record);
return ok;
}
static int set_generation(int dir, unsigned long long generation)
{
char record[32], temp_name[43] = {0};
size_t length = (size_t)snprintf(record, sizeof record, "%llu\n", generation);
int fd = holy_temporary_at(dir, temp_name), ok = 0;
if (fd < 0) return 0;
if (length < sizeof record && write_all(fd, record, length) && !fsync(fd) &&
!renameat(dir, temp_name, dir, "generation") && !fsync(dir)) ok = 1;
close(fd);
if (!ok) unlinkat(dir, temp_name, 0);
return ok;
}
static int state_set(const char *const *digests, size_t count, const char *choice,
const char *approved, const char *root_path,
const char *const *bindings, size_t binding_count,
const char *default_source_id,
const char *catalog_index,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged, size_t privileged_count,
const char *const *skipped_hooks, size_t skipped_count,
char plan_hash[65], int quiet)
{
struct install_set set = {0};
unsigned long long generation;
int root = -1, dir = -1, installed = -1, transactions = -1, result = 1, journaled = 0;
size_t i;
if (plan_hash) plan_hash[0] = 0;
if ((approved && !valid_digest(approved)) || (choice && !set_choice_valid(choice))) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
dir = root < 0 ? -1 : state_dir_at(root, 0);
if (dir < 0 || flock(dir, approved ? LOCK_EX : LOCK_SH) ||
!state_layout(dir, 0) || !read_generation(dir, &generation) ||
generation == ULLONG_MAX || !empty_child(dir, "index")) goto done;
if (!empty_child(dir, "transactions")) { result = 5; goto done; }
if (!installed_valid(dir)) goto done;
result = build_set(root_path, root, dir, generation, digests, count, choice, 0, bindings, binding_count,
default_source_id, catalog_index,
accepted_arch, accepted_count, accepted_privileged, privileged_count,
skipped_hooks, skipped_count, &set);
if (result) goto done;
if (!approved) {
if (plan_hash) memcpy(plan_hash, set.hash, 65);
if (quiet) goto done;
printf("plan-set generation %llu root %s artifacts %zu paths %zu sha256 %s read-only\n",
generation, set.resolution.root, set.count, set.paths, set.hash);
if (set.catalog_index[0]) printf("catalog-index %s\n", set.catalog_index);
for (i = 0; i < set.count; ++i)
printf("selected %s %s %s\n", set.items[i].identity.digest,
set.items[i].identity.name,
set.items[i].reused ? "installed" :
strcmp(set.items[i].identity.digest, set.resolution.root) ? "dependency" : "explicit");
for (i = 0; i < set.resolution.edge_count; ++i) {
const struct holy_resolved_edge *edge = &set.resolution.edges[i];
printf("requirement %s consumer %s provider %s %s %s\n",
edge->id, edge->consumer, edge->provider, edge->kind, edge->target);
}
for (i = 0; i < set.count; ++i) if (set.items[i].source_record)
printf("binding %s %s", set.items[i].identity.digest, set.items[i].source_record);
for (i = 0; i < set.count; ++i) if (set.items[i].architecture[0])
printf("architecture %s %s accepted-unverified scope artifact\n",
set.items[i].identity.digest, set.items[i].architecture);
for (i = 0; i < set.count; ++i) if (set.items[i].privileged)
printf("privileged %s setuid accepted scope artifact\n",
set.items[i].identity.digest);
for (i = 0; i < set.count; ++i) if (set.items[i].skipped_hooks)
printf("hooks %s skipped installed-unconfigured scope artifact\n",
set.items[i].identity.digest);
goto done;
}
if (strcmp(set.hash, approved)) { result = 3; goto done; }
installed = child_dir(dir, "installed", 0);
transactions = child_dir(dir, "transactions", 0);
if (installed < 0 || transactions < 0) { result = 1; goto done; }
if (!write_set_journal(transactions, generation, &set, choice,
accepted_arch, accepted_count,
accepted_privileged, privileged_count,
skipped_hooks, skipped_count)) {
struct stat st;
result = fstatat(transactions, "set-journal", &st, AT_SYMLINK_NOFOLLOW) ? 1 : 5;
goto done;
}
journaled = 1;
result = 5;
for (i = 0; i < set.count; ++i) {
struct set_item *item = &set.items[i];
if (item->reused) continue;
if (!holy_install_payload(item->snapshot, root, item->privileged) ||
!save_instance(installed, item->identity.digest, item->snapshot, generation,
set.graph, set.graph_length,
strcmp(item->identity.digest, set.resolution.root) ? "dependency" : "explicit",
item->source_record, item->architecture[0] ? item->architecture : NULL,
item->privileged, item->skipped_hooks))
goto done;
printf("applied %s\n", item->identity.digest);
}
if (!set_generation(dir, generation + 1) ||
unlinkat(transactions, "set-journal", 0) || fsync(transactions)) goto done;
printf("committed-set %s generation %llu artifacts %zu\n", set.hash, generation + 1, set.count);
result = 0;
done:
if (result && !quiet) fprintf(stderr, "holypkg: package set failed (status %d)%s\n", result,
journaled ? "; incomplete set journal retained" : "");
free_set(&set);
if (transactions >= 0) close(transactions);
if (installed >= 0) close(installed);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_set(const char *const *digests, size_t count, const char *choice,
const char *approved, const char *root_path,
const char *const *bindings, size_t binding_count,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged, size_t privileged_count,
const char *const *skipped_hooks, size_t skipped_count,
char plan_hash[65])
{
return state_set(digests, count, choice, approved, root_path, bindings,
binding_count, NULL, NULL, accepted_arch, accepted_count,
accepted_privileged, privileged_count, skipped_hooks, skipped_count, plan_hash, 0);
}
int holy_state_set_source(const char *const *digests, size_t count,
const char *source_id, const char *catalog_index,
const char *choice,
const char *approved, const char *root_path,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged, size_t privileged_count,
char plan_hash[65])
{
if (!source_id || !catalog_index) return 2;
return state_set(digests, count, choice, approved, root_path, NULL, 0,
source_id, catalog_index, accepted_arch, accepted_count,
accepted_privileged, privileged_count, NULL, 0, plan_hash, 0);
}
int holy_state_set_source_bindings(const char *const *digests, size_t count,
const char *source_id, const char *catalog_index,
const char *const *bindings, size_t binding_count,
const char *choice, const char *approved,
const char *root_path,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged, size_t privileged_count,
char plan_hash[65])
{
if (!source_id || !catalog_index) return 2;
return state_set(digests, count, choice, approved, root_path,
bindings, binding_count, source_id, catalog_index,
accepted_arch, accepted_count,
accepted_privileged, privileged_count, NULL, 0, plan_hash, 0);
}
int holy_state_probe_source_bindings(const char *const *digests, size_t count,
const char *source_id, const char *catalog_index,
const char *const *bindings, size_t binding_count,
const char *choice, const char *root_path,
const char *const *accepted_arch, size_t accepted_count,
const char *const *accepted_privileged,
size_t privileged_count)
{
if (!source_id || !catalog_index) return 2;
return state_set(digests, count, choice, NULL, root_path,
bindings, binding_count, source_id, catalog_index,
accepted_arch, accepted_count,
accepted_privileged, privileged_count, NULL, 0, NULL, 1);
}
static int instance_matches_snapshot(int item, const char *snapshot)
{
static const char *const names[] = {"meta", "files", "deps", "origin", "provides", "hooks", "transform"};
struct archive *archive = archive_read_new();
struct archive_entry *entry;
struct holy_package_identity identity = {0};
unsigned seen = 0;
char incoming[8192], installed[8192];
int status, ok = 0, has_claims, has_hooks, has_transform, has_config;
struct stat claims_stat;
size_t i;
has_claims = !fstatat(item, "provides", &claims_stat, AT_SYMLINK_NOFOLLOW);
if (!has_claims && errno != ENOENT) goto done;
has_hooks = !fstatat(item, "hooks", &claims_stat, AT_SYMLINK_NOFOLLOW);
if (!has_hooks && errno != ENOENT) goto done;
has_transform = !fstatat(item, "transform", &claims_stat, AT_SYMLINK_NOFOLLOW);
if (!has_transform && errno != ENOENT) goto done;
has_config = !fstatat(item, "config-state", &claims_stat, AT_SYMLINK_NOFOLLOW);
if (!has_config && errno != ENOENT) goto done;
if (has_config && (!holy_package_identity(snapshot, &identity) ||
!config_state_valid(item, identity.digest))) goto done;
if (!archive || archive_read_support_filter_lz4(archive) != ARCHIVE_OK ||
archive_read_support_format_tar(archive) != ARCHIVE_OK ||
archive_read_open_filename(archive, snapshot, 8192) != ARCHIVE_OK) goto done;
while ((status = archive_read_next_header(archive, &entry)) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
struct stat st;
la_ssize_t got;
int fd;
for (i = 0; i < sizeof names / sizeof *names; ++i)
if (path && !strncmp(path, "HOLY/", 5) && !strcmp(path + 5, names[i])) break;
if (i == sizeof names / sizeof *names || (i == 4 && !has_claims) ||
(i == 5 && !has_hooks) || (i == 6 && !has_transform)) {
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
continue;
}
if (seen & (1u << i)) goto done;
fd = openat(item, i == 1 && has_config ? "package-files" : names[i],
O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) goto done;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size != archive_entry_size(entry)) {
close(fd); goto done;
}
while ((got = archive_read_data(archive, incoming, sizeof incoming)) > 0) {
size_t used = 0;
while (used < (size_t)got) {
ssize_t read_count = read(fd, installed + used, (size_t)got - used);
if (read_count < 0 && errno == EINTR) continue;
if (read_count <= 0) { close(fd); goto done; }
used += (size_t)read_count;
}
if (memcmp(incoming, installed, (size_t)got)) { close(fd); goto done; }
}
close(fd);
if (got) goto done;
seen |= 1u << i;
}
ok = status == ARCHIVE_EOF &&
seen == ((1u << 4) - 1u) + (has_claims ? (1u << 4) : 0) +
(has_hooks ? (1u << 5) : 0) + (has_transform ? (1u << 6) : 0);
done:
holy_package_identity_free(&identity);
archive_read_free(archive);
return ok;
}
static int instance_reason_matches(int item, const char *reason)
{
char buffer[1024], expected[64];
ssize_t got;
int fd = openat(item, "state", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0) return 0;
got = read(fd, buffer, sizeof buffer - 1);
close(fd);
if (got <= 0) return 0;
buffer[got] = 0;
snprintf(expected, sizeof expected, "\nreason %s\n", reason);
return strstr(buffer, expected) != NULL;
}
static int recover_set(const char *root_path, int resume)
{
struct install_set set = {0};
struct set_journal journal = {0};
unsigned long long generation, recorded;
const char **digests = NULL;
unsigned char *present = NULL;
size_t i, j;
struct utsname host;
int root = -1, dir = -1, installed = -1, transactions = -1, result = 5;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
dir = root < 0 ? -1 : state_dir_at(root, 0);
if (dir < 0 || flock(dir, LOCK_EX) || !state_layout(dir, 0) ||
!empty_child(dir, "index") || !read_generation(dir, &generation)) { result = 1; goto done; }
if (read_set_journal(dir, &journal) != 1 ||
(generation != journal.generation && generation != journal.generation + 1) ||
!installed_valid(dir)) goto done;
if (journal.host[0] && (uname(&host) || strcmp(host.machine, journal.host))) goto done;
digests = calloc(journal.count, sizeof *digests);
present = calloc(journal.count, 1);
if (!digests || !present) { result = 1; goto done; }
digests[0] = journal.root;
for (i = 0, j = 1; i < journal.count; ++i)
if (strcmp(journal.root, journal.digests[i])) digests[j++] = journal.digests[i];
if (build_set(root_path, root, dir, journal.generation, digests, journal.count,
strcmp(journal.choice, "-") ? journal.choice : NULL, 1,
(const char *const *)journal.bindings, journal.binding_count,
NULL, journal.catalog_index[0] ? journal.catalog_index : NULL,
(const char *const *)journal.accepted_arch, journal.accepted_count,
(const char *const *)journal.accepted_privileged, journal.privileged_count,
(const char *const *)journal.skipped_hooks, journal.skipped_count,
&set) ||
set.count != journal.count || strcmp(set.hash, journal.hash)) goto done;
installed = child_dir(dir, "installed", 0);
transactions = child_dir(dir, "transactions", 0);
if (installed < 0 || transactions < 0) goto done;
for (i = 0; i < set.count; ++i) {
char graph[65], expected[65];
unsigned char hash[32];
unsigned int length;
size_t k;
struct stat st;
int item, files, ok;
const struct set_item *candidate = &set.items[i];
if (candidate->reused) { present[i] = 1; continue; }
if (fstatat(installed, candidate->identity.digest, &st, AT_SYMLINK_NOFOLLOW)) {
struct plan_hash claims = {0};
if (errno != ENOENT || !resume || generation != journal.generation ||
slot_available(dir, &candidate->identity, candidate->source_id) != 1 ||
!holy_install_preflight_resume(candidate->snapshot, root, candidate->privileged)) goto done;
claims.dir = dir;
claims.hash = EVP_MD_CTX_new();
ok = claims.hash && EVP_DigestInit_ex(claims.hash, EVP_sha256(), NULL) == 1 &&
holy_verify_visit(candidate->snapshot, plan_entry, &claims);
EVP_MD_CTX_free(claims.hash);
if (!ok) goto done;
continue;
}
present[i] = 1;
item = child_dir(installed, candidate->identity.digest, 0);
files = item < 0 ? -1 : openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
ok = files >= 0 && instance_state_generation(item, candidate->identity.digest, &recorded) &&
instance_source_matches(item, candidate->source_record) &&
instance_architecture_matches(item, candidate->architecture) &&
recorded == journal.generation + 1 && graph_digest(item, graph) &&
instance_reason_matches(item, strcmp(candidate->identity.digest, set.resolution.root) ?
"dependency" : "explicit") &&
instance_matches_snapshot(item, candidate->snapshot) &&
EVP_Digest(set.graph, set.graph_length, hash, &length, EVP_sha256(), NULL) == 1 &&
length == 32;
if (ok) {
for (k = 0; k < 32; ++k) snprintf(expected + k * 2, 3, "%02x", hash[k]);
ok = !strcmp(expected, graph) && exclusive_claims(installed, candidate->identity.digest, files) == 1 &&
holy_install_check_manifest(files, root) == 1;
}
if (files >= 0) close(files);
if (item >= 0) close(item);
if (!ok) goto done;
}
for (i = 0; i < set.count; ++i) if (!present[i]) {
const struct set_item *item = &set.items[i];
if (!holy_install_payload_missing(item->snapshot, root) ||
!save_instance(installed, item->identity.digest, item->snapshot, journal.generation,
set.graph, set.graph_length,
strcmp(item->identity.digest, set.resolution.root) ? "dependency" : "explicit",
item->source_record, item->architecture[0] ? item->architecture : NULL,
item->privileged, item->skipped_hooks))
goto done;
printf("resumed %s\n", item->identity.digest);
}
if (generation == journal.generation && !set_generation(dir, generation + 1)) goto done;
if (fsync(dir) || unlinkat(transactions, "set-journal", 0) || fsync(transactions)) goto done;
printf("recovered-set %s generation %llu artifacts %zu\n",
set.hash, journal.generation + 1, set.count);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: set recovery requires inspection (status %d)\n", result);
free(present);
free(digests);
free_set(&set);
free_set_journal(&journal);
if (transactions >= 0) close(transactions);
if (installed >= 0) close(installed);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
int holy_state_finish_set(const char *root_path)
{
return recover_set(root_path, 0);
}
int holy_state_continue_set(const char *root_path)
{
return recover_set(root_path, 1);
}
static int repair_hash(int root, unsigned long long generation, const char *digest,
const char *graph, const char *manifest, char output[65])
{
struct stat st;
char identity[128];
unsigned char hash[32];
unsigned int length;
size_t i;
EVP_MD_CTX *ctx = EVP_MD_CTX_new();
int ok = 0;
if (!ctx || fstat(root, &st) || EVP_DigestInit_ex(ctx, EVP_sha256(), NULL) != 1 ||
!hash_text(ctx, "holy-repair-missing-2") || !hash_text(ctx, digest) ||
!hash_text(ctx, graph) || !hash_text(ctx, manifest)) goto done;
snprintf(identity, sizeof identity, "%ju:%ju:%llu", (uintmax_t)st.st_dev,
(uintmax_t)st.st_ino, generation);
if (!hash_text(ctx, identity) || EVP_DigestFinal_ex(ctx, hash, &length) != 1 || length != 32) goto done;
for (i = 0; i < 32; ++i) snprintf(output + i * 2, 3, "%02x", hash[i]);
ok = 1;
done:
EVP_MD_CTX_free(ctx);
return ok;
}
int holy_state_repair(const char *digest, const char *approved, const char *root_path)
{
int root = -1, dir = -1, installed = -1, item = -1, files = -1, transactions = -1;
int result = 1, stage = 0, journaled = 0, resume = digest == NULL, transformed = 0;
char artifact[65], expected[65], actual[65], graph[65], manifest[65], journal[256];
char *snapshot = NULL;
unsigned long long generation, recorded;
struct stat root_st;
size_t length;
if (!resume && (!valid_digest(digest) || (approved && !valid_digest(approved)))) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
dir = root < 0 ? -1 : state_dir_at(root, 0);
if (dir < 0 || fstat(root, &root_st) || flock(dir, approved || resume ? LOCK_EX : LOCK_SH) ||
!state_layout(dir, 0) || !empty_child(dir, "index") ||
!read_generation(dir, &generation) || generation == ULLONG_MAX) goto done;
recorded = generation;
if (resume) {
result = 5;
if (set_journal_present(dir) ||
journal_valid(dir, generation, &recorded, artifact, expected, &stage) != 1 ||
stage != 2) goto done;
{
DIR *list;
struct dirent *entry;
int valid = 1, journal_dir = child_dir(dir, "transactions", 0);
if (journal_dir < 0) goto done;
list = directory_stream(journal_dir);
if (!list) { close(journal_dir); goto done; }
errno = 0;
while ((entry = readdir(list))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (completed_update(journal_dir, entry->d_name)) { errno = 0; continue; }
if (strcmp(entry->d_name, "journal")) { valid = 0; break; }
errno = 0;
}
if (!entry && errno) valid = 0;
closedir(list);
close(journal_dir);
if (!valid) goto done;
}
journaled = 1;
digest = artifact;
approved = expected;
} else if (!empty_child(dir, "transactions")) { result = 5; goto done; }
if (!installed_valid(dir)) goto done;
installed = child_dir(dir, "installed", 0);
item = installed < 0 ? -1 : child_dir(installed, digest, 0);
if (item < 0) { result = 6; goto done; }
{
struct stat st;
transformed = !fstatat(item, "config-state", &st, AT_SYMLINK_NOFOLLOW);
if (!transformed && errno != ENOENT) goto done;
}
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
snapshot = holy_cache_snapshot(digest, root_path);
if (!snapshot) { result = 6; goto done; }
if (files < 0 || !instance_matches_snapshot(item, snapshot) ||
!graph_digest(item, graph) || !instance_record_digest(item, "files", manifest) ||
!repair_hash(root, recorded, digest, graph, manifest, actual)) goto done;
{
struct plan_hash claims = {0};
int valid;
claims.completed = 1;
claims.hash = EVP_MD_CTX_new();
valid = claims.hash && EVP_DigestInit_ex(claims.hash, EVP_sha256(), NULL) == 1 &&
holy_verify_visit(snapshot, plan_entry, &claims);
EVP_MD_CTX_free(claims.hash);
if (!valid) { result = resume ? 5 : 4; goto done; }
}
if (approved && strcmp(approved, actual)) { result = 3; goto done; }
if (exclusive_claims(installed, digest, files) != 1 ||
(transformed ? holy_install_check_repair_transformed(files, root) :
holy_install_check_or_missing(files, root)) != 1) {
result = resume ? 5 : 4; goto done;
}
if (!same_root(root_path, &root_st)) { result = 4; goto done; }
if (!approved) {
printf("repair-plan generation %llu artifact %s sha256 %s missing-only read-only\n",
generation, digest, actual);
result = 0;
goto done;
}
transactions = child_dir(dir, "transactions", 0);
if (transactions < 0) goto done;
result = 5;
if (!resume) {
length = (size_t)snprintf(journal, sizeof journal,
"format holy-journal-1\nstage repairing\ngeneration %llu\nartifact %s\nplan %s\n",
recorded, digest, actual);
if (length >= sizeof journal || !record_file(transactions, "journal", journal, length)) goto done;
journaled = 1;
}
if (!(transformed ? holy_install_payload_missing_mapped(snapshot, root, files) :
holy_install_payload_missing(snapshot, root)) ||
holy_install_check_manifest(files, root) != 1 ||
(generation == recorded && !set_generation(dir, recorded + 1)) || fsync(dir) ||
unlinkat(transactions, "journal", 0) || fsync(transactions)) goto done;
printf("repaired %s generation %llu missing-only\n", digest, recorded + 1);
result = 0;
done:
if (result) fprintf(stderr, "holypkg: missing-file repair failed (status %d)%s\n", result,
journaled ? "; repair journal retained" : "");
if (snapshot) { unlink(snapshot); free(snapshot); }
if (files >= 0) close(files);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
if (transactions >= 0) close(transactions);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
return result;
}
struct update_journal {
unsigned long long generation;
char old[65], next[65], plan[65];
int architecture, privileged;
};
static char *update_record(int dir, const char *name)
{
struct stat st;
int fd = openat(dir, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
char *bytes = NULL;
size_t used = 0;
if (fd < 0) return NULL;
errno = 0;
if (fstat(fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 ||
st.st_size > 64 * 1024 * 1024 || (st.st_mode & 0022) ||
(st.st_uid != 0 && st.st_uid != geteuid())) goto done;
bytes = malloc((size_t)st.st_size + 1);
if (!bytes) goto done;
while (used < (size_t)st.st_size) {
ssize_t got = read(fd, bytes + used, (size_t)st.st_size - used);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) { free(bytes); bytes = NULL; goto done; }
used += (size_t)got;
}
if (memchr(bytes, 0, used)) { free(bytes); bytes = NULL; goto done; }
bytes[used] = 0;
done:
close(fd);
return bytes;
}
static int remove_identity(const char *digest, unsigned long long generation,
int broken, char plan[192], char identity[65])
{
static const char digits[] = "0123456789abcdef";
unsigned char hash[32];
unsigned length = 0;
size_t i;
int n = snprintf(plan, 192,
"format holy-remove-1\ngeneration %llu\nartifact %s\naccept-broken %s\n",
generation, digest, broken ? "yes" : "no");
if (n < 0 || n >= 192 || EVP_Digest(plan, (size_t)n, hash, &length,
EVP_sha256(), NULL) != 1 || length != 32) return 0;
for (i = 0; i < 32; ++i) {
identity[i * 2] = digits[hash[i] >> 4];
identity[i * 2 + 1] = digits[hash[i] & 15];
}
identity[64] = 0;
return 1;
}
static int remove_workdir(int transactions, const char *digest,
unsigned long long generation, int broken)
{
char plan[192], identity[65];
if (!remove_identity(digest, generation, broken, plan, identity)) return -1;
return child_dir(transactions, identity, 0);
}
static int remove_record(int transactions, const char *digest,
unsigned long long generation, int broken, int create)
{
char plan[192], identity[65], decisions[32];
char *saved = NULL;
int child = -1, ok = 0;
if (!remove_identity(digest, generation, broken, plan, identity)) return 0;
if (create && mkdirat(transactions, identity, 0700) && errno != EEXIST) return 0;
child = child_dir(transactions, identity, 0);
if (child < 0) return 0;
saved = update_record(child, "plan");
if (!saved && create && errno == ENOENT) {
if (!record_file(child, "plan", plan, strlen(plan))) goto done;
saved = update_record(child, "plan");
}
if (!saved || strcmp(saved, plan)) goto done;
free(saved); saved = NULL;
snprintf(decisions, sizeof decisions, "accept-broken %s\n", broken ? "yes" : "no");
saved = update_record(child, "decisions");
if (!saved && create && errno == ENOENT) {
if (!record_file(child, "decisions", decisions, strlen(decisions))) goto done;
saved = update_record(child, "decisions");
}
ok = saved && !strcmp(saved, decisions) && !fsync(child) && !fsync(transactions);
done:
free(saved);
close(child);
return ok;
}
static int commit_remove_record(int transactions, const char *digest,
unsigned long long generation, int broken)
{
char plan[192], identity[65], line[66];
char *saved;
int child, ok;
if (!remove_identity(digest, generation, broken, plan, identity) ||
!remove_record(transactions, digest, generation, broken, 0)) return 0;
child = child_dir(transactions, identity, 0);
if (child < 0) return 0;
snprintf(line, sizeof line, "%s\n", identity);
saved = update_record(child, "committed");
if (!saved && errno == ENOENT) {
if (!record_file(child, "committed", line, strlen(line))) {
close(child); return 0;
}
saved = update_record(child, "committed");
}
ok = saved && !strcmp(saved, line) && !fsync(transactions);
free(saved);
close(child);
return ok;
}
static int completed_update(int transactions, const char *name)
{
int child, ok;
char *record, *plan;
if (!valid_digest(name)) return 0;
child = child_dir(transactions, name, 0);
if (child < 0) return 0;
record = update_record(child, "committed");
ok = record && strlen(record) == 65 && !memcmp(record, name, 64) && record[64] == '\n';
free(record);
plan = ok ? update_record(child, "plan") : NULL;
if (!plan) ok = 0;
else if (!strncmp(plan, "format holy-remove-1\n", 21)) {
unsigned long long generation;
char artifact[65], choice[4], canonical[192], identity[65];
char *decision = update_record(child, "decisions");
ok = sscanf(plan,
"format holy-remove-1\ngeneration %llu\nartifact %64[0-9a-f]\naccept-broken %3s",
&generation, artifact, choice) == 3 && valid_digest(artifact) &&
(!strcmp(choice, "yes") || !strcmp(choice, "no")) &&
remove_identity(artifact, generation, !strcmp(choice, "yes"),
canonical, identity) && !strcmp(plan, canonical) &&
!strcmp(name, identity) && decision &&
!strcmp(decision, !strcmp(choice, "yes") ?
"accept-broken yes\n" : "accept-broken no\n");
free(decision);
}
free(plan); close(child);
return ok;
}
static int update_pending(int dir)
{
struct stat st;
int transactions = child_dir(dir, "transactions", 0), result = -1;
if (transactions < 0) return -1;
if (fstatat(transactions, "update", &st, AT_SYMLINK_NOFOLLOW)) {
if (errno == ENOENT) result = 0;
} else if (S_ISDIR(st.st_mode) && !(st.st_mode & 0022) &&
(st.st_uid == 0 || st.st_uid == geteuid())) result = 1;
close(transactions);
return result;
}
static int update_replace(int dir, const char *name, const char *record)
{
char temporary[43] = {0};
int fd = holy_temporary_at(dir, temporary), ok = 0;
if (fd < 0) return 0;
if (!write_all(fd, record, strlen(record)) || fsync(fd)) goto done;
if (renameat(dir, temporary, dir, name) || fsync(dir)) goto done;
ok = 1;
done:
close(fd);
if (!ok) unlinkat(dir, temporary, 0);
return ok;
}
static int read_update_journal(int work, unsigned long long current, struct update_journal *journal)
{
char *record = update_record(work, "journal"), prefix[128];
size_t length, record_length;
int attempt, version, ok = 0;
if (!record) return 0;
record_length = strlen(record);
for (attempt = 0; attempt < 2 && !ok; ++attempt) for (version = 1; version <= 4; ++version) {
const char *p;
size_t tail = version >= 3 ? 77 : 0;
if (version == 2 || version == 4) tail += 83;
if (attempt && !current) break;
journal->generation = current - (unsigned)attempt;
length = (size_t)snprintf(prefix, sizeof prefix,
"format holy-update-journal-%d\ngeneration %llu\nold ",
version, journal->generation);
if (length >= sizeof prefix || record_length != length + 204 + tail ||
memcmp(record, prefix, length)) continue;
p = record + length;
if (p[64] != '\n' || memcmp(p + 65, "new ", 4) || p[133] != '\n' ||
memcmp(p + 134, "plan ", 5) || p[203] != '\n') continue;
memcpy(journal->old, p, 64); journal->old[64] = 0;
memcpy(journal->next, p + 69, 64); journal->next[64] = 0;
memcpy(journal->plan, p + 139, 64); journal->plan[64] = 0;
journal->architecture = version >= 3;
journal->privileged = version == 2 || version == 4;
if (journal->architecture &&
(memcmp(p + 204, "accept-arch ", 12) ||
memcmp(p + 216, journal->next, 64) || p[280] != '\n')) continue;
if (journal->privileged &&
(memcmp(p + 204 + (journal->architecture ? 77 : 0), "accept-privileged ", 18) ||
memcmp(p + 222 + (journal->architecture ? 77 : 0), journal->next, 64) ||
p[286 + (journal->architecture ? 77 : 0)] != '\n')) continue;
ok = valid_digest(journal->old) && valid_digest(journal->next) &&
valid_digest(journal->plan) && strcmp(journal->old, journal->next) &&
journal->generation != ULLONG_MAX;
if (ok) break;
}
free(record);
return ok;
}
static int instance_graph(const struct holy_resolution *full, const char *digest,
char **record, size_t *length)
{
struct holy_resolution part = {0};
size_t i, j, count = 1;
int ok = 0;
memcpy(part.root, digest, 65);
part.artifacts = calloc(full->artifact_count, sizeof *part.artifacts);
part.edges = calloc(full->edge_count ? full->edge_count : 1, sizeof *part.edges);
if (!part.artifacts || !part.edges) goto done;
part.artifacts[0] = (char *)digest;
for (i = 0; i < full->edge_count; ++i) if (!strcmp(full->edges[i].consumer, digest)) {
part.edges[part.edge_count++] = full->edges[i];
for (j = 0; j < count; ++j)
if (!strcmp(part.artifacts[j], full->edges[i].provider)) break;
if (j == count) {
if (count == full->artifact_count) goto done;
part.artifacts[count++] = full->edges[i].provider;
}
}
part.artifact_count = count;
qsort(part.artifacts, count, sizeof *part.artifacts, compare_instance_names);
ok = holy_resolution_record(&part, record, length);
done:
free(part.artifacts); free(part.edges);
return ok;
}
static int clear_update_installed(int parent, const struct holy_resolution *resolution)
{
static const char *const files[] = {"meta", "files", "deps", "origin", "graph", "state", "source", "provides", "hooks", "transform", "hooks-state", "package-files", "config-state"};
int installed = child_dir(parent, "installed", 1), item = -1, ok = 0;
DIR *list = NULL, *members = NULL;
struct dirent *entry;
size_t i;
if (installed < 0 || !(list = directory_stream(installed))) goto done;
errno = 0;
while ((entry = readdir(list))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
for (i = 0; i < resolution->artifact_count; ++i)
if (!strcmp(entry->d_name, resolution->artifacts[i])) break;
if (i == resolution->artifact_count || (item = child_dir(installed, entry->d_name, 0)) < 0 ||
!(members = directory_stream(item))) goto done;
errno = 0;
for (;;) {
struct stat st;
struct dirent *member = readdir(members);
if (!member) { if (errno) goto done; break; }
if (!strcmp(member->d_name, ".") || !strcmp(member->d_name, "..")) continue;
for (i = 0; i < sizeof files / sizeof *files; ++i)
if (!strcmp(member->d_name, files[i])) break;
if (i == sizeof files / sizeof *files || fstatat(item, member->d_name, &st, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(st.st_mode) || st.st_uid != geteuid() || (st.st_mode & 0022)) goto done;
errno = 0;
}
closedir(members); members = NULL;
for (i = 0; i < sizeof files / sizeof *files; ++i)
if (unlinkat(item, files[i], 0) && errno != ENOENT) goto done;
if (fsync(item)) goto done;
close(item); item = -1;
if (unlinkat(installed, entry->d_name, AT_REMOVEDIR) || fsync(installed)) goto done;
errno = 0;
}
ok = errno == 0;
done:
if (members) closedir(members);
if (list) closedir(list);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
return ok;
}
static int update_config_manifest(int old_item, int proposed,
const struct holy_file_plan *plan,
const char *digest, const char *transaction,
int replaced)
{
char *raw = NULL, *installed = NULL, *state = NULL;
char raw_hash[65], installed_hash[65], record[360];
size_t length = 0, i;
int transformed = 0, result = 0;
if (replaced) {
for (i = 0; i < plan->count; ++i)
if (plan->changes[i].keep_config) { transformed = 1; break; }
} else {
struct stat st;
transformed = !fstatat(old_item, "config-state", &st, AT_SYMLINK_NOFOLLOW);
if (!transformed && errno != ENOENT) return 0;
}
if (!transformed) return 1;
raw = update_record(proposed, "files");
if (!raw || !instance_record_digest(proposed, "files", raw_hash)) goto done;
if (replaced) {
if (!holy_file_plan_installed_manifest(plan, raw, strlen(raw),
&installed, &length)) goto done;
} else {
char previous_hash[65];
if (!config_state_valid(old_item, digest) ||
!instance_record_digest(old_item, "package-files", previous_hash) ||
strcmp(previous_hash, raw_hash)) goto done;
installed = update_record(old_item, "files");
state = update_record(old_item, "config-state");
if (!installed || !state) goto done;
length = strlen(installed);
}
if (renameat(proposed, "files", proposed, "package-files") || fsync(proposed) ||
!record_file(proposed, "files", installed, length) ||
!instance_record_digest(proposed, "files", installed_hash)) goto done;
if (replaced) {
int written = snprintf(record, sizeof record,
"format holy-config-transform-1\nsource %s\nraw %s\ninstalled %s\nplan %s\n",
digest, raw_hash, installed_hash, transaction);
if (written < 0 || (size_t)written >= sizeof record ||
!record_file(proposed, "config-state", record, (size_t)written)) goto done;
} else if (!record_file(proposed, "config-state", state, strlen(state))) goto done;
result = config_state_valid(proposed, digest);
done:
free(raw); free(installed); free(state);
return result;
}
static int update_instances(int next_db, int before, char **names, char **snapshots,
size_t count, size_t replaced, const char *new_digest,
const char *new_source, unsigned long long generation,
const struct holy_resolution *resolution,
const char *new_architecture, int new_privileged,
const struct holy_file_plan *file_plan,
const char *transaction, int create)
{
int installed = -1, item = -1, proposed = -1, ok = 0;
char *source = NULL, *graph = NULL;
size_t i, length = 0;
if (create && !clear_update_installed(next_db, resolution)) return 0;
installed = child_dir(next_db, "installed", 0);
if (installed < 0) goto done;
for (i = 0; i < count; ++i) {
const char *digest = i == replaced ? new_digest : names[i], *reason;
unsigned long long recorded;
char expected[65], actual[65], architecture[96];
unsigned char hash[32];
unsigned hash_size;
size_t j;
item = child_dir(before, names[i], 0);
if (item < 0) goto done;
if (!instance_architecture(item, architecture)) goto done;
if (i == replaced) {
if (new_architecture && !architecture_valid(new_architecture)) goto done;
snprintf(architecture, sizeof architecture, "%s",
new_architecture ? new_architecture : "");
}
reason = instance_reason_matches(item, "dependency") ? "dependency" : "explicit";
if (i == replaced && new_source) source = strdup(new_source);
else source = update_record(item, "source");
if (!source && ((i == replaced && new_source) || errno != ENOENT)) goto done;
if (!instance_graph(resolution, digest, &graph, &length)) goto done;
if (create) {
char *state_record = update_record(item, "state");
int privileged;
if (!state_record) goto done;
privileged = i == replaced ? new_privileged :
strstr(state_record, "\nprivileged ") != NULL;
free(state_record);
if (!save_instance(installed, digest, snapshots[i], generation, graph, length,
reason, source, architecture[0] ? architecture : NULL,
privileged, 0)) goto done;
}
proposed = child_dir(installed, digest, 0);
if (proposed >= 0 && create &&
!update_config_manifest(item, proposed, file_plan, digest, transaction,
i == replaced)) goto done;
if (proposed < 0 || !instance_state_generation(proposed, digest, &recorded) || recorded != generation + 1 ||
!instance_reason_matches(proposed, reason) || !instance_source_matches(proposed, source) ||
!instance_architecture_matches(proposed, architecture) ||
!instance_matches_snapshot(proposed, snapshots[i]) || !graph_digest(proposed, actual) ||
EVP_Digest(graph, length, hash, &hash_size, EVP_sha256(), NULL) != 1 || hash_size != 32) goto done;
if (i == replaced) {
char marker[96], *state_record = update_record(proposed, "state");
int observed_privileged;
if (!state_record) goto done;
snprintf(marker, sizeof marker, "\nprivileged %s\n", digest);
observed_privileged = strstr(state_record, marker) != NULL;
free(state_record);
if (observed_privileged != new_privileged) goto done;
}
for (j = 0; j < 32; ++j) snprintf(expected + j * 2, 3, "%02x", hash[j]);
if (strcmp(expected, actual)) goto done;
free(source); source = NULL; free(graph); graph = NULL;
close(item); item = -1; close(proposed); proposed = -1;
}
{
DIR *list = directory_stream(installed);
struct dirent *entry;
size_t found = 0;
if (!list) goto done;
errno = 0;
while ((entry = readdir(list))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
for (i = 0; i < resolution->artifact_count; ++i)
if (!strcmp(entry->d_name, resolution->artifacts[i])) break;
if (i == resolution->artifact_count) break;
++found; errno = 0;
}
ok = !entry && !errno && found == count;
closedir(list);
}
ok = ok && installed_valid(next_db) && !fsync(installed);
done:
free(source); free(graph);
if (item >= 0) close(item);
if (proposed >= 0) close(proposed);
if (installed >= 0) close(installed);
return ok;
}
struct update_progress {
int work;
const struct holy_file_plan *files;
};
static int update_exchange_available(int work)
{
int a = child_dir(work, "exchange-a", 1), b = child_dir(work, "exchange-b", 1), ok = 0;
if (a < 0 || b < 0) goto done;
#ifdef SYS_renameat2
if (syscall(SYS_renameat2, work, "exchange-a", work, "exchange-b", 2u)) {
fputs("holypkg: renameat2(RENAME_EXCHANGE) unavailable for installed database\n", stderr);
goto done;
}
ok = !fsync(work) && !unlinkat(work, "exchange-a", AT_REMOVEDIR) &&
!unlinkat(work, "exchange-b", AT_REMOVEDIR) && !fsync(work);
#else
fputs("holypkg: renameat2(RENAME_EXCHANGE) unavailable\n", stderr);
#endif
done:
if (a >= 0) close(a);
if (b >= 0) close(b);
return ok;
}
static int update_progress(void *context, size_t index, int completed)
{
struct update_progress *p = context;
char record[160];
snprintf(record, sizeof record, "stage applying\nchange %s\nresult %s\n",
p->files->changes[index].id, completed ? "done" : "intent");
return update_replace(p->work, "progress", record);
}
static int finish_update(int root, int db, int transactions, int work, int before,
char **names, char **snapshots, size_t count, size_t replaced,
const struct update_journal *journal, const char *source,
const char *new_architecture,
const struct holy_resolution *resolution, const struct holy_file_plan *files,
int resume, int swapped, unsigned long long current)
{
int next_db = -1, result = 5;
size_t failed = 0;
char committed[66];
struct update_progress progress = {work, files};
struct stat expected, observed;
next_db = child_dir(work, "next-db", !swapped);
if (next_db < 0) goto done;
if (!swapped) {
if (current != journal->generation || !update_exchange_available(work) ||
!update_instances(next_db, before, names, snapshots, count, replaced,
journal->next, source, journal->generation, resolution,
new_architecture,
journal->privileged, files, journal->plan, 1) ||
!update_replace(work, "progress", "stage prepared\n")) goto done;
if (holy_file_plan_stage(files, snapshots[replaced], root, resume, &failed) ||
holy_file_plan_apply(files, root, update_progress, &progress, &failed)) {
fprintf(stderr, "holypkg: incomplete update at file change %zu; inspect reserved staging objects\n", failed);
goto done;
}
if (fstat(before, &expected) || fstatat(db, "installed", &observed, AT_SYMLINK_NOFOLLOW) ||
expected.st_dev != observed.st_dev || expected.st_ino != observed.st_ino ||
!update_replace(work, "progress", "stage publishing\n")) goto done;
#ifdef SYS_renameat2
if (syscall(SYS_renameat2, db, "installed", next_db, "installed", 2u)) {
if (errno == ENOSYS || errno == EINVAL || errno == EOPNOTSUPP)
fputs("holypkg: renameat2(RENAME_EXCHANGE) unavailable; update remains incomplete\n", stderr);
goto done;
}
#else
fputs("holypkg: renameat2(RENAME_EXCHANGE) unavailable; update remains incomplete\n", stderr);
goto done;
#endif
}
if (fsync(next_db) || fsync(db)) goto done;
if (holy_file_plan_finished(files, root) ||
!update_instances(db, before, names, snapshots, count, replaced,
journal->next, source, journal->generation, resolution,
new_architecture,
journal->privileged, files, journal->plan, 0) ||
(current == journal->generation && !set_generation(db, journal->generation + 1)) || fsync(db)) goto done;
snprintf(committed, sizeof committed, "%s\n", journal->plan);
if (holy_file_plan_cleanup(files, root) ||
!update_replace(work, "progress", "stage committed\n") ||
!update_replace(work, "committed", committed)) goto done;
#ifdef SYS_renameat2
if (syscall(SYS_renameat2, transactions, "update", transactions, journal->plan, 1u) || fsync(transactions)) goto done;
#else
goto done;
#endif
printf("updated %s to %s generation %llu plan %s\n", journal->old, journal->next,
journal->generation + 1, journal->plan);
result = 0;
done:
if (next_db >= 0) close(next_db);
return result;
}
static int state_update(const char *old_digest, const char *new_digest,
const char *expected, const char *accepted_arch,
const char *accepted_privileged,
const char *root_path, int resume,
char prepared_hash[65], char **prepared_record)
{
int root = -1, dir = -1, installed = -1, item = -1, files = -1, result = 1, pending;
int old_privileged = 0, new_privileged = 0;
int transactions = -1, work = -1, old_db = -1, reference_db = -1, swapped = 0, journaled = 0;
struct update_journal journal = {0};
char *saved = NULL;
char **names = NULL, **snapshots = NULL, **states = NULL;
const char **selected_ids = NULL;
char source[65], registry[65], existing[65], approved[65], checksum[65];
char new_architecture[96] = {0};
char *old_snapshot = NULL, *new_snapshot = NULL, *source_record = NULL;
char *file_record = NULL, *graph_record = NULL, *record = NULL;
size_t count = 0, i, old_index = 0, file_size = 0, graph_size = 0, record_size = 0, failed;
unsigned long long generation, recorded, current_generation;
struct stat root_st, db_st;
struct holy_package_identity old = {0}, next = {0};
struct holy_file_plan changes = {0};
struct holy_resolution resolution = {0};
struct install_set claims = {0};
struct plan_hash validation = {0};
DIR *list = NULL;
struct dirent *entry;
FILE *out = NULL;
unsigned char bytes[32];
unsigned length;
if (prepared_record) *prepared_record = NULL;
if (prepared_hash) prepared_hash[0] = 0;
if (!resume && (!valid_digest(old_digest) || !valid_digest(new_digest) ||
(expected && !valid_digest(expected)) ||
(accepted_arch && (!valid_digest(accepted_arch) ||
strcmp(accepted_arch, new_digest))) ||
(accepted_privileged && (!valid_digest(accepted_privileged) ||
strcmp(accepted_privileged, new_digest))))) return 2;
if (!resume && !strcmp(old_digest, new_digest)) return 3;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0 || fstat(root, &root_st) || (dir = state_dir_at(root, 0)) < 0 ||
flock(dir, expected || resume ? LOCK_EX : LOCK_SH) || fstat(dir, &db_st) || !state_layout(dir, 0) ||
!read_generation(dir, &generation)) goto done;
current_generation = generation;
reference_db = dir;
if (resume) {
struct stat a, b;
int old_present, new_present, live;
result = 6;
transactions = child_dir(dir, "transactions", 0);
work = transactions < 0 ? -1 : child_dir(transactions, "update", 0);
if (work < 0) goto done;
journaled = 1; result = 5;
if (!read_update_journal(work, generation, &journal)) goto done;
old_digest = journal.old; new_digest = journal.next; expected = journal.plan;
accepted_arch = journal.architecture ? journal.next : NULL;
accepted_privileged = journal.privileged ? journal.next : NULL;
generation = journal.generation;
saved = update_record(work, "plan");
if (!saved && errno != ENOENT) goto done;
live = child_dir(dir, "installed", 0);
if (live < 0) goto done;
old_present = !fstatat(live, old_digest, &a, AT_SYMLINK_NOFOLLOW);
new_present = !fstatat(live, new_digest, &b, AT_SYMLINK_NOFOLLOW);
close(live);
if (old_present == new_present) goto done;
swapped = new_present;
if (swapped) {
old_db = child_dir(work, "next-db", 0);
if (old_db < 0) goto done;
reference_db = old_db;
} else if (current_generation != generation) goto done;
} else {
pending = transaction_pending(dir, generation);
if (pending < 0) goto done;
if (pending) { result = 5; goto done; }
pending = pending_child(dir, generation, existing, approved);
if (pending < 0) goto done;
if (pending) { result = 5; goto done; }
}
result = 1;
if (!installed_valid(reference_db) || (installed = child_dir(reference_db, "installed", 0)) < 0 ||
!(list = directory_stream(installed))) goto done;
names = calloc(10000, sizeof *names);
snapshots = calloc(10000, sizeof *snapshots);
states = calloc(10000, sizeof *states);
if (!names || !snapshots || !states) goto done;
errno = 0;
while ((entry = readdir(list))) {
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (count == 10000) { result = 6; goto done; }
names[count] = strdup(entry->d_name);
if (!names[count]) goto done;
++count;
errno = 0;
}
if (errno) goto done;
if (count) qsort(names, count, sizeof *names, compare_instance_names);
for (old_index = 0; old_index < count; ++old_index)
if (!strcmp(names[old_index], old_digest)) break;
if (old_index == count) { result = 6; goto done; }
for (i = 0; i < count; ++i)
if (!strcmp(names[i], new_digest)) { result = 4; goto done; }
for (i = 0; i < count; ++i) {
char state[65], architecture[96];
snapshots[i] = holy_cache_snapshot(names[i], root_path);
if (!snapshots[i]) { result = 6; goto done; }
item = child_dir(installed, names[i], 0);
if (item < 0) goto done;
if (!instance_architecture(item, architecture)) goto done;
files = openat(item, "files", O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (files < 0 || !instance_state_generation(item, names[i], &recorded) ||
recorded > generation || !instance_matches_snapshot(item, snapshots[i]) ||
!instance_record_digest(item, "state", state)) goto done;
if (i == old_index) {
char *state_record = update_record(item, "state"), marker[96];
if (!state_record) goto done;
snprintf(marker, sizeof marker, "\nprivileged %s\n", old_digest);
old_privileged = strstr(state_record, marker) != NULL;
free(state_record);
}
states[i] = strdup(state);
if (!states[i]) goto done;
if (exclusive_claims(installed, names[i], files) != 1 ||
((!resume || i != old_index) &&
(i == old_index ? holy_install_check_manifest_except_configs(files, root) :
holy_install_check_manifest(files, root)) != 1) ||
(!resume && check_graph(installed, root, names[i], NULL) != 1)) { result = 4; goto done; }
if (i == old_index && !installed_source_id(item, source)) goto done;
close(files); files = -1;
close(item); item = -1;
}
strcpy(registry, "-");
if (strcmp(source, "-")) {
result = holy_source_record(dir, source, &source_record, registry);
if (result) goto done;
}
result = 6;
new_snapshot = holy_cache_snapshot(new_digest, root_path);
if (!new_snapshot || !holy_package_identity(snapshots[old_index], &old) ||
!holy_package_identity(new_snapshot, &next) ||
!recorded_transform(new_snapshot) || !instance_preflight(new_snapshot)) goto done;
if (!same_slot(&old, source, &next, source)) { result = 4; goto done; }
{
struct privileged_scan scan = {0};
int scanned = holy_verify_visit(new_snapshot, scan_privileged, &scan);
if (!scanned) {
result = scan.unsupported ? 6 : 1;
free(scan.first); goto done;
}
new_privileged = scan.count != 0;
if (new_privileged && !accepted_privileged) {
fprintf(stderr, "holypkg: decision-required privileged update artifact=%s path=%s mode=%04o; --accept-privileged %s permits setuid placement\n",
new_digest, scan.first, scan.mode, new_digest);
result = 3;
free(scan.first); goto done;
}
free(scan.first);
if (accepted_privileged && !new_privileged) { result = 2; goto done; }
}
{
struct utsname host;
if (uname(&host)) { result = 1; goto done; }
if (!native_architecture(host.machine, next.arch)) {
if (!accepted_arch) {
fprintf(stderr, "holypkg: decision-required architecture artifact=%s host=%s target=%s; --accept-arch %s permits placement without proving execution\n",
new_digest, host.machine, next.arch, new_digest);
result = 3; goto done;
}
snprintf(new_architecture, sizeof new_architecture, "%s %s", host.machine, next.arch);
if (!architecture_valid(new_architecture)) { result = 2; goto done; }
} else if (accepted_arch) {
result = 2; goto done;
}
}
pending = slot_available_except(reference_db, &next, source, old_digest);
if (pending != 1) { result = pending < 0 ? 1 : 4; goto done; }
result = holy_preview_resolved(new_snapshot, root_path, 1, new_privileged, 0);
if (result) goto done;
result = explicit_elf_paths(new_snapshot, 1);
if (result) goto done;
validation.completed = 1;
validation.accepted_privileged = new_privileged;
validation.hash = EVP_MD_CTX_new();
result = 6;
if (!validation.hash || EVP_DigestInit_ex(validation.hash, EVP_sha256(), NULL) != 1 ||
!holy_verify_visit(new_snapshot, plan_entry, &validation) ||
!holy_file_plan_collect(snapshots[old_index], new_snapshot, &changes)) goto done;
result = holy_file_plan_preserve_configs(&changes, root, resume ? saved : NULL);
if (result) goto done;
result = 6;
if (!holy_file_plan_record(&changes, &file_record, &file_size)) goto done;
changes.before_privileged = old_privileged;
changes.after_privileged = new_privileged;
result = holy_file_plan_check(&changes, root, resume, &failed);
if (result) {
fprintf(stderr, "holypkg: update file preflight failed at change %zu\n", failed);
goto done;
}
old_snapshot = snapshots[old_index];
snapshots[old_index] = new_snapshot;
new_snapshot = NULL;
for (i = 0; i < count; ++i)
if (!holy_verify_visit(snapshots[i], set_claim, &claims)) { result = 6; goto done; }
for (i = 0; i < changes.count; ++i)
if (changes.changes[i].keep_config &&
!set_claim(&claims, changes.changes[i].after)) { result = 1; goto done; }
if (!set_claims_valid(&claims)) { result = 4; goto done; }
result = holy_resolve_collect_set((const char *const *)snapshots, count, &resolution);
if (result) goto done;
selected_ids = calloc(count, sizeof *selected_ids);
if (!selected_ids) { result = 1; goto done; }
for (i = 0; i < count; ++i)
selected_ids[i] = i == old_index ? new_digest : names[i];
if (!selected_soname_paths(&resolution, selected_ids,
(const char *const *)snapshots, count, root,
claims.claims, claims.claim_count)) {
result = 3; goto done;
}
result = 1;
if (!holy_resolution_record(&resolution, &graph_record, &graph_size)) goto done;
out = open_memstream(&record, &record_size);
if (!out) goto done;
fprintf(out, "[update]\nformat holy-update-plan-1\ngeneration %llu\n"
"root %ju %ju\ndatabase %ju %ju\nold %s\nnew %s\nregistry %s\n",
generation, (uintmax_t)root_st.st_dev, (uintmax_t)root_st.st_ino,
(uintmax_t)db_st.st_dev, (uintmax_t)db_st.st_ino, old_digest, new_digest, registry);
if (accepted_arch) fprintf(out, "accept-arch %s\narchitecture %s\n", new_digest, new_architecture);
if (new_privileged) fprintf(out, "accept-privileged %s\n", new_digest);
if (source_record) fputs(source_record, out);
else fputs("source - local\n", out);
fputs("delivery local\n[installed]\n", out);
for (i = 0; i < count; ++i) fprintf(out, "instance %s %s\n", names[i], states[i]);
fputs("[files]\n", out);
fwrite(file_record, 1, file_size, out);
fputs("[graph]\n", out);
fwrite(graph_record, 1, graph_size, out);
pending = ferror(out);
if (fclose(out)) pending = 1;
out = NULL;
if (pending || !same_root(root_path, &root_st) ||
EVP_Digest(record, record_size, bytes, &length, EVP_sha256(), NULL) != 1 || length != 32)
goto done;
for (i = 0; i < 32; ++i) snprintf(checksum + i * 2, 3, "%02x", bytes[i]);
if (!expected) {
if (prepared_record) {
if (prepared_hash) memcpy(prepared_hash, checksum, 65);
*prepared_record = record;
record = NULL;
result = 0;
goto done;
}
if (printf("plan-update sha256 %s read-only\n", checksum) < 0 ||
fwrite(record, 1, record_size, stdout) != record_size) goto done;
result = 0;
goto done;
}
if (strcmp(expected, checksum) || (saved && strcmp(saved, record))) { result = 3; goto done; }
if (generation == ULLONG_MAX || record_size > 64 * 1024 * 1024) { result = 6; goto done; }
if (!resume) {
char header[512];
int header_size;
result = holy_file_plan_reservations(&changes, root);
if (result) goto done;
result = 1;
transactions = child_dir(dir, "transactions", 0);
if (transactions < 0 || mkdirat(transactions, "update", 0700)) goto done;
journaled = 1;
if (fsync(transactions) || (work = child_dir(transactions, "update", 0)) < 0) goto done;
journal.generation = generation;
memcpy(journal.old, old_digest, 65); memcpy(journal.next, new_digest, 65); memcpy(journal.plan, checksum, 65);
header_size = snprintf(header, sizeof header,
"format holy-update-journal-%d\ngeneration %llu\nold %s\nnew %s\nplan %s\n%s%s%s%s%s%s",
accepted_arch ? (new_privileged ? 4 : 3) : (new_privileged ? 2 : 1),
generation, old_digest, new_digest, checksum,
accepted_arch ? "accept-arch " : "", accepted_arch ? new_digest : "",
accepted_arch ? "\n" : "",
new_privileged ? "accept-privileged " : "",
new_privileged ? new_digest : "", new_privileged ? "\n" : "");
if (header_size < 0 || (size_t)header_size >= sizeof header) goto done;
journal.architecture = !!accepted_arch;
journal.privileged = new_privileged;
if (!update_replace(work, "journal", header)) goto done;
}
if (!saved && !update_replace(work, "plan", record)) goto done;
result = finish_update(root, dir, transactions, work, installed, names, snapshots,
count, old_index, &journal, source_record,
new_architecture[0] ? new_architecture : NULL, &resolution,
&changes, resume, swapped, current_generation);
done:
if (result) {
fprintf(stderr, "holypkg: update failed (status %d)%s\n", result, journaled ? "; update journal retained" : "");
if (journaled) result = 5;
}
if (out) fclose(out);
if (list) closedir(list);
if (files >= 0) close(files);
if (item >= 0) close(item);
if (installed >= 0) close(installed);
if (old_db >= 0) close(old_db);
if (work >= 0) close(work);
if (transactions >= 0) close(transactions);
free(saved);
free(selected_ids);
if (dir >= 0) close(dir);
if (root >= 0) close(root);
if (old_snapshot) { unlink(old_snapshot); free(old_snapshot); }
if (new_snapshot) { unlink(new_snapshot); free(new_snapshot); }
for (i = 0; i < count; ++i) {
free(names[i]);
if (snapshots && snapshots[i]) { unlink(snapshots[i]); free(snapshots[i]); }
if (states) free(states[i]);
}
free(names); free(snapshots); free(states); free(source_record);
free(file_record); free(graph_record); free(record);
holy_package_identity_free(&old); holy_package_identity_free(&next);
holy_file_plan_free(&changes); holy_resolution_free(&resolution); free_set(&claims);
EVP_MD_CTX_free(validation.hash);
return result;
}
int holy_state_update_plan(const char *old_digest, const char *new_digest,
const char *accepted_arch, const char *accepted_privileged,
const char *root_path)
{
return state_update(old_digest, new_digest, NULL, accepted_arch,
accepted_privileged, root_path, 0, NULL, NULL);
}
int holy_state_update_prepare(const char *old_digest, const char *new_digest,
const char *accepted_arch, const char *accepted_privileged,
const char *root_path, char hash[65], char **record)
{
if (!hash || !record) return 2;
return state_update(old_digest, new_digest, NULL, accepted_arch,
accepted_privileged, root_path, 0, hash, record);
}
int holy_state_apply_update(const char *plan, const char *old_digest,
const char *new_digest, const char *accepted_arch,
const char *accepted_privileged,
const char *root_path)
{
return state_update(old_digest, new_digest, plan, accepted_arch,
accepted_privileged, root_path, 0, NULL, NULL);
}
int holy_state_recover_update(const char *root_path)
{
return state_update(NULL, NULL, NULL, NULL, NULL, root_path, 1, NULL, NULL);
}
int holy_state_rollback(const char *transaction, const char *approved,
const char *accepted_arch, const char *accepted_privileged,
const char *root_path)
{
struct update_journal journal = {0};
unsigned long long generation;
unsigned char digest[32];
unsigned digest_size;
char computed[65], hash[65], lineage[160], generation_line[64];
char *committed = NULL, *source_plan = NULL, *installed_section;
char *record = NULL, *marker = NULL;
int root = -1, db = -1, transactions = -1, work = -1, result = 1;
size_t i;
if (!valid_digest(transaction) || (approved && !valid_digest(approved)) ||
(accepted_arch && !valid_digest(accepted_arch)) ||
(accepted_privileged && !valid_digest(accepted_privileged))) return 2;
root = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (root < 0 || (db = state_dir_at(root, 0)) < 0 || flock(db, LOCK_SH) ||
!state_layout(db, 0) || (transactions = child_dir(db, "transactions", 0)) < 0 ||
(work = child_dir(transactions, transaction, 0)) < 0) goto done;
committed = update_record(work, "committed");
source_plan = update_record(work, "plan");
marker = update_record(work, "journal");
if (!committed || strlen(committed) != 65 ||
memcmp(committed, transaction, 64) || committed[64] != '\n' ||
!source_plan || !marker ||
sscanf(marker, "format holy-update-journal-%*d\ngeneration %llu\n", &generation) != 1 ||
generation == ULLONG_MAX ||
!read_update_journal(work, generation + 1, &journal) ||
strcmp(journal.plan, transaction) ||
EVP_Digest(source_plan, strlen(source_plan), digest, &digest_size,
EVP_sha256(), NULL) != 1 || digest_size != 32) {
result = 2; goto done;
}
for (i = 0; i < 32; ++i) snprintf(computed + i * 2, 3, "%02x", digest[i]);
if (strcmp(computed, transaction) ||
strncmp(source_plan, "[update]\nformat holy-update-plan-1\n",
sizeof "[update]\nformat holy-update-plan-1\n" - 1)) {
result = 2; goto done;
}
snprintf(lineage, sizeof lineage, "\nold %s\nnew %s\n", journal.old, journal.next);
snprintf(generation_line, sizeof generation_line, "\ngeneration %llu\n",
journal.generation);
installed_section = strstr(source_plan, "\n[installed]\n");
if (!installed_section || !strstr(source_plan, lineage) ||
strstr(source_plan, lineage) >= installed_section ||
!strstr(source_plan, generation_line) ||
strstr(source_plan, generation_line) >= installed_section) {
result = 2; goto done;
}
result = 0;
done:
free(committed); free(source_plan); free(marker);
if (work >= 0) close(work);
if (transactions >= 0) close(transactions);
if (db >= 0) close(db);
if (root >= 0) close(root);
if (result) return result;
if (approved) {
result = holy_state_apply_update(approved, journal.next, journal.old,
accepted_arch, accepted_privileged, root_path);
if (!result) printf("rollback %s restored %s\n", transaction, journal.old);
return result;
}
result = holy_state_update_prepare(journal.next, journal.old,
accepted_arch, accepted_privileged,
root_path, hash, &record);
if (result) return result;
if (printf("rollback-plan transaction %s current %s target %s sha256 %s read-only\n",
transaction, journal.next, journal.old, hash) < 0 ||
fputs(record, stdout) == EOF) result = 1;
free(record);
return result;
}