# 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, at the end of a long 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`: PROPOSAL for system images (bootstrap OUTPUT, the sysroot library change). Open decisions are in section 9. - `GLACIER_BOOTSTRAP.txt`: PLAN for `glacier-bootstrap`. Prerequisites are in section 7, decisions in section 9 (9c and 9d are decided). - `SECURITY_REVIEW_2026-07-16.md`: older review findings. Nothing from the 2026-10-04 session is committed. libglacier-ng is on branch `dev` with a large uncommitted diff (`src/`, the docs). Commit only when the user asks. ## 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 | `glacier-ng/lib/glacier/` holds `.so`s built on the host (glibc). They won't load on musl. Every Alpine image and test builds libglacier-ng from source instead. ## 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. - **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. - **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. ## 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. Both wait for remote repos + signing (ROADMAP 3.1). Don't implement them against local files. - `everest-bootscripts` is the name of the base `/etc` package. Its source lives in `~/Projects/everest/pkgsrc/`. ## Core architecture **`gl_context_t`** (transaction.h) is the central abstraction. Fields: `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`, and `overwrite` (last field, zeroed by the init functions). **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 | - 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:** rename-based promotion (stage → live, previous live → `old/`), then fsync the base. Then `gl_relink_store` + `gl_rebuild_index` regenerate links and index from a *fresh live context* and rescan the WHOLE store (every commit relinks every package, so link-time logic must be idempotent). `old/` is then deleted. **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 | | `/.displaced/` | originals displaced by `-O`, restored when the link that replaced them is pruned | | `/.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 `.`/`..`. Every caller passes a glacier-only path; keep it that way. `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` lines). 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, ready for `syspkg --root`. **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...` `-x NAME...` `-O` `-S` `-V` `-h` `-v`; `-f`/`-u` are stubs | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` | | `syspkg` | `-n` `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v` | root via doas; `-h`/`-v` without root | | `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (stub) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) | - **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()`. - **syspkg, `-n`:** creates the tree plus `/glacier/sys`. - **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 ancestor directory and every loaded `.lua`/`.so` must be root-owned and not group/world-writable. The preamble is DUPLICATED in both files on purpose: change one, change both. - **gpkg** uses the older CWD-relative convention (`require("lib/...")`, `ffi.load("./lib/glacier/...")`): run it from its own directory. - **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 -s`; `-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 `