#define _POSIX_C_SOURCE 200809L #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #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 * "-" (see gl_cat_dir_and_ver * in pkg.c, which is what created it). Strip the * known "-" 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 /.displaced/, * 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 /.etc-state/. 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/, 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/? */ 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/ 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 "-/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/, store// 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; }