+--------------------------------------------+ | glacier-bootstrap | +--------------------------------------------+ STATUS: LOCAL MODE IMPLEMENTED 2026-10-06 (glacier-ng/glacier-bootstrap, tests/bootstrap_alpine.sh; see section 10 for how it differs from the plan below). Remote mode (-r) and the pinning helper are still a plan. Section 9 lists the decisions still open. glacier-bootstrap takes a template index and builds a chrootable Everest system in a target directory, from either a local directory of packages or a remote repository. It is the "glacier-bootstrap TEMPLATE_INDEX TARGET" tool named in SECURITY_DESIGN.txt section 10, and the `syspkg --root` step of IMAGE_DESIGN.txt's pipeline. Companion documents: IMAGE_DESIGN.txt (why images are bootstrap OUTPUT, the sysroot library change), SECURITY_DESIGN.txt (refusal matrix, syspkg's privilege model), PACKAGE_SCOPES.txt (link routing). +--------------------------------------------+ | 1. What it does | +--------------------------------------------+ glacier-bootstrap [-n] [-t] (-d PKGDIR | -r URL) TEMPLATE TARGET -d PKGDIR packages come from a local directory (gbuild/gworld output: NAME-VER.gpkg plus NAME-VER.gpkg.sha256) -r URL packages come from a remote repository (section 5) -n dry run: parse, resolve, fetch, verify, print the plan; never touch TARGET -t after installing, smoke-test: chroot TARGET /bin/sh -c true Sequence: 1. parse TEMPLATE (section 3) 2. resolve: check the package list is complete (section 4) 3. fetch into a private cache; verify every hash (section 5) 4. doas syspkg --root TARGET -n create the tree 5. doas syspkg --root TARGET --strict -l PKG... ONE transaction 6. doas syspkg --root TARGET -y TARGET's indexes 7. optional smoke test (-t) Step 5 is one transaction on purpose: TARGET ends up either the whole system or an empty tree, never half a system. +--------------------------------------------+ | 2. Trust and privilege | +--------------------------------------------+ glacier-bootstrap NEVER WRITES INTO TARGET ITSELF. Every byte under TARGET goes through `doas syspkg --root TARGET`. - SECURITY_DESIGN.txt section 5: syspkg owns ALL system-scope mutation, "one tool to secure, one tool to audit." A bootstrapper that extracted packages itself would be the second, unaudited entry point that section warns about. - glacier-bootstrap runs UNPRIVILEGED. Parsing, resolving, fetching and verifying need no root, so it needs no third copy of the hardened root-loading preamble (syspkg and gstore already carry it; CLAUDE.md flags the duplication). Root is borrowed only for the two syspkg calls. - Its own verification is early-fail, for good error messages. syspkg's is authoritative: syspkg re-hashes every file at the moment it reads it (refusal matrix, SECURITY_DESIGN section 4). So a file swapped in the cache between bootstrap's check and syspkg's read is caught. The cache is the invoking user's (0700). The only party who could swap files there is the person who can already run doas syspkg. Where trust comes from, per source: local (-d) The sidecar next to each .gpkg, same as syspkg today. INTEGRITY ONLY, which is all the current tree offers anywhere. remote, pinned The TEMPLATE pins each package's sha256 (section 3). glacier-bootstrap writes the sidecar FROM THE PIN, not from anything the server sent. So syspkg's existing "sidecar matches file" check enforces the template's hash, with no syspkg change needed. The server is an untrusted blob store. Authenticity is exactly as good as the template file's, which you hold locally and can review or diff. remote, signed Later: a signed repo index (the agreed next major item after the checkpoints). Only then may a template name a remote package without a pin. remote, unpinned and unsigned: REFUSED. A .gpkg and its .sha256 fetched from the same server prove nothing (SECURITY_DESIGN section 1: the hash travelling with the file). +--------------------------------------------+ | 3. Template index format | +--------------------------------------------+ Extends IMAGE_DESIGN.txt section 5's KEY=value proposal with one line per package, so packages can carry pins: # templates/minimal.template NAME=minimal PKG=filesystem 1.0.0 PKG=musl 1.2.5 sha256:3a5f... PKG=busybox 1.37.0 sha256:91c0... - Exact versions only, no ranges. A template describes ONE system, reproducibly. - A pin is optional for -d (if present, it must ALSO match the sidecar) and required for -r until signed repos exist. - Strict parsing: unknown keys, a duplicate PKG name, a malformed pin and an empty package list are all hard errors. SYS_PROFILE is an unknown key (9b): nothing could check it until manifests record a profile (ROADMAP 3.3). Remember gworld bug 10: a field silently never parsing, with a default that looks right. - A `glacier-bootstrap --pin PKGDIR TEMPLATE` helper that fills pins in from local sidecars is the obvious way to produce a pinned template from a gworld build. Pinning then becomes a review step, not hand-copying hex. +--------------------------------------------+ | 4. Resolution | +--------------------------------------------+ Manifests already carry PKG_DEPS and PKG_CONFS (name + version), and libglacier parses both into gl_pkg_t. Nothing consumes them today, and every existing package leaves them empty. (IMAGE_DESIGN.txt section 5 says glacier has no runtime deps at all. The format exists; the use doesn't.) Recommendation: CHECK, don't auto-resolve. - Every PKG_DEPS entry of every listed package must itself be listed, at that version or later (decided 2026-10-06: the ROADMAP 2.3 rule, so the template check and syspkg's commit check agree). Otherwise error: "base::busybox requires musl >= 1.2.5, which the template does not list." / "..., but the template lists 1.2.4." - No two listed packages may conflict (PKG_CONFS). - Each manifest's PKG_NAME/PKG_VER must match its template line. This catches a mis-named file in -d mode, where there may be no pin. Why not pull dependencies in automatically: anything auto-added would be an unpinned package the template's author never reviewed, which defeats the trust model in section 2. Closure resolution can come with signed repos. SYS_PROFILE can't be checked against packages yet: manifests don't record which profile they were built under (open item in CLAUDE.md). Once they do, a mismatched package is a resolution error. +--------------------------------------------+ | 5. Package sources | +--------------------------------------------+ LOCAL (-d PKGDIR): a flat directory, exactly what gbuild emits — NAME-VER.gpkg, NAME-VER.gpkg.sha256. No index file. The template's package list says what to take from the directory. REMOTE (-r URL): the same flat layout over HTTPS: URL/NAME-VER.gpkg - Fetch by shelling out to curl (`--proto =https --fail`), the same way gbuild shells out to git and tar, into $XDG_CACHE_HOME/glacier-bootstrap/ (0700) under a temporary name. Hash it, and only then rename it into place. - Server-provided .sha256 files are never consulted (section 2). - Cache hits are re-hashed against the pin on every run. The cache is a download optimization, not a trusted store. - Later, with signing: URL/repo.idx plus its signature, listing name, version, repo and hash. The fetch path stays the same. +--------------------------------------------+ | 6. What "chrootable" requires | +--------------------------------------------+ Found while planning, by reading the code and the packages in pkgs/. Each one is a hard blocker for `chroot TARGET /bin/sh` working at all, independent of glacier-bootstrap itself: a. [FIXED 2026-10-04 — extraction + linking, with both defenses below; tests/test_package_symlinks.lua] SYMLINKS IN PACKAGES ARE DROPPED. The extraction loop in gl_install_pkg handles only AE_IFDIR and AE_IFREG (istoreutils.c); symlink entries never reach the store. gl_link_pkg likewise links only regular files. Real packages need symlinks: - musl ships lib/ld-musl-x86_64.so.1 -> /usr/lib/libc.so. That is the dynamic loader every musl binary asks the kernel for. It is currently never installed; Alpine-based tests passed only because Alpine provides its own. - shared-library soname links (libz.so.1 -> libz.so.1.3.1); - busybox applets. Fix: extract symlink entries verbatim into the store, and link them like regular files. SECURITY TRAP to design around: an archive shipping `x -> /etc` followed by `x/passwd` writes through the symlink, as root, into the real /etc (classic tar-slip via symlink). Extraction must never write through a symlink: create symlinks only after every regular file, and/or refuse any entry whose parent path is a symlink in the store. b. [DONE 2026-10-04 — gl_create_fs_tree, run by syspkg -n; takes a root prefix ready for --root; tests/test_fs_tree.lua] NO TOP-LEVEL TREE. Base packages link only under /usr (PACKAGE_SCOPES section 3), and a leading "usr/" is the only prefix stripped. So musl's lib/ld-musl... would land at /usr/lib, while PT_INTERP says /lib/ld-musl-x86_64.so.1. TARGET needs merged /usr: /bin -> usr/bin /lib -> usr/lib /sbin -> usr/bin plus the mountpoints and dirs nothing ships: /dev /proc /sys /tmp (1777) /run /var /root /home /etc. No package can provide these under the current link rules (everything base goes under /usr). Recommendation: `syspkg -n` creates this skeleton as part of system init (under --root, and on a real system). Creating the system's skeleton is system-scope mutation, so it belongs to syspkg, not to the bootstrapper. c. [DONE 2026-10-04 — decision 9c, option 1; tests/test_config_files.lua] /etc HAS NO STORY. A base package's etc/passwd would link to /usr/etc/passwd. And config files can't simply be symlinks into the store anyway: editing them would modify the store and break its hashes. This needs its own decision (section 9c). For the first chroot milestone, the skeleton step can write a minimal /etc/passwd and /etc/group (root only), and /etc/profile. d. NO SHELL. The only packages that exist are musl, kilo and sbase, and sbase is coreutils with no sh. A busybox (or mksh/dash) recipe in base is needed. e. REPO TIERS. sbase's manifest says PKG_REPO = "core", which is not a tier (base / extra / community). Under system scope it would link into /glacier/sys/links, not /usr/bin. Rebuild it as base. f. [sidecars generated for the existing archives 2026-10-04] NO SIDECARS. None of pkgs/*.gpkg has a .sha256. They were built before checkpoint 2. Rebuild them with the current gbuild. +--------------------------------------------+ | 7. Prerequisites, in order | +--------------------------------------------+ 1. [DONE] Hash verification at install time (SECURITY_DESIGN section 4). Pins mean nothing until syspkg actually checks sidecars. 2. [DONE] Symlink extraction + linking, with the write-through-symlink defense (6a). Needed for anything to run. 3. [DONE 2026-10-05] sysroot in the library (IMAGE_DESIGN section 3), plus the "host /usr and /glacier untouched" test. 4. [DONE 2026-10-05, as -R/--root] syspkg --root DIR (IMAGE_DESIGN section 4), --strict (any link refusal fails the transaction), and the skeleton on -n (6b) [the skeleton is DONE; --root and --strict are not]. 5. [DONE: ~/Projects/everest builds musl, busybox, linux and everest-bootscripts, all base, with sidecars.] Packages: busybox recipe (base), sbase rebuilt as base, every package rebuilt with sidecars (6d-f). 6. [DONE 2026-10-06: Everest's `everest.sh initramfs` now builds its tree this way, and the result boots in QEMU.] glacier-bootstrap, local mode. MILESTONE: `glacier-bootstrap -t -d pkgs templates/minimal.template /mnt/everest` succeeds, `chroot /mnt/everest /bin/sh` gives a working shell, and the host is byte-identical before and after. 7. Remote mode with pinned templates, plus the --pin helper. 8. Signed repo index (separate design pass), then unpinned remote. 1 and 2 are independent and can go in either order. 3 and 4 are the bulk of the library/syspkg work. 6 itself is small: it's mostly orchestration over things that have to exist anyway. +--------------------------------------------+ | 8. Implementation shape | +--------------------------------------------+ - LuaJIT, in glacier-ng, alongside gworld. It shares gworld's KEY=value parsing (factor that into lib/ rather than copy it), and reads manifests through libglacier's existing parser via FFI, so PKG_DEPS etc. parse exactly the way syspkg will see them. - Unprivileged, so the CWD-relative require convention is fine here. - Shells out to: curl (remote), doas syspkg, chroot (for -t, via doas). - Exit codes distinguish parse / resolve / fetch / verify / install failure, so an image build script can tell them apart. +--------------------------------------------+ | 9. Open decisions | +--------------------------------------------+ a. IMAGE_DESIGN.txt section 9a still stands: images are bootstrap OUTPUT, never install INPUT. Everything here assumes yes. b. DECIDED 2026-10-06: NAME= plus one `PKG=name version [sha256:hex]` per package; no SYS_PROFILE until manifests record one (section 3). c. DECIDED 2026-10-04: option 1, copy on install, .new on conflict. Implemented in istoreutils.c (install_config / prune_config): a system-scope package's top-level etc/ is copied into /etc, never linked. An admin-modified, admin-deleted or not-glacier-installed file is never overwritten; package changes to it are offered once as FILE.new. Unmodified files are updated in place and removed with their package. User scope still links etc/ as before (open). d. DECIDED 2026-10-04: syspkg -n creates the top-level tree itself (6b). Implemented as gl_create_fs_tree(root, verbose), called by gl_init_sys before the index is written. e. DECIDED 2026-10-05: automatic closure, once there is a source to resolve from (remote repos, ROADMAP 3.1). Until then a missing dependency refuses; the resolver is written so 3.1 plugs in. The index already records auto vs explicit installs (ROADMAP 2.3). Section 4's template check still applies to bootstrap: a template lists what it installs. f. Pinning helper: part of glacier-bootstrap (--pin) or of gworld (which already knows what it just built). +--------------------------------------------+ | 10. As implemented (local mode, 2026-10-06)| +--------------------------------------------+ glacier-bootstrap [-hvntV] -d PKGDIR TEMPLATE TARGET Where it differs from, or adds to, sections 1-8: - syspkg is the one on the caller's PATH (decided 2026-10-06), resolved to an absolute path first and run through doas, or directly when already root. -t runs `doas chroot`. - Order: parse, then verify, then resolve. Manifests are read only from VERIFIED copies: each PKGDIR/NAME-VER.gpkg is copied into a private mktemp -d while being hashed, checked against its sidecar and the pin, and given a sidecar written from that hash. syspkg is handed the copies, so a pin binds syspkg too, and a file swapped in PKGDIR later is never read. The copies are removed on every exit. - TARGET must be absolute, not /, and pass gl_check_sysroot (checked read-only up front). It must be fresh, or initialised by syspkg -n with nothing installed: then -n is skipped, so a run declined at syspkg's prompt can simply be repeated. TARGET with packages is refused. - syspkg's prompt is passed through (it's the review step). It exits 0 when declined, so afterwards TARGET's index is read (it's world- readable) and every template package must be listed. - Every problem in a phase is reported before failing. Exit status: 1 usage or target, 2 template, 3 verification, 4 resolution, 5 installation, 6 smoke test, 7 repositories. - Repositories (2026-10-07; the user: bootstrap must leave a semi-functional system, so it configures repositories, pacstrap style). The configuration comes from a PACKAGE in the template (Everest: everest-repos, shipping /etc/glacier/repos.conf and keys/*.pub), so bootstrap still never writes into TARGET. After the install check, if TARGET's repos.conf gives any repository a url, it runs `syspkg -R TARGET -y`: syspkg reads TARGET's OWN keyring under -R for repository operations (REPO_DESIGN section 6, "Keyrings under -R"), fetches into TARGET/glacier/sys/repos, and so sets TARGET's rollback floor at build time. A failed fetch: exit 7, TARGET stays installed, no "Bootstrapped" line. No repos.conf, or no url: `[!] No remote repositories are configured in TARGET.` and exit 0. Copying the host's repos.conf (pacstrap's mirrorlist copy) is left out for now (user, 2026-10-07). - Its own lib/ is found from its realpath (package.path and ffi.load), not the current directory; no root-ownership checks (unprivileged). - Not implemented: -r (remote) and --pin (9f still open).