Files
libglacier-ng/GLACIER_BOOTSTRAP.txt
2026-10-14 00:50:56 -04:00

360 lines
18 KiB
Plaintext

+--------------------------------------------+
| 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).