# CLAUDE.md Context for Claude Code sessions on libglacier-ng: the package manager for Everest Linux. Kept as a handoff, so a fresh session starts with full continuity instead of re-deriving everything. Last full rewrite: 2026-10-04; brought fully up to date at the end of the 2026-10-07 session. **Update it as you go.** ## Start here 1. `ROADMAP.txt`: what's left, in priority order, plus the decisions that are still open (section 6). Pick work from it. 2. This file: how things work, what the user wants, how to test. 3. The design docs, which are authoritative for their topics and not repeated here: - `SECURITY_DESIGN.txt`: integrity verification, scope separation, refusal matrix, syspkg's privilege model. - `PACKAGE_SCOPES.txt`: layout, link routing, `/etc` config handling. - `IMAGE_DESIGN.txt`: system images (bootstrap OUTPUT). Sections 3 (sysroot) and 4 (`syspkg --root`) are IMPLEMENTED (milestones 1-2); the rest is still a proposal. Open decisions are in section 9. - `GLACIER_BOOTSTRAP.txt`: `glacier-bootstrap`. Local mode is IMPLEMENTED (ROADMAP 1.3; section 10 says how it differs from the plan); remote mode and `--pin` are still a plan. Open: 9a, 9f. - `REPO_DESIGN.txt`: remote repos + signing (ROADMAP 3.1). Sections 1 and 10 = the user's decisions (TLS library still open); steps 1-4 of section 9 DONE (see "3.1 status"); sections 7, 5 and 6 describe steps 2, 3 and 4 as implemented. - `SECURITY_REVIEW_2026-10-06.md`: the full audit (findings, fixes, what was removed, what's still open). - `SECURITY_REVIEW_2026-07-16.md`: older review findings. Nothing from the 2026-10-04 through 2026-10-07 sessions is committed. The user said "do not commit yet" (2026-10-05), and on 2026-10-06 that their git server is down: they will commit themselves once it's back. libglacier-ng is on branch `dev` with a large uncommitted diff (`src/`, the docs, `build/`). Commit only when the user asks; when they do, propose a few logical commits. 2026-10-05 session: system-scope permissions fix (bug 24), ROADMAP 2.3 dependencies (`src/deps.c`), index corruption on abort (bug 25), ROADMAP 1.1 sysroot, live removal deleting foreign files (bug 26), ROADMAP 1.2 (`syspkg -R`, `--strict`) + 2.6 (file collisions), gpkg ignoring unknown flags (bug 27). 2026-10-06 session: ROADMAP 1.3 `glacier-bootstrap` local mode, and Everest's initramfs switched to it (boots in QEMU). Tidy-up (gbuild's hint removed). ROADMAP 1.4 DONE (packages for glacier itself; details in ROADMAP 1.4 and section 7): full regression green, new `tests/installed_alpine.sh` for the installed layout, which found bug 29 (fixed, mutation-checked, Everest rebuilt and booted with the fix). The installer (1.5) is DEFERRED by the user: manual installation is one of Everest's main features. Then ROADMAP 3.1 was designed with the user (REPO_DESIGN.txt, every decision taken; only the TLS library for curl in base is open) and steps 1, 2 and (that evening, after the laptop died mid-rebuild) 3 of its section 9 were done. 2026-10-07 session: ROADMAP 3.1 step 4 (`-f`, `-u`, dependency closure, the cache; see "3.1 status"). Help text for -f/-u/-O shortened at the user's request. Refresh output (user's wording): `[i] Syncing all repositories...`, then `[✓] All repositories synced successfully.` if every repo succeeded; the per-repo success/up-to-date lines only with `-V`; errors always (repo_alpine.sh checks both ways). Then (same day) glacier-bootstrap configures repositories (user: it must leave a semi-functional system, pacstrap-like): see "Bootstrap repositories" below. 2026-10-09: help text. `gsign`, `grepo`, `gsha256sum` (C, `getopt_long`, static `help()` in each tool's own file), `glacier-bootstrap` and `gworld` now have `-h/--help` and `-v/--version` (`v4.0.0`) in gpkg's format (`%-25s` `{-x --long} ARG` lines, GPL footer), with long forms for every flag. `-h` goes to stdout, exit 0; usage errors stay one usage block on stderr, exit 1. repo_alpine.sh checks the help of grepo/gsign/gsha256sum. Backups: `glacier-bootstrap.old2`, `gworld.old`, `tests/repo_alpine.sh.old5`. ### Bootstrap repositories (2026-10-07) The user chose: the config comes from a NEW base package (`everest-repos` 1.0.0: `etc/glacier/repos.conf` + `keys/everest-2026.pub`, source `~/Projects/everest/pkgsrc/everest-repos`; everest-bootscripts 1.3.0 no longer ships `etc/glacier`, its old copy is `pkgsrc/everest-bootscripts/glacier.old`); bootstrap runs `syspkg -R T -y` after installing and a failed fetch is an error (exit 7, no "Bootstrapped"); under `-R`, `-y`/`-f`/`-u` use T's OWN keyring (`keyring(sysroot)` in gpkg_common.lua; `require_trusted` also refuses a file whose realpath leaves T). `-R -l` signatures stay the host's (`HOST_KEYRING`). No url in T: `[!] No remote repositories are configured in T.`, exit 0 (Everest today: base isn't published). Copying the host's repos.conf (pacstrap's mirrorlist) left out for now. The user said everything a template lists for base also goes in the base repo (the published repo = the template's base packages); no url yet. Everest rebuilt (bootscripts 1.3.0, everest-repos, glacier-ng 4.0.0 in place), initramfs remade, booted: `/etc/glacier` from everest-repos, 12 packages. ### Audit and cleanup (2026-10-06, late) At the user's request, before 3.1 step 4: a full security audit (`SECURITY_REVIEW_2026-10-06.md`, bugs 30-35 below), unused code removed, and junk cleaned up. The user's choices: delete only the `.old` backups and stray binaries (every prototype/note file stays: libglacier-ng's `src/{deb,gpkg,templ,report,test_*}.c`, `src/html`, `pkg/`, glacier-ng's `todo`, `tree/`, `test-*`, `pkgs/`...); libglacier-ng's build outputs are untracked (`.gitignore`: `/build/`; the deletions show in the next commit); the `.old` backup rule STAYS (see "Backups" below for current ones). New tests: `tests/audit_alpine.sh` (26 checks), `tests/test_abort_unlocked.lua`. Full regression green; Everest rebuilt and booted. Everest's own `.old` files were not touched (not one of the two repos). ### 3.1 status Step 4 DONE (2026-10-07; REPO_DESIGN section 6 "As implemented"). The user chose: the FIRST repo (repos.conf order) serving a name wins, at its highest version; a dependency installed too old is upgraded in the same transaction (keeps its auto/explicit mark); cached downloads are deleted after the commit that installs them. - libglacier-ng: `gl_scope_cache_dir(ctx, repo, out)` (transaction.c; `/cache/REPO`, checked like `repos` via the shared static `checked_subdir`); `log.c` help lists `-f`/`-u`. - glacier-ng `gpkg_common.lua`: `M.fetch(names, opts)`, `M.upgrade(names, opts)`; `load_indexes` (refresh, then read the accepted copies), `pick`, `close_deps`, `download` (SIZE-capped, hashed), `matches_index` (manifest vs index entry), `install_plan`. `M.install` is now `verify_all` + `install_copies(copies, original, opts, meta)`, shared with -f/-u (`meta`: auto, may_replace, dep, cache). `verify_and_copy(path, vouched)` takes the index's hash in place of a sidecar. syspkg `-f`/`-u` modes; gpkg `-f`/`-u` (no longer "not implemented"). - New suite `tests/fetch_alpine.sh` (84 checks; 16 Lua mutants + 1 C, all caught except the download-level hash check, equivalent: kept as defence in depth). The `gpkg -u "%s%n"` format check moved there from audit_alpine.sh. Full regression green. - Everest's libglacier-ng/glacier-ng 4.0.0 were REBUILT in place 2026-10-07 with -f/-u/-y (user: keep 4.0.0, no bump) and the initramfs remade; in QEMU `gpkg -f hi` as a wheel user now says `[x] No remote repositories are configured.` (shipped repos.conf has no url; no curl in base). `container.sh initramfs` now `mkdir -p`s `$G/lib/glacier` (gone from the tree). Not done: no published repo; TLS library still open. Next: step 5 (`glacier-bootstrap -r`), then 6 (user repos). Step 3 DONE (2026-10-06, evening; REPO_DESIGN section 5 "As implemented"). The user chose: a refresh-only flag `-y`, the date-form SERIAL (YYYYMMDDNN), grepo in C. - libglacier-ng: `src/grepo.c` (`grepo -s KEY.sec -r REPO [-n SERIAL] [-e DAYS] DIR`, Makefile target in `all`, installed to bin, built from source like gsign); `pkg.c`: `gl_index_parse(text, len, repo, out)` / `gl_index_free` (strict; takes the signed BYTES; refuses NUL and '@'), `gl_conf_repos(path, out, len)` ("NAME\tURL\n" lines), repos.conf validation shared in `read_repos_conf`; `transaction.c`: `gl_scope_state_dir(ctx, "repos", out)` (creates/checks `/repos`). `log.c` help lists `-y`. - glacier-ng: `M.refresh(opts)` in `gpkg_common.lua` (fetch file:// or curl https, verify with a key listed for THAT repo, parse, rollback / same-serial / expiry checks, rename into `/repos`, under the scope lock); `syspkg -y` (needs root and `-n`; `-R` works), `gpkg -y` (system repos.conf + keyring until step 6). Dockerfiles copy grepo to /usr/local/bin. New suite `tests/repo_alpine.sh` (103 checks; 18 mutants, all caught). Full regression green. Everest's libglacier-ng and glacier-ng packages rebuilt with it. - Next: step 4 (`-f` with dependency closure, then `-u`; the cache in `/cache`, section 5's Fetch). Step 2 DONE (2026-10-06, second part of the session; REPO_DESIGN section 7 "As implemented" has the details): - libglacier-ng `src/pkg.c`: `gl_conf_repo_keys(path, repo, out, len)`, a strict repos.conf reader; new statuses `GL_CONF_ERR_INVALID = 4`, `GL_CONF_ERR_SPACE = 5`. - glacier-ng `lib/gpkg_common.lua`: `verify_all(paths, opts)` refuses a base package in system scope without a trusted signature (`signed_for`, `trusted_keys`, `untrusted`); `verify_and_copy` returns the sidecar's bytes. glacier-bootstrap copies the sidecar's BYTES and the `.sig`. `gbuild -k`/`--key`, `tests/mkpkg -k`. - Every `Dockerfile.*` installs gsign + gsha256sum in /usr/local/bin and a TEST keyring (`glacier-test`, secret `/root/glacier-test.sec`). `syspkg_alpine.sh` / `overwrite_alpine.sh` make their own key. - New suite `tests/keyring_alpine.sh` (68 checks; 17 mutants: 10 Lua/ shell, 7 C, all caught). Full regression green. - Everest: `./everest.sh keygen` made `everest-2026`; the SECRET key is `~/.config/everest/keys/everest-2026.sec` (user's choice: outside the project), mounted read-only for `build`, which refuses without it. Every package is built `gbuild -k`. everest-bootscripts 1.2.0 ships `etc/glacier/repos.conf` (base: everest-2026) and `etc/glacier/keys/everest-2026.pub`; `container.sh initramfs` installs the same into the build container's `/etc/glacier`. The signed `build all` was cut off after busybox (laptop died); the other 9 were rebuilt `-k` the evening of 2026-10-06, `initramfs` ran through glacier-bootstrap (signature checks passed for all 11), and Everest booted in QEMU: repos.conf + key in `/etc/glacier`, index lists all 11, syspkg/gpkg run as root and as a fresh user. - Base KEEP_OLD backups can't be restored with `-lO` (no signature): the user accepted that until the restore interface (REPO_DESIGN 10f). Step 1 DONE (2026-10-06): - `src/monocypher/`: Monocypher 4.0.3, vendored UNMODIFIED (provenance and sha512 in its README; GitHub and monocypher.org copies matched). Compiles clean under the project's `-Werror` flags. - `src/sign.{c,h}`: signify formats (strict parse, canonical base64), `gl_sign_read_pubkey`/`_read_sig`/`_check_seckey`/`_verify`/ `_verify_file`/`_file`/`_generate`, statuses `GL_SIGN_OK`.. `GL_SIGN_ERR_WRITE = 9`. Built into `libglacier_verify.so` (NOT into the combined `libglacier-ng.so`). - `src/gsign.c` (`-G`/`-S`/`-V`, lg_printf messages naming the file at fault), Makefile targets `gsign` (in `all`, installed to bin like gsha256sum). - `glacier-ng/tests/sign_alpine.sh` (alpine:latest, mounts /libsrc): round trips, signify interop both ways (Alpine `signify`), encrypted and corrupted secret keys, malformed key/sig files, 64 MiB limit, O_NOFOLLOW, key modes, a direct FFI call of `gl_sign_generate`. Passes in full. (`luajit -e CODE ARG` runs ARG as a script, so the FFI snippet is written to `/gen.lua` first.) - Mutation checks: 12 mutants, all caught except m9 (padding position, `j < 2`), which is EQUIVALENT: kept, it prevents `3 - pad` underflowing. m5 (fchmod), m6 (final newline) and m10 (O_EXCL) needed the umask-077, final-char and FFI checks respectively. - `glacier-ng/lib/glacier_cdef.lua`: `gl_sign_*` declarations and `M.sign_name()`; checked against the real `.so` (struct sizes 40/72, every call on signify-made files). Full regression green after it. Also open, for the user (raised 2026-10-06, not answered yet): - libgcc: luajit is built `-static-libgcc` (Everest has no libgcc_s). Claude's call; the alternative is a GCC runtime package in base (ROADMAP section 6). - Leftover `~/Projects/everest/pkgs/everest-bootscripts-1.0.0.gpkg` (+ sidecar), superseded by 1.1.0. Not deleted: ask first. ### Everest's glacier packages (1.4) - `~/Projects/everest/scripts/container.sh` builds zlib, libarchive (gzip/tar only), libconfig, luajit (pinned commit, `LDFLAGS=-static-libgcc`), libglacier-ng, glacier-ng (all base) and doas (extra, OpenDoas, no PAM). - glacier-ng layout: `usr/lib/glacier-ng/{syspkg,gpkg,gstore,lib/*.lua}`, `lib/glacier -> /usr/lib/glacier` (absolute: relative links resolve inside the store), `usr/bin/X -> ../lib/glacier-ng/X`, `etc/glacier/glacier.conf`. - everest-bootscripts 1.2.0 (1.1.0 before signing) ships `/etc/doas.conf` (0400): `permit :wheel` plus nopass `gstore args -n` for BOTH `cmd gstore` and `cmd /usr/bin/gstore` (OpenDoas matches `cmd` against what was typed). - `templates/minimal.template` and `PACKAGES` in everest.sh list all 12 packages (everest-repos added 2026-10-07). `Dockerfile.build` has autoconf, automake, libtool, cmake, pkgconf. ## The three projects | Path | What | Git? | |---|---|---| | `~/Projects/libglacier-ng` | C library, design docs, this file | yes (`dev`) | | `~/Projects/glacier-ng` | Frontend: `gpkg`, `gstore`, `syspkg`, `gbuild`, `gworld`, `lib/*.lua`, `tests/`, Dockerfiles, `pkgs/` | **no**: back files up before editing (`file.old`; if that exists, `.old2`, `.old3`, ...) | | `~/Projects/everest` | The distro build: `everest.sh build\|initramfs\|boot`, packages, bootable QEMU image | **no**: same backup rule (but never inside `pkgsrc/*/etc/`, see Boot test) | Nothing prebuilt is kept in either repo (the host-built `.so`s in `glacier-ng/lib/glacier/` were removed 2026-10-06). Every image and test builds libglacier-ng from source; the Dockerfiles create `/app/lib/glacier/` with the musl `.so`s. ## What this project is A from-scratch, security-focused Linux distribution (Everest) with its own package manager (libglacier-ng + `gpkg`/`gstore`/`syspkg`) and build tooling (`gbuild`/`gworld`). It's a C library, LuaJIT+FFI CLI tools and bash build scripts. The user writes it; Claude implements and designs alongside them. ## How the user wants things done These are preferences stated in sessions. Follow them. - **Output format:** every message uses glacier's `lg_printf` levels: `[x]` is error (2), `[!]` warning (1), `[i]` info (0), `[✓]` success (3). Messages are capitalized. Exact strings the user chose include `Cannot open system index: Permission denied` (no period) and `Nothing to do.` (success level). - The pre-trust-check `die()` in syspkg/gstore reproduces the `[x]` format byte for byte, because it can't load `libglacier_log.so` before that file has been verified. - **Never say "Completed" after an error.** If the post-commit link pass refused any path (`ctx->link_refusals`, counted only for packages installed by that transaction), gpkg/syspkg end with `[x] N paths were not linked.` (`1 path was not linked.`) and exit 1. The transaction itself stays committed (2026-10-05). - **No hint messages.** Don't print lines that point users to other flags or commands (for example "use -lO to ..."). The user called these clutter. A refusal states the fact, in one line. The user also removed a `Run doas gstore -n ...` follow-up hint from gstore themselves. - **What goes where.** ALL design docs (and ROADMAP, this file) live in libglacier-ng, frontend ones included. Other than docs, libglacier-ng holds only backend material (C sources, the build). Frontend code, tests, Dockerfiles and everything the bootstrap runs on live in glacier-ng. Clarified 2026-10-06. - **Ask before settling a design choice that is the user's to make**, and give a recommendation. They have answered every such question promptly. Don't silently pick a layout or semantics. - **Verify by running, in Docker or QEMU, never by inspection alone.** For any security fix, also prove the test catches the bug: remove the defence in a throwaway copy and confirm the test fails ("mutation check"). Several defences here are verified that way. - Use doas, not sudo. syspkg is never setuid. - **Help text is terse** (2026-10-07): one short phrase per flag, no mechanism details (`-f` "Install packages from a remote repository", `-u` "Update system or individual packages", `-O` "Allow Glacier to overwrite existing files"). ## Decisions the user has made (don't relitigate) - **Per-uid user layout** (`/glacier/usr//...`, root-owned parent). Both root and the user can create it: root runs `gstore -n UID`; a user runs `doas gstore -n` through a doas rule pinned to `args -n` (option "B"). - **`syspkg -n` creates the top-level tree itself** (GLACIER_BOOTSTRAP 9d). - **`/etc` config files: option 1.** Copy on install, `.new` on conflict (GLACIER_BOOTSTRAP 9c). - **`-O` with `-l` = replace.** `-O` displaces non-glacier files, and `-lO` also replaces an installed *different version* (upgrade or downgrade from a local file). Plain `-l` with another version installed refuses in one line. - **`KEEP_OLD`** in `/etc/glacier/glacier.conf`: the number of backups of replaced versions to keep per package (0 = none). - It has **no default.** It ships as `KEEP_OLD = 1`. - If it's missing or invalid, an operation that would replace a version errors. Only then: other operations don't need it. - "Backed up" means the old package's files are compressed into a `.gpkg`. A restore interface comes later. - **`gpkg -u` / `syspkg -u` upgrade from a REMOTE repository**, and `gpkg -f` is `-l` but fetched by name from a remote repository. Done 2026-10-07 (3.1 step 4). Never against local files. - **-f/-u details (2026-10-07):** first repo by priority wins (then its highest version); a too-old installed dependency is upgraded in the same transaction; cached downloads are deleted after commit. - **Dependencies (ROADMAP 2.3, decided 2026-10-05):** see the spec in ROADMAP 2.3. In short: minimum versions, conflicts both ways by name, no override flag, report every problem, missing deps refuse until 3.1 brings automatic closure, auto/explicit recorded in the index now. - **sysroot (IMAGE_DESIGN section 3) accepted as proposed**, plus the library refuses a non-"/" sysroot for `GL_SCOPE_USR`. Order of work: 2.3 first, then 1.1. - **System scope is world-readable** (dirs 0755, index files 0644): every user must be able to run what syspkg installs (2026-10-05). User scope stays 0700, and other users' programs stay unreachable to non-root users. Root CAN run them by full path, but other users' links are deliberately NOT on root's `PATH` (a user could get code run as root). - **Link refusals after a commit** end with `[x] N paths were not linked.` and exit 1, never "Completed" (the user's wording rule: don't say "completed" if there was an error). - **`syspkg -R DIR`** (not `-r`), `--root DIR`, `--root=DIR`; `-R` IMPLIES `--strict`; `--strict` alone is opt-in on the host. - **File collisions (2.6) are always refused**, every scope, strict or not (folded into 1.2 at the user's go-ahead). - **PATH:** per-user links, then `/glacier/sys/links/bin`, then the system (`/usr/bin` on Everest), in the Docker images AND Everest's `/etc/profile`. Non-base system packages must be on `PATH`; base programs must be runnable by every user. - `everest-bootscripts` is the name of the base `/etc` package. Its source lives in `~/Projects/everest/pkgsrc/`. - **glacier-bootstrap (2026-10-06):** template = `NAME=` + `PKG=name ver [sha256:hex]` lines, no `SYS_PROFILE` (unknown key until ROADMAP 3.3); it runs the `syspkg` on PATH (doas unless root); a template dependency must be listed at that version OR LATER (the 2.3 rule); Everest's initramfs is built through it. - **Installing Everest (2026-10-07):** possible from a live Everest system OR any Linux host; the LIVE SYSTEM is the preferred, officially supported way (so base/the live medium must eventually carry what a manual install needs). The user does the first QEMU disk install themselves: don't start it. - **Everest supports glibc as well as musl** (2026-10-07), Gentoo/Void style: the user picks the libc at install time, one per system. Today everything is built for musl (Alpine); glibc needs ROADMAP 4.1's profiles. How packages are kept apart is still open (ROADMAP 6). - **Signing (2026-10-06, REPO_DESIGN 10e/10f):** Everest's SECRET key stays outside the project (`~/.config/everest/keys`, mounted read-only); a base KEEP_OLD backup refusing under `-lO` (unsigned) is accepted until the restore interface. ## Core architecture **`gl_context_t`** (transaction.h) is the central abstraction. Fields, in order: `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`, then: | Field | Set by | Meaning | |---|---|---| | `overwrite` | caller (after init, which zeroes it) | `-O`: the next link pass may displace foreign files | | `sysroot` | `*_at` init | install root, `""` = the running system | | `link_refusals` | link pass; commit copies the relink's count back | paths refused for this transaction's packages | | `strict` | caller (after init, which zeroes it) | refuse those before committing instead | New fields go at the END, and `lib/glacier_cdef.lua` must mirror the struct exactly (FFI has no compiler to catch a mismatch). **Status codes worth knowing:** `GL_TXN_ERR_DEPS = 9` covers every pre-commit refusal (dependency, conflict, file collision, strict link refusal); the library has already printed why, so the CLIs print nothing more. Live-context equivalents: `GL_INSTALL_ERR_DEPS = 8`, `GL_REMOVE_ERR_DEPS = 4`. `GL_INSTALL_ERR_SCOPE = 7` is base-in-user- scope. **Install root / sysroot** (ROADMAP 1.1, done 2026-10-05): - `gl_init_live_context_at` / `gl_init_stage_context_at` / `gl_init_sys_at` take a sysroot; the old functions wrap them with `"/"`. `gl_normalize_sysroot` (common.h) refuses relative paths and `.`/`..` components. **User scope refuses any sysroot** other than `/`. - The context's paths are **INSTALL paths** (`T/glacier/sys/store`), so every filesystem operation uses them unchanged: a forgotten translation writes into T, never the host. That's deliberately the reverse of IMAGE_DESIGN's wording (runtime paths stored, `gl_at` on every operation), with the same effect and a safer failure mode. - Translate ONLY where a path's runtime meaning matters: `gl_runtime_path(ctx, p)` for link targets, `is_glacier_symlink` prefixes and `.displaced` keys; `gl_at(ctx, runtime, out)` to check a link's target (prune). `/usr` and `/etc` destinations are `ctx->sysroot` + path. - **Trust check** (`gl_check_sysroot` in transaction.c, also inside every `*_at` init and `gl_init_sys_at`; helper `gl_sysroot_trusted` in common.h): the root and every ancestor must exist and be root-owned, non-symlink directories, not group/world-writable. So `/tmp/x` is refused. Messages: `Refusing install root R: P is a symlink.` / `is missing.` / `is not root-owned, or is group/world-writable.` - Test: `tests/sysroot_alpine.sh` (host `/usr`, `/glacier`, `/etc` byte-identical; `chroot T` runs a linked binary; 8 mutations caught). - Not translated, on purpose: `KEEP_OLD` is read from the HOST's `/etc/glacier/glacier.conf` even under `-R` (the build host's policy); user scope's dependency view of "the system" is always the host's. **Layout.** Each scope owns ONE base directory holding `{index,store,links,stage,old,lock,backup}`. Every create, rename and lock a transaction does stays inside it. | Scope | Base dir | Ownership | |---|---|---| | `GL_SCOPE_USR` | `/glacier/usr//` | uid, mode 0700, inside a root-owned 0755 `/glacier/usr` | | `GL_SCOPE_SYS` | `/glacier/sys/` | root, world-readable: dirs 0755, index files 0644 (`gl_scope_dir_mode`/`gl_scope_file_mode` in transaction.h), whatever the umask | - The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment are in `common.h`. - `gl_init_stage_context` refuses a user base that isn't a real directory (lstat) owned by its uid with no group/other bits, or whose `/glacier/usr` is loosened. `gl_usr_base_status` exports that check, so gpkg can say why before any prompt. **Transaction lifecycle.** `gl_init_live_context` / `gl_init_stage_context` → `gl_commit_transaction` or `gl_abort_transaction`. - Stage init takes the lock FIRST, then clears any leftover stage. - Stage contexts hardlink-seed `index`/`store` from live, so removing a live package inside a transaction needs no special casing. Seeding copies dotfiles: only `.` and `..` are skipped. - A stage on another filesystem fails at seeding with `GL_TXN_ERR_XDEV`. - **Commit:** first `gl_check_txn` (deps, conflicts, collisions, and strict link refusals if `ctx->strict`); any problem → nothing changes, `GL_TXN_ERR_DEPS`. Then rename-based promotion (stage → live, previous live → `old/`), then fsync the base. Then `gl_relink_store` regenerates links from a *fresh live context* and rescans the WHOLE store (every commit relinks every package, so link-time logic must be idempotent); its `link_refusals` is copied back into the caller's context. `old/` is then deleted. (The index is rebuilt inside the stage by each install/remove, and promoted with it.) - **Live contexts** (no transaction; tests and third-party callers): `gl_install_pkg` runs `gl_check_plan` itself, and `gl_remove_pkg` removes the package's links by pruning (`gl_relink_store`), like a commit. - **Index files** are named to the second and seeding hardlinks them: never open a seeded file for writing, always write elsewhere and `rename()` (bug 25). **Link routing** (per package, in `gl_link_pkg`): | Scope | Repo | Destination | |---|---|---| | USR | any | `/links` | | SYS | `base` | `/usr` | | SYS | anything else | `/glacier/sys/links` | - A package's leading `usr/` is stripped. - In system scope, a package's top-level `etc/` is never linked: it's COPIED into `/etc` (see `/etc` config handling below). **Repos.** `base` (the minimal system; boot-critical by definition, a repo-level rule), `extra`, `community`. `extra` and `community` behave identically. **Hidden files in the store** (dot-prefixed, so linking, index rebuild and the store scanners skip them, while seeding carries them through transactions): | File | Purpose | |---|---| | `/.glacier-manifest` | the package's manifest, saved at install, used by `gl_backup_pkg` | | `/.glacier-fresh` | one-shot "installed in this transaction"; link conflicts are only reported when present | | `/.glacier-overwrite` | one-shot `-O` permission for this package's next link pass | | `/.glacier-auto` | installed as a dependency, not by name (index field 4 = `auto`); set by `gl_set_pkg_auto`, nothing sets it until 3.1 | | `/.displaced/` | originals displaced by `-O`, restored when the link that replaced them is pruned; keyed by RUNTIME path (no sysroot prefix) | | `/.etc-state/` | the last version of each `/etc` file a package offered | **Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax). - It's read with `gl_conf_get_int(path, key, &out)` in `pkg.c`. Missing file, missing key and wrong type are distinct statuses, and there are no defaults. - The shipped copy is `glacier-ng/etc/glacier/glacier.conf`. Every Dockerfile installs it. ## Safety invariants: read before touching links/symlink code `/usr` is NOT glacier-exclusive territory. Treat every link destination as possibly shared: - **Ownership test.** `is_glacier_symlink(path, store_prefix)` is the only test for "did glacier create this". Never touch anything it returns false for, except through `-O`'s displace/restore. - **Overwriting.** `gl_link_pkg` won't overwrite a non-glacier path without the `-O` marker. It never descends through *another package's* directory symlink into that package's store directory. - **Pruning.** `gl_relink_store` PRUNES stale links; it never wipes a links destination. A link is stale only if its own store target is gone (checked with `lstat`, not by following it): a package symlink may legitimately dangle. - **Stage links.** The stage links tree is never seeded. - **Extraction never writes through a symlink.** - Symlink entries are created only after all regular content. - Any entry whose parent is a symlink is refused (`crosses_symlink`, plus `O_NOFOLLOW`). - Any entry with `..` or an absolute path refuses the whole package. - A failed extraction removes its half-made version directory. - **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`, and walks by descriptor (`openat(O_NOFOLLOW)`, `unlinkat`): an entry swapped for a symlink is unlinked, never followed (bug 31). Every caller passes a glacier-only path; keep it that way. - **Root reading a user's tree** (`add_entry_at`, `gstore -b`) is fd-relative with `O_NOFOLLOW` too (bug 32). - **Reserved names.** A package's top-level `files/.glacier-*` entries are refused (`GL_MARKER_PREFIX`, common.h): those names are glacier's own version-dir files, and every marker write uses `O_NOFOLLOW` (bug 30). The marker names are defined ONCE, in common.h. - **Manifests are untrusted**: `gl_gpm2gpkg` refuses `@include` and dependency/conflict entries without a valid name (bug 33). The CLIs read manifests ONLY through the library (`manifest_of` in gpkg_common.lua), never by pattern matching (bug 34). `gl_delete_user` lstat-checks its target first, because a symlinked base would otherwise have its *target* emptied. - **Wide wrappers.** Don't add convenience wrappers that silently touch a broader filesystem scope than asked. This codebase has been bitten twice by exactly that. ## Feature reference (current behaviour) **Integrity verification** (`gpkg_common.lua`: `verify_and_copy`, `verify_all`): - Each package is copied into a private `mktemp -d` directory while being hashed with `libglacier_verify.so`. The result is checked against `FILE.sha256` (`sha256:`). - Everything after that (summary, manifest read, `gl_install_pkg`) reads ONLY the copy. - A missing or malformed sidecar counts as a mismatch and is refused before the prompt; one bad package refuses the whole batch. No bypass exists. - This lives in the CLIs' shared module, not the library (as SECURITY_DESIGN section 4 specifies), so a third-party caller of `gl_install_pkg` is not verified (ROADMAP 3.2). - The base→USR refusal (`GL_INSTALL_ERR_SCOPE = 7`), by contrast, *is* in the library. **`-O` / displace:** - A displaced foreign file or symlink moves to `/.displaced/`: renamed, or on `EXDEV` copied (keeping mode, owner and mtime) and then unlinked. - Directories, special files and paths that already have a backup are still refused. - In user scope `-O` is effectively a no-op, because the links tree is rebuilt from scratch on every commit. **Version replacement** (`-lO`, `M.install` in `gpkg_common.lua`): 1. Installed entries are read from the live index's `current` file (`repo::name::version::auto|explicit` lines; files before 2026-10-05 stop at the version, and every parser accepts both). 2. A same name+repo with a different version is a replacement. Without `-O` it's refused; with `-O`, `keep_old()` reads `KEEP_OLD`, and the summary shows `old -> new`. 3. In the transaction: `gl_backup_pkg(live, repo, name, X.gpkg.part)`, then `gl_remove_pkg` from the stage, then install. 4. After the commit: rename `.part` → `/backup///-.gpkg`, write its sidecar, and run `gl_prune_backups(dir, KEEP_OLD)` (newest by nanosecond mtime). - On any abort, `finish()` deletes the `.part`. - A backup is a complete installable `.gpkg`, so `-lO backup.gpkg` restores it. **`/etc` config handling** (`install_config` / `prune_config` in istoreutils.c), system scope only: | Situation | Result | |---|---| | `/etc/F` missing, no record | install | | missing, record exists | the admin deleted it: stays deleted | | unchanged since glacier wrote it | updated in place | | admin-modified, or never glacier's | never touched; a package change is offered ONCE as `F.new` | | no package ships it any more | removed if unchanged, kept otherwise | User-scope `etc/` is still linked (ROADMAP 2.9). **Top-level tree.** `gl_create_fs_tree(root, verbose)` (exported) creates the merged-/usr tree: `/bin` and `/sbin` → `usr/bin`, `/lib` → `usr/lib`, `/usr/sbin` → `bin`, all RELATIVE links. It also creates `/usr/{bin,lib}`, `/etc`, `/dev`, `/proc`, `/sys`, `/run`, `/tmp` and `/var/tmp` (1777), `/var`, `/root` (0700) and `/home`. - It creates only what's missing, of any type. - `gl_init_sys` runs it before writing the index. The index marks `-n` as complete, so a second `syspkg -n` is refused: `Cannot create /glacier/sys: already exists`. - It takes a root prefix; `gl_init_sys_at` / `syspkg -R DIR -n` use it. **Dependencies and conflicts** (ROADMAP 2.3; `src/deps.c`, built into `libglacier_istoreutils.so`; rules in `deps.h`): - `gl_check_plan(live, adds, n, "repo::name" removes, n)` runs before the prompt (`check_plan` in `gpkg_common.lua`). `gl_check_txn(stage)` compares stage with live, and `gl_commit_transaction` calls it itself (`GL_TXN_ERR_DEPS = 9`). A live context runs the plan check inside `gl_install_pkg`/`gl_remove_pkg` (`GL_INSTALL_ERR_DEPS = 8`, `GL_REMOVE_ERR_DEPS = 4`). So no library caller can skip it. - It reads each version dir's `.glacier-manifest`; a version without one has no deps. Only what the transaction changes is checked. - `-x` of a name installed in several repos is only checked at commit (its repo is chosen after the prompt). - Claude's own calls, not reviewed by the user: conflicts only apply within one scope; only what a transaction changes is checked. **Strict link check** (`check_links` in deps.c; `ctx->strict`): system scope only, for this transaction's packages, mirrors `gl_link_pkg`: a foreign file/symlink at a file's destination (unless `-O` and no earlier `.displaced` copy), a non-directory where a directory goes, or a directory over another package's symlink → `[x] Refusing to overwrite non-glacier path: P` / `Refusing to merge directory ...`. A foreign symlink where a directory goes (`usr/sbin -> bin`) is followed, as the link pass does. **File collisions** (ROADMAP 2.6, `check_collisions` in deps.c): two packages whose entries land on the same path (by `dest_of`, which mirrors `gl_link_pkg`) are refused in every scope, before the prompt and at commit, when either is in the transaction: `[x] extra::a and extra::b both ship /glacier/sys/links/bin/x.` Sharing a directory is fine; a file or symlink where the other has a directory is a collision. **Signatures** (REPO_DESIGN section 7; `gpkg_common.lua`): in system scope, a package whose LIBRARY-read manifest (`gl_read_pkg_manifest`, never `read_manifest_fields`) says base needs `FILE.sha256.sig` over the exact sidecar bytes verified, by the key with that keynum among those `/etc/glacier/repos.conf` lists for base (`gl_conf_repo_keys`). Else `[x] base::NAME is not signed by a key trusted for base.` before the prompt. repos.conf and keys (and all ancestors, real paths too) must be root-owned, not group/world-writable. Under `-R`, the HOST's keyring for this check; `-y`/`-f`/`-u` under `-R` use T's (inside T only). No repos.conf = nothing trusted. gpkg / other repos: sidecar only. **Repository indexes** (REPO_DESIGN sections 4-5; `M.refresh` in `gpkg_common.lua`): `syspkg -y` / `gpkg -y` fetch `URL/repo.idx` + `.sig` for every repos.conf entry with a url, check the signature over the fetched bytes with a key listed for that repo, `gl_index_parse` the same bytes, refuse SERIAL below the accepted one (or equal with other bytes) and an expired VALID_UNTIL, then rename into `/repos/REPO.idx` (+ `.sig`). Any failure keeps the accepted copy; every repo is tried; exit 1 if any failed. **`-f` / `-u`** (REPO_DESIGN section 6; `M.fetch`/`M.upgrade`): refresh first (any failure refuses), pick each name from the first repo serving it, add missing deps (auto) and upgrade too-old ones, download into `/cache/REPO` (hash and SIZE from the index), copy privately with the index's hash, require the manifest to equal the index entry (`[x] extra::x 1.0.0 does not match its index.`), then the `-l` path (`install_copies`): summary with `(dependency)`, 2.3 checks, prompt, staged transaction, `gl_set_pkg_auto`, backups, cache files deleted after commit. A base package fetched this way needs no `.sig` (the base index vouches). `-f` of an installed other version needs `-O`; `-u` and dependency upgrades don't. gpkg never fetches from base. **Package symlinks** are extracted verbatim and linked like files. Hardlink, device and fifo entries are skipped. `add_to_archive` (used by backups and `gstore -b`) archives symlinks. ## CLIs (glacier-ng) | Tool | Flags | Runs as | |---|---|---| | `gpkg` | `-l FILE...` `-f NAME...` `-u [NAME...]` `-x NAME...` `-y` `-O` `-S` `-V` `-h` `-v` | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` | | `syspkg` | `-n` `-l FILE...` `-f NAME...` `-u [NAME...]` `-x NAME...` `-y` `-O` `-S` `-V` `-R DIR` `--strict` `-h` `-v` | root via doas; `-h`/`-v` without root | | `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (refuses: not implemented) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) | | `glacier-bootstrap` | `-d PKGDIR TEMPLATE TARGET` `-n` `-t` `-V` `-h` `-v` | the user; borrows root only for `doas syspkg` / `doas chroot` | - **gpkg:** modifiers `-O`/`-S`/`-V` are applied in a pre-pass, so their position doesn't matter (`-lO` == `-Ol`). It finds its user via `/proc/self/loginuid`, falling back to `getuid()`. Unknown flags are refused (`[x] Unknown option: -R`); gpkg has no `-R`/`--strict`. - **Endings (both CLIs):** `[✓] Completed with no errors.` (exit 0), or after link refusals `[x] N paths were not linked.` (exit 1; `report_done` in gpkg_common.lua). Refusals before the prompt exit 1 with only the `[x]` lines. - **syspkg, `-n`:** creates the tree plus `/glacier/sys`. - **syspkg, `-R DIR` / `--root DIR` / `--root=DIR`** (last in a bundle: `-lR DIR`): operate on the system under DIR, through the `*_at` contexts. Refused first by `gl_check_sysroot`: DIR and every ancestor must be root-owned, non-symlink directories, not group/world-writable (`/tmp/...` is refused). Implies `--strict`. - **syspkg, `--strict`:** anything the link pass would refuse fails the whole transaction before the prompt (and at commit, `ctx->strict`); `-O` still displaces. - **syspkg as non-root:** `-n`/`-l`/`-x` are refused after argument parsing with `Cannot open system index: Permission denied`. - **gstore, `-n` without a UID, under doas:** acts for the invoking user (loginuid, then `DOAS_USER`, then getuid). The doas rule `permit nopass :wheel as root cmd /path/gstore args -n` is the boundary that stops a user naming another UID. - **Shared code:** `lib/gpkg_common.lua` (`M.install`, `M.remove`, verification, replacement, backups). - **The `-x` summary** comes from the live index; a name that isn't installed fails before the prompt. - **Hardened loading** (syspkg and gstore): `#!/usr/bin/luajit -E`, `package.path` and every `ffi.load` pinned to their own `realpath`, and when euid is 0, every loaded `.lua`/`.so`, and every ancestor of the script's realpath AND of each loaded file's realpath, must be root-owned and not group/world-writable (bug 29). The preamble is DUPLICATED in both files on purpose: change one, change both. - **gpkg** (2026-10-06) loads `lib/` from its own realpath, like glacier-bootstrap (no ownership checks; it's unprivileged), so an installed `/usr/bin/gpkg` works from any directory. - **glacier-bootstrap** (GLACIER_BOOTSTRAP section 10): parse → verify (copies each `PKGDIR/NAME-VER.gpkg` into a private `mktemp -d` while hashing; sidecar + pin; writes the copy's sidecar from that hash) → resolve (reads manifests of the COPIES via `gl_read_pkg_manifest`: name/version match, deps listed at >= version, conflicts) → `syspkg -R T -n` (skipped if T is initialised with nothing installed) → `syspkg -R T -l COPIES` (one strict transaction; its prompt passes through) → checks T's index lists every package (syspkg exits 0 when declined) → `syspkg -R T -y` if T's repos.conf has a url → `-t` smoke test. Never writes into T itself. Exit: 1 usage/target, 2 template, 3 verify, 4 resolve, 5 install, 6 smoke, 7 repositories. Loads its `lib/` from its realpath, no root checks. No `-r` / `--pin` yet. - **Declarations:** `lib/glacier_cdef.lua` is the single source of truth for every `ffi.cdef`. `tests/glacier_ffi.lua` is only a wrapper around it. Declaration ORDER matters: a declaration using `gl_context_t` must come after its typedef. ## Build tooling - **gbuild** (bash, the only copy is `glacier-ng/gbuild`): - `-n -V -g -c -d -m -D -C -b -i -o -p -j -F -t -x -u -r -k -s` (`-k KEY.sec`: sign the sidecar with gsign, `$GSIGN_BIN` / build/bin / PATH, key tried before building); `-v`/`--version` prints the version, `--verbose` turns on verbose output. - Autodetects the build system, with autotools checked BEFORE a bare Makefile. - Writes `NAME-VER.gpkg` plus a `.sha256` sidecar, using `gsha256sum` from `$GSHA256SUM_BIN`, else `