300 lines
14 KiB
Plaintext
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).
|