#define _DEFAULT_SOURCE #define _POSIX_C_SOURCE 200809L #include "install.h" #include "verify.h" #include "config.h" #include "script.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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; }