10/4
This commit is contained in:
1 parent
c432274936
commit
ee4bcc3b77
27 files changed
+2719
-589
No files matched your search
@@ -0,0 +1,299 @@
|
||||
+--------------------------------------------+
|
||||
| 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).
|
||||
Reference in new issue
Block a user