Files
libglacier-ng/src/transaction.c
T
2026-10-05 14:47:58 -04:00

411 lines
10 KiB
C

#include <linux/limits.h>
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include "common.h"
#include "istoreutils.h"
#include "log.h"
#include "transaction.h"
/* The one directory a scope owns; everything below derives from it.
* See the layout comment in common.h. */
static void
scope_base(gl_ctx_scope_t scope, uid_t uid, char *out)
{
switch (scope) {
case GL_SCOPE_USR:
gl_usr_base(uid, out, PATH_MAX);
break;
case GL_SCOPE_SYS:
snprintf(out, PATH_MAX, "%s", GL_SYS_ROOT);
break;
}
}
static void
scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *name, char *out)
{
char base[PATH_MAX];
scope_base(scope, uid, base);
snprintf(out, PATH_MAX, "%s/%s", base, name);
}
static int
fsync_dir(const char *path)
{
int fd = open(path, O_RDONLY | O_DIRECTORY);
if (fd < 0) {
return -1;
}
int r = fsync(fd);
close(fd);
return r;
}
static int
atomic_swap(const char *live, const char *stage, const char *backup)
{
if (rename(live, backup) != 0) {
return -1;
}
if (rename(stage, live) != 0) {
rename(backup, live);
return -1;
}
return 0;
}
static void
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
char *store_out, char *links_out)
{
scope_subdir(scope, uid, "index", index_out);
scope_subdir(scope, uid, "store", store_out);
/* For system scope this is the DEFAULT links location — for
* packages the operator explicitly installs under system scope that
* aren't part of the base system. Base-repo packages get routed to
* /usr instead, but that's a per-package decision made in
* gl_link_pkg/gl_relink_store (which know a package's repo), not
* something the context can decide up front. See GL_BASE_REPO in
* istoreutils.c. */
scope_subdir(scope, uid, "links", links_out);
}
static int
check_same_filesystem(const char *a, const char *b)
{
struct stat sa, sb;
if (stat(a, &sa) != 0 || stat(b, &sb) != 0) {
return -1;
}
return sa.st_dev == sb.st_dev;
}
static int
hardlink_tree(const char *src, const char *dest)
{
DIR *dir = opendir(src);
if (!dir) {
return (errno == ENOENT) ? 0 : -1;
}
struct dirent *ent;
while ((ent = readdir(dir))) {
/* Only . and .. — dotfiles are real content here: a package's
* own dotfiles, and the store's .displaced originals (see
* gl_link_pkg), both of which would otherwise be dropped from
* the stage and lost on commit. */
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
char s[PATH_MAX], d[PATH_MAX];
snprintf(s, sizeof(s), "%s/%s", src, ent->d_name);
snprintf(d, sizeof(d), "%s/%s", dest, ent->d_name);
struct stat st;
if(lstat(s, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
if (mkdir(d, st.st_mode & 0777) != 0 && errno != EEXIST) {
closedir(dir);
return -1;
}
if (hardlink_tree(s, d) != 0) {
closedir(dir);
return -1;
}
}
else if (S_ISLNK(st.st_mode)) {
char target[PATH_MAX];
ssize_t n = readlink(s, target, sizeof(target) - 1);
if (n < 0) { continue; }
target[n] = '\0';
symlink(target, d);
}
else {
if (link(s, d) != 0 && errno != EEXIST) {
closedir(dir);
return -1;
}
}
}
closedir(dir);
return 0;
}
int
gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
{
if (!ctx) {
return -1;
}
ctx->uid = uid;
ctx->mode = GL_CTX_LIVE;
ctx->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
ctx->links_path);
return 0;
}
gl_txn_status_t
gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
{
if (!ctx) {
return GL_TXN_ERR_NO_TXN;
}
ctx->uid = uid;
ctx->mode = GL_CTX_STAGE;
ctx->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
if (scope == GL_SCOPE_USR) {
switch (gl_usr_base_check(uid)) {
case GL_USR_BASE_OK:
break;
case GL_USR_BASE_MISSING:
lg_printf(2, "No Glacier directory for UID %u. Run doas gstore -n first.", uid);
return GL_TXN_ERR_PERM;
case GL_USR_BASE_UNSAFE_ROOT:
lg_printf(2, "Refusing to run: %s is not root-owned, or is group/world-writable.",
GL_USR_ROOT);
return GL_TXN_ERR_PERM;
case GL_USR_BASE_UNSAFE:
lg_printf(2, "Refusing to run: %s/%u is not a directory owned by UID %u with mode 0700.",
GL_USR_ROOT, uid, uid);
return GL_TXN_ERR_PERM;
}
}
char stage_base[PATH_MAX - 7];
scope_subdir(scope, uid, "stage", stage_base);
snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base);
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
snprintf(ctx->links_path, PATH_MAX, "%s/links", stage_base);
/* Lock BEFORE clearing a leftover stage: the other order let a
* second invocation wipe a running transaction's stage and only
* then discover it couldn't get the lock. */
gl_txn_status_t lock_status = gl_begin_transaction(ctx);
if (lock_status != GL_TXN_OK) {
return lock_status;
}
/* EBUSY: stage_base is itself a mount point. Its contents are gone
* and the directory stays, which is all a fresh stage needs (the
* mkdirps below tolerate it existing). Commit then reports
* GL_TXN_ERR_XDEV, as it should for a stage on another filesystem. */
struct stat st;
if (stat(stage_base, &st) == 0) {
if (gl_rmdir_recursive(stage_base) != 0 && errno != EBUSY) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
}
if (gl_mkdirp(ctx->index_path, 0700) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
if (gl_mkdirp(ctx->store_path, 0700) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
if (gl_mkdirp(ctx->links_path, 0700) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
if (chown(stage_base, uid, (gid_t)-1) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_PERM;
}
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
derive_base_paths(scope, uid, live_index, live_store, live_links);
/* live_links is populated (derive_base_paths always fills all
* three) but intentionally unused below — see the comment on the
* hardlink_tree call. */
/* Only index and store need copy-on-write seeding — the staged
* links tree is never actually read from or written to (gl_link_pkg
* skips GL_CTX_STAGE entirely; gl_relink_store always builds its
* own fresh live context rather than touching ctx->links_path of a
* stage context). Seeding it was always wasted work; now that
* live_links can be /usr for system scope, it would also be
* actively dangerous — hardlinking the entire /usr tree on every
* transaction, and risking EXDEV outright if /usr and the stage
* area are on different filesystems. */
if (hardlink_tree(live_index, ctx->index_path) != 0 ||
hardlink_tree(live_store, ctx->store_path) != 0) {
/* link() can't cross filesystems: a stage on a different
* fs than live is caught here, before anything is staged,
* rather than at commit's rename. Same condition, same
* status as commit's check. */
int seed_errno = errno;
gl_abort_transaction(ctx);
return seed_errno == EXDEV ? GL_TXN_ERR_XDEV : GL_TXN_ERR_MKDIR;
}
return GL_TXN_OK;
}
gl_txn_status_t
gl_begin_transaction(gl_context_t *ctx)
{
if (!ctx) {
return GL_TXN_ERR_NO_TXN;
}
char lock_path[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "lock", lock_path);
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
if (fd < 0) {
return GL_TXN_ERR_LOCK;
}
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
close(fd);
return GL_TXN_ERR_LOCK;
}
ctx->lock_fd = fd;
return GL_TXN_OK;
}
gl_txn_status_t
gl_commit_transaction(gl_context_t *ctx)
{
if (!ctx || ctx->lock_fd < 0) {
return GL_TXN_ERR_NO_TXN;
}
if (ctx->mode != GL_CTX_STAGE) {
return GL_TXN_ERR_NO_TXN;
}
char stage_base[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "stage", stage_base);
int same = check_same_filesystem(ctx->root_path, stage_base);
if (same < 0) {
return GL_TXN_ERR_RENAME; /* failed to stat either side */
}
if (!same) {
return GL_TXN_ERR_XDEV;
}
/* Derive live paths the same way gl_init_live_context would */
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
derive_base_paths(ctx->scope, ctx->uid, live_index, live_store,
live_links);
/* Derive old/backup paths */
char old_base[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "old", old_base);
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
snprintf(old_index, PATH_MAX, "%s/index", old_base);
snprintf(old_store, PATH_MAX, "%s/store", old_base);
snprintf(old_links, PATH_MAX, "%s/links", old_base);
struct stat old_st;
if (stat(old_base, &old_st) == 0) {
if (gl_rmdir_recursive(old_base) != 0) {
return GL_TXN_ERR_RENAME;
}
}
if (gl_mkdirp(old_base, 0700) != 0) {
return GL_TXN_ERR_RENAME;
}
if (atomic_swap(live_index, ctx->index_path, old_index) != 0) {
return GL_TXN_ERR_RENAME;
}
if (atomic_swap(live_store, ctx->store_path, old_store) != 0) {
rename(live_index, ctx->index_path);
rename(old_index, live_index);
return GL_TXN_ERR_RENAME;
}
if (atomic_swap(live_links, ctx->links_path, old_links) != 0) {
rename(live_store, ctx->store_path);
rename(old_store, live_store);
rename(live_index, ctx->index_path);
rename(old_index, live_index);
return GL_TXN_ERR_RENAME;
}
/* fsync the directory the renames happened in */
char base[PATH_MAX];
scope_base(ctx->scope, ctx->uid, base);
fsync_dir(base);
if (ctx->scope == GL_SCOPE_SYS) {
fsync_dir("/usr"); /* base-repo packages land here instead */
}
gl_context_t live_ctx;
gl_init_live_context(&live_ctx, ctx->scope, ctx->uid);
if (gl_relink_store(&live_ctx) != 0) {
lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid);
}
gl_rmdir_recursive(old_base);
/* Remove stage base */
gl_rmdir_recursive(stage_base);
close(ctx->lock_fd);
ctx->lock_fd = -1;
return GL_TXN_OK;
}
gl_txn_status_t
gl_abort_transaction(gl_context_t *ctx)
{
if (!ctx) {
return GL_TXN_ERR_NO_TXN;
}
/* Remove stage area — live is untouched */
char stage_uid[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "stage", stage_uid);
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
/* Release lock */
if (ctx->lock_fd >= 0) {
close(ctx->lock_fd);
ctx->lock_fd = -1;
}
return GL_TXN_OK;
}