10/4
This commit is contained in:
1 parent
c432274936
commit
ee4bcc3b77
27 files changed
+2711
-581
No files matched your search
@@ -0,0 +1,16 @@
|
|||||||
|
BasedOnStyle: LLVM
|
||||||
|
|
||||||
|
BreakBeforeBraces: Custom
|
||||||
|
BraceWrapping:
|
||||||
|
AfterFunction: true
|
||||||
|
AfterControlStatement: false
|
||||||
|
AfterEnum: true
|
||||||
|
AfterStruct: true
|
||||||
|
AfterUnion: true
|
||||||
|
AfterClass: true
|
||||||
|
|
||||||
|
AlwaysBreakAfterReturnType: All
|
||||||
|
|
||||||
|
IndentWidth: 8
|
||||||
|
TabWidth: 8
|
||||||
|
UseTab: Always
|
||||||
@@ -1,261 +1,448 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Context for Claude Code sessions on this repo (libglacier-ng, the package
|
Context for Claude Code sessions on libglacier-ng: the package manager
|
||||||
manager for the Everest/Glacier Linux distribution). This file exists
|
for Everest Linux. Kept as a handoff, so a fresh session starts with full
|
||||||
because there's no built-in way to hand off a claude.ai planning
|
continuity instead of re-deriving everything. Last full rewrite:
|
||||||
conversation into Claude Code — this is that handoff, written down.
|
2026-10-04, at the end of a long session. **Update it as you go.**
|
||||||
|
|
||||||
For the symlink-routing / package-scope design specifically, see
|
## Start here
|
||||||
`PACKAGE_SCOPES.txt` at the repo root — that doc is the source of truth
|
|
||||||
for that topic and isn't duplicated here.
|
1. `ROADMAP.txt`: what's left, in priority order, plus the decisions that
|
||||||
|
are still open (section 6). Pick work from it.
|
||||||
|
2. This file: how things work, what the user wants, how to test.
|
||||||
|
3. The design docs, which are authoritative for their topics and not
|
||||||
|
repeated here:
|
||||||
|
- `SECURITY_DESIGN.txt`: integrity verification, scope separation,
|
||||||
|
refusal matrix, syspkg's privilege model.
|
||||||
|
- `PACKAGE_SCOPES.txt`: layout, link routing, `/etc` config handling.
|
||||||
|
- `IMAGE_DESIGN.txt`: PROPOSAL for system images (bootstrap OUTPUT,
|
||||||
|
the sysroot library change). Open decisions are in section 9.
|
||||||
|
- `GLACIER_BOOTSTRAP.txt`: PLAN for `glacier-bootstrap`. Prerequisites
|
||||||
|
are in section 7, decisions in section 9 (9c and 9d are decided).
|
||||||
|
- `SECURITY_REVIEW_2026-07-16.md`: older review findings.
|
||||||
|
|
||||||
|
Nothing from the 2026-10-04 session is committed. libglacier-ng is on
|
||||||
|
branch `dev` with a large uncommitted diff (`src/`, the docs). Commit only
|
||||||
|
when the user asks.
|
||||||
|
|
||||||
|
## The three projects
|
||||||
|
|
||||||
|
| Path | What | Git? |
|
||||||
|
|---|---|---|
|
||||||
|
| `~/Projects/libglacier-ng` | C library, design docs, this file | yes (`dev`) |
|
||||||
|
| `~/Projects/glacier-ng` | Frontend: `gpkg`, `gstore`, `syspkg`, `gbuild`, `gworld`, `lib/*.lua`, `tests/`, Dockerfiles, `pkgs/` | **no**: back files up before editing (`file.old`; if that exists, `.old2`, `.old3`, ...) |
|
||||||
|
| `~/Projects/everest` | The distro build: `everest.sh build\|initramfs\|boot`, packages, bootable QEMU image | no |
|
||||||
|
|
||||||
|
`glacier-ng/lib/glacier/` holds `.so`s built on the host (glibc). They
|
||||||
|
won't load on musl. Every Alpine image and test builds libglacier-ng from
|
||||||
|
source instead.
|
||||||
|
|
||||||
## What this project is
|
## What this project is
|
||||||
|
|
||||||
A from-scratch Linux distribution (Everest, built on the Glacier
|
A from-scratch, security-focused Linux distribution (Everest) with its own
|
||||||
package-management model) with its own package manager
|
package manager (libglacier-ng + `gpkg`/`gstore`/`syspkg`) and build tooling
|
||||||
(`libglacier-ng` + `gpkg`/`gstore` CLI tools) and its own build
|
(`gbuild`/`gworld`). It's a C library, LuaJIT+FFI CLI tools and bash build
|
||||||
tooling (`gbuild`/`gworld`) for bootstrapping a set of packages up to
|
scripts. The user writes it; Claude implements and designs alongside them.
|
||||||
a minimal bootable system.
|
|
||||||
|
## How the user wants things done
|
||||||
|
|
||||||
|
These are preferences stated in sessions. Follow them.
|
||||||
|
|
||||||
|
- **Output format:** every message uses glacier's `lg_printf` levels:
|
||||||
|
`[x]` is error (2), `[!]` warning (1), `[i]` info (0), `[✓]` success (3).
|
||||||
|
Messages are capitalized. Exact strings the user chose include
|
||||||
|
`Cannot open system index: Permission denied` (no period) and
|
||||||
|
`Nothing to do.` (success level).
|
||||||
|
- The pre-trust-check `die()` in syspkg/gstore reproduces the `[x]`
|
||||||
|
format byte for byte, because it can't load `libglacier_log.so`
|
||||||
|
before that file has been verified.
|
||||||
|
- **No hint messages.** Don't print lines that point users to other flags
|
||||||
|
or commands (for example "use -lO to ..."). The user called these
|
||||||
|
clutter. A refusal states the fact, in one line. The user also removed a
|
||||||
|
`Run doas gstore -n ...` follow-up hint from gstore themselves.
|
||||||
|
- **Ask before settling a design choice that is the user's to make**, and
|
||||||
|
give a recommendation. They have answered every such question promptly.
|
||||||
|
Don't silently pick a layout or semantics.
|
||||||
|
- **Verify by running, in Docker or QEMU, never by inspection alone.** For
|
||||||
|
any security fix, also prove the test catches the bug: remove the
|
||||||
|
defence in a throwaway copy and confirm the test fails ("mutation
|
||||||
|
check"). Several defences here are verified that way.
|
||||||
|
- Use doas, not sudo. syspkg is never setuid.
|
||||||
|
|
||||||
|
## Decisions the user has made (don't relitigate)
|
||||||
|
|
||||||
|
- **Per-uid user layout** (`/glacier/usr/<uid>/...`, root-owned parent).
|
||||||
|
Both root and the user can create it: root runs `gstore -n UID`; a user
|
||||||
|
runs `doas gstore -n` through a doas rule pinned to `args -n`
|
||||||
|
(option "B").
|
||||||
|
- **`syspkg -n` creates the top-level tree itself** (GLACIER_BOOTSTRAP 9d).
|
||||||
|
- **`/etc` config files: option 1.** Copy on install, `.new` on conflict
|
||||||
|
(GLACIER_BOOTSTRAP 9c).
|
||||||
|
- **`-O` with `-l` = replace.** `-O` displaces non-glacier files, and
|
||||||
|
`-lO` also replaces an installed *different version* (upgrade or
|
||||||
|
downgrade from a local file). Plain `-l` with another version installed
|
||||||
|
refuses in one line.
|
||||||
|
- **`KEEP_OLD`** in `/etc/glacier/glacier.conf`: the number of backups of
|
||||||
|
replaced versions to keep per package (0 = none).
|
||||||
|
- It has **no default.** It ships as `KEEP_OLD = 1`.
|
||||||
|
- If it's missing or invalid, an operation that would replace a version
|
||||||
|
errors. Only then: other operations don't need it.
|
||||||
|
- "Backed up" means the old package's files are compressed into a
|
||||||
|
`.gpkg`. A restore interface comes later.
|
||||||
|
- **`gpkg -u` / `syspkg -u` upgrade from a REMOTE repository**, and
|
||||||
|
`gpkg -f` is `-l` but fetched by name from a remote repository. Both wait
|
||||||
|
for remote repos + signing (ROADMAP 3.1). Don't implement them against
|
||||||
|
local files.
|
||||||
|
- `everest-bootscripts` is the name of the base `/etc` package. Its source
|
||||||
|
lives in `~/Projects/everest/pkgsrc/`.
|
||||||
|
|
||||||
## Core architecture
|
## Core architecture
|
||||||
|
|
||||||
**`gl_context_t`** (transaction.h) is the central abstraction: `uid`,
|
**`gl_context_t`** (transaction.h) is the central abstraction. Fields:
|
||||||
`root_path`, `index_path`, `store_path`, `links_path`, `mode`
|
`uid`, `root_path`, `index_path`, `store_path`, `links_path`, `mode`
|
||||||
(`GL_CTX_LIVE` / `GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR` /
|
(`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR`/`GL_SCOPE_SYS`),
|
||||||
`GL_SCOPE_SYS`), `lock_fd`.
|
`lock_fd`, and `overwrite` (last field, zeroed by the init functions).
|
||||||
|
|
||||||
**Transaction lifecycle**: `gl_init_live_context` / `gl_init_stage_context`
|
**Layout.** Each scope owns ONE base directory holding
|
||||||
→ `gl_commit_transaction` or `gl_abort_transaction`. Staged installs
|
`{index,store,links,stage,old,lock,backup}`. Every create, rename and lock
|
||||||
hardlink-seed `index`/`store` from live at stage-init time (copy-on-write,
|
a transaction does stays inside it.
|
||||||
so removing a live package inside a transaction doesn't require special
|
|
||||||
casing). The `links` tree is deliberately **not** seeded — see below.
|
|
||||||
|
|
||||||
**Commit** does atomic rename-based promotion (stage → live, with the
|
| Scope | Base dir | Ownership |
|
||||||
previous live moved to `old/` as a rollback point), fsyncs the parent
|
|---|---|---|
|
||||||
dirs, then calls `gl_relink_store` to regenerate symlinks and
|
| `GL_SCOPE_USR` | `/glacier/usr/<uid>/` | uid, mode 0700, inside a root-owned 0755 `/glacier/usr` |
|
||||||
`gl_rebuild_index` to regenerate the index — both from a fresh
|
| `GL_SCOPE_SYS` | `/glacier/sys/` | root |
|
||||||
`GL_CTX_LIVE` context, not the just-committed stage context.
|
|
||||||
|
|
||||||
**Scopes**:
|
- The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment
|
||||||
- `GL_SCOPE_USR`: per-uid, `/glacier/usr/{index,store,links}/<uid>`.
|
are in `common.h`.
|
||||||
- `GL_SCOPE_SYS`: system-wide, `/glacier/sys/{index,store}`. Symlink
|
- `gl_init_stage_context` refuses a user base that isn't a real directory
|
||||||
destination depends on the package's **repo** — see `PACKAGE_SCOPES.txt`.
|
(lstat) owned by its uid with no group/other bits, or whose
|
||||||
Short version: `repo == "base"` → `/usr`; anything else →
|
`/glacier/usr` is loosened. `gl_usr_base_status` exports that check, so
|
||||||
`/glacier/sys/links`. This is decided per-package (in `gl_link_pkg` /
|
gpkg can say why before any prompt.
|
||||||
`gl_relink_store`), not baked into the context.
|
|
||||||
|
|
||||||
**Repos**: `base` (minimal working system, boot-critical by definition —
|
**Transaction lifecycle.** `gl_init_live_context` / `gl_init_stage_context`
|
||||||
this is a repo-level classification, not a per-package flag), `extra`
|
→ `gl_commit_transaction` or `gl_abort_transaction`.
|
||||||
(important but not required), `community` (everything else). `extra`
|
- Stage init takes the lock FIRST, then clears any leftover stage.
|
||||||
and `community` currently behave identically — see open items.
|
- Stage contexts hardlink-seed `index`/`store` from live, so removing a
|
||||||
|
live package inside a transaction needs no special casing. Seeding
|
||||||
|
copies dotfiles: only `.` and `..` are skipped.
|
||||||
|
- A stage on another filesystem fails at seeding with `GL_TXN_ERR_XDEV`.
|
||||||
|
- **Commit:** rename-based promotion (stage → live, previous live → `old/`),
|
||||||
|
then fsync the base. Then `gl_relink_store` + `gl_rebuild_index`
|
||||||
|
regenerate links and index from a *fresh live context* and rescan the
|
||||||
|
WHOLE store (every commit relinks every package, so link-time logic must
|
||||||
|
be idempotent). `old/` is then deleted.
|
||||||
|
|
||||||
## Safety mechanisms (read before touching links/symlink code)
|
**Link routing** (per package, in `gl_link_pkg`):
|
||||||
|
|
||||||
`ctx->links_path` is **not always glacier-exclusive territory** —
|
| Scope | Repo | Destination |
|
||||||
for system-scope `base` packages it's literally `/usr`, which holds
|
|---|---|---|
|
||||||
plenty of content glacier has no business touching. Everything that
|
| USR | any | `<base>/links` |
|
||||||
reads or writes a links destination has to treat it as "possibly
|
| SYS | `base` | `/usr` |
|
||||||
shared":
|
| SYS | anything else | `/glacier/sys/links` |
|
||||||
|
|
||||||
- `is_glacier_symlink(path, store_prefix)` (istoreutils.c) is the one
|
- A package's leading `usr/` is stripped.
|
||||||
source of truth for "did glacier create this." True only if `path`
|
- In system scope, a package's top-level `etc/` is never linked: it's
|
||||||
is a symlink whose target lives under `store_prefix`. Never touch a
|
COPIED into `/etc` (see `/etc` config handling below).
|
||||||
path this returns false for.
|
|
||||||
- `gl_link_pkg` will not overwrite an existing path unless
|
|
||||||
`is_glacier_symlink` says it's safe to replace.
|
|
||||||
- `gl_relink_store` prunes stale symlinks (`prune_stale_links`) rather
|
|
||||||
than wiping and rebuilding the whole links directory — a full
|
|
||||||
`rm -rf` of `ctx->links_path` would be catastrophic once that path
|
|
||||||
can be `/usr`. It prunes **both** possible system-scope destinations
|
|
||||||
(`/glacier/sys/links` and `/usr`) since a single pass can't know in
|
|
||||||
advance whether any base-repo packages are involved.
|
|
||||||
- The stage links tree is never hardlink-seeded — it's genuinely
|
|
||||||
unused (`gl_link_pkg` only ever runs against `GL_CTX_LIVE`;
|
|
||||||
`gl_relink_store` always builds its own fresh live context). Seeding
|
|
||||||
it would mean hardlinking all of `/usr` on every staged system
|
|
||||||
transaction, and risking `EXDEV` if `/usr` and the stage area are on
|
|
||||||
different filesystems.
|
|
||||||
|
|
||||||
## Bug history (so it doesn't get relitigated or reintroduced)
|
**Repos.** `base` (the minimal system; boot-critical by definition, a
|
||||||
|
repo-level rule), `extra`, `community`. `extra` and `community` behave
|
||||||
|
identically.
|
||||||
|
|
||||||
Roughly chronological, all already fixed in the current tree:
|
**Hidden files in the store** (dot-prefixed, so linking, index rebuild and
|
||||||
|
the store scanners skip them, while seeding carries them through
|
||||||
|
transactions):
|
||||||
|
|
||||||
1. **`gl_abort_transaction` never actually released the lock file** — a
|
| File | Purpose |
|
||||||
copy-paste bug wrote into the wrong variable. Fixed by moving off
|
|---|---|
|
||||||
`O_EXCL`-based locking entirely: `gl_begin_transaction` now just
|
| `<ver>/.glacier-manifest` | the package's manifest, saved at install, used by `gl_backup_pkg` |
|
||||||
`open(O_CREAT)` + `flock()`, and lock files are never `unlink()`'d
|
| `<ver>/.glacier-fresh` | one-shot "installed in this transaction"; link conflicts are only reported when present |
|
||||||
(the kernel releases `flock()` automatically on process death, even
|
| `<ver>/.glacier-overwrite` | one-shot `-O` permission for this package's next link pass |
|
||||||
`SIGKILL`; explicit unlinking has its own TOCTOU race with a second
|
| `<store>/.displaced/<abs path>` | originals displaced by `-O`, restored when the link that replaced them is pruned |
|
||||||
process creating a new inode at the same path).
|
| `<store>/.etc-state/<path>` | the last version of each `/etc` file a package offered |
|
||||||
2. **`EXDEV` wasn't checked before renaming** — `gl_commit_transaction`
|
|
||||||
now `stat()`s both sides and fails cleanly with `GL_TXN_ERR_XDEV`
|
|
||||||
before attempting any renames if stage and live aren't on the same
|
|
||||||
filesystem.
|
|
||||||
3. **Segfault on install**, root cause: the Makefile's `transaction`
|
|
||||||
target didn't link `libglacier_transaction.so` against
|
|
||||||
`libglacier_istoreutils.so`/`libglacier_log.so`, even though
|
|
||||||
`transaction.c` calls `gl_relink_store()` and `lg_printf()`. Fixed
|
|
||||||
by adding those `-l` flags and an explicit `transaction: log
|
|
||||||
istoreutils` prerequisite (build order matters now).
|
|
||||||
4. **Symlinks going stale after commit** — `gl_link_pkg` used to run
|
|
||||||
during staged installs too, embedding the *stage* path as the
|
|
||||||
symlink target. Fixed: `gl_link_pkg` only runs for `GL_CTX_LIVE`;
|
|
||||||
links are fully regenerated post-commit by `gl_relink_store`.
|
|
||||||
5. **Dangling symlinks never cleaned up on removal** — see "Safety
|
|
||||||
mechanisms" above; this is what `prune_stale_links` fixes.
|
|
||||||
6. **`GL_TXN_ERR_RENAME` on every commit after the first** — leftover
|
|
||||||
`old/` directory from a previous commit made the next commit's
|
|
||||||
`rename()` fail with `ENOTEMPTY`. Fixed: `gl_commit_transaction`
|
|
||||||
wipes `old_base` if present before using it.
|
|
||||||
7. **`-Wstringop-truncation` errors** (real bug, not noise) — four
|
|
||||||
`strncpy(dst, ctx->foo_path, PATH_MAX - 1)` calls that could leave
|
|
||||||
`dst` unterminated. Fixed by switching to `snprintf(dst,
|
|
||||||
sizeof(dst), "%s", ...)`, which always terminates.
|
|
||||||
(`-Wformat-truncation`, separately, is suppressed via
|
|
||||||
`-Wno-error=format-truncation` in `config.mk` — those warnings are
|
|
||||||
about genuinely-safe `snprintf` truncation, not a real bug.)
|
|
||||||
8. **`gbuild` build-system autodetection** checked for a bare
|
|
||||||
`Makefile` before `configure`/`configure.ac`. Broke musl (and any
|
|
||||||
project with a hand-rolled, non-autotools build system that ships
|
|
||||||
both) since its `Makefile` is non-functional without `configure`
|
|
||||||
generating `config.mak` first. Fixed by reordering the checks.
|
|
||||||
9. **Index only stored `repo::pkgname`, no version.** Now
|
|
||||||
`repo::pkgname::version` (`gl_rebuild_index` looks up the
|
|
||||||
`<pkgname>-<version>` store subdirectory). `struct gpkg_entry` in
|
|
||||||
`istoreutils.h` needs a `char *ver;` field — this was a manual
|
|
||||||
header edit, double check it's actually present.
|
|
||||||
10. **`gworld` recipe parser silently dropped `SYS_PROFILE`** — the
|
|
||||||
key-matching regex only matched uppercase letters, not
|
|
||||||
underscores. Fixed to `^([%u_][%u%d_]*)=(.*)$`. Caught by an
|
|
||||||
actual precedence test, not code review — worth remembering that
|
|
||||||
class of bug (a field silently never parsing, falling back to a
|
|
||||||
default that happens to look right) is easy to miss by inspection
|
|
||||||
alone.
|
|
||||||
|
|
||||||
## CLI additions
|
**Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax).
|
||||||
|
- It's read with `gl_conf_get_int(path, key, &out)` in `pkg.c`. Missing
|
||||||
|
file, missing key and wrong type are distinct statuses, and there are no
|
||||||
|
defaults.
|
||||||
|
- The shipped copy is `glacier-ng/etc/glacier/glacier.conf`. Every
|
||||||
|
Dockerfile installs it.
|
||||||
|
|
||||||
- `gpkg -s` / `--system`: operate against `GL_SCOPE_SYS` instead of a
|
## Safety invariants: read before touching links/symlink code
|
||||||
uid's tree. Requires root (checked via `geteuid()`). Must appear
|
|
||||||
*before* `-l`/`-x` in a grouped flag string (e.g. `-sl`, not `-ls`) —
|
|
||||||
flags are handled in parse order, same constraint `-V`/`-S` already
|
|
||||||
had.
|
|
||||||
- `gstore -N` / `--new-system`: calls `gl_init_sys()`. Requires root.
|
|
||||||
- `gbuild -P` / `--profile PROFILE`: system profile, `ARCH-LIBC[-FEATURE...]`
|
|
||||||
(e.g. `x86_64-musl`, `x86_64-glibc-multilib`). Derives a cross-compile
|
|
||||||
prefix (translating `glibc`→`gnu` per real GNU triplet convention),
|
|
||||||
detects native-vs-cross against the build host, and only pre-fills
|
|
||||||
`-t`/`-x` defaults for whichever the user didn't already set
|
|
||||||
explicitly. Features export as `GL_FEATURE_<NAME>=1` env vars for
|
|
||||||
custom `-b`/`-i` commands to branch on — gbuild itself doesn't
|
|
||||||
hardcode per-feature behavior, that's intentional.
|
|
||||||
|
|
||||||
## Build system
|
`/usr` is NOT glacier-exclusive territory. Treat every link destination as
|
||||||
|
possibly shared:
|
||||||
|
|
||||||
`Makefile` builds separate `.so`s per module
|
- **Ownership test.** `is_glacier_symlink(path, store_prefix)` is the only
|
||||||
(`libglacier_log.so`, `libglacier_pkg.so`, `libglacier_istoreutils.so`,
|
test for "did glacier create this". Never touch anything it returns
|
||||||
`libglacier_transaction.so`, `libglacier_dag.so`) — **this split is
|
false for, except through `-O`'s displace/restore.
|
||||||
deliberate Unix-philosophy design**, not an accident: a third-party
|
- **Overwriting.** `gl_link_pkg` won't overwrite a non-glacier path without
|
||||||
glacier-compatible program should be able to link only
|
the `-O` marker. It never descends through *another package's* directory
|
||||||
`libglacier_log.so` without pulling in everything else. `gpkg`/`gstore`
|
symlink into that package's store directory.
|
||||||
load the specific per-module `.so`s they need directly, matching this.
|
- **Pruning.** `gl_relink_store` PRUNES stale links; it never wipes a links
|
||||||
|
destination. A link is stale only if its own store target is gone
|
||||||
|
(checked with `lstat`, not by following it): a package symlink may
|
||||||
|
legitimately dangle.
|
||||||
|
- **Stage links.** The stage links tree is never seeded.
|
||||||
|
- **Extraction never writes through a symlink.**
|
||||||
|
- Symlink entries are created only after all regular content.
|
||||||
|
- Any entry whose parent is a symlink is refused (`crosses_symlink`,
|
||||||
|
plus `O_NOFOLLOW`).
|
||||||
|
- Any entry with `..` or an absolute path refuses the whole package.
|
||||||
|
- A failed extraction removes its half-made version directory.
|
||||||
|
- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`. Every
|
||||||
|
caller passes a glacier-only path; keep it that way. `gl_delete_user`
|
||||||
|
lstat-checks its target first, because a symlinked base would otherwise
|
||||||
|
have its *target* emptied.
|
||||||
|
- **Wide wrappers.** Don't add convenience wrappers that silently touch a
|
||||||
|
broader filesystem scope than asked. This codebase has been bitten twice
|
||||||
|
by exactly that.
|
||||||
|
|
||||||
There's also a `libglacier-ng` Makefile target producing a **combined**
|
## Feature reference (current behaviour)
|
||||||
`.so` — this exists *solely* for the Lua FFI test harness's
|
|
||||||
convenience (one thing to `ffi.load()` instead of juggling rpath
|
|
||||||
chains in a throwaway test process). It is **not** in `all` and **not**
|
|
||||||
installed. Don't reach for it outside `tests/` — that's a sign
|
|
||||||
something should be split-loaded instead.
|
|
||||||
|
|
||||||
`config.mk` holds `CFLAGS`, including `-Wno-error=format-truncation`
|
**Integrity verification** (`gpkg_common.lua`: `verify_and_copy`,
|
||||||
(see bug #7 above for why that specific warning is suppressed and
|
`verify_all`):
|
||||||
others aren't).
|
- Each package is copied into a private `mktemp -d` directory while being
|
||||||
|
hashed with `libglacier_verify.so`. The result is checked against
|
||||||
|
`FILE.sha256` (`sha256:<hex>`).
|
||||||
|
- Everything after that (summary, manifest read, `gl_install_pkg`) reads
|
||||||
|
ONLY the copy.
|
||||||
|
- A missing or malformed sidecar counts as a mismatch and is refused before
|
||||||
|
the prompt; one bad package refuses the whole batch. No bypass exists.
|
||||||
|
- This lives in the CLIs' shared module, not the library (as
|
||||||
|
SECURITY_DESIGN section 4 specifies), so a third-party caller of
|
||||||
|
`gl_install_pkg` is not verified (ROADMAP 3.2).
|
||||||
|
- The base→USR refusal (`GL_INSTALL_ERR_SCOPE = 7`), by contrast, *is* in
|
||||||
|
the library.
|
||||||
|
|
||||||
## Lua tooling
|
**`-O` / displace:**
|
||||||
|
- A displaced foreign file or symlink moves to `<store>/.displaced/`:
|
||||||
|
renamed, or on `EXDEV` copied (keeping mode, owner and mtime) and then
|
||||||
|
unlinked.
|
||||||
|
- Directories, special files and paths that already have a backup are
|
||||||
|
still refused.
|
||||||
|
- In user scope `-O` is effectively a no-op, because the links tree is
|
||||||
|
rebuilt from scratch on every commit.
|
||||||
|
|
||||||
- `gpkg`, `gstore`: LuaJIT + FFI, calling directly into the `.so`s.
|
**Version replacement** (`-lO`, `M.install` in `gpkg_common.lua`):
|
||||||
- `lib/glacier_cdef.lua`: single shared source of truth for every
|
1. Installed entries are read from the live index's `current` file
|
||||||
`ffi.cdef()` declaration, used by `gpkg`, `gstore`, and the test
|
(`repo::name::version` lines).
|
||||||
suite. This used to be duplicated three ways, which is exactly what
|
2. A same name+repo with a different version is a replacement. Without
|
||||||
let a stale signature drift silently into `gpkg` at one point — if
|
`-O` it's refused; with `-O`, `keep_old()` reads `KEEP_OLD`, and the
|
||||||
you're editing a C function signature, this file needs the matching
|
summary shows `old -> new`.
|
||||||
update, and it's the *only* place that needs it now.
|
3. In the transaction: `gl_backup_pkg(live, repo, name, X.gpkg.part)`, then
|
||||||
- `tests/*.lua`: LuaJIT FFI test suite exercising real filesystem/process
|
`gl_remove_pkg` from the stage, then install.
|
||||||
behavior (`fork`+`SIGKILL` for lock crash-recovery, real `mount()`
|
4. After the commit: rename `.part` → `<scope>/backup/<repo>/<name>/<name>-<ver>.gpkg`,
|
||||||
for the `EXDEV` check, etc.) — not mocked. Run via:
|
write its sidecar, and run `gl_prune_backups(dir, KEEP_OLD)` (newest by
|
||||||
|
nanosecond mtime).
|
||||||
|
- On any abort, `finish()` deletes the `.part`.
|
||||||
|
- A backup is a complete installable `.gpkg`, so `-lO backup.gpkg`
|
||||||
|
restores it.
|
||||||
|
|
||||||
|
**`/etc` config handling** (`install_config` / `prune_config` in
|
||||||
|
istoreutils.c), system scope only:
|
||||||
|
|
||||||
|
| Situation | Result |
|
||||||
|
|---|---|
|
||||||
|
| `/etc/F` missing, no record | install |
|
||||||
|
| missing, record exists | the admin deleted it: stays deleted |
|
||||||
|
| unchanged since glacier wrote it | updated in place |
|
||||||
|
| admin-modified, or never glacier's | never touched; a package change is offered ONCE as `F.new` |
|
||||||
|
| no package ships it any more | removed if unchanged, kept otherwise |
|
||||||
|
|
||||||
|
User-scope `etc/` is still linked (ROADMAP 2.9).
|
||||||
|
|
||||||
|
**Top-level tree.** `gl_create_fs_tree(root, verbose)` (exported) creates
|
||||||
|
the merged-/usr tree: `/bin` and `/sbin` → `usr/bin`, `/lib` → `usr/lib`,
|
||||||
|
`/usr/sbin` → `bin`, all RELATIVE links. It also creates `/usr/{bin,lib}`,
|
||||||
|
`/etc`, `/dev`, `/proc`, `/sys`, `/run`, `/tmp` and `/var/tmp` (1777),
|
||||||
|
`/var`, `/root` (0700) and `/home`.
|
||||||
|
- It creates only what's missing, of any type.
|
||||||
|
- `gl_init_sys` runs it before writing the index. The index marks `-n` as
|
||||||
|
complete, so a second `syspkg -n` is refused: `Cannot create
|
||||||
|
/glacier/sys: already exists`.
|
||||||
|
- It takes a root prefix, ready for `syspkg --root`.
|
||||||
|
|
||||||
|
**Package symlinks** are extracted verbatim and linked like files.
|
||||||
|
Hardlink, device and fifo entries are skipped. `add_to_archive` (used by
|
||||||
|
backups and `gstore -b`) archives symlinks.
|
||||||
|
|
||||||
|
## CLIs (glacier-ng)
|
||||||
|
|
||||||
|
| Tool | Flags | Runs as |
|
||||||
|
|---|---|---|
|
||||||
|
| `gpkg` | `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v`; `-f`/`-u` are stubs | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` |
|
||||||
|
| `syspkg` | `-n` `-l FILE...` `-x NAME...` `-O` `-S` `-V` `-h` `-v` | root via doas; `-h`/`-v` without root |
|
||||||
|
| `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (stub) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) |
|
||||||
|
|
||||||
|
- **gpkg:** modifiers `-O`/`-S`/`-V` are applied in a pre-pass, so their
|
||||||
|
position doesn't matter (`-lO` == `-Ol`). It finds its user via
|
||||||
|
`/proc/self/loginuid`, falling back to `getuid()`.
|
||||||
|
- **syspkg, `-n`:** creates the tree plus `/glacier/sys`.
|
||||||
|
- **syspkg as non-root:** `-n`/`-l`/`-x` are refused after argument
|
||||||
|
parsing with `Cannot open system index: Permission denied`.
|
||||||
|
- **gstore, `-n` without a UID, under doas:** acts for the invoking user
|
||||||
|
(loginuid, then `DOAS_USER`, then getuid). The doas rule
|
||||||
|
`permit nopass :wheel as root cmd /path/gstore args -n` is the
|
||||||
|
boundary that stops a user naming another UID.
|
||||||
|
- **Shared code:** `lib/gpkg_common.lua` (`M.install`, `M.remove`,
|
||||||
|
verification, replacement, backups).
|
||||||
|
- **The `-x` summary** comes from the live index; a name that isn't
|
||||||
|
installed fails before the prompt.
|
||||||
|
- **Hardened loading** (syspkg and gstore): `#!/usr/bin/luajit -E`,
|
||||||
|
`package.path` and every `ffi.load` pinned to their own `realpath`, and
|
||||||
|
when euid is 0, every ancestor directory and every loaded `.lua`/`.so`
|
||||||
|
must be root-owned and not group/world-writable. The preamble is
|
||||||
|
DUPLICATED in both files on purpose: change one, change both.
|
||||||
|
- **gpkg** uses the older CWD-relative convention (`require("lib/...")`,
|
||||||
|
`ffi.load("./lib/glacier/...")`): run it from its own directory.
|
||||||
|
- **Declarations:** `lib/glacier_cdef.lua` is the single source of truth
|
||||||
|
for every `ffi.cdef`. `tests/glacier_ffi.lua` is only a wrapper around
|
||||||
|
it. Declaration ORDER matters: a declaration using `gl_context_t` must
|
||||||
|
come after its typedef.
|
||||||
|
|
||||||
|
## Build tooling
|
||||||
|
|
||||||
|
- **gbuild** (bash, the only copy is `glacier-ng/gbuild`):
|
||||||
|
- `-n -V -g -c -d -m -D -C -b -i -o -p -j -F -t -x -u -r -s`;
|
||||||
|
`-v`/`--version` prints the version, `--verbose` turns on verbose
|
||||||
|
output.
|
||||||
|
- Autodetects the build system, with autotools checked BEFORE a bare
|
||||||
|
Makefile.
|
||||||
|
- Writes `NAME-VER.gpkg` plus a `.sha256` sidecar, using `gsha256sum`
|
||||||
|
from `$GSHA256SUM_BIN`, else `<script dir>/build/bin`, else PATH.
|
||||||
|
- **There is NO `-P` system-profile support, and gworld has no
|
||||||
|
`SYS_PROFILE` field** (ROADMAP 4.1). The target libc is decided by
|
||||||
|
where gbuild runs: Alpine for musl.
|
||||||
|
- **gworld** (Lua): batch orchestrator, reads `recipes/*.recipe`, sorts
|
||||||
|
topologically by `DEPS`, runs gbuild per package.
|
||||||
|
- **`~/Projects/everest`:** the gbuild command for each package lives in
|
||||||
|
`scripts/container.sh`; `everest.sh` runs it inside an Alpine image
|
||||||
|
(`Dockerfile.build`).
|
||||||
|
- **Kernel version:** linux `6.12.112`; plain `v6.12` doesn't build with
|
||||||
|
GCC 15.
|
||||||
|
|
||||||
|
## libglacier-ng build
|
||||||
|
|
||||||
|
- **Separate `.so` per module** (`libglacier_log`, `_pkg`, `_istoreutils`,
|
||||||
|
`_transaction`, `_dag`, `_verify`). This split is deliberate: a
|
||||||
|
third-party program links only what it needs.
|
||||||
|
- **The combined `libglacier-ng.so`** (`make libglacier-ng`) exists only
|
||||||
|
for the FFI tests.
|
||||||
|
- **`libglacier_verify.so`** is private: no public header.
|
||||||
|
- **Warnings:** `config.mk` builds with `-Werror`. `-Wno-format-truncation`
|
||||||
|
is set because that warning is noise; `-Wstringop-truncation` stays
|
||||||
|
fatal.
|
||||||
|
- **Makefile quirk:** `make log` builds only `log.o`; the shared object
|
||||||
|
comes from `make log_test` (or plain `make`).
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
Run everything in throwaway containers. Several suites write to `/usr`,
|
||||||
|
`/etc` and `/glacier`.
|
||||||
|
|
||||||
|
**FFI suite** (`glacier-ng/tests/*.lua`; 8 files, all passing):
|
||||||
```sh
|
```sh
|
||||||
cd tests
|
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
|
||||||
make -C .. libglacier-ng # builds the test-only combined .so
|
-v ~/Projects/libglacier-ng:/libsrc:ro alpine:latest sh -c '
|
||||||
GLACIER_TEST_CONFIRM=yes LIBGLACIER_SO=$(pwd)/../build/lib/shared/libglacier-ng.so luajit run_all.lua
|
apk add --no-cache build-base luajit luajit-dev libarchive-dev libconfig-dev linux-headers tar >/dev/null
|
||||||
|
cp -a /libsrc /build && cd /build && make clean && make && make libglacier-ng
|
||||||
|
cp -a /src /g && cd /g/tests && GLACIER_TEST_CONFIRM=yes \
|
||||||
|
LIBGLACIER_SO=/build/build/lib/shared/libglacier-ng.so luajit run_all.lua'
|
||||||
```
|
```
|
||||||
`test_exdev.lua` needs `--cap-add=SYS_ADMIN` in Docker to actually
|
GNU `tar` is required: the hostile-archive fixtures use `tar --append`.
|
||||||
exercise the mount path; it skips cleanly (not a failure) without it.
|
|
||||||
|
|
||||||
## Build automation (bootstrap toolchain)
|
**Shell suites** (`glacier-ng/tests/*.sh`):
|
||||||
|
|
||||||
- `gbuild` (bash): single-package builder. Autodetects build system
|
| Suite | Image |
|
||||||
(cmake/meson/cargo/go/autotools/make), supports cross-compilation
|
|---|---|
|
||||||
(`-t`/`-x`, now also `-P` for profiles).
|
| `syspkg_alpine.sh`, `overwrite_alpine.sh` | `alpine:latest` with `/src`, `/libsrc` and `/t.sh` mounted (see each header); `overwrite_alpine.sh` runs both as-is and with `--tmpfs /glacier:exec` |
|
||||||
- `gworld` (Lua): batch orchestrator. Reads `recipes/*.recipe`,
|
| `verify_alpine.sh`, `user_layout_alpine.sh`, `upgrade_alpine.sh` | `glacier-alpine_root` |
|
||||||
topologically sorts by `DEPS`, invokes `gbuild` once per package in
|
|
||||||
order. `-s SYSROOT` gives every package in the batch a shared
|
|
||||||
staging dir — relies on `gbuild -p` never wiping its target, so
|
|
||||||
headers/libs accumulate across the batch for free.
|
|
||||||
- Recipe format: `NAME VER REPO URL/REF-or-SRC DEPS BUILD INSTALL
|
|
||||||
FLAGS SYS_PROFILE` (plain `key=value`, see `recipes/musl.recipe` for
|
|
||||||
a real example).
|
|
||||||
- Real multi-pass toolchain bootstraps (LFS-style — gcc built twice,
|
|
||||||
etc.) should use separate recipes per pass (`gcc-pass1`,
|
|
||||||
`gcc-pass2`), publishing only the final pass under the real
|
|
||||||
name/repo. The DAG models build steps, not abstract packages.
|
|
||||||
- **`grootstrap` needs a rebuild, not yet done.** The old design (raw
|
|
||||||
`tar` extraction into a target dir + chroot + register everything
|
|
||||||
under an arbitrary bootstrap uid) predates the `GL_SCOPE_SYS` +
|
|
||||||
base-repo design and is now more complicated than necessary. Once
|
|
||||||
`gpkg`/`gstore` themselves exist on the target (still an unsolved
|
|
||||||
bootstrapping-the-bootstrapper problem — how do they get there
|
|
||||||
before a working package manager exists to install them with),
|
|
||||||
every package including `base` ones can go through the same `gpkg
|
|
||||||
-s -l` path; the repo alone routes symlinks correctly. No more
|
|
||||||
special-casing base packages during bootstrap.
|
|
||||||
|
|
||||||
## Known open items / stubs
|
Build the `glacier-alpine_root` image with:
|
||||||
|
```sh
|
||||||
|
cd ~/Projects/glacier-ng && docker build -f Dockerfile.alpine_root \
|
||||||
|
--build-context libglacier=../libglacier-ng -t glacier-alpine_root .
|
||||||
|
docker run --rm -v "$PWD/tests/NAME.sh:/t.sh:ro" glacier-alpine_root sh /t.sh
|
||||||
|
```
|
||||||
|
|
||||||
- `gpkg -f` / `-u` (merge/update) are unimplemented stubs. `-f`
|
**Interactive images:** `./run_docker.sh Dockerfile.{alpine,alpine_root,arch,debian}`.
|
||||||
references an undefined `uid` variable (should be `uidn`) — will
|
`run_docker.sh` passes the `libglacier` build context. `alpine`, `arch`
|
||||||
nil-concatenation-error the moment it's actually implemented.
|
and `debian` default to user `glacier` (uid 1000, in wheel); `alpine_root`
|
||||||
- `gpkg -x`'s confirmation summary shows placeholder `unknown::pkg
|
defaults to root. All of them have:
|
||||||
0.0.0 (unknown)` since it has no local manifest to read version/repo
|
- root-owned `/app`;
|
||||||
from for a bare package name. Would need `resolve_repo` called
|
- no `/glacier` until `gstore -n` / `syspkg -n`;
|
||||||
before building the summary, not just before the removal itself.
|
- the doas rule plus `permit nopass glacier`;
|
||||||
- `gpkg -x` doesn't print a completion message on success (`-l` does:
|
- `glacier.conf` installed.
|
||||||
"Completed with no errors."). Cosmetic inconsistency.
|
|
||||||
- Reinstalling an up-to-date package double-logs ("Installed X" +
|
|
||||||
"staged from X" for the same event).
|
|
||||||
- `extra` vs `community` repos don't currently diverge in behavior —
|
|
||||||
both just mean "not base" for symlink-routing purposes.
|
|
||||||
- `gl_relink_store`/`gl_rebuild_index` are full rescans of the store on
|
|
||||||
every commit — fine at current scale, worth revisiting if large
|
|
||||||
transactions become common (a dirty-set of touched packages instead
|
|
||||||
of full rescans).
|
|
||||||
- Package manifests don't record which `SYS_PROFILE` they were built
|
|
||||||
under — deliberately not added yet (would need a `pkg.c` /
|
|
||||||
`gl_gpm2gpkg` change), flagged as a future option, not a gap that
|
|
||||||
needs fixing now.
|
|
||||||
- A stray nested `lib/glacier/lib/glacier/` directory was spotted once
|
|
||||||
in a `tree` listing — looked like a leftover `make install`
|
|
||||||
artifact, not in any load path, harmless but worth a `rm -rf`
|
|
||||||
eventually.
|
|
||||||
|
|
||||||
## Conventions worth knowing before editing
|
**Boot test:** `cd ~/Projects/everest && ./everest.sh boot` boots QEMU
|
||||||
|
(KVM). Press Enter for a root shell; `poweroff -f` to leave.
|
||||||
|
|
||||||
- C source generally uses tabs for indentation, though the codebase is
|
## Bug history: so these don't get reintroduced
|
||||||
inconsistent about it in places (some blocks use spaces). ALWAYS USE TABS WHEN POSSIBLE.
|
|
||||||
- `PATH_MAX`-sized stack buffers + `snprintf` is the standard pattern
|
All fixed in the current tree.
|
||||||
for path construction throughout; prefer it over `strncpy` for
|
|
||||||
anything copying into a fixed buffer (see bug #7).
|
1. `gl_abort_transaction` never released the lock (wrong variable). Now
|
||||||
- Don't add convenience wrappers that silently touch a broader
|
`open(O_CREAT)` + `flock()`, and lock files are never unlinked.
|
||||||
filesystem scope than what's asked — this codebase has already been
|
2. No `EXDEV` check before renaming. Commit `stat()`s both sides and
|
||||||
bitten twice by exactly that class of mistake (`gl_relink_store`'s
|
returns `GL_TXN_ERR_XDEV`.
|
||||||
wipe, `gl_link_pkg`'s blind `unlink`), both only becoming dangerous
|
3. Segfault on install: the `transaction` target didn't link
|
||||||
once a path assumption (`links_path` is glacier-exclusive) quietly
|
`istoreutils`/`log`.
|
||||||
stopped holding.
|
4. Stale links post-commit: `gl_link_pkg` ran during staged installs,
|
||||||
|
embedding stage paths. It now runs only for LIVE contexts, and links
|
||||||
|
regenerate post-commit.
|
||||||
|
5. Dangling links were never cleaned on removal → `prune_stale_links`.
|
||||||
|
6. `GL_TXN_ERR_RENAME` after the first commit: a leftover `old/`. It's now
|
||||||
|
wiped first.
|
||||||
|
7. `-Wstringop-truncation`: `strncpy(..., PATH_MAX - 1)` could leave
|
||||||
|
strings unterminated. Use `snprintf(dst, sizeof dst, "%s", ...)`.
|
||||||
|
8. gbuild detected a bare `Makefile` before `configure`. The fix had been
|
||||||
|
lost from the tree and was re-applied 2026-10-04.
|
||||||
|
9. The index had no version field; it's now `repo::pkgname::version`.
|
||||||
|
10. [The code this was about is NOT in the tree: ROADMAP 4.1.] A recipe
|
||||||
|
field silently never parsed and fell back to a plausible default.
|
||||||
|
Remember the class.
|
||||||
|
11. `gsha256sum.c` included `sha256.c` instead of `.h`.
|
||||||
|
12. gbuild told users to run `gpkg -f` (a stub) instead of `gpkg -l`.
|
||||||
|
13. `gpkg -f`'s stub referenced an undefined `uid`.
|
||||||
|
14. Path traversal from manifests/archives: `PKG_NAME`/`PKG_REPO` must
|
||||||
|
pass `gl_valid_pkg_ident`, and `..`/absolute entries refuse the
|
||||||
|
package. Finding 1 (self-declared `base`) stays open until signing.
|
||||||
|
15. Stage seeding and `gl_rmdir_recursive` skipped ALL dot-entries:
|
||||||
|
dotfiles vanished, and `old/` couldn't be removed (bug 6 again).
|
||||||
|
16. Per-user transactions could never work for a real user (shared
|
||||||
|
writable parents needed). Fixed by the per-uid layout. Found alongside:
|
||||||
|
- `gstore -n` never created parent directories;
|
||||||
|
- the stage was wiped before the lock was taken;
|
||||||
|
- cross-filesystem seeding was reported as `MKDIR`;
|
||||||
|
- `gl_restore_istore` doubled the uid and accepted `..` entries;
|
||||||
|
- gstore loaded the combined `.so` via `LIBGLACIER_SO` from the
|
||||||
|
environment.
|
||||||
|
17. Package symlinks were silently dropped at extraction, and only regular
|
||||||
|
files were linked, so musl's loader was never installed. Alpine-based
|
||||||
|
tests passed only because Alpine provides its own loader.
|
||||||
|
18. `add_to_archive` (`gstore -b`, backups) silently dropped symlinks.
|
||||||
|
19. gpkg acted on `-l` as soon as it parsed it, so `-lO` ignored its `O`.
|
||||||
|
20. `tests/glacier_ffi.lua` duplicated the cdefs and had drifted (it was
|
||||||
|
missing `GL_INSTALL_ERR_SCOPE`). It's now a wrapper.
|
||||||
|
21. Link conflicts were re-reported on every system commit, for every
|
||||||
|
package (it now uses the `.glacier-fresh` marker).
|
||||||
|
22. A failed extraction left a half-made version directory, so in a live
|
||||||
|
context every retry reported `ALREADY_INSTALLED`.
|
||||||
|
23. Backups written in the same second were pruned in the wrong order
|
||||||
|
(whole-second mtimes).
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
- **C style:** tabs, but the codebase is inconsistent; match the
|
||||||
|
surrounding function.
|
||||||
|
- **Paths:** `PATH_MAX` stack buffers plus `snprintf` for path
|
||||||
|
construction.
|
||||||
|
- **Lua requires:** gpkg uses bare CWD-relative requires.
|
||||||
|
syspkg and gstore pin their paths (see CLIs).
|
||||||
|
- **Lua numbers in C varargs:** a Lua number reaches C varargs as a
|
||||||
|
double, so never pass one to `lg_printf("%d")`. Use `%s` with
|
||||||
|
`tostring()`, or `ffi.new("int", n)`.
|
||||||
|
- **Shell commands from Lua:** always `shell_quote` interpolated paths
|
||||||
|
(syspkg runs as root).
|
||||||
|
- **Verify by running.** Several real bugs here were found only by
|
||||||
|
executing something.
|
||||||
@@ -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).
|
||||||
@@ -0,0 +1,280 @@
|
|||||||
|
+--------------------------------------------+
|
||||||
|
| Building Everest system images |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
STATUS: PROPOSAL. Nothing here is implemented, and the items in
|
||||||
|
section 9 are decisions still to be made, not settled ones. This is the
|
||||||
|
"dedicated design pass" SECURITY_DESIGN.txt section 10 says the
|
||||||
|
installer needs, started from the build side rather than the install
|
||||||
|
side.
|
||||||
|
|
||||||
|
Companion documents: SECURITY_DESIGN.txt (refusal matrix, syspkg
|
||||||
|
privilege model, section 10's installer reference material),
|
||||||
|
PACKAGE_SCOPES.txt (symlink routing, why /usr is shared territory).
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 0. Why now |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
The first real `syspkg -l musl` (Alpine container, 2026-10-04) worked
|
||||||
|
exactly as designed, and showed why testing on a foreign distro has
|
||||||
|
run out of road: Alpine's own musl-dev already owns 231 of the paths
|
||||||
|
musl links into /usr, so every one was refused, and the -O/--overwrite
|
||||||
|
flag had to be added just to make musl's files the live ones.
|
||||||
|
|
||||||
|
In a system glacier builds itself, /usr starts empty. No conflicts, no
|
||||||
|
need for -O, and every file in /usr is a glacier symlink by
|
||||||
|
construction. That is the actual target. Every test so far has run
|
||||||
|
against a host distro's /usr instead.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 1. What "system image" means here |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
SECURITY_DESIGN.txt section 10 records that the old "system image
|
||||||
|
tarball" idea was DISCARDED in favor of package-list-driven
|
||||||
|
bootstrapping (glacier-bootstrap TEMPLATE_INDEX TARGET). This proposal
|
||||||
|
does not bring it back. The distinction:
|
||||||
|
|
||||||
|
REJECTED an image as the INPUT to an install: a prebuilt rootfs
|
||||||
|
tarball unpacked onto the target. Files arrive without
|
||||||
|
passing through syspkg, so nothing in it is under the
|
||||||
|
integrity guarantee.
|
||||||
|
|
||||||
|
PROPOSED an image as the OUTPUT of a bootstrap: a target root
|
||||||
|
built entirely by `syspkg` installing a defined package
|
||||||
|
list, every package through the normal verified path.
|
||||||
|
Packing that directory (tarball, OCI, disk image) is just
|
||||||
|
shipping a result, not a second way of producing one.
|
||||||
|
|
||||||
|
So "build a system image" = "run the bootstrap against a directory,
|
||||||
|
then optionally pack it". The bootstrap is the thing being designed;
|
||||||
|
the packing formats are thin (section 6).
|
||||||
|
|
||||||
|
DECISION NEEDED (9a): confirm this reading of section 10.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 2. Pipeline |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
image definition (section 5: profile + package list)
|
||||||
|
|
|
||||||
|
gworld -P PROFILE build every recipe the image needs
|
||||||
|
| -> pkgs/*.gpkg + *.gpkg.sha256
|
||||||
|
syspkg --root T -n create T/glacier/sys/...
|
||||||
|
|
|
||||||
|
syspkg --root T -l PKG... install the package list into T
|
||||||
|
| (base -> T/usr, others ->
|
||||||
|
| T/glacier/sys/links)
|
||||||
|
finalize T /etc skeleton, doas.conf, etc.
|
||||||
|
|
|
||||||
|
pack T dir | rootfs tarball | OCI | disk
|
||||||
|
|
||||||
|
Everything above already exists except `--root` (section 3/4), the
|
||||||
|
image definition (section 5), finalize, and pack.
|
||||||
|
|
||||||
|
The bootstrapping-the-bootstrapper problem (PACKAGE_SCOPES.txt section
|
||||||
|
6) mostly dissolves: syspkg, libglacier-ng, luajit and the rest of the
|
||||||
|
glacier tooling are just base-repo packages in the list. The build
|
||||||
|
host's syspkg installs them into T through the same verified path as
|
||||||
|
everything else. The trust edge moves from "whatever lays syspkg down"
|
||||||
|
to "the build host's syspkg". That's an honest edge, but a much smaller
|
||||||
|
and more auditable one than an unverified bootstrap step.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 3. Library prerequisite: install root |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
Every path in the library is a hardcoded absolute path today:
|
||||||
|
pkg.c's gl_root/gl_sys*/gl_usr*, transaction.c's GL_*_BASE and
|
||||||
|
GL_*_INDEX/STORE/LINKS macros, derive_base_paths, the stage and old
|
||||||
|
paths in init/commit, the fsync_dir list, GL_BASE_LINKS_DEST ("/usr").
|
||||||
|
ctx->root_path exists but is always "/glacier" and nothing reads it as
|
||||||
|
a prefix.
|
||||||
|
|
||||||
|
Building into T needs TWO distinct roots, and conflating them is the
|
||||||
|
dangerous mistake:
|
||||||
|
|
||||||
|
install root where bytes are written: T (e.g. /mnt/everest)
|
||||||
|
runtime root what the paths mean once the /
|
||||||
|
image is booted / chrooted
|
||||||
|
|
||||||
|
Symlink TARGETS must use runtime paths (/glacier/sys/store/...), or the
|
||||||
|
image breaks the moment it's booted. Every FILESYSTEM OPERATION must
|
||||||
|
use install-root paths (T/glacier/sys/store/..., T/usr/...).
|
||||||
|
|
||||||
|
The specific trap: prune_stale_links decides a glacier symlink is stale
|
||||||
|
by stat()ing through it. For an absolute target, that resolves against
|
||||||
|
the HOST's filesystem, not T. That would prune live links because the
|
||||||
|
host doesn't have the package, or worse, keep stale ones because it
|
||||||
|
does. is_glacier_symlink is fine: it only compares the readlink()
|
||||||
|
string against the runtime store prefix. But every "does the target
|
||||||
|
exist" check has to become "does T + target exist". The same goes for
|
||||||
|
restore_displaced and for anything else that follows a link.
|
||||||
|
|
||||||
|
Proposed shape:
|
||||||
|
|
||||||
|
- gl_context_t gains `char sysroot[PATH_MAX]` ("" == "/"; appended
|
||||||
|
at the end, matched in lib/glacier_cdef.lua and
|
||||||
|
tests/glacier_ffi.lua).
|
||||||
|
- New gl_init_live_context_at / gl_init_stage_context_at /
|
||||||
|
gl_init_sys_at taking a sysroot. The existing functions become
|
||||||
|
wrappers passing "/", so every current caller is unchanged.
|
||||||
|
- One helper, gl_at(ctx, runtime_path, out), used for EVERY
|
||||||
|
filesystem operation. Runtime paths stay what's stored and compared.
|
||||||
|
- GL_BASE_LINKS_DEST resolves to sysroot + "/usr". Never the host's
|
||||||
|
/usr when a sysroot is set.
|
||||||
|
|
||||||
|
The non-negotiable test: an image build must leave the host's /usr and
|
||||||
|
/glacier byte-for-byte untouched. That is a test assertion, not a code
|
||||||
|
review item. Same class of bug as the old gl_relink_store wipe: a path
|
||||||
|
assumption quietly stops holding.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 4. syspkg --root |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
syspkg gains `-r/--root DIR`, which selects the *_at init variants.
|
||||||
|
gpkg/gstore do NOT get it. Per-user scope has no business in an image
|
||||||
|
build, and section 5 of SECURITY_DESIGN.txt keeps those tools from ever
|
||||||
|
reaching system scope.
|
||||||
|
|
||||||
|
Privilege: still root via doas, still no setuid. New trust check, same
|
||||||
|
reasoning as syspkg's existing self-check: DIR and every ancestor must
|
||||||
|
be root-owned and not group/world-writable. A root process writing into
|
||||||
|
a user-controlled tree is a symlink-swap away from writing somewhere
|
||||||
|
else entirely (replace T/usr with a symlink to the host's /usr between
|
||||||
|
two syscalls). Same refusal style, same message format.
|
||||||
|
|
||||||
|
The hash refusal matrix applies unchanged. Everything in an image is
|
||||||
|
either base (always refuse unverified) or system scope (always refuse
|
||||||
|
unverified), so an image build can only ever contain packages with
|
||||||
|
valid sidecars. That's the property we want; it means section 2's
|
||||||
|
gworld step must produce sidecars for everything (gbuild already does).
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 5. Image definition = template index |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
SECURITY_DESIGN.txt section 10 noted that the old guide's "template
|
||||||
|
index" questions (32-bit libs? which libc? SELinux? init system?) are
|
||||||
|
the same axes SYS_PROFILE already encodes. Proposal: a template index
|
||||||
|
IS a profile plus an explicit package list, in the same KEY=value
|
||||||
|
format the recipe parser already reads:
|
||||||
|
|
||||||
|
# images/minimal.image
|
||||||
|
NAME=minimal
|
||||||
|
SYS_PROFILE=x86_64-musl
|
||||||
|
PKGS=musl busybox luajit libarchive zlib libconfig libglacier-ng glacier-ng doas
|
||||||
|
RECIPES=recipes/
|
||||||
|
|
||||||
|
No runtime dependency resolution yet. Manifests DO carry PKG_DEPS and
|
||||||
|
PKG_CONFS, and libglacier parses them, but nothing consumes them and
|
||||||
|
every package leaves them empty. (DEPS in recipes is build ordering.)
|
||||||
|
So the list is explicit and complete. GLACIER_BOOTSTRAP.txt section 4
|
||||||
|
proposes checking it against PKG_DEPS rather than auto-resolving.
|
||||||
|
|
||||||
|
Remember gworld bug 10: a field that silently never parses and falls
|
||||||
|
back to a plausible default. Unknown keys and an empty PKGS must be
|
||||||
|
hard errors.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 6. Output formats |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
In order of usefulness right now:
|
||||||
|
|
||||||
|
a. Directory. What the pipeline produces anyway. chroot-able
|
||||||
|
immediately, which is the first milestone (section 8).
|
||||||
|
|
||||||
|
b. Rootfs tarball -> `docker import`. Replaces the Alpine-based test
|
||||||
|
Dockerfiles with a real Everest userland. Ends the glibc-vs-musl
|
||||||
|
.so mismatch and the missing-linux-headers class of problem,
|
||||||
|
because the test environment is Everest. Every syspkg/gpkg test
|
||||||
|
then runs against a /usr glacier built.
|
||||||
|
|
||||||
|
c. Bootable disk image (kernel, bootloader, partition layout per the
|
||||||
|
old guide's /boot + optional separate /glacier partition). Later,
|
||||||
|
and blocked on there being kernel/bootloader recipes at all.
|
||||||
|
|
||||||
|
Reproducibility, so two builds of the same definition can be compared:
|
||||||
|
index files are named by wall-clock time (make_index_name), and tar
|
||||||
|
records mtimes and ownership. Honour SOURCE_DATE_EPOCH in
|
||||||
|
make_index_name, and pack with sorted entries, clamped mtimes and
|
||||||
|
numeric root:root ownership.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 7. Building without a host /usr |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
Consequences of /usr starting empty that the design has to own:
|
||||||
|
|
||||||
|
- Strict linking. In an image build, any "Refusing to overwrite
|
||||||
|
non-glacier path" means two packages in the list conflict, or
|
||||||
|
finalize wrote somewhere it shouldn't. It should FAIL the build, not
|
||||||
|
warn (an image-build flag on the library, or syspkg treating the
|
||||||
|
warning count as an error under --root). -O must never be used in
|
||||||
|
an image build; with an empty /usr it has nothing legitimate to do.
|
||||||
|
- Paths the system needs that no package ships: /etc (passwd, group,
|
||||||
|
doas.conf), /dev /proc /sys mountpoints, /tmp, /var. Either a `base`
|
||||||
|
package (e.g. `filesystem`, Arch's approach) or the finalize step.
|
||||||
|
Recommendation: a package, so it's verified and owned like
|
||||||
|
everything else. Finalize then shrinks to things that genuinely
|
||||||
|
vary per image (hostname, users).
|
||||||
|
- /bin, /lib, /sbin. PACKAGE_SCOPES routes base to /usr only. Either
|
||||||
|
the filesystem package ships /bin -> usr/bin style merged-usr
|
||||||
|
symlinks, or every #! /bin/sh breaks. Recommendation: merged /usr,
|
||||||
|
shipped by the filesystem package.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 8. Milestones |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
1. sysroot in the library (section 3), plus the host-untouched test.
|
||||||
|
No CLI changes, testable through FFI alone.
|
||||||
|
2. syspkg --root (section 4), with the root-owned-target check.
|
||||||
|
3. A `filesystem` base package (section 7).
|
||||||
|
4. images/minimal.image + a driver script (gworld -> syspkg --root
|
||||||
|
-> finalize). Milestone: `chroot T /bin/sh` works, and
|
||||||
|
`chroot T syspkg -l` installs a package from inside.
|
||||||
|
5. Tarball + `docker import`; switch the test suite to run on it.
|
||||||
|
6. Disk images: blocked on kernel/bootloader recipes and on the
|
||||||
|
installer side of section 10 (partitioning, the separate /glacier
|
||||||
|
partition question).
|
||||||
|
|
||||||
|
Hash verification at install time (SECURITY_DESIGN.txt section 4)
|
||||||
|
should land before or alongside milestone 2. An image build is
|
||||||
|
"base-repo packages, the strictest tier" by definition, so it's the
|
||||||
|
first real consumer of the refusal matrix.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 9. Open decisions |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
a. Section 1's reading of SECURITY_DESIGN section 10: images as
|
||||||
|
bootstrap OUTPUT are fine, images as install INPUT stay rejected.
|
||||||
|
b. Template index format: the KEY=value file in section 5, or
|
||||||
|
something else.
|
||||||
|
c. `filesystem` package vs finalize script for /etc and friends, and
|
||||||
|
merged /usr (section 7).
|
||||||
|
d. Build privilege: building as real root inside a throwaway
|
||||||
|
container (works today), or rootless via user namespaces (unshare
|
||||||
|
-r) later. Rootless changes nothing in section 4's checks, since
|
||||||
|
inside the namespace the tree IS root-owned, but it does need the
|
||||||
|
packing step to write numeric root ownership regardless of who
|
||||||
|
built it.
|
||||||
|
e. Whether the image's own index/store should be built with the
|
||||||
|
image's final paths only, or also carry provenance: which
|
||||||
|
SYS_PROFILE and which image definition produced it. CLAUDE.md
|
||||||
|
already lists "manifests don't record SYS_PROFILE" as open. An
|
||||||
|
image is where that gap starts to hurt.
|
||||||
+24
-9
@@ -15,20 +15,25 @@ up to and including destroying a live /usr.
|
|||||||
+------------------------------------+
|
+------------------------------------+
|
||||||
|
|
||||||
USR (GL_SCOPE_USR)
|
USR (GL_SCOPE_USR)
|
||||||
Per-uid packages. Index/store/links all live under a specific
|
Per-uid packages. Everything a uid's transactions touch lives
|
||||||
uid's tree:
|
under ONE directory that uid owns:
|
||||||
/glacier/usr/index/<uid>
|
/glacier/usr/<uid>/{index,store,links,stage,old,lock}
|
||||||
/glacier/usr/store/<uid>
|
/glacier/usr itself is root-owned (0755) and each /glacier/usr/<uid>
|
||||||
/glacier/usr/links/<uid>
|
is owned by that uid (0700), so every create/rename/lock a
|
||||||
Symlinks always land under /glacier/usr/links/<uid>/{bin,...}.
|
transaction does happens inside the user's own directory — no shared
|
||||||
|
writable parent, so no uid can create or squat another's. Root
|
||||||
|
creates each one: `gstore -n UID` directly, or a user for themselves
|
||||||
|
via a doas rule pinned to `gstore -n` (see gstore's header). The
|
||||||
|
library refuses a base that isn't a real directory owned by its uid
|
||||||
|
with no group/other bits, or whose /glacier/usr is loosened.
|
||||||
|
Symlinks always land under /glacier/usr/<uid>/links/{bin,...}.
|
||||||
There is no base-repo exception for USR scope. Nothing installed
|
There is no base-repo exception for USR scope. Nothing installed
|
||||||
under a per-user scope is ever considered part of the base system,
|
under a per-user scope is ever considered part of the base system,
|
||||||
so nothing here ever touches /usr.
|
so nothing here ever touches /usr.
|
||||||
|
|
||||||
SYS (GL_SCOPE_SYS)
|
SYS (GL_SCOPE_SYS)
|
||||||
System-wide packages, not owned by any particular uid:
|
System-wide packages, not owned by any particular uid:
|
||||||
/glacier/sys/index
|
/glacier/sys/{index,store,links,stage,old,lock}
|
||||||
/glacier/sys/store
|
|
||||||
Symlink destination depends on the package's repo (see section 3).
|
Symlink destination depends on the package's repo (see section 3).
|
||||||
Requires root. gpkg enforces this via geteuid() before it will
|
Requires root. gpkg enforces this via geteuid() before it will
|
||||||
even attempt a system-scope operation.
|
even attempt a system-scope operation.
|
||||||
@@ -69,9 +74,11 @@ decides where that package's activation symlinks physically land.
|
|||||||
| 3. Symlink placement rule |
|
| 3. Symlink placement rule |
|
||||||
+------------------------------------+
|
+------------------------------------+
|
||||||
|
|
||||||
scope == USR -> /glacier/usr/links/<uid>
|
scope == USR -> /glacier/usr/<uid>/links
|
||||||
scope == SYS && repo == "base" -> /usr (*)
|
scope == SYS && repo == "base" -> /usr (*)
|
||||||
scope == SYS && repo != "base" -> /glacier/sys/links
|
scope == SYS && repo != "base" -> /glacier/sys/links
|
||||||
|
scope == SYS, package path etc/... -> COPIED into /etc (any repo;
|
||||||
|
config, not linked — see below)
|
||||||
|
|
||||||
(*) This is the base-system exception. Packages that are part of the
|
(*) This is the base-system exception. Packages that are part of the
|
||||||
minimal working system get their symlinks placed directly into
|
minimal working system get their symlinks placed directly into
|
||||||
@@ -84,6 +91,14 @@ decides where that package's activation symlinks physically land.
|
|||||||
same as it always has. The base system is the exception to the
|
same as it always has. The base system is the exception to the
|
||||||
norm, not the other way around.
|
norm, not the other way around.
|
||||||
|
|
||||||
|
Config files are the exception to linking. Editing a symlink into the
|
||||||
|
store would edit the store itself, so a system-scope package's etc/ is
|
||||||
|
copied into /etc instead. glacier keeps a record of the last version
|
||||||
|
each package offered (<store>/.etc-state/). A file the admin changed,
|
||||||
|
deleted, or that glacier never installed is never overwritten; package
|
||||||
|
changes to it arrive once as FILE.new. Unchanged files are updated in
|
||||||
|
place and removed with their package (pacman-style).
|
||||||
|
|
||||||
This decision is made per-package, at the point where a package's repo
|
This decision is made per-package, at the point where a package's repo
|
||||||
is actually known (inside gl_link_pkg / gl_unlink_pkg / the walk loop
|
is actually known (inside gl_link_pkg / gl_unlink_pkg / the walk loop
|
||||||
in gl_relink_store) — NOT baked into the context at creation time.
|
in gl_relink_store) — NOT baked into the context at creation time.
|
||||||
|
|||||||
+208
@@ -0,0 +1,208 @@
|
|||||||
|
+--------------------------------------------+
|
||||||
|
| Glacier roadmap |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
What's left to build, in priority order. Status as of 2026-10-04.
|
||||||
|
Design detail lives in the companion docs; this file says WHAT and in
|
||||||
|
what order, and links to WHERE:
|
||||||
|
|
||||||
|
SECURITY_DESIGN.txt integrity, scope separation, refusal matrix
|
||||||
|
PACKAGE_SCOPES.txt layout, link routing, /etc config handling
|
||||||
|
IMAGE_DESIGN.txt system images as bootstrap output, sysroot
|
||||||
|
GLACIER_BOOTSTRAP.txt glacier-bootstrap plan, prerequisites, decisions
|
||||||
|
|
||||||
|
Mark an item [DONE yyyy-mm-dd] rather than deleting it, so the history
|
||||||
|
stays readable. CLAUDE.md "Known open items" tracks small bugs; this
|
||||||
|
file tracks work.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 1. Install Everest with glacier alone |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
The critical path: from "QEMU boots an initramfs assembled by a script"
|
||||||
|
(~/Projects/everest) to "glacier installs a system".
|
||||||
|
|
||||||
|
1.1 sysroot in the library (IMAGE_DESIGN section 3). Every path is
|
||||||
|
hardcoded to /glacier and /usr. Needs separate install-root
|
||||||
|
(where bytes are written) and runtime-root (what symlink targets
|
||||||
|
say). The trap: prune_stale_links follows absolute links, so a
|
||||||
|
naive prefix would judge the image's links against the HOST's
|
||||||
|
filesystem. Required test: an image build leaves the host's /usr
|
||||||
|
and /glacier byte-identical.
|
||||||
|
|
||||||
|
1.2 syspkg --root DIR (refuses unless DIR and its ancestors are
|
||||||
|
root-owned and not group/world-writable) and --strict (any link
|
||||||
|
refusal fails the transaction; an image's /usr starts empty).
|
||||||
|
gl_create_fs_tree already takes a root prefix.
|
||||||
|
|
||||||
|
1.3 glacier-bootstrap, local mode (GLACIER_BOOTSTRAP.txt sections 1-6).
|
||||||
|
Template index -> resolve -> verify -> syspkg --root, one
|
||||||
|
transaction. Never writes into TARGET itself.
|
||||||
|
|
||||||
|
1.4 Packages for glacier itself, so an installed system can manage
|
||||||
|
itself: luajit, libarchive (+ zlib), libconfig, libglacier-ng,
|
||||||
|
glacier-ng (syspkg, gpkg, gstore), doas.
|
||||||
|
|
||||||
|
1.5 The installer. Undesigned; needs its own design session
|
||||||
|
(SECURITY_DESIGN section 10). Partitioning, a bootloader or an
|
||||||
|
EFI-stub kernel on the ESP, copying the kernel there (a symlink
|
||||||
|
won't do on FAT), a disk image.
|
||||||
|
|
||||||
|
Already done on this path: hash verification, package symlinks, the
|
||||||
|
merged-/usr tree on syspkg -n, /etc config handling, a QEMU-bootable
|
||||||
|
musl + busybox + linux + everest-bootscripts set.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 2. Package-manager features |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
2.1 UPGRADES. Spec (2026-10-04):
|
||||||
|
- Replacing an installed version is `-lO` (local file) — plain
|
||||||
|
-l refuses ("extra::foo 1.0.0 is already installed.", nothing
|
||||||
|
more). [DONE 2026-10-04]
|
||||||
|
- The replaced version is BACKED UP: its files compressed into
|
||||||
|
<scope>/backup/<repo>/<name>/<name>-<ver>.gpkg + .sha256, a
|
||||||
|
complete installable package (saved manifest included).
|
||||||
|
KEEP_OLD in glacier.conf = how many backups to keep per
|
||||||
|
package (0 = none). No default: glacier.conf ships with
|
||||||
|
KEEP_OLD = 1, and an operation that would replace a version
|
||||||
|
refuses if it's missing or invalid. [DONE 2026-10-04]
|
||||||
|
- A restore interface for backups: later. (A backup can already
|
||||||
|
be reinstalled with -lO.)
|
||||||
|
- `gpkg -u` / `syspkg -u` = upgrade from a REMOTE repository;
|
||||||
|
waits for 3.1, like -f.
|
||||||
|
- /etc follows the existing config rules regardless.
|
||||||
|
|
||||||
|
2.2 gpkg -f / syspkg -f: MERGE. Spec (2026-10-04): exactly like -l,
|
||||||
|
except the package comes from a remote repository by name
|
||||||
|
instead of from a local file. Same summary, verification, scope
|
||||||
|
rules and transaction. Depends on 3.1 (remote repositories +
|
||||||
|
signing): until then there is nothing safe to fetch from.
|
||||||
|
|
||||||
|
2.3 Dependencies. PKG_DEPS / PKG_CONFS are parsed and ignored.
|
||||||
|
- install: refuse if a dependency isn't installed (or in the
|
||||||
|
same transaction);
|
||||||
|
- REMOVE: refuse to remove a package others depend on. Today
|
||||||
|
nothing stops `syspkg -x musl` from breaking every binary on
|
||||||
|
the system. Highest-value item in this section;
|
||||||
|
- conflicts: refuse to install a package that conflicts with
|
||||||
|
an installed one.
|
||||||
|
Check-only vs automatic resolution: GLACIER_BOOTSTRAP decision 9e.
|
||||||
|
|
||||||
|
2.4 Queries: list installed packages, show a package's details, find
|
||||||
|
which package owns a file. The index has the data; no CLI shows it.
|
||||||
|
|
||||||
|
2.5 Rollback. Every commit moves the previous state to old/ and then
|
||||||
|
deletes it. Keep it (or the last N), plus a command to return to
|
||||||
|
it. Interacts with 2.1's KEEP_OLD.
|
||||||
|
|
||||||
|
2.6 File conflicts between packages: two packages shipping the same
|
||||||
|
path — whichever links last silently wins. Detect and refuse.
|
||||||
|
|
||||||
|
2.7 glacier.conf parsing. [DONE 2026-10-04 for integers:
|
||||||
|
gl_conf_get_int, no defaults, missing/unreadable/wrong-type are
|
||||||
|
distinct errors.] Still open: rejecting UNKNOWN keys (CLAUDE.md bug
|
||||||
|
10's lesson — a misspelled KEEP_OLD currently reads as "not set",
|
||||||
|
which at least fails loudly).
|
||||||
|
|
||||||
|
2.8 gstore: -s (sync) is a stub; there's no restore command although
|
||||||
|
gstore -b's output tells you to run -r (gl_restore_istore exists).
|
||||||
|
|
||||||
|
2.9 User-scope config files: user packages still link etc/ into a
|
||||||
|
links tree that's rebuilt on every commit.
|
||||||
|
|
||||||
|
2.10 Hardlink entries in package archives are skipped at extraction.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 3. Security |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
3.1 Remote repositories + signing (authenticity, keyring). The agreed
|
||||||
|
next major item; separate design pass. Unblocks 2.2 and remote
|
||||||
|
glacier-bootstrap. Until then, GLACIER_BOOTSTRAP's pinned
|
||||||
|
templates are the safe bridge for remote fetches.
|
||||||
|
|
||||||
|
3.2 Hash verification lives in the CLIs' shared module. A third-party
|
||||||
|
tool calling gl_install_pkg directly isn't verified. Consider
|
||||||
|
moving it into the library, like the base-in-user-scope refusal.
|
||||||
|
|
||||||
|
3.3 Manifests don't record the SYS_PROFILE they were built under, so a
|
||||||
|
package built for the wrong target can't be detected.
|
||||||
|
|
||||||
|
3.4 gstore -n accepts UIDs with no passwd entry (pre-provisions a
|
||||||
|
directory nobody owns yet; a typo creates a stray one).
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 4. Build tooling |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
4.1 gbuild system profiles (-P ARCH-LIBC[-FEATURE...]) and gworld's
|
||||||
|
SYS_PROFILE recipe field. CLAUDE.md described both as done; neither
|
||||||
|
exists in the tree (only one gbuild, in glacier-ng). Today the
|
||||||
|
target libc is decided by where gbuild runs (~/Projects/everest
|
||||||
|
builds in Alpine for musl).
|
||||||
|
|
||||||
|
4.2 Move ~/Projects/everest's per-package gbuild commands into gworld
|
||||||
|
recipes once 4.1 lands.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 5. Performance (not urgent) |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
5.1 gl_relink_store / gl_rebuild_index rescan the whole store on every
|
||||||
|
commit. Fine at current scale; a dirty set is the fix.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 6. Decisions waiting |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
- Images are only ever bootstrap OUTPUT (IMAGE_DESIGN 9a /
|
||||||
|
GLACIER_BOOTSTRAP 9a).
|
||||||
|
- Template index format (GLACIER_BOOTSTRAP 9b).
|
||||||
|
- Dependencies: check-only or automatic (GLACIER_BOOTSTRAP 9e).
|
||||||
|
- Where the pinning helper lives (GLACIER_BOOTSTRAP 9f).
|
||||||
|
- 2.1's open questions a-d (upgrade backups).
|
||||||
|
- extra vs community: should they ever behave differently?
|
||||||
|
- The installer (1.5).
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 7. Done |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
[DONE 2026-10-04] Version replacement + backups (2.1): -lO replaces,
|
||||||
|
KEEP_OLD backups, gl_backup_pkg / gl_prune_backups / gl_conf_get_int,
|
||||||
|
the manifest saved in the store (.glacier-manifest), add_to_archive
|
||||||
|
learns symlinks (gstore -b silently dropped them too), gpkg modifier
|
||||||
|
flags (-O -S -V) order-independent so `gpkg -lO` works.
|
||||||
|
tests/upgrade_alpine.sh (29 checks).
|
||||||
|
[DONE 2026-10-04] Correctness batch:
|
||||||
|
- gbuild: autotools detected before a bare Makefile (bug 8 had
|
||||||
|
regressed); --cross-prefix fixed (its option-table key was
|
||||||
|
misspelled); -v/--version print the version as documented, with
|
||||||
|
--verbose for verbose output.
|
||||||
|
- gpkg -x: summary resolved from the index (was "unknown::pkg
|
||||||
|
0.0.0"); a package that isn't installed fails before the prompt.
|
||||||
|
- gpkg: precise refusal for a missing / squatted / loosened user
|
||||||
|
directory (was "No index found"), via gl_usr_base_status.
|
||||||
|
- gpkg -h lists -l and -S; -f/-u described by the 2.1/2.2 specs.
|
||||||
|
- Up-to-date reinstall logs once.
|
||||||
|
- Link conflicts reported once, by the package that has them, not
|
||||||
|
again on every later system commit (.glacier-fresh marker).
|
||||||
|
- A failed extraction no longer leaves a half-extracted version dir
|
||||||
|
(in a live context, every retry then said "already installed").
|
||||||
|
- Dead old-layout code removed from pkg.c (gl_prepare_pkgdir and the
|
||||||
|
gl_usr_* / gl_sys_* dir_t constants).
|
||||||
|
- tests/glacier_ffi.lua is a wrapper over lib/glacier_cdef.lua; its
|
||||||
|
own copy had already drifted (missing GL_INSTALL_ERR_SCOPE).
|
||||||
|
- Dockerfile.arch / .debian moved to the per-uid layout (root-owned
|
||||||
|
/app, libglacier-ng built from source, the gstore -n doas rule).
|
||||||
|
[DONE 2026-10-04] Hash verification; package symlinks; per-uid user
|
||||||
|
layout + doas gstore -n; syspkg -n top-level tree; /etc config
|
||||||
|
handling (copy + .new); -O/--overwrite; everest-bootscripts.
|
||||||
@@ -179,6 +179,14 @@ is being preserved exactly as-is — gstore stays permanently
|
|||||||
user-scope-only; the fix is removing -N, not touching the bounds
|
user-scope-only; the fix is removing -N, not touching the bounds
|
||||||
check.
|
check.
|
||||||
|
|
||||||
|
LATER AMENDMENT: `gstore -n` does run as root — it creates
|
||||||
|
/glacier/usr/<uid> inside a root-owned parent (PACKAGE_SCOPES.txt
|
||||||
|
section 1). Still user scope only. Root runs it with an explicit UID; a
|
||||||
|
user runs it for themselves through a doas rule pinned to exactly
|
||||||
|
`-n`, which is what stops them naming another UID (same boundary as
|
||||||
|
section 6). Being root-run, gstore follows syspkg's loading rules
|
||||||
|
(pinned paths, -E, root-ownership checks).
|
||||||
|
|
||||||
|
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
| 6. syspkg privilege model |
|
| 6. syspkg privilege model |
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+65
-1
@@ -20,6 +20,64 @@ typedef struct
|
|||||||
}
|
}
|
||||||
dir_t;
|
dir_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* On-disk layout. Each scope keeps everything it ever creates, renames
|
||||||
|
* or locks under ONE base directory it owns:
|
||||||
|
*
|
||||||
|
* /glacier/usr/<uid>/{index,store,links,stage,old,lock} owned by uid, 0700
|
||||||
|
* /glacier/sys/{index,store,links,stage,old,lock} root
|
||||||
|
*
|
||||||
|
* /glacier/usr itself is root-owned and not group/world-writable, so a
|
||||||
|
* user can never create (or squat) another uid's base; root creates
|
||||||
|
* each one (gl_init_user, via `gstore -n` directly or through doas).
|
||||||
|
* Every transaction step — stage, old/, lock, the commit renames — then
|
||||||
|
* happens inside a directory the acting user owns, which is what lets
|
||||||
|
* an unprivileged user run their own transactions at all.
|
||||||
|
*/
|
||||||
|
#define GL_USR_ROOT "/glacier/usr"
|
||||||
|
#define GL_SYS_ROOT "/glacier/sys"
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
gl_usr_base(uid_t uid, char *out, size_t outsz)
|
||||||
|
{
|
||||||
|
snprintf(out, outsz, GL_USR_ROOT "/%u", (unsigned)uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_USR_BASE_OK = 0,
|
||||||
|
GL_USR_BASE_MISSING, /* no /glacier/usr/<uid> yet: run gstore -n */
|
||||||
|
GL_USR_BASE_UNSAFE_ROOT, /* /glacier/usr not root-owned, or group/world-writable */
|
||||||
|
GL_USR_BASE_UNSAFE /* not a real dir, not owned by uid, or not 0700-ish */
|
||||||
|
} gl_usr_base_status_t;
|
||||||
|
|
||||||
|
/* Refuse to operate on a user base we didn't hand out. lstat(), never
|
||||||
|
* stat(): a symlink in place of /glacier/usr/<uid> is exactly what a
|
||||||
|
* squatter would plant. Backstop for the layout invariant above, so a
|
||||||
|
* hand-loosened /glacier/usr can't silently turn into one user writing
|
||||||
|
* another's store. */
|
||||||
|
static inline gl_usr_base_status_t
|
||||||
|
gl_usr_base_check(uid_t uid)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
|
||||||
|
if (lstat(GL_USR_ROOT, &st) != 0) { return GL_USR_BASE_MISSING; }
|
||||||
|
if (!S_ISDIR(st.st_mode) || st.st_uid != 0 ||
|
||||||
|
(st.st_mode & (S_IWGRP | S_IWOTH)) != 0) {
|
||||||
|
return GL_USR_BASE_UNSAFE_ROOT;
|
||||||
|
}
|
||||||
|
|
||||||
|
char base[PATH_MAX];
|
||||||
|
gl_usr_base(uid, base, sizeof(base));
|
||||||
|
|
||||||
|
if (lstat(base, &st) != 0) { return GL_USR_BASE_MISSING; }
|
||||||
|
if (!S_ISDIR(st.st_mode) || st.st_uid != uid ||
|
||||||
|
(st.st_mode & (S_IRWXG | S_IRWXO)) != 0) {
|
||||||
|
return GL_USR_BASE_UNSAFE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GL_USR_BASE_OK;
|
||||||
|
}
|
||||||
|
|
||||||
static inline int
|
static inline int
|
||||||
gl_mkdirp(const char *path, mode_t mode)
|
gl_mkdirp(const char *path, mode_t mode)
|
||||||
{
|
{
|
||||||
@@ -52,7 +110,13 @@ gl_rmdir_recursive(const char *path)
|
|||||||
|
|
||||||
struct dirent *ent;
|
struct dirent *ent;
|
||||||
while ((ent = readdir(dir))) {
|
while ((ent = readdir(dir))) {
|
||||||
if (ent->d_name[0] == '.') { continue; }
|
/* Only . and .. — skipping every dotfile left any directory
|
||||||
|
* containing one (a package's own dotfiles, the store's
|
||||||
|
* .displaced tree) impossible to rmdir, so the old/ and stage
|
||||||
|
* wipes silently failed and the next commit hit ENOTEMPTY. */
|
||||||
|
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
char full[PATH_MAX];
|
char full[PATH_MAX];
|
||||||
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
|
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
|
||||||
|
|||||||
+1189
-180
File diff suppressed because it is too large.
Load diff
@@ -73,9 +73,13 @@ struct gindex {
|
|||||||
|
|
||||||
bool gl_usr_istore_exists(index_store_t index_or_store, int uid);
|
bool gl_usr_istore_exists(index_store_t index_or_store, int uid);
|
||||||
bool gl_sys_istore_exists(index_store_t index_or_store);
|
bool gl_sys_istore_exists(index_store_t index_or_store);
|
||||||
|
int gl_usr_base_status(uid_t uid);
|
||||||
|
int gl_backup_pkg(gl_context_t *ctx, const char *repo, const char *name, const char *out_path);
|
||||||
|
int gl_prune_backups(const char *dir, int keep);
|
||||||
|
|
||||||
int gl_init_user(uid_t uid, bool isVerbose);
|
int gl_init_user(uid_t uid, bool isVerbose);
|
||||||
int gl_init_sys(bool isVerbose);
|
int gl_init_sys(bool isVerbose);
|
||||||
|
int gl_create_fs_tree(const char *root, bool isVerbose);
|
||||||
int gl_delete_user(uid_t uid, bool isVerbose);
|
int gl_delete_user(uid_t uid, bool isVerbose);
|
||||||
|
|
||||||
int gl_parse_index(struct gindex *idx, FILE *f);
|
int gl_parse_index(struct gindex *idx, FILE *f);
|
||||||
|
|||||||
@@ -181,13 +181,16 @@ lg_gpkg_help(int argc, char *argv[])
|
|||||||
(void)argc;
|
(void)argc;
|
||||||
|
|
||||||
printf("%s: Install, update, and remove packages\n", argv[0]);
|
printf("%s: Install, update, and remove packages\n", argv[0]);
|
||||||
printf("usage: %s [-hv] [-fux PKG] [-V]\n", argv[0]);
|
printf("usage: %s [-hv] [-flux PKG] [-OSV]\n", argv[0]);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
||||||
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
||||||
printf("%-25s %s\n", "{-f --merge} PKG...", "Merge a package into the index and store");
|
printf("%-25s %s\n", "{-l --local} FILE...", "Install local package files");
|
||||||
printf("%-25s %s\n", "{-u --update} PKG...", "Upgrade a merged package");
|
printf("%-25s %s\n", "{-f --merge} PKG...", "Install packages from a remote repository (not yet implemented)");
|
||||||
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove a package from the index and store");
|
printf("%-25s %s\n", "{-u --update} PKG...", "Upgrade installed packages (not yet implemented)");
|
||||||
|
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove installed packages");
|
||||||
|
printf("%-25s %s\n", "{-O --overwrite}", "With -l: replace an installed version (backed up per KEEP_OLD) and existing non-glacier files");
|
||||||
|
printf("%-25s %s\n", "{-S --skip-installed}", "Skip packages that are already up to date");
|
||||||
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("This program is free software.\n");
|
printf("This program is free software.\n");
|
||||||
@@ -200,17 +203,19 @@ lg_syspkg_help(int argc, char *argv[])
|
|||||||
(void)argc;
|
(void)argc;
|
||||||
|
|
||||||
printf("%s: Manage the system package index\n", argv[0]);
|
printf("%s: Manage the system package index\n", argv[0]);
|
||||||
printf("usage: %s [-hv] [-fux PKG]\n", argv[0]);
|
printf("usage: %s [-hv] [-n] [-lx PKG] [-OSV]\n", argv[0]);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("{-h}\tShow this message\n"
|
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
||||||
"{-v}\tShow the current version\n"
|
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
||||||
"{-f} PKG...\tMerge a package into the user index\n"
|
printf("%-25s %s\n", "{-n --new}", "Create the system index and store");
|
||||||
"{-u} PKG...\tUpgrade a merged package\n"
|
printf("%-25s %s\n", "{-l --local} PKG...", "Install a local package into the system index and store");
|
||||||
"{-x} PKG...\tRemove a merged package\n"
|
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove a package from the system index and store");
|
||||||
"\n"
|
printf("%-25s %s\n", "{-O --overwrite}", "With -l: replace an installed version (backed up per KEEP_OLD) and existing non-glacier files");
|
||||||
"This program is free software.\n"
|
printf("%-25s %s\n", "{-S --skip-installed}", "Skip packages that are already up to date");
|
||||||
"See the GNU GPL version 3 for details.\n"
|
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
||||||
);
|
printf("\n");
|
||||||
|
printf("This program is free software.\n");
|
||||||
|
printf("See the GNU GPL version 3 for details.\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -219,12 +224,12 @@ lg_gstore_help(int argc, char *argv[])
|
|||||||
(void)argc;
|
(void)argc;
|
||||||
|
|
||||||
printf("%s: Manage Glacier packages indexes/stores\n", argv[0]);
|
printf("%s: Manage Glacier packages indexes/stores\n", argv[0]);
|
||||||
printf("usage: %s [-hv] [-snbx UID] [-V]\n", argv[0]);
|
printf("usage: %s [-hv] [-n [UID]] [-sbx UID] [-V]\n", argv[0]);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
||||||
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
||||||
printf("%-25s %s\n", "{-s --sync} UID", "Sync UID's store with their index");
|
printf("%-25s %s\n", "{-s --sync} UID", "Sync UID's store with their index");
|
||||||
printf("%-25s %s\n", "{-n --new} UID", "Create new store and index for UID");
|
printf("%-25s %s\n", "{-n --new} [UID]", "Create store and index for UID (as root), or for yourself (doas gstore -n)");
|
||||||
printf("%-25s %s\n", "{-b --backup} UID OUTFILE", "Create tar backup of UID's index and store at OUTFILE");
|
printf("%-25s %s\n", "{-b --backup} UID OUTFILE", "Create tar backup of UID's index and store at OUTFILE");
|
||||||
printf("%-25s %s\n", "{-x --delete} UID", "Delete UID's store and index");
|
printf("%-25s %s\n", "{-x --delete} UID", "Delete UID's store and index");
|
||||||
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
||||||
|
|||||||
@@ -15,17 +15,6 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "pkg.h"
|
#include "pkg.h"
|
||||||
|
|
||||||
/* directory definitions */
|
|
||||||
const dir_t gl_root = { "/glacier", 0750 };
|
|
||||||
|
|
||||||
const dir_t gl_usr = { "/glacier/usr", 0700 };
|
|
||||||
const dir_t gl_usr_index = { "/glacier/usr/index", 0700 };
|
|
||||||
const dir_t gl_usr_store = { "/glacier/usr/store", 0700 };
|
|
||||||
|
|
||||||
const dir_t gl_sys = { "/glacier/sys", 0700 };
|
|
||||||
const dir_t gl_sys_index = { "/glacier/sys/index", 0700 };
|
|
||||||
const dir_t gl_sys_store = { "/glacier/sys/store", 0700 };
|
|
||||||
|
|
||||||
/* null dependencies/conflicts for error throwing */
|
/* null dependencies/conflicts for error throwing */
|
||||||
dep_t null_dep = { "null", {-1, -1, -1, NULL} };
|
dep_t null_dep = { "null", {-1, -1, -1, NULL} };
|
||||||
conf_t null_conf = { "null", {-1, -1, -1, NULL} };
|
conf_t null_conf = { "null", {-1, -1, -1, NULL} };
|
||||||
@@ -133,24 +122,64 @@ gl_pkg_t
|
|||||||
return new_pkg;
|
return new_pkg;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* gl_valid_pkg_ident
|
||||||
|
* DESCRIPTION: checks that a package name or repo name is safe to use as a
|
||||||
|
* single path component: non-empty, only [A-Za-z0-9._-], and not
|
||||||
|
* "." or "..". Both come straight out of an untrusted manifest (or
|
||||||
|
* off a command line) and get spliced into store paths, so
|
||||||
|
* anything else could escape ctx->store_path.
|
||||||
|
* PARAMETERS:
|
||||||
|
* ~ const char *s (the identifier to check; NULL is invalid)
|
||||||
|
* RETURNS:
|
||||||
|
* ~ true if safe, false otherwise
|
||||||
|
*/
|
||||||
|
bool
|
||||||
|
gl_valid_pkg_ident(const char *s)
|
||||||
|
{
|
||||||
|
if (!s || s[0] == '\0') { return false; }
|
||||||
|
if (strcmp(s, ".") == 0 || strcmp(s, "..") == 0) { return false; }
|
||||||
|
|
||||||
|
for (const char *p = s; *p; p++) {
|
||||||
|
char c = *p;
|
||||||
|
if (!((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') ||
|
||||||
|
(c >= '0' && c <= '9') || c == '.' || c == '_' || c == '-')) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
int
|
int
|
||||||
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
|
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
|
||||||
{
|
{
|
||||||
config_t cfg;
|
config_t cfg;
|
||||||
config_setting_t *deps, *confs;
|
config_setting_t *deps, *confs;
|
||||||
const char *tmp_str;
|
const char *tmp_str;
|
||||||
|
const char *name = NULL, *repo = NULL;
|
||||||
|
|
||||||
config_init(&cfg);
|
config_init(&cfg);
|
||||||
|
|
||||||
if (!config_read_file(&cfg, filename)) {
|
if (!config_read_file(&cfg, filename)) {
|
||||||
|
config_destroy(&cfg);
|
||||||
return ENOENT;
|
return ENOENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (config_lookup_string(&cfg, "PKG_NAME", &tmp_str)) { pkg->pkg_name = strdup(tmp_str); }
|
/* Validate before anything is strdup'd, so a rejected manifest leaves
|
||||||
|
* pkg untouched and the caller has nothing to free. */
|
||||||
|
config_lookup_string(&cfg, "PKG_NAME", &name);
|
||||||
|
config_lookup_string(&cfg, "PKG_REPO", &repo);
|
||||||
|
if (!gl_valid_pkg_ident(name) || !gl_valid_pkg_ident(repo)) {
|
||||||
|
config_destroy(&cfg);
|
||||||
|
return EINVAL;
|
||||||
|
}
|
||||||
|
|
||||||
|
pkg->pkg_name = strdup(name);
|
||||||
|
pkg->pkg_repo = strdup(repo);
|
||||||
if (config_lookup_string(&cfg, "PKG_DESC", &tmp_str)) { pkg->pkg_desc = strdup(tmp_str); }
|
if (config_lookup_string(&cfg, "PKG_DESC", &tmp_str)) { pkg->pkg_desc = strdup(tmp_str); }
|
||||||
if (config_lookup_string(&cfg, "PKG_MAINT", &tmp_str)) { pkg->pkg_maint = strdup(tmp_str); }
|
if (config_lookup_string(&cfg, "PKG_MAINT", &tmp_str)) { pkg->pkg_maint = strdup(tmp_str); }
|
||||||
if (config_lookup_string(&cfg, "PKG_COPYRIGHT", &tmp_str)) { pkg->pkg_copyright = strdup(tmp_str); }
|
if (config_lookup_string(&cfg, "PKG_COPYRIGHT", &tmp_str)) { pkg->pkg_copyright = strdup(tmp_str); }
|
||||||
if (config_lookup_string(&cfg, "PKG_REPO", &tmp_str)) { pkg->pkg_repo = strdup(tmp_str); }
|
|
||||||
if (config_lookup_string(&cfg, "PKG_VER", &tmp_str)) { pkg->pkg_ver = gl_parse_semver(tmp_str); }
|
if (config_lookup_string(&cfg, "PKG_VER", &tmp_str)) { pkg->pkg_ver = gl_parse_semver(tmp_str); }
|
||||||
|
|
||||||
deps = config_lookup(&cfg, "PKG_DEPS");
|
deps = config_lookup(&cfg, "PKG_DEPS");
|
||||||
@@ -281,34 +310,6 @@ gl_free_pkg(gl_pkg_t *pkg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int
|
|
||||||
gl_prepare_pkgdir(gl_pkg_t *pkg, bool is_syspkg)
|
|
||||||
{
|
|
||||||
|
|
||||||
dir_t pkg_root = { pkg -> pkg_name, 0750 };
|
|
||||||
dir_t pkg_store_final;
|
|
||||||
dir_t pkg_index_final;
|
|
||||||
|
|
||||||
if (!is_syspkg) {
|
|
||||||
dir_t pkg_store_initial = gl_return_dir(&gl_usr_store, &pkg_root, 0750);
|
|
||||||
pkg_store_final = gl_return_dir_and_ver(&pkg_store_initial, pkg, 0750);
|
|
||||||
dir_t pkg_index_initial = gl_return_dir(&gl_usr_index, &pkg_root, 0750);
|
|
||||||
pkg_index_final = gl_return_dir_and_ver(&pkg_index_initial, pkg, 0750);
|
|
||||||
}
|
|
||||||
else if (is_syspkg) {
|
|
||||||
dir_t pkg_store_initial = gl_return_dir(&gl_sys_store, &pkg_root, 0750);
|
|
||||||
pkg_store_final = gl_return_dir_and_ver(&pkg_store_initial, pkg, 0750);
|
|
||||||
dir_t pkg_index_initial = gl_return_dir(&gl_sys_index, &pkg_root, 0750);
|
|
||||||
pkg_index_final = gl_return_dir_and_ver(&pkg_index_initial, pkg, 0750);
|
|
||||||
}
|
|
||||||
|
|
||||||
printf("%s\n%s\n", pkg_index_final.path, pkg_store_final.path);
|
|
||||||
gl_free_dir(&pkg_index_final);
|
|
||||||
gl_free_dir(&pkg_store_final);
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
void
|
void
|
||||||
gl_show_pkg_props(const gl_pkg_t *pkg)
|
gl_show_pkg_props(const gl_pkg_t *pkg)
|
||||||
{
|
{
|
||||||
@@ -346,3 +347,39 @@ gl_show_pkg_props(const gl_pkg_t *pkg)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Read integer `key` from a libconfig file (glacier.conf). Distinguishes
|
||||||
|
* the failures a caller has to report differently:
|
||||||
|
* GL_CONF_OK *out set
|
||||||
|
* GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig
|
||||||
|
* GL_CONF_ERR_MISSING file fine, key absent
|
||||||
|
* GL_CONF_ERR_TYPE key present but not an integer
|
||||||
|
* There are deliberately no defaults here: a setting the caller needs and
|
||||||
|
* the file doesn't have is an error, not a silent fallback (CLAUDE.md bug
|
||||||
|
* 10 is what a plausible default hides).
|
||||||
|
*/
|
||||||
|
gl_conf_status_t
|
||||||
|
gl_conf_get_int(const char *path, const char *key, int *out)
|
||||||
|
{
|
||||||
|
config_t cfg;
|
||||||
|
config_init(&cfg);
|
||||||
|
|
||||||
|
if (!config_read_file(&cfg, path)) {
|
||||||
|
config_destroy(&cfg);
|
||||||
|
return GL_CONF_ERR_READ;
|
||||||
|
}
|
||||||
|
|
||||||
|
config_setting_t *setting = config_lookup(&cfg, key);
|
||||||
|
if (!setting) {
|
||||||
|
config_destroy(&cfg);
|
||||||
|
return GL_CONF_ERR_MISSING;
|
||||||
|
}
|
||||||
|
if (config_setting_type(setting) != CONFIG_TYPE_INT) {
|
||||||
|
config_destroy(&cfg);
|
||||||
|
return GL_CONF_ERR_TYPE;
|
||||||
|
}
|
||||||
|
|
||||||
|
*out = config_setting_get_int(setting);
|
||||||
|
config_destroy(&cfg);
|
||||||
|
return GL_CONF_OK;
|
||||||
|
}
|
||||||
@@ -43,16 +43,6 @@ typedef struct {
|
|||||||
}
|
}
|
||||||
gl_pkg_t;
|
gl_pkg_t;
|
||||||
|
|
||||||
extern const dir_t gl_root;
|
|
||||||
|
|
||||||
extern const dir_t gl_usr;
|
|
||||||
extern const dir_t gl_usr_index;
|
|
||||||
extern const dir_t gl_usr_store;
|
|
||||||
|
|
||||||
extern const dir_t gl_sys;
|
|
||||||
extern const dir_t gl_sys_index;
|
|
||||||
extern const dir_t gl_sys_store;
|
|
||||||
|
|
||||||
extern dep_t null_dep;
|
extern dep_t null_dep;
|
||||||
extern conf_t null_conf;
|
extern conf_t null_conf;
|
||||||
extern gl_pkg_t empty;
|
extern gl_pkg_t empty;
|
||||||
@@ -78,8 +68,19 @@ dir_t gl_return_dir_and_ver(const dir_t *base, gl_pkg_t *pkg, mode_t mode);
|
|||||||
void gl_free_path(char *path);
|
void gl_free_path(char *path);
|
||||||
void gl_free_dir(dir_t *dir);
|
void gl_free_dir(dir_t *dir);
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_CONF_OK = 0,
|
||||||
|
GL_CONF_ERR_READ = 1,
|
||||||
|
GL_CONF_ERR_MISSING = 2,
|
||||||
|
GL_CONF_ERR_TYPE = 3
|
||||||
|
} gl_conf_status_t;
|
||||||
|
|
||||||
|
#define GL_CONF_PATH "/etc/glacier/glacier.conf"
|
||||||
|
|
||||||
|
gl_conf_status_t gl_conf_get_int(const char *path, const char *key, int *out);
|
||||||
|
|
||||||
|
bool gl_valid_pkg_ident(const char *s);
|
||||||
int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg);
|
int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg);
|
||||||
int gl_prepare_pkgdir(gl_pkg_t *pkg, bool is_syspkg);
|
|
||||||
char *join_with_cwd(const char *path);
|
char *join_with_cwd(const char *path);
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
+88
-97
@@ -16,17 +16,28 @@
|
|||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "transaction.h"
|
#include "transaction.h"
|
||||||
|
|
||||||
#define GL_STAGE_BASE "/glacier/usr/stage"
|
/* The one directory a scope owns; everything below derives from it.
|
||||||
#define GL_LOCK_BASE "/glacier/usr/lock"
|
* See the layout comment in common.h. */
|
||||||
#define GL_OLD_BASE "/glacier/usr/old"
|
static void
|
||||||
|
scope_base(gl_ctx_scope_t scope, uid_t uid, char *out)
|
||||||
|
{
|
||||||
|
switch (scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
gl_usr_base(uid, out, PATH_MAX);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
snprintf(out, PATH_MAX, "%s", GL_SYS_ROOT);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#define GL_USR_INDEX "/glacier/usr/index"
|
static void
|
||||||
#define GL_USR_STORE "/glacier/usr/store"
|
scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *name, char *out)
|
||||||
#define GL_USR_LINKS "/glacier/usr/links"
|
{
|
||||||
|
char base[PATH_MAX];
|
||||||
#define GL_SYS_INDEX "/glacier/sys/index"
|
scope_base(scope, uid, base);
|
||||||
#define GL_SYS_STORE "/glacier/sys/store"
|
snprintf(out, PATH_MAX, "%s/%s", base, name);
|
||||||
#define GL_SYS_LINKS "/glacier/sys/links"
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
fsync_dir(const char *path)
|
fsync_dir(const char *path)
|
||||||
@@ -59,25 +70,16 @@ static void
|
|||||||
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
|
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
|
||||||
char *store_out, char *links_out)
|
char *store_out, char *links_out)
|
||||||
{
|
{
|
||||||
switch (scope) {
|
scope_subdir(scope, uid, "index", index_out);
|
||||||
case GL_SCOPE_USR:
|
scope_subdir(scope, uid, "store", store_out);
|
||||||
snprintf(index_out, PATH_MAX, "/glacier/usr/index/%u", uid);
|
/* For system scope this is the DEFAULT links location — for
|
||||||
snprintf(store_out, PATH_MAX, "/glacier/usr/store/%u", uid);
|
* packages the operator explicitly installs under system scope that
|
||||||
snprintf(links_out, PATH_MAX, "/glacier/usr/links/%u", uid);
|
* aren't part of the base system. Base-repo packages get routed to
|
||||||
break;
|
* /usr instead, but that's a per-package decision made in
|
||||||
case GL_SCOPE_SYS:
|
* gl_link_pkg/gl_relink_store (which know a package's repo), not
|
||||||
strncpy(index_out, "/glacier/sys/index", PATH_MAX - 1);
|
* something the context can decide up front. See GL_BASE_REPO in
|
||||||
strncpy(store_out, "/glacier/sys/store", PATH_MAX - 1);
|
* istoreutils.c. */
|
||||||
/* This is the DEFAULT system-scope links location — for
|
scope_subdir(scope, uid, "links", links_out);
|
||||||
* packages the operator explicitly installs under system
|
|
||||||
* scope that aren't part of the base system. Base-repo
|
|
||||||
* packages get routed to /usr instead, but that's a
|
|
||||||
* per-package decision made in gl_link_pkg/gl_relink_store
|
|
||||||
* (which know a package's repo), not something the context
|
|
||||||
* can decide up front. See GL_BASE_REPO in istoreutils.c. */
|
|
||||||
strncpy(links_out, "/glacier/sys/links", PATH_MAX - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@@ -100,7 +102,13 @@ hardlink_tree(const char *src, const char *dest)
|
|||||||
|
|
||||||
struct dirent *ent;
|
struct dirent *ent;
|
||||||
while ((ent = readdir(dir))) {
|
while ((ent = readdir(dir))) {
|
||||||
if (ent->d_name[0] == '.') { continue; }
|
/* Only . and .. — dotfiles are real content here: a package's
|
||||||
|
* own dotfiles, and the store's .displaced originals (see
|
||||||
|
* gl_link_pkg), both of which would otherwise be dropped from
|
||||||
|
* the stage and lost on commit. */
|
||||||
|
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
char s[PATH_MAX], d[PATH_MAX];
|
char s[PATH_MAX], d[PATH_MAX];
|
||||||
snprintf(s, sizeof(s), "%s/%s", src, ent->d_name);
|
snprintf(s, sizeof(s), "%s/%s", src, ent->d_name);
|
||||||
@@ -149,6 +157,7 @@ gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
|||||||
ctx->mode = GL_CTX_LIVE;
|
ctx->mode = GL_CTX_LIVE;
|
||||||
ctx->scope = scope;
|
ctx->scope = scope;
|
||||||
ctx->lock_fd = -1;
|
ctx->lock_fd = -1;
|
||||||
|
ctx->overwrite = 0;
|
||||||
|
|
||||||
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
|
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
|
||||||
derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
|
derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
|
||||||
@@ -168,40 +177,55 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
|||||||
ctx->mode = GL_CTX_STAGE;
|
ctx->mode = GL_CTX_STAGE;
|
||||||
ctx->scope = scope;
|
ctx->scope = scope;
|
||||||
ctx->lock_fd = -1;
|
ctx->lock_fd = -1;
|
||||||
|
ctx->overwrite = 0;
|
||||||
|
|
||||||
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
|
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
|
||||||
|
|
||||||
char stage_base[PATH_MAX - 7];
|
if (scope == GL_SCOPE_USR) {
|
||||||
|
switch (gl_usr_base_check(uid)) {
|
||||||
switch (scope) {
|
case GL_USR_BASE_OK:
|
||||||
case GL_SCOPE_USR:
|
|
||||||
snprintf(stage_base, sizeof(stage_base), "/glacier/usr/stage/%u", uid);
|
|
||||||
break;
|
|
||||||
case GL_SCOPE_SYS:
|
|
||||||
strncpy(stage_base, "/glacier/sys/stage", sizeof(stage_base));
|
|
||||||
break;
|
break;
|
||||||
|
case GL_USR_BASE_MISSING:
|
||||||
|
lg_printf(2, "No Glacier directory for UID %u. Run doas gstore -n first.", uid);
|
||||||
|
return GL_TXN_ERR_PERM;
|
||||||
|
case GL_USR_BASE_UNSAFE_ROOT:
|
||||||
|
lg_printf(2, "Refusing to run: %s is not root-owned, or is group/world-writable.",
|
||||||
|
GL_USR_ROOT);
|
||||||
|
return GL_TXN_ERR_PERM;
|
||||||
|
case GL_USR_BASE_UNSAFE:
|
||||||
|
lg_printf(2, "Refusing to run: %s/%u is not a directory owned by UID %u with mode 0700.",
|
||||||
|
GL_USR_ROOT, uid, uid);
|
||||||
|
return GL_TXN_ERR_PERM;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
char stage_base[PATH_MAX - 7];
|
||||||
|
scope_subdir(scope, uid, "stage", stage_base);
|
||||||
|
|
||||||
snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base);
|
snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base);
|
||||||
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
|
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
|
||||||
snprintf(ctx->links_path, PATH_MAX, "%s/links", stage_base);
|
snprintf(ctx->links_path, PATH_MAX, "%s/links", stage_base);
|
||||||
|
|
||||||
struct stat st;
|
/* Lock BEFORE clearing a leftover stage: the other order let a
|
||||||
if (stat(stage_base, &st) == 0) {
|
* second invocation wipe a running transaction's stage and only
|
||||||
if (gl_rmdir_recursive(stage_base) != 0) {
|
* then discover it couldn't get the lock. */
|
||||||
return GL_TXN_ERR_MKDIR;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl_mkdirp(GL_LOCK_BASE, 0700) != 0 && errno != EEXIST) {
|
|
||||||
return GL_TXN_ERR_MKDIR;
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_txn_status_t lock_status = gl_begin_transaction(ctx);
|
gl_txn_status_t lock_status = gl_begin_transaction(ctx);
|
||||||
if (lock_status != GL_TXN_OK) {
|
if (lock_status != GL_TXN_OK) {
|
||||||
return lock_status;
|
return lock_status;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* EBUSY: stage_base is itself a mount point. Its contents are gone
|
||||||
|
* and the directory stays, which is all a fresh stage needs (the
|
||||||
|
* mkdirps below tolerate it existing). Commit then reports
|
||||||
|
* GL_TXN_ERR_XDEV, as it should for a stage on another filesystem. */
|
||||||
|
struct stat st;
|
||||||
|
if (stat(stage_base, &st) == 0) {
|
||||||
|
if (gl_rmdir_recursive(stage_base) != 0 && errno != EBUSY) {
|
||||||
|
gl_abort_transaction(ctx);
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (gl_mkdirp(ctx->index_path, 0700) != 0) {
|
if (gl_mkdirp(ctx->index_path, 0700) != 0) {
|
||||||
gl_abort_transaction(ctx);
|
gl_abort_transaction(ctx);
|
||||||
return GL_TXN_ERR_MKDIR;
|
return GL_TXN_ERR_MKDIR;
|
||||||
@@ -239,8 +263,13 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
|||||||
* area are on different filesystems. */
|
* area are on different filesystems. */
|
||||||
if (hardlink_tree(live_index, ctx->index_path) != 0 ||
|
if (hardlink_tree(live_index, ctx->index_path) != 0 ||
|
||||||
hardlink_tree(live_store, ctx->store_path) != 0) {
|
hardlink_tree(live_store, ctx->store_path) != 0) {
|
||||||
|
/* link() can't cross filesystems: a stage on a different
|
||||||
|
* fs than live is caught here, before anything is staged,
|
||||||
|
* rather than at commit's rename. Same condition, same
|
||||||
|
* status as commit's check. */
|
||||||
|
int seed_errno = errno;
|
||||||
gl_abort_transaction(ctx);
|
gl_abort_transaction(ctx);
|
||||||
return GL_TXN_ERR_MKDIR;
|
return seed_errno == EXDEV ? GL_TXN_ERR_XDEV : GL_TXN_ERR_MKDIR;
|
||||||
}
|
}
|
||||||
|
|
||||||
return GL_TXN_OK;
|
return GL_TXN_OK;
|
||||||
@@ -254,15 +283,7 @@ gl_begin_transaction(gl_context_t *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
char lock_path[PATH_MAX];
|
char lock_path[PATH_MAX];
|
||||||
switch (ctx->scope) {
|
scope_subdir(ctx->scope, ctx->uid, "lock", lock_path);
|
||||||
case GL_SCOPE_USR:
|
|
||||||
snprintf(lock_path, PATH_MAX, "%s/%u.lock", GL_LOCK_BASE,
|
|
||||||
ctx->uid);
|
|
||||||
break;
|
|
||||||
case GL_SCOPE_SYS:
|
|
||||||
snprintf(lock_path, PATH_MAX, "%s/sys.lock", GL_LOCK_BASE);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
|
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
@@ -289,15 +310,7 @@ gl_commit_transaction(gl_context_t *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
char stage_base[PATH_MAX];
|
char stage_base[PATH_MAX];
|
||||||
switch (ctx->scope) {
|
scope_subdir(ctx->scope, ctx->uid, "stage", stage_base);
|
||||||
case GL_SCOPE_USR:
|
|
||||||
snprintf(stage_base, PATH_MAX, "/glacier/usr/stage/%u",
|
|
||||||
ctx->uid);
|
|
||||||
break;
|
|
||||||
case GL_SCOPE_SYS:
|
|
||||||
strncpy(stage_base, "/glacier/sys/stage", PATH_MAX - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
int same = check_same_filesystem(ctx->root_path, stage_base);
|
int same = check_same_filesystem(ctx->root_path, stage_base);
|
||||||
if (same < 0) {
|
if (same < 0) {
|
||||||
@@ -314,14 +327,7 @@ gl_commit_transaction(gl_context_t *ctx)
|
|||||||
|
|
||||||
/* Derive old/backup paths */
|
/* Derive old/backup paths */
|
||||||
char old_base[PATH_MAX];
|
char old_base[PATH_MAX];
|
||||||
switch (ctx->scope) {
|
scope_subdir(ctx->scope, ctx->uid, "old", old_base);
|
||||||
case GL_SCOPE_USR:
|
|
||||||
snprintf(old_base, PATH_MAX, "/glacier/usr/old/%u", ctx->uid);
|
|
||||||
break;
|
|
||||||
case GL_SCOPE_SYS:
|
|
||||||
strncpy(old_base, "/glacier/sys/old", PATH_MAX - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
|
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
|
||||||
snprintf(old_index, PATH_MAX, "%s/index", old_base);
|
snprintf(old_index, PATH_MAX, "%s/index", old_base);
|
||||||
@@ -355,19 +361,12 @@ gl_commit_transaction(gl_context_t *ctx)
|
|||||||
return GL_TXN_ERR_RENAME;
|
return GL_TXN_ERR_RENAME;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* fsync the parent directories, not the uid subdirs */
|
/* fsync the directory the renames happened in */
|
||||||
switch (ctx->scope) {
|
char base[PATH_MAX];
|
||||||
case GL_SCOPE_USR:
|
scope_base(ctx->scope, ctx->uid, base);
|
||||||
fsync_dir("/glacier/usr/index");
|
fsync_dir(base);
|
||||||
fsync_dir("/glacier/usr/store");
|
if (ctx->scope == GL_SCOPE_SYS) {
|
||||||
fsync_dir("/glacier/usr/links");
|
|
||||||
break;
|
|
||||||
case GL_SCOPE_SYS:
|
|
||||||
fsync_dir("/glacier/sys/index");
|
|
||||||
fsync_dir("/glacier/sys/store");
|
|
||||||
fsync_dir("/glacier/sys/links");
|
|
||||||
fsync_dir("/usr"); /* base-repo packages land here instead */
|
fsync_dir("/usr"); /* base-repo packages land here instead */
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_context_t live_ctx;
|
gl_context_t live_ctx;
|
||||||
@@ -397,15 +396,7 @@ gl_abort_transaction(gl_context_t *ctx)
|
|||||||
|
|
||||||
/* Remove stage area — live is untouched */
|
/* Remove stage area — live is untouched */
|
||||||
char stage_uid[PATH_MAX];
|
char stage_uid[PATH_MAX];
|
||||||
switch (ctx->scope) {
|
scope_subdir(ctx->scope, ctx->uid, "stage", stage_uid);
|
||||||
case GL_SCOPE_USR:
|
|
||||||
snprintf(stage_uid, PATH_MAX, "/glacier/usr/stage/%u",
|
|
||||||
ctx->uid);
|
|
||||||
break;
|
|
||||||
case GL_SCOPE_SYS:
|
|
||||||
strncpy(stage_uid, "/glacier/sys/stage", PATH_MAX - 1);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
|
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
|
||||||
|
|
||||||
|
|||||||
@@ -31,6 +31,12 @@ typedef struct {
|
|||||||
gl_ctx_scope_t scope;
|
gl_ctx_scope_t scope;
|
||||||
|
|
||||||
int lock_fd;
|
int lock_fd;
|
||||||
|
|
||||||
|
/* Set by the caller after gl_init_*_context (which zero it).
|
||||||
|
* Packages installed while this is non-zero may displace
|
||||||
|
* existing non-glacier files at their link destinations; see
|
||||||
|
* gl_link_pkg. */
|
||||||
|
int overwrite;
|
||||||
} gl_context_t;
|
} gl_context_t;
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
|
|||||||
Reference in new issue
Block a user