# 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 `