_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/install.c
#define _DEFAULT_SOURCE
#define _POSIX_C_SOURCE 200809L
#include "install.h"
#include "verify.h"
#include "config.h"
#include "script.h"
#include <archive.h>
#include <archive_entry.h>
#include <openssl/evp.h>
#include <errno.h>
#include <dirent.h>
#include <stdint.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <limits.h>
#include <sys/stat.h>
#include <sys/syscall.h>
#include <unistd.h>
static int transition_matches(int root, const struct holy_manifest_entry *entry,
struct stat *observed);
struct group_observation { char *group; struct stat state; };
struct observed_groups { struct group_observation *items; size_t count; };
static int observe_group(struct observed_groups *groups, const char *group,
const struct stat *state)
{
struct group_observation *next;
if (!group || !strcmp(group, "-")) return 1;
if (groups->count == SIZE_MAX / sizeof *next) return 0;
next = realloc(groups->items, (groups->count + 1) * sizeof *next);
if (!next) return 0;
groups->items = next;
next[groups->count].group = strdup(group);
if (!next[groups->count].group) return 0;
next[groups->count++].state = *state;
return 1;
}
static int group_order(const void *left, const void *right)
{
return strcmp(((const struct group_observation *)left)->group,
((const struct group_observation *)right)->group);
}
static int groups_intact(struct observed_groups *groups)
{
size_t i;
if (groups->count) qsort(groups->items, groups->count, sizeof *groups->items, group_order);
for (i = 1; i < groups->count; ++i) {
const struct group_observation *a = &groups->items[i-1], *b = &groups->items[i];
if (!strcmp(a->group, b->group) &&
(a->state.st_dev != b->state.st_dev || a->state.st_ino != b->state.st_ino)) {
fputs("holypkg: changed-hardlink-group\n", stderr);
return 0;
}
}
return 1;
}
static void groups_free(struct observed_groups *groups)
{
size_t i;
for (i = 0; i < groups->count; ++i) free(groups->items[i].group);
free(groups->items);
}
static int parent_fd(int root, const char *path, char **storage, const char **base)
{
char *cursor, *slash;
int current;
struct stat st;
*storage = strdup(path);
if (!*storage) return -1;
current = dup(root);
if (current < 0) goto fail;
cursor = *storage;
while ((slash = strchr(cursor, '/')) != NULL) {
int next;
*slash = '\0';
if (!*cursor || !strcmp(cursor, ".") || !strcmp(cursor, "..")) { errno = EINVAL; goto fail; }
next = openat(current, cursor, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (next < 0 || fstat(next, &st) || !S_ISDIR(st.st_mode) ||
(st.st_uid != 0 && st.st_uid != geteuid()) ||
(st.st_mode & 0022)) {
if (next >= 0) { close(next); errno = EPERM; }
goto fail;
}
close(current);
current = next;
cursor = slash + 1;
}
if (!*cursor || !strcmp(cursor, ".") || !strcmp(cursor, "..")) { errno = EINVAL; goto fail; }
*base = cursor;
return current;
fail:
{
int error = errno;
if (current >= 0) close(current);
free(*storage);
*storage = NULL;
errno = error;
}
return -1;
}
struct directory_list {
const struct holy_manifest_entry **items;
size_t count;
};
static int directory_order(const void *left, const void *right)
{
const struct holy_manifest_entry *a = *(const struct holy_manifest_entry *const *)left;
const struct holy_manifest_entry *b = *(const struct holy_manifest_entry *const *)right;
return strcmp(a->path, b->path);
}
static int declared_directory(const struct directory_list *list, const char *path)
{
size_t low = 0, high = list->count;
while (low < high) {
size_t middle = low + (high - low) / 2;
int order = strcmp(list->items[middle]->path, path);
if (!order) return 1;
if (order < 0) low = middle + 1;
else high = middle;
}
return 0;
}
static int planned_parents(int root, const char *path, const struct directory_list *list)
{
char *copy = strdup(path), *cursor, *slash;
int current = dup(root), missing = 0, ok = 0;
if (!copy || current < 0 || !*path || *path == '/') goto done;
cursor = copy;
while ((slash = strchr(cursor, '/'))) {
int next = -1;
struct stat st;
*slash = 0;
if (!*cursor || !strcmp(cursor, ".") || !strcmp(cursor, "..")) goto done;
if (!missing) {
next = openat(current, cursor, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (next < 0) {
if (errno != ENOENT) goto done;
missing = 1;
} else {
if (fstat(next, &st) || (st.st_uid != 0 && st.st_uid != geteuid()) || (st.st_mode & 0022)) {
close(next); goto done;
}
close(current); current = next;
}
}
if (missing && !declared_directory(list, copy)) goto done;
*slash = '/'; cursor = slash + 1;
}
ok = *cursor && strcmp(cursor, ".") && strcmp(cursor, "..");
done:
if (current >= 0) close(current);
free(copy);
return ok;
}
static int directory_name(const struct holy_manifest_entry *entry, char name[75])
{
unsigned char hash[32];
unsigned length;
size_t i;
if (EVP_Digest(entry->path, strlen(entry->path), hash, &length, EVP_sha256(), NULL) != 1 ||
length != 32) return 0;
memcpy(name, ".holy-dir-", 10);
for (i = 0; i < 32; ++i) snprintf(name + 10 + i * 2, 3, "%02x", hash[i]);
return 1;
}
static int directory_empty(int fd)
{
int copy = openat(fd, ".", O_RDONLY | O_DIRECTORY | O_CLOEXEC), ok = 0;
DIR *stream;
struct dirent *entry;
if (copy < 0) return 0;
stream = fdopendir(copy);
if (!stream) { close(copy); return 0; }
errno = 0;
while ((entry = readdir(stream)))
if (strcmp(entry->d_name, ".") && strcmp(entry->d_name, "..")) break;
ok = !entry && !errno;
closedir(stream);
return ok;
}
static int directory_stage(int root, const struct holy_manifest_entry *entry, int create, int recovering)
{
char name[75], *storage = NULL;
const char *base;
struct stat st;
int parent = -1, child = -1, ok = 0, exists;
if (!directory_name(entry, name)) goto done;
parent = parent_fd(root, entry->path, &storage, &base);
if (parent < 0) { ok = !create && errno == ENOENT; goto done; }
exists = !fstatat(parent, name, &st, AT_SYMLINK_NOFOLLOW);
if (!exists && errno != ENOENT) goto done;
if (exists && !recovering) goto done;
if (!create) { ok = 1; goto done; }
if (!exists && (mkdirat(parent, name, 0700) || fsync(parent))) goto done;
child = openat(parent, name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (child < 0 || fstat(child, &st) || st.st_uid != geteuid() ||
!directory_empty(child)) goto done;
if (exists) {
if ((st.st_mode & 07777) != entry->mode || (long long)st.st_gid != entry->gid) goto done;
} else if (fchown(child, (uid_t)-1, (gid_t)entry->gid) || fchmod(child, entry->mode)) goto done;
if (fsync(child) || fstat(child, &st)) goto done;
{
struct stat current;
if (fstatat(parent, name, ¤t, AT_SYMLINK_NOFOLLOW) ||
current.st_dev != st.st_dev || current.st_ino != st.st_ino) goto done;
}
#ifdef SYS_renameat2
if (syscall(SYS_renameat2, parent, name, parent, base, 1u) || fsync(parent)) goto done;
#else
errno = ENOSYS;
goto done;
#endif
ok = 1;
done:
if (!ok) fputs("holypkg: directory staging requires inspection or a supported renameat2 filesystem\n", stderr);
if (child >= 0) close(child);
if (parent >= 0) close(parent);
free(storage);
return ok;
}
int holy_install_directory_plan(int root, const struct holy_manifest_entry *entries,
size_t count, int create, int recovering)
{
struct directory_list list = {0};
size_t i;
int ok = 0;
if (count > SIZE_MAX / sizeof *list.items) return 0;
list.items = calloc(count ? count : 1, sizeof *list.items);
if (!list.items) return 0;
for (i = 0; i < count; ++i) if (entries[i].directory) {
const struct holy_manifest_entry *e = &entries[i];
if (!e->path || e->hardlink || e->link || e->group || e->size ||
(e->mode & ~0777u) || (e->mode & 0022) || !(e->mode & 0100) ||
e->uid != (long long)geteuid() || e->gid != (long long)getegid()) goto done;
list.items[list.count++] = e;
}
qsort(list.items, list.count, sizeof *list.items, directory_order);
for (i = 1; i < list.count; ++i)
if (!strcmp(list.items[i-1]->path, list.items[i]->path)) goto done;
for (i = 0; i < count; ++i)
if (!planned_parents(root, entries[i].path, &list)) goto done;
for (i = 0; i < list.count; ++i) {
int state = holy_install_check_entry(root, list.items[i]);
if (state != 1 && (state != 2 || !directory_stage(root, list.items[i], 0, recovering))) goto done;
}
if (create) for (i = 0; i < list.count; ++i) {
int state = holy_install_check_entry(root, list.items[i]);
if (state != 1 && (state != 2 || !directory_stage(root, list.items[i], 1, recovering))) goto done;
}
ok = 1;
done:
free(list.items);
return ok;
}
struct root_check {
int root, recovering, accepted_privileged, files_fd;
struct holy_manifest_entry *directories;
size_t count;
struct directory_list parents;
struct observed_groups groups;
};
static const char *repair_target(int files_fd, const char *path, int config,
char **allocated)
{
size_t length;
int owner;
*allocated = NULL;
if (files_fd < 0 || !config) return path;
length = strlen(path);
if (length > SIZE_MAX - sizeof ".holy-new") return NULL;
*allocated = malloc(length + sizeof ".holy-new");
if (!*allocated) return NULL;
memcpy(*allocated, path, length);
memcpy(*allocated + length, ".holy-new", sizeof ".holy-new");
owner = holy_install_manifest_owns(files_fd, *allocated);
if (owner == 1) return *allocated;
free(*allocated); *allocated = NULL;
return owner == 0 ? path : NULL;
}
static int collect_directory(void *context, const struct holy_manifest_entry *entry)
{
struct root_check *check = context;
struct holy_manifest_entry *items;
if (!entry->directory) return 1;
if (check->count == SIZE_MAX / sizeof *items) return 0;
items = realloc(check->directories, (check->count + 1) * sizeof *items);
if (!items) return 0;
check->directories = items;
items[check->count] = *entry;
items[check->count].path = strdup(entry->path);
if (!items[check->count].path) return 0;
++check->count;
return 1;
}
static int check_entry(void *context, const struct holy_manifest_entry *entry)
{
struct root_check *check = context;
struct holy_manifest_entry mapped = *entry;
char *allocated = NULL;
int state;
struct stat observed;
mapped.path = repair_target(check->files_fd, entry->path, entry->config, &allocated);
if (!mapped.path) return 0;
if ((entry->link && (entry->mode != 0777 || entry->link[0] == '/')) ||
((entry->mode & 07000) &&
(!check->accepted_privileged || entry->directory || entry->link || entry->hardlink ||
(entry->mode & 03000))) || entry->uid != (long long)geteuid() ||
entry->gid != (long long)getegid() ||
!planned_parents(check->root, mapped.path, &check->parents)) {
free(allocated); return 0;
}
state = transition_matches(check->root, &mapped, &observed);
if (state == 1 && !observe_group(&check->groups, entry->group, &observed)) {
free(allocated); return 0;
}
free(allocated);
return state == 2 || (state == 1 && (entry->directory || check->recovering));
}
static int prepare_directories(const char *snapshot, int root, int create,
int recovering, int accepted_privileged, int files_fd)
{
struct root_check check = {0};
size_t i;
int ok = 0;
check.root = root; check.recovering = recovering;
check.accepted_privileged = accepted_privileged; check.files_fd = files_fd;
if (!holy_verify_visit(snapshot, collect_directory, &check)) goto done;
check.parents.items = calloc(check.count ? check.count : 1, sizeof *check.parents.items);
if (!check.parents.items) goto done;
check.parents.count = check.count;
for (i = 0; i < check.count; ++i) check.parents.items[i] = &check.directories[i];
qsort(check.parents.items, check.count, sizeof *check.parents.items, directory_order);
ok = holy_install_directory_plan(root, check.directories, check.count, 0, recovering) &&
holy_verify_visit(snapshot, check_entry, &check) && groups_intact(&check.groups) &&
(!create || holy_install_directory_plan(root, check.directories, check.count, 1, recovering));
done:
for (i = 0; i < check.count; ++i) free((char *)check.directories[i].path);
free(check.parents.items);
free(check.directories);
groups_free(&check.groups);
if (!ok) fputs("holypkg: install requires intact or declared safe directories and matching payload state\n", stderr);
return ok;
}
int holy_install_preflight(const char *snapshot, int root, int accepted_privileged)
{
return prepare_directories(snapshot, root, 0, 0, accepted_privileged, -1);
}
int holy_install_preflight_resume(const char *snapshot, int root, int accepted_privileged)
{
return prepare_directories(snapshot, root, 0, 1, accepted_privileged, -1);
}
static int link_payload(int root, const char *source, const struct holy_manifest_entry *destination,
int missing_only)
{
struct holy_manifest_entry input = *destination;
struct stat expected, linked;
char *source_storage = NULL, *dest_storage = NULL;
const char *source_base, *dest_base;
int source_parent = -1, dest_parent = -1, fd = -1, ok = 0;
input.path = source; input.hardlink = NULL;
if (transition_matches(root, &input, &expected) != 1) goto done;
source_parent = parent_fd(root, source, &source_storage, &source_base);
dest_parent = parent_fd(root, destination->path, &dest_storage, &dest_base);
if (source_parent < 0 || dest_parent < 0) goto done;
fd = openat(source_parent, source_base, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || fstat(fd, &linked) || !S_ISREG(linked.st_mode) ||
linked.st_dev != expected.st_dev || linked.st_ino != expected.st_ino) goto done;
if (linkat(source_parent, source_base, dest_parent, dest_base, 0) &&
(!missing_only || errno != EEXIST)) {
fprintf(stderr, "holypkg: linkat: %s\n", strerror(errno));
goto done;
}
if (fstatat(dest_parent, dest_base, &linked, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(linked.st_mode) || linked.st_dev != expected.st_dev ||
linked.st_ino != expected.st_ino || fsync(fd) || fsync(dest_parent)) goto done;
ok = 1;
done:
if (!ok) fputs("holypkg: hardlink creation failed; inspect source, target and filesystem\n", stderr);
if (fd >= 0) close(fd);
if (source_parent >= 0) close(source_parent);
if (dest_parent >= 0) close(dest_parent);
free(source_storage); free(dest_storage);
return ok;
}
struct link_install { int root, missing_only, restore_anchor; };
static int install_link(void *context, const struct holy_manifest_entry *entry)
{
struct link_install *links = context;
struct holy_manifest_entry anchor;
int state;
if (!entry->hardlink) return 1;
if (!links->restore_anchor)
return link_payload(links->root, entry->hardlink, entry, links->missing_only);
anchor = *entry; anchor.path = entry->hardlink; anchor.hardlink = NULL;
state = transition_matches(links->root, &anchor, NULL);
if (state == 1) return 1;
if (state != 2) return 0;
state = transition_matches(links->root, entry, NULL);
return state == 2 || (state == 1 && link_payload(links->root, entry->path, &anchor, 0));
}
struct repair_configs { char **paths; size_t count; };
static int collect_repair_config(void *context, const struct holy_manifest_entry *entry)
{
struct repair_configs *configs = context;
char **next;
if (!entry->config || entry->directory || entry->link || entry->hardlink || entry->group) return 1;
if (configs->count == SIZE_MAX / sizeof *next) return 0;
next = realloc(configs->paths, (configs->count + 1) * sizeof *next);
if (!next) return 0;
configs->paths = next;
configs->paths[configs->count] = strdup(entry->path);
if (!configs->paths[configs->count]) return 0;
++configs->count;
return 1;
}
static int repair_config_path(const struct repair_configs *configs, const char *path)
{
size_t i;
for (i = 0; i < configs->count; ++i)
if (!strcmp(configs->paths[i], path)) return 1;
return 0;
}
static int install_payload(const char *snapshot, int root, int missing_only,
int accepted_privileged, int files_fd)
{
struct archive *archive = archive_read_new();
struct archive_entry *entry;
struct repair_configs configs = {0};
char buffer[65536];
int status, ok = 0;
struct link_install links = {root, missing_only, 1};
if (!archive) return 0;
if ((files_fd >= 0 && !holy_verify_visit(snapshot, collect_repair_config, &configs)) ||
!prepare_directories(snapshot, root, 1, missing_only, accepted_privileged, files_fd) ||
(missing_only && !holy_verify_visit(snapshot, install_link, &links))) goto done;
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);
const char *target_path;
char *mapped = NULL;
char *storage = NULL;
const char *base;
int parent = -1, fd = -1;
la_ssize_t got;
la_int64_t count = 0;
if (!path || strncmp(path, "DATA/", 5) || !path[5] ||
archive_entry_filetype(entry) == AE_IFDIR || archive_entry_hardlink(entry)) {
if (archive_read_data_skip(archive) != ARCHIVE_OK) goto done;
continue;
}
target_path = repair_target(files_fd, path + 5,
repair_config_path(&configs, path + 5), &mapped);
if (!target_path) goto done;
if (archive_entry_filetype(entry) == AE_IFLNK) {
const char *target = archive_entry_symlink(entry);
if (!target || target[0] == '/' ||
!holy_safe_link(path + 5, target) ||
archive_entry_perm(entry) != 0777 ||
archive_entry_size(entry) != 0) goto file_done;
parent = parent_fd(root, target_path, &storage, &base);
if (parent < 0) goto file_done;
if (symlinkat(target, parent, base)) {
struct stat st;
size_t size = strlen(target);
char *actual;
int same;
if (!missing_only || errno != EEXIST ||
fstatat(parent, base, &st, AT_SYMLINK_NOFOLLOW) || !S_ISLNK(st.st_mode) ||
(long long)st.st_uid != archive_entry_uid(entry) ||
(long long)st.st_gid != archive_entry_gid(entry)) goto file_done;
actual = malloc(size + 1);
if (!actual) goto file_done;
got = readlinkat(parent, base, actual, size + 1);
same = got >= 0 && (size_t)got == size && !memcmp(target, actual, size);
free(actual);
if (!same) goto file_done;
}
if (fsync(parent) || archive_read_data_skip(archive) != ARCHIVE_OK) goto file_done;
close(parent);
free(storage);
free(mapped);
continue;
}
if (archive_entry_filetype(entry) != AE_IFREG ||
archive_entry_hardlink(entry) || archive_entry_size(entry) < 0) goto file_done;
if ((archive_entry_perm(entry) & 07000) &&
(!accepted_privileged || (archive_entry_perm(entry) & 03000))) goto file_done;
parent = parent_fd(root, target_path, &storage, &base);
if (parent < 0) goto file_done;
if (missing_only) {
struct stat st;
fd = openat(parent, base, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd >= 0) {
char current[65536];
if (fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_mode & 07777) != archive_entry_perm(entry) ||
(long long)st.st_uid != archive_entry_uid(entry) ||
(long long)st.st_gid != archive_entry_gid(entry) ||
st.st_size != archive_entry_size(entry)) goto file_done;
while ((got = archive_read_data(archive, buffer, sizeof buffer)) > 0) {
size_t used = 0;
while (used < (size_t)got) {
ssize_t n = read(fd, current + used, (size_t)got - used);
if (n < 0 && errno == EINTR) continue;
if (n <= 0) goto file_done;
used += (size_t)n;
}
if (memcmp(current, buffer, (size_t)got)) goto file_done;
}
if (got || fsync(fd)) goto file_done;
close(fd);
close(parent);
free(storage);
free(mapped);
continue;
}
if (errno != ENOENT) goto file_done;
}
fd = openat(parent, base, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) goto file_done;
while ((got = archive_read_data(archive, buffer, sizeof buffer)) > 0) {
size_t offset = 0;
if (count > archive_entry_size(entry) - got) break;
count += got;
while (offset < (size_t)got) {
ssize_t sent = write(fd, buffer + offset, (size_t)got - offset);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) goto file_done;
offset += (size_t)sent;
}
}
if (got != 0 || count != archive_entry_size(entry) ||
fchmod(fd, archive_entry_perm(entry)) || fsync(fd) ||
fsync(parent)) goto file_done;
close(fd);
close(parent);
free(storage);
free(mapped);
continue;
file_done:
if (fd >= 0) close(fd);
if (parent >= 0) close(parent);
free(storage);
free(mapped);
goto done;
}
links.restore_anchor = 0;
ok = status == ARCHIVE_EOF && holy_verify_visit(snapshot, install_link, &links);
done:
archive_read_free(archive);
while (configs.count) free(configs.paths[--configs.count]);
free(configs.paths);
if (!ok) fprintf(stderr, "holypkg: install payload incomplete; inspect transaction journal\n");
return ok;
}
int holy_install_payload(const char *snapshot, int root, int accepted_privileged)
{
return install_payload(snapshot, root, 0, accepted_privileged, -1);
}
int holy_install_payload_missing(const char *snapshot, int root)
{
return install_payload(snapshot, root, 1, 1, -1);
}
int holy_install_payload_missing_mapped(const char *snapshot, int root, int files_fd)
{
return install_payload(snapshot, root, 1, 1, files_fd);
}
static int decimal(const char *text, int base, unsigned long long *value)
{
char *end;
errno = 0;
if (!*text || *text == '-' || *text == '+') return 0;
*value = strtoull(text, &end, base);
return !errno && !*end;
}
static int check_file(int root, char **v, struct stat *observed)
{
unsigned long long mode, uid, gid, size;
struct stat st;
char *storage = NULL;
const char *base;
int parent, fd = -1, result = -1;
EVP_MD_CTX *hash = NULL;
unsigned char digest[32];
unsigned int digest_size;
char buffer[65536];
ssize_t got;
size_t i;
int symlink = !strcmp(v[0], "symlink");
int hardlink = !strcmp(v[0], "hardlink");
if ((strcmp(v[0], "dir") && strcmp(v[0], "file") && !symlink && !hardlink) ||
!decimal(v[2], 8, &mode) || mode > 07777 ||
!decimal(v[5], 10, &uid) || uid > 0x7fffffff ||
!decimal(v[6], 10, &gid) || gid > 0x7fffffff ||
!decimal(v[7], 10, &size) || size > LLONG_MAX ||
(strcmp(v[9], "none") && strcmp(v[9], "config") &&
strcmp(v[9], "mutable") && strcmp(v[9], "config,mutable")) ||
(strcmp(v[9], "none") && (symlink || hardlink || !strcmp(v[0], "dir"))) ||
strcmp(v[10], "-") ||
((symlink || !strcmp(v[0], "dir")) && strcmp(v[11], "-")) ||
(hardlink && !strcmp(v[11], "-")))
return -1;
if (strcmp(v[11], "-") && (!v[11][0] ||
strspn(v[11], "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789_.-") != strlen(v[11]))) return -1;
if (symlink && (mode != 0777 || v[12][0] == '/' ||
!holy_safe_link(v[1], v[12]))) return -1;
if (!strcmp(v[0], "dir") || symlink) {
if (size || strcmp(v[8], "-")) return -1;
} else if (strlen(v[8]) != 64) return -1;
parent = parent_fd(root, v[1], &storage, &base);
if (parent < 0) return errno == ENOENT ? 2 : 0;
if (symlink) {
size_t length = strlen(v[12]);
char *target;
if (fstatat(parent, base, &st, AT_SYMLINK_NOFOLLOW)) {
result = errno == ENOENT ? 2 : 0;
goto done;
}
if (!S_ISLNK(st.st_mode) || (st.st_mode & 07777) != mode ||
(unsigned long long)st.st_uid != uid ||
(unsigned long long)st.st_gid != gid) { result = 0; goto done; }
target = malloc(length + 1);
if (!target) goto done;
got = readlinkat(parent, base, target, length + 1);
result = got >= 0 && (size_t)got == length &&
!memcmp(target, v[12], length);
free(target);
if (result && observed) *observed = st;
goto done;
}
if (!strcmp(v[0], "dir")) {
if (fstatat(parent, base, &st, AT_SYMLINK_NOFOLLOW)) {
result = errno == ENOENT ? 2 : 0; goto done;
}
result = S_ISDIR(st.st_mode) &&
(st.st_mode & 07777) == mode &&
(unsigned long long)st.st_uid == uid &&
(unsigned long long)st.st_gid == gid &&
(st.st_uid == 0 || st.st_uid == geteuid()) && !(st.st_mode & 0022);
goto done;
}
fd = openat(parent, base, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0 && errno == ENOENT) { result = 2; goto done; }
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_mode & 07777) != mode || (unsigned long long)st.st_uid != uid ||
(unsigned long long)st.st_gid != gid ||
(unsigned long long)st.st_size != size) { result = 0; goto done; }
hash = EVP_MD_CTX_new();
if (!hash || EVP_DigestInit_ex(hash, EVP_sha256(), NULL) != 1) goto done;
while ((got = read(fd, buffer, sizeof buffer)) > 0)
if (EVP_DigestUpdate(hash, buffer, (size_t)got) != 1) goto done;
if (got < 0 || EVP_DigestFinal_ex(hash, digest, &digest_size) != 1 ||
digest_size != sizeof digest) goto done;
result = 1;
for (i = 0; i < sizeof digest; ++i) {
char hex[3];
snprintf(hex, sizeof hex, "%02x", digest[i]);
if (v[8][i * 2] != hex[0] || v[8][i * 2 + 1] != hex[1]) result = 0;
}
if (result && observed) *observed = st;
done:
EVP_MD_CTX_free(hash);
if (fd >= 0) close(fd);
close(parent);
free(storage);
return result;
}
static void report_changed(const char *path, const char *code)
{
const unsigned char *p = (const unsigned char *)path;
fprintf(stderr, "holypkg: %s ", code);
for (; *p; ++p)
if (*p == '\\' || *p <= 32 || *p >= 127)
fprintf(stderr, "\\x%02x", (unsigned int)*p);
else fputc(*p, stderr);
fputc('\n', stderr);
}
static int same_object(const struct stat *before, const struct stat *after)
{
return before->st_dev == after->st_dev && before->st_ino == after->st_ino &&
before->st_mode == after->st_mode && before->st_uid == after->st_uid &&
before->st_gid == after->st_gid && before->st_size == after->st_size &&
before->st_mtim.tv_sec == after->st_mtim.tv_sec && before->st_mtim.tv_nsec == after->st_mtim.tv_nsec &&
before->st_ctim.tv_sec == after->st_ctim.tv_sec && before->st_ctim.tv_nsec == after->st_ctim.tv_nsec;
}
int holy_install_observe_regular(int root, const struct holy_manifest_entry *entry,
struct holy_manifest_entry *observed,
unsigned char digest[32])
{
char *storage = NULL;
const char *base;
char buffer[65536];
struct stat start, end, path_state;
EVP_MD_CTX *hash = NULL;
unsigned int length;
ssize_t got;
int parent = -1, fd = -1, ok = 0;
if (!entry || !entry->path || !entry->config || entry->directory ||
entry->link || entry->hardlink || entry->group || !observed || !digest) return 0;
parent = parent_fd(root, entry->path, &storage, &base);
if (parent < 0) goto done;
fd = openat(parent, base, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
hash = EVP_MD_CTX_new();
if (fd < 0 || !hash || fstat(fd, &start) || !S_ISREG(start.st_mode) ||
start.st_size < 0 || start.st_uid != entry->uid || start.st_gid != entry->gid ||
(start.st_mode & 07000) || EVP_DigestInit_ex(hash, EVP_sha256(), NULL) != 1) goto done;
while ((got = read(fd, buffer, sizeof buffer)) > 0)
if (EVP_DigestUpdate(hash, buffer, (size_t)got) != 1) goto done;
if (got < 0 || EVP_DigestFinal_ex(hash, digest, &length) != 1 || length != 32 ||
fstat(fd, &end) || fstatat(parent, base, &path_state, AT_SYMLINK_NOFOLLOW) ||
!same_object(&start, &end) || !same_object(&start, &path_state)) goto done;
*observed = *entry;
observed->hash = digest;
observed->size = start.st_size;
observed->mode = (unsigned int)(start.st_mode & 07777);
ok = 1;
done:
EVP_MD_CTX_free(hash);
if (fd >= 0) close(fd);
if (parent >= 0) close(parent);
free(storage);
return ok;
}
static int remove_file(int root, const char *path, const struct stat *observed)
{
char *storage = NULL;
const char *base;
struct stat st;
int parent = parent_fd(root, path, &storage, &base), ok = 0;
if (parent < 0) return 0;
if (!fstatat(parent, base, &st, AT_SYMLINK_NOFOLLOW) &&
(S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) &&
same_object(&st, observed) &&
!unlinkat(parent, base, 0) && !fsync(parent)) ok = 1;
close(parent);
free(storage);
return ok;
}
struct manifest_row {
char **fields;
size_t count;
int checked;
struct stat observed;
};
static int row_path_order(const void *a, const void *b)
{
return strcmp((*(const struct manifest_row *const *)a)->fields[1],
(*(const struct manifest_row *const *)b)->fields[1]);
}
static int row_group_order(const void *a, const void *b)
{
return strcmp((*(const struct manifest_row *const *)a)->fields[11],
(*(const struct manifest_row *const *)b)->fields[11]);
}
static int row_inode_order(const void *a, const void *b)
{
const struct stat *left = &(*(const struct manifest_row *const *)a)->observed;
const struct stat *right = &(*(const struct manifest_row *const *)b)->observed;
if (left->st_dev != right->st_dev) return left->st_dev < right->st_dev ? -1 : 1;
if (left->st_ino != right->st_ino) return left->st_ino < right->st_ino ? -1 : 1;
return 0;
}
static int row_links(struct manifest_row *rows, size_t count)
{
struct manifest_row **paths = NULL, **groups = NULL;
size_t i, j, group_count = 0;
int ok = 0;
if (count > SIZE_MAX / sizeof *paths) return 0;
paths = calloc(count ? count : 1, sizeof *paths);
groups = calloc(count ? count : 1, sizeof *groups);
if (!paths || !groups) goto done;
for (i = 0; i < count; ++i) {
paths[i] = &rows[i];
if (strcmp(rows[i].fields[11], "-")) groups[group_count++] = &rows[i];
}
qsort(paths, count, sizeof *paths, row_path_order);
for (i = 1; i < count; ++i)
if (!strcmp(paths[i-1]->fields[1], paths[i]->fields[1])) goto done;
for (i = 0; i < count; ++i) if (!strcmp(rows[i].fields[0], "hardlink")) {
char **v = rows[i].fields, *key_fields[2] = {NULL, v[12]};
struct manifest_row key = {0}, *key_ptr = &key, **found;
static const size_t attributes[] = {2, 5, 6, 7, 8, 11};
key.fields = key_fields;
found = bsearch(&key_ptr, paths, count, sizeof *paths, row_path_order);
if (!found || strcmp((*found)->fields[0], "file")) goto done;
for (j = 0; j < sizeof attributes / sizeof *attributes; ++j) {
size_t field = attributes[j];
if (field == 8 || field == 11) {
if (strcmp(v[field], (*found)->fields[field])) goto done;
} else {
unsigned long long left, right;
if (!decimal(v[field], field == 2 ? 8 : 10, &left) ||
!decimal((*found)->fields[field], field == 2 ? 8 : 10, &right) || left != right) goto done;
}
}
}
qsort(groups, group_count, sizeof *groups, row_group_order);
for (i = 0; i < group_count;) {
size_t end = i + 1, observed = group_count;
char **first = groups[i]->fields;
const char *anchor = !strcmp(first[0], "hardlink") ? first[12] : first[1];
int drift = 0;
while (end < group_count && !strcmp(first[11], groups[end]->fields[11])) ++end;
for (j = i; j < end; ++j) {
char **v = groups[j]->fields;
const char *target = !strcmp(v[0], "hardlink") ? v[12] : v[1];
if (strcmp(anchor, target)) goto done;
if (groups[j]->checked != 1) continue;
if (observed == group_count) observed = j;
else if (groups[j]->observed.st_dev != groups[observed]->observed.st_dev ||
groups[j]->observed.st_ino != groups[observed]->observed.st_ino) drift = 1;
}
if (drift) for (j = i; j < end; ++j)
if (groups[j]->checked == 1) groups[j]->checked = 0;
i = end;
}
{
size_t inodes = 0;
for (i = 0; i < count; ++i)
if (rows[i].checked == 1 && S_ISREG(rows[i].observed.st_mode)) paths[inodes++] = &rows[i];
qsort(paths, inodes, sizeof *paths, row_inode_order);
for (i = 0; i < inodes;) {
size_t end = i + 1;
const char *group = paths[i]->fields[11];
int drift = 0;
while (end < inodes && !row_inode_order(&paths[i], &paths[end])) ++end;
for (j = i + 1; j < end; ++j)
if (!strcmp(group, "-") || strcmp(group, paths[j]->fields[11])) drift = 1;
if (drift) for (j = i; j < end; ++j) paths[j]->checked = 0;
i = end;
}
}
ok = 1;
done:
free(paths); free(groups);
return ok;
}
static int walk_manifest(int files_fd, int root, int mode,
holy_install_finding finding, void *context, const char *filter,
holy_install_regular_visit regular, void *regular_context)
{
struct stat st;
struct manifest_row *rows = NULL;
char *text = NULL;
size_t length, used = 0, start = 0, i, line = 0, matches = 0, row_count = 0, capacity = 0;
int result = -1;
if (lseek(files_fd, 0, SEEK_SET) != 0 ||
fstat(files_fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 ||
st.st_size > 16 * 1024 * 1024) return -1;
length = (size_t)st.st_size;
text = malloc(length + 1);
if (!text) return -1;
while (used < length) {
ssize_t got = read(files_fd, text + used, length - used);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) goto done;
used += (size_t)got;
}
text[length] = '\0';
for (i = 0; i <= length; ++i) {
char **v = NULL, *error = NULL;
size_t count = 0;
if (i < length && text[i] != '\n') continue;
++line;
if (memchr(text + start, '\0', i - start) ||
!holy_lex(text + start, i - start, &v, &count, "installed/files", line, &error)) {
free(error); holy_tokens_free(v, count); goto done;
}
start = i + 1;
if (!count) { holy_tokens_free(v, count); continue; }
if (count != ((!strcmp(v[0], "symlink") || !strcmp(v[0], "hardlink")) ? 13u : 12u)) {
holy_tokens_free(v, count); goto done;
}
if (row_count == capacity) {
size_t next_capacity = capacity ? capacity * 2 : 32;
struct manifest_row *next;
if (next_capacity < capacity || next_capacity > SIZE_MAX / sizeof *next) {
holy_tokens_free(v, count); goto done;
}
next = realloc(rows, next_capacity * sizeof *next);
if (!next) { holy_tokens_free(v, count); goto done; }
rows = next; capacity = next_capacity;
}
memset(&rows[row_count], 0, sizeof *rows);
rows[row_count].fields = v; rows[row_count].count = count;
rows[row_count++].checked = 2;
}
{
const char *group = NULL;
if (filter) for (i = 0; i < row_count; ++i)
if (!strcmp(filter, rows[i].fields[1]) && strcmp(rows[i].fields[11], "-"))
group = rows[i].fields[11];
for (i = 0; i < row_count; ++i) {
struct manifest_row *row = &rows[i];
if (filter && strcmp(filter, row->fields[1]) &&
(!group || strcmp(group, row->fields[11]))) continue;
row->checked = check_file(root, row->fields, &row->observed);
if (row->checked < 0) goto done;
}
}
if (!row_links(rows, row_count)) goto done;
result = 1;
for (i = 0; i < row_count; ++i) {
struct manifest_row *row = &rows[i];
char **v = row->fields;
int checked = row->checked;
int preserved = 0;
size_t j, path_length;
if (filter && strcmp(filter, v[1])) continue;
++matches;
path_length = strlen(v[1]);
if (mode == 5 && !strcmp(v[0], "file") &&
(!strcmp(v[9], "config") || !strcmp(v[9], "config,mutable")) &&
path_length <= SIZE_MAX - sizeof ".holy-new")
for (j = 0; j < row_count; ++j)
if (strlen(rows[j].fields[1]) == path_length + sizeof ".holy-new" - 1 &&
!memcmp(rows[j].fields[1], v[1], path_length) &&
!strcmp(rows[j].fields[1] + path_length, ".holy-new")) {
preserved = 1; break;
}
if ((mode == 1 || mode == 2 || mode == 3) &&
(!strcmp(v[9], "config") || !strcmp(v[9], "config,mutable"))) continue;
if (mode == 4 && checked == 0 && !strcmp(v[0], "file") &&
(!strcmp(v[9], "config") || !strcmp(v[9], "config,mutable"))) continue;
if (checked == 2) {
if (mode != 2 && mode != 3 && (mode != 5 || preserved) && result == 1) result = 0;
if (mode != 2 && mode != 3) report_changed(v[1], "missing-file");
} else if (!checked) {
report_changed(v[1], !strcmp(v[9], "config") ||
!strcmp(v[9], "config,mutable") ? "changed-config" : "changed-file");
if (result == 1) result = 0;
} else if ((mode == 1 || mode == 2) && strcmp(v[0], "dir")) {
struct stat current;
if (check_file(root, v, ¤t) != 1 ||
current.st_dev != row->observed.st_dev || current.st_ino != row->observed.st_ino ||
!remove_file(root, v[1], ¤t)) result = 0;
}
if (finding && (checked == 0 || checked == 2) &&
!finding(context, v[1], checked == 2 ? "missing-file" :
(!strcmp(v[9], "config") || !strcmp(v[9], "config,mutable")) ?
"changed-config" : "changed-file", NULL)) result = -1;
if (regular && checked == 1 && S_ISREG(row->observed.st_mode)) {
char *storage = NULL;
const char *base;
struct stat opened;
int parent = parent_fd(root, v[1], &storage, &base), fd = -1;
if (parent < 0) { free(storage); result = -1; goto done; }
fd = openat(parent, base, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || fstat(fd, &opened) ||
opened.st_dev != row->observed.st_dev || opened.st_ino != row->observed.st_ino ||
opened.st_size != row->observed.st_size || opened.st_mode != row->observed.st_mode ||
!regular(regular_context, v[1], fd)) result = -1;
if (fd >= 0) close(fd);
close(parent); free(storage);
if (result < 0) goto done;
}
if (finding && checked == 1 && !strcmp(v[0], "file") &&
(row->observed.st_mode & 0111)) {
char *storage = NULL, *interpreter = NULL;
const char *base;
struct stat opened;
int parent = parent_fd(root, v[1], &storage, &base), fd = -1;
int script, status;
if (parent < 0) { free(storage); result = -1; goto done; }
fd = openat(parent, base, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC);
if (fd < 0 || fstat(fd, &opened) ||
opened.st_dev != row->observed.st_dev || opened.st_ino != row->observed.st_ino ||
opened.st_size != row->observed.st_size || opened.st_mode != row->observed.st_mode) {
if (fd >= 0) close(fd);
close(parent); free(storage); result = -1; goto done;
}
script = holy_script_read_fd(fd, opened.st_size, opened.st_mode, &interpreter);
close(fd); close(parent); free(storage);
if (script < 0) { result = -1; goto done; }
if (script) {
const char *code;
status = script == 3 ? -1 : holy_script_target_status(root, interpreter);
if (script == 2 && status == 1) status = -1;
code = status == 0 ? "missing-interpreter" :
status == -2 ? "unavailable-path-resolution" : "unknown-interpreter";
if (status != 1) {
fprintf(stderr, "holypkg: %s consumer=%s interpreter=%s\n",
code, v[1], interpreter);
if (!finding(context, v[1], code, interpreter)) result = -1;
else if (result == 1) result = 0;
}
}
free(interpreter);
}
if (result < 0 || (mode && result != 1)) goto done;
}
if (filter && matches != 1) result = -1;
done:
for (i = 0; i < row_count; ++i) holy_tokens_free(rows[i].fields, rows[i].count);
free(rows); free(text);
return result;
}
int holy_install_check_manifest(int files_fd, int root)
{
return walk_manifest(files_fd, root, 0, NULL, NULL, NULL, NULL, NULL);
}
int holy_install_check_manifest_except_configs(int files_fd, int root)
{
return walk_manifest(files_fd, root, 4, NULL, NULL, NULL, NULL, NULL);
}
int holy_install_check_report(int files_fd, int root,
holy_install_finding finding, void *context)
{
return walk_manifest(files_fd, root, 0, finding, context, NULL, NULL, NULL);
}
int holy_install_visit_regular(int files_fd, int root, const char *filter,
holy_install_regular_visit visit, void *context)
{
if (!visit) return -1;
return walk_manifest(files_fd, root, 0, NULL, NULL, filter, visit, context);
}
int holy_install_check_path(int files_fd, int root, const char *path)
{
return walk_manifest(files_fd, root, 0, NULL, NULL, path, NULL, NULL);
}
int holy_install_check_or_missing(int files_fd, int root)
{
return walk_manifest(files_fd, root, 3, NULL, NULL, NULL, NULL, NULL);
}
int holy_install_check_repair_transformed(int files_fd, int root)
{
return walk_manifest(files_fd, root, 5, NULL, NULL, NULL, NULL, NULL);
}
int holy_install_remove_manifest(int files_fd, int root)
{
if (holy_install_check_manifest(files_fd, root) != 1) return 0;
return walk_manifest(files_fd, root, 1, NULL, NULL, NULL, NULL, NULL) == 1;
}
int holy_install_finish_remove_manifest(int files_fd, int root)
{
if (walk_manifest(files_fd, root, 3, NULL, NULL, NULL, NULL, NULL) != 1) return 0;
return walk_manifest(files_fd, root, 2, NULL, NULL, NULL, NULL, NULL) == 1;
}
static int manifest_claims(int files_fd, const char *path, int other)
{
struct stat st;
FILE *stream = NULL;
char *line = NULL;
size_t capacity = 0, number = 0;
ssize_t length;
int copy, found = 0;
if (fstat(files_fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 ||
st.st_size > 16 * 1024 * 1024 || lseek(files_fd, 0, SEEK_SET)) return -1;
copy = dup(files_fd);
if (copy < 0) return -1;
stream = fdopen(copy, "r");
if (!stream) { close(copy); return -1; }
while ((length = getline(&line, &capacity, stream)) >= 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/files", number, &error)) {
free(error);
found = -1;
break;
}
if (count) {
if (count != ((!strcmp(v[0], "symlink") || !strcmp(v[0], "hardlink")) ? 13u : 12u) ||
(strcmp(v[0], "file") && strcmp(v[0], "dir") &&
strcmp(v[0], "symlink") && strcmp(v[0], "hardlink")))
found = -1;
else if (other >= 0) {
int kind = holy_install_manifest_owns(other, v[1]);
if (kind < 0) found = -1;
else if (kind && (kind == 1 || strcmp(v[0], "dir"))) found = 1;
} else if (!strcmp(v[1], path)) {
if (found) found = -1;
else found = !strcmp(v[0], "dir") ? 2 : 1;
}
}
holy_tokens_free(v, count);
if (found < 0) break;
}
if (ferror(stream) || st.st_size != ftello(stream)) found = -1;
free(line);
fclose(stream);
return found;
}
int holy_install_manifest_owns(int files_fd, const char *path)
{
return manifest_claims(files_fd, path, -1);
}
static int manifest_executable_path(int files_fd, const char *path, unsigned int depth)
{
struct stat st;
FILE *stream = NULL;
char *line = NULL, *link = NULL;
size_t capacity = 0, number = 0;
ssize_t length;
int copy, found = 0, result = 0;
if (depth == 16) return 0;
if (fstat(files_fd, &st) || !S_ISREG(st.st_mode) || st.st_size < 0 ||
st.st_size > 16 * 1024 * 1024 || lseek(files_fd, 0, SEEK_SET)) return -1;
copy = dup(files_fd);
if (copy < 0) return -1;
stream = fdopen(copy, "r");
if (!stream) { close(copy); return -1; }
while ((length = getline(&line, &capacity, stream)) >= 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/files", number, &error)) result = -1;
free(error);
if (result >= 0 && count) {
int symlink = !strcmp(v[0], "symlink");
int hardlink = !strcmp(v[0], "hardlink");
if (count != (symlink || hardlink ? 13u : 12u) ||
(strcmp(v[0], "file") && strcmp(v[0], "dir") &&
!symlink && !hardlink)) result = -1;
else if (!strcmp(v[1], path)) {
unsigned long long mode;
if (found++) result = -1;
else if (symlink) {
if (!holy_safe_link(v[1], v[12]) || !(link = strdup(v[12]))) result = -1;
} else if (!strcmp(v[0], "file") || hardlink) {
if (!decimal(v[2], 8, &mode) || mode > 07777) result = -1;
else result = (mode & 0111) != 0;
}
}
}
holy_tokens_free(v, count);
if (result < 0) break;
}
if (ferror(stream) || st.st_size != ftello(stream)) result = -1;
free(line);
fclose(stream);
if (result >= 0 && link) {
char *next = holy_relative_link_path(path, strlen(path), link, "");
result = next ? manifest_executable_path(files_fd, next, depth + 1) : -1;
free(next);
}
free(link);
return result;
}
int holy_install_manifest_executable(int files_fd, const char *path)
{
if (!path || !*path || *path == '/') return -1;
return manifest_executable_path(files_fd, path, 0);
}
int holy_install_manifests_conflict(int left_fd, int right_fd)
{
return manifest_claims(left_fd, NULL, right_fd);
}
static int transition_entry_valid(const struct holy_manifest_entry *entry)
{
return entry && entry->path && entry->path[0] && entry->path[0] != '/' &&
entry->path[strlen(entry->path) - 1] != '/' &&
!entry->directory && !(entry->link && (entry->hardlink || entry->group)) &&
!(entry->mode & ~07777u) &&
(!(entry->mode & 07000) ||
((entry->mode & 07000) == 04000 && (entry->mode & 0111) &&
!entry->link && !entry->hardlink && !entry->group)) &&
entry->uid == (long long)geteuid() &&
entry->gid == (long long)getegid() && entry->size >= 0 &&
(entry->link ? entry->mode == 0777 && !entry->size &&
entry->link[0] != '/' && holy_safe_link(entry->path, entry->link) :
entry->hash != NULL);
}
static int transition_matches(int root, const struct holy_manifest_entry *entry,
struct stat *observed)
{
char numbers[4][32], hash[65], *v[13];
size_t i;
if (!entry || !entry->path || (entry->directory != 0 && entry->directory != 1) ||
(entry->directory && (entry->link || entry->hardlink || entry->group)) ||
(!entry->directory && !entry->link && !entry->hash)) return -1;
snprintf(numbers[0], sizeof numbers[0], "%o", entry->mode);
snprintf(numbers[1], sizeof numbers[1], "%lld", entry->uid);
snprintf(numbers[2], sizeof numbers[2], "%lld", entry->gid);
snprintf(numbers[3], sizeof numbers[3], "%lld", entry->size);
if (!entry->link && !entry->directory) for (i = 0; i < 32; ++i)
snprintf(hash + i * 2, 3, "%02x", entry->hash[i]);
v[0] = entry->directory ? "dir" : entry->link ? "symlink" : entry->hardlink ? "hardlink" : "file";
v[1] = (char *)entry->path; v[2] = numbers[0]; v[3] = v[4] = "-";
v[5] = numbers[1]; v[6] = numbers[2]; v[7] = numbers[3];
v[8] = entry->link || entry->directory ? "-" : hash;
v[9] = entry->config ? entry->mutable ? "config,mutable" : "config" :
entry->mutable ? "mutable" : "none";
v[10] = "-"; v[11] = entry->group ? (char *)entry->group : "-";
v[12] = (char *)(entry->link ? entry->link : entry->hardlink);
return check_file(root, v, observed);
}
static int transition_absent(int root, const char *path)
{
char *storage = NULL;
const char *base;
struct stat st;
int parent = parent_fd(root, path, &storage, &base), absent = 0;
if (parent >= 0) {
absent = fstatat(parent, base, &st, AT_SYMLINK_NOFOLLOW) && errno == ENOENT;
close(parent);
} else absent = errno == ENOENT;
free(storage);
return absent;
}
int holy_install_check_entry(int root, const struct holy_manifest_entry *entry)
{
struct stat observed, target;
char *storage = NULL;
const char *base;
int parent, result = transition_matches(root, entry, &observed);
if (result != 1 || !entry->hardlink) return result;
parent = parent_fd(root, entry->hardlink, &storage, &base);
result = parent >= 0 && !fstatat(parent, base, &target, AT_SYMLINK_NOFOLLOW) &&
S_ISREG(target.st_mode) && target.st_dev == observed.st_dev && target.st_ino == observed.st_ino;
if (parent >= 0) close(parent);
free(storage);
return result;
}
static char *transition_temporary(const char *path, const char *name)
{
const char *slash = strrchr(path, '/'), *base = slash ? slash + 1 : path;
size_t prefix = slash ? (size_t)(slash - path + 1) : 0, length;
char *result;
if (!name || strncmp(name, ".holy-update-", 13) || !name[13] ||
strchr(name, '/') || !strcmp(base, name)) return NULL;
length = strlen(name);
if (length > (size_t)-1 - prefix - 1) return NULL;
result = malloc(prefix + length + 1);
if (result) { memcpy(result, path, prefix); memcpy(result + prefix, name, length + 1); }
return result;
}
int holy_install_entry_state(int root, const struct holy_manifest_entry *entry,
const char *temporary, struct stat *observed)
{
struct holy_manifest_entry staged;
char *path;
int result;
if (!temporary) return transition_matches(root, entry, observed);
if (!transition_entry_valid(entry) || !(path = transition_temporary(entry->path, temporary))) return -1;
staged = *entry; staged.path = path;
result = transition_matches(root, &staged, observed);
free(path);
return result;
}
int holy_install_prepare_link(int root, const struct holy_manifest_entry *next,
const char *temporary, const struct holy_manifest_entry *source,
const char *source_temporary, int recovering)
{
struct holy_manifest_entry staged;
char *path = NULL, *from = NULL;
int ok = 0;
if (!transition_entry_valid(next) || next->link || !source || source->link ||
!(path = transition_temporary(next->path, temporary))) goto done;
from = source_temporary ? transition_temporary(source->path, source_temporary) : strdup(source->path);
if (!from) goto done;
staged = *next; staged.path = path;
ok = link_payload(root, from, &staged, recovering);
done:
free(path); free(from);
return ok;
}
int holy_install_remove_temporary(int root, const struct holy_manifest_entry *entry,
const char *temporary)
{
struct stat observed;
char *path;
int state = holy_install_entry_state(root, entry, temporary, &observed), ok;
if (state == 2) return 1;
if (state != 1 || !(path = transition_temporary(entry->path, temporary))) return 0;
ok = remove_file(root, path, &observed);
free(path);
return ok;
}
int holy_install_transition_check(int root, const struct holy_manifest_entry *before,
const struct holy_manifest_entry *after, int recovering)
{
const char *path;
if ((!before && !after) || (before && !transition_entry_valid(before)) ||
(after && !transition_entry_valid(after)) ||
(before && after && strcmp(before->path, after->path))) return 0;
path = before ? before->path : after->path;
if (before ? transition_matches(root, before, NULL) == 1 : transition_absent(root, path)) return 1;
return recovering && (after ? transition_matches(root, after, NULL) == 1 : transition_absent(root, path));
}
int holy_install_prepare_file(int root, const struct holy_manifest_entry *next,
int content_fd, const char *temporary)
{
struct holy_manifest_entry staged;
char *path = NULL, *storage = NULL;
const char *base;
char buffer[65536];
struct stat input;
int parent = -1, fd = -1, created = 0, ok = 0;
long long offset = 0;
if (!transition_entry_valid(next) || next->hardlink || !(path = transition_temporary(next->path, temporary))) goto done;
staged = *next; staged.path = path;
parent = parent_fd(root, path, &storage, &base);
if (parent < 0) goto done;
if (next->link) {
if (symlinkat(next->link, parent, base)) goto done;
created = 1;
} else {
if (fstat(content_fd, &input) || !S_ISREG(input.st_mode) || input.st_size != next->size) goto done;
fd = openat(parent, base, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0) goto done;
created = 1;
while (offset < next->size) {
size_t used = 0, amount = (unsigned long long)(next->size - offset) < sizeof buffer ?
(size_t)(next->size - offset) : sizeof buffer;
ssize_t got = pread(content_fd, buffer, amount, (off_t)offset);
if (got < 0 && errno == EINTR) continue;
if (got <= 0) goto done;
while (used < (size_t)got) {
ssize_t sent = write(fd, buffer + used, (size_t)got - used);
if (sent < 0 && errno == EINTR) continue;
if (sent <= 0) goto done;
used += (size_t)sent;
}
offset += got;
}
if (fchmod(fd, next->mode) || fsync(fd)) goto done;
}
if (transition_matches(root, &staged, NULL) != 1 || fsync(parent)) goto done;
ok = 1;
done:
if (fd >= 0) close(fd);
if (!ok && created) { unlinkat(parent, base, 0); fsync(parent); }
if (parent >= 0) close(parent);
free(storage); free(path);
return ok;
}
int holy_install_temporary_state(int root, const struct holy_manifest_entry *next,
const char *temporary)
{
struct holy_manifest_entry staged;
char *path;
int result;
if (!transition_entry_valid(next) || !(path = transition_temporary(next->path, temporary))) return 0;
staged = *next; staged.path = path;
result = transition_matches(root, &staged, NULL);
free(path);
return result < 0 ? 0 : result;
}
int holy_install_transition(int root, const struct holy_manifest_entry *before,
const struct holy_manifest_entry *after, const char *temporary)
{
struct holy_manifest_entry staged;
struct stat original, ready, current;
char *path = NULL, *storage = NULL;
const char *target = before ? before->path : after ? after->path : NULL, *base;
int parent = -1, ready_state, completed, ok = 0;
if (!holy_install_transition_check(root, before, after, 1)) return 0;
parent = parent_fd(root, target, &storage, &base);
if (parent < 0) goto done;
if (!after) {
if (transition_absent(root, target)) ok = !fsync(parent);
else if (transition_matches(root, before, &original) == 1) ok = remove_file(root, target, &original);
goto done;
}
path = transition_temporary(target, temporary);
if (!path) goto done;
staged = *after; staged.path = path;
completed = transition_matches(root, after, NULL) == 1;
ready_state = transition_matches(root, &staged, &ready);
if (completed && ready_state == 1 &&
(after->group || (before && (before->group || before->hardlink)))) {
struct stat published;
if (transition_matches(root, after, &published) != 1) goto done;
if (published.st_dev != ready.st_dev || published.st_ino != ready.st_ino) completed = 0;
}
if (completed) {
if (ready_state == 2) ok = !fsync(parent);
else if (ready_state == 1) ok = remove_file(root, path, &ready);
goto done;
}
if (ready_state != 1 || (before && transition_matches(root, before, &original) != 1)) goto done;
if (fstatat(parent, temporary, ¤t, AT_SYMLINK_NOFOLLOW) || !same_object(&ready, ¤t)) goto done;
if (before) {
if (fstatat(parent, base, ¤t, AT_SYMLINK_NOFOLLOW) || !same_object(&original, ¤t)) goto done;
if (renameat(parent, temporary, parent, base)) {
perror("holypkg: replace update payload");
goto done;
}
} else {
#ifdef SYS_renameat2
/* rename_noreplace also works on filesystems without hard links. */
if (syscall(SYS_renameat2, parent, temporary, parent, base, 1u)) {
if (errno == ENOSYS || errno == EINVAL || errno == EOPNOTSUPP)
fputs("holypkg: renameat2(RENAME_NOREPLACE) unavailable\n", stderr);
goto done;
}
#else
errno = ENOSYS;
fputs("holypkg: renameat2(RENAME_NOREPLACE) unavailable\n", stderr);
goto done;
#endif
}
ok = !fsync(parent);
done:
if (parent >= 0) close(parent);
free(storage); free(path);
return ok;
}