Files
libglacier-ng/GLACIER_BOOTSTRAP.txt
T
2026-10-05 14:47:58 -04:00

300 lines
14 KiB
Plaintext

+--------------------------------------------+
| glacier-bootstrap |
+--------------------------------------------+
STATUS: PLAN. Nothing here is implemented. Section 9 lists the
decisions still open; everything else is a recommendation to argue
with, not a settled design.
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. 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
SYS_PROFILE=x86_64-musl
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. 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,
with a matching version. Otherwise error: "busybox needs musl 1.2.5,
which the template does not list."
- 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. sysroot in the library (IMAGE_DESIGN section 3), plus the "host
/usr and /glacier untouched" test.
4. 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. Packages: busybox recipe (base), sbase rebuilt as base, every
package rebuilt with sidecars (6d-f).
6. 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. Template format (section 3): one PKG= line per package with an
optional pin, or something else.
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. Dependency handling (section 4): check-only, as recommended, or
automatic closure.
f. Pinning helper: part of glacier-bootstrap (--pin) or of gworld
(which already knows what it just built).