#include #define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #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; }