2352 lines
81 KiB
C
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;
|
|
}
|