Files
libglacier-ng/src/istoreutils.c
T
2026-10-14 00:50:56 -04:00

2352 lines
81 KiB
C

#define _POSIX_C_SOURCE 200809L
#include <archive.h>
#include <archive_entry.h>
#include <ctype.h>
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <time.h>
#include <unistd.h>
#include "common.h"
#include "deps.h"
#include "istoreutils.h"
#include "log.h"
#include "pkg.h"
#include "transaction.h"
/* Layout: see the comment above GL_USR_ROOT in common.h. */
/* Base-system exception: packages in the "base" repo under system
* scope get their symlinks placed directly in /usr rather than the
* default isolated /glacier/sys/links, so boot/init/scripts that
* hardcode /usr/bin, /usr/lib, etc. work without glacier-aware PATH
* setup. Everything else installed under system scope (an operator
* running `gpkg -s` on something that isn't part of the base system)
* stays under /glacier/sys/links. See gl_link_pkg / gl_relink_store. */
#define GL_BASE_REPO "base"
/* An extracted entry's mode: the archive's, never group/world-writable,
* and applied explicitly so the caller's umask doesn't decide whether
* other users can run a system package. */
#define GL_ENTRY_MODE(e) ((mode_t)(archive_entry_perm(e) & 07755))
#define GL_BASE_LINKS_DEST "/usr"
bool
gl_usr_istore_exists(index_store_t index_or_store, int uid)
{
char path[PATH_MAX];
struct stat st;
switch (index_or_store) {
case INDEX:
snprintf(path, sizeof(path), GL_USR_ROOT "/%d/index", uid);
break;
case STORE:
snprintf(path, sizeof(path), GL_USR_ROOT "/%d/store", uid);
break;
default:
printf("libglacier: argument index_or_store of function gl_usr_istore_exists must be INDEX or STORE\n");
return false;
}
if (stat(path, &st) != 0) {
return false;
}
return S_ISDIR(st.st_mode);
}
/* Exported form of common.h's gl_usr_base_check, so a CLI can say WHY a
* user's directory is unusable (missing, squatted, loosened parent)
* before it ever starts a transaction. Returns a gl_usr_base_status_t. */
int
gl_usr_base_status(uid_t uid)
{
return (int)gl_usr_base_check(uid);
}
static void
make_index_name(char *buffer, size_t buffersz)
{
time_t now = time(NULL);
struct tm *tm = localtime(&now);
strftime(buffer, buffersz, "index-%m_%d_%Y@%H:%M:%S", tm);
}
static int
create_index(const char *ind_path, uid_t uid, mode_t mode)
{
char fname[64];
make_index_name(fname, sizeof(fname));
char full_path[PATH_MAX];
snprintf(full_path, sizeof(full_path), "%s/%s", ind_path, fname);
FILE *f = fopen(full_path, "w");
if (!f) { return -1; }
fprintf(f, "uid = %d\n", uid);
fprintf(f, "listed = 0\n\n");
fclose(f);
/* fopen()'s mode is subject to the caller's umask; every later
* index file goes through mkstemp() in gl_rebuild_index(), which
* then sets the scope's mode explicitly. Do the same here so the
* first index isn't the one file whose permissions depend on
* whatever umask gstore happened to run under. */
if (chmod(full_path, mode) != 0) { return -1; }
if (chown(full_path, uid, (gid_t)-1) != 0) { return -1; }
char link_path[PATH_MAX];
snprintf(link_path, sizeof(link_path), "%s/current", ind_path);
unlink(link_path);
if (symlink(fname, link_path) != 0) { return -1; }
if (lchown(link_path, uid, (gid_t)-1) != 0) { return -1; }
return 0;
}
static bool
is_dir(const char *d)
{
struct stat st;
return stat(d, &st) == 0 && S_ISDIR(st.st_mode);
}
/*
* is_glacier_symlink
* Returns true only if `path` is a symlink whose target lives under
* `store_prefix` — i.e. a symlink glacier itself created, as opposed
* to some unrelated file, directory, or foreign symlink that happens
* to occupy that path. This distinction only matters once a scope's
* links_path can alias a directory glacier doesn't exclusively own
* (e.g. /usr for system-scope packages) rather than always being an
* isolated glacier-only tree. Everywhere this is used, the rule is the
* same: if this returns false, never touch the path — not overwrite
* it, not prune it, nothing.
*/
static bool
is_glacier_symlink(const char *path, const char *store_prefix)
{
char target[PATH_MAX];
ssize_t n = readlink(path, target, sizeof(target) - 1);
if (n < 0) { return false; } /* not a symlink, or doesn't exist */
target[n] = '\0';
/* The store itself, or something under it: "/glacier/sys/store"
* must not also match "/glacier/sys/store2/...". */
size_t plen = strlen(store_prefix);
return strncmp(target, store_prefix, plen) == 0 &&
(target[plen] == '/' || target[plen] == '\0');
}
int
gl_init_user(uid_t uid, bool isVerbose)
{
if (isVerbose) { lg_printf(0, "Checking if UID is within acceptable bounds..."); }
if (uid < 1000) {
if (isVerbose) {
lg_printf(2, "Attempted to create index and store for UID < 1000. Quitting...");
}
return 2;
}
/* The shared parents. Only root can create these (and only root
* should own them) — see the layout comment in common.h. */
const char *parents[] = { "/glacier", GL_USR_ROOT };
for (int i = 0; i < 2; i++) {
if (mkdir(parents[i], 0755) == -1 && errno != EEXIST) {
if (errno == EACCES) {
lg_printf(2, "Cannot create %s: permission denied (run as root, or doas gstore -n)",
parents[i]);
} else {
lg_printf(2, "Cannot create %s: %s", parents[i], strerror(errno));
}
return 1;
}
}
/* Never hand out a base under a parent other users could write to:
* that's the squatting hole the per-uid layout exists to close. */
if (gl_usr_base_check(uid) == GL_USR_BASE_UNSAFE_ROOT) {
lg_printf(2, "Refusing to create Glacier directories: %s is not root-owned, or is group/world-writable",
GL_USR_ROOT);
return 1;
}
char base[PATH_MAX];
gl_usr_base(uid, base, sizeof(base));
if (isVerbose) { lg_printf(0, "Creating %s", base); }
if (mkdir(base, 0700) == -1) {
switch (errno) {
case EEXIST:
lg_printf(2, "Cannot create %s: already exists", base);
return 1;
case EACCES:
lg_printf(2, "Cannot create %s: permission denied (run as root, or doas gstore -n)", base);
return 1;
case ENOSPC:
lg_printf(2, "Cannot create %s: no space left on device", base);
return 1;
case EROFS:
lg_printf(2, "Cannot create %s: read only filesystem", base);
return 1;
default:
lg_printf(2, "Cannot create %s: %s", base, strerror(errno));
return -1;
}
}
/* From here on everything lives inside a base we just created, so a
* failure removes it again rather than leaving a half-made one that
* the next `gstore -n` would refuse as "already exists". mkdir() is
* subject to umask, so chmod explicitly: gl_usr_base_check insists
* on no group/other bits. */
const char *subdirs[] = { "index", "store", "links" };
char sub[PATH_MAX];
char ind_path[PATH_MAX];
snprintf(ind_path, sizeof(ind_path), "%s/index", base);
if (chmod(base, 0700) != 0) { goto fail; }
for (int i = 0; i < 3; i++) {
snprintf(sub, sizeof(sub), "%s/%s", base, subdirs[i]);
if (isVerbose) { lg_printf(0, "Creating %s", sub); }
if (mkdir(sub, 0700) != 0 || chown(sub, uid, (gid_t)-1) != 0) {
lg_printf(2, "Cannot create %s: %s", sub, strerror(errno));
goto fail;
}
}
if (create_index(ind_path, uid, gl_scope_file_mode(GL_SCOPE_USR)) != 0) {
lg_printf(2, "Cannot create initial index file");
goto fail;
}
/* Hand the base over last, so it only ever becomes the user's once
* it's complete. */
if (isVerbose) { lg_printf(0, "Changing ownership of %s to UID %u", base, uid); }
if (chown(base, uid, (gid_t)-1) != 0) {
lg_printf(2, "Cannot change ownership of %s: %s", base, strerror(errno));
goto fail;
}
return 0;
fail:
gl_rmdir_recursive(base);
return 1;
}
/*
* The top-level tree of a merged-/usr system, rooted at `root` ("" or
* "/" for the running system; the target directory for a future
* `syspkg --root`). Base-repo packages only ever link under /usr
* (PACKAGE_SCOPES.txt section 3), so everything outside it — the
* /bin /sbin /lib -> usr/... links that make e.g. musl's PT_INTERP
* /lib/ld-musl-x86_64.so.1 resolve, and the dirs nothing ships — has to
* come from here. Creating it is system-scope mutation, so it's part of
* system init (syspkg -n), not of any package or bootstrap tool.
*
* ONLY creates what is missing. Anything already at a path — of any
* type, even a real /bin directory where this would make a symlink — is
* left exactly as it is: same rule as everywhere else that touches
* shared territory (is_glacier_symlink, gl_link_pkg). Nothing is written
* into /etc; how config files work is still an open decision
* (GLACIER_BOOTSTRAP.txt section 9c).
*/
int
gl_create_fs_tree(const char *root, bool isVerbose)
{
static const struct {
const char *path;
const char *link; /* non-NULL: create a symlink to this */
mode_t mode; /* directories only */
} tree[] = {
{ "/usr", NULL, 0755 },
{ "/usr/bin", NULL, 0755 },
{ "/usr/lib", NULL, 0755 },
{ "/usr/sbin", "bin", 0 },
{ "/bin", "usr/bin", 0 },
{ "/sbin", "usr/bin", 0 },
{ "/lib", "usr/lib", 0 },
{ "/etc", NULL, 0755 },
{ "/dev", NULL, 0755 },
{ "/proc", NULL, 0555 },
{ "/sys", NULL, 0555 },
{ "/run", NULL, 0755 },
{ "/tmp", NULL, 01777 },
{ "/var", NULL, 0755 },
{ "/var/tmp", NULL, 01777 },
{ "/root", NULL, 0700 },
{ "/home", NULL, 0755 },
};
if (!root) { root = ""; }
size_t rlen = strlen(root);
if (rlen > 0 && root[rlen - 1] == '/') { rlen--; } /* "/" -> "" */
for (size_t i = 0; i < sizeof(tree) / sizeof(tree[0]); i++) {
char path[PATH_MAX];
snprintf(path, sizeof(path), "%.*s%s", (int)rlen, root, tree[i].path);
struct stat st;
if (lstat(path, &st) == 0) {
if (isVerbose) { lg_printf(0, "Leaving existing %s", path); }
continue;
}
if (errno != ENOENT) {
lg_printf(2, "Cannot stat %s: %s", path, strerror(errno));
return 1;
}
if (tree[i].link) {
if (symlink(tree[i].link, path) != 0) {
lg_printf(2, "Cannot create %s -> %s: %s", path, tree[i].link, strerror(errno));
return 1;
}
if (isVerbose) { lg_printf(0, "Created %s -> %s", path, tree[i].link); }
} else {
/* chmod after mkdir: mkdir's mode is masked by umask, which
* would drop the sticky bit's companions on /tmp. */
if (mkdir(path, tree[i].mode) != 0 || chmod(path, tree[i].mode) != 0) {
lg_printf(2, "Cannot create %s: %s", path, strerror(errno));
return 1;
}
if (isVerbose) { lg_printf(0, "Created %s", path); }
}
}
return 0;
}
int
gl_init_sys(bool isVerbose)
{
return gl_init_sys_at("/", isVerbose);
}
/* gl_init_sys under an install root (IMAGE_DESIGN section 3): the tree,
* /glacier/sys and its index are all created under `sysroot`, which
* must already exist. "/" is the running system. */
int
gl_init_sys_at(const char *sysroot, bool isVerbose)
{
char root[PATH_MAX];
if (gl_normalize_sysroot(sysroot, root, sizeof(root)) != 0) {
lg_printf(2, "Refusing install root %s: not an absolute path without . or .. components.",
sysroot ? sysroot : "(null)");
return 1;
}
if (root[0] != '\0') {
char bad[PATH_MAX];
const char *why = "";
if (gl_sysroot_trusted(root, bad, sizeof(bad), &why) != 0) {
lg_printf(2, "Refusing install root %s: %s is %s.", root, bad, why);
return 1;
}
}
char glacier[PATH_MAX], sys[PATH_MAX], index[PATH_MAX], store[PATH_MAX], links[PATH_MAX];
snprintf(glacier, sizeof(glacier), "%s/glacier", root);
snprintf(sys, sizeof(sys), "%s%s", root, GL_SYS_ROOT);
snprintf(index, sizeof(index), "%s%s/index", root, GL_SYS_ROOT);
snprintf(store, sizeof(store), "%s%s/store", root, GL_SYS_ROOT);
snprintf(links, sizeof(links), "%s%s/links", root, GL_SYS_ROOT);
/* Already initialized == a system index exists. Refuse, same as
* gl_init_user does for an existing user base, before touching
* anything. A partial tree (dirs but no index yet, e.g. pre-created
* by an image build) is NOT this case: it gets completed below. */
char current[PATH_MAX];
snprintf(current, sizeof(current), "%s/current", index);
struct stat st;
if (lstat(current, &st) == 0) {
lg_printf(2, "Cannot create %s: already exists", sys);
return 1;
}
/* The top-level tree goes BEFORE the index: the index is what marks
* -n as done, so if the tree fails part-way a re-run can finish it
* instead of being refused as "already exists". */
if (gl_create_fs_tree(root[0] ? root : "/", isVerbose) != 0) {
return 1;
}
/* Parents first: unlike /glacier/usr, nothing else creates these,
* and mkdir() of sys/index would otherwise fail ENOENT. */
const char *paths[] = {
glacier,
sys,
index,
store,
links,
};
mode_t mode = gl_scope_dir_mode(GL_SCOPE_SYS);
for (int i = 0; i < 5; i++) {
if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); }
/* chmod after a successful mkdir: the store must be 0755
* whatever umask syspkg runs under (see gl_scope_dir_mode).
* A directory that already existed is left as it is. */
if (mkdir(paths[i], mode) == 0) {
if (chmod(paths[i], mode) != 0) {
lg_printf(2, "Cannot set the mode of %s: %s", paths[i], strerror(errno));
return 1;
}
} else {
switch (errno) {
case EEXIST:
break;
case EACCES:
lg_printf(2, "Cannot create %s: permission denied", paths[i]);
return 1;
case ENOSPC:
lg_printf(2, "Cannot create %s: no space left on device", paths[i]);
return 1;
case EROFS:
lg_printf(2, "Cannot create %s: read-only filesystem", paths[i]);
return 1;
default:
lg_printf(2, "Cannot create %s: mkdir failed", paths[i]);
return -1;
}
}
}
/* Checked again: create_index() unlink()s `current` first, so if a
* concurrent `syspkg -n` got here first, seeding now would reset its
* fresh index. lstat() rather than stat() so a dangling `current`
* still counts as present and is left for a human to look at. */
if (lstat(current, &st) == 0) {
lg_printf(2, "Cannot create %s: already exists", sys);
return 1;
}
if (errno != ENOENT) {
lg_printf(2, "Cannot stat %s", current);
return 1;
}
if (isVerbose) { lg_printf(0, "Creating initial system index"); }
if (create_index(index, 0, gl_scope_file_mode(GL_SCOPE_SYS)) != 0) {
lg_printf(2, "Cannot create initial system index file");
return 1;
}
return 0;
}
int
gl_rebuild_index(gl_context_t *ctx)
{
if (!ctx) { return -1; }
DIR *store_dir = opendir(ctx->store_path);
if (!store_dir) return -1;
/* Write to a new timestamped index file */
char fname[64];
make_index_name(fname, sizeof(fname));
char new_index_path[PATH_MAX];
snprintf(new_index_path, sizeof(new_index_path), "%s/%s",
ctx->index_path, fname);
/* Entries go to an anonymous temp file first (the header needs
* the count). Never fopen(new_index_path, "w"): in a stage, an
* index file written in the same second as live's has the same
* name and is a HARDLINK to live's (seeding), so truncating it
* rewrote the live index in place, and an aborted transaction
* left live listing the stage's packages. The new index only
* reaches its name by rename(), which replaces the entry, not
* the inode. */
FILE *out = tmpfile();
if (!out) {
closedir(store_dir);
return -1;
}
size_t listed = 0;
struct dirent *repo_ent;
while ((repo_ent = readdir(store_dir))) {
if (repo_ent->d_name[0] == '.') { continue; }
char repo_path[PATH_MAX];
snprintf(repo_path, sizeof(repo_path), "%s/%s",
ctx->store_path, repo_ent->d_name);
struct stat rst;
if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) {
continue;
}
DIR *repo_dir = opendir(repo_path);
if (!repo_dir) continue;
struct dirent *pkg_ent;
while ((pkg_ent = readdir(repo_dir))) {
if (pkg_ent->d_name[0] == '.') { continue; }
char pkg_path[PATH_MAX];
snprintf(pkg_path, sizeof(pkg_path), "%s/%s",
repo_path, pkg_ent->d_name);
struct stat pst;
if (stat(pkg_path, &pst) != 0 || !S_ISDIR(pst.st_mode)) {
continue;
}
/* One version dir per package name, named
* "<pkg_name>-<version>" (see gl_cat_dir_and_ver
* in pkg.c, which is what created it). Strip the
* known "<pkg_name>-" prefix to recover just the
* version part — safe even if pkg_name itself
* contains dashes, since we already know its
* exact length from pkg_ent->d_name. */
char pkg_ver[64] = "unknown";
bool automatic = false;
DIR *ver_dir = opendir(pkg_path);
if (ver_dir) {
struct dirent *ver_ent;
while ((ver_ent = readdir(ver_dir))) {
if (ver_ent->d_name[0] == '.') { continue; }
size_t prefix_len = strlen(pkg_ent->d_name) + 1;
if (strlen(ver_ent->d_name) > prefix_len) {
snprintf(pkg_ver, sizeof(pkg_ver), "%s",
ver_ent->d_name + prefix_len);
}
char marker[PATH_MAX];
struct stat mst;
snprintf(marker, sizeof(marker), "%s/%s/" GL_AUTO_MARKER,
pkg_path, ver_ent->d_name);
automatic = lstat(marker, &mst) == 0;
break;
}
closedir(ver_dir);
}
/* repo::name::version::auto|explicit. "auto" =
* pulled in as a dependency (GL_AUTO_MARKER),
* not asked for by name. */
fprintf(out, "%s::%s::%s::%s\n",
repo_ent->d_name,
pkg_ent->d_name,
pkg_ver,
automatic ? "auto" : "explicit");
listed++;
}
closedir(repo_dir);
}
closedir(store_dir);
/* Prepend header by copying through a temp file then renaming over */
FILE *tmp_in = out;
rewind(tmp_in);
char tmp_path[PATH_MAX];
snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX",
ctx->index_path);
int tmp_fd = mkstemp(tmp_path);
if (tmp_fd < 0) { fclose(tmp_in); return -1; }
if (fchmod(tmp_fd, gl_scope_file_mode(ctx->scope)) != 0) {
close(tmp_fd); unlink(tmp_path); fclose(tmp_in); return -1;
}
FILE *tmp_out = fdopen(tmp_fd, "w");
if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; }
fprintf(tmp_out, "uid = %d\n", ctx->uid);
fprintf(tmp_out, "listed = %zu\n\n", listed);
char line[512];
while (fgets(line, sizeof(line), tmp_in)) {
fputs(line, tmp_out);
}
fclose(tmp_in);
fclose(tmp_out);
rename(tmp_path, new_index_path);
chown(new_index_path, ctx->uid, (gid_t)-1);
/* Update the "current" symlink to point at the new index */
char link_path[PATH_MAX];
snprintf(link_path, sizeof(link_path), "%s/current", ctx->index_path);
unlink(link_path);
if (symlink(fname, link_path) != 0) { return -1; }
lchown(link_path, ctx->uid, (gid_t)-1);
return 0;
}
/* Archive the entry `name` of the directory open at `parent` (AT_FDCWD:
* a path) as `archive_path`, recursively. Everything is opened relative
* to its parent's descriptor with O_NOFOLLOW, and what is archived is
* what the descriptor turned out to be: root backing up a user's store
* (gstore -b) can't be steered into a symlink to /etc/shadow, or into
* /root, by swapping an entry between a check and an open. */
static int
add_entry_at(struct archive *a, int parent, const char *name, const char *archive_path)
{
struct stat st;
if (fstatat(parent, name, &st, AT_SYMLINK_NOFOLLOW) != 0) {
return -1;
}
/* Symlinks too: a package's soname links and its loader
* (musl's lib/ld-musl-*.so.1) are symlinks, and a backup that
* dropped them wouldn't restore a working package. */
if (S_ISLNK(st.st_mode)) {
char target[PATH_MAX];
ssize_t n = readlinkat(parent, name, target, sizeof(target) - 1);
if (n < 0) { return -1; }
target[n] = '\0';
struct archive_entry *entry = archive_entry_new();
archive_entry_set_pathname(entry, archive_path);
archive_entry_set_filetype(entry, AE_IFLNK);
archive_entry_set_perm(entry, 0777);
archive_entry_set_symlink(entry, target);
int rc = archive_write_header(a, entry) == ARCHIVE_OK ? 0 : -1;
archive_entry_free(entry);
return rc;
}
if (!S_ISDIR(st.st_mode) && !S_ISREG(st.st_mode)) {
return 0; /* devices, fifos, sockets: never in a package */
}
/* O_NONBLOCK: an entry swapped for a fifo must not hang us. */
int fd = openat(parent, name, O_RDONLY | O_NOFOLLOW | O_NONBLOCK | O_CLOEXEC |
(S_ISDIR(st.st_mode) ? O_DIRECTORY : 0));
if (fd < 0 || fstat(fd, &st) != 0) {
if (fd >= 0) { close(fd); }
return -1;
}
struct archive_entry *entry = archive_entry_new();
archive_entry_set_pathname(entry, archive_path);
archive_entry_set_perm(entry, st.st_mode & 0777);
if (S_ISDIR(st.st_mode)) {
archive_entry_set_filetype(entry, AE_IFDIR);
int rc = archive_write_header(a, entry) == ARCHIVE_OK ? 0 : -1;
archive_entry_free(entry);
DIR *dir = rc == 0 ? fdopendir(fd) : NULL;
if (!dir) {
close(fd);
return -1;
}
struct dirent *ent;
while (rc == 0 && (ent = readdir(dir))) {
if (strcmp(ent->d_name, ".") == 0 ||
strcmp(ent->d_name, "..") == 0) {
continue;
}
char child_arch[PATH_MAX];
snprintf(child_arch, sizeof(child_arch),
"%s/%s", archive_path, ent->d_name);
rc = add_entry_at(a, dirfd(dir), ent->d_name, child_arch);
}
closedir(dir);
return rc;
}
if (!S_ISREG(st.st_mode)) { /* swapped for something else */
archive_entry_free(entry);
close(fd);
return 0;
}
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_size(entry, st.st_size);
int rc = archive_write_header(a, entry) == ARCHIVE_OK ? 0 : -1;
archive_entry_free(entry);
char buf[8192];
ssize_t n = 0;
while (rc == 0 && (n = read(fd, buf, sizeof(buf))) > 0) {
if (archive_write_data(a, buf, (size_t)n) < 0) { rc = -1; }
}
close(fd);
return (rc == 0 && n == 0) ? 0 : -1;
}
static int
add_to_archive(struct archive *a, const char *real_path, const char *archive_path)
{
return add_entry_at(a, AT_FDCWD, real_path, archive_path);
}
/* Write `content` as a regular file entry. */
static int
add_buffer_to_archive(struct archive *a, const char *archive_path, const char *content)
{
size_t len = strlen(content);
struct archive_entry *entry = archive_entry_new();
archive_entry_set_pathname(entry, archive_path);
archive_entry_set_size(entry, (la_int64_t)len);
archive_entry_set_filetype(entry, AE_IFREG);
archive_entry_set_perm(entry, 0644);
int rc = (archive_write_header(a, entry) == ARCHIVE_OK &&
archive_write_data(a, content, len) == (la_ssize_t)len) ? 0 : -1;
archive_entry_free(entry);
return rc;
}
/* Find the one installed version dir of repo/name: its path and its
* directory name (NAME-VER). Returns 0 if found. */
static int
find_version_dir(const char *store, const char *repo, const char *name,
char *path_out, size_t path_sz, char *dirname_out, size_t dirname_sz)
{
char name_dir[PATH_MAX];
snprintf(name_dir, sizeof(name_dir), "%s/%s/%s", store, repo, name);
DIR *d = opendir(name_dir);
if (!d) { return -1; }
int rc = -1;
struct dirent *ent;
while ((ent = readdir(d))) {
if (ent->d_name[0] == '.') { continue; }
snprintf(path_out, path_sz, "%s/%s", name_dir, ent->d_name);
snprintf(dirname_out, dirname_sz, "%s", ent->d_name);
rc = 0;
break;
}
closedir(d);
return rc;
}
/*
* gl_set_pkg_auto
* DESCRIPTION: marks the installed repo/name as installed automatically
* (as a dependency) or explicitly, and rebuilds ctx's index so
* its 4th field says so. Nothing calls this with true until
* dependencies are resolved from remote repositories (ROADMAP
* 2.3 / 3.1); reinstalling a package by name drops the marker.
* RETURNS: 0 on success, -1 if the package isn't installed or the marker
* or index can't be written
*/
int
gl_set_pkg_auto(gl_context_t *ctx, const char *repo, const char *name, bool automatic)
{
if (!ctx || !gl_valid_pkg_ident(repo) || !gl_valid_pkg_ident(name)) { return -1; }
char vdir[PATH_MAX], dirname[NAME_MAX + 1];
if (find_version_dir(ctx->store_path, repo, name, vdir, sizeof(vdir),
dirname, sizeof(dirname)) != 0) {
return -1;
}
char marker[PATH_MAX];
snprintf(marker, sizeof(marker), "%s/" GL_AUTO_MARKER, vdir);
if (automatic) {
int fd = open(marker, O_WRONLY | O_CREAT | O_NOFOLLOW, 0600);
if (fd < 0) { return -1; }
close(fd);
} else if (unlink(marker) != 0 && errno != ENOENT) {
return -1;
}
return gl_rebuild_index(ctx) == 0 ? 0 : -1;
}
/*
* Archive the installed version of repo/name back into a .gpkg at
* out_path (same layout gl_install_pkg reads: NAME-VER/manifest.gpm.cfg
* + NAME-VER/files/...), so a backup is itself an installable package.
* The manifest is the one saved at install time (.glacier-manifest); a
* package installed before that existed gets a minimal one (name, version,
* repo). glacier's own dot-markers in the version dir are left out.
*/
int
gl_backup_pkg(gl_context_t *ctx, const char *repo, const char *name, const char *out_path)
{
if (!ctx || !gl_valid_pkg_ident(repo) || !gl_valid_pkg_ident(name)) { return -1; }
char ver_path[PATH_MAX], ver_name[NAME_MAX + 1];
if (find_version_dir(ctx->store_path, repo, name, ver_path, sizeof(ver_path),
ver_name, sizeof(ver_name)) != 0) {
return -1;
}
char parent[PATH_MAX];
snprintf(parent, sizeof(parent), "%s", out_path);
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
if (gl_mkdirp(parent, 0700) != 0) { return -1; }
}
struct archive *a = archive_write_new();
if (!a) { return -1; }
if (archive_write_add_filter_gzip(a) != ARCHIVE_OK ||
archive_write_set_format_pax_restricted(a) != ARCHIVE_OK ||
archive_write_open_filename(a, out_path) != ARCHIVE_OK) {
archive_write_free(a);
return -1;
}
int rc = 0;
char arch[PATH_MAX], real[PATH_MAX];
/* manifest */
snprintf(real, sizeof(real), "%s/" GL_MANIFEST_COPY, ver_path);
snprintf(arch, sizeof(arch), "%s/manifest.gpm.cfg", ver_name);
struct stat st;
if (lstat(real, &st) == 0) {
rc = add_to_archive(a, real, arch);
} else {
const char *ver = ver_name + strlen(name);
if (*ver == '-') { ver++; }
char manifest[1024];
snprintf(manifest, sizeof(manifest),
"// Reconstructed by glacier: the installed package predates\n"
"// saved manifests, so only these fields are known.\n"
"PKG_NAME = \"%s\";\nPKG_VER = \"%s\";\nPKG_REPO = \"%s\";\n"
"PKG_DESC = \"\";\nPKG_MAINT = \"\";\nPKG_COPYRIGHT = \"\";\n"
"PKG_DEPS = ();\nPKG_CONFS = ();\n",
name, ver, repo);
rc = add_buffer_to_archive(a, arch, manifest);
}
/* files/: every top-level entry except glacier's dot-markers */
DIR *d = rc == 0 ? opendir(ver_path) : NULL;
if (rc == 0 && !d) { rc = -1; }
struct dirent *ent;
while (rc == 0 && (ent = readdir(d))) {
if (ent->d_name[0] == '.') { continue; }
snprintf(real, sizeof(real), "%s/%s", ver_path, ent->d_name);
snprintf(arch, sizeof(arch), "%s/files/%s", ver_name, ent->d_name);
rc = add_to_archive(a, real, arch);
}
if (d) { closedir(d); }
if (archive_write_close(a) != ARCHIVE_OK) { rc = -1; }
archive_write_free(a);
if (rc != 0) { unlink(out_path); }
return rc;
}
/* Keep the `keep` newest *.gpkg in `dir` (by mtime), deleting the rest
* along with their .sha256 sidecars. Anything else in `dir` is left. */
int
gl_prune_backups(const char *dir, int keep)
{
if (keep < 0) { return -1; }
DIR *d = opendir(dir);
if (!d) { return errno == ENOENT ? 0 : -1; }
/* Nanosecond mtimes: two backups of one package can easily be
* written within the same second (upgrade, then restore). */
struct backup_ent { char name[NAME_MAX + 1]; struct timespec mtime; } found[256];
int n = 0;
struct dirent *ent;
while ((ent = readdir(d)) && n < 256) {
size_t len = strlen(ent->d_name);
if (len < 6 || strcmp(ent->d_name + len - 5, ".gpkg") != 0) { continue; }
char p[PATH_MAX];
struct stat st;
snprintf(p, sizeof(p), "%s/%s", dir, ent->d_name);
if (lstat(p, &st) != 0 || !S_ISREG(st.st_mode)) { continue; }
snprintf(found[n].name, sizeof(found[n].name), "%s", ent->d_name);
found[n].mtime = st.st_mtim;
n++;
}
closedir(d);
/* newest first (insertion sort; n is tiny) */
for (int i = 1; i < n; i++) {
for (int j = i; j > 0 &&
(found[j].mtime.tv_sec > found[j - 1].mtime.tv_sec ||
(found[j].mtime.tv_sec == found[j - 1].mtime.tv_sec &&
found[j].mtime.tv_nsec > found[j - 1].mtime.tv_nsec)); j--) {
struct backup_ent t = found[j];
found[j] = found[j - 1];
found[j - 1] = t;
}
}
for (int i = keep; i < n; i++) {
char p[PATH_MAX];
snprintf(p, sizeof(p), "%s/%s", dir, found[i].name);
unlink(p);
snprintf(p, sizeof(p), "%s/%s.sha256", dir, found[i].name);
unlink(p);
}
return 0;
}
gl_backup_status_t
gl_backup_istore(const char *tar_path,
const char *glacier_root,
const char *uid)
{
struct archive *a = archive_write_new();
if (!a) { return GL_BACKUP_ERR_ALLOC; }
if (archive_write_add_filter_gzip(a) != ARCHIVE_OK) {
archive_write_free(a);
return GL_BACKUP_ERR_OPEN;
}
if (archive_write_set_format_pax_restricted(a) != ARCHIVE_OK) {
archive_write_free(a);
return GL_BACKUP_ERR_OPEN;
}
if (archive_write_open_filename(a, tar_path) != ARCHIVE_OK) {
archive_write_free(a);
return GL_BACKUP_ERR_OPEN;
}
char index_path[1024];
char store_path[1024];
char links_path[1024];
char arch_index[512];
char arch_store[512];
char arch_links[512];
snprintf(index_path, sizeof(index_path), "%s/%s/index", glacier_root, uid);
snprintf(store_path, sizeof(store_path), "%s/%s/store", glacier_root, uid);
snprintf(links_path, sizeof(links_path), "%s/%s/links", glacier_root, uid);
snprintf(arch_index, sizeof(arch_index), "%s/index", uid);
snprintf(arch_store, sizeof(arch_store), "%s/store", uid);
snprintf(arch_links, sizeof(arch_links), "%s/links", uid);
if (add_to_archive(a, index_path, arch_index) != 0) {
archive_write_close(a);
archive_write_free(a);
return GL_BACKUP_ERR_ADD_INDEX;
}
if (add_to_archive(a, store_path, arch_store) != 0) {
archive_write_close(a);
archive_write_free(a);
return GL_BACKUP_ERR_ADD_STORE;
}
if (add_to_archive(a, links_path, arch_links) != 0) {
archive_write_close(a);
archive_write_free(a);
return GL_BACKUP_ERR_ADD_LINKS;
}
if (archive_write_close(a) != ARCHIVE_OK) {
archive_write_free(a);
return GL_BACKUP_ERR_CLOSE;
}
archive_write_free(a);
return GL_BACKUP_OK;
}
gl_remove_status_t
gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
{
if (!ctx) { return GL_REMOVE_ERR_NOT_FOUND; }
/* pkg_name comes from a command line here rather than a manifest,
* but it's spliced into the same store path and what follows
* deletes things — same rule as gl_gpm2gpkg. */
if (!gl_valid_pkg_ident(pkg_name) || !gl_valid_pkg_ident(pkg_repo)) {
return GL_REMOVE_ERR_NOT_FOUND;
}
/* find the package base directory in the store */
char pkg_store_base[PATH_MAX];
snprintf(pkg_store_base, sizeof(pkg_store_base),
"%s/%s/%s", ctx->store_path, pkg_repo, pkg_name);
struct stat st;
if (stat(pkg_store_base, &st) != 0 || !S_ISDIR(st.st_mode)) {
return GL_REMOVE_ERR_NOT_FOUND;
}
/* Live context: no commit will check this (see gl_install_pkg). */
if (ctx->mode == GL_CTX_LIVE) {
char id[2 * NAME_MAX + 3];
snprintf(id, sizeof(id), "%s::%s", pkg_repo, pkg_name);
const char *ids = id;
if (gl_check_plan(ctx, NULL, 0, &ids, 1) != 0) {
return GL_REMOVE_ERR_DEPS;
}
}
/* find the versioned subdirectory */
DIR *dir = opendir(pkg_store_base);
if (!dir) { return GL_REMOVE_ERR_NOT_FOUND; }
char pkg_store_final[PATH_MAX] = {0};
struct dirent *ent;
while ((ent = readdir(dir))) {
if (ent->d_name[0] == '.') { continue; }
snprintf(pkg_store_final, sizeof(pkg_store_final),
"%s/%s", pkg_store_base, ent->d_name);
break;
}
closedir(dir);
if (pkg_store_final[0] == '\0') {
return GL_REMOVE_ERR_NOT_FOUND;
}
/* remove package from store */
if (gl_rmdir_recursive(pkg_store_base) != 0) {
return GL_REMOVE_ERR_UNLINK;
}
/* rebuild index */
if (gl_rebuild_index(ctx) != 0) {
return GL_REMOVE_ERR_INDEX;
}
/* Live context: drop the package's links the way a commit does,
* by pruning glacier's now-stale links (which also restores what
* -O displaced, and handles /etc). This used to unlink() each of
* the package's destinations unchecked, deleting foreign files
* the package had been refused to replace (CLAUDE.md bug 26).
* GL_CTX_STAGE: nothing to do, links are derived after commit. */
if (ctx->mode == GL_CTX_LIVE && gl_relink_store(ctx) != 0) {
return GL_REMOVE_ERR_UNLINK;
}
return GL_REMOVE_OK;
}
/*
* Displaced originals.
*
* With ctx->overwrite, gl_link_pkg may replace a non-glacier file at a
* link destination (e.g. a distro-owned /usr/include/stdio.h). The
* original is never deleted: it moves to <store>/.displaced/<abs path>,
* and prune_stale_links moves it back once the glacier symlink that
* replaced it goes stale (i.e. its package was removed). Keeping it in
* the store rather than a separate tree means it's carried through
* transactions by the same seeding as everything else, and user scope
* can write it without needing anything under /glacier/usr itself.
*
* Which packages may displace is decided per install, not per store:
* gl_install_pkg drops GL_OVERWRITE_MARKER into the package's store dir
* and gl_link_pkg consumes it on the next link pass. Only the packages
* named in that command get the right, and only once — a later relink
* of the same store (any other transaction's commit) never displaces
* anything on its own.
*/
#define GL_DISPLACED_DIR ".displaced"
/* GL_FRESH_MARKER (common.h) is the same one-shot mechanism, for every
* install: marks a package as new in this transaction, so its link
* conflicts are reported once, when it's installed — not again on every
* later commit's full relink. */
static int
displaced_path(char *buf, size_t bufsz, const char *store_path, const char *path)
{
int n = snprintf(buf, bufsz, "%s/" GL_DISPLACED_DIR "%s", store_path, path);
return (n < 0 || (size_t)n >= bufsz) ? -1 : 0;
}
/* Copy fallback for move_path when src and dst are on different
* filesystems (/usr vs /glacier is a normal layout). Regular files and
* symlinks only; owner, mode and timestamps carried over. */
static int
copy_entry(const char *src, const char *dst, const struct stat *st)
{
if (S_ISLNK(st->st_mode)) {
char target[PATH_MAX];
ssize_t n = readlink(src, target, sizeof(target) - 1);
if (n < 0) { return -1; }
target[n] = '\0';
if (symlink(target, dst) != 0) { return -1; }
if (lchown(dst, st->st_uid, st->st_gid) != 0) {
/* non-root (user scope): the copy is ours, fine */
}
return 0;
}
int in = open(src, O_RDONLY);
if (in < 0) { return -1; }
int out = open(dst, O_WRONLY | O_CREAT | O_EXCL, 0600);
if (out < 0) { close(in); return -1; }
int rc = 0;
char buf[65536];
ssize_t n;
while (rc == 0 && (n = read(in, buf, sizeof(buf))) != 0) {
if (n < 0) { rc = -1; break; }
char *p = buf;
while (n > 0) {
ssize_t w = write(out, p, (size_t)n);
if (w < 0) { rc = -1; break; }
p += w;
n -= w;
}
}
/* chown before chmod: chown clears setuid/setgid bits */
if (rc == 0 && fchown(out, st->st_uid, st->st_gid) != 0) {
/* non-root (user scope): the copy is ours, fine */
}
if (rc == 0 && fchmod(out, st->st_mode & 07777) != 0) { rc = -1; }
if (rc == 0) {
struct timespec ts[2] = { st->st_atim, st->st_mtim };
futimens(out, ts);
}
if (rc == 0 && fsync(out) != 0) { rc = -1; }
close(in);
if (close(out) != 0) { rc = -1; }
if (rc != 0) { unlink(dst); }
return rc;
}
/* rename(), or copy+unlink across filesystems. dst must not exist. */
static int
move_path(const char *src, const char *dst)
{
if (rename(src, dst) == 0) { return 0; }
if (errno != EXDEV) { return -1; }
struct stat st;
if (lstat(src, &st) != 0) { return -1; }
if (!S_ISREG(st.st_mode) && !S_ISLNK(st.st_mode)) {
errno = EINVAL;
return -1;
}
if (copy_entry(src, dst, &st) != 0) { return -1; }
if (unlink(src) != 0) {
int e = errno;
unlink(dst);
errno = e;
return -1;
}
return 0;
}
/* Move the non-glacier entry at `path` into the store's displaced tree
* so a glacier symlink can take its place. Refuses (returns -1, path
* untouched) for anything but a regular file or symlink, and if a
* displaced original for this path is already being kept — never
* overwrite the one copy of something we promised to put back. */
/* `path` is an install path; its backup is keyed by its RUNTIME path, so
* an image's .displaced means the same thing once the image is booted. */
static int
displace_path(const gl_context_t *ctx, const char *path, const struct stat *st)
{
if (!S_ISREG(st->st_mode) && !S_ISLNK(st->st_mode)) {
lg_printf(1, "Refusing to overwrite %s: not a regular file or symlink", path);
return -1;
}
char backup[PATH_MAX];
if (displaced_path(backup, sizeof(backup), ctx->store_path,
gl_runtime_path(ctx, path)) != 0) {
lg_printf(1, "Refusing to overwrite %s: path too long", path);
return -1;
}
struct stat bst;
if (lstat(backup, &bst) == 0) {
lg_printf(1, "Refusing to overwrite %s: an earlier original is already kept at %s",
path, backup);
return -1;
}
char parent[PATH_MAX];
snprintf(parent, sizeof(parent), "%s", backup);
char *slash = strrchr(parent, '/');
if (slash) { *slash = '\0'; }
if (gl_mkdirp(parent, 0700) != 0) {
lg_printf(1, "Refusing to overwrite %s: cannot create %s", path, parent);
return -1;
}
if (move_path(path, backup) != 0) {
lg_printf(1, "Refusing to overwrite %s: cannot move it aside: %s",
path, strerror(errno));
return -1;
}
return 0;
}
/* Put a displaced original back at `path` (which must not exist), then
* drop now-empty parents of its backup up to the displaced root.
* Returns 1 if restored, 0 if there was nothing to restore, -1 on
* failure (backup left where it is). */
static int
restore_displaced(const gl_context_t *ctx, const char *path)
{
const char *store_path = ctx->store_path;
char backup[PATH_MAX];
if (displaced_path(backup, sizeof(backup), store_path,
gl_runtime_path(ctx, path)) != 0) { return 0; }
struct stat bst;
if (lstat(backup, &bst) != 0) { return 0; }
if (move_path(backup, path) != 0) {
lg_printf(1, "Cannot restore %s from %s: %s", path, backup, strerror(errno));
return -1;
}
char root[PATH_MAX];
snprintf(root, sizeof(root), "%s/" GL_DISPLACED_DIR, store_path);
char dir[PATH_MAX];
snprintf(dir, sizeof(dir), "%s", backup);
char *slash;
while ((slash = strrchr(dir, '/')) != NULL) {
*slash = '\0';
if (strcmp(dir, root) == 0 || rmdir(dir) != 0) { break; }
}
return 1;
}
/*
* Configuration files (decision GLACIER_BOOTSTRAP.txt 9c: option 1,
* copy on install, .new on conflict).
*
* A system-scope package's top-level etc/ is COPIED into /etc rather
* than linked: config is the one thing the admin is meant to edit, and
* editing through a link would edit the store itself (breaking its
* hashes, and leaking into the hardlink-seeded copies of it).
*
* For each file glacier keeps a record of the last version the package
* offered for it, at <store>/.etc-state/<path>. That lives in the store
* like .displaced, so it's carried through transactions by seeding.
* Rules, applied on every link pass (so they must be idempotent), with
* S = the package's file, T = /etc/<path>, R = the record:
*
* T missing, no R -> copy S to T, R = S
* T missing, R exists -> the admin deleted it: left deleted
* T == R (admin left it alone) -> if S != R: replace T with S, R = S
* T != R, or T exists without R -> admin-modified or not glacier's:
* NEVER touched. If S != R, offer S as
* T.new; then R = S, so the same offer
* isn't re-made every commit.
* no package ships it any more -> T removed if T == R, otherwise kept
* (prune_config)
*/
#define GL_SYS_ETC "/etc"
#define GL_ETC_STATE_DIR ".etc-state"
/* Same type and same bytes (regular files) or same target (symlinks).
* Missing on either side counts as different. */
static bool
config_equal(const char *a, const char *b)
{
struct stat sa, sb;
if (lstat(a, &sa) != 0 || lstat(b, &sb) != 0) { return false; }
if (S_ISLNK(sa.st_mode) && S_ISLNK(sb.st_mode)) {
char ta[PATH_MAX], tb[PATH_MAX];
ssize_t na = readlink(a, ta, sizeof(ta) - 1);
ssize_t nb = readlink(b, tb, sizeof(tb) - 1);
return na >= 0 && na == nb && memcmp(ta, tb, (size_t)na) == 0;
}
if (!S_ISREG(sa.st_mode) || !S_ISREG(sb.st_mode) || sa.st_size != sb.st_size) {
return false;
}
FILE *fa = fopen(a, "rb");
FILE *fb = fopen(b, "rb");
bool same = fa && fb;
char ba[8192], bb[8192];
while (same) {
size_t na = fread(ba, 1, sizeof(ba), fa);
size_t nb = fread(bb, 1, sizeof(bb), fb);
if (na != nb || memcmp(ba, bb, na) != 0) { same = false; }
if (na == 0) { break; }
}
if (fa) { fclose(fa); }
if (fb) { fclose(fb); }
return same;
}
/* Copy `src` (file or symlink) to `dst`, replacing it atomically via a
* temporary + rename(). Parent directories are created. */
static int
config_put(const char *src, const char *dst, mode_t parent_mode)
{
char parent[PATH_MAX];
snprintf(parent, sizeof(parent), "%s", dst);
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
if (gl_mkdirp(parent, parent_mode) != 0) { return -1; }
}
struct stat st;
if (lstat(src, &st) != 0) { return -1; }
char tmp[PATH_MAX];
if (snprintf(tmp, sizeof(tmp), "%s.glacier-tmp", dst) >= (int)sizeof(tmp)) { return -1; }
unlink(tmp);
if (copy_entry(src, tmp, &st) != 0) { return -1; }
if (rename(tmp, dst) != 0) {
unlink(tmp);
return -1;
}
return 0;
}
static int
install_config(gl_context_t *ctx, const char *src, const char *rel)
{
char target[PATH_MAX], record[PATH_MAX], offer[PATH_MAX];
snprintf(target, sizeof(target), "%s" GL_SYS_ETC "/%s", ctx->sysroot, rel);
snprintf(record, sizeof(record), "%s/" GL_ETC_STATE_DIR "/%s", ctx->store_path, rel);
snprintf(offer, sizeof(offer), "%s.new", target);
struct stat st;
bool have_target = lstat(target, &st) == 0;
bool have_record = lstat(record, &st) == 0;
if (!have_target && have_record) {
/* Installed before, gone now: the admin deleted it. That's a
* decision like an edit, so respect it — otherwise every later
* commit (of ANY package, since every commit relinks) would
* quietly bring it back. */
} else if (!have_target) {
if (config_put(src, target, 0755) != 0) {
lg_printf(2, "Cannot install %s: %s", target, strerror(errno));
return -1;
}
} else if (have_record && config_equal(target, record)) {
if (config_equal(src, record)) { return 0; } /* nothing new */
if (config_put(src, target, 0755) != 0) {
lg_printf(2, "Cannot update %s: %s", target, strerror(errno));
return -1;
}
} else {
if (have_record && config_equal(src, record)) { return 0; } /* already offered */
if (!config_equal(src, target) && !config_equal(src, offer)) {
if (config_put(src, offer, 0755) != 0) {
lg_printf(2, "Cannot write %s: %s", offer, strerror(errno));
return -1;
}
lg_printf(1, have_record
? "Kept modified %s; the package's new version is %s"
: "Kept existing %s (not installed by glacier); the package's version is %s",
target, offer);
}
}
if (config_put(src, record, 0700) != 0) {
lg_printf(2, "Cannot record %s: %s", record, strerror(errno));
return -1;
}
return 0;
}
/* Does any package in the store still ship etc/<rel>? */
static bool
store_provides_config(const char *store, const char *rel)
{
DIR *repos = opendir(store);
if (!repos) { return false; }
bool found = false;
struct dirent *r;
while (!found && (r = readdir(repos))) {
if (r->d_name[0] == '.') { continue; }
char rp[PATH_MAX];
snprintf(rp, sizeof(rp), "%s/%s", store, r->d_name);
DIR *names = opendir(rp);
if (!names) { continue; }
struct dirent *n;
while (!found && (n = readdir(names))) {
if (n->d_name[0] == '.') { continue; }
char np[PATH_MAX];
snprintf(np, sizeof(np), "%s/%s", rp, n->d_name);
DIR *vers = opendir(np);
if (!vers) { continue; }
struct dirent *v;
while (!found && (v = readdir(vers))) {
if (v->d_name[0] == '.') { continue; }
char f[PATH_MAX];
struct stat st;
snprintf(f, sizeof(f), "%s/%s/etc/%s", np, v->d_name, rel);
found = lstat(f, &st) == 0 && !S_ISDIR(st.st_mode);
}
closedir(vers);
}
closedir(names);
}
closedir(repos);
return found;
}
/* Walk the records; for any file no package ships any more, remove it
* from /etc if the admin never changed it, keep it if they did. */
static void
prune_config_dir(const char *store, const char *etc, const char *state_root,
const char *sub)
{
char dir[PATH_MAX];
snprintf(dir, sizeof(dir), "%s%s%s", state_root, sub[0] ? "/" : "", sub);
DIR *d = opendir(dir);
if (!d) { return; }
struct dirent *ent;
while ((ent = readdir(d))) {
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) { continue; }
char rel[PATH_MAX], record[PATH_MAX];
snprintf(rel, sizeof(rel), "%s%s%s", sub, sub[0] ? "/" : "", ent->d_name);
snprintf(record, sizeof(record), "%s/%s", state_root, rel);
struct stat st;
if (lstat(record, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
prune_config_dir(store, etc, state_root, rel);
rmdir(record); /* only succeeds once empty */
continue;
}
if (store_provides_config(store, rel)) { continue; }
char target[PATH_MAX];
snprintf(target, sizeof(target), "%s/%s", etc, rel);
if (config_equal(target, record)) {
unlink(target);
} else if (lstat(target, &st) == 0) {
lg_printf(1, "Kept %s: it differs from what glacier installed, and no package ships it any more", target);
}
unlink(record);
}
closedir(d);
}
static void
prune_config(gl_context_t *ctx)
{
char state_root[PATH_MAX];
snprintf(state_root, sizeof(state_root), "%s/" GL_ETC_STATE_DIR, ctx->store_path);
char etc[PATH_MAX];
snprintf(etc, sizeof(etc), "%s" GL_SYS_ETC, ctx->sysroot);
prune_config_dir(ctx->store_path, etc, state_root, "");
}
static int
gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo)
{
/* Install paths for everything touched on disk; the RUNTIME store
* path for what a link says and for recognising glacier's links
* (see gl_context_t.sysroot). */
const char *store_rt = gl_runtime_path(ctx, ctx->store_path);
char links_base[PATH_MAX];
if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) {
snprintf(links_base, sizeof(links_base), "%s%s", ctx->sysroot,
GL_BASE_LINKS_DEST);
} else {
snprintf(links_base, sizeof(links_base), "%s", ctx->links_path);
}
size_t strip_len = strlen(pkg_store_final);
/* Consumed up front, whether or not linking then succeeds: the
* right to displace is for this one link pass only (see
* GL_OVERWRITE_MARKER), and must never leak into a later commit. */
char marker[PATH_MAX];
snprintf(marker, sizeof(marker), "%s/" GL_OVERWRITE_MARKER, pkg_store_final);
bool overwrite = unlink(marker) == 0;
snprintf(marker, sizeof(marker), "%s/" GL_FRESH_MARKER, pkg_store_final);
bool fresh = unlink(marker) == 0;
int displaced = 0;
/* use a stack-based queue to walk the tree without recursion */
char *stack[4096];
int top = 0;
stack[top++] = strdup(pkg_store_final);
while (top > 0) {
char *current = stack[--top];
DIR *dir = opendir(current);
if (!dir) {
free(current);
continue;
}
struct dirent *ent;
while ((ent = readdir(dir))) {
if (ent->d_name[0] == '.') { continue; }
char full_path[PATH_MAX];
snprintf(full_path, sizeof(full_path), "%s/%s",
current, ent->d_name);
struct stat st;
if (lstat(full_path, &st) != 0) { continue; }
/* relative path inside the package, e.g. usr/bin/kilo */
const char *rel = full_path + strip_len;
if (rel[0] == '/') { rel++; }
/* System scope: the package's top-level etc/ is config,
* copied into /etc (see install_config), never linked. */
if (ctx->scope == GL_SCOPE_SYS &&
(strcmp(rel, "etc") == 0 || strncmp(rel, "etc/", 4) == 0)) {
if (S_ISDIR(st.st_mode)) {
if (top < 4096) { stack[top++] = strdup(full_path); }
} else if ((S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) &&
install_config(ctx, full_path, rel + 4) != 0) {
closedir(dir);
free(current);
while (top > 0) { free(stack[--top]); }
return -1;
}
continue;
}
/* strip leading usr/ so bin/kilo lands at links/1000/bin/kilo */
if (strncmp(rel, "usr/", 4) == 0) { rel += 4; }
char link_path[PATH_MAX];
snprintf(link_path, sizeof(link_path), "%s/%s",
links_base, rel);
if (S_ISDIR(st.st_mode)) {
/* Never descend through another package's
* symlink: if pkg A links usr/lib64 -> lib
* and this package has usr/lib64/..., the
* mkdirp and symlinks below would land
* INSIDE A's store directory. */
struct stat lst;
if (lstat(link_path, &lst) == 0 && S_ISLNK(lst.st_mode) &&
is_glacier_symlink(link_path, store_rt)) {
/* Reported (and counted) once, by
* the transaction that installs
* this package, like the refusal
* below (bug 21). */
if (fresh) {
lg_printf(1,
"Refusing to merge directory %s into another package's symlink",
link_path);
ctx->link_refusals++;
}
continue;
}
/* create the directory in the links tree */
gl_mkdirp(link_path, gl_scope_dir_mode(ctx->scope));
/* push onto stack to recurse into */
if (top < 4096) {
stack[top++] = strdup(full_path);
}
} else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
/* Regular files and the package's own
* symlinks are linked alike: the link
* points at the store entry, which for a
* symlink then resolves its own (runtime)
* target. Pointing at the store entry,
* not copying its target, keeps
* is_glacier_symlink true for it. */
/* create parent dirs in links tree */
char parent[PATH_MAX];
strncpy(parent, link_path, sizeof(parent));
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
gl_mkdirp(parent, gl_scope_dir_mode(ctx->scope));
}
/* Only ever replace an existing entry if it's a
* symlink we created ourselves — or, for a
* package installed with overwrite, after
* moving the original aside to be restored
* on removal. See is_glacier_symlink's
* comment for why this check exists at all. */
bool may_link = true;
bool was_displaced = false;
struct stat existing_st;
if (lstat(link_path, &existing_st) == 0 &&
!is_glacier_symlink(link_path, store_rt)) {
if (!overwrite) {
if (fresh) {
lg_printf(1,
"Refusing to overwrite non-glacier path: %s",
link_path);
ctx->link_refusals++;
}
may_link = false;
} else if (displace_path(ctx, link_path,
&existing_st) != 0) {
/* only with the -O marker,
* i.e. a fresh package */
ctx->link_refusals++;
may_link = false;
} else {
was_displaced = true;
displaced++;
}
}
if (may_link) {
unlink(link_path);
/* symlink store path -> links tree; the
* target is the runtime path */
if (symlink(gl_runtime_path(ctx, full_path), link_path) != 0) {
if (was_displaced) {
restore_displaced(ctx, link_path);
}
closedir(dir);
free(current);
while (top > 0) { free(stack[--top]); }
return -1;
}
lchown(link_path, ctx->uid, (gid_t)-1);
}
}
}
closedir(dir);
free(current);
}
if (displaced > 0) {
const char *pkg_label = strrchr(pkg_store_final, '/');
lg_printf(0, "%s: displaced %d existing path%s to %s"
GL_DISPLACED_DIR,
pkg_label ? pkg_label + 1 : pkg_store_final,
displaced, displaced == 1 ? "" : "s", ctx->store_path);
}
return 0;
}
int
gl_delete_user(uid_t uid, bool isVerbose)
{
char base[PATH_MAX];
gl_usr_base(uid, base, sizeof(base));
/* lstat, not stat: gl_rmdir_recursive opendir()s its argument,
* so a symlink planted at /glacier/usr/<uid> would have it empty
* the link's TARGET. Ownership is deliberately not required —
* removing a squatted base owned by the wrong uid is exactly what
* root needs this for. */
struct stat st;
if (lstat(base, &st) != 0) {
lg_printf(2, "No Glacier directories for UID %u", uid);
return 1;
}
if (!S_ISDIR(st.st_mode)) {
lg_printf(2, "Refusing to remove %s: not a directory", base);
return 1;
}
if (isVerbose) { lg_printf(0, "Removing Glacier directories for UID %u...", uid); }
if (gl_rmdir_recursive(base) != 0) {
lg_printf(2, "Failed to remove %s: %s", base, strerror(errno));
return 1;
}
lg_printf(3, "Successfully removed Glacier directories for UID %u.", uid);
return 0;
}
gl_repo_list_t
gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name)
{
gl_repo_list_t result = { NULL, 0 };
if (!ctx || !gl_valid_pkg_ident(pkg_name)) { return result; }
char store_path[PATH_MAX];
snprintf(store_path, sizeof(store_path), "%s", ctx->store_path);
DIR *uid_dir = opendir(store_path);
if (!uid_dir) { return result; }
struct dirent *repo_entry;
while ((repo_entry = readdir(uid_dir)) != NULL) {
if (repo_entry->d_name[0] == '.') { continue; }
char pkg_path[PATH_MAX];
snprintf(pkg_path, sizeof(pkg_path),
"%s/%s/%s", store_path, repo_entry->d_name, pkg_name);
struct stat st;
if (stat(pkg_path, &st) == 0 && S_ISDIR(st.st_mode)) {
char **tmp = realloc(result.repos,
(result.count + 1) * sizeof(char *));
if (!tmp) { break; }
result.repos = tmp;
result.repos[result.count] = strdup(repo_entry->d_name);
result.count++;
}
}
closedir(uid_dir);
return result;
}
void
gl_free_repo_list(gl_repo_list_t *list)
{
if (!list) { return; }
for (size_t i = 0; i < list->count; i++) { free(list->repos[i]); }
free(list->repos);
list->repos = NULL;
list->count = 0;
}
/*
* A tar entry path (after the "<name>-<ver>/files/" prefix is stripped) is
* safe to join onto the package's store directory only if it is relative
* and has no ".." component. Anything else is tar-slip: the entry name is
* fully controlled by whoever built the archive.
*/
static bool
is_safe_rel_path(const char *rel)
{
if (rel[0] == '/') { return false; }
const char *p = rel;
while (*p) {
const char *end = strchr(p, '/');
size_t len = end ? (size_t)(end - p) : strlen(p);
if (len == 2 && p[0] == '.' && p[1] == '.') { return false; }
if (!end) { break; }
p = end + 1;
}
return true;
}
/* True if any directory component of `path` strictly below `base` is a
* symlink (the final component is not checked — callers use
* O_NOFOLLOW / symlink() / mkdir(), none of which follow it). Used so
* package extraction can never write through a symlink. */
static bool
crosses_symlink(const char *base, const char *path)
{
size_t blen = strlen(base);
if (strncmp(path, base, blen) != 0 || path[blen] != '/') { return true; }
char buf[PATH_MAX];
snprintf(buf, sizeof(buf), "%s", path);
for (char *p = buf + blen + 1; *p; p++) {
if (*p != '/') { continue; }
*p = '\0';
struct stat st;
if (lstat(buf, &st) == 0 && S_ISLNK(st.st_mode)) { return true; }
*p = '/';
}
return false;
}
/*
* gl_read_pkg_manifest
* DESCRIPTION: parses the manifest.gpm.cfg inside a .gpkg into *pkg
* (which must be zeroed; free it with gl_free_pkg on success).
* RETURNS: GL_INSTALL_OK, GL_INSTALL_ERR_OPEN (archive unreadable) or
* GL_INSTALL_ERR_MANIFEST (missing, unparseable, or unsafe name/repo)
*/
int
gl_read_pkg_manifest(const char *gpkg_path, gl_pkg_t *pkg)
{
struct archive *a = archive_read_new();
archive_read_support_filter_gzip(a);
archive_read_support_format_tar(a);
if (archive_read_open_filename(a, gpkg_path, 10240) != ARCHIVE_OK) {
archive_read_free(a);
return GL_INSTALL_ERR_OPEN;
}
struct archive_entry *entry;
bool manifest_found = false;
while (archive_read_next_header(a, &entry) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
if (strstr(path, "manifest.gpm.cfg")) {
char tmp_manifest[PATH_MAX];
strncpy(tmp_manifest, "/tmp/gpkg_manifest_XXXXXX",
sizeof(tmp_manifest));
int fd = mkstemp(tmp_manifest);
if (fd < 0) {
archive_read_free(a);
return GL_INSTALL_ERR_MANIFEST;
}
char buffer[8192];
ssize_t n;
while ((n = archive_read_data(a, buffer, sizeof(buffer))) > 0) {
ssize_t written = write(fd, buffer, (size_t)n);
if (written < 0 || written != n) {
close(fd);
unlink(tmp_manifest);
archive_read_free(a);
return GL_INSTALL_ERR_MANIFEST;
}
}
close(fd);
int rc = gl_gpm2gpkg(tmp_manifest, pkg);
if (rc != 0) {
if (rc == EINVAL) {
lg_printf(2,
"Manifest has a missing or unsafe name ([A-Za-z0-9._-] only).");
}
unlink(tmp_manifest);
archive_read_free(a);
return GL_INSTALL_ERR_MANIFEST;
}
unlink(tmp_manifest);
manifest_found = true;
break;
}
archive_read_data_skip(a);
}
archive_read_free(a);
if (!manifest_found) {
return GL_INSTALL_ERR_MANIFEST;
}
return GL_INSTALL_OK;
}
gl_install_status_t
gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
{
if (!ctx) { return GL_INSTALL_ERR_OPEN; }
gl_pkg_t pkg = {0};
int mrc = gl_read_pkg_manifest(gpkg_path, &pkg);
if (mrc != GL_INSTALL_OK) {
return (gl_install_status_t)mrc;
}
/*
* Base-repo packages can NEVER be installed under per-user scope.
* Enforced here, at the library level, rather than in any CLI: the
* split-.so architecture means a third-party tool can call
* gl_install_pkg directly without going through gpkg/syspkg, and a
* rule only one CLI respects is not a real guarantee. See
* SECURITY_DESIGN.txt section 8.
*
* This is the earliest possible point — pkg.pkg_repo only exists
* once the manifest is parsed — and deliberately sits before any
* store path construction or filesystem mutation below.
*/
if (ctx->scope == GL_SCOPE_USR &&
strcmp(pkg.pkg_repo, GL_BASE_REPO) == 0) {
lg_printf(2,
"Refusing to install base-repo package '%s' under user scope.",
pkg.pkg_name);
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_SCOPE;
}
/* A live context has no commit to check it (gl_check_txn), so the
* dependency rules are applied here instead. Staged installs are
* checked as a whole at commit: a batch may satisfy itself. */
if (ctx->mode == GL_CTX_LIVE) {
const char *self = gpkg_path;
if (gl_check_plan(ctx, &self, 1, NULL, 0) != 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_DEPS;
}
}
char pkg_store_base[PATH_MAX];
char pkg_store_final[PATH_MAX];
/* construct the directory for the package in the store */
snprintf(pkg_store_base, sizeof(pkg_store_base),
"%s/%s/%s", ctx->store_path, pkg.pkg_repo, pkg.pkg_name);
snprintf(pkg_store_final, sizeof(pkg_store_final),
"%s/%s-%d.%d.%d", pkg_store_base, pkg.pkg_name,
pkg.pkg_ver.maj, pkg.pkg_ver.min, pkg.pkg_ver.pat);
/* store/<repo>, store/<repo>/<name> and the version dir get the
* scope's mode explicitly, whatever the umask: in system scope
* every user must be able to reach what /usr links to. */
mode_t dir_mode = gl_scope_dir_mode(ctx->scope);
char pkg_repo_dir[PATH_MAX];
snprintf(pkg_repo_dir, sizeof(pkg_repo_dir), "%s/%s", ctx->store_path, pkg.pkg_repo);
if (gl_mkdirp(pkg_store_base, dir_mode) == -1 ||
chmod(pkg_repo_dir, dir_mode) != 0 || chmod(pkg_store_base, dir_mode) != 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
if (mkdir(pkg_store_final, dir_mode) == -1) {
if (errno == EEXIST) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_ALREADY_INSTALLED;
}
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
if (chmod(pkg_store_final, dir_mode) != 0) {
gl_rmdir_recursive(pkg_store_final);
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
/* now the archive must be reopened in order to extract package files */
struct archive_entry *entry;
struct archive *a2 = archive_read_new();
archive_read_support_filter_gzip(a2);
archive_read_support_format_tar(a2);
if (archive_read_open_filename(a2, gpkg_path, 10240) != ARCHIVE_OK) {
archive_read_free(a2);
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
char strip_prefix[PATH_MAX];
snprintf(strip_prefix, sizeof(strip_prefix),
"%s-%d.%d.%d/files/", pkg.pkg_name,
pkg.pkg_ver.maj, pkg.pkg_ver.min, pkg.pkg_ver.pat);
/* Symlink entries are collected here and only created once every
* directory and regular file is in place. While regular content is
* being written the package dir therefore contains no symlinks at
* all, so no entry can be written *through* one — the classic
* symlink tar-slip, where `x -> /etc` followed by `x/passwd` writes
* the real /etc/passwd, as root under syspkg. crosses_symlink() and
* O_NOFOLLOW below are the second line of defense, and also cover
* a deferred symlink whose parent is another deferred symlink. */
struct deferred_link { char *dest; char *target; } *links = NULL;
size_t nlinks = 0;
gl_install_status_t ret = GL_INSTALL_ERR_EXTRACT;
char manifest_entry[PATH_MAX];
snprintf(manifest_entry, sizeof(manifest_entry), "%s-%d.%d.%d/manifest.gpm.cfg",
pkg.pkg_name, pkg.pkg_ver.maj, pkg.pkg_ver.min, pkg.pkg_ver.pat);
while (archive_read_next_header(a2, &entry) == ARCHIVE_OK) {
const char *entry_path = archive_entry_pathname(entry);
/* Keep the manifest beside the files (GL_MANIFEST_COPY). */
if (strcmp(entry_path, manifest_entry) == 0 &&
archive_entry_filetype(entry) == AE_IFREG) {
char mpath[PATH_MAX];
snprintf(mpath, sizeof(mpath), "%s/" GL_MANIFEST_COPY, pkg_store_final);
int mfd = open(mpath, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0644);
if (mfd < 0) { goto extract_fail; }
char buffer[8192];
ssize_t n;
while ((n = archive_read_data(a2, buffer, sizeof(buffer))) > 0) {
if (write(mfd, buffer, (size_t)n) != n) {
close(mfd);
goto extract_fail;
}
}
close(mfd);
continue;
}
if (strncmp(entry_path, strip_prefix, strlen(strip_prefix)) != 0) {
archive_read_data_skip(a2);
continue;
}
const char *rel_path = entry_path + strlen(strip_prefix);
if (rel_path[0] == '\0') {
archive_read_data_skip(a2);
continue;
}
/* Refuse the whole package rather than skip the entry: an
* archive carrying a traversal path is hostile or broken, and
* installing the rest of it would be a partial install of
* something we've already stopped trusting. */
if (!is_safe_rel_path(rel_path)) {
lg_printf(2, "Refusing unsafe archive entry path '%s'.",
entry_path);
goto extract_fail;
}
/* Top-level .glacier-* names are glacier's own files in the
* version dir (GL_MARKER_PREFIX). A package shipping one could
* grant itself -O, rewrite its saved manifest, or, as a
* symlink, redirect glacier's marker write (as root, under
* syspkg) to any file on the system. */
if (strncmp(rel_path, GL_MARKER_PREFIX, strlen(GL_MARKER_PREFIX)) == 0) {
lg_printf(2, "Refusing archive entry '%s': %s* names are reserved.",
entry_path, GL_MARKER_PREFIX);
goto extract_fail;
}
char dest_path[PATH_MAX];
snprintf(dest_path, sizeof(dest_path), "%s/%s",
pkg_store_final, rel_path);
if (crosses_symlink(pkg_store_final, dest_path)) {
lg_printf(2, "Refusing archive entry '%s': a parent directory is a symlink.",
entry_path);
goto extract_fail;
}
switch (archive_entry_filetype(entry)) {
case AE_IFDIR:
if (gl_mkdir_exact(dest_path, GL_ENTRY_MODE(entry)) != 0) {
goto extract_fail;
}
break;
case AE_IFREG: {
char parent[PATH_MAX];
snprintf(parent, sizeof(parent), "%s", dest_path);
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
gl_mkdirp(parent, dir_mode);
}
int fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW,
GL_ENTRY_MODE(entry));
if (fd < 0) { goto extract_fail; }
if (fchmod(fd, GL_ENTRY_MODE(entry)) != 0) {
close(fd);
goto extract_fail;
}
char buffer[8192];
ssize_t n;
while ((n = archive_read_data(a2, buffer, sizeof(buffer))) > 0) {
ssize_t written = write(fd, buffer, (size_t)n);
if (written < 0 || written != n) {
close(fd);
goto extract_fail;
}
}
close(fd);
break;
}
case AE_IFLNK: {
/* The target is stored verbatim and means what it says
* at RUNTIME: musl's lib/ld-musl-x86_64.so.1 ->
* /usr/lib/libc.so resolves against the running system,
* not the store. Nothing glacier does afterwards writes
* through a store symlink (removal and seeding lstat()). */
const char *target = archive_entry_symlink(entry);
if (!target || target[0] == '\0') {
lg_printf(2, "Refusing archive entry '%s': symlink with no target.",
entry_path);
goto extract_fail;
}
struct deferred_link *grown = realloc(links,
(nlinks + 1) * sizeof(*links));
if (!grown) { goto extract_fail; }
links = grown;
links[nlinks].dest = strdup(dest_path);
links[nlinks].target = strdup(target);
nlinks++;
if (!links[nlinks - 1].dest || !links[nlinks - 1].target) {
goto extract_fail;
}
break;
}
default:
/* Hardlinks, devices, fifos: nothing a package needs, and
* nothing worth trusting an archive to create. Skipped,
* same as before symlinks were supported. */
archive_read_data_skip(a2);
break;
}
}
for (size_t i = 0; i < nlinks; i++) {
if (crosses_symlink(pkg_store_final, links[i].dest)) {
lg_printf(2, "Refusing symlink '%s': a parent directory is a symlink.",
links[i].dest);
goto extract_fail;
}
char parent[PATH_MAX];
snprintf(parent, sizeof(parent), "%s", links[i].dest);
char *slash = strrchr(parent, '/');
if (slash) {
*slash = '\0';
gl_mkdirp(parent, dir_mode);
}
/* symlink() fails EEXIST rather than replacing: an archive that
* ships both a file/dir and a symlink at one path is refused. */
if (symlink(links[i].target, links[i].dest) != 0) {
lg_printf(2, "Cannot create symlink %s: %s", links[i].dest, strerror(errno));
goto extract_fail;
}
}
ret = GL_INSTALL_OK;
extract_fail:
for (size_t i = 0; i < nlinks; i++) {
free(links[i].dest);
free(links[i].target);
}
free(links);
if (ret != GL_INSTALL_OK) {
archive_read_free(a2);
/* Remove the half-extracted version dir (created by this call,
* just above). Left behind, it made the next attempt report
* GL_INSTALL_ERR_ALREADY_INSTALLED — harmless in a stage that's
* about to be aborted, wrong in a live context. */
gl_rmdir_recursive(pkg_store_final);
gl_free_pkg(&pkg);
return ret;
}
archive_read_free(a2);
/* now the index will be updated */
/* Resolve "current" symlink to confirm an index exists to rebuild
* against. The actual write happens in gl_rebuild_index below —
* anything written here would just be overwritten by that call,
* since it creates a fresh timestamped index file and repoints
* "current" at it rather than editing this one in place. */
char current_link[PATH_MAX];
snprintf(current_link, sizeof(current_link), "%s/current", ctx->index_path);
char current_target[PATH_MAX];
ssize_t len = readlink(current_link, current_target, sizeof(current_target) - 1);
if (len < 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_INDEX;
}
current_target[len] = '\0';
if (gl_rebuild_index(ctx) != 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_INDEX;
}
/* Grant this package (and only it) the right to displace existing
* non-glacier files on its next link pass — immediately below for a
* live context, post-commit relink for a staged one. */
const char *markers[] = { GL_FRESH_MARKER, ctx->overwrite ? GL_OVERWRITE_MARKER : NULL };
for (int m = 0; m < 2; m++) {
if (!markers[m]) { continue; }
char marker[PATH_MAX];
snprintf(marker, sizeof(marker), "%s/%s", pkg_store_final, markers[m]);
int mfd = open(marker, O_WRONLY | O_CREAT | O_TRUNC | O_NOFOLLOW, 0600);
if (mfd < 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
close(mfd);
}
if (ctx->mode == GL_CTX_LIVE) {
if (gl_link_pkg(ctx, pkg_store_final, pkg.pkg_repo) != 0) {
gl_free_pkg(&pkg);
return GL_INSTALL_ERR_EXTRACT;
}
}
/* GL_CTX_STAGE: skip. A symlink written here would target
* ctx->store_path, which is the stage path — it goes stale the
* moment gl_commit_transaction renames the store into its live
* location. The links tree is fully derived from the store, so
* it's regenerated wholesale post-commit instead of tracked
* incrementally here (see relink_store in transaction.c). */
gl_free_pkg(&pkg);
return GL_INSTALL_OK;
}
/*
* prune_stale_links
* Recursively removes only the symlinks glacier itself created (per
* is_glacier_symlink) whose store target no longer exists — i.e. links
* left behind by a package that's since been removed. Directories,
* regular files, and symlinks glacier didn't create are never touched,
* recursed into for directories aside. This must stay this
* conservative because links_path is not always glacier-exclusive
* territory: for system-scope packages it can be /usr itself, which
* holds plenty of content glacier has no business deleting.
*/
static void
prune_stale_links(const gl_context_t *ctx, const char *dir_path, int *restored)
{
/* Links say runtime paths; their targets are checked under the
* install root. Without that, an image's links would be judged
* against the HOST's store: pruned because the host lacks the
* package, or kept because it has it (IMAGE_DESIGN section 3). */
const char *store_prefix = gl_runtime_path(ctx, ctx->store_path);
DIR *dir = opendir(dir_path);
if (!dir) { return; }
struct dirent *ent;
while ((ent = readdir(dir))) {
if (ent->d_name[0] == '.') { continue; }
char full[PATH_MAX];
snprintf(full, sizeof(full), "%s/%s", dir_path, ent->d_name);
struct stat lst;
if (lstat(full, &lst) != 0) { continue; }
if (S_ISLNK(lst.st_mode)) {
if (!is_glacier_symlink(full, store_prefix)) {
continue; /* not ours - never touch */
}
/* Stale means the STORE entry is gone (package removed),
* checked with lstat on the link's own target. Not
* stat() through it: a package's own symlink may
* legitimately dangle, and following the chain would
* prune (and un-displace) it on every commit. */
char store_entry[PATH_MAX], entry_at[PATH_MAX];
ssize_t tn = readlink(full, store_entry, sizeof(store_entry) - 1);
struct stat target_st;
if (tn >= 0) { store_entry[tn] = '\0'; }
if (tn >= 0 && gl_at(ctx, store_entry, entry_at, sizeof(entry_at)) == 0 &&
lstat(entry_at, &target_st) != 0 && errno == ENOENT) {
/* our symlink, but its target is gone — and if
* installing it displaced an original, put that back */
if (unlink(full) == 0 &&
restore_displaced(ctx, full) == 1) {
(*restored)++;
}
}
} else if (S_ISDIR(lst.st_mode)) {
prune_stale_links(ctx, full, restored);
}
/* regular files: never ours, never touched */
}
closedir(dir);
}
int
gl_relink_store(gl_context_t *ctx)
{
/* This function's contract is "derive the links tree entirely
* from the store" (same as gl_rebuild_index does for the index) -
* but "derive fully" now means prune-then-refresh, not
* wipe-then-rebuild. links_path may be a directory glacier
* doesn't exclusively own (e.g. /usr for system scope), so a
* recursive delete of the whole tree is off the table; only
* glacier's own stale symlinks get removed. gl_mkdirp is
* non-destructive (mkdir tolerates EEXIST) so this is still safe
* to call even if links_path already exists. */
if (gl_mkdirp(ctx->links_path, gl_scope_dir_mode(ctx->scope)) != 0) {
return -1;
}
int restored = 0;
prune_stale_links(ctx, ctx->links_path, &restored);
/* Base-repo packages under system scope route to /usr instead of
* ctx->links_path (see GL_BASE_REPO) — prune there too, since a
* package that WAS in the base repo and got removed/moved leaves
* its stale symlink in /usr, not in ctx->links_path. Only relevant
* for system scope; /usr is never a valid destination for
* per-uid packages. */
if (ctx->scope == GL_SCOPE_SYS) {
char usr[PATH_MAX];
snprintf(usr, sizeof(usr), "%s" GL_BASE_LINKS_DEST, ctx->sysroot);
prune_stale_links(ctx, usr, &restored);
prune_config(ctx);
}
if (restored > 0) {
lg_printf(0, "Restored %d displaced original%s", restored,
restored == 1 ? "" : "s");
}
DIR *repo_dir = opendir(ctx->store_path);
if (!repo_dir) return (errno == ENOENT) ? 0 : -1;
struct dirent *repo_ent;
while ((repo_ent = readdir(repo_dir))) {
if (repo_ent->d_name[0] == '.') continue;
char repo_path[PATH_MAX];
snprintf(repo_path, sizeof(repo_path), "%s/%s",
ctx->store_path, repo_ent->d_name);
if (!is_dir(repo_path)) continue;
DIR *name_dir = opendir(repo_path);
if (!name_dir) continue;
struct dirent *name_ent;
while ((name_ent = readdir(name_dir))) {
if (name_ent->d_name[0] == '.') continue;
char name_path[PATH_MAX];
snprintf(name_path, sizeof(name_path), "%s/%s",
repo_path, name_ent->d_name);
if (!is_dir(name_path)) continue;
/* one version dir per package name today */
DIR *ver_dir = opendir(name_path);
if (!ver_dir) continue;
struct dirent *ver_ent;
while ((ver_ent = readdir(ver_dir))) {
if (ver_ent->d_name[0] == '.') continue;
char ver_path[PATH_MAX];
snprintf(ver_path, sizeof(ver_path), "%s/%s",
name_path, ver_ent->d_name);
if (!is_dir(ver_path)) continue;
if (gl_link_pkg(ctx, ver_path, repo_ent->d_name) != 0) {
closedir(ver_dir); closedir(name_dir);
closedir(repo_dir);
return -1;
}
}
closedir(ver_dir);
}
closedir(name_dir);
}
closedir(repo_dir);
return 0;
}