This commit is contained in:
2026-07-19 17:49:23 -04:00
parent 0d8859b063
commit 7b22e3746a
29 changed files with 3949 additions and 193 deletions

261
CLAUDE.md Normal file
View File

@@ -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}/<uid>`.
- `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
`<pkgname>-<version>` 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_<NAME>=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.

View File

@@ -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

177
PACKAGE_SCOPES.txt Normal file
View File

@@ -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/<uid>
/glacier/usr/store/<uid>
/glacier/usr/links/<uid>
Symlinks always land under /glacier/usr/links/<uid>/{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/<uid>
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.

379
SECURITY_DESIGN.txt Normal file
View File

@@ -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:<hex>", 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:<hex>
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:<hex>` 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.

View File

@@ -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
`"<pkg_name>-<ver>/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
`"<pkg_name>-<ver>/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:** MediumHigh
* **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
```

BIN
build/bin/gsha256sum Executable file

Binary file not shown.

BIN
build/lib/shared/libglacier-ng.so Executable file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
build/lib/tmp/sha256.o Normal file

Binary file not shown.

View File

@@ -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

View File

@@ -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 <dirent.h>
#include <errno.h>
#include <linux/limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
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

21
src/gsha256sum.c Normal file
View File

@@ -0,0 +1,21 @@
#include <stdio.h>
#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;
}

View File

@@ -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
* "<pkg_name>-<version>" (see gl_cat_dir_and_ver
* in pkg.c, which is what created it). Strip the
* known "<pkg_name>-" prefix to recover just the
* version part — safe even if pkg_name itself
* contains dashes, since we already know its
* exact length from pkg_ent->d_name. */
char pkg_ver[64] = "unknown";
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,10 +829,15 @@ 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) {
/* 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 */
if (gl_rmdir_recursive(pkg_store_base) != 0) {
@@ -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,7 +919,17 @@ gl_link_pkg(const char *pkg_store_final, uid_t uid)
gl_mkdirp(parent, 0700);
}
/* remove existing symlink if present */
/* 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 */
@@ -906,7 +940,8 @@ gl_link_pkg(const char *pkg_store_final, uid_t uid)
return -1;
}
lchown(link_path, uid, (gid_t)-1);
lchown(link_path, ctx->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) {
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;
}

1365
src/istoreutils.c.back Normal file

File diff suppressed because it is too large Load Diff

302
src/istoreutils.c.rej Normal file
View File

@@ -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;
}

View File

@@ -5,6 +5,8 @@
#include <stdio.h>
#include <sys/types.h>
#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);

101
src/istoreutils.h.back Normal file
View File

@@ -0,0 +1,101 @@
#ifndef ISTOREUTILS_H_
#define ISTOREUTILS_H_
#include <stdbool.h>
#include <stdio.h>
#include <sys/types.h>
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

203
src/sha256.c Normal file
View File

@@ -0,0 +1,203 @@
/*
* gl_sha256 — vendored SHA-256 (FIPS 180-4).
* See sha256.h for API rationale.
*/
#include <errno.h>
#include <stdio.h>
#include <string.h>
#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;
}

26
src/sha256.h Normal file
View File

@@ -0,0 +1,26 @@
#ifndef GL_SHA256_H_
#define GL_SHA256_H_
#include <stddef.h>
#include <stdint.h>
#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

420
src/transaction.c Normal file
View File

@@ -0,0 +1,420 @@
#include <linux/limits.h>
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/file.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#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;
}

55
src/transaction.h Normal file
View File

@@ -0,0 +1,55 @@
#ifndef TRANSACTION_H_
#define TRANSACTION_H_
#define _POSIX_C_SOURCE 200809L
#include <limits.h>
#include <stdbool.h>
#include <sys/types.h>
#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