This commit is contained in:
lw-everestlinux committed 2026-10-05 14:47:58 -04:00
1 parent c432274936
commit ee4bcc3b77
27 files changed
+2711 -581

No files matched your search

+16
View File
@@ -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
+413 -226
View File
@@ -1,261 +1,448 @@
# CLAUDE.md
Context for Claude Code sessions on this repo (libglacier-ng, the package
manager for the Everest/Glacier Linux distribution). This file exists
because there's no built-in way to hand off a claude.ai planning
conversation into Claude Code — this is that handoff, written down.
Context for Claude Code sessions on libglacier-ng: the package manager
for Everest Linux. Kept as a handoff, so a fresh session starts with full
continuity instead of re-deriving everything. Last full rewrite:
2026-10-04, at the end of a long session. **Update it as you go.**
For the symlink-routing / package-scope design specifically, see
`PACKAGE_SCOPES.txt` at the repo root — that doc is the source of truth
for that topic and isn't duplicated here.
## Start 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
A from-scratch Linux distribution (Everest, built on the Glacier
package-management model) with its own package manager
(`libglacier-ng` + `gpkg`/`gstore` CLI tools) and its own build
tooling (`gbuild`/`gworld`) for bootstrapping a set of packages up to
a minimal bootable system.
A from-scratch, security-focused Linux distribution (Everest) with its own
package manager (libglacier-ng + `gpkg`/`gstore`/`syspkg`) and build tooling
(`gbuild`/`gworld`). It's a C library, LuaJIT+FFI CLI tools and bash build
scripts. The user writes it; Claude implements and designs alongside them.
## 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
**`gl_context_t`** (transaction.h) is the central abstraction: `uid`,
`root_path`, `index_path`, `store_path`, `links_path`, `mode`
(`GL_CTX_LIVE` / `GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR` /
`GL_SCOPE_SYS`), `lock_fd`.
**`gl_context_t`** (transaction.h) is the central abstraction. Fields:
`uid`, `root_path`, `index_path`, `store_path`, `links_path`, `mode`
(`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR`/`GL_SCOPE_SYS`),
`lock_fd`, and `overwrite` (last field, zeroed by the init functions).
**Transaction lifecycle**: `gl_init_live_context` / `gl_init_stage_context`
→ `gl_commit_transaction` or `gl_abort_transaction`. Staged installs
hardlink-seed `index`/`store` from live at stage-init time (copy-on-write,
so removing a live package inside a transaction doesn't require special
casing). The `links` tree is deliberately **not** seeded — see below.
**Layout.** Each scope owns ONE base directory holding
`{index,store,links,stage,old,lock,backup}`. Every create, rename and lock
a transaction does stays inside it.
**Commit** does atomic rename-based promotion (stage → live, with the
previous live moved to `old/` as a rollback point), fsyncs the parent
dirs, then calls `gl_relink_store` to regenerate symlinks and
`gl_rebuild_index` to regenerate the index — both from a fresh
`GL_CTX_LIVE` context, not the just-committed stage context.
| Scope | Base dir | Ownership |
|---|---|---|
| `GL_SCOPE_USR` | `/glacier/usr/<uid>/` | uid, mode 0700, inside a root-owned 0755 `/glacier/usr` |
| `GL_SCOPE_SYS` | `/glacier/sys/` | root |
**Scopes**:
- `GL_SCOPE_USR`: per-uid, `/glacier/usr/{index,store,links}/<uid>`.
- `GL_SCOPE_SYS`: system-wide, `/glacier/sys/{index,store}`. Symlink
destination depends on the package's **repo** — see `PACKAGE_SCOPES.txt`.
Short version: `repo == "base"` → `/usr`; anything else →
`/glacier/sys/links`. This is decided per-package (in `gl_link_pkg` /
`gl_relink_store`), not baked into the context.
- The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment
are in `common.h`.
- `gl_init_stage_context` refuses a user base that isn't a real directory
(lstat) owned by its uid with no group/other bits, or whose
`/glacier/usr` is loosened. `gl_usr_base_status` exports that check, so
gpkg can say why before any prompt.
**Repos**: `base` (minimal working system, boot-critical by definition —
this is a repo-level classification, not a per-package flag), `extra`
(important but not required), `community` (everything else). `extra`
and `community` currently behave identically — see open items.
**Transaction lifecycle.** `gl_init_live_context` / `gl_init_stage_context`
→ `gl_commit_transaction` or `gl_abort_transaction`.
- Stage init takes the lock FIRST, then clears any leftover stage.
- 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** —
for system-scope `base` packages it's literally `/usr`, which holds
plenty of content glacier has no business touching. Everything that
reads or writes a links destination has to treat it as "possibly
shared":
| Scope | Repo | Destination |
|---|---|---|
| USR | any | `<base>/links` |
| SYS | `base` | `/usr` |
| SYS | anything else | `/glacier/sys/links` |
- `is_glacier_symlink(path, store_prefix)` (istoreutils.c) is the one
source of truth for "did glacier create this." True only if `path`
is a symlink whose target lives under `store_prefix`. Never touch a
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.
- A package's leading `usr/` is stripped.
- In system scope, a package's top-level `etc/` is never linked: it's
COPIED into `/etc` (see `/etc` config handling below).
## 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
copy-paste bug wrote into the wrong variable. Fixed by moving off
`O_EXCL`-based locking entirely: `gl_begin_transaction` now just
`open(O_CREAT)` + `flock()`, and lock files are never `unlink()`'d
(the kernel releases `flock()` automatically on process death, even
`SIGKILL`; explicit unlinking has its own TOCTOU race with a second
process creating a new inode at the same path).
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.
| File | Purpose |
|---|---|
| `<ver>/.glacier-manifest` | the package's manifest, saved at install, used by `gl_backup_pkg` |
| `<ver>/.glacier-fresh` | one-shot "installed in this transaction"; link conflicts are only reported when present |
| `<ver>/.glacier-overwrite` | one-shot `-O` permission for this package's next link pass |
| `<store>/.displaced/<abs path>` | originals displaced by `-O`, restored when the link that replaced them is pruned |
| `<store>/.etc-state/<path>` | the last version of each `/etc` file a package offered |
## 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
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.
## Safety invariants: read before touching links/symlink code
## Build system
`/usr` is NOT glacier-exclusive territory. Treat every link destination as
possibly shared:
`Makefile` builds separate `.so`s per module
(`libglacier_log.so`, `libglacier_pkg.so`, `libglacier_istoreutils.so`,
`libglacier_transaction.so`, `libglacier_dag.so`) — **this split is
deliberate Unix-philosophy design**, not an accident: a third-party
glacier-compatible program should be able to link only
`libglacier_log.so` without pulling in everything else. `gpkg`/`gstore`
load the specific per-module `.so`s they need directly, matching this.
- **Ownership test.** `is_glacier_symlink(path, store_prefix)` is the only
test for "did glacier create this". Never touch anything it returns
false for, except through `-O`'s displace/restore.
- **Overwriting.** `gl_link_pkg` won't overwrite a non-glacier path without
the `-O` marker. It never descends through *another package's* directory
symlink into that package's store directory.
- **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**
`.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.
## Feature reference (current behaviour)
`config.mk` holds `CFLAGS`, including `-Wno-error=format-truncation`
(see bug #7 above for why that specific warning is suppressed and
others aren't).
**Integrity verification** (`gpkg_common.lua`: `verify_and_copy`,
`verify_all`):
- 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.
- `lib/glacier_cdef.lua`: single shared source of truth for every
`ffi.cdef()` declaration, used by `gpkg`, `gstore`, and the test
suite. This used to be duplicated three ways, which is exactly what
let a stale signature drift silently into `gpkg` at one point — if
you're editing a C function signature, this file needs the matching
update, and it's the *only* place that needs it now.
- `tests/*.lua`: LuaJIT FFI test suite exercising real filesystem/process
behavior (`fork`+`SIGKILL` for lock crash-recovery, real `mount()`
for the `EXDEV` check, etc.) — not mocked. Run via:
**Version replacement** (`-lO`, `M.install` in `gpkg_common.lua`):
1. Installed entries are read from the live index's `current` file
(`repo::name::version` lines).
2. A same name+repo with a different version is a replacement. Without
`-O` it's refused; with `-O`, `keep_old()` reads `KEEP_OLD`, and the
summary shows `old -> new`.
3. In the transaction: `gl_backup_pkg(live, repo, name, X.gpkg.part)`, then
`gl_remove_pkg` from the stage, then install.
4. After the commit: rename `.part` → `<scope>/backup/<repo>/<name>/<name>-<ver>.gpkg`,
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
cd tests
make -C .. libglacier-ng # builds the test-only combined .so
GLACIER_TEST_CONFIRM=yes LIBGLACIER_SO=$(pwd)/../build/lib/shared/libglacier-ng.so luajit run_all.lua
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
-v ~/Projects/libglacier-ng:/libsrc:ro alpine:latest sh -c '
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
exercise the mount path; it skips cleanly (not a failure) without it.
GNU `tar` is required: the hostile-archive fixtures use `tar --append`.
## Build automation (bootstrap toolchain)
**Shell suites** (`glacier-ng/tests/*.sh`):
- `gbuild` (bash): single-package builder. Autodetects build system
(cmake/meson/cargo/go/autotools/make), supports cross-compilation
(`-t`/`-x`, now also `-P` for profiles).
- `gworld` (Lua): batch orchestrator. Reads `recipes/*.recipe`,
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.
| Suite | Image |
|---|---|
| `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` |
| `verify_alpine.sh`, `user_layout_alpine.sh`, `upgrade_alpine.sh` | `glacier-alpine_root` |
## 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`
references an undefined `uid` variable (should be `uidn`) — will
nil-concatenation-error the moment it's actually implemented.
- `gpkg -x`'s confirmation summary shows placeholder `unknown::pkg
0.0.0 (unknown)` since it has no local manifest to read version/repo
from for a bare package name. Would need `resolve_repo` called
before building the summary, not just before the removal itself.
- `gpkg -x` doesn't print a completion message on success (`-l` does:
"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.
**Interactive images:** `./run_docker.sh Dockerfile.{alpine,alpine_root,arch,debian}`.
`run_docker.sh` passes the `libglacier` build context. `alpine`, `arch`
and `debian` default to user `glacier` (uid 1000, in wheel); `alpine_root`
defaults to root. All of them have:
- root-owned `/app`;
- no `/glacier` until `gstore -n` / `syspkg -n`;
- the doas rule plus `permit nopass glacier`;
- `glacier.conf` installed.
## 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
inconsistent about it in places (some blocks use spaces). ALWAYS USE TABS WHEN POSSIBLE.
- `PATH_MAX`-sized stack buffers + `snprintf` is the standard pattern
for path construction throughout; prefer it over `strncpy` for
anything copying into a fixed buffer (see bug #7).
- Don't add convenience wrappers that silently touch a broader
filesystem scope than what's asked — this codebase has already been
bitten twice by exactly that class of mistake (`gl_relink_store`'s
wipe, `gl_link_pkg`'s blind `unlink`), both only becoming dangerous
once a path assumption (`links_path` is glacier-exclusive) quietly
stopped holding.
## Bug history: so these don't get reintroduced
All fixed in the current tree.
1. `gl_abort_transaction` never released the lock (wrong variable). Now
`open(O_CREAT)` + `flock()`, and lock files are never unlinked.
2. No `EXDEV` check before renaming. Commit `stat()`s both sides and
returns `GL_TXN_ERR_XDEV`.
3. Segfault on install: the `transaction` target didn't link
`istoreutils`/`log`.
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.
+299
View File
@@ -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).
+280
View File
@@ -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
View File
@@ -15,20 +15,25 @@ up to and including destroying a live /usr.
+------------------------------------+
USR (GL_SCOPE_USR)
Per-uid packages. Index/store/links all live under a specific
uid's tree:
/glacier/usr/index/<uid>
/glacier/usr/store/<uid>
/glacier/usr/links/<uid>
Symlinks always land under /glacier/usr/links/<uid>/{bin,...}.
Per-uid packages. Everything a uid's transactions touch lives
under ONE directory that uid owns:
/glacier/usr/<uid>/{index,store,links,stage,old,lock}
/glacier/usr itself is root-owned (0755) and each /glacier/usr/<uid>
is owned by that uid (0700), so every create/rename/lock a
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
under a per-user scope is ever considered part of the base system,
so nothing here ever touches /usr.
SYS (GL_SCOPE_SYS)
System-wide packages, not owned by any particular uid:
/glacier/sys/index
/glacier/sys/store
/glacier/sys/{index,store,links,stage,old,lock}
Symlink destination depends on the package's repo (see section 3).
Requires root. gpkg enforces this via geteuid() before it will
even attempt a system-scope operation.
@@ -69,9 +74,11 @@ decides where that package's activation symlinks physically land.
| 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" -> /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
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
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
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.
+208
View File
@@ -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.
+8
View File
@@ -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
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 |
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
View File
@@ -20,6 +20,64 @@ typedef struct
}
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
gl_mkdirp(const char *path, mode_t mode)
{
@@ -52,7 +110,13 @@ gl_rmdir_recursive(const char *path)
struct dirent *ent;
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];
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
+1189 -180
View File
File diff suppressed because it is too large. Load diff
+4
View File
@@ -73,9 +73,13 @@ struct gindex {
bool gl_usr_istore_exists(index_store_t index_or_store, int uid);
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_sys(bool isVerbose);
int gl_create_fs_tree(const char *root, bool isVerbose);
int gl_delete_user(uid_t uid, bool isVerbose);
int gl_parse_index(struct gindex *idx, FILE *f);
+21 -16
View File
@@ -181,13 +181,16 @@ lg_gpkg_help(int argc, char *argv[])
(void)argc;
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("%-25s %s\n", "{-h --help}", "Show this message");
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", "{-u --update} PKG...", "Upgrade a merged package");
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove a package from the index and store");
printf("%-25s %s\n", "{-l --local} FILE...", "Install local package files");
printf("%-25s %s\n", "{-f --merge} PKG...", "Install packages from a remote repository (not yet implemented)");
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("\n");
printf("This program is free software.\n");
@@ -200,17 +203,19 @@ lg_syspkg_help(int argc, char *argv[])
(void)argc;
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("{-h}\tShow this message\n"
"{-v}\tShow the current version\n"
"{-f} PKG...\tMerge a package into the user index\n"
"{-u} PKG...\tUpgrade a merged package\n"
"{-x} PKG...\tRemove a merged package\n"
"\n"
"This program is free software.\n"
"See the GNU GPL version 3 for details.\n"
);
printf("%-25s %s\n", "{-h --help}", "Show this message");
printf("%-25s %s\n", "{-v --version}", "Show the current version");
printf("%-25s %s\n", "{-n --new}", "Create the system index and store");
printf("%-25s %s\n", "{-l --local} PKG...", "Install a local package into the system index and store");
printf("%-25s %s\n", "{-x --remove} PKG...", "Remove a package from the system index and store");
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("\n");
printf("This program is free software.\n");
printf("See the GNU GPL version 3 for details.\n");
}
void
@@ -219,12 +224,12 @@ lg_gstore_help(int argc, char *argv[])
(void)argc;
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("%-25s %s\n", "{-h --help}", "Show this message");
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", "{-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", "{-x --delete} UID", "Delete UID's store and index");
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
+78 -41
View File
@@ -15,17 +15,6 @@
#include "common.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 */
dep_t null_dep = { "null", {-1, -1, -1, NULL} };
conf_t null_conf = { "null", {-1, -1, -1, NULL} };
@@ -133,24 +122,64 @@ gl_pkg_t
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
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
{
config_t cfg;
config_setting_t *deps, *confs;
const char *tmp_str;
const char *name = NULL, *repo = NULL;
config_init(&cfg);
if (!config_read_file(&cfg, filename)) {
config_destroy(&cfg);
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_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_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); }
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
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;
}
+12 -11
View File
@@ -43,16 +43,6 @@ typedef struct {
}
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 conf_t null_conf;
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_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_prepare_pkgdir(gl_pkg_t *pkg, bool is_syspkg);
char *join_with_cwd(const char *path);
#endif
+88 -97
View File
@@ -16,17 +16,28 @@
#include "log.h"
#include "transaction.h"
#define GL_STAGE_BASE "/glacier/usr/stage"
#define GL_LOCK_BASE "/glacier/usr/lock"
#define GL_OLD_BASE "/glacier/usr/old"
/* The one directory a scope owns; everything below derives from it.
* See the layout comment in common.h. */
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"
#define GL_USR_STORE "/glacier/usr/store"
#define GL_USR_LINKS "/glacier/usr/links"
#define GL_SYS_INDEX "/glacier/sys/index"
#define GL_SYS_STORE "/glacier/sys/store"
#define GL_SYS_LINKS "/glacier/sys/links"
static void
scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *name, char *out)
{
char base[PATH_MAX];
scope_base(scope, uid, base);
snprintf(out, PATH_MAX, "%s/%s", base, name);
}
static int
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,
char *store_out, char *links_out)
{
switch (scope) {
case GL_SCOPE_USR:
snprintf(index_out, PATH_MAX, "/glacier/usr/index/%u", uid);
snprintf(store_out, PATH_MAX, "/glacier/usr/store/%u", uid);
snprintf(links_out, PATH_MAX, "/glacier/usr/links/%u", uid);
break;
case GL_SCOPE_SYS:
strncpy(index_out, "/glacier/sys/index", PATH_MAX - 1);
strncpy(store_out, "/glacier/sys/store", PATH_MAX - 1);
/* This is the DEFAULT system-scope links location — for
* 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;
}
scope_subdir(scope, uid, "index", index_out);
scope_subdir(scope, uid, "store", store_out);
/* For system scope this is the DEFAULT links location — for
* 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. */
scope_subdir(scope, uid, "links", links_out);
}
static int
@@ -100,7 +102,13 @@ hardlink_tree(const char *src, const char *dest)
struct dirent *ent;
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];
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->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
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->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
char stage_base[PATH_MAX - 7];
switch (scope) {
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));
if (scope == GL_SCOPE_USR) {
switch (gl_usr_base_check(uid)) {
case GL_USR_BASE_OK:
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->store_path, PATH_MAX, "%s/store", stage_base);
snprintf(ctx->links_path, PATH_MAX, "%s/links", stage_base);
struct stat st;
if (stat(stage_base, &st) == 0) {
if (gl_rmdir_recursive(stage_base) != 0) {
return GL_TXN_ERR_MKDIR;
}
}
if (gl_mkdirp(GL_LOCK_BASE, 0700) != 0 && errno != EEXIST) {
return GL_TXN_ERR_MKDIR;
}
/* Lock BEFORE clearing a leftover stage: the other order let a
* second invocation wipe a running transaction's stage and only
* then discover it couldn't get the lock. */
gl_txn_status_t lock_status = gl_begin_transaction(ctx);
if (lock_status != GL_TXN_OK) {
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) {
gl_abort_transaction(ctx);
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. */
if (hardlink_tree(live_index, ctx->index_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);
return GL_TXN_ERR_MKDIR;
return seed_errno == EXDEV ? GL_TXN_ERR_XDEV : GL_TXN_ERR_MKDIR;
}
return GL_TXN_OK;
@@ -254,15 +283,7 @@ gl_begin_transaction(gl_context_t *ctx)
}
char lock_path[PATH_MAX];
switch (ctx->scope) {
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;
}
scope_subdir(ctx->scope, ctx->uid, "lock", lock_path);
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
if (fd < 0) {
@@ -289,15 +310,7 @@ gl_commit_transaction(gl_context_t *ctx)
}
char stage_base[PATH_MAX];
switch (ctx->scope) {
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;
}
scope_subdir(ctx->scope, ctx->uid, "stage", stage_base);
int same = check_same_filesystem(ctx->root_path, stage_base);
if (same < 0) {
@@ -314,14 +327,7 @@ gl_commit_transaction(gl_context_t *ctx)
/* Derive old/backup paths */
char old_base[PATH_MAX];
switch (ctx->scope) {
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;
}
scope_subdir(ctx->scope, ctx->uid, "old", old_base);
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
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;
}
/* fsync the parent directories, not the uid subdirs */
switch (ctx->scope) {
case GL_SCOPE_USR:
fsync_dir("/glacier/usr/index");
fsync_dir("/glacier/usr/store");
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 the directory the renames happened in */
char base[PATH_MAX];
scope_base(ctx->scope, ctx->uid, base);
fsync_dir(base);
if (ctx->scope == GL_SCOPE_SYS) {
fsync_dir("/usr"); /* base-repo packages land here instead */
break;
}
gl_context_t live_ctx;
@@ -397,15 +396,7 @@ gl_abort_transaction(gl_context_t *ctx)
/* Remove stage area — live is untouched */
char stage_uid[PATH_MAX];
switch (ctx->scope) {
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;
}
scope_subdir(ctx->scope, ctx->uid, "stage", stage_uid);
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
+6
View File
@@ -31,6 +31,12 @@ typedef struct {
gl_ctx_scope_t scope;
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;
typedef enum {