diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..f2a944b --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,261 @@ +# CLAUDE.md + +Context for Claude Code sessions on this repo (libglacier-ng, the package +manager for the Everest/Glacier Linux distribution). This file exists +because there's no built-in way to hand off a claude.ai planning +conversation into Claude Code — this is that handoff, written down. + +For the symlink-routing / package-scope design specifically, see +`PACKAGE_SCOPES.txt` at the repo root — that doc is the source of truth +for that topic and isn't duplicated here. + +## What this project is + +A from-scratch Linux distribution (Everest, built on the Glacier +package-management model) with its own package manager +(`libglacier-ng` + `gpkg`/`gstore` CLI tools) and its own build +tooling (`gbuild`/`gworld`) for bootstrapping a set of packages up to +a minimal bootable system. + +## Core architecture + +**`gl_context_t`** (transaction.h) is the central abstraction: `uid`, +`root_path`, `index_path`, `store_path`, `links_path`, `mode` +(`GL_CTX_LIVE` / `GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR` / +`GL_SCOPE_SYS`), `lock_fd`. + +**Transaction lifecycle**: `gl_init_live_context` / `gl_init_stage_context` +→ `gl_commit_transaction` or `gl_abort_transaction`. Staged installs +hardlink-seed `index`/`store` from live at stage-init time (copy-on-write, +so removing a live package inside a transaction doesn't require special +casing). The `links` tree is deliberately **not** seeded — see below. + +**Commit** does atomic rename-based promotion (stage → live, with the +previous live moved to `old/` as a rollback point), fsyncs the parent +dirs, then calls `gl_relink_store` to regenerate symlinks and +`gl_rebuild_index` to regenerate the index — both from a fresh +`GL_CTX_LIVE` context, not the just-committed stage context. + +**Scopes**: +- `GL_SCOPE_USR`: per-uid, `/glacier/usr/{index,store,links}/`. +- `GL_SCOPE_SYS`: system-wide, `/glacier/sys/{index,store}`. Symlink + destination depends on the package's **repo** — see `PACKAGE_SCOPES.txt`. + Short version: `repo == "base"` → `/usr`; anything else → + `/glacier/sys/links`. This is decided per-package (in `gl_link_pkg` / + `gl_relink_store`), not baked into the context. + +**Repos**: `base` (minimal working system, boot-critical by definition — +this is a repo-level classification, not a per-package flag), `extra` +(important but not required), `community` (everything else). `extra` +and `community` currently behave identically — see open items. + +## Safety mechanisms (read before touching links/symlink code) + +`ctx->links_path` is **not always glacier-exclusive territory** — +for system-scope `base` packages it's literally `/usr`, which holds +plenty of content glacier has no business touching. Everything that +reads or writes a links destination has to treat it as "possibly +shared": + +- `is_glacier_symlink(path, store_prefix)` (istoreutils.c) is the one + source of truth for "did glacier create this." True only if `path` + is a symlink whose target lives under `store_prefix`. Never touch a + path this returns false for. +- `gl_link_pkg` will not overwrite an existing path unless + `is_glacier_symlink` says it's safe to replace. +- `gl_relink_store` prunes stale symlinks (`prune_stale_links`) rather + than wiping and rebuilding the whole links directory — a full + `rm -rf` of `ctx->links_path` would be catastrophic once that path + can be `/usr`. It prunes **both** possible system-scope destinations + (`/glacier/sys/links` and `/usr`) since a single pass can't know in + advance whether any base-repo packages are involved. +- The stage links tree is never hardlink-seeded — it's genuinely + unused (`gl_link_pkg` only ever runs against `GL_CTX_LIVE`; + `gl_relink_store` always builds its own fresh live context). Seeding + it would mean hardlinking all of `/usr` on every staged system + transaction, and risking `EXDEV` if `/usr` and the stage area are on + different filesystems. + +## Bug history (so it doesn't get relitigated or reintroduced) + +Roughly chronological, all already fixed in the current tree: + +1. **`gl_abort_transaction` never actually released the lock file** — a + copy-paste bug wrote into the wrong variable. Fixed by moving off + `O_EXCL`-based locking entirely: `gl_begin_transaction` now just + `open(O_CREAT)` + `flock()`, and lock files are never `unlink()`'d + (the kernel releases `flock()` automatically on process death, even + `SIGKILL`; explicit unlinking has its own TOCTOU race with a second + process creating a new inode at the same path). +2. **`EXDEV` wasn't checked before renaming** — `gl_commit_transaction` + now `stat()`s both sides and fails cleanly with `GL_TXN_ERR_XDEV` + before attempting any renames if stage and live aren't on the same + filesystem. +3. **Segfault on install**, root cause: the Makefile's `transaction` + target didn't link `libglacier_transaction.so` against + `libglacier_istoreutils.so`/`libglacier_log.so`, even though + `transaction.c` calls `gl_relink_store()` and `lg_printf()`. Fixed + by adding those `-l` flags and an explicit `transaction: log + istoreutils` prerequisite (build order matters now). +4. **Symlinks going stale after commit** — `gl_link_pkg` used to run + during staged installs too, embedding the *stage* path as the + symlink target. Fixed: `gl_link_pkg` only runs for `GL_CTX_LIVE`; + links are fully regenerated post-commit by `gl_relink_store`. +5. **Dangling symlinks never cleaned up on removal** — see "Safety + mechanisms" above; this is what `prune_stale_links` fixes. +6. **`GL_TXN_ERR_RENAME` on every commit after the first** — leftover + `old/` directory from a previous commit made the next commit's + `rename()` fail with `ENOTEMPTY`. Fixed: `gl_commit_transaction` + wipes `old_base` if present before using it. +7. **`-Wstringop-truncation` errors** (real bug, not noise) — four + `strncpy(dst, ctx->foo_path, PATH_MAX - 1)` calls that could leave + `dst` unterminated. Fixed by switching to `snprintf(dst, + sizeof(dst), "%s", ...)`, which always terminates. + (`-Wformat-truncation`, separately, is suppressed via + `-Wno-error=format-truncation` in `config.mk` — those warnings are + about genuinely-safe `snprintf` truncation, not a real bug.) +8. **`gbuild` build-system autodetection** checked for a bare + `Makefile` before `configure`/`configure.ac`. Broke musl (and any + project with a hand-rolled, non-autotools build system that ships + both) since its `Makefile` is non-functional without `configure` + generating `config.mak` first. Fixed by reordering the checks. +9. **Index only stored `repo::pkgname`, no version.** Now + `repo::pkgname::version` (`gl_rebuild_index` looks up the + `-` store subdirectory). `struct gpkg_entry` in + `istoreutils.h` needs a `char *ver;` field — this was a manual + header edit, double check it's actually present. +10. **`gworld` recipe parser silently dropped `SYS_PROFILE`** — the + key-matching regex only matched uppercase letters, not + underscores. Fixed to `^([%u_][%u%d_]*)=(.*)$`. Caught by an + actual precedence test, not code review — worth remembering that + class of bug (a field silently never parsing, falling back to a + default that happens to look right) is easy to miss by inspection + alone. + +## CLI additions + +- `gpkg -s` / `--system`: operate against `GL_SCOPE_SYS` instead of a + uid's tree. Requires root (checked via `geteuid()`). Must appear + *before* `-l`/`-x` in a grouped flag string (e.g. `-sl`, not `-ls`) — + flags are handled in parse order, same constraint `-V`/`-S` already + had. +- `gstore -N` / `--new-system`: calls `gl_init_sys()`. Requires root. +- `gbuild -P` / `--profile PROFILE`: system profile, `ARCH-LIBC[-FEATURE...]` + (e.g. `x86_64-musl`, `x86_64-glibc-multilib`). Derives a cross-compile + prefix (translating `glibc`→`gnu` per real GNU triplet convention), + detects native-vs-cross against the build host, and only pre-fills + `-t`/`-x` defaults for whichever the user didn't already set + explicitly. Features export as `GL_FEATURE_=1` env vars for + custom `-b`/`-i` commands to branch on — gbuild itself doesn't + hardcode per-feature behavior, that's intentional. + +## Build system + +`Makefile` builds separate `.so`s per module +(`libglacier_log.so`, `libglacier_pkg.so`, `libglacier_istoreutils.so`, +`libglacier_transaction.so`, `libglacier_dag.so`) — **this split is +deliberate Unix-philosophy design**, not an accident: a third-party +glacier-compatible program should be able to link only +`libglacier_log.so` without pulling in everything else. `gpkg`/`gstore` +load the specific per-module `.so`s they need directly, matching this. + +There's also a `libglacier-ng` Makefile target producing a **combined** +`.so` — this exists *solely* for the Lua FFI test harness's +convenience (one thing to `ffi.load()` instead of juggling rpath +chains in a throwaway test process). It is **not** in `all` and **not** +installed. Don't reach for it outside `tests/` — that's a sign +something should be split-loaded instead. + +`config.mk` holds `CFLAGS`, including `-Wno-error=format-truncation` +(see bug #7 above for why that specific warning is suppressed and +others aren't). + +## Lua tooling + +- `gpkg`, `gstore`: LuaJIT + FFI, calling directly into the `.so`s. +- `lib/glacier_cdef.lua`: single shared source of truth for every + `ffi.cdef()` declaration, used by `gpkg`, `gstore`, and the test + suite. This used to be duplicated three ways, which is exactly what + let a stale signature drift silently into `gpkg` at one point — if + you're editing a C function signature, this file needs the matching + update, and it's the *only* place that needs it now. +- `tests/*.lua`: LuaJIT FFI test suite exercising real filesystem/process + behavior (`fork`+`SIGKILL` for lock crash-recovery, real `mount()` + for the `EXDEV` check, etc.) — not mocked. Run via: + ```sh + cd tests + make -C .. libglacier-ng # builds the test-only combined .so + GLACIER_TEST_CONFIRM=yes LIBGLACIER_SO=$(pwd)/../build/lib/shared/libglacier-ng.so luajit run_all.lua + ``` + `test_exdev.lua` needs `--cap-add=SYS_ADMIN` in Docker to actually + exercise the mount path; it skips cleanly (not a failure) without it. + +## Build automation (bootstrap toolchain) + +- `gbuild` (bash): single-package builder. Autodetects build system + (cmake/meson/cargo/go/autotools/make), supports cross-compilation + (`-t`/`-x`, now also `-P` for profiles). +- `gworld` (Lua): batch orchestrator. Reads `recipes/*.recipe`, + topologically sorts by `DEPS`, invokes `gbuild` once per package in + order. `-s SYSROOT` gives every package in the batch a shared + staging dir — relies on `gbuild -p` never wiping its target, so + headers/libs accumulate across the batch for free. +- Recipe format: `NAME VER REPO URL/REF-or-SRC DEPS BUILD INSTALL + FLAGS SYS_PROFILE` (plain `key=value`, see `recipes/musl.recipe` for + a real example). +- Real multi-pass toolchain bootstraps (LFS-style — gcc built twice, + etc.) should use separate recipes per pass (`gcc-pass1`, + `gcc-pass2`), publishing only the final pass under the real + name/repo. The DAG models build steps, not abstract packages. +- **`grootstrap` needs a rebuild, not yet done.** The old design (raw + `tar` extraction into a target dir + chroot + register everything + under an arbitrary bootstrap uid) predates the `GL_SCOPE_SYS` + + base-repo design and is now more complicated than necessary. Once + `gpkg`/`gstore` themselves exist on the target (still an unsolved + bootstrapping-the-bootstrapper problem — how do they get there + before a working package manager exists to install them with), + every package including `base` ones can go through the same `gpkg + -s -l` path; the repo alone routes symlinks correctly. No more + special-casing base packages during bootstrap. + +## Known open items / stubs + +- `gpkg -f` / `-u` (merge/update) are unimplemented stubs. `-f` + references an undefined `uid` variable (should be `uidn`) — will + nil-concatenation-error the moment it's actually implemented. +- `gpkg -x`'s confirmation summary shows placeholder `unknown::pkg + 0.0.0 (unknown)` since it has no local manifest to read version/repo + from for a bare package name. Would need `resolve_repo` called + before building the summary, not just before the removal itself. +- `gpkg -x` doesn't print a completion message on success (`-l` does: + "Completed with no errors."). Cosmetic inconsistency. +- Reinstalling an up-to-date package double-logs ("Installed X" + + "staged from X" for the same event). +- `extra` vs `community` repos don't currently diverge in behavior — + both just mean "not base" for symlink-routing purposes. +- `gl_relink_store`/`gl_rebuild_index` are full rescans of the store on + every commit — fine at current scale, worth revisiting if large + transactions become common (a dirty-set of touched packages instead + of full rescans). +- Package manifests don't record which `SYS_PROFILE` they were built + under — deliberately not added yet (would need a `pkg.c` / + `gl_gpm2gpkg` change), flagged as a future option, not a gap that + needs fixing now. +- A stray nested `lib/glacier/lib/glacier/` directory was spotted once + in a `tree` listing — looked like a leftover `make install` + artifact, not in any load path, harmless but worth a `rm -rf` + eventually. + +## Conventions worth knowing before editing + +- C source generally uses tabs for indentation, though the codebase is + inconsistent about it in places (some blocks use spaces). ALWAYS USE TABS WHEN POSSIBLE. +- `PATH_MAX`-sized stack buffers + `snprintf` is the standard pattern + for path construction throughout; prefer it over `strncpy` for + anything copying into a fixed buffer (see bug #7). +- Don't add convenience wrappers that silently touch a broader + filesystem scope than what's asked — this codebase has already been + bitten twice by exactly that class of mistake (`gl_relink_store`'s + wipe, `gl_link_pkg`'s blind `unlink`), both only becoming dangerous + once a path assumption (`links_path` is glacier-exclusive) quietly + stopped holding. diff --git a/Makefile b/Makefile index c459893..3b627c5 100644 --- a/Makefile +++ b/Makefile @@ -1,8 +1,8 @@ include config.mk -.PHONY: all prepare log log_test rt pkg istoreutils make_conf dag install clean static libglacier.a install_static +.PHONY: all prepare log log_test rt pkg istoreutils make_conf dag install clean static libglacier.a install_static libglacier-ng sha256 gsha256sum libglacier_verify -all: prepare log log_test rt pkg istoreutils make_conf dag +all: prepare log log_test rt pkg transaction istoreutils make_conf dag sha256 libglacier_verify gsha256sum @echo "libglacier-ng has finished building" static: prepare log_static pkg_static istoreutils_static libglacier.a @@ -29,6 +29,7 @@ prepare: mkdir -p build/lib/tmp mkdir -p build/lib/shared mkdir -p build/include/glacier + mkdir -p build/bin log: $(CC) $(CFLAGS) src/log.c -c -o build/lib/tmp/log.o @@ -40,6 +41,22 @@ log_test: rt: $(CC) $(CFLAGS) src/runtime.c -c -o build/lib/tmp/runtime.o +sha256: + $(CC) $(CFLAGS) src/sha256.c -c -o build/lib/tmp/sha256.o + +libglacier_verify: + $(CC) $(CFLAGS) -shared -fPIC src/sha256.c \ + -o build/lib/shared/libglacier_verify.so + +gsha256sum: prepare + $(CC) $(CFLAGS) src/gsha256sum.c src/sha256.c -o build/bin/gsha256sum + +transaction: log istoreutils + $(CC) $(CFLAGS) -shared -fPIC src/transaction.c \ + -Lbuild/lib/shared -lglacier_istoreutils -lglacier_log \ + -Wl,-rpath,'$$ORIGIN' \ + -o build/lib/shared/libglacier_transaction.so + istoreutils: pkg $(CC) $(CFLAGS) -shared -fPIC src/istoreutils.c \ -Lbuild/lib/shared -lglacier_log -lglacier_pkg -larchive \ @@ -55,6 +72,12 @@ pkg: make_conf: $(CC) $(CFLAGS) src/make_conf.c -c -o build/lib/tmp/make_conf.o +libglacier-ng: prepare + $(CC) $(CFLAGS) -shared -fPIC \ + src/log.c src/pkg.c src/transaction.c src/istoreutils.c \ + -larchive -lconfig \ + -o build/lib/shared/libglacier-ng.so + dag: $(CC) $(CFLAGS) -shared -fPIC src/dag.c \ -Lbuild/lib/shared -lglacier_log \ @@ -65,8 +88,10 @@ install: mkdir -p $(PREFIX)/lib/glacier/ install build/lib/shared/libglacier_log.so $(PREFIX)/lib/glacier/ -m 755 install build/lib/shared/libglacier_istoreutils.so $(PREFIX)/lib/glacier/ -m 755 + install build/lib/shared/libglacier_transaction.so $(PREFIX)/lib/glacier/ -m 755 install build/lib/shared/libglacier_pkg.so $(PREFIX)/lib/glacier -m 755 - install build/lib/shared/libglacier_dag.so $(PREFIX)/lib/glacier -m 755 + install build/lib/shared/libglacier_verify.so $(PREFIX)/lib/glacier -m 755 + install build/bin/gsha256sum $(PREFIX)/bin/ -m 755 install_static: mkdir -p $(PREFIX)/lib/glacier diff --git a/PACKAGE_SCOPES.txt b/PACKAGE_SCOPES.txt new file mode 100644 index 0000000..00547f5 --- /dev/null +++ b/PACKAGE_SCOPES.txt @@ -0,0 +1,177 @@ ++------------------------------------+ +| Package scopes and symlink routing | ++------------------------------------+ + +This document records the design decisions behind how glacier packages +are scoped, how their repo determines where their symlinks land, and +what "boot-critical" means in this system. It exists because these +rules aren't obvious from reading the code alone, and getting them +wrong (particularly the symlink-placement rule) has real consequences +up to and including destroying a live /usr. + + ++------------------------------------+ +| 1. The two scopes | ++------------------------------------+ + +USR (GL_SCOPE_USR) + Per-uid packages. Index/store/links all live under a specific + uid's tree: + /glacier/usr/index/ + /glacier/usr/store/ + /glacier/usr/links/ + Symlinks always land under /glacier/usr/links//{bin,...}. + There is no base-repo exception for USR scope. Nothing installed + under a per-user scope is ever considered part of the base system, + so nothing here ever touches /usr. + +SYS (GL_SCOPE_SYS) + System-wide packages, not owned by any particular uid: + /glacier/sys/index + /glacier/sys/store + Symlink destination depends on the package's repo (see section 3). + Requires root. gpkg enforces this via geteuid() before it will + even attempt a system-scope operation. + + ++------------------------------------+ +| 2. Repo semantics | ++------------------------------------+ + +A package's repo (PKG_REPO in its manifest) is not just a label for +organizing the store — under system scope, it is the one thing that +decides where that package's activation symlinks physically land. + + base + Packages that make up a minimal working system. ALL packages + in this repo are considered boot-critical BY DEFINITION — this + is a repo-level classification, not a per-package flag. A + package doesn't have to be individually essential for booting + to count; if it's filed under base, it's treated as part of + the base system, full stop. + + extra + Software that may be important but is not necessary to run + Everest, nor to install it. + + community + Everything else. + + NOTE: extra and community are currently NOT distinguished by the + library itself — both simply mean "not base," and both route to + the same default system-scope links location. If they need to + diverge further later (different confirmation prompts, different + trust handling, whatever), that is a deliberate future change, not + something already implemented. + + ++------------------------------------+ +| 3. Symlink placement rule | ++------------------------------------+ + + scope == USR -> /glacier/usr/links/ + scope == SYS && repo == "base" -> /usr (*) + scope == SYS && repo != "base" -> /glacier/sys/links + +(*) This is the base-system exception. Packages that are part of the + minimal working system get their symlinks placed directly into + /usr/bin, /usr/lib, etc. — real FHS locations — so that anything + with a hardcoded path (init scripts, systemd units, shebang lines) + works without needing glacier-aware PATH setup. + + Everything an operator installs under system scope that ISN'T + part of the base system stays isolated under /glacier/sys/links, + same as it always has. The base system is the exception to the + norm, not the other way around. + +This decision is made per-package, at the point where a package's repo +is actually known (inside gl_link_pkg / gl_unlink_pkg / the walk loop +in gl_relink_store) — NOT baked into the context at creation time. +gl_context_t's links_path field always holds the DEFAULT destination +for that scope; the /usr override is computed separately per package. + + ++------------------------------------+ +| 4. Why /usr as a target is safe | ++------------------------------------+ + +/usr is not glacier-exclusive territory the way /glacier/sys/links is. +It holds plenty of content glacier has no business touching. Every +place that reads or writes into a links destination therefore treats +that destination as "possibly shared, not owned": + + - is_glacier_symlink(path, store_prefix) is the single source of + truth for "did glacier itself create this." It reports true only + if `path` is a symlink whose target lives under the relevant + store path. Anything else — a real file, a directory, a foreign + symlink pointing somewhere else entirely — is never touched. + + - gl_link_pkg refuses to overwrite an existing path unless + is_glacier_symlink says it's safe to replace. It will not clobber + a foreign file that happens to occupy the same path. + + - gl_relink_store no longer wipes and rebuilds its links directory + wholesale (that was safe only when the target was glacier-only + territory). It prunes ONLY stale symlinks glacier itself created + (identified via is_glacier_symlink, removed only if their store + target no longer exists) and leaves everything else alone. + + - A single system-scope relink pass prunes BOTH possible + destinations (/glacier/sys/links and /usr), since it can't know in + advance whether any base-repo packages are involved without + checking both. + + - The stage tree's links directory is no longer hardlink-seeded at + all. It was always unused (gl_link_pkg only ever runs against a + LIVE context; gl_relink_store always builds its own fresh live + context rather than touching a staged one) — and now that live + links_path can be /usr, seeding it would mean hardlinking the + entire /usr tree on every staged system transaction, and risking + EXDEV outright if /usr and the stage area are on different + filesystems. + + ++------------------------------------+ +| 5. Updating base-system packages | ++------------------------------------+ + +Base-repo packages are tracked in the system index like anything else, +and updating them works exactly the same way updating any other +package does — same staged transaction, same commit/rollback. No +separate mechanism was built for this, because none was needed: + + gpkg -s newmusl.gpkg + +...updates musl in place under system scope, symlinks and all, +whether it was originally installed by a bootstrap tool or by a normal +`gpkg -s` call later. There is no meaningful distinction between +"how it originally got there" and "how you update it." + +Replacing something as fundamental as libc on a LIVE running system is +safe under this model specifically because of POSIX unlink semantics: +a process that already has a shared object open keeps working off the +old inode even after the symlink swap happens. Only newly-spawned +processes after the swap see the new version. This falls directly out +of the atomic-rename commit design already in place — nothing extra +was added to make it work. + + ++------------------------------------+ +| 6. Open items | ++------------------------------------+ + + - grootstrap has not been rebuilt against this design yet. The + simplification this design enables: once gpkg/gstore themselves + exist on the target (still a bootstrapping problem that needs + solving separately), EVERY package — base included — can go + through the same `gpkg -s -l` path. There is no longer a need for + a separate raw-extraction special-case for base packages + specifically; the repo alone routes them to /usr correctly. + + - extra vs community currently behave identically (see section 2). + Whether they should diverge, and how, is undecided. + + - The bootstrapping-a-bootstrapper problem (getting gpkg/gstore onto + a target filesystem before there's a working package manager to + install them with) is still unsolved and orthogonal to everything + in this document. diff --git a/SECURITY_DESIGN.txt b/SECURITY_DESIGN.txt new file mode 100644 index 0000000..882db5c --- /dev/null +++ b/SECURITY_DESIGN.txt @@ -0,0 +1,379 @@ ++--------------------------------------------+ +| Package integrity & scope security | ++--------------------------------------------+ + +This document records design decisions made in one planning session, +covering package integrity verification and the hard separation +between per-user and system-scope package tools. NONE OF THIS IS +IMPLEMENTED YET. It exists so implementation can start from a settled +plan instead of re-deriving it. + +Companion document: PACKAGE_SCOPES.txt (repo/scope symlink routing — +referenced but not repeated here). + + ++--------------------------------------------+ +| 1. Scope of this phase | ++--------------------------------------------+ + +Two different guarantees are in play, and they are NOT the same thing: + + integrity - has this file been corrupted or altered + authenticity - did this actually come from who it claims to be + +A hash alone only ever proves integrity. If the hash travels with the +file from the same untrusted source, tampering both together defeats +it. This phase is INTEGRITY ONLY. Authenticity (signing, a trusted +keyring) is a deliberate future phase, tied to remote-repo support — +not being designed in detail now, but a few choices below are made to +stay cheap to extend into later. + +Everest is a security-focused distro (this is also *why* per-user +package isolation exists in the first place). Two sides of the CIA +triad — confidentiality is out of scope here, but integrity and +(eventually) authenticity are both treated as first-class, not +best-effort. + + ++--------------------------------------------+ +| 2. Hash algorithm: SHA-256 | ++--------------------------------------------+ + +Decision: SHA-256, not SHA-512. + +Rationale (SHA-512 was seriously considered, not dismissed by default): + - SHA-512's usual advantage (faster on 64-bit CPUs, due to 64-bit + word operations) does not hold across this project's actual + target set. Everest explicitly supports multiple architectures + via SYS_PROFILE, including 32-bit ones — on a 32-bit target, + SHA-512 is SLOWER than SHA-256 (64-bit math has to be emulated). + - SHA-256's 128-bit collision resistance is not a meaningful + weakness for this use case. There is no realistic attack path + where 128 bits is the bottleneck for "did this archive get + corrupted or tampered with." + - SHA-256 is what every comparable tool already defaults to (apk, + pacman, dpkg), and is the simpler implementation to vendor and + audit (smaller constant tables, no 64-bit rotates). + +Escape hatch, explicitly kept open: hash values are namespaced as +"sha256:", never a bare hex string, specifically so a future +algorithm change is additive, not a format migration. "sha256 can +always be added to base if needed" — i.e. if the vendored +implementation is ever found lacking, the fallback is depending on a +real sha256sum binary once one exists in the base repo, not a redesign +of the sidecar format. + + ++--------------------------------------------+ +| 3. What gets hashed, and how | ++--------------------------------------------+ + +WHAT: whole-archive only, for now. Per-file hashing (and the +eventual `gpkg --verify`-style installed-file audit it would enable) +is explicitly deferred, not rejected. + +WHERE: a sidecar file next to the .gpkg — + + musl-1.2.5.gpkg + musl-1.2.5.gpkg.sha256 <- contains exactly: sha256: + +Sidecar was chosen over embedding in the manifest specifically because +a whole-archive hash cannot cleanly be embedded inside the file it is +hashing without excluding the hash field itself from the computation. +This also generalizes naturally to a future remote repo index that +just lists hashes for everything it serves. + +IMPLEMENTATION SHAPE: one shared vendored implementation, compiled +two different ways, so there is exactly one place the algorithm +itself is ever implemented: + + - src/sha256.c / src/sha256.h + Streaming API (init/update/final), not just "hash this file" — + chosen now specifically so per-file hashing later reuses the + same primitive without a rewrite. + + - gpkg / syspkg side: + Compiled into a PRIVATE .so (no public header, not part of the + split-library public API surface). Loaded directly via FFI from + gpkg_common.lua (see section 6). Private for now because this is + new, unaudited code; promoting it to a real public + libglacier_crypto.so is the natural path once per-file hashing + is built and the API has proven itself. + + - gbuild side: + Compiled into a tiny standalone CLI helper, `gsha256sum FILE`, + printing `sha256:` to stdout. gbuild shells out to it the + same way it already shells out to git/tar/make. This keeps + gbuild from depending on the host having sha256sum or openssl + installed, while still not needing gbuild to link against the + private .so directly (gbuild is a bash script, not something + that can drive FFI). + + gbuild change: after producing the .gpkg archive, hash it with + gsha256sum and write the sidecar alongside it. This is not optional + groundwork — the moment verification is turned on, EVERYTHING + becomes a "missing sidecar" failure until this exists, so the two + changes (gbuild writes sidecars / gpkg+syspkg check them) ship as + ONE atomic change, not sequential phases. + + ++--------------------------------------------+ +| 4. Refusal policy | ++--------------------------------------------+ + +Missing sidecar and hash mismatch are treated IDENTICALLY. There is no +soft "unverified, proceed anyway by default" tier — absence of proof +is treated the same as proof of tampering. + + repo == "base" -> ALWAYS refuse. No flag, no + scope, no exception. Ever. + scope == SYS (via syspkg) -> ALWAYS refuse, for ANY repo, + including extra/community. + No flag overrides this. + scope == USR && repo != base -> refuse by default. The ONLY + place a bypass flag could ever + apply, once one exists. + +No bypass flag exists yet. One may be added later, but ONLY for the +USR-scope/non-base case above — never for base, never for system +scope, regardless of how the flag is invoked. + +Net effect, stated as a real security property: there is no flag +combination, present or future, that can install a corrupted base +package or a corrupted system-scope package. + +HOOK POINT: gpkg's read_manifest_fields already resolves a package's +repo before any transaction machinery runs (it's needed for the +existing confirmation summary). Verification belongs right there, +before gl_init_stage_context is ever called. A failure aborts that +package the same way any other install error already does today — no +new failure-handling pattern needed. + + ++--------------------------------------------+ +| 5. Tool separation: gpkg / gstore / syspkg | ++--------------------------------------------+ + +gpkg and gstore are BOTH strictly per-user tools. Neither has, or will +ever have, any code path into GL_SCOPE_SYS. This is a reversal of +earlier work in this project (gpkg briefly gained a -s/--system flag +and gstore briefly gained -N/--new-system) — both need to come back +out. Recorded here explicitly so it isn't missed during +implementation: removing capability that already exists in the +current tree, not just "don't add it." + +syspkg is a new, separate binary — the SOLE tool with any system-scope +capability at all: install, remove, AND store creation/init (what +gstore -N used to do). One tool to secure, one tool to audit, one +place the privilege boundary has to be enforced. + +Why store creation belongs in syspkg and not "the installer calling +gl_init_sys() directly": that would create a second, independent, +unaudited entry point into system-scope mutation — the exact problem +already solved for install/remove. syspkg owning ALL system-scope +mutation, with nothing else able to touch it, is the actual point. + +gstore's existing UID bounds-check behavior (hard-refuses < 1000, even +under doas, even as UID 0) is confirmed already working correctly and +is being preserved exactly as-is — gstore stays permanently +user-scope-only; the fix is removing -N, not touching the bounds +check. + + ++--------------------------------------------+ +| 6. syspkg privilege model | ++--------------------------------------------+ + +syspkg is always run as root. It is invoked via doas (or sudo) — +NEVER made setuid. This was an explicit rejected alternative, not an +unconsidered default: + +A setuid LuaJIT interpreter is a real anti-pattern. It would inherit +environment variables, library search paths, and require() resolution +from whatever untrusted shell invoked it — all of which become +privilege-escalation surface the instant the process is running as +root. This is the same class of problem that has made setuid +scripts (shell, Perl, anything with a runtime) a long-standing +security no-go. + +syspkg's own responsibility is limited to checking geteuid() == 0 and +refusing otherwise. The actual privilege boundary is enforced by doas +— a hardened, audited, purpose-built tool for exactly this — not +reimplemented inside syspkg itself. + + ++--------------------------------------------+ +| 7. Shared code: gpkg_common.lua | ++--------------------------------------------+ + +gpkg and syspkg need nearly identical machinery: manifest reading, the +summary/confirmation prompt, staged-transaction handling +(gl_init_stage_context -> install/remove loop -> commit/abort), and +the hash-verification check from section 4. All of that lives in +lib/gpkg_common.lua, required by both. + +What stays DISTINCT per-binary: + - Which scope each one is even capable of requesting. gpkg's code + simply never constructs a GL_SCOPE_SYS context — not "doesn't + expose a flag for it," does not have the capability in its code + path at all. + - The privilege model (only syspkg is ever invoked with elevation). + +This split means the refusal matrix in section 4 lives ONCE in the +shared module — it does not need to be written twice or kept in sync +across two files. syspkg is simply the only caller that can ever +reach the scope == SYS branch of it. + +This decision exists partly because of history in this project: a +previously-duplicated ffi.cdef block across three files caused a real, +hard-to-diagnose bug. Not repeating that shape here on purpose. + + ++--------------------------------------------+ +| 8. Base repo cannot install to USR | ++--------------------------------------------+ + +A package in the base repo can NEVER be installed under GL_SCOPE_USR +(per-user scope). This is very unlikely to be attempted in practice, +but is being hardcoded as a real invariant anyway, not left as an +assumption. + +ENFORCEMENT LEVEL: the library itself (gl_install_pkg), not any CLI. +scope == GL_SCOPE_USR && repo == GL_BASE_REPO refuses unconditionally, +regardless of which caller asked for it. + +Rationale: the whole point of the split-.so architecture is that a +third-party tool can be built directly against libglacier-ng without +going through gpkg/syspkg at all. A rule that only one CLI happens to +respect is not a real guarantee. This must be a property of the +package system itself. + + ++--------------------------------------------+ +| 9. syspkg self-update | ++--------------------------------------------+ + +syspkg will itself ship as a base-repo package (it has to, to be part +of the minimal system) and therefore has to be able to update itself +while running. Resolved the same way the earlier libc-live-update +question was resolved (see PACKAGE_SCOPES.txt section 5): POSIX +unlink() semantics already make this safe — a running process keeps +its old inode open even after the symlink swap, only newly-spawned +processes see the new version. No special self-replace mechanism +needed; this falls directly out of the existing atomic-rename commit +design. + + ++--------------------------------------------+ +| 10. Open: the installer / grootstrap | ++--------------------------------------------+ + +STATUS: genuinely undecided, not just unimplemented. The actual +process of installing Everest end-to-end is, in the author's own +words, "kind of elusive" right now. grootstrap's rework is explicitly +BLOCKED on this getting its own dedicated design pass — not something +to force an answer on tonight. + +What IS clear: + - grootstrap (or whatever the installer turns out to be) cannot be + correctly rebuilt until syspkg, gpkg_common.lua, and hash + verification all exist — every package a bootstrap installs is + base-repo, the strictest tier, so the installer is the FIRST real + consumer of all three at once. + - Whatever places syspkg itself onto a target filesystem, before + syspkg exists there to enforce anything, is definitionally outside + the "corrupted base package can never install" guarantee. This is + an honest edge, not a flaw — it only closes once the installer's + actual shape is designed. Live/install media is presumably the + root of trust at that point (if you trust what you booted from, + you trust what it lays down) — but this has not been decided, only + named as the shape of the problem. + +REFERENCE MATERIAL (from an old installation guide at +everestlinux.org/install, read only through its "Syncing the Build +Environment" section — earlier material only, deliberately not +consulting anything past that point. Describes a previous, +possibly-outdated vision of the install process; treat as INPUT to the +future design pass, not as decisions already made. No system image +tarball concept appears anywhere in this range — that idea is +discarded entirely, not merely unmentioned): + + - "system mountpoint" — a target directory (example used: + /mnt/everest, with a SYS_MNT env var pointing at it) that the new + system's root gets built at, kept separate from the host doing the + building. Conceptually the same thing grootstrap's TARGET_DIR + already is. + + - Partition layout described around that mountpoint: an EFI system + partition at .../boot, an OPTIONAL dedicated partition for + /glacier itself (suggested minimum 15 GB), an optional /home + partition, and the remainder as root. The optional standalone + glacier partition is worth noting specifically — it implies + /glacier was originally conceived as something that could be its + own filesystem, separate from root, not necessarily just a + directory tree living on the root filesystem. Not a decision, + just a detail worth not losing. + + - "template index" — a named, downloadable list of packages + defining what a given install should contain (the guide's own + words: "provides a list of packages which can be merged into a + build environment, thereby creating a semi-functional system"). + The guide frames CHOOSING a template index around a specific set + of questions: does the install need 32-bit libraries, will it run + proprietary software that can't be recompiled, does it need a + specific C library for the hardware, is SELinux wanted, is a + non-default init system wanted. Those are essentially the exact + axes SYS_PROFILE already encodes (ARCH-LIBC[-FEATURE...]) — this + is a real correspondence, not a coincidence to ignore. Worth + deciding whether a template index simply IS a SYS_PROFILE plus a + recipes/ set under current design, or a separate downstream + concept (e.g. a pre-resolved list of already-built package + references to fetch, rather than a list of things to build from + source). The guide also warns that switching template + indexes/profiles after install requires rebuilding most of the + system — consistent with SYS_PROFILE being a foundational, + not-meant-to-change-casually choice in the current design too. + + - "glacier-bootstrap PATH_TO_TEMPLATE_INDEX /mnt/everest" — a + referenced (possibly not-yet-existing-in-current-form) tool + described as taking "the specified template index, and + bootstrap[ping] a system at the specified directory, using the + packages listed within the template index as a guide." This is + package-list-driven bootstrapping, not image-based — it lines up + with what grootstrap is already trying to be (extract/install a + defined set of packages into a target directory), not with + anything tarball-shaped. + +None of the above is a decision. It's what exists to react to when the +installer gets its own design session — scoped deliberately to only +the early, environment-setup half of the old guide for now. + + ++--------------------------------------------+ +| 11. Build order for tomorrow | ++--------------------------------------------+ + +Dependency order, not necessarily literal implementation order within +a day, but grootstrap specifically cannot start until the first three +exist: + + 1. src/sha256.c / src/sha256.h (no dependencies) + 2. gsha256sum CLI (needs 1) + 3. private hash .so for gpkg/syspkg (needs 1) + 4. gbuild: write sidecar on build (needs 2) + 5. lib/gpkg_common.lua (shared logic extraction) + 6. gl_install_pkg: base->USR refusal (library-level, needs nothing + above, can happen any time) + 7. gpkg: remove -s/--system entirely (needs 5, to not duplicate + logic while trimming it) + 8. gstore: remove -N/--new-system entirely + 9. syspkg: new binary — install/remove/init under GL_SCOPE_SYS, + doas-only, no setuid (needs 1, 3, 5) + 10. grootstrap rework BLOCKED — needs 4, 9, AND a + separate installer design + pass (section 10). Do not + start until that exists. + +Verify-downloads-from-remote-repos (authenticity, signing, keyring) +is the explicitly agreed NEXT major item after everything above is +implemented — not part of this phase. diff --git a/SECURITY_REVIEW_2026-07-16.md b/SECURITY_REVIEW_2026-07-16.md new file mode 100644 index 0000000..dec0589 --- /dev/null +++ b/SECURITY_REVIEW_2026-07-16.md @@ -0,0 +1,130 @@ +# Security Review — libglacier-ng (dev branch, working tree) + +Date: 2026-07-16 +Scope: uncommitted working-tree changes (Makefile, config.mk, src/common.h, +src/istoreutils.{c,h}, new src/transaction.{c,h}). The `dev` branch had no +new commits vs. `origin/dev`, so the actual diff under review is the +unstaged/untracked working-tree state. + +All three findings below share one root cause: package manifests +(`PKG_NAME` / `PKG_REPO`, read from `manifest.gpm.cfg` inside the `.gpkg` +archive being installed) are fully attacker-controlled and are never +validated anywhere in the codebase. A full-repo search for +`signature|verify|trusted|checksum|sha256|gpg|allowlist` returned zero +hits — there is no package authenticity or provenance check at all. + +This PR's changes (`GL_SCOPE_SYS` / `gl_init_sys`, and the new +`GL_BASE_REPO` → `/usr` symlink routing) newly plug that untrusted data +into root-privileged, system-wide install paths, which is what escalates +these from "attacker writes into their own store" to "attacker writes as +root, or shadows real `/usr` binaries." + +--- + +## Finding 1 — Authorization bypass via self-declared "base" repo + +* **File:** `src/istoreutils.c:742` (also `:859`, `:1404`) +* **Severity:** High +* **Category:** `authorization-bypass` +* **Confidence:** High + +**Description:** System-scope symlink placement into `/usr` is gated only +by a package's manifest self-declaring `PKG_REPO="base"`. There is no +check that the package actually originated from a trusted/vetted base +repository — the classification is just a string the package author +wrote into their own manifest. + +**Exploit scenario:** An operator runs `gpkg -s malicious.gpkg` (a +normal, documented system-scope install per `PACKAGE_SCOPES.txt`) on a +package whose `manifest.gpm.cfg` the attacker fully controls. Setting +`PKG_REPO="base"` causes `gl_link_pkg` (istoreutils.c:742) and +`gl_relink_store` (istoreutils.c:1404) to route the package's symlinks +into `GL_BASE_LINKS_DEST` (`/usr`) instead of the isolated +`/glacier/sys/links` tree — the same privileged, real-FHS locations init +scripts, systemd units, and shebang lines trust. Combined with Finding 3 +(unsanitized `PKG_NAME`), the attacker can target specific paths such as +`/usr/bin/sudo`. + +**Recommendation:** Don't trust the manifest's self-declared repo for +privilege routing. Either (a) determine repo/trust from the source the +package was fetched from (signed repo metadata, not embedded manifest +fields), or (b) require a separate signature/checksum check before a +package is allowed to claim `base` classification and land in `/usr`. + +--- + +## Finding 2 — Tar-slip path traversal during archive extraction + +* **File:** `src/istoreutils.c:1242` (loop starting ~1234, `dest_path` built at 1249) +* **Severity:** High +* **Category:** `path-traversal` +* **Confidence:** High + +**Description:** `gl_install_pkg` extracts the package tar with a +hand-rolled `open()`/`mkdir()` loop instead of libarchive's +`archive_write_disk` (which offers `ARCHIVE_EXTRACT_SECURE_NODOTDOT` / +`SECURE_SYMLINKS` protections). The per-entry relative path (`rel_path`, +istoreutils.c:1242) is taken directly from the tar entry name after +stripping a known prefix, and is concatenated into `dest_path` via +`snprintf` with no rejection of `..` segments or absolute paths. + +**Exploit scenario:** Because the strip prefix is +`"-/files/"` and `pkg_name` is itself attacker-controlled +(pulled verbatim from the manifest, no validation), the attacker fully +controls the prefix match and can name a tar entry +`"-/files/../../../../etc/cron.d/pwn"`. After prefix +stripping this becomes `../../../../etc/cron.d/pwn`, written wherever the +installing process can write. This code path is reachable for both +per-uid and (newly, via this PR) root-privileged system-scope installs, +so the same bug now yields root-level arbitrary file write. + +**Recommendation:** Reject any entry path containing `..` components or a +leading `/` before extraction, or switch to `archive_write_disk` with +`ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS | +ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS` set. + +--- + +## Finding 3 — Path traversal via unsanitized PKG_NAME / PKG_REPO in store paths + +* **File:** `src/istoreutils.c:1196` (manifest parsing: `src/pkg.c:149,153`) +* **Severity:** Medium–High +* **Category:** `path-traversal` +* **Confidence:** High + +**Description:** `gl_gpm2gpkg` (`pkg.c:149,153`) reads `PKG_NAME` and +`PKG_REPO` straight out of the archive's manifest config with +`config_lookup_string` + `strdup`, no validation. `gl_install_pkg` then +builds the package's store directory as `"%s/%s/%s"` from +`ctx->store_path`, `pkg.pkg_repo`, `pkg.pkg_name` (istoreutils.c:1196) +and `mkdir -p`s it. + +**Exploit scenario:** A manifest with `PKG_REPO="../../../../tmp"` (or a +`PKG_NAME` containing `../` segments) causes the package directory — +and, via the extraction loop right after (Finding 2), arbitrary files — +to be created outside `ctx->store_path` entirely. Previously this only +affected a single user's own per-uid store; with this PR's new +`GL_SCOPE_SYS` (`ctx->store_path == "/glacier/sys/store"`, root-owned), +the same bug now lets an untrusted package write anywhere root can +write. + +**Recommendation:** Validate `PKG_NAME` and `PKG_REPO` against an +allowlist pattern (e.g. `^[A-Za-z0-9._-]+$`) immediately after parsing +in `gl_gpm2gpkg`, rejecting the manifest outright if either field +contains `/`, `..`, or is empty. + +--- + +## Note — unrelated working-tree cleanup + +The working tree also contains what look like leftover failed-patch +artifacts, not part of the reviewed logic but worth cleaning up before +committing: + +``` +istoreutils.c.diff +istoreutils.h.diff +src/istoreutils.c.back +src/istoreutils.c.rej +src/istoreutils.h.back +``` diff --git a/build/bin/gsha256sum b/build/bin/gsha256sum new file mode 100755 index 0000000..cbab322 Binary files /dev/null and b/build/bin/gsha256sum differ diff --git a/build/lib/shared/libglacier-ng.so b/build/lib/shared/libglacier-ng.so new file mode 100755 index 0000000..275d8b1 Binary files /dev/null and b/build/lib/shared/libglacier-ng.so differ diff --git a/build/lib/shared/libglacier_dag.so b/build/lib/shared/libglacier_dag.so index b585dd9..22a98bf 100755 Binary files a/build/lib/shared/libglacier_dag.so and b/build/lib/shared/libglacier_dag.so differ diff --git a/build/lib/shared/libglacier_istoreutils.so b/build/lib/shared/libglacier_istoreutils.so index 890ae47..8182851 100755 Binary files a/build/lib/shared/libglacier_istoreutils.so and b/build/lib/shared/libglacier_istoreutils.so differ diff --git a/build/lib/shared/libglacier_log.so b/build/lib/shared/libglacier_log.so index 7fde155..e6395d1 100755 Binary files a/build/lib/shared/libglacier_log.so and b/build/lib/shared/libglacier_log.so differ diff --git a/build/lib/shared/libglacier_pkg.so b/build/lib/shared/libglacier_pkg.so index 7933c5e..cba673e 100755 Binary files a/build/lib/shared/libglacier_pkg.so and b/build/lib/shared/libglacier_pkg.so differ diff --git a/build/lib/shared/libglacier_transaction.so b/build/lib/shared/libglacier_transaction.so new file mode 100755 index 0000000..ab0fa22 Binary files /dev/null and b/build/lib/shared/libglacier_transaction.so differ diff --git a/build/lib/shared/libglacier_verify.so b/build/lib/shared/libglacier_verify.so new file mode 100755 index 0000000..d7146ba Binary files /dev/null and b/build/lib/shared/libglacier_verify.so differ diff --git a/build/lib/tmp/log.o b/build/lib/tmp/log.o index 2aeab6a..3039c7c 100644 Binary files a/build/lib/tmp/log.o and b/build/lib/tmp/log.o differ diff --git a/build/lib/tmp/make_conf.o b/build/lib/tmp/make_conf.o index 5ea55e9..14f6d64 100644 Binary files a/build/lib/tmp/make_conf.o and b/build/lib/tmp/make_conf.o differ diff --git a/build/lib/tmp/runtime.o b/build/lib/tmp/runtime.o index 0df3025..db54453 100644 Binary files a/build/lib/tmp/runtime.o and b/build/lib/tmp/runtime.o differ diff --git a/build/lib/tmp/sha256.o b/build/lib/tmp/sha256.o new file mode 100644 index 0000000..f118c6c Binary files /dev/null and b/build/lib/tmp/sha256.o differ diff --git a/config.mk b/config.mk index 6658c60..d57e28c 100644 --- a/config.mk +++ b/config.mk @@ -4,6 +4,6 @@ PREFIX ?= /usr -CFLAGS = -std=c11 -pedantic -O2 -flto -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror +CFLAGS = -std=c11 -pedantic -O2 -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror -Wno-format-truncation TEST_DIR = /home/lw/Projects/glacier-ng/lib diff --git a/src/common.h b/src/common.h index 963dc3d..96c29ef 100644 --- a/src/common.h +++ b/src/common.h @@ -1,17 +1,17 @@ #ifndef COMMON_H_ #define COMMON_H_ -#ifndef _POSIX_C_SOURCE #define _POSIX_C_SOURCE 200809L -#endif -#ifndef _DEFAULT_SOURCE -#define _DEFAULT_SOURCE -#endif -#ifndef _XOPEN_SOURCE #define _XOPEN_SOURCE 700 -#endif +#include +#include +#include +#include +#include +#include #include +#include typedef struct { @@ -20,4 +20,57 @@ typedef struct } dir_t; +static inline int +gl_mkdirp(const char *path, mode_t mode) +{ + char tmp[PATH_MAX]; + strncpy(tmp, path, sizeof(tmp)); + tmp[sizeof(tmp) - 1] = '\0'; + + for (char *p = tmp + 1; *p; p++) { + if (*p == '/') { + *p = '\0'; + if (mkdir(tmp, mode) == -1 && errno != EEXIST) { + return -1; + } + *p = '/'; + } + } + + if (mkdir(tmp, mode) == -1 && errno != EEXIST) { + return -1; + } + + return 0; +} + +static inline int +gl_rmdir_recursive(const char *path) +{ + DIR *dir = opendir(path); + if (!dir) { return -1; } + + struct dirent *ent; + while ((ent = readdir(dir))) { + if (ent->d_name[0] == '.') { continue; } + + char full[PATH_MAX]; + snprintf(full, sizeof(full), "%s/%s", path, ent->d_name); + + struct stat st; + if (lstat(full, &st) != 0) { continue; } + + if (S_ISDIR(st.st_mode)) { + gl_rmdir_recursive(full); + } + else { + unlink(full); + } + } + + closedir(dir); + return rmdir(path); +} + + #endif diff --git a/src/gsha256sum.c b/src/gsha256sum.c new file mode 100644 index 0000000..1f26b5b --- /dev/null +++ b/src/gsha256sum.c @@ -0,0 +1,21 @@ +#include + +#include "sha256.h" + +int +main(int argc, char *argv[]) +{ + if (argc != 2) { + fprintf(stderr, "usage: %s FILE\n", argv[0]); + return 1; + } + + char hex[GL_SHA256_HEX_SIZE]; + if (gl_sha256_file(argv[1], hex) != 0) { + perror(argv[1]); + return 1; + } + + printf("sha256:%s\n", hex); + return 0; +} diff --git a/src/istoreutils.c b/src/istoreutils.c index deaf3f2..3cb2409 100644 --- a/src/istoreutils.c +++ b/src/istoreutils.c @@ -21,6 +21,7 @@ #include "istoreutils.h" #include "log.h" #include "pkg.h" +#include "transaction.h" static const dir_t usr_index = { "/glacier/usr/index", 0700 }; static const dir_t sys_index = { "/glacier/sys/index", 0700 }; @@ -29,6 +30,17 @@ static const dir_t usr_store = { "/glacier/usr/store", 0700 }; static const dir_t sys_store = { "/glacier/sys/store", 0700 }; static const dir_t usr_links = { "/glacier/usr/links", 0700 }; +static const dir_t sys_links = { "/glacier/sys/links", 0700 }; + +/* 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" +#define GL_BASE_LINKS_DEST "/usr" bool gl_usr_istore_exists(index_store_t index_or_store, int uid) @@ -115,6 +127,36 @@ create_index(const char *ind_path, uid_t uid) 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'; + return strncmp(target, store_prefix, strlen(store_prefix)) == 0; +} + + int gl_init_user(uid_t uid, bool isVerbose) { @@ -253,6 +295,46 @@ gl_init_user(uid_t uid, bool isVerbose) return 0; } +int +gl_init_sys(bool isVerbose) +{ + const char *paths[] = { + sys_index.path, + sys_store.path, + sys_links.path, + }; + mode_t modes[] = { + sys_index.mode, + sys_store.mode, + sys_links.mode, + }; + + for (int i = 0; i < 3; i++) { + if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); } + + if (mkdir(paths[i], modes[i]) == -1) { + 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; + } + } + } + + return 0; +} + static void trim(char *s) { @@ -313,17 +395,34 @@ gl_parse_index(struct gindex *idx, FILE *f) char *first = strstr(line, "::"); if (!first) { continue; } - *first = 0; + char *repo = line; - char *pkg = first + 2; + char *rest = first + 2; + + char *second = strstr(rest, "::"); + char *pkg; + char *ver; + if (second) { + *second = 0; + pkg = rest; + ver = second + 2; + } else { + /* Older index files may only have repo::pkg with no + * version field — tolerate that rather than reject it. */ + pkg = rest; + ver = "unknown"; + } + trim(repo); trim(pkg); + trim(ver); struct gpkg_entry ent; ent.repo = dupstr(repo); ent.pkg = dupstr(pkg); - if (!ent.repo || !ent.pkg) { return -1; } + ent.ver = dupstr(ver); + if (!ent.repo || !ent.pkg || !ent.ver) { return -1; } struct gpkg_entry *tmp = realloc(idx->entries, sizeof(*idx->entries) * (idx->count + 1)); @@ -341,80 +440,19 @@ gl_free_index(struct gindex *idx) for (size_t i = 0; i < idx->count; i++) { free(idx->entries[i].repo); free(idx->entries[i].pkg); + free(idx->entries[i].ver); } free(idx->entries); idx->entries = NULL; idx->count = 0; } -/* int -gl_rebuild_index(uid_t uid, const char *out_dir) +gl_rebuild_index(gl_context_t *ctx) { - char store_path[PATH_MAX]; - snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid); + if (!ctx) { return -1; } - DIR *store_dir = opendir(store_path); - if (!store_dir) return -1; - - FILE *out = fopen(out_dir, "w"); - if (!out) 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", - 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; - } - - fprintf(out, "%s::%s\n", - repo_ent->d_name, - pkg_ent->d_name); - listed++; - } - closedir(repo_dir); - } - - rewind(out); - fprintf(out, "uid = %d\n", uid); - - closedir(store_dir); - fclose(out); - return 0; -} -*/ - -int -gl_rebuild_index(uid_t uid, const char *out_dir) -{ - char store_path[PATH_MAX]; - snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid); - - DIR *store_dir = opendir(store_path); + DIR *store_dir = opendir(ctx->store_path); if (!store_dir) return -1; /* Write to a new timestamped index file */ @@ -422,7 +460,8 @@ gl_rebuild_index(uid_t uid, const char *out_dir) make_index_name(fname, sizeof(fname)); char new_index_path[PATH_MAX]; - snprintf(new_index_path, sizeof(new_index_path), "%s/%s", out_dir, fname); + snprintf(new_index_path, sizeof(new_index_path), "%s/%s", + ctx->index_path, fname); FILE *out = fopen(new_index_path, "w"); if (!out) { @@ -438,7 +477,7 @@ gl_rebuild_index(uid_t uid, const char *out_dir) char repo_path[PATH_MAX]; snprintf(repo_path, sizeof(repo_path), "%s/%s", - store_path, repo_ent->d_name); + ctx->store_path, repo_ent->d_name); struct stat rst; if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) { @@ -461,9 +500,33 @@ gl_rebuild_index(uid_t uid, const char *out_dir) continue; } - fprintf(out, "%s::%s\n", + /* 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"; + 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); + } + break; + } + closedir(ver_dir); + } + + fprintf(out, "%s::%s::%s\n", repo_ent->d_name, - pkg_ent->d_name); + pkg_ent->d_name, + pkg_ver); listed++; } closedir(repo_dir); @@ -476,14 +539,15 @@ gl_rebuild_index(uid_t uid, const char *out_dir) if (!tmp_in) return -1; char tmp_path[PATH_MAX]; - snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX", out_dir); + 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; } FILE *tmp_out = fdopen(tmp_fd, "w"); if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; } - fprintf(tmp_out, "uid = %d\n", uid); + fprintf(tmp_out, "uid = %d\n", ctx->uid); fprintf(tmp_out, "listed = %zu\n\n", listed); char line[512]; @@ -494,15 +558,15 @@ gl_rebuild_index(uid_t uid, const char *out_dir) fclose(tmp_out); rename(tmp_path, new_index_path); - chown(new_index_path, uid, (gid_t)-1); + 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", out_dir); + 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, uid, (gid_t)-1); + lchown(link_path, ctx->uid, (gid_t)-1); return 0; } @@ -672,62 +736,14 @@ gl_backup_istore(const char *tar_path, } static int -gl_mkdirp(const char *path, mode_t mode) -{ - char tmp[PATH_MAX]; - strncpy(tmp, path, sizeof(tmp)); - tmp[sizeof(tmp) - 1] = '\0'; - - for (char *p = tmp + 1; *p; p++) { - if (*p == '/') { - *p = '\0'; - if (mkdir(tmp, mode) == -1 && errno != EEXIST) { - return -1; - } - *p = '/'; - } - } - - if (mkdir(tmp, mode) == -1 && errno != EEXIST) { - return -1; - } - - return 0; -} - -int -gl_rmdir_recursive(const char *path) -{ - DIR *dir = opendir(path); - if (!dir) { return -1; } - - struct dirent *ent; - while ((ent = readdir(dir))) { - if (ent->d_name[0] == '.') { continue; } - - char full[PATH_MAX]; - snprintf(full, sizeof(full), "%s/%s", path, ent->d_name); - - struct stat st; - if (lstat(full, &st) != 0) { continue; } - - if (S_ISDIR(st.st_mode)) { - gl_rmdir_recursive(full); - } - else { - unlink(full); - } - } - - closedir(dir); - return rmdir(path); -} - -static int -gl_unlink_pkg(const char *pkg_store_final, uid_t uid) +gl_unlink_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo) { char links_base[PATH_MAX]; - snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid); + if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) { + snprintf(links_base, sizeof(links_base), "%s", GL_BASE_LINKS_DEST); + } else { + snprintf(links_base, sizeof(links_base), "%s", ctx->links_path); + } size_t strip_len = strlen(pkg_store_final); @@ -781,12 +797,14 @@ gl_unlink_pkg(const char *pkg_store_final, uid_t uid) } gl_remove_status_t -gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid) +gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo) { + if (!ctx) { 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/%d/%s/%s", usr_store.path, uid, pkg_repo, pkg_name); + "%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)) { @@ -811,9 +829,14 @@ gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid) return GL_REMOVE_ERR_NOT_FOUND; } - /* remove symlinks from links tree */ - if (gl_unlink_pkg(pkg_store_final, uid) != 0) { - return GL_REMOVE_ERR_UNLINK; + /* remove symlinks from links tree. GL_CTX_STAGE: skip — the + * stage links tree isn't seeded (links are fully derived from + * the store post-commit by relink_store), so there's nothing + * there to unlink. */ + if (ctx->mode == GL_CTX_LIVE) { + if (gl_unlink_pkg(ctx, pkg_store_final, pkg_repo) != 0) { + return GL_REMOVE_ERR_UNLINK; + } } /* remove package from store */ @@ -822,10 +845,7 @@ gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid) } /* rebuild index */ - char index_dir[PATH_MAX]; - snprintf(index_dir, sizeof(index_dir), "%s/%d", usr_index.path, uid); - - if (gl_rebuild_index(uid, index_dir) != 0) { + if (gl_rebuild_index(ctx) != 0) { return GL_REMOVE_ERR_INDEX; } @@ -833,10 +853,14 @@ gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid) } static int -gl_link_pkg(const char *pkg_store_final, uid_t uid) +gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo) { char links_base[PATH_MAX]; - snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid); + if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) { + snprintf(links_base, sizeof(links_base), "%s", GL_BASE_LINKS_DEST); + } else { + snprintf(links_base, sizeof(links_base), "%s", ctx->links_path); + } size_t strip_len = strlen(pkg_store_final); @@ -895,18 +919,29 @@ gl_link_pkg(const char *pkg_store_final, uid_t uid) gl_mkdirp(parent, 0700); } - /* remove existing symlink if present */ - unlink(link_path); + /* Only ever replace an existing entry if it's a + * symlink we created ourselves. See + * is_glacier_symlink's comment for why this + * check exists at all. */ + struct stat existing_st; + if (lstat(link_path, &existing_st) == 0 && + !is_glacier_symlink(link_path, ctx->store_path)) { + lg_printf(1, + "Refusing to overwrite non-glacier path: %s", + link_path); + } else { + unlink(link_path); - /* symlink store path -> links tree */ - if (symlink(full_path, link_path) != 0) { - closedir(dir); - free(current); - while (top > 0) { free(stack[--top]); } - return -1; + /* symlink store path -> links tree */ + if (symlink(full_path, link_path) != 0) { + closedir(dir); + free(current); + while (top > 0) { free(stack[--top]); } + return -1; + } + + lchown(link_path, ctx->uid, (gid_t)-1); } - - lchown(link_path, uid, (gid_t)-1); } } @@ -949,6 +984,78 @@ gl_delete_user(uid_t uid, bool isVerbose) return 0; } +char +*gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name) +{ + 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 NULL; } + + 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 *repo = strdup(repo_entry->d_name); + closedir(uid_dir); + return repo; + } + } + + closedir(uid_dir); + return NULL; +} + +gl_repo_list_t +gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name) +{ + gl_repo_list_t result = { NULL, 0 }; + + 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; +} + gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid) { @@ -1011,8 +1118,9 @@ gl_restore_istore(const char *tar_path, const char *glacier_root, const char *ui } gl_install_status_t -gl_install_pkg(const char *gpkg_path, uid_t uid) +gl_install_pkg(gl_context_t *ctx, const char *gpkg_path) { + if (!ctx) { return GL_INSTALL_ERR_OPEN; } char original_cwd[PATH_MAX]; if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) { return GL_INSTALL_ERR_OPEN; @@ -1086,7 +1194,7 @@ gl_install_pkg(const char *gpkg_path, uid_t uid) /* construct the directory for the package in the store */ snprintf(pkg_store_base, sizeof(pkg_store_base), - "%s/%d/%s/%s", usr_store.path, uid, pkg.pkg_repo, pkg.pkg_name); + "%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, @@ -1186,15 +1294,13 @@ gl_install_pkg(const char *gpkg_path, uid_t uid) archive_read_free(a2); /* now the index will be updated */ - - /* now the index will be updated */ - char index_dir[PATH_MAX]; - snprintf(index_dir, sizeof(index_dir), - "%s/%d", usr_index.path, uid); - - /* Resolve "current" symlink to find the actual index file to append to */ + /* 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", index_dir); + 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); @@ -1204,31 +1310,148 @@ gl_install_pkg(const char *gpkg_path, uid_t uid) } current_target[len] = '\0'; - char index_path[PATH_MAX]; - snprintf(index_path, sizeof(index_path), "%s/%s", index_dir, current_target); - - FILE *idx = fopen(index_path, "a"); - if (!idx) { + if (gl_rebuild_index(ctx) != 0) { gl_free_pkg(&pkg); return GL_INSTALL_ERR_INDEX; } - fprintf(idx, "%s::%s\n", pkg.pkg_repo, pkg.pkg_name); - fclose(idx); - - if (gl_rebuild_index(uid, index_dir) != 0) { - gl_free_pkg(&pkg); - return GL_INSTALL_ERR_INDEX; - } - - if (gl_link_pkg(pkg_store_final, uid) != 0) { - gl_free_pkg(&pkg); - return GL_INSTALL_ERR_EXTRACT; + 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); chdir(original_cwd); 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 char *dir_path, const char *store_prefix) +{ + 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 */ + } + struct stat target_st; + if (stat(full, &target_st) != 0 && errno == ENOENT) { + /* our symlink, but its target is gone */ + unlink(full); + } + } else if (S_ISDIR(lst.st_mode)) { + prune_stale_links(full, store_prefix); + } + /* 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, 0700) != 0) { + return -1; + } + prune_stale_links(ctx->links_path, ctx->store_path); + + /* 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) { + prune_stale_links(GL_BASE_LINKS_DEST, ctx->store_path); + } + + 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; +} \ No newline at end of file diff --git a/src/istoreutils.c.back b/src/istoreutils.c.back new file mode 100644 index 0000000..5d52e76 --- /dev/null +++ b/src/istoreutils.c.back @@ -0,0 +1,1365 @@ +#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 "istoreutils.h" +#include "log.h" +#include "pkg.h" +#include "transaction.h" + +static const dir_t usr_index = { "/glacier/usr/index", 0700 }; +static const dir_t sys_index = { "/glacier/sys/index", 0700 }; + +static const dir_t usr_store = { "/glacier/usr/store", 0700 }; +static const dir_t sys_store = { "/glacier/sys/store", 0700 }; + +static const dir_t usr_links = { "/glacier/usr/links", 0700 }; +static const dir_t sys_links = { "/glacier/sys/links", 0700 }; + +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), "%s/%d", usr_index.path, uid); + break; + case STORE: + snprintf(path, sizeof(path), "%s/%d", usr_store.path, 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); +} + +bool +gl_sys_istore_exists(index_store_t index_or_store) +{ + char path[PATH_MAX]; + struct stat st; + + switch (index_or_store) { + case INDEX: + snprintf(path, sizeof(path), "%s", sys_index.path); + break; + case STORE: + snprintf(path, sizeof(path), "%s", sys_store.path); + break; + default: + printf("libglacier: argument index_or_store of function gl_sys_istore_exists must be INDEX or STORE\n"); + return false; + } + + if (stat(path, &st) != 0) { + return false; + } + + return S_ISDIR(st.st_mode); +} + +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", tm); +} + +static int +create_index(const char *ind_path, uid_t uid) +{ + 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); + + 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); +} + + +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; + } + + if (isVerbose) { lg_printf(0, "Constructing paths for index and store..."); } + + char ind_path[PATH_MAX]; + char sto_path[PATH_MAX]; + char lnk_path[PATH_MAX]; + + snprintf(ind_path, sizeof(ind_path), "%s/%d", usr_index.path, uid); + snprintf(sto_path, sizeof(sto_path), "%s/%d", usr_store.path, uid); + snprintf(lnk_path, sizeof(lnk_path), "%s/%d", usr_links.path, uid); + + if (isVerbose) { lg_printf(0, "Paths constructed:\n%s\n%s", ind_path, sto_path); } + + if (isVerbose) { lg_printf(0, "Attempting to create index directory at %s", ind_path); } + if (mkdir(ind_path, 0700) == -1) { + switch (errno) { + case EACCES: + lg_printf(2, "Cannot create index: permission denied"); + return 1; + break; + case EEXIST: + lg_printf(2, "Cannot create index: directory already exists"); + return 1; + break; + case ENOENT: + /* should never happen, but here for completeness */ + lg_printf(2, "Cannot create index: parent directory does not exist"); + return 1; + break; + case ENOSPC: + lg_printf(2, "Cannot create index: no space left on device"); + return 1; + break; + case EROFS: + lg_printf(2, "Cannot create index: read only filesystem"); + return 1; + break; + default: + printf("libglacier: mkdir error in function gl_init_user\n"); + return -1; + break; + } + } + + if (isVerbose) { lg_printf(0, "Attempting to create store directory at %s", sto_path); } + if (mkdir(sto_path, 0700) == -1) { + switch (errno) { + case EACCES: + lg_printf(2, "Cannot create store: permission denied"); + return 1; + break; + case EEXIST: + lg_printf(2, "Cannot create store: directory already exists"); + return 1; + break; + case ENOENT: + /* should never happen, but here for completeness */ + lg_printf(2, "Cannot create store: parent directory does not exist"); + return 1; + break; + case ENOSPC: + lg_printf(2, "Cannot create store: no space left on device"); + return 1; + break; + case EROFS: + lg_printf(2, "Cannot create store: read only filesystem"); + return 1; + break; + default: + printf("libglacier: mkdir error in function gl_init_user\n"); + return -1; + break; + } + } + + if (isVerbose) { lg_printf(0, "Attempting to create links directory at %s", lnk_path); } + if (mkdir(lnk_path, 0700) == -1) { + switch (errno) { + case EACCES: + lg_printf(2, "Cannot create store: permission denied"); + return 1; + break; + case EEXIST: + lg_printf(2, "Cannot create store: directory already exists"); + return 1; + break; + case ENOENT: + /* should never happen, but here for completeness */ + lg_printf(2, "Cannot create store: parent directory does not exist"); + return 1; + break; + case ENOSPC: + lg_printf(2, "Cannot create store: no space left on device"); + return 1; + break; + case EROFS: + lg_printf(2, "Cannot create store: read only filesystem"); + return 1; + break; + default: + printf("libglacier: mkdir error in function gl_init_user\n"); + return -1; + break; + } + } + + if (isVerbose) { lg_printf(0, "Changing ownership of index and store to UID %i", uid); } + + if (chown(sto_path, uid, (gid_t)-1) != 0) { + return 1; + } + + if (chown(ind_path, uid, (gid_t)-1) != 0) { + return 1; + } + + if (chown(lnk_path, uid, (gid_t)-1) != 0) { + return 1; + } + + if (create_index(ind_path, uid) != 0) { + /* No switch for errno. chown() shouldn't fail if mkdir() didn't */ + lg_printf(2, "Cannot create initial index file"); + return 1; + } + return 0; +} + +int +gl_init_sys(bool isVerbose) +{ + const char *paths[] = { + sys_index.path, + sys_store.path, + sys_links.path, + }; + mode_t modes[] = { + sys_index.mode, + sys_store.mode, + sys_links.mode, + }; + + for (int i = 0; i < 3; i++) { + if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); } + + if (mkdir(paths[i], modes[i]) == -1) { + 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; + } + } + } + + return 0; +} + +static void +trim(char *s) +{ + char *p = s; + while (*p && isspace((unsigned char)*p)) { + p++; + } + + if (p != s) { + memmove(s, p, strlen(p) + 1); + } + + size_t len = strlen(s); + while (len && isspace((unsigned char)s[len - 1])) { + s[--len] = 0; + } +} + +static char +*dupstr(const char *s) +{ + size_t n = strlen(s); + char *p = malloc(n + 1); + if (!p) { + return NULL; + } + + memcpy(p, s, n + 1); + return p; +} + +int +gl_parse_index(struct gindex *idx, FILE *f) +{ + idx->uid = -1; + idx->listed = -1; + idx->entries = NULL; + idx->count = 0; + + char line[512]; + + while (fgets(line, sizeof(line), f)) { + trim(line); + if (!line[0] || line[0] == '#') { continue; } + + char *eq = strchr(line, '='); + if (eq) { + *eq = 0; + char *key = line; + char *val = eq + 1; + trim(key); + trim(val); + + if (strcmp(key, "uid") == 0) { idx->uid = atoi(val); } + else if (strcmp(key, "listed") == 0) { idx->listed=atoi(val); } + continue; + } + + char *first = strstr(line, "::"); + if (!first) { continue; } + + *first = 0; + char *repo = line; + char *pkg = first + 2; + trim(repo); + trim(pkg); + + struct gpkg_entry ent; + ent.repo = dupstr(repo); + ent.pkg = dupstr(pkg); + if (!ent.repo || !ent.pkg) { return -1; } + + struct gpkg_entry *tmp = + realloc(idx->entries, sizeof(*idx->entries) * (idx->count + 1)); + if (!tmp) { return -1; } + idx->entries = tmp; + idx->entries[idx->count++] = ent; + } + + return 0; +} + +void +gl_free_index(struct gindex *idx) +{ + for (size_t i = 0; i < idx->count; i++) { + free(idx->entries[i].repo); + free(idx->entries[i].pkg); + } + + free(idx->entries); + idx->entries = NULL; + idx->count = 0; +} +/* +int +gl_rebuild_index(uid_t uid, const char *out_dir) +{ + char store_path[PATH_MAX]; + snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid); + + DIR *store_dir = opendir(store_path); + if (!store_dir) return -1; + + FILE *out = fopen(out_dir, "w"); + if (!out) 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", + 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; + } + + fprintf(out, "%s::%s\n", + repo_ent->d_name, + pkg_ent->d_name); + listed++; + } + closedir(repo_dir); + } + + rewind(out); + fprintf(out, "uid = %d\n", uid); + + closedir(store_dir); + fclose(out); + 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); + + FILE *out = fopen(new_index_path, "w"); + 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; + } + + fprintf(out, "%s::%s\n", + repo_ent->d_name, + pkg_ent->d_name); + listed++; + } + closedir(repo_dir); + } + closedir(store_dir); + fclose(out); + + /* Prepend header by copying through a temp file then renaming over */ + FILE *tmp_in = fopen(new_index_path, "r"); + if (!tmp_in) return -1; + + 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; } + + 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; +} + +static int +add_to_archive(struct archive *a, const char *real_path, const char *archive_path) +{ + struct stat st; + + if (lstat(real_path, &st) != 0) { + return -1; + } + + if (S_ISDIR(st.st_mode)) { + struct archive_entry *entry = archive_entry_new(); + archive_entry_set_pathname(entry, archive_path); + archive_entry_set_filetype(entry, AE_IFDIR); + archive_entry_set_perm(entry, st.st_mode & 0777); + + if (archive_write_header(a, entry) != ARCHIVE_OK) { + archive_entry_free(entry); + return -1; + } + + archive_entry_free(entry); + + DIR *dir = opendir(real_path); /* NEW */ + if (!dir) { + return -1; + } + + struct dirent *ent; + while ((ent = readdir(dir))) { /* NEW */ + if (strcmp(ent->d_name, ".") == 0 || + strcmp(ent->d_name, "..") == 0) { + continue; + } + + char child_real[PATH_MAX]; /* NEW */ + char child_arch[PATH_MAX]; /* NEW */ + + snprintf(child_real, sizeof(child_real), + "%s/%s", real_path, ent->d_name); + + snprintf(child_arch, sizeof(child_arch), + "%s/%s", archive_path, ent->d_name); + + if (add_to_archive(a, child_real, child_arch) != 0) { + closedir(dir); + return -1; + } + } + + closedir(dir); + return 0; + } + + /* existing file handling logic continues below */ + + if (S_ISREG(st.st_mode)) { + + int fd = open(real_path, O_RDONLY); + if (fd < 0) { + return -1; + } + + struct archive_entry *entry = archive_entry_new(); + archive_entry_set_pathname(entry, archive_path); + archive_entry_set_size(entry, st.st_size); + archive_entry_set_filetype(entry, AE_IFREG); + archive_entry_set_perm(entry, st.st_mode & 0777); + + if (archive_write_header(a, entry) != ARCHIVE_OK) { + archive_entry_free(entry); + close(fd); + return -1; + } + + char buf[8192]; + ssize_t n; + + while ((n = read(fd, buf, sizeof(buf))) > 0) { + if (archive_write_data(a, buf, (size_t)n) < 0) { + archive_entry_free(entry); + close(fd); + return -1; + } + } + + archive_entry_free(entry); + close(fd); + + if (n < 0) { + return -1; + } + + return 0; + } + + return 0; /* NEW: ignore other file types */ +} + +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/index/%s", glacier_root, uid); + snprintf(store_path, sizeof(store_path), "%s/store/%s", glacier_root, uid); + snprintf(links_path, sizeof(links_path), "%s/links/%s", 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; +} + +static int +gl_unlink_pkg(gl_context_t *ctx, const char *pkg_store_final) +{ + char links_base[PATH_MAX]; + strncpy(links_base, ctx->links_path, PATH_MAX - 1); + + size_t strip_len = strlen(pkg_store_final); + + 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; } + const char *rel = full_path + strip_len; + if (rel[0] == '/') { rel++; } + + 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)) { + if (top < 4096) { + stack[top++] = strdup(full_path); + } + } + else if (S_ISREG(st.st_mode)) { + unlink(link_path); + } + } + + closedir(dir); + free(current); + } + + return 0; +} + +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; } + + /* 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; + } + + /* 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 symlinks from links tree. GL_CTX_STAGE: skip — the + * stage links tree isn't seeded (links are fully derived from + * the store post-commit by relink_store), so there's nothing + * there to unlink. */ + if (ctx->mode == GL_CTX_LIVE) { + if (gl_unlink_pkg(ctx, pkg_store_final) != 0) { + return GL_REMOVE_ERR_UNLINK; + } + } + + /* 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; + } + + return GL_REMOVE_OK; +} + +static int +gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final) +{ + char links_base[PATH_MAX]; + strncpy(links_base, ctx->links_path, PATH_MAX - 1); + + size_t strip_len = strlen(pkg_store_final); + + /* 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++; } + + /* 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)) { + /* create the directory in the links tree */ + gl_mkdirp(link_path, 0700); + + /* push onto stack to recurse into */ + if (top < 4096) { + stack[top++] = strdup(full_path); + } + } else if (S_ISREG(st.st_mode)) { + /* 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, 0700); + } + + /* remove existing symlink if present */ + unlink(link_path); + + /* symlink store path -> links tree */ + if (symlink(full_path, link_path) != 0) { + closedir(dir); + free(current); + while (top > 0) { free(stack[--top]); } + return -1; + } + + lchown(link_path, ctx->uid, (gid_t)-1); + } + } + + closedir(dir); + free(current); + } + + return 0; +} + +int +gl_delete_user(uid_t uid, bool isVerbose) +{ + char ind_path[PATH_MAX]; + char sto_path[PATH_MAX]; + char lnk_path[PATH_MAX]; + + snprintf(ind_path, sizeof(ind_path), "%s/%d", usr_index.path, uid); + snprintf(sto_path, sizeof(sto_path), "%s/%d", usr_store.path, uid); + snprintf(lnk_path, sizeof(lnk_path), "%s/%d", usr_links.path, uid); + + if (isVerbose) { lg_printf(0, "Removing Glacier directories for UID %d...", uid); } + + if (gl_rmdir_recursive(ind_path) != 0) { + lg_printf(2, "Failed to remove index for UID %d", uid); + return 1; + } + + if (gl_rmdir_recursive(sto_path) != 0) { + lg_printf(2, "Failed to remove store for UID %d", uid); + return 1; + } + + if (gl_rmdir_recursive(lnk_path) != 0) { + lg_printf(2, "Failed to remove links for UID %d", uid); + return 1; + } + + lg_printf(3, "Successfully removed Glacier directories for UID %d.", uid); + return 0; +} + +char +*gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name) +{ + char store_path[PATH_MAX]; + strncpy(store_path, ctx->store_path, PATH_MAX - 1); + + DIR *uid_dir = opendir(store_path); + if (!uid_dir) { return NULL; } + + 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 *repo = strdup(repo_entry->d_name); + closedir(uid_dir); + return repo; + } + } + + closedir(uid_dir); + return NULL; +} + +gl_repo_list_t +gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name) +{ + gl_repo_list_t result = { NULL, 0 }; + + char store_path[PATH_MAX]; + strncpy(store_path, ctx->store_path, PATH_MAX - 1); + + 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; +} + +gl_restore_status_t +gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid) +{ + struct archive *a = archive_read_new(); + archive_read_support_filter_gzip(a); + archive_read_support_format_tar(a); + + if (archive_read_open_filename(a, tar_path, 10240) != ARCHIVE_OK) { + archive_read_free(a); + return GL_RESTORE_ERR_OPEN; + } + + struct archive_entry *entry; + while (archive_read_next_header(a, &entry) == ARCHIVE_OK) { + const char *rel = archive_entry_pathname(entry); + + char dest[PATH_MAX]; + snprintf(dest, sizeof(dest), "%s/%s/%s", + glacier_root, uid, rel); + + archive_entry_set_pathname(entry, dest); + + if (archive_entry_filetype(entry) == AE_IFDIR) { + gl_mkdirp(dest, archive_entry_perm(entry)); + archive_read_data_skip(a); + continue; + } + + /* create parent dirs */ + char parent[PATH_MAX]; + strncpy(parent, dest, sizeof(parent)); + char *slash = strrchr(parent, '/'); + if (slash) { + *slash = '\0'; + gl_mkdirp(parent, 0700); + } + + int fd = open(dest, O_WRONLY | O_CREAT | O_TRUNC, + archive_entry_perm(entry)); + if (fd < 0) { + archive_read_free(a); + return GL_RESTORE_ERR_EXTRACT; + } + + char buf[8192]; + ssize_t n; + while ((n = archive_read_data(a, buf, sizeof(buf))) > 0) { + ssize_t written = write(fd, buf, (size_t)n); + if (written < 0 || written != n) { + close(fd); + archive_read_free(a); + return GL_RESTORE_ERR_EXTRACT; + } + } + close(fd); + } + + archive_read_free(a); + return GL_RESTORE_OK; +} + +gl_install_status_t +gl_install_pkg(gl_context_t *ctx, const char *gpkg_path) +{ + if (!ctx) { return GL_INSTALL_ERR_OPEN; } + char original_cwd[PATH_MAX]; + if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) { + return GL_INSTALL_ERR_OPEN; + } + + /* The archive is opened and the manifest is parsed here */ + 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; + gl_pkg_t pkg = {0}; + 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); + + if (gl_gpm2gpkg(tmp_manifest, &pkg) != 0) { + 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; + } + + 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); + + if (gl_mkdirp(pkg_store_base, 0700) == -1) { + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_EXTRACT; + } + + if (mkdir(pkg_store_final, 0700) == -1) { + if (errno == EEXIST) { + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_ALREADY_INSTALLED; + } + + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_EXTRACT; + } + + /* now the archive must be reopened in order to extract package files */ + 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); + + while (archive_read_next_header(a2, &entry) == ARCHIVE_OK) { + const char *entry_path = archive_entry_pathname(entry); + + 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; + } + + char dest_path[PATH_MAX]; + snprintf(dest_path, sizeof(dest_path), "%s/%s", + pkg_store_final, rel_path); + + if (archive_entry_filetype(entry) == AE_IFDIR) { + if (mkdir(dest_path, archive_entry_perm(entry)) == -1 + && errno != EEXIST) { + archive_read_free(a2); + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_EXTRACT; + } + } + else if (archive_entry_filetype(entry) == AE_IFREG) { + char parent[PATH_MAX]; + strncpy(parent, dest_path, sizeof(parent)); + char *slash = strrchr(parent, '/'); + if (slash) { + *slash = '\0'; + gl_mkdirp(parent, 0700); + } + + int fd = open(dest_path, O_WRONLY | O_CREAT | O_TRUNC, + archive_entry_perm(entry)); + + if (fd < 0) { + archive_read_free(a2); + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_EXTRACT; + } + + 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); + archive_read_free(a2); + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_EXTRACT; + } + } + + close(fd); + } + } + + archive_read_free(a2); + + /* now the index will be updated */ + /* Resolve "current" symlink to find the actual index file to append to */ + 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'; + + char index_path[PATH_MAX]; + snprintf(index_path, sizeof(index_path), "%s/%s", + ctx->index_path, current_target); + + FILE *idx = fopen(index_path, "a"); + if (!idx) { + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_INDEX; + } + + fprintf(idx, "%s::%s\n", pkg.pkg_repo, pkg.pkg_name); + fclose(idx); + + if (gl_rebuild_index(ctx) != 0) { + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_INDEX; + } + + if (ctx->mode == GL_CTX_LIVE) { + if (gl_link_pkg(ctx, pkg_store_final) != 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); + chdir(original_cwd); + return GL_INSTALL_OK; +} + +int +gl_relink_store(gl_context_t *ctx) +{ + 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) != 0) { + closedir(ver_dir); closedir(name_dir); + closedir(repo_dir); + return -1; + } + } + closedir(ver_dir); + } + closedir(name_dir); + } + closedir(repo_dir); + return 0; +} \ No newline at end of file diff --git a/src/istoreutils.c.rej b/src/istoreutils.c.rej new file mode 100644 index 0000000..617ab7f --- /dev/null +++ b/src/istoreutils.c.rej @@ -0,0 +1,302 @@ +--- istoreutils.c.orig 2026-06-01 19:43:00.493773073 +0000 ++++ istoreutils.c.new 2026-06-01 19:43:00.573469330 +0000 +@@ -21,6 +21,7 @@ + #include "istoreutils.h" + #include "log.h" + #include "pkg.h" ++#include "transaction.h" + + static const dir_t usr_index = { "/glacier/usr/index", 0700 }; + static const dir_t sys_index = { "/glacier/sys/index", 0700 }; +@@ -29,6 +30,7 @@ + static const dir_t sys_store = { "/glacier/sys/store", 0700 }; + + static const dir_t usr_links = { "/glacier/usr/links", 0700 }; ++static const dir_t sys_links = { "/glacier/sys/links", 0700 }; + + bool + gl_usr_istore_exists(index_store_t index_or_store, int uid) +@@ -253,6 +255,46 @@ + return 0; + } + ++int ++gl_init_sys(bool isVerbose) ++{ ++ const char *paths[] = { ++ sys_index.path, ++ sys_store.path, ++ sys_links.path, ++ }; ++ mode_t modes[] = { ++ sys_index.mode, ++ sys_store.mode, ++ sys_links.mode, ++ }; ++ ++ for (int i = 0; i < 3; i++) { ++ if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); } ++ ++ if (mkdir(paths[i], modes[i]) == -1) { ++ 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; ++ } ++ } ++ } ++ ++ return 0; ++} ++ + static void + trim(char *s) + { +@@ -409,12 +451,11 @@ + */ + + int +-gl_rebuild_index(uid_t uid, const char *out_dir) ++gl_rebuild_index(gl_context_t *ctx) + { +- char store_path[PATH_MAX]; +- snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid); ++ if (!ctx) { return -1; } + +- DIR *store_dir = opendir(store_path); ++ DIR *store_dir = opendir(ctx->store_path); + if (!store_dir) return -1; + + /* Write to a new timestamped index file */ +@@ -422,7 +463,8 @@ + make_index_name(fname, sizeof(fname)); + + char new_index_path[PATH_MAX]; +- snprintf(new_index_path, sizeof(new_index_path), "%s/%s", out_dir, fname); ++ snprintf(new_index_path, sizeof(new_index_path), "%s/%s", ++ ctx->index_path, fname); + + FILE *out = fopen(new_index_path, "w"); + if (!out) { +@@ -438,7 +480,7 @@ + + char repo_path[PATH_MAX]; + snprintf(repo_path, sizeof(repo_path), "%s/%s", +- store_path, repo_ent->d_name); ++ ctx->store_path, repo_ent->d_name); + + struct stat rst; + if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) { +@@ -476,14 +518,15 @@ + if (!tmp_in) return -1; + + char tmp_path[PATH_MAX]; +- snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX", out_dir); ++ 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; } + + FILE *tmp_out = fdopen(tmp_fd, "w"); + if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; } + +- fprintf(tmp_out, "uid = %d\n", uid); ++ fprintf(tmp_out, "uid = %d\n", ctx->uid); + fprintf(tmp_out, "listed = %zu\n\n", listed); + + char line[512]; +@@ -494,15 +537,15 @@ + fclose(tmp_out); + + rename(tmp_path, new_index_path); +- chown(new_index_path, uid, (gid_t)-1); ++ 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", out_dir); ++ 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, uid, (gid_t)-1); ++ lchown(link_path, ctx->uid, (gid_t)-1); + + return 0; + } +@@ -672,10 +715,10 @@ + } + + static int +-gl_unlink_pkg(const char *pkg_store_final, uid_t uid) ++gl_unlink_pkg(gl_context_t *ctx, const char *pkg_store_final) + { + char links_base[PATH_MAX]; +- snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid); ++ strncpy(links_base, ctx->links_path, PATH_MAX - 1); + + size_t strip_len = strlen(pkg_store_final); + +@@ -729,12 +772,14 @@ + } + + gl_remove_status_t +-gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid) ++gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo) + { ++ if (!ctx) { 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/%d/%s/%s", usr_store.path, uid, pkg_repo, pkg_name); ++ "%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)) { +@@ -760,7 +805,7 @@ + } + + /* remove symlinks from links tree */ +- if (gl_unlink_pkg(pkg_store_final, uid) != 0) { ++ if (gl_unlink_pkg(ctx, pkg_store_final) != 0) { + return GL_REMOVE_ERR_UNLINK; + } + +@@ -770,10 +815,7 @@ + } + + /* rebuild index */ +- char index_dir[PATH_MAX]; +- snprintf(index_dir, sizeof(index_dir), "%s/%d", usr_index.path, uid); +- +- if (gl_rebuild_index(uid, index_dir) != 0) { ++ if (gl_rebuild_index(ctx) != 0) { + return GL_REMOVE_ERR_INDEX; + } + +@@ -781,10 +823,10 @@ + } + + static int +-gl_link_pkg(const char *pkg_store_final, uid_t uid) ++gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final) + { + char links_base[PATH_MAX]; +- snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid); ++ strncpy(links_base, ctx->links_path, PATH_MAX - 1); + + size_t strip_len = strlen(pkg_store_final); + +@@ -854,7 +896,7 @@ + return -1; + } + +- lchown(link_path, uid, (gid_t)-1); ++ lchown(link_path, ctx->uid, (gid_t)-1); + } + } + +@@ -898,11 +940,10 @@ + } + + char +-*gl_find_pkg_repo(const char *pkg_name, uid_t uid) ++*gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name) + { + char store_path[PATH_MAX]; +- snprintf(store_path, sizeof(store_path), +-"/glacier/usr/store/%u", uid); ++ strncpy(store_path, ctx->store_path, PATH_MAX - 1); + + DIR *uid_dir = opendir(store_path); + if (!uid_dir) { return NULL; } +@@ -927,13 +968,12 @@ + } + + gl_repo_list_t +-gl_find_pkg_repos(const char *pkg_name, uid_t uid) ++gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name) + { + gl_repo_list_t result = { NULL, 0 }; + + char store_path[PATH_MAX]; +- snprintf(store_path, sizeof(store_path), +- "/glacier/usr/store/%u", uid); ++ strncpy(store_path, ctx->store_path, PATH_MAX - 1); + + DIR *uid_dir = opendir(store_path); + if (!uid_dir) { return result; } +@@ -1033,8 +1073,9 @@ + } + + gl_install_status_t +-gl_install_pkg(const char *gpkg_path, uid_t uid) ++gl_install_pkg(gl_context_t *ctx, const char *gpkg_path) + { ++ if (!ctx) { return GL_INSTALL_ERR_OPEN; } + char original_cwd[PATH_MAX]; + if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) { + return GL_INSTALL_ERR_OPEN; +@@ -1108,7 +1149,7 @@ + + /* construct the directory for the package in the store */ + snprintf(pkg_store_base, sizeof(pkg_store_base), +- "%s/%d/%s/%s", usr_store.path, uid, pkg.pkg_repo, pkg.pkg_name); ++ "%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, +@@ -1208,15 +1249,9 @@ + archive_read_free(a2); + + /* now the index will be updated */ +- +- /* now the index will be updated */ +- char index_dir[PATH_MAX]; +- snprintf(index_dir, sizeof(index_dir), +- "%s/%d", usr_index.path, uid); +- + /* Resolve "current" symlink to find the actual index file to append to */ + char current_link[PATH_MAX]; +- snprintf(current_link, sizeof(current_link), "%s/current", index_dir); ++ 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); +@@ -1227,7 +1262,8 @@ + current_target[len] = '\0'; + + char index_path[PATH_MAX]; +- snprintf(index_path, sizeof(index_path), "%s/%s", index_dir, current_target); ++ snprintf(index_path, sizeof(index_path), "%s/%s", ++ ctx->index_path, current_target); + + FILE *idx = fopen(index_path, "a"); + if (!idx) { +@@ -1238,12 +1274,12 @@ + fprintf(idx, "%s::%s\n", pkg.pkg_repo, pkg.pkg_name); + fclose(idx); + +- if (gl_rebuild_index(uid, index_dir) != 0) { ++ if (gl_rebuild_index(ctx) != 0) { + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_INDEX; + } + +- if (gl_link_pkg(pkg_store_final, uid) != 0) { ++ if (gl_link_pkg(ctx, pkg_store_final) != 0) { + gl_free_pkg(&pkg); + return GL_INSTALL_ERR_EXTRACT; + } diff --git a/src/istoreutils.h b/src/istoreutils.h index 77dd546..db50d4a 100644 --- a/src/istoreutils.h +++ b/src/istoreutils.h @@ -5,6 +5,8 @@ #include #include +#include "transaction.h" + typedef enum { INDEX, STORE @@ -58,6 +60,7 @@ typedef enum { struct gpkg_entry { char *repo; char *pkg; + char *ver; }; struct gindex { @@ -71,21 +74,33 @@ bool gl_usr_istore_exists(index_store_t index_or_store, int uid); bool gl_sys_istore_exists(index_store_t index_or_store); int gl_init_user(uid_t uid, bool isVerbose); +int gl_init_sys(bool isVerbose); int gl_delete_user(uid_t uid, bool isVerbose); int gl_parse_index(struct gindex *idx, FILE *f); void gl_free_index(struct gindex *idx); -int gl_rebuild_index(uid_t uid, const char *out_dir); +int gl_rebuild_index(gl_context_t *ctx); -char *gl_find_pkg_repo(const char *pkg_name, uid_t uid); +char *gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name); + +typedef struct { + char **repos; + size_t count; +} +gl_repo_list_t; + +gl_repo_list_t gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name); +void gl_free_repo_list(gl_repo_list_t *list); gl_backup_status_t gl_backup_istore(const char *tar_path, const char *glacier_root, const char *uid); gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid); -gl_install_status_t gl_install_pkg(const char *gpkg_path, uid_t uid); -gl_remove_status_t gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid); +gl_install_status_t gl_install_pkg(gl_context_t *ctx, const char *gpkg_path); +gl_remove_status_t gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo); + +int gl_relink_store(gl_context_t *ctx); diff --git a/src/istoreutils.h.back b/src/istoreutils.h.back new file mode 100644 index 0000000..a3ee8e3 --- /dev/null +++ b/src/istoreutils.h.back @@ -0,0 +1,101 @@ +#ifndef ISTOREUTILS_H_ +#define ISTOREUTILS_H_ + +#include +#include +#include + +typedef enum { + INDEX, + STORE +} +index_store_t; + +typedef enum { + GL_BACKUP_OK = 0, + GL_BACKUP_ERR_ALLOC = 1, + GL_BACKUP_ERR_OPEN = 2, + GL_BACKUP_ERR_ADD_INDEX = 3, + GL_BACKUP_ERR_ADD_STORE = 4, + GL_BACKUP_ERR_ADD_LINKS = 5, + GL_BACKUP_ERR_CLOSE = 6 +} +gl_backup_status_t; + +typedef enum { + GL_RESTORE_OK = 0, + GL_RESTORE_ERR_OPEN = 1, + GL_RESTORE_ERR_EXTRACT = 2 +} gl_restore_status_t; + +/* + * GL_INSTALL_OK: installation successful + * GL_INSTALL_ERR_OPEN: unable to open .gpkg file + * GL_INSTALL_ERR_MANIFEST: manifest is missing or unparseable + * GL_INSTALL_ERR_EXTRACT: unable to extract files + * GL_INSTALL_ERR_INDEX: unable to update index + * GL_INSTALL_ERR_ALREADY_INSTALLED: package already installed + */ + +typedef enum { + GL_INSTALL_OK = 0, + GL_INSTALL_ERR_OPEN = 1, + GL_INSTALL_ERR_MANIFEST = 2, + GL_INSTALL_ERR_EXTRACT = 3, + GL_INSTALL_ERR_INDEX = 4, + GL_INSTALL_ERR_ALREADY_INSTALLED = 5, + GL_INSTALL_SKIPPED = 6, +} +gl_install_status_t; + +typedef enum { + GL_REMOVE_OK = 0, + GL_REMOVE_ERR_NOT_FOUND = 1, + GL_REMOVE_ERR_UNLINK = 2, + GL_REMOVE_ERR_INDEX = 3, +} gl_remove_status_t; + +struct gpkg_entry { + char *repo; + char *pkg; +}; + +struct gindex { + int uid; + int listed; + struct gpkg_entry *entries; + size_t count; +}; + +bool gl_usr_istore_exists(index_store_t index_or_store, int uid); +bool gl_sys_istore_exists(index_store_t index_or_store); + +int gl_init_user(uid_t uid, bool isVerbose); +int gl_delete_user(uid_t uid, bool isVerbose); + +int gl_parse_index(struct gindex *idx, FILE *f); +void gl_free_index(struct gindex *idx); + +int gl_rebuild_index(uid_t uid, const char *out_dir); + +char *gl_find_pkg_repo(const char *pkg_name, uid_t uid); + +typedef struct { + char **repos; + size_t count; +} +gl_repo_list_t; + +gl_repo_list_t gl_find_pkg_repos(const char *pkg_name, uid_t uid); +void gl_free_repo_list(gl_repo_list_t *list); + +gl_backup_status_t gl_backup_istore(const char *tar_path, const char *glacier_root, + const char *uid); +gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid); + +gl_install_status_t gl_install_pkg(const char *gpkg_path, uid_t uid); +gl_remove_status_t gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid); + + + +#endif diff --git a/src/sha256.c b/src/sha256.c new file mode 100644 index 0000000..c1a2153 --- /dev/null +++ b/src/sha256.c @@ -0,0 +1,203 @@ +/* + * gl_sha256 — vendored SHA-256 (FIPS 180-4). + * See sha256.h for API rationale. + */ + +#include +#include +#include + +#include "sha256.h" + +#define ROTRIGHT(a, b) (((a) >> (b)) | ((a) << (32 - (b)))) + +#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z))) +#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z))) +#define EP0(x) (ROTRIGHT(x, 2) ^ ROTRIGHT(x, 13) ^ ROTRIGHT(x, 22)) +#define EP1(x) (ROTRIGHT(x, 6) ^ ROTRIGHT(x, 11) ^ ROTRIGHT(x, 25)) +#define SIG0(x) (ROTRIGHT(x, 7) ^ ROTRIGHT(x, 18) ^ ((x) >> 3)) +#define SIG1(x) (ROTRIGHT(x, 17) ^ ROTRIGHT(x, 19) ^ ((x) >> 10)) + +static const uint32_t k[64] = { + 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, + 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5, + 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, + 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174, + 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, + 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da, + 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, + 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967, + 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, + 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85, + 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, + 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070, + 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, + 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3, + 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, + 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2, +}; + +static void +sha256_transform(gl_sha256_ctx_t *ctx, const uint8_t data[64]) +{ + uint32_t m[64]; + uint32_t i, j; + + for (i = 0, j = 0; i < 16; ++i, j += 4) { + m[i] = ((uint32_t)data[j] << 24) | + ((uint32_t)data[j + 1] << 16) | + ((uint32_t)data[j + 2] << 8) | + ((uint32_t)data[j + 3]); + } + for (; i < 64; ++i) { + m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16]; + } + + uint32_t a = ctx->state[0]; + uint32_t b = ctx->state[1]; + uint32_t c = ctx->state[2]; + uint32_t d = ctx->state[3]; + uint32_t e = ctx->state[4]; + uint32_t f = ctx->state[5]; + uint32_t g = ctx->state[6]; + uint32_t h = ctx->state[7]; + + for (i = 0; i < 64; ++i) { + uint32_t t1 = h + EP1(e) + CH(e, f, g) + k[i] + m[i]; + uint32_t t2 = EP0(a) + MAJ(a, b, c); + h = g; + g = f; + f = e; + e = d + t1; + d = c; + c = b; + b = a; + a = t1 + t2; + } + + ctx->state[0] += a; + ctx->state[1] += b; + ctx->state[2] += c; + ctx->state[3] += d; + ctx->state[4] += e; + ctx->state[5] += f; + ctx->state[6] += g; + ctx->state[7] += h; +} + +void +gl_sha256_init(gl_sha256_ctx_t *ctx) +{ + ctx->datalen = 0; + ctx->bitlen = 0; + ctx->state[0] = 0x6a09e667; + ctx->state[1] = 0xbb67ae85; + ctx->state[2] = 0x3c6ef372; + ctx->state[3] = 0xa54ff53a; + ctx->state[4] = 0x510e527f; + ctx->state[5] = 0x9b05688c; + ctx->state[6] = 0x1f83d9ab; + ctx->state[7] = 0x5be0cd19; +} + +void +gl_sha256_update(gl_sha256_ctx_t *ctx, const uint8_t *data, size_t len) +{ + size_t i; + + for (i = 0; i < len; ++i) { + ctx->data[ctx->datalen] = data[i]; + ctx->datalen++; + if (ctx->datalen == 64) { + sha256_transform(ctx, ctx->data); + ctx->bitlen += 512; + ctx->datalen = 0; + } + } +} + +void +gl_sha256_final(gl_sha256_ctx_t *ctx, uint8_t digest[GL_SHA256_DIGEST_SIZE]) +{ + size_t i = ctx->datalen; + + if (ctx->datalen < 56) { + ctx->data[i++] = 0x80; + while (i < 56) { + ctx->data[i++] = 0x00; + } + } else { + ctx->data[i++] = 0x80; + while (i < 64) { + ctx->data[i++] = 0x00; + } + sha256_transform(ctx, ctx->data); + memset(ctx->data, 0, 56); + } + + ctx->bitlen += (uint64_t)ctx->datalen * 8; + ctx->data[63] = (uint8_t)(ctx->bitlen); + ctx->data[62] = (uint8_t)(ctx->bitlen >> 8); + ctx->data[61] = (uint8_t)(ctx->bitlen >> 16); + ctx->data[60] = (uint8_t)(ctx->bitlen >> 24); + ctx->data[59] = (uint8_t)(ctx->bitlen >> 32); + ctx->data[58] = (uint8_t)(ctx->bitlen >> 40); + ctx->data[57] = (uint8_t)(ctx->bitlen >> 48); + ctx->data[56] = (uint8_t)(ctx->bitlen >> 56); + sha256_transform(ctx, ctx->data); + + for (i = 0; i < 4; ++i) { + uint32_t shift = 24 - (uint32_t)i * 8; + digest[i] = (uint8_t)((ctx->state[0] >> shift) & 0xffU); + digest[i + 4] = (uint8_t)((ctx->state[1] >> shift) & 0xffU); + digest[i + 8] = (uint8_t)((ctx->state[2] >> shift) & 0xffU); + digest[i + 12] = (uint8_t)((ctx->state[3] >> shift) & 0xffU); + digest[i + 16] = (uint8_t)((ctx->state[4] >> shift) & 0xffU); + digest[i + 20] = (uint8_t)((ctx->state[5] >> shift) & 0xffU); + digest[i + 24] = (uint8_t)((ctx->state[6] >> shift) & 0xffU); + digest[i + 28] = (uint8_t)((ctx->state[7] >> shift) & 0xffU); + } +} + +void +gl_sha256_to_hex(const uint8_t digest[GL_SHA256_DIGEST_SIZE], char *out) +{ + static const char hexchars[] = "0123456789abcdef"; + size_t i; + + for (i = 0; i < GL_SHA256_DIGEST_SIZE; i++) { + out[i * 2] = hexchars[(digest[i] >> 4) & 0x0fU]; + out[i * 2 + 1] = hexchars[digest[i] & 0x0fU]; + } + out[GL_SHA256_DIGEST_SIZE * 2] = '\0'; +} + +int +gl_sha256_file(const char *path, char *hex_out) +{ + FILE *f = fopen(path, "rb"); + if (!f) { + return -1; + } + + gl_sha256_ctx_t ctx; + gl_sha256_init(&ctx); + + uint8_t buf[65536]; + size_t n; + while ((n = fread(buf, 1, sizeof(buf), f)) > 0) { + gl_sha256_update(&ctx, buf, n); + } + + if (ferror(f)) { + fclose(f); + errno = EIO; + return -1; + } + fclose(f); + + uint8_t digest[GL_SHA256_DIGEST_SIZE]; + gl_sha256_final(&ctx, digest); + gl_sha256_to_hex(digest, hex_out); + return 0; +} diff --git a/src/sha256.h b/src/sha256.h new file mode 100644 index 0000000..18f1267 --- /dev/null +++ b/src/sha256.h @@ -0,0 +1,26 @@ +#ifndef GL_SHA256_H_ +#define GL_SHA256_H_ + +#include +#include + +#define GL_SHA256_DIGEST_SIZE 32 +#define GL_SHA256_HEX_SIZE (GL_SHA256_DIGEST_SIZE * 2 + 1) + +typedef struct { + uint32_t state[8]; + uint64_t bitlen; + uint8_t data[64]; + size_t datalen; +} +gl_sha256_ctx_t; + +void gl_sha256_init(gl_sha256_ctx_t *ctx); +void gl_sha256_update(gl_sha256_ctx_t *ctx, const uint8_t *data, size_t len); +void gl_sha256_final(gl_sha256_ctx_t *ctx, uint8_t digest[GL_SHA256_DIGEST_SIZE]); + +void gl_sha256_to_hex(const uint8_t digest[GL_SHA256_DIGEST_SIZE], char *out); + +int gl_sha256_file(const char *path, char *hex_out); + +#endif diff --git a/src/transaction.c b/src/transaction.c new file mode 100644 index 0000000..78ae174 --- /dev/null +++ b/src/transaction.c @@ -0,0 +1,420 @@ +#include +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "common.h" +#include "istoreutils.h" +#include "log.h" +#include "transaction.h" + +#define GL_STAGE_BASE "/glacier/usr/stage" +#define GL_LOCK_BASE "/glacier/usr/lock" +#define GL_OLD_BASE "/glacier/usr/old" + +#define GL_USR_INDEX "/glacier/usr/index" +#define GL_USR_STORE "/glacier/usr/store" +#define GL_USR_LINKS "/glacier/usr/links" + +#define GL_SYS_INDEX "/glacier/sys/index" +#define GL_SYS_STORE "/glacier/sys/store" +#define GL_SYS_LINKS "/glacier/sys/links" + +static int +fsync_dir(const char *path) +{ + int fd = open(path, O_RDONLY | O_DIRECTORY); + if (fd < 0) { + return -1; + } + int r = fsync(fd); + close(fd); + return r; +} + +static int +atomic_swap(const char *live, const char *stage, const char *backup) +{ + if (rename(live, backup) != 0) { + return -1; + } + + if (rename(stage, live) != 0) { + rename(backup, live); + return -1; + } + + return 0; +} + +static void +derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out, + char *store_out, char *links_out) +{ + switch (scope) { + case GL_SCOPE_USR: + snprintf(index_out, PATH_MAX, "/glacier/usr/index/%u", uid); + snprintf(store_out, PATH_MAX, "/glacier/usr/store/%u", uid); + snprintf(links_out, PATH_MAX, "/glacier/usr/links/%u", uid); + break; + case GL_SCOPE_SYS: + strncpy(index_out, "/glacier/sys/index", PATH_MAX - 1); + strncpy(store_out, "/glacier/sys/store", PATH_MAX - 1); + /* This is the DEFAULT system-scope links location — for + * packages the operator explicitly installs under system + * scope that aren't part of the base system. Base-repo + * packages get routed to /usr instead, but that's a + * per-package decision made in gl_link_pkg/gl_relink_store + * (which know a package's repo), not something the context + * can decide up front. See GL_BASE_REPO in istoreutils.c. */ + strncpy(links_out, "/glacier/sys/links", PATH_MAX - 1); + break; + } +} + +static int +check_same_filesystem(const char *a, const char *b) +{ + struct stat sa, sb; + if (stat(a, &sa) != 0 || stat(b, &sb) != 0) { + return -1; + } + return sa.st_dev == sb.st_dev; +} + +static int +hardlink_tree(const char *src, const char *dest) +{ + DIR *dir = opendir(src); + if (!dir) { + return (errno == ENOENT) ? 0 : -1; + } + + struct dirent *ent; + while ((ent = readdir(dir))) { + if (ent->d_name[0] == '.') { continue; } + + char s[PATH_MAX], d[PATH_MAX]; + snprintf(s, sizeof(s), "%s/%s", src, ent->d_name); + snprintf(d, sizeof(d), "%s/%s", dest, ent->d_name); + + struct stat st; + if(lstat(s, &st) != 0) { continue; } + + if (S_ISDIR(st.st_mode)) { + if (mkdir(d, st.st_mode & 0777) != 0 && errno != EEXIST) { + closedir(dir); + return -1; + } + if (hardlink_tree(s, d) != 0) { + closedir(dir); + return -1; + } + } + else if (S_ISLNK(st.st_mode)) { + char target[PATH_MAX]; + ssize_t n = readlink(s, target, sizeof(target) - 1); + if (n < 0) { continue; } + target[n] = '\0'; + symlink(target, d); + } + else { + if (link(s, d) != 0 && errno != EEXIST) { + closedir(dir); + return -1; + } + } + } + + closedir(dir); + return 0; +} + +int +gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid) +{ + if (!ctx) { + return -1; + } + + ctx->uid = uid; + ctx->mode = GL_CTX_LIVE; + ctx->scope = scope; + ctx->lock_fd = -1; + + strncpy(ctx->root_path, "/glacier", PATH_MAX - 1); + derive_base_paths(scope, uid, ctx->index_path, ctx->store_path, + ctx->links_path); + + return 0; +} + +gl_txn_status_t +gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid) +{ + if (!ctx) { + return GL_TXN_ERR_NO_TXN; + } + + ctx->uid = uid; + ctx->mode = GL_CTX_STAGE; + ctx->scope = scope; + ctx->lock_fd = -1; + + strncpy(ctx->root_path, "/glacier", PATH_MAX - 1); + + char stage_base[PATH_MAX - 7]; + + switch (scope) { + case GL_SCOPE_USR: + snprintf(stage_base, sizeof(stage_base), "/glacier/usr/stage/%u", uid); + break; + case GL_SCOPE_SYS: + strncpy(stage_base, "/glacier/sys/stage", sizeof(stage_base)); + break; + } + + snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base); + snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base); + snprintf(ctx->links_path, PATH_MAX, "%s/links", stage_base); + + struct stat st; + if (stat(stage_base, &st) == 0) { + if (gl_rmdir_recursive(stage_base) != 0) { + return GL_TXN_ERR_MKDIR; + } + } + + if (gl_mkdirp(GL_LOCK_BASE, 0700) != 0 && errno != EEXIST) { + return GL_TXN_ERR_MKDIR; + } + + gl_txn_status_t lock_status = gl_begin_transaction(ctx); + if (lock_status != GL_TXN_OK) { + return lock_status; + } + + if (gl_mkdirp(ctx->index_path, 0700) != 0) { + gl_abort_transaction(ctx); + return GL_TXN_ERR_MKDIR; + } + + if (gl_mkdirp(ctx->store_path, 0700) != 0) { + gl_abort_transaction(ctx); + return GL_TXN_ERR_MKDIR; + } + + if (gl_mkdirp(ctx->links_path, 0700) != 0) { + gl_abort_transaction(ctx); + return GL_TXN_ERR_MKDIR; + } + + if (chown(stage_base, uid, (gid_t)-1) != 0) { + gl_abort_transaction(ctx); + return GL_TXN_ERR_PERM; + } + + char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX]; + derive_base_paths(scope, uid, live_index, live_store, live_links); + /* live_links is populated (derive_base_paths always fills all + * three) but intentionally unused below — see the comment on the + * hardlink_tree call. */ + + /* Only index and store need copy-on-write seeding — the staged + * links tree is never actually read from or written to (gl_link_pkg + * skips GL_CTX_STAGE entirely; gl_relink_store always builds its + * own fresh live context rather than touching ctx->links_path of a + * stage context). Seeding it was always wasted work; now that + * live_links can be /usr for system scope, it would also be + * actively dangerous — hardlinking the entire /usr tree on every + * transaction, and risking EXDEV outright if /usr and the stage + * area are on different filesystems. */ + if (hardlink_tree(live_index, ctx->index_path) != 0 || + hardlink_tree(live_store, ctx->store_path) != 0) { + gl_abort_transaction(ctx); + return GL_TXN_ERR_MKDIR; + } + + return GL_TXN_OK; +} + +gl_txn_status_t +gl_begin_transaction(gl_context_t *ctx) +{ + if (!ctx) { + return GL_TXN_ERR_NO_TXN; + } + + char lock_path[PATH_MAX]; + switch (ctx->scope) { + case GL_SCOPE_USR: + snprintf(lock_path, PATH_MAX, "%s/%u.lock", GL_LOCK_BASE, + ctx->uid); + break; + case GL_SCOPE_SYS: + snprintf(lock_path, PATH_MAX, "%s/sys.lock", GL_LOCK_BASE); + break; + } + + int fd = open(lock_path, O_WRONLY | O_CREAT, 0600); + if (fd < 0) { + return GL_TXN_ERR_LOCK; + } + + if (flock(fd, LOCK_EX | LOCK_NB) != 0) { + close(fd); + return GL_TXN_ERR_LOCK; + } + + ctx->lock_fd = fd; + return GL_TXN_OK; +} + +gl_txn_status_t +gl_commit_transaction(gl_context_t *ctx) +{ + if (!ctx || ctx->lock_fd < 0) { + return GL_TXN_ERR_NO_TXN; + } + if (ctx->mode != GL_CTX_STAGE) { + return GL_TXN_ERR_NO_TXN; + } + + char stage_base[PATH_MAX]; + switch (ctx->scope) { + case GL_SCOPE_USR: + snprintf(stage_base, PATH_MAX, "/glacier/usr/stage/%u", + ctx->uid); + break; + case GL_SCOPE_SYS: + strncpy(stage_base, "/glacier/sys/stage", PATH_MAX - 1); + break; + } + + int same = check_same_filesystem(ctx->root_path, stage_base); + if (same < 0) { + return GL_TXN_ERR_RENAME; /* failed to stat either side */ + } + if (!same) { + return GL_TXN_ERR_XDEV; + } + + /* Derive live paths the same way gl_init_live_context would */ + char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX]; + derive_base_paths(ctx->scope, ctx->uid, live_index, live_store, + live_links); + + /* Derive old/backup paths */ + char old_base[PATH_MAX]; + switch (ctx->scope) { + case GL_SCOPE_USR: + snprintf(old_base, PATH_MAX, "/glacier/usr/old/%u", ctx->uid); + break; + case GL_SCOPE_SYS: + strncpy(old_base, "/glacier/sys/old", PATH_MAX - 1); + break; + } + + char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX]; + snprintf(old_index, PATH_MAX, "%s/index", old_base); + snprintf(old_store, PATH_MAX, "%s/store", old_base); + snprintf(old_links, PATH_MAX, "%s/links", old_base); + + struct stat old_st; + if (stat(old_base, &old_st) == 0) { + if (gl_rmdir_recursive(old_base) != 0) { + return GL_TXN_ERR_RENAME; + } + } + + if (gl_mkdirp(old_base, 0700) != 0) { + return GL_TXN_ERR_RENAME; + } + + if (atomic_swap(live_index, ctx->index_path, old_index) != 0) { + return GL_TXN_ERR_RENAME; + } + if (atomic_swap(live_store, ctx->store_path, old_store) != 0) { + rename(live_index, ctx->index_path); + rename(old_index, live_index); + return GL_TXN_ERR_RENAME; + } + if (atomic_swap(live_links, ctx->links_path, old_links) != 0) { + rename(live_store, ctx->store_path); + rename(old_store, live_store); + rename(live_index, ctx->index_path); + rename(old_index, live_index); + return GL_TXN_ERR_RENAME; + } + + /* fsync the parent directories, not the uid subdirs */ + switch (ctx->scope) { + case GL_SCOPE_USR: + fsync_dir("/glacier/usr/index"); + fsync_dir("/glacier/usr/store"); + fsync_dir("/glacier/usr/links"); + break; + case GL_SCOPE_SYS: + fsync_dir("/glacier/sys/index"); + fsync_dir("/glacier/sys/store"); + fsync_dir("/glacier/sys/links"); + fsync_dir("/usr"); /* base-repo packages land here instead */ + break; + } + + gl_context_t live_ctx; + gl_init_live_context(&live_ctx, ctx->scope, ctx->uid); + + if (gl_relink_store(&live_ctx) != 0) { + lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid); + } + + gl_rmdir_recursive(old_base); + + /* Remove stage base */ + gl_rmdir_recursive(stage_base); + + close(ctx->lock_fd); + ctx->lock_fd = -1; + + return GL_TXN_OK; +} + +gl_txn_status_t +gl_abort_transaction(gl_context_t *ctx) +{ + if (!ctx) { + return GL_TXN_ERR_NO_TXN; + } + + /* Remove stage area — live is untouched */ + char stage_uid[PATH_MAX]; + switch (ctx->scope) { + case GL_SCOPE_USR: + snprintf(stage_uid, PATH_MAX, "/glacier/usr/stage/%u", + ctx->uid); + break; + case GL_SCOPE_SYS: + strncpy(stage_uid, "/glacier/sys/stage", PATH_MAX - 1); + break; + } + + gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */ + + /* Release lock */ + + if (ctx->lock_fd >= 0) { + close(ctx->lock_fd); + ctx->lock_fd = -1; + } + + return GL_TXN_OK; +} \ No newline at end of file diff --git a/src/transaction.h b/src/transaction.h new file mode 100644 index 0000000..b925b69 --- /dev/null +++ b/src/transaction.h @@ -0,0 +1,55 @@ +#ifndef TRANSACTION_H_ +#define TRANSACTION_H_ + +#define _POSIX_C_SOURCE 200809L + +#include +#include +#include + +#define GL_UID_NONE ((uid_t)0) + +typedef enum { + GL_CTX_LIVE, + GL_CTX_STAGE +} gl_ctx_mode_t; + +typedef enum { + GL_SCOPE_USR, + GL_SCOPE_SYS +} gl_ctx_scope_t; + +typedef struct { + uid_t uid; + + char root_path[PATH_MAX]; + char index_path[PATH_MAX]; + char store_path[PATH_MAX]; + char links_path[PATH_MAX]; + + gl_ctx_mode_t mode; + gl_ctx_scope_t scope; + + int lock_fd; +} gl_context_t; + +typedef enum { + GL_TXN_OK = 0, /* success */ + GL_TXN_ERR_LOCK = 1, /* unable to acquire lock */ + GL_TXN_ERR_MKDIR = 2, /* unable to create txn stage */ + GL_TXN_ERR_PERM = 3, /* permission error */ + GL_TXN_ERR_RENAME = 4, /* atomic swap failed */ + GL_TXN_ERR_SYNC = 5, /* fsync failed */ + GL_TXN_ERR_CLEANUP = 6, /* cleanup failed */ + GL_TXN_ERR_NO_TXN = 7, /* aborted without beginning */ + GL_TXN_ERR_XDEV = 8 /* stage and live on different filesystems */ +} gl_txn_status_t; + +int gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid); + +gl_txn_status_t gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid); +gl_txn_status_t gl_begin_transaction(gl_context_t *ctx); +gl_txn_status_t gl_commit_transaction(gl_context_t *ctx); +gl_txn_status_t gl_abort_transaction(gl_context_t *ctx); + +#endif