58 KiB
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
ROADMAP.txt: what's left, in priority order, plus the decisions that are still open (section 6). Pick work from it.- This file: how things work, what the user wants, how to test.
- 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,/etcconfig 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--pinare 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;<base>/cache/REPO, checked likereposvia the shared staticchecked_subdir);log.chelp 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.installis nowverify_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/-umodes; 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). Thegpkg -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 hias a wheel user now says[x] No remote repositories are configured.(shipped repos.conf has no url; no curl in base).container.sh initramfsnowmkdir -ps$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 inall, 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 inread_repos_conf;transaction.c:gl_scope_state_dir(ctx, "repos", out)(creates/checks<base>/repos).log.chelp lists-y. - glacier-ng:
M.refresh(opts)ingpkg_common.lua(fetch file:// or curl https, verify with a key listed for THAT repo, parse, rollback / same-serial / expiry checks, rename into<base>/repos, under the scope lock);syspkg -y(needs root and-n;-Rworks),gpkg -y(system repos.conf + keyring until step 6). Dockerfiles copy grepo to /usr/local/bin. New suitetests/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 (
-fwith dependency closure, then-u; the cache in<base>/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 statusesGL_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_copyreturns 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.shmake 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 keygenmadeeverest-2026; the SECRET key is~/.config/everest/keys/everest-2026.sec(user's choice: outside the project), mounted read-only forbuild, which refuses without it. Every package is builtgbuild -k. everest-bootscripts 1.2.0 shipsetc/glacier/repos.conf(base: everest-2026) andetc/glacier/keys/everest-2026.pub;container.sh initramfsinstalls the same into the build container's/etc/glacier. The signedbuild allwas cut off after busybox (laptop died); the other 9 were rebuilt-kthe evening of 2026-10-06,initramfsran 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-Werrorflags.src/sign.{c,h}: signify formats (strict parse, canonical base64),gl_sign_read_pubkey/_read_sig/_check_seckey/_verify/_verify_file/_file/_generate, statusesGL_SIGN_OK..GL_SIGN_ERR_WRITE = 9. Built intolibglacier_verify.so(NOT into the combinedlibglacier-ng.so).src/gsign.c(-G/-S/-V, lg_printf messages naming the file at fault), Makefile targetsgsign(inall, installed to bin like gsha256sum).glacier-ng/tests/sign_alpine.sh(alpine:latest, mounts /libsrc): round trips, signify interop both ways (Alpinesignify), encrypted and corrupted secret keys, malformed key/sig files, 64 MiB limit, O_NOFOLLOW, key modes, a direct FFI call ofgl_sign_generate. Passes in full. (luajit -e CODE ARGruns ARG as a script, so the FFI snippet is written to/gen.luafirst.)- Mutation checks: 12 mutants, all caught except m9 (padding position,
j < 2), which is EQUIVALENT: kept, it prevents3 - padunderflowing. 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 andM.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.shbuilds 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 :wheelplus nopassgstore args -nfor BOTHcmd gstoreandcmd /usr/bin/gstore(OpenDoas matchescmdagainst what was typed). templates/minimal.templateandPACKAGESin everest.sh list all 12 packages (everest-repos added 2026-10-07).Dockerfile.buildhas 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 .sos 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 .sos.
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_printflevels:[x]is error (2),[!]warning (1),[i]info (0),[✓]success (3). Messages are capitalized. Exact strings the user chose includeCannot open system index: Permission denied(no period) andNothing to do.(success level).- The pre-trust-check
die()in syspkg/gstore reproduces the[x]format byte for byte, because it can't loadlibglacier_log.sobefore that file has been verified.
- The pre-trust-check
- 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/<uid>/..., root-owned parent). Both root and the user can create it: root runsgstore -n UID; a user runsdoas gstore -nthrough a doas rule pinned toargs -n(option "B"). syspkg -ncreates the top-level tree itself (GLACIER_BOOTSTRAP 9d)./etcconfig files: option 1. Copy on install,.newon conflict (GLACIER_BOOTSTRAP 9c).-Owith-l= replace.-Odisplaces non-glacier files, and-lOalso replaces an installed different version (upgrade or downgrade from a local file). Plain-lwith another version installed refuses in one line.KEEP_OLDin/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.
- It has no default. It ships as
gpkg -u/syspkg -uupgrade from a REMOTE repository, andgpkg -fis-lbut 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;-RIMPLIES--strict;--strictalone 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/binon Everest), in the Docker images AND Everest's/etc/profile. Non-base system packages must be onPATH; base programs must be runnable by every user. everest-bootscriptsis the name of the base/etcpackage. Its source lives in~/Projects/everest/pkgsrc/.- glacier-bootstrap (2026-10-06): template =
NAME=+PKG=name ver [sha256:hex]lines, noSYS_PROFILE(unknown key until ROADMAP 3.3); it runs thesyspkgon 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_attake 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_aton 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_symlinkprefixes and.displacedkeys;gl_at(ctx, runtime, out)to check a link's target (prune)./usrand/etcdestinations arectx->sysroot+ path. - Trust check (
gl_check_sysrootin transaction.c, also inside every*_atinit andgl_init_sys_at; helpergl_sysroot_trustedin common.h): the root and every ancestor must exist and be root-owned, non-symlink directories, not group/world-writable. So/tmp/xis 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,/etcbyte-identical;chroot Truns a linked binary; 8 mutations caught). - Not translated, on purpose:
KEEP_OLDis read from the HOST's/etc/glacier/glacier.confeven 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>/ |
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_checkand the layout comment are incommon.h. gl_init_stage_contextrefuses a user base that isn't a real directory (lstat) owned by its uid with no group/other bits, or whose/glacier/usris loosened.gl_usr_base_statusexports 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/storefrom 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 ifctx->strict); any problem → nothing changes,GL_TXN_ERR_DEPS. Then rename-based promotion (stage → live, previous live →old/), then fsync the base. Thengl_relink_storeregenerates links from a fresh live context and rescans the WHOLE store (every commit relinks every package, so link-time logic must be idempotent); itslink_refusalsis 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_pkgrunsgl_check_planitself, andgl_remove_pkgremoves 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 | <base>/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/etcconfig 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 |
|---|---|
<ver>/.glacier-manifest |
the package's manifest, saved at install, used by gl_backup_pkg |
<ver>/.glacier-fresh |
one-shot "installed in this transaction"; link conflicts are only reported when present |
<ver>/.glacier-overwrite |
one-shot -O permission for this package's next link pass |
<ver>/.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 |
<store>/.displaced/<runtime abs path> |
originals displaced by -O, restored when the link that replaced them is pruned; keyed by RUNTIME path (no sysroot prefix) |
<store>/.etc-state/<path> |
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)inpkg.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_pkgwon't overwrite a non-glacier path without the-Omarker. It never descends through another package's directory symlink into that package's store directory. - Pruning.
gl_relink_storePRUNES stale links; it never wipes a links destination. A link is stale only if its own store target is gone (checked withlstat, 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, plusO_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_recursiveskips 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 withO_NOFOLLOWtoo (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 usesO_NOFOLLOW(bug 30). The marker names are defined ONCE, in common.h. - Manifests are untrusted:
gl_gpm2gpkgrefuses@includeand dependency/conflict entries without a valid name (bug 33). The CLIs read manifests ONLY through the library (manifest_ofin gpkg_common.lua), never by pattern matching (bug 34).gl_delete_userlstat-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 -ddirectory while being hashed withlibglacier_verify.so. The result is checked againstFILE.sha256(sha256:<hex>). - 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_pkgis 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
<store>/.displaced/: renamed, or onEXDEVcopied (keeping mode, owner and mtime) and then unlinked. - Directories, special files and paths that already have a backup are still refused.
- In user scope
-Ois effectively a no-op, because the links tree is rebuilt from scratch on every commit.
Version replacement (-lO, M.install in gpkg_common.lua):
- Installed entries are read from the live index's
currentfile (repo::name::version::auto|explicitlines; files before 2026-10-05 stop at the version, and every parser accepts both). - A same name+repo with a different version is a replacement. Without
-Oit's refused; with-O,keep_old()readsKEEP_OLD, and the summary showsold -> new. - In the transaction:
gl_backup_pkg(live, repo, name, X.gpkg.part), thengl_remove_pkgfrom the stage, then install. - After the commit: rename
.part→<scope>/backup/<repo>/<name>/<name>-<ver>.gpkg, write its sidecar, and rungl_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.gpkgrestores 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_sysruns it before writing the index. The index marks-nas complete, so a secondsyspkg -nis refused:Cannot create /glacier/sys: already exists.- It takes a root prefix;
gl_init_sys_at/syspkg -R DIR -nuse 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_planingpkg_common.lua).gl_check_txn(stage)compares stage with live, andgl_commit_transactioncalls it itself (GL_TXN_ERR_DEPS = 9). A live context runs the plan check insidegl_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. -xof 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
<base>/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 <base>/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/-Vare applied in a pre-pass, so their position doesn't matter (-lO==-Ol). It finds its user via/proc/self/loginuid, falling back togetuid(). 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_donein 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*_atcontexts. Refused first bygl_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);-Ostill displaces. - syspkg as non-root:
-n/-l/-xare refused after argument parsing withCannot open system index: Permission denied. - gstore,
-nwithout a UID, under doas: acts for the invoking user (loginuid, thenDOAS_USER, then getuid). The doas rulepermit nopass :wheel as root cmd /path/gstore args -nis the boundary that stops a user naming another UID. - Shared code:
lib/gpkg_common.lua(M.install,M.remove, verification, replacement, backups). - The
-xsummary 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.pathand everyffi.loadpinned to their ownrealpath, 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/gpkgworks from any directory. - glacier-bootstrap (GLACIER_BOOTSTRAP section 10): parse →
verify (copies each
PKGDIR/NAME-VER.gpkginto a privatemktemp -dwhile hashing; sidecar + pin; writes the copy's sidecar from that hash) → resolve (reads manifests of the COPIES viagl_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 -yif T's repos.conf has a url →-tsmoke test. Never writes into T itself. Exit: 1 usage/target, 2 template, 3 verify, 4 resolve, 5 install, 6 smoke, 7 repositories. Loads itslib/from its realpath, no root checks. No-r/--pinyet. - Declarations:
lib/glacier_cdef.luais the single source of truth for everyffi.cdef.tests/glacier_ffi.luais only a wrapper around it. Declaration ORDER matters: a declaration usinggl_context_tmust 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/--versionprints the version,--verboseturns on verbose output.- Autodetects the build system, with autotools checked BEFORE a bare Makefile.
- Writes
NAME-VER.gpkgplus a.sha256sidecar, usinggsha256sumfrom$GSHA256SUM_BIN, else<script dir>/build/bin, else PATH. - There is NO
-Psystem-profile support, and gworld has noSYS_PROFILEfield (ROADMAP 4.1). The target libc is decided by where gbuild runs: Alpine for musl.
- gworld (Lua): batch orchestrator, reads
recipes/*.recipe, sorts topologically byDEPS, runs gbuild per package. ~/Projects/everest: the gbuild command for each package lives inscripts/container.sh;everest.shruns it inside an Alpine image (Dockerfile.build).- Kernel version: linux
6.12.112; plainv6.12doesn't build with GCC 15. - Everest's image IS built through glacier (2026-10-06):
container.sh initramfscopies glacier-ng to a root-owned/opt/glacier-ng(with the musl-built.sos; syspkg's trust checks need that) and runsglacier-bootstrap -t -d /everest/pkgs templates/minimal.template /target(root-owned: not under/tmp). T's links name RUNTIME paths, so anything reading through them from the build host must resolve inside T (vmlinuz is read withchroot T cat). Onlyrootexists in the shipped/etc/passwd. - gbuild ends with the package's contents list. Its "To install with gpkg: gpkg -l ..." hint was removed 2026-10-06 at the user's request.
libglacier-ng build
- Separate
.soper module (libglacier_log,_pkg,_istoreutils,_transaction,_verify;_dagwas removed 2026-10-06, unused). This split is deliberate: a third-party program links only what it needs. - The combined
libglacier-ng.so(make libglacier-ng) exists only for the FFI tests. libglacier_verify.sois private: no public header.- Tools in
build/bin:gsha256sum,gsign,grepo(gsign and grepo compile the sources they need in, no rpath). Maintainer tools, later "glacier-tools": Everest's libglacier-ng package dropsusr/bin. - Warnings:
config.mkbuilds with-Werror.-Wno-format-truncationis set because that warning is noise;-Wstringop-truncationstays fatal. - Makefile quirk:
make logbuilds onlylog.o; the shared object comes frommake log_test(or plainmake).
Testing
Run everything in throwaway containers. Several suites write to /usr,
/etc and /glacier.
FFI suite (glacier-ng/tests/*.lua; 11 files, all passing):
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
-v ~/Projects/libglacier-ng:/libsrc:ro alpine:latest sh -c '
apk add --no-cache build-base luajit luajit-dev libarchive-dev libconfig-dev linux-headers tar >/dev/null
cp -a /libsrc /build && cd /build && make clean && make && make libglacier-ng
cp -a /src /g && cd /g/tests && GLACIER_TEST_CONFIRM=yes \
LIBGLACIER_SO=/build/build/lib/shared/libglacier-ng.so luajit run_all.lua'
GNU tar is required: the hostile-archive fixtures use tar --append.
Shell suites (glacier-ng/tests/*.sh):
| Suite | Image |
|---|---|
sign_alpine.sh |
alpine:latest with /libsrc and /t.sh mounted |
syspkg_alpine.sh, overwrite_alpine.sh |
alpine:latest with /src, /libsrc and /t.sh mounted (see each header); overwrite_alpine.sh runs both as-is and with --tmpfs /glacier:exec |
verify_alpine.sh, user_layout_alpine.sh, upgrade_alpine.sh, sys_perms_alpine.sh, deps_alpine.sh, sysroot_alpine.sh, root_alpine.sh, bootstrap_alpine.sh, installed_alpine.sh, keyring_alpine.sh, repo_alpine.sh, audit_alpine.sh, fetch_alpine.sh |
glacier-alpine_root |
Build the glacier-alpine_root image with:
cd ~/Projects/glacier-ng && docker build -f Dockerfile.alpine_root \
--build-context libglacier=../libglacier-ng -t glacier-alpine_root .
docker run --rm -v "$PWD/tests/NAME.sh:/t.sh:ro" glacier-alpine_root sh /t.sh
glacier-ng/lib/glacier/ does not exist in the tree (the .sos are
built per image); syspkg_alpine/overwrite_alpine mkdir -p it
(2026-10-07). If Docker fails with veth ... operation not supported,
the host kernel was upgraded without a reboot (modules missing): use
docker build --network host and docker run --network=host (or
--network none where nothing is fetched) until the user reboots.
Mutating a Lua file syspkg loads: mount it elsewhere and cp it over
/app/lib/... inside the container (a bind mount is user-owned, so
syspkg's trust check refuses it).
Demo packages: glacier-ng/tests/mkpkg [-o DIR] [-k KEY.sec] [-d NAME:VER]... [-c NAME:VER]... NAME VERSION REPO writes a one-script
.gpkg plus its sidecar (/app/tests/mkpkg in the images); a base one
needs -k /root/glacier-test.sec (the images' test key) for syspkg. The user plays with changes
in ./run_docker.sh Dockerfile.alpine_root using it; for an image,
mkdir -p /mnt/img && ./syspkg -R /mnt/img -n.
Mutation checks, how: cp -a libglacier-ng (and glacier-ng, for Lua
changes) into the scratchpad, break the defence there, then
docker build -f Dockerfile.alpine_root --build-context libglacier=COPY -t glacier-mut . from the (copied) glacier-ng and run the suite against
glacier-mut; delete the image after. Docker tags must be lowercase. A
mutation that doesn't compile under -Werror proves nothing: rewrite it
so it compiles. Watch for checks that pass for the wrong reason (e.g. a
refusal caused by a missing directory instead of the defence). In shell
suites, sh -c '... $OUT ...' sees an EMPTY $OUT (not exported), so a
negative check written that way passes vacuously; use a function
(lacks in bootstrap_alpine.sh). Found 2026-10-06 by a surviving
mutation. The host shell is zsh: an unquoted $VAR holding -v a:b
is NOT word-split, so a docker command built that way silently runs
without the mount (a mutant "survived" that way, 2026-10-06). To
mutate a Lua CLI without rebuilding, bind-mount the mutated file over
/app/NAME (-v $PWD/mut:/app/glacier-bootstrap:ro).
Full regression = the FFI suite + syspkg_alpine.sh +
overwrite_alpine.sh (both ways) + sign_alpine.sh + the thirteen glacier-alpine_root
suites, all in parallel; the two output-based suites (syspkg_alpine,
user_layout_alpine) have no PASS lines, so grep their output for
Unknown|traceback|failed|GL_ instead (only --bogus is expected).
Interactive images: ./run_docker.sh Dockerfile.{alpine,alpine_root,arch,debian}.
run_docker.sh passes the libglacier build context. alpine, arch
and debian default to user glacier (uid 1000, in wheel); alpine_root
defaults to root. All of them have:
- root-owned
/app; - no
/glacieruntilgstore -n/syspkg -n; - the doas rule plus
permit nopass glacier; /etc/profile.d/glacier.shputting/glacier/usr/<uid>/links/binthen/glacier/sys/links/binonPATH(andlibonLD_LIBRARY_PATH) unconditionally, since/glacieronly appears after login (fixed 2026-10-05: it had no system links and checked-dat login);glacier.confinstalled.
Boot test: cd ~/Projects/everest && ./everest.sh boot boots QEMU
(KVM). Press Enter for a root shell; poweroff -f to leave. To test as
a non-root user, adduser -D -H -h /tmp tester at runtime, then
su - tester -c ... (nothing shipped has a user besides root).
Everest's /etc/profile sets PATH to per-user links, then
/glacier/sys/links/bin, then /usr/bin (2026-10-05). Backups of
everest-bootscripts files go NEXT TO etc/, never inside it: the package
copies etc/ wholesale. After changing a package:
./everest.sh build PKG && ./everest.sh initramfs, then boot.
To drive the boot non-interactively, fork QEMU on a pty from Python
(pty.fork()), wait for Please press Enter, send lines and match the
# /$ prompt (no expect on the host).
Bug history: so these don't get reintroduced
All fixed in the current tree.
-
gl_abort_transactionnever released the lock (wrong variable). Nowopen(O_CREAT)+flock(), and lock files are never unlinked. -
No
EXDEVcheck before renaming. Commitstat()s both sides and returnsGL_TXN_ERR_XDEV. -
Segfault on install: the
transactiontarget didn't linkistoreutils/log. -
Stale links post-commit:
gl_link_pkgran during staged installs, embedding stage paths. It now runs only for LIVE contexts, and links regenerate post-commit. -
Dangling links were never cleaned on removal →
prune_stale_links. -
GL_TXN_ERR_RENAMEafter the first commit: a leftoverold/. It's now wiped first. -
-Wstringop-truncation:strncpy(..., PATH_MAX - 1)could leave strings unterminated. Usesnprintf(dst, sizeof dst, "%s", ...). -
gbuild detected a bare
Makefilebeforeconfigure. The fix had been lost from the tree and was re-applied 2026-10-04. -
The index had no version field; it's now
repo::pkgname::version. -
[The code this was about is NOT in the tree: ROADMAP 4.1.] A recipe field silently never parsed and fell back to a plausible default. Remember the class.
-
gsha256sum.cincludedsha256.cinstead of.h. -
gbuild told users to run
gpkg -f(a stub) instead ofgpkg -l. -
gpkg -f's stub referenced an undefineduid. -
Path traversal from manifests/archives:
PKG_NAME/PKG_REPOmust passgl_valid_pkg_ident, and../absolute entries refuse the package. Finding 1 (self-declaredbase) stays open until signing. -
Stage seeding and
gl_rmdir_recursiveskipped ALL dot-entries: dotfiles vanished, andold/couldn't be removed (bug 6 again). -
Per-user transactions could never work for a real user (shared writable parents needed). Fixed by the per-uid layout. Found alongside:
gstore -nnever created parent directories;- the stage was wiped before the lock was taken;
- cross-filesystem seeding was reported as
MKDIR; gl_restore_istoredoubled the uid and accepted..entries;- gstore loaded the combined
.soviaLIBGLACIER_SOfrom the environment.
-
Package symlinks were silently dropped at extraction, and only regular files were linked, so musl's loader was never installed. Alpine-based tests passed only because Alpine provides its own loader.
-
add_to_archive(gstore -b, backups) silently dropped symlinks. -
gpkg acted on
-las soon as it parsed it, so-lOignored itsO. -
tests/glacier_ffi.luaduplicated the cdefs and had drifted (it was missingGL_INSTALL_ERR_SCOPE). It's now a wrapper. -
Link conflicts were re-reported on every system commit, for every package (it now uses the
.glacier-freshmarker). -
A failed extraction left a half-made version directory, so in a live context every retry reported
ALREADY_INSTALLED. -
Backups written in the same second were pruned in the wrong order (whole-second mtimes).
-
System scope was root 0700 (index, store, links, every version dir), so no non-root user could run anything syspkg installed, through
/usror/glacier/sys/links. Found 2026-10-05. Also fixed: every mode glacier creates was filtered by the caller's umask (gl_mkdirp, seeding, extracted files), so syspkg under umask 077 made even a 0755 store private.gl_mkdirp/gl_mkdir_exactnow chmod what they create, and extracted entries getarchive mode & 07755explicitly (never group/world-writable). Test:tests/sys_perms_alpine.sh(mutation-checked). No migration: a store made before the fix keeps its 0700 dirs until reinstalled. -
gl_rebuild_indexfopen(name, "w")'d the new index file. Index names have one-second resolution and seeding HARDLINKS live's index files into the stage, so a staged install in the same second as the last commit rewrote the LIVE index in place; an aborted transaction then left live listing the stage's packages (and history files were overwritten). It now writes totmpfile()and onlyrename()s onto the name. Found 2026-10-05 bydeps_alpine.sh; regression testtests/test_index_abort.lua(mutation-checked). Rule: never open a seeded file for writing; replace it by rename. -
Live-context removal (
gl_unlink_pkg)unlink()ed the destination of every regular file in the package WITHOUTis_glacier_symlink: a foreign file the package had been refused to replace was deleted, and the package's own symlink entries were never unlinked. The CLIs always stage, so only library callers were exposed. Removed; live removal now prunes like a commit (gl_relink_store). Found 2026-10-05;tests/test_live_remove_foreign.lua(fails on the old code). -
gpkg's
handle_flaghad noelse: an unknown flag (-R) was silently ignored and the NEXT word reported as the unknown option. Now[x] Unknown option: -R(capitalized, as syspkg). 2026-10-05. -
gbuild's
clone_sourceprinted its "Shallow clone failed" warning (and the full clone's/checkout's output) to stdout, whichSOURCE_DIR="$(clone_source)"captured, so building from a commit hash (-r <sha>) always failed. Those now go to stderr. Found 2026-10-06 building luajit. -
syspkg's and gstore's trust checks walked only the ancestors of the script's own realpath. Installed,
lib/glacier -> /usr/lib/glacierand the.sos link into libglacier-ng's store dir, so a non-root- owned directory there was never checked: root loaded.sos from it. Both now also check every ancestor of each loaded file's realpath. Found 2026-10-06 bytests/installed_alpine.sh(both mutations caught). -
A package could ship top-level
files/.glacier-*entries. As a symlink,.glacier-freshmade syspkg truncate its target as root (an unsigned extra package emptied/root/victim, reproduced);.glacier-overwriteself-granted-O. Reserved names refused, marker writesO_NOFOLLOW. 2026-10-06 audit. -
gl_rmdir_recursive(lstat, then opendir by path) could be steered into a symlink's target when root deleted a user's tree (gstore -x). Now fd-relative. Race test in audit_alpine.sh. -
add_to_archive(same class,gstore -b). Now fd-relative; no test fails on the old code (window too narrow; review only). -
Manifests could
@includeany file (read as root); a dependency entry without a name cut the list short. Both refused. -
gpkg_common's
read_manifest_fields(tar + pattern match) could disagree with the library: a decoy comment hid a replacement. Nowmanifest_of(gl_read_pkg_manifest). -
Format strings: gstore's
argv[0]/UID and gpkg-u's operand reachedlg_printfas the format;%dwas given Lua numbers; gstore UIDs wrapped past uid_t;gl_abort_transactionwiped a stage without holding the lock. All fixed (lg_printfnow has a printf format attribute).
Known gaps (not bugs, not yet scheduled)
- No migration for stores made before bug 24's fix (0700 dirs stay).
- Extracted entries keep setuid/setgid bits (
& 07755), as before. - No size limit on extraction (a decompression bomb fills the disk).
- Bug 32's fix has no test that fails on the old code.
- Hash verification is still CLI-only (ROADMAP 3.2); everything else added 2026-10-05 is enforced in the library.
- A dependency refusal for
-xof a name installed in several repos comes at commit, after the prompt (its repo is chosen after it).
Backups of glacier-ng / everest files (no git there)
The rule stands (back up before editing: file.old, then .old2...).
All of glacier-ng's backups were deleted 2026-10-06 at the user's
request. The 2026-10-07 session made new ones: lib/glacier_cdef.lua.old,
lib/gpkg_common.lua.old, gpkg.old, syspkg.old,
tests/{audit,overwrite,syspkg,repo}_alpine.sh.old,
lib/gpkg_common.lua.old2/.old3, tests/repo_alpine.sh.old2/.old3,
tests/fetch_alpine.sh.old, lib/gpkg_common.lua.old4,
glacier-bootstrap.old, tests/bootstrap_alpine.sh.old,
tests/repo_alpine.sh.old4, tests/fetch_alpine.sh.old2; Everest:
scripts/container.sh.old4, everest.sh.old3, README.md.old3,
templates/minimal.template.old2, pkgsrc/everest-bootscripts/glacier.old.
Everest still has its earlier ones (everest.sh.old/.old2,
scripts/container.sh.old/.old2/.old3, README.md.old/.old2,
Dockerfile.build.old, templates/minimal.template.old,
pkgsrc/everest-bootscripts/profile.old). Leave them unless the user
says to clean up.
Conventions
- C style: tabs, but the codebase is inconsistent; match the surrounding function.
- Paths:
PATH_MAXstack buffers plussnprintffor path construction. - Lua requires: gpkg uses bare CWD-relative requires. syspkg and gstore pin their paths (see CLIs).
- Lua numbers in C varargs: a Lua number reaches C varargs as a
double, so never pass one to
lg_printf("%d"). Use%swithtostring(), orffi.new("int", n). - Shell commands from Lua: always
shell_quoteinterpolated paths (syspkg runs as root). - Verify by running. Several real bugs here were found only by executing something.