_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file src/cache.c
#define _POSIX_C_SOURCE 200809L
#include "cache.h"
#include "deps.h"
#include "fetch.h"
#include "package.h"
#include "scan.h"
#include "stage.h"
#include "verify.h"
#include "provides.h"
#include "state.h"
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <inttypes.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/file.h>
#include <unistd.h>
static int cache_directory(const char *root_path, int create)
{
static const char *const parts[] = {
"var", "cache", "holypkg", "objects", "sha256"
};
struct stat st;
size_t i;
int current = open(root_path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (current < 0) return -1;
for (i = 0; i <= sizeof parts / sizeof *parts; ++i) {
int next;
if (fstat(current, &st) ||
(st.st_uid != geteuid() && st.st_uid != 0) ||
(st.st_mode & 0022)) {
fprintf(stderr, "holypkg: unsafe cache directory owner or mode\n");
close(current);
return -1;
}
if (i == sizeof parts / sizeof *parts) return current;
if (create) {
if (mkdirat(current, parts[i], 0700)) {
if (errno != EEXIST) { close(current); return -1; }
} else if (fsync(current)) { close(current); return -1; }
}
next = openat(current, parts[i], O_RDONLY | O_DIRECTORY |
O_NOFOLLOW | O_CLOEXEC);
close(current);
if (next < 0) return -1;
current = next;
}
return -1;
}
static int unavailable_dir(int cache, int create)
{
struct stat st;
int dir;
if (create && mkdirat(cache, ".unavailable", 0700) && errno != EEXIST) return -1;
dir = openat(cache, ".unavailable", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (dir < 0) return -1;
if (fstat(dir, &st) || (st.st_uid != geteuid() && st.st_uid != 0) ||
(st.st_mode & 0022)) { close(dir); errno = EPERM; return -1; }
return dir;
}
static int clear_unavailable(int cache, const char *digest)
{
int dir = unavailable_dir(cache, 0), ok;
if (dir < 0) return errno == ENOENT;
ok = !unlinkat(dir, digest, 0) || errno == ENOENT;
if (ok) ok = !fsync(dir);
close(dir);
return ok;
}
static int mark_unavailable(int cache, const char *digest)
{
char record[120];
size_t length = (size_t)snprintf(record, sizeof record,
"format holy-cache-unavailable-1\nartifact %s\n", digest);
struct stat st;
int dir = unavailable_dir(cache, 1), fd, ok = 0;
if (dir < 0 || length >= sizeof record) return 0;
fd = openat(dir, digest, O_WRONLY | O_CREAT | O_EXCL | O_NOFOLLOW | O_CLOEXEC, 0600);
if (fd < 0 && errno == EEXIST) {
char existing[120];
fd = openat(dir, digest, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd >= 0 && !fstat(fd, &st) && S_ISREG(st.st_mode) &&
st.st_size == (off_t)length && !(st.st_mode & 0022) &&
(st.st_uid == geteuid() || st.st_uid == 0) &&
pread(fd, existing, length, 0) == (ssize_t)length &&
!memcmp(existing, record, length)) ok = 1;
} else if (fd >= 0) {
ok = write(fd, record, length) == (ssize_t)length && !fsync(fd);
}
if (fd >= 0) close(fd);
if (ok) ok = !fsync(dir);
close(dir);
return ok;
}
int holy_cache_stage_local_digest(const char *source, const char *root_path,
char output[65])
{
struct holy_package_identity identity = {0};
struct stat st;
char name[70];
char *snapshot = holy_stage_local(source, "holy-cache");
int current = -1, ok = 0;
if (output) output[0] = 0;
if (!snapshot || !holy_verify_with_output(snapshot, 0) ||
!holy_scan_local_with_output(snapshot, 0) ||
!holy_deps_local_with_output(snapshot, 0) ||
!holy_provides_local(snapshot, 0) ||
!holy_package_identity(snapshot, &identity)) goto done;
current = cache_directory(root_path, 1);
if (current < 0 || flock(current, LOCK_EX)) goto done;
if (!holy_fetch_at(snapshot, current, identity.digest, name) ||
fstatat(current, name, &st, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(st.st_mode) || st.st_uid != geteuid() ||
(st.st_mode & 0022) || !clear_unavailable(current, identity.digest)) goto done;
printf("%s/var/cache/holypkg/objects/sha256/%s\n", root_path, name);
if (output) memcpy(output, identity.digest, 65);
ok = 1;
done:
if (!ok) fprintf(stderr, "holypkg: cache stage failed; inspect target cache\n");
if (current >= 0) close(current);
holy_package_identity_free(&identity);
if (snapshot) { unlink(snapshot); free(snapshot); }
return ok;
}
int holy_cache_stage_local(const char *source, const char *root_path)
{
return holy_cache_stage_local_digest(source, root_path, NULL);
}
char *holy_cache_snapshot(const char *digest, const char *root_path)
{
struct holy_package_identity identity = {0};
struct stat st;
char name[70], *snapshot = NULL;
size_t i;
int dir = -1, fd = -1, ok = 0;
if (!digest || strlen(digest) != 64) goto done;
for (i = 0; i < 64; ++i)
if (!((digest[i] >= '0' && digest[i] <= '9') ||
(digest[i] >= 'a' && digest[i] <= 'f'))) goto done;
snprintf(name, sizeof name, "%s.holy", digest);
dir = cache_directory(root_path, 0);
if (dir < 0) goto done;
fd = openat(dir, name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0 || fstat(fd, &st) || !S_ISREG(st.st_mode) ||
(st.st_mode & 0022)) goto done;
snapshot = holy_stage_fd(fd, "holy-cache-verify");
if (!snapshot || !holy_verify_with_output(snapshot, 0) ||
!holy_scan_local_with_output(snapshot, 0) ||
!holy_deps_local_with_output(snapshot, 0) ||
!holy_provides_local(snapshot, 0) ||
!holy_package_identity(snapshot, &identity) ||
strcmp(identity.digest, digest)) goto done;
ok = 1;
done:
if (!ok) fprintf(stderr, "holypkg: cache object verification failed\n");
holy_package_identity_free(&identity);
if (fd >= 0) close(fd);
if (dir >= 0) close(dir);
if (!ok && snapshot) { unlink(snapshot); free(snapshot); snapshot = NULL; }
return snapshot;
}
static int verify_object(const char *digest, const char *root_path, int emit)
{
struct holy_package_identity identity = {0};
char *snapshot = holy_cache_snapshot(digest, root_path);
int ok = snapshot != NULL;
if (ok && emit) {
ok = holy_package_identity(snapshot, &identity);
if (ok) printf("verified %s %s %s %s\n", identity.digest, identity.name,
identity.arch, identity.libc);
}
holy_package_identity_free(&identity);
if (snapshot) { unlink(snapshot); free(snapshot); }
return ok;
}
int holy_cache_verify(const char *digest, const char *root_path)
{
return verify_object(digest, root_path, 1);
}
int holy_cache_object(const char *digest, const char *root_path)
{
return verify_object(digest, root_path, 0);
}
static int cache_name(const char *name)
{
size_t i;
if (strlen(name) != 69 || strcmp(name + 64, ".holy")) return 0;
for (i = 0; i < 64; ++i)
if (!((name[i] >= '0' && name[i] <= '9') ||
(name[i] >= 'a' && name[i] <= 'f'))) return 0;
return 1;
}
static int unavailable_name(const char *name)
{
char object[70];
if (strlen(name) != 64) return 0;
snprintf(object, sizeof object, "%s.holy", name);
return cache_name(object);
}
static int name_order(const void *left, const void *right)
{
const char *const *a = left, *const *b = right;
return strcmp(*a, *b);
}
static int cache_names(int dir, char ***output, size_t *count, int unavailable)
{
DIR *list = NULL;
struct dirent *entry;
char **names = NULL;
size_t used = 0;
int ok = 0;
*output = NULL; *count = 0;
list = fdopendir(dup(dir));
if (!list) return 0;
errno = 0;
while ((entry = readdir(list))) {
struct stat st;
char **grown;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..") ||
(!unavailable && !strcmp(entry->d_name, ".unavailable"))) continue;
if (!(unavailable ? unavailable_name(entry->d_name) : cache_name(entry->d_name)) ||
used == 100000 ||
fstatat(dir, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) ||
!S_ISREG(st.st_mode) || (st.st_mode & 0022)) goto done;
grown = realloc(names, (used + 1) * sizeof *grown);
if (!grown) goto done;
names = grown;
names[used] = strdup(entry->d_name);
if (!names[used]) goto done;
++used;
errno = 0;
}
if (errno) goto done;
qsort(names, used, sizeof *names, name_order);
*output = names; *count = used; names = NULL; used = 0;
ok = 1;
done:
while (used) free(names[--used]);
free(names);
closedir(list);
return ok;
}
static int transaction_refs(int dir, const char *digest, unsigned depth)
{
DIR *list;
struct dirent *entry;
int result = 0;
if (depth > 4 || (list = fdopendir(dup(dir))) == NULL) return -1;
errno = 0;
while ((entry = readdir(list))) {
struct stat st;
int fd;
if (!strcmp(entry->d_name, ".") || !strcmp(entry->d_name, "..")) continue;
if (strstr(entry->d_name, digest)) { result = 1; break; }
if (fstatat(dir, entry->d_name, &st, AT_SYMLINK_NOFOLLOW) ||
(st.st_mode & 0022)) { result = -1; break; }
if (S_ISDIR(st.st_mode)) {
fd = openat(dir, entry->d_name, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) { result = -1; break; }
result = transaction_refs(fd, digest, depth + 1);
close(fd);
} else if (S_ISREG(st.st_mode) && st.st_size <= 4 * 1024 * 1024) {
char buffer[4096 + 63];
ssize_t got;
size_t carry = 0, i;
fd = openat(dir, entry->d_name, O_RDONLY | O_NOFOLLOW | O_CLOEXEC | O_NONBLOCK);
if (fd < 0) { result = -1; break; }
while ((got = read(fd, buffer + carry, 4096)) > 0) {
size_t length = carry + (size_t)got;
for (i = 0; i + 64 <= length; ++i)
if (!memcmp(buffer + i, digest, 64)) { result = 1; break; }
if (result) break;
carry = length < 63 ? length : 63;
memmove(buffer, buffer + length - carry, carry);
}
if (got < 0) result = -1;
close(fd);
} else result = -1;
if (result) break;
errno = 0;
}
if (!entry && errno) result = -1;
closedir(list);
return result;
}
int holy_cache_list(const char *root_path)
{
char **names = NULL;
size_t count = 0, i;
unsigned long long generation;
int status = 1, state = -1, installed = -1, cache = -1, unavailable = -1, result = 1;
state = holy_state_lock(root_path, 0, &generation, &status);
if (state < 0) return status;
installed = openat(state, "installed", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
cache = cache_directory(root_path, 0);
if (installed < 0 || cache < 0 || flock(cache, LOCK_SH) ||
!cache_names(cache, &names, &count, 0)) goto done;
for (i = 0; i < count; ++i) {
struct stat st, used;
char digest[65];
memcpy(digest, names[i], 64); digest[64] = 0;
if (fstatat(cache, names[i], &st, AT_SYMLINK_NOFOLLOW) || !S_ISREG(st.st_mode)) goto done;
if (!fstatat(installed, digest, &used, AT_SYMLINK_NOFOLLOW)) {
if (!S_ISDIR(used.st_mode)) goto done;
printf("cache %s size %ju installed\n", digest, (uintmax_t)st.st_size);
} else if (errno == ENOENT)
printf("cache %s size %ju retained\n", digest, (uintmax_t)st.st_size);
else goto done;
}
for (i = 0; i < count; ++i) free(names[i]);
free(names); names = NULL; count = 0;
unavailable = unavailable_dir(cache, 0);
if (unavailable >= 0) {
if (!cache_names(unavailable, &names, &count, 1)) goto done;
for (i = 0; i < count; ++i) {
struct stat st;
char object[70];
snprintf(object, sizeof object, "%s.holy", names[i]);
if (fstatat(cache, object, &st, AT_SYMLINK_NOFOLLOW) == 0) continue;
if (errno != ENOENT) goto done;
printf("cache %s unavailable\n", names[i]);
}
} else if (errno != ENOENT) goto done;
result = ferror(stdout) ? 1 : 0;
done:
for (i = 0; i < count; ++i) free(names[i]);
free(names);
if (unavailable >= 0) close(unavailable);
if (cache >= 0) close(cache);
if (installed >= 0) close(installed);
if (state >= 0) close(state);
return result;
}
int holy_cache_clean(const char *digest, const char *root_path, int yes,
int accept_unavailable)
{
char name[70];
struct stat st, installed_st;
unsigned long long generation;
int status = 1, state = -1, installed = -1, transactions = -1;
int cache = -1, result = 1, refs, installed_ref = 0;
if (accept_unavailable && !yes) return 2;
if (!digest || strlen(digest) != 64) return 2;
snprintf(name, sizeof name, "%s.holy", digest);
if (!cache_name(name)) return 2;
state = holy_state_lock(root_path, yes, &generation, &status);
if (state < 0) return status;
installed = openat(state, "installed", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
transactions = openat(state, "transactions", O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
cache = cache_directory(root_path, 0);
if (installed < 0 || transactions < 0 || cache < 0 ||
flock(cache, yes ? LOCK_EX : LOCK_SH)) goto done;
if (fstatat(cache, name, &st, AT_SYMLINK_NOFOLLOW)) {
result = errno == ENOENT ? 6 : 1; goto done;
}
if (!S_ISREG(st.st_mode) || (st.st_mode & 0022)) goto done;
if (!fstatat(installed, digest, &installed_st, AT_SYMLINK_NOFOLLOW)) {
if (!S_ISDIR(installed_st.st_mode)) goto done;
installed_ref = 1;
} else if (errno != ENOENT) goto done;
refs = transaction_refs(transactions, digest, 0);
if (refs < 0) goto done;
if (!yes) {
printf("cache-clean-plan %s size %ju generation %llu installed %d transactions %d read-only\n",
digest, (uintmax_t)st.st_size, generation, installed_ref, refs);
result = 0; goto done;
}
if ((installed_ref || refs) && !accept_unavailable) {
fprintf(stderr, "holypkg: decision-required cache object %s installed=%d transactions=%d; use --accept-unavailable after review\n",
digest, installed_ref, refs);
result = 3; goto done;
}
if (!mark_unavailable(cache, digest)) goto done;
if (unlinkat(cache, name, 0) || fsync(cache)) goto done;
printf("cache-cleaned %s unavailable\n", digest);
result = 0;
done:
if (cache >= 0) close(cache);
if (transactions >= 0) close(transactions);
if (installed >= 0) close(installed);
if (state >= 0) close(state);
return result;
}