This commit is contained in:
lw-everestlinux committed 2026-10-14 00:50:56 -04:00
1 parent ee4bcc3b77
commit ae54f74070
53 files changed
+8916 -1424

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# Build outputs (make; make libglacier-ng)
/build/
+642 -43
View File
@@ -3,7 +3,8 @@
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.**
2026-10-04; brought fully up to date at the end of the 2026-10-07
session. **Update it as you go.**
## Start here
@@ -15,15 +16,231 @@ continuity instead of re-deriving everything. Last full rewrite:
- `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).
- `IMAGE_DESIGN.txt`: system images (bootstrap OUTPUT). Sections 3
(sysroot) and 4 (`syspkg --root`) are IMPLEMENTED (milestones 1-2);
the rest is still a proposal. Open decisions are in section 9.
- `GLACIER_BOOTSTRAP.txt`: `glacier-bootstrap`. Local mode is
IMPLEMENTED (ROADMAP 1.3; section 10 says how it differs from the
plan); remote mode and `--pin` are still a plan. Open: 9a, 9f.
- `REPO_DESIGN.txt`: remote repos + signing (ROADMAP 3.1). Sections
1 and 10 = the user's decisions (TLS library still open); steps
1-4 of section 9 DONE (see "3.1 status"); sections 7, 5 and 6
describe steps 2, 3 and 4 as implemented.
- `SECURITY_REVIEW_2026-10-06.md`: the full audit (findings, fixes,
what was removed, what's still open).
- `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.
Nothing from the 2026-10-04 through 2026-10-07 sessions is
committed. The user said "do not commit yet" (2026-10-05), and on
2026-10-06 that their git server is down: they will commit themselves
once it's back. libglacier-ng is on branch
`dev` with a large uncommitted diff (`src/`, the docs, `build/`). Commit
only when the user asks; when they do, propose a few logical commits.
2026-10-05 session: system-scope permissions fix (bug 24), ROADMAP 2.3
dependencies (`src/deps.c`), index corruption on abort (bug 25), ROADMAP
1.1 sysroot, live removal deleting foreign files (bug 26), ROADMAP 1.2
(`syspkg -R`, `--strict`) + 2.6 (file collisions), gpkg ignoring unknown
flags (bug 27).
2026-10-06 session: ROADMAP 1.3 `glacier-bootstrap` local mode, and
Everest's initramfs switched to it (boots in QEMU). Tidy-up (gbuild's
hint removed). ROADMAP 1.4 DONE (packages for glacier itself; details in
ROADMAP 1.4 and section 7): full regression green, new
`tests/installed_alpine.sh` for the installed layout, which found bug 29
(fixed, mutation-checked, Everest rebuilt and booted with the fix).
The installer (1.5) is DEFERRED by the user: manual installation is
one of Everest's main features. Then ROADMAP 3.1 was designed with the
user (REPO_DESIGN.txt, every decision taken; only the TLS library for
curl in base is open) and steps 1, 2 and (that evening, after the
laptop died mid-rebuild) 3 of its section 9 were done.
2026-10-07 session: ROADMAP 3.1 step 4 (`-f`, `-u`, dependency
closure, the cache; see "3.1 status"). Help text for -f/-u/-O
shortened at the user's request. Refresh output (user's wording): `[i] Syncing
all repositories...`, then `[✓] All repositories synced successfully.`
if every repo succeeded; the per-repo success/up-to-date lines only
with `-V`; errors always (repo_alpine.sh checks both ways).
Then (same day) glacier-bootstrap configures repositories (user: it must
leave a semi-functional system, pacstrap-like): see "Bootstrap
repositories" below.
2026-10-09: help text. `gsign`, `grepo`, `gsha256sum` (C, `getopt_long`,
static `help()` in each tool's own file), `glacier-bootstrap` and `gworld`
now have `-h/--help` and `-v/--version` (`v4.0.0`) in gpkg's format
(`%-25s` `{-x --long} ARG` lines, GPL footer), with long forms for every
flag. `-h` goes to stdout, exit 0; usage errors stay one usage block on
stderr, exit 1. repo_alpine.sh checks the help of grepo/gsign/gsha256sum.
Backups: `glacier-bootstrap.old2`, `gworld.old`, `tests/repo_alpine.sh.old5`.
### Bootstrap repositories (2026-10-07)
The user chose: the config comes from a NEW base package
(`everest-repos` 1.0.0: `etc/glacier/repos.conf` + `keys/everest-2026.pub`,
source `~/Projects/everest/pkgsrc/everest-repos`; everest-bootscripts
1.3.0 no longer ships `etc/glacier`, its old copy is
`pkgsrc/everest-bootscripts/glacier.old`); bootstrap runs `syspkg -R T
-y` after installing and a failed fetch is an error (exit 7, no
"Bootstrapped"); under `-R`, `-y`/`-f`/`-u` use T's OWN keyring
(`keyring(sysroot)` in gpkg_common.lua; `require_trusted` also refuses a
file whose realpath leaves T). `-R -l` signatures stay the host's
(`HOST_KEYRING`). No url in T: `[!] No remote repositories are
configured in T.`, exit 0 (Everest today: base isn't published).
Copying the host's repos.conf (pacstrap's mirrorlist) left out for now.
The user said everything a template lists for base also goes in the
base repo (the published repo = the template's base packages); no url
yet. Everest rebuilt (bootscripts 1.3.0, everest-repos, glacier-ng 4.0.0
in place), initramfs remade, booted: `/etc/glacier` from everest-repos,
12 packages.
### Audit and cleanup (2026-10-06, late)
At the user's request, before 3.1 step 4: a full security audit
(`SECURITY_REVIEW_2026-10-06.md`, bugs 30-35 below), unused code removed,
and junk cleaned up. The user's choices: delete only the `.old` backups
and stray binaries (every prototype/note file stays: libglacier-ng's
`src/{deb,gpkg,templ,report,test_*}.c`, `src/html`, `pkg/`, glacier-ng's
`todo`, `tree/`, `test-*`, `pkgs/`...); libglacier-ng's build outputs are
untracked (`.gitignore`: `/build/`; the deletions show in the next
commit); the `.old` backup rule STAYS (see "Backups" below for current ones). New tests:
`tests/audit_alpine.sh` (26 checks), `tests/test_abort_unlocked.lua`.
Full regression green; Everest rebuilt and booted. Everest's own `.old`
files were not touched (not one of the two repos).
### 3.1 status
Step 4 DONE (2026-10-07; REPO_DESIGN section 6 "As implemented"). The
user chose: the FIRST repo (repos.conf order) serving a name wins, at
its highest version; a dependency installed too old is upgraded in the
same transaction (keeps its auto/explicit mark); cached downloads are
deleted after the commit that installs them.
- libglacier-ng: `gl_scope_cache_dir(ctx, repo, out)` (transaction.c;
`<base>/cache/REPO`, checked like `repos` via the shared static
`checked_subdir`); `log.c` help lists `-f`/`-u`.
- glacier-ng `gpkg_common.lua`: `M.fetch(names, opts)`,
`M.upgrade(names, opts)`; `load_indexes` (refresh, then read the
accepted copies), `pick`, `close_deps`, `download` (SIZE-capped,
hashed), `matches_index` (manifest vs index entry), `install_plan`.
`M.install` is now `verify_all` + `install_copies(copies, original,
opts, meta)`, shared with -f/-u (`meta`: auto, may_replace, dep,
cache). `verify_and_copy(path, vouched)` takes the index's hash in
place of a sidecar. syspkg `-f`/`-u` modes; gpkg `-f`/`-u` (no
longer "not implemented").
- New suite `tests/fetch_alpine.sh` (84 checks; 16 Lua mutants + 1 C,
all caught except the download-level hash check, equivalent: kept as
defence in depth). The `gpkg -u "%s%n"` format check moved there from
audit_alpine.sh. Full regression green.
- Everest's libglacier-ng/glacier-ng 4.0.0 were REBUILT in place
2026-10-07 with -f/-u/-y (user: keep 4.0.0, no bump) and the
initramfs remade; in QEMU `gpkg -f hi` as a wheel user now says
`[x] No remote repositories are configured.` (shipped repos.conf has
no url; no curl in base). `container.sh initramfs` now `mkdir -p`s
`$G/lib/glacier` (gone from the tree). Not done: no published repo;
TLS library still open. Next: step 5 (`glacier-bootstrap -r`),
then 6 (user repos).
Step 3 DONE (2026-10-06, evening; REPO_DESIGN section 5 "As
implemented"). The user chose: a refresh-only flag `-y`, the date-form
SERIAL (YYYYMMDDNN), grepo in C.
- libglacier-ng: `src/grepo.c` (`grepo -s KEY.sec -r REPO [-n SERIAL]
[-e DAYS] DIR`, Makefile target in `all`, installed to bin, built from
source like gsign); `pkg.c`: `gl_index_parse(text, len, repo, out)` /
`gl_index_free` (strict; takes the signed BYTES; refuses NUL and '@'),
`gl_conf_repos(path, out, len)` ("NAME\tURL\n" lines), repos.conf
validation shared in `read_repos_conf`; `transaction.c`:
`gl_scope_state_dir(ctx, "repos", out)` (creates/checks
`<base>/repos`). `log.c` help lists `-y`.
- glacier-ng: `M.refresh(opts)` in `gpkg_common.lua` (fetch file:// or
curl https, verify with a key listed for THAT repo, parse, rollback /
same-serial / expiry checks, rename into `<base>/repos`, under the
scope lock); `syspkg -y` (needs root and `-n`; `-R` works), `gpkg -y`
(system repos.conf + keyring until step 6). Dockerfiles copy grepo to
/usr/local/bin. New suite `tests/repo_alpine.sh` (103 checks; 18
mutants, all caught). Full regression green. Everest's libglacier-ng
and glacier-ng packages rebuilt with it.
- Next: step 4 (`-f` with dependency closure, then `-u`; the cache in
`<base>/cache`, section 5's Fetch).
Step 2 DONE (2026-10-06, second part of the session; REPO_DESIGN
section 7 "As implemented" has the details):
- libglacier-ng `src/pkg.c`: `gl_conf_repo_keys(path, repo, out, len)`,
a strict repos.conf reader; new statuses `GL_CONF_ERR_INVALID = 4`,
`GL_CONF_ERR_SPACE = 5`.
- glacier-ng `lib/gpkg_common.lua`: `verify_all(paths, opts)` refuses a
base package in system scope without a trusted signature
(`signed_for`, `trusted_keys`, `untrusted`); `verify_and_copy` returns
the sidecar's bytes. glacier-bootstrap copies the sidecar's BYTES and
the `.sig`. `gbuild -k`/`--key`, `tests/mkpkg -k`.
- Every `Dockerfile.*` installs gsign + gsha256sum in /usr/local/bin and
a TEST keyring (`glacier-test`, secret `/root/glacier-test.sec`).
`syspkg_alpine.sh` / `overwrite_alpine.sh` make their own key.
- New suite `tests/keyring_alpine.sh` (68 checks; 17 mutants: 10 Lua/
shell, 7 C, all caught). Full regression green.
- Everest: `./everest.sh keygen` made `everest-2026`; the SECRET key is
`~/.config/everest/keys/everest-2026.sec` (user's choice: outside the
project), mounted read-only for `build`, which refuses without it.
Every package is built `gbuild -k`. everest-bootscripts 1.2.0 ships
`etc/glacier/repos.conf` (base: everest-2026) and
`etc/glacier/keys/everest-2026.pub`; `container.sh initramfs`
installs the same into the build container's `/etc/glacier`.
The signed `build all` was cut off after busybox (laptop died); the
other 9 were rebuilt `-k` the evening of 2026-10-06, `initramfs` ran
through glacier-bootstrap (signature checks passed for all 11), and
Everest booted in QEMU: repos.conf + key in `/etc/glacier`, index
lists all 11, syspkg/gpkg run as root and as a fresh user.
- Base KEEP_OLD backups can't be restored with `-lO` (no signature):
the user accepted that until the restore interface (REPO_DESIGN 10f).
Step 1 DONE (2026-10-06):
- `src/monocypher/`: Monocypher 4.0.3, vendored UNMODIFIED (provenance
and sha512 in its README; GitHub and monocypher.org copies matched).
Compiles clean under the project's `-Werror` flags.
- `src/sign.{c,h}`: signify formats (strict parse, canonical base64),
`gl_sign_read_pubkey`/`_read_sig`/`_check_seckey`/`_verify`/
`_verify_file`/`_file`/`_generate`, statuses `GL_SIGN_OK`..
`GL_SIGN_ERR_WRITE = 9`. Built into `libglacier_verify.so` (NOT into
the combined `libglacier-ng.so`).
- `src/gsign.c` (`-G`/`-S`/`-V`, lg_printf messages naming the file at
fault), Makefile targets `gsign` (in `all`, installed to bin like
gsha256sum).
- `glacier-ng/tests/sign_alpine.sh` (alpine:latest, mounts /libsrc):
round trips, signify interop both ways (Alpine `signify`), encrypted
and corrupted secret keys, malformed key/sig files, 64 MiB limit,
O_NOFOLLOW, key modes, a direct FFI call of `gl_sign_generate`.
Passes in full. (`luajit -e CODE ARG` runs ARG as a script, so the
FFI snippet is written to `/gen.lua` first.)
- Mutation checks: 12 mutants, all caught except m9 (padding position,
`j < 2`), which is EQUIVALENT: kept, it prevents `3 - pad`
underflowing. m5 (fchmod), m6 (final newline) and m10 (O_EXCL) needed
the umask-077, final-char and FFI checks respectively.
- `glacier-ng/lib/glacier_cdef.lua`: `gl_sign_*` declarations and
`M.sign_name()`; checked against the real `.so` (struct sizes 40/72,
every call on signify-made files). Full regression green after it.
Also open, for the user (raised 2026-10-06, not answered yet):
- libgcc: luajit is built `-static-libgcc` (Everest has no libgcc_s).
Claude's call; the alternative is a GCC runtime package in base
(ROADMAP section 6).
- Leftover `~/Projects/everest/pkgs/everest-bootscripts-1.0.0.gpkg`
(+ sidecar), superseded by 1.1.0. Not deleted: ask first.
### Everest's glacier packages (1.4)
- `~/Projects/everest/scripts/container.sh` builds zlib, libarchive
(gzip/tar only), libconfig, luajit (pinned commit,
`LDFLAGS=-static-libgcc`), libglacier-ng, glacier-ng (all base) and
doas (extra, OpenDoas, no PAM).
- glacier-ng layout: `usr/lib/glacier-ng/{syspkg,gpkg,gstore,lib/*.lua}`,
`lib/glacier -> /usr/lib/glacier` (absolute: relative links resolve
inside the store), `usr/bin/X -> ../lib/glacier-ng/X`,
`etc/glacier/glacier.conf`.
- everest-bootscripts 1.2.0 (1.1.0 before signing) ships `/etc/doas.conf` (0400): `permit
:wheel` plus nopass `gstore args -n` for BOTH `cmd gstore` and `cmd
/usr/bin/gstore` (OpenDoas matches `cmd` against what was typed).
- `templates/minimal.template` and `PACKAGES` in everest.sh list all 12
packages (everest-repos added 2026-10-07). `Dockerfile.build` has autoconf, automake, libtool, cmake,
pkgconf.
## The three projects
@@ -31,11 +248,12 @@ when the user asks.
|---|---|---|
| `~/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 |
| `~/Projects/everest` | The distro build: `everest.sh build\|initramfs\|boot`, packages, bootable QEMU image | **no**: same backup rule (but never inside `pkgsrc/*/etc/`, see Boot test) |
`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.
Nothing prebuilt is kept in either repo (the host-built `.so`s in
`glacier-ng/lib/glacier/` were removed 2026-10-06). Every image and test
builds libglacier-ng from source; the Dockerfiles create
`/app/lib/glacier/` with the musl `.so`s.
## What this project is
@@ -56,10 +274,20 @@ These are preferences stated in sessions. Follow them.
- 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.
- **Never say "Completed" after an error.** If the post-commit link pass
refused any path (`ctx->link_refusals`, counted only for packages
installed by that transaction), gpkg/syspkg end with
`[x] N paths were not linked.` (`1 path was not linked.`) and exit 1.
The transaction itself stays committed (2026-10-05).
- **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.
- **What goes where.** ALL design docs (and ROADMAP, this file) live in
libglacier-ng, frontend ones included. Other than docs, libglacier-ng
holds only backend material (C sources, the build). Frontend code,
tests, Dockerfiles and everything the bootstrap runs on live in
glacier-ng. Clarified 2026-10-06.
- **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.
@@ -68,6 +296,10 @@ These are preferences stated in sessions. Follow them.
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.
- **Help text is terse** (2026-10-07): one short phrase per flag, no
mechanism details (`-f` "Install packages from a remote repository",
`-u` "Update system or individual packages", `-O` "Allow Glacier to
overwrite existing files").
## Decisions the user has made (don't relitigate)
@@ -90,18 +322,105 @@ These are preferences stated in sessions. Follow them.
- "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.
`gpkg -f` is `-l` but fetched by name from a remote repository. Done
2026-10-07 (3.1 step 4). Never against local files.
- **-f/-u details (2026-10-07):** first repo by priority wins (then its
highest version); a too-old installed dependency is upgraded in the
same transaction; cached downloads are deleted after commit.
- **Dependencies (ROADMAP 2.3, decided 2026-10-05):** see the spec in
ROADMAP 2.3. In short: minimum versions, conflicts both ways by name,
no override flag, report every problem, missing deps refuse until 3.1
brings automatic closure, auto/explicit recorded in the index now.
- **sysroot (IMAGE_DESIGN section 3) accepted as proposed**, plus the
library refuses a non-"/" sysroot for `GL_SCOPE_USR`. Order of work:
2.3 first, then 1.1.
- **System scope is world-readable** (dirs 0755, index files 0644): every
user must be able to run what syspkg installs (2026-10-05). User scope
stays 0700, and other users' programs stay unreachable to non-root
users. Root CAN run them by full path, but other users' links are
deliberately NOT on root's `PATH` (a user could get code run as root).
- **Link refusals after a commit** end with `[x] N paths were not linked.`
and exit 1, never "Completed" (the user's wording rule: don't say
"completed" if there was an error).
- **`syspkg -R DIR`** (not `-r`), `--root DIR`, `--root=DIR`; `-R`
IMPLIES `--strict`; `--strict` alone is opt-in on the host.
- **File collisions (2.6) are always refused**, every scope, strict or
not (folded into 1.2 at the user's go-ahead).
- **PATH:** per-user links, then `/glacier/sys/links/bin`, then the system
(`/usr/bin` on Everest), in the Docker images AND Everest's
`/etc/profile`. Non-base system packages must be on `PATH`; base
programs must be runnable by every user.
- `everest-bootscripts` is the name of the base `/etc` package. Its source
lives in `~/Projects/everest/pkgsrc/`.
- **glacier-bootstrap (2026-10-06):** template = `NAME=` + `PKG=name ver
[sha256:hex]` lines, no `SYS_PROFILE` (unknown key until ROADMAP 3.3);
it runs the `syspkg` on PATH (doas unless root); a template dependency
must be listed at that version OR LATER (the 2.3 rule); Everest's
initramfs is built through it.
- **Installing Everest (2026-10-07):** possible from a live Everest
system OR any Linux host; the LIVE SYSTEM is the preferred, officially
supported way (so base/the live medium must eventually carry what a
manual install needs). The user does the first QEMU disk install
themselves: don't start it.
- **Everest supports glibc as well as musl** (2026-10-07), Gentoo/Void
style: the user picks the libc at install time, one per system.
Today everything is built for musl (Alpine); glibc needs ROADMAP
4.1's profiles. How packages are kept apart is still open (ROADMAP 6).
- **Signing (2026-10-06, REPO_DESIGN 10e/10f):** Everest's SECRET key
stays outside the project (`~/.config/everest/keys`, mounted
read-only); a base KEEP_OLD backup refusing under `-lO` (unsigned) is
accepted until the restore interface.
## Core architecture
**`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).
**`gl_context_t`** (transaction.h) is the central abstraction. Fields, in
order: `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`, then:
| Field | Set by | Meaning |
|---|---|---|
| `overwrite` | caller (after init, which zeroes it) | `-O`: the next link pass may displace foreign files |
| `sysroot` | `*_at` init | install root, `""` = the running system |
| `link_refusals` | link pass; commit copies the relink's count back | paths refused for this transaction's packages |
| `strict` | caller (after init, which zeroes it) | refuse those before committing instead |
New fields go at the END, and `lib/glacier_cdef.lua` must mirror the
struct exactly (FFI has no compiler to catch a mismatch).
**Status codes worth knowing:** `GL_TXN_ERR_DEPS = 9` covers every
pre-commit refusal (dependency, conflict, file collision, strict link
refusal); the library has already printed why, so the CLIs print nothing
more. Live-context equivalents: `GL_INSTALL_ERR_DEPS = 8`,
`GL_REMOVE_ERR_DEPS = 4`. `GL_INSTALL_ERR_SCOPE = 7` is base-in-user-
scope.
**Install root / sysroot** (ROADMAP 1.1, done 2026-10-05):
- `gl_init_live_context_at` / `gl_init_stage_context_at` / `gl_init_sys_at`
take a sysroot; the old functions wrap them with `"/"`.
`gl_normalize_sysroot` (common.h) refuses relative paths and `.`/`..`
components. **User scope refuses any sysroot** other than `/`.
- The context's paths are **INSTALL paths** (`T/glacier/sys/store`), so
every filesystem operation uses them unchanged: a forgotten translation
writes into T, never the host. That's deliberately the reverse of
IMAGE_DESIGN's wording (runtime paths stored, `gl_at` on every
operation), with the same effect and a safer failure mode.
- Translate ONLY where a path's runtime meaning matters:
`gl_runtime_path(ctx, p)` for link targets, `is_glacier_symlink`
prefixes and `.displaced` keys; `gl_at(ctx, runtime, out)` to check a
link's target (prune). `/usr` and `/etc` destinations are
`ctx->sysroot` + path.
- **Trust check** (`gl_check_sysroot` in transaction.c, also inside
every `*_at` init and `gl_init_sys_at`; helper `gl_sysroot_trusted` in
common.h): the root and every ancestor must exist and be root-owned,
non-symlink directories, not group/world-writable. So `/tmp/x` is
refused. Messages: `Refusing install root R: P is a symlink.` / `is
missing.` / `is not root-owned, or is group/world-writable.`
- Test: `tests/sysroot_alpine.sh` (host `/usr`, `/glacier`, `/etc`
byte-identical; `chroot T` runs a linked binary; 8 mutations caught).
- Not translated, on purpose: `KEEP_OLD` is read from the HOST's
`/etc/glacier/glacier.conf` even under `-R` (the build host's policy);
user scope's dependency view of "the system" is always the host's.
**Layout.** Each scope owns ONE base directory holding
`{index,store,links,stage,old,lock,backup}`. Every create, rename and lock
@@ -110,7 +429,7 @@ a transaction does stays inside it.
| 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 |
| `GL_SCOPE_SYS` | `/glacier/sys/` | root, world-readable: dirs 0755, index files 0644 (`gl_scope_dir_mode`/`gl_scope_file_mode` in transaction.h), whatever the umask |
- The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment
are in `common.h`.
@@ -126,11 +445,22 @@ a transaction does stays inside it.
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.
- **Commit:** first `gl_check_txn` (deps, conflicts, collisions, and
strict link refusals if `ctx->strict`); any problem → nothing changes,
`GL_TXN_ERR_DEPS`. Then rename-based promotion (stage → live, previous
live → `old/`), then fsync the base. Then `gl_relink_store` regenerates
links from a *fresh live context* and rescans the WHOLE store (every
commit relinks every package, so link-time logic must be idempotent);
its `link_refusals` is copied back into the caller's context. `old/` is
then deleted. (The index is rebuilt inside the stage by each
install/remove, and promoted with it.)
- **Live contexts** (no transaction; tests and third-party callers):
`gl_install_pkg` runs `gl_check_plan` itself, and `gl_remove_pkg`
removes the package's links by pruning (`gl_relink_store`), like a
commit.
- **Index files** are named to the second and seeding hardlinks them:
never open a seeded file for writing, always write elsewhere and
`rename()` (bug 25).
**Link routing** (per package, in `gl_link_pkg`):
@@ -157,7 +487,8 @@ transactions):
| `<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 |
| `<ver>/.glacier-auto` | installed as a dependency, not by name (index field 4 = `auto`); set by `gl_set_pkg_auto`, nothing sets it until 3.1 |
| `<store>/.displaced/<runtime abs path>` | originals displaced by `-O`, restored when the link that replaced them is pruned; keyed by RUNTIME path (no sysroot prefix) |
| `<store>/.etc-state/<path>` | the last version of each `/etc` file a package offered |
**Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax).
@@ -189,8 +520,20 @@ possibly shared:
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`
- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`, and
walks by descriptor (`openat(O_NOFOLLOW)`, `unlinkat`): an entry
swapped for a symlink is unlinked, never followed (bug 31). Every
caller passes a glacier-only path; keep it that way.
- **Root reading a user's tree** (`add_entry_at`, `gstore -b`) is
fd-relative with `O_NOFOLLOW` too (bug 32).
- **Reserved names.** A package's top-level `files/.glacier-*` entries
are refused (`GL_MARKER_PREFIX`, common.h): those names are glacier's
own version-dir files, and every marker write uses `O_NOFOLLOW`
(bug 30). The marker names are defined ONCE, in common.h.
- **Manifests are untrusted**: `gl_gpm2gpkg` refuses `@include` and
dependency/conflict entries without a valid name (bug 33). The CLIs
read manifests ONLY through the library (`manifest_of` in
gpkg_common.lua), never by pattern matching (bug 34). `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
@@ -225,7 +568,8 @@ possibly shared:
**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).
(`repo::name::version::auto|explicit` lines; files before 2026-10-05
stop at the version, and every parser accepts both).
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`.
@@ -260,7 +604,70 @@ the merged-/usr tree: `/bin` and `/sbin` → `usr/bin`, `/lib` → `usr/lib`,
- `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`.
- It takes a root prefix; `gl_init_sys_at` / `syspkg -R DIR -n` use it.
**Dependencies and conflicts** (ROADMAP 2.3; `src/deps.c`, built into
`libglacier_istoreutils.so`; rules in `deps.h`):
- `gl_check_plan(live, adds, n, "repo::name" removes, n)` runs before the
prompt (`check_plan` in `gpkg_common.lua`). `gl_check_txn(stage)`
compares stage with live, and `gl_commit_transaction` calls it itself
(`GL_TXN_ERR_DEPS = 9`). A live context runs the plan check inside
`gl_install_pkg`/`gl_remove_pkg` (`GL_INSTALL_ERR_DEPS = 8`,
`GL_REMOVE_ERR_DEPS = 4`). So no library caller can skip it.
- It reads each version dir's `.glacier-manifest`; a version without one
has no deps. Only what the transaction changes is checked.
- `-x` of a name installed in several repos is only checked at commit
(its repo is chosen after the prompt).
- Claude's own calls, not reviewed by the user: conflicts only apply
within one scope; only what a transaction changes is checked.
**Strict link check** (`check_links` in deps.c; `ctx->strict`): system
scope only, for this transaction's packages, mirrors `gl_link_pkg`: a
foreign file/symlink at a file's destination (unless `-O` and no earlier
`.displaced` copy), a non-directory where a directory goes, or a
directory over another package's symlink → `[x] Refusing to overwrite
non-glacier path: P` / `Refusing to merge directory ...`. A foreign
symlink where a directory goes (`usr/sbin -> bin`) is followed, as the
link pass does.
**File collisions** (ROADMAP 2.6, `check_collisions` in deps.c): two
packages whose entries land on the same path (by `dest_of`, which mirrors
`gl_link_pkg`) are refused in every scope, before the prompt and at
commit, when either is in the transaction: `[x] extra::a and extra::b
both ship /glacier/sys/links/bin/x.` Sharing a directory is fine; a file
or symlink where the other has a directory is a collision.
**Signatures** (REPO_DESIGN section 7; `gpkg_common.lua`): in system
scope, a package whose LIBRARY-read manifest (`gl_read_pkg_manifest`,
never `read_manifest_fields`) says base needs `FILE.sha256.sig` over the
exact sidecar bytes verified, by the key with that keynum among those
`/etc/glacier/repos.conf` lists for base (`gl_conf_repo_keys`). Else
`[x] base::NAME is not signed by a key trusted for base.` before the
prompt. repos.conf and keys (and all ancestors, real paths too) must be
root-owned, not group/world-writable. Under `-R`, the HOST's keyring
for this check; `-y`/`-f`/`-u` under `-R` use T's (inside T only).
No repos.conf = nothing trusted. gpkg / other repos: sidecar only.
**Repository indexes** (REPO_DESIGN sections 4-5; `M.refresh` in
`gpkg_common.lua`): `syspkg -y` / `gpkg -y` fetch `URL/repo.idx` + `.sig`
for every repos.conf entry with a url, check the signature over the
fetched bytes with a key listed for that repo, `gl_index_parse` the
same bytes, refuse SERIAL below the accepted one (or equal with other
bytes) and an expired VALID_UNTIL, then rename into
`<base>/repos/REPO.idx` (+ `.sig`). Any failure keeps the accepted
copy; every repo is tried; exit 1 if any failed.
**`-f` / `-u`** (REPO_DESIGN section 6; `M.fetch`/`M.upgrade`):
refresh first (any failure refuses), pick each name from the first
repo serving it, add missing deps (auto) and upgrade too-old ones,
download into `<base>/cache/REPO` (hash and SIZE from the index), copy
privately with the index's hash, require the manifest to equal the
index entry (`[x] extra::x 1.0.0 does not match its index.`), then the
`-l` path (`install_copies`): summary with `(dependency)`, 2.3 checks,
prompt, staged transaction, `gl_set_pkg_auto`, backups, cache files
deleted after commit. A base package fetched this way needs no `.sig`
(the base index vouches). `-f` of an installed other version needs
`-O`; `-u` and dependency upgrades don't. gpkg never fetches from base.
**Package symlinks** are extracted verbatim and linked like files.
Hardlink, device and fifo entries are skipped. `add_to_archive` (used by
@@ -270,14 +677,28 @@ backups and `gstore -b`) archives symlinks.
| 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` | `-l FILE...` `-f NAME...` `-u [NAME...]` `-x NAME...` `-y` `-O` `-S` `-V` `-h` `-v` | the user; per-user scope only, NEVER constructs `GL_SCOPE_SYS` |
| `syspkg` | `-n` `-l FILE...` `-f NAME...` `-u [NAME...]` `-x NAME...` `-y` `-O` `-S` `-V` `-R DIR` `--strict` `-h` `-v` | root via doas; `-h`/`-v` without root |
| `gstore` | `-n [UID]` `-b UID OUTFILE` `-x UID` `-s` (refuses: not implemented) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) |
| `glacier-bootstrap` | `-d PKGDIR TEMPLATE TARGET` `-n` `-t` `-V` `-h` `-v` | the user; borrows root only for `doas syspkg` / `doas chroot` |
- **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()`.
`/proc/self/loginuid`, falling back to `getuid()`. Unknown flags are
refused (`[x] Unknown option: -R`); gpkg has no `-R`/`--strict`.
- **Endings (both CLIs):** `[✓] Completed with no errors.` (exit 0), or
after link refusals `[x] N paths were not linked.` (exit 1;
`report_done` in gpkg_common.lua). Refusals before the prompt exit 1
with only the `[x]` lines.
- **syspkg, `-n`:** creates the tree plus `/glacier/sys`.
- **syspkg, `-R DIR` / `--root DIR` / `--root=DIR`** (last in a bundle:
`-lR DIR`): operate on the system under DIR, through the `*_at`
contexts. Refused first by `gl_check_sysroot`: DIR and every ancestor
must be root-owned, non-symlink directories, not group/world-writable
(`/tmp/...` is refused). Implies `--strict`.
- **syspkg, `--strict`:** anything the link pass would refuse fails the
whole transaction before the prompt (and at commit, `ctx->strict`);
`-O` still displaces.
- **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
@@ -290,11 +711,25 @@ backups and `gstore -b`) archives symlinks.
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
when euid is 0, every loaded `.lua`/`.so`, and every ancestor of the
script's realpath AND of each loaded file's realpath, must be
root-owned and not group/world-writable (bug 29). 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.
- **gpkg** (2026-10-06) loads `lib/` from its own realpath, like
glacier-bootstrap (no ownership checks; it's unprivileged), so an
installed `/usr/bin/gpkg` works from any directory.
- **glacier-bootstrap** (GLACIER_BOOTSTRAP section 10): parse →
verify (copies each `PKGDIR/NAME-VER.gpkg` into a private `mktemp -d`
while hashing; sidecar + pin; writes the copy's sidecar from that hash)
→ resolve (reads manifests of the COPIES via `gl_read_pkg_manifest`:
name/version match, deps listed at >= version, conflicts) → `syspkg -R
T -n` (skipped if T is initialised with nothing installed) → `syspkg -R
T -l COPIES` (one strict transaction; its prompt passes through) →
checks T's index lists every package (syspkg exits 0 when declined) →
`syspkg -R T -y` if T's repos.conf has a url → `-t` smoke test.
Never writes into T itself. Exit: 1 usage/target, 2 template,
3 verify, 4 resolve, 5 install, 6 smoke, 7 repositories. Loads its `lib/`
from its realpath, no root checks. No `-r` / `--pin` yet.
- **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
@@ -303,7 +738,9 @@ backups and `gstore -b`) archives symlinks.
## 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`;
- `-n -V -g -c -d -m -D -C -b -i -o -p -j -F -t -x -u -r -k -s`
(`-k KEY.sec`: sign the sidecar with gsign, `$GSIGN_BIN` / build/bin
/ PATH, key tried before building);
`-v`/`--version` prints the version, `--verbose` turns on verbose
output.
- Autodetects the build system, with autotools checked BEFORE a bare
@@ -320,15 +757,29 @@ backups and `gstore -b`) archives symlinks.
(`Dockerfile.build`).
- **Kernel version:** linux `6.12.112`; plain `v6.12` doesn't build with
GCC 15.
- **Everest's image IS built through glacier** (2026-10-06): `container.sh
initramfs` copies glacier-ng to a root-owned `/opt/glacier-ng` (with
the musl-built `.so`s; syspkg's trust checks need that) and runs
`glacier-bootstrap -t -d /everest/pkgs templates/minimal.template
/target` (root-owned: not under `/tmp`). T's links name RUNTIME paths,
so anything reading through them from the build host must resolve
inside T (vmlinuz is read with `chroot T cat`). Only `root` exists in
the shipped `/etc/passwd`.
- gbuild ends with the package's contents list. Its "To install with
gpkg: gpkg -l ..." hint was removed 2026-10-06 at the user's request.
## libglacier-ng build
- **Separate `.so` per module** (`libglacier_log`, `_pkg`, `_istoreutils`,
`_transaction`, `_dag`, `_verify`). This split is deliberate: a
`_transaction`, `_verify`; `_dag` was removed 2026-10-06, unused). 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.
- **Tools** in `build/bin`: `gsha256sum`, `gsign`, `grepo` (gsign and
grepo compile the sources they need in, no rpath). Maintainer tools,
later "glacier-tools": Everest's libglacier-ng package drops
`usr/bin`.
- **Warnings:** `config.mk` builds with `-Werror`. `-Wno-format-truncation`
is set because that warning is noise; `-Wstringop-truncation` stays
fatal.
@@ -340,7 +791,7 @@ backups and `gstore -b`) archives symlinks.
Run everything in throwaway containers. Several suites write to `/usr`,
`/etc` and `/glacier`.
**FFI suite** (`glacier-ng/tests/*.lua`; 8 files, all passing):
**FFI suite** (`glacier-ng/tests/*.lua`; 11 files, all passing):
```sh
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
-v ~/Projects/libglacier-ng:/libsrc:ro alpine:latest sh -c '
@@ -355,8 +806,9 @@ GNU `tar` is required: the hostile-archive fixtures use `tar --append`.
| Suite | Image |
|---|---|
| `sign_alpine.sh` | `alpine:latest` with `/libsrc` and `/t.sh` mounted |
| `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` |
| `verify_alpine.sh`, `user_layout_alpine.sh`, `upgrade_alpine.sh`, `sys_perms_alpine.sh`, `deps_alpine.sh`, `sysroot_alpine.sh`, `root_alpine.sh`, `bootstrap_alpine.sh`, `installed_alpine.sh`, `keyring_alpine.sh`, `repo_alpine.sh`, `audit_alpine.sh`, `fetch_alpine.sh` | `glacier-alpine_root` |
Build the `glacier-alpine_root` image with:
```sh
@@ -365,6 +817,46 @@ cd ~/Projects/glacier-ng && docker build -f Dockerfile.alpine_root \
docker run --rm -v "$PWD/tests/NAME.sh:/t.sh:ro" glacier-alpine_root sh /t.sh
```
`glacier-ng/lib/glacier/` does not exist in the tree (the `.so`s are
built per image); syspkg_alpine/overwrite_alpine `mkdir -p` it
(2026-10-07). If Docker fails with `veth ... operation not supported`,
the host kernel was upgraded without a reboot (modules missing): use
`docker build --network host` and `docker run --network=host` (or
`--network none` where nothing is fetched) until the user reboots.
Mutating a Lua file syspkg loads: mount it elsewhere and `cp` it over
`/app/lib/...` inside the container (a bind mount is user-owned, so
syspkg's trust check refuses it).
**Demo packages:** `glacier-ng/tests/mkpkg [-o DIR] [-k KEY.sec]
[-d NAME:VER]... [-c NAME:VER]... NAME VERSION REPO` writes a one-script
`.gpkg` plus its sidecar (`/app/tests/mkpkg` in the images); a base one
needs `-k /root/glacier-test.sec` (the images' test key) for syspkg. The user plays with changes
in `./run_docker.sh Dockerfile.alpine_root` using it; for an image,
`mkdir -p /mnt/img && ./syspkg -R /mnt/img -n`.
**Mutation checks, how:** `cp -a` libglacier-ng (and glacier-ng, for Lua
changes) into the scratchpad, break the defence there, then
`docker build -f Dockerfile.alpine_root --build-context libglacier=COPY
-t glacier-mut .` from the (copied) glacier-ng and run the suite against
`glacier-mut`; delete the image after. Docker tags must be lowercase. A
mutation that doesn't compile under `-Werror` proves nothing: rewrite it
so it compiles. Watch for checks that pass for the wrong reason (e.g. a
refusal caused by a missing directory instead of the defence). In shell
suites, `sh -c '... $OUT ...'` sees an EMPTY `$OUT` (not exported), so a
negative check written that way passes vacuously; use a function
(`lacks` in bootstrap_alpine.sh). Found 2026-10-06 by a surviving
mutation. The host shell is zsh: an unquoted `$VAR` holding `-v a:b`
is NOT word-split, so a docker command built that way silently runs
without the mount (a mutant "survived" that way, 2026-10-06). To
mutate a Lua CLI without rebuilding, bind-mount the mutated file over
`/app/NAME` (`-v $PWD/mut:/app/glacier-bootstrap:ro`).
**Full regression** = the FFI suite + `syspkg_alpine.sh` +
`overwrite_alpine.sh` (both ways) + `sign_alpine.sh` + the thirteen `glacier-alpine_root`
suites, all in parallel; the two output-based suites (`syspkg_alpine`,
`user_layout_alpine`) have no PASS lines, so grep their output for
`Unknown|traceback|failed|GL_` instead (only `--bogus` is expected).
**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`
@@ -372,10 +864,24 @@ defaults to root. All of them have:
- root-owned `/app`;
- no `/glacier` until `gstore -n` / `syspkg -n`;
- the doas rule plus `permit nopass glacier`;
- `/etc/profile.d/glacier.sh` putting `/glacier/usr/<uid>/links/bin` then
`/glacier/sys/links/bin` on `PATH` (and `lib` on `LD_LIBRARY_PATH`)
unconditionally, since `/glacier` only appears after login (fixed
2026-10-05: it had no system links and checked `-d` at login);
- `glacier.conf` installed.
**Boot test:** `cd ~/Projects/everest && ./everest.sh boot` boots QEMU
(KVM). Press Enter for a root shell; `poweroff -f` to leave.
(KVM). Press Enter for a root shell; `poweroff -f` to leave. To test as
a non-root user, `adduser -D -H -h /tmp tester` at runtime, then
`su - tester -c ...` (nothing shipped has a user besides root).
Everest's `/etc/profile` sets `PATH` to per-user links, then
`/glacier/sys/links/bin`, then `/usr/bin` (2026-10-05). Backups of
everest-bootscripts files go NEXT TO `etc/`, never inside it: the package
copies `etc/` wholesale. After changing a package:
`./everest.sh build PKG && ./everest.sh initramfs`, then boot.
To drive the boot non-interactively, fork QEMU on a pty from Python
(`pty.fork()`), wait for `Please press Enter`, send lines and match the
`# `/`$ ` prompt (no `expect` on the host).
## Bug history: so these don't get reintroduced
@@ -430,6 +936,99 @@ All fixed in the current tree.
context every retry reported `ALREADY_INSTALLED`.
23. Backups written in the same second were pruned in the wrong order
(whole-second mtimes).
24. System scope was root 0700 (index, store, links, every version dir),
so no non-root user could run anything syspkg installed, through
`/usr` or `/glacier/sys/links`. Found 2026-10-05. Also fixed: every
mode glacier creates was filtered by the caller's umask (`gl_mkdirp`,
seeding, extracted files), so syspkg under umask 077 made even a
0755 store private. `gl_mkdirp`/`gl_mkdir_exact` now chmod what they
create, and extracted entries get `archive mode & 07755` explicitly
(never group/world-writable). Test: `tests/sys_perms_alpine.sh`
(mutation-checked). No migration: a store made before the fix keeps
its 0700 dirs until reinstalled.
25. `gl_rebuild_index` `fopen(name, "w")`'d the new index file. Index
names have one-second resolution and seeding HARDLINKS live's index
files into the stage, so a staged install in the same second as the
last commit rewrote the LIVE index in place; an aborted transaction
then left live listing the stage's packages (and history files were
overwritten). It now writes to `tmpfile()` and only `rename()`s onto
the name. Found 2026-10-05 by `deps_alpine.sh`; regression test
`tests/test_index_abort.lua` (mutation-checked). Rule: never open a
seeded file for writing; replace it by rename.
26. Live-context removal (`gl_unlink_pkg`) `unlink()`ed the destination
of every regular file in the package WITHOUT `is_glacier_symlink`:
a foreign file the package had been refused to replace was deleted,
and the package's own symlink entries were never unlinked. The CLIs
always stage, so only library callers were exposed. Removed;
live removal now prunes like a commit (`gl_relink_store`). Found
2026-10-05; `tests/test_live_remove_foreign.lua` (fails on the old
code).
27. gpkg's `handle_flag` had no `else`: an unknown flag (`-R`) was
silently ignored and the NEXT word reported as the unknown option.
Now `[x] Unknown option: -R` (capitalized, as syspkg). 2026-10-05.
28. gbuild's `clone_source` printed its "Shallow clone failed" warning
(and the full clone's/checkout's output) to stdout, which
`SOURCE_DIR="$(clone_source)"` captured, so building from a commit
hash (`-r <sha>`) always failed. Those now go to stderr. Found
2026-10-06 building luajit.
29. syspkg's and gstore's trust checks walked only the ancestors of the
script's own realpath. Installed, `lib/glacier -> /usr/lib/glacier`
and the `.so`s link into libglacier-ng's store dir, so a non-root-
owned directory there was never checked: root loaded `.so`s from it.
Both now also check every ancestor of each loaded file's realpath.
Found 2026-10-06 by `tests/installed_alpine.sh` (both mutations
caught).
30. A package could ship top-level `files/.glacier-*` entries. As a
symlink, `.glacier-fresh` made syspkg truncate its target as root
(an unsigned extra package emptied `/root/victim`, reproduced);
`.glacier-overwrite` self-granted `-O`. Reserved names refused,
marker writes `O_NOFOLLOW`. 2026-10-06 audit.
31. `gl_rmdir_recursive` (lstat, then opendir by path) could be steered
into a symlink's target when root deleted a user's tree (`gstore
-x`). Now fd-relative. Race test in audit_alpine.sh.
32. `add_to_archive` (same class, `gstore -b`). Now fd-relative; no test
fails on the old code (window too narrow; review only).
33. Manifests could `@include` any file (read as root); a dependency
entry without a name cut the list short. Both refused.
34. gpkg_common's `read_manifest_fields` (tar + pattern match) could
disagree with the library: a decoy comment hid a replacement. Now
`manifest_of` (`gl_read_pkg_manifest`).
35. Format strings: gstore's `argv[0]`/UID and gpkg `-u`'s operand
reached `lg_printf` as the format; `%d` was given Lua numbers;
gstore UIDs wrapped past uid_t; `gl_abort_transaction` wiped a stage
without holding the lock. All fixed (`lg_printf` now has a printf
format attribute).
## Known gaps (not bugs, not yet scheduled)
- No migration for stores made before bug 24's fix (0700 dirs stay).
- Extracted entries keep setuid/setgid bits (`& 07755`), as before.
- No size limit on extraction (a decompression bomb fills the disk).
- Bug 32's fix has no test that fails on the old code.
- Hash verification is still CLI-only (ROADMAP 3.2); everything else
added 2026-10-05 is enforced in the library.
- A dependency refusal for `-x` of a name installed in several repos
comes at commit, after the prompt (its repo is chosen after it).
## Backups of glacier-ng / everest files (no git there)
The rule stands (back up before editing: `file.old`, then `.old2`...).
All of glacier-ng's backups were deleted 2026-10-06 at the user's
request. The 2026-10-07 session made new ones: `lib/glacier_cdef.lua.old`,
`lib/gpkg_common.lua.old`, `gpkg.old`, `syspkg.old`,
`tests/{audit,overwrite,syspkg,repo}_alpine.sh.old`,
`lib/gpkg_common.lua.old2`/`.old3`, `tests/repo_alpine.sh.old2`/`.old3`,
`tests/fetch_alpine.sh.old`, `lib/gpkg_common.lua.old4`,
`glacier-bootstrap.old`, `tests/bootstrap_alpine.sh.old`,
`tests/repo_alpine.sh.old4`, `tests/fetch_alpine.sh.old2`; Everest:
`scripts/container.sh.old4`, `everest.sh.old3`, `README.md.old3`,
`templates/minimal.template.old2`, `pkgsrc/everest-bootscripts/glacier.old`.
Everest still has its earlier ones (`everest.sh.old`/`.old2`,
`scripts/container.sh.old`/`.old2`/`.old3`, `README.md.old`/`.old2`,
`Dockerfile.build.old`, `templates/minimal.template.old`,
`pkgsrc/everest-bootscripts/profile.old`). Leave them unless the user
says to clean up.
## Conventions
+76 -16
View File
@@ -2,9 +2,10 @@
| 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.
STATUS: LOCAL MODE IMPLEMENTED 2026-10-06 (glacier-ng/glacier-bootstrap,
tests/bootstrap_alpine.sh; see section 10 for how it differs from the
plan below). Remote mode (-r) and the pinning helper are still a plan.
Section 9 lists the decisions still open.
glacier-bootstrap takes a template index and builds a chrootable
Everest system in a target directory, from either a local directory of
@@ -37,7 +38,8 @@ Sequence:
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)
6. doas syspkg --root TARGET -y TARGET's indexes
7. 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.
@@ -100,7 +102,6 @@ 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...
@@ -110,7 +111,9 @@ per package, so packages can carry pins:
- 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
and an empty package list are all hard errors. SYS_PROFILE is an
unknown key (9b): nothing could check it until manifests record a
profile (ROADMAP 3.3). 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
@@ -131,8 +134,10 @@ 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."
at that version or later (decided 2026-10-06: the ROADMAP 2.3 rule, so
the template check and syspkg's commit check agree). Otherwise error:
"base::busybox requires musl >= 1.2.5, which the template does not
list." / "..., but the template lists 1.2.4."
- 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.
@@ -242,14 +247,18 @@ independent of glacier-bootstrap itself:
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
3. [DONE 2026-10-05] 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
4. [DONE 2026-10-05, as -R/--root] 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
5. [DONE: ~/Projects/everest builds musl, busybox, linux and
everest-bootscripts, all base, with sidecars.] Packages: busybox
recipe (base), sbase rebuilt as base, every
package rebuilt with sidecars (6d-f).
6. glacier-bootstrap, local mode.
6. [DONE 2026-10-06: Everest's `everest.sh initramfs` now builds its
tree this way, and the result boots in QEMU.] 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.
@@ -281,8 +290,8 @@ orchestration over things that have to exist anyway.
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.
b. DECIDED 2026-10-06: NAME= plus one `PKG=name version [sha256:hex]`
per package; no SYS_PROFILE until manifests record one (section 3).
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
@@ -293,7 +302,58 @@ orchestration over things that have to exist anyway.
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.
e. DECIDED 2026-10-05: automatic closure, once there is a source to
resolve from (remote repos, ROADMAP 3.1). Until then a missing
dependency refuses; the resolver is written so 3.1 plugs in. The
index already records auto vs explicit installs (ROADMAP 2.3).
Section 4's template check still applies to bootstrap: a template
lists what it installs.
f. Pinning helper: part of glacier-bootstrap (--pin) or of gworld
(which already knows what it just built).
+--------------------------------------------+
| 10. As implemented (local mode, 2026-10-06)|
+--------------------------------------------+
glacier-bootstrap [-hvntV] -d PKGDIR TEMPLATE TARGET
Where it differs from, or adds to, sections 1-8:
- syspkg is the one on the caller's PATH (decided 2026-10-06),
resolved to an absolute path first and run through doas, or
directly when already root. -t runs `doas chroot`.
- Order: parse, then verify, then resolve. Manifests are read only
from VERIFIED copies: each PKGDIR/NAME-VER.gpkg is copied into a
private mktemp -d while being hashed, checked against its sidecar and
the pin, and given a sidecar written from that hash. syspkg is handed
the copies, so a pin binds syspkg too, and a file swapped in PKGDIR
later is never read. The copies are removed on every exit.
- TARGET must be absolute, not /, and pass gl_check_sysroot (checked
read-only up front). It must be fresh, or initialised by syspkg -n
with nothing installed: then -n is skipped, so a run declined at
syspkg's prompt can simply be repeated. TARGET with packages is
refused.
- syspkg's prompt is passed through (it's the review step). It exits 0
when declined, so afterwards TARGET's index is read (it's world-
readable) and every template package must be listed.
- Every problem in a phase is reported before failing. Exit status:
1 usage or target, 2 template, 3 verification, 4 resolution,
5 installation, 6 smoke test, 7 repositories.
- Repositories (2026-10-07; the user: bootstrap must leave a
semi-functional system, so it configures repositories, pacstrap
style). The configuration comes from a PACKAGE in the template
(Everest: everest-repos, shipping /etc/glacier/repos.conf and
keys/*.pub), so bootstrap still never writes into TARGET. After the
install check, if TARGET's repos.conf gives any repository a url,
it runs `syspkg -R TARGET -y`: syspkg reads TARGET's OWN keyring
under -R for repository operations (REPO_DESIGN section 6, "Keyrings
under -R"), fetches into TARGET/glacier/sys/repos, and so sets
TARGET's rollback floor at build time. A failed fetch: exit 7,
TARGET stays installed, no "Bootstrapped" line. No repos.conf, or
no url: `[!] No remote repositories are configured in TARGET.` and
exit 0. Copying the host's repos.conf (pacstrap's mirrorlist copy)
is left out for now (user, 2026-10-07).
- Its own lib/ is found from its realpath (package.path and ffi.load),
not the current directory; no root-ownership checks (unprivileged).
- Not implemented: -r (remote) and --pin (9f still open).
+16 -3
View File
@@ -116,8 +116,11 @@ 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:
Shape (ACCEPTED 2026-10-05):
- The library refuses a sysroot other than "/" for GL_SCOPE_USR
contexts. Only system scope may target another root, for every
caller, not just the CLIs (section 4).
- gl_context_t gains `char sysroot[PATH_MAX]` ("" == "/"; appended
at the end, matched in lib/glacier_cdef.lua and
tests/glacier_ffi.lua).
@@ -126,6 +129,12 @@ Proposed shape:
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.
AS BUILT (2026-10-05): the reverse, same effect. The context stores
INSTALL paths, used for every filesystem operation unchanged, and
gl_runtime_path() translates only where runtime meaning matters
(link targets, is_glacier_symlink, .displaced keys); gl_at() checks
a link's target during prune. A missed translation then lands in T,
never on the host.
- GL_BASE_LINKS_DEST resolves to sysroot + "/usr". Never the host's
/usr when a sysroot is set.
@@ -239,9 +248,13 @@ Consequences of /usr starting empty that the design has to own:
| 8. Milestones |
+--------------------------------------------+
1. sysroot in the library (section 3), plus the host-untouched test.
1. [DONE 2026-10-05] sysroot in the library (section 3), plus the
host-untouched test (glacier-ng/tests/sysroot_alpine.sh). Implemented
with INSTALL paths in the context and runtime translation where a
path's meaning matters; see CLAUDE.md "Install root / sysroot".
No CLI changes, testable through FFI alone.
2. syspkg --root (section 4), with the root-owned-target check.
2. [DONE 2026-10-05] syspkg --root (section 4), with the root-owned-target
check, as -R/--root (implies --strict). glacier-ng/tests/root_alpine.sh.
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
+24 -22
View File
@@ -1,8 +1,8 @@
include config.mk
.PHONY: all prepare log log_test rt pkg istoreutils make_conf dag install clean static libglacier.a install_static libglacier-ng sha256 gsha256sum libglacier_verify
.PHONY: all prepare log log_test pkg transaction istoreutils install clean static libglacier.a install_static libglacier-ng sha256 gsha256sum libglacier_verify gsign grepo
all: prepare log log_test rt pkg transaction istoreutils make_conf dag sha256 libglacier_verify gsha256sum
all: prepare log log_test pkg transaction istoreutils sha256 libglacier_verify gsha256sum gsign grepo
@echo "libglacier-ng has finished building"
static: prepare log_static pkg_static istoreutils_static libglacier.a
@@ -15,12 +15,14 @@ pkg_static:
istoreutils_static: pkg_static
$(CC) $(CFLAGS) -c src/istoreutils.c -o build/lib/tmp/istoreutils.o
$(CC) $(CFLAGS) -c src/deps.c -o build/lib/tmp/deps.o
libglacier.a:
ar rcs build/lib/libglacier.a \
build/lib/tmp/log.o \
build/lib/tmp/pkg.o \
build/lib/tmp/istoreutils.o
build/lib/tmp/istoreutils.o \
build/lib/tmp/deps.o
prepare:
mkdir -p build
@@ -38,19 +40,30 @@ log:
log_test:
$(CC) $(CFLAGS) -shared -fPIC src/log.c -o build/lib/shared/libglacier_log.so
rt:
$(CC) $(CFLAGS) src/runtime.c -c -o build/lib/tmp/runtime.o
sha256:
$(CC) $(CFLAGS) src/sha256.c -c -o build/lib/tmp/sha256.o
# Monocypher, vendored unmodified (src/monocypher/README): Ed25519 for
# sign.c.
MONOCYPHER = src/monocypher/monocypher.c src/monocypher/monocypher-ed25519.c
libglacier_verify:
$(CC) $(CFLAGS) -shared -fPIC src/sha256.c \
$(CC) $(CFLAGS) -shared -fPIC src/sha256.c src/sign.c $(MONOCYPHER) \
-o build/lib/shared/libglacier_verify.so
gsha256sum: prepare
$(CC) $(CFLAGS) src/gsha256sum.c src/sha256.c -o build/bin/gsha256sum
gsign: prepare
$(CC) $(CFLAGS) src/gsign.c src/sign.c src/log.c $(MONOCYPHER) -o build/bin/gsign
# grepo reads packages (manifest, hash) and signs the index: built
# from source like gsign, so it needs no rpath to the shared objects.
grepo: prepare
$(CC) $(CFLAGS) src/grepo.c src/sign.c src/sha256.c src/log.c src/pkg.c \
src/istoreutils.c src/deps.c src/transaction.c $(MONOCYPHER) \
-larchive -lconfig -o build/bin/grepo
transaction: log istoreutils
$(CC) $(CFLAGS) -shared -fPIC src/transaction.c \
-Lbuild/lib/shared -lglacier_istoreutils -lglacier_log \
@@ -58,7 +71,7 @@ transaction: log istoreutils
-o build/lib/shared/libglacier_transaction.so
istoreutils: pkg
$(CC) $(CFLAGS) -shared -fPIC src/istoreutils.c \
$(CC) $(CFLAGS) -shared -fPIC src/istoreutils.c src/deps.c \
-Lbuild/lib/shared -lglacier_log -lglacier_pkg -larchive \
-Wl,-rpath,'$$ORIGIN' \
-o build/lib/shared/libglacier_istoreutils.so
@@ -69,21 +82,12 @@ pkg:
-Wl,-rpath,'$$ORIGIN' \
-o build/lib/shared/libglacier_pkg.so
make_conf:
$(CC) $(CFLAGS) src/make_conf.c -c -o build/lib/tmp/make_conf.o
libglacier-ng: prepare
$(CC) $(CFLAGS) -shared -fPIC \
src/log.c src/pkg.c src/transaction.c src/istoreutils.c \
src/log.c src/pkg.c src/transaction.c src/istoreutils.c src/deps.c \
-larchive -lconfig \
-o build/lib/shared/libglacier-ng.so
dag:
$(CC) $(CFLAGS) -shared -fPIC src/dag.c \
-Lbuild/lib/shared -lglacier_log \
-Wl,-rpath,'$$ORIGIN' \
-o build/lib/shared/libglacier_dag.so
install:
mkdir -p $(PREFIX)/lib/glacier/
install build/lib/shared/libglacier_log.so $(PREFIX)/lib/glacier/ -m 755
@@ -92,14 +96,12 @@ install:
install build/lib/shared/libglacier_pkg.so $(PREFIX)/lib/glacier -m 755
install build/lib/shared/libglacier_verify.so $(PREFIX)/lib/glacier -m 755
install build/bin/gsha256sum $(PREFIX)/bin/ -m 755
install build/bin/gsign $(PREFIX)/bin/ -m 755
install build/bin/grepo $(PREFIX)/bin/ -m 755
install_static:
mkdir -p $(PREFIX)/lib/glacier
install build/lib/libglacier.a $(PREFIX)/lib/glacier/ -m 644
install_test_dir:
install build/lib/shared/lg_log.so $(TEST_DIR)
install build/lib/shared/
clean:
rm -rf build
+7
View File
@@ -35,6 +35,13 @@ SYS (GL_SCOPE_SYS)
System-wide packages, not owned by any particular uid:
/glacier/sys/{index,store,links,stage,old,lock}
Symlink destination depends on the package's repo (see section 3).
Root-owned and world-READABLE, like /usr: index/store/links, the
store's repo/name/version dirs and implicit parent dirs are 0755,
index files 0644 (gl_scope_dir_mode / gl_scope_file_mode). /usr's
links point into the store, so every user must be able to traverse
it, and user-scope dependency checks read the system index. The
modes are applied explicitly, not left to syspkg's umask. The lock
stays 0600; .displaced, .etc-state and backup/ stay 0700.
Requires root. gpkg enforces this via geteuid() before it will
even attempt a system-scope operation.
+446
View File
@@ -0,0 +1,446 @@
+--------------------------------------------+
| Remote repositories and signing |
+--------------------------------------------+
STATUS: DESIGN, 2026-10-06. ROADMAP 3.1. Steps 1 (Ed25519, signify
formats, gsign), 2 (keyring, the local base rule, gbuild -k,
bootstrap carrying .sig, Everest's key) and 3 (grepo, the index,
refresh with -y) of section 9 DONE 2026-10-06, step 4 (-f, -u,
dependency closure, the cache) DONE 2026-10-07; sections 7, 5 and 6
say how they came out. Step 5 next. See CLAUDE.md.
Sections 1 and 10 are decided; the only open item is 10a's TLS library.
Adds AUTHENTICITY to glacier: SECURITY_DESIGN.txt section 1 deferred it
to "a future phase, tied to remote-repo support". This is that phase.
It also unblocks gpkg/syspkg -f (ROADMAP 2.2), -u (2.1), automatic
dependency closure (2.3) and glacier-bootstrap -r (GLACIER_BOOTSTRAP
section 5), and closes Finding 1 of SECURITY_REVIEW_2026-07-16.md (a
manifest that says PKG_REPO = "base" gets linked into /usr).
Companion documents: SECURITY_DESIGN.txt (integrity, refusal matrix),
PACKAGE_SCOPES.txt (repo routing), GLACIER_BOOTSTRAP.txt (pins).
+--------------------------------------------+
| 1. Decisions (the user, 2026-10-06) |
+--------------------------------------------+
a. Signatures: Ed25519, vendored (Monocypher's Ed25519 variant, which
brings its own SHA-512), compiled into the private
libglacier_verify.so next to sha256. Key and signature files are
signify-compatible, so OpenBSD's signify can make and check them
too. No new base dependency.
b. What is signed: one INDEX per repository (repo.idx + repo.idx.sig),
listing every package it serves with its sha256. Packages are
verified through the hash in the signed index, i.e. through the
existing sha256 path. The index has a serial; a client refuses an
index older than the last one it accepted (rollback).
c. Local base packages: once signing exists, `syspkg -l FILE` of a
package whose manifest says base requires a signature by a key
trusted for base. extra/community local installs stay
sidecar-only (integrity). Manual installation keeps working: a
builder makes a key and root adds it to the keyring.
d. Users may add their own repositories and keys, for their own scope
only (gpkg). Never read by syspkg.
+--------------------------------------------+
| 2. Keys and signatures (proposal) |
+--------------------------------------------+
signify's formats, byte for byte (two lines: "untrusted comment: ..."
then base64):
public key "Ed" | keynum[8] | pubkey[32]
signature "Ed" | keynum[8] | sig[64]
secret key "Ed" | "BK" | rounds[4] | salt[16] | checksum[8] |
keynum[8] | seckey[64]
- keynum is a random 8-byte key id; a signature names the key that
made it, so verification picks the key by keynum and never tries
every key in the keyring.
- Signing is pure Ed25519 over the WHOLE message, as signify does. So
we never sign a .gpkg directly (it would have to be read into memory
whole). What gets signed is small:
- a repository's repo.idx;
- a local package's SIDECAR (NAME-VER.gpkg.sha256, the one line
"sha256:<hex>"), as NAME-VER.gpkg.sha256.sig. Checking the
sidecar against the file is the existing path; the signature
makes the sidecar authentic. `signify -V -p KEY -m X.sha256`
checks the same file.
- Secret keys: glacier's tools read and write only UNENCRYPTED secret
keys (rounds = 0, what `signify -G -n` makes): passphrase support
needs bcrypt_pbkdf, which is more vendored crypto for a build-host
convenience. A passphrase-protected key works by signing with
signify itself (10b).
New tool `gsign` (C, built with gsha256sum; a build/maintainer tool,
later in "glacier-tools", never in base):
gsign -G -p KEY.pub -s KEY.sec [-c COMMENT] make a keypair
gsign -S -s KEY.sec -m FILE [-x SIGFILE] sign (default FILE.sig)
gsign -V -p KEY.pub -m FILE [-x SIGFILE] verify
gbuild gains `-k KEY.sec`: sign the sidecar it writes.
+--------------------------------------------+
| 3. Keyring and repository configuration |
+--------------------------------------------+
System (root-owned; syspkg applies its trust checks to these files and
every ancestor, like its own lib/):
/etc/glacier/keys/NAME.pub
/etc/glacier/repos.conf
repos.conf (libconfig, like glacier.conf):
repos = (
{ name = "base"; url = "https://pkg.example.org/base";
keys = [ "everest-2026" ]; },
{ name = "extra"; url = "https://pkg.example.org/extra";
keys = [ "everest-2026" ]; }
);
- A key is trusted ONLY for the repos that list it. That binding is
what makes "base" mean something: the base index must be signed by a
key listed for base, and a local base package's sidecar likewise.
Several keys per repo allow rotation (old and new side by side).
- Order = priority, when one name is served by several repos.
- name follows the gl_valid_pkg_ident rules. url is https:// or
file:// (local mirrors, tests); http:// is refused.
- url is optional: an entry without one only says which keys are
trusted for that repo, for a system installed and kept up by hand
from signed local files (section 7).
- Missing file = no remote repositories; -f/-u then refuse in one
line. No defaults, as with KEEP_OLD.
User (gpkg only):
~/.config/glacier/repos.conf ($XDG_CONFIG_HOME)
~/.config/glacier/keys/NAME.pub
- Merged AFTER the system list (lower priority).
- A user repo named "base", or named like a system repo, is refused.
- System keys are trusted for system repos only; a user's keys for the
user's repos only.
+--------------------------------------------+
| 4. The index |
+--------------------------------------------+
URL/repo.idx and URL/repo.idx.sig. libconfig, parsed only AFTER its
signature checks:
REPO = "base";
SERIAL = 2026100601L; # strictly increases with each publish
VALID_UNTIL = 1761955200L; # unix time; required (10c)
PKGS = (
{ NAME = "musl"; VER = "1.2.5"; FILE = "musl-1.2.5.gpkg";
SHA256 = "sha256:3a5f..."; SIZE = 812345L;
DEPS = (); CONFS = (); },
...
);
- REPO must equal the configured name the index was fetched for.
- Every version a repo serves is listed (so a pinned or older version
can still be fetched by -f NAME=VER later).
- DEPS/CONFS repeat the manifest's, so dependency closure can be
planned before downloading anything. After download, the manifest
must match the entry (NAME, VER, PKG_REPO = REPO, DEPS, CONFS):
a mismatch refuses the package. That is the Finding 1 rule for
remote packages: a package is "base" because the base index lists
it AND it says so, never because it says so.
- FILE is a bare file name (gl_valid_pkg_ident rules plus ".gpkg"):
no paths, no URLs.
New tool `grepo` (maintainer side, glacier-tools): `grepo -s KEY.sec
-r REPO DIR` reads every NAME-VER.gpkg in DIR (manifests, hashes),
writes DIR/repo.idx with SERIAL = the previous one + 1 (or the date
form), and signs it. The server is then any static file host.
+--------------------------------------------+
| 5. Client state and fetching |
+--------------------------------------------+
Inside each scope's base directory (one more entry beside index, store,
...; same ownership and modes as the rest):
<base>/repos/REPO.idx, REPO.idx.sig the last ACCEPTED index
<base>/cache/REPO/FILE downloaded packages
Refresh (every -f / -u; indexes are small; 10d):
1. curl --proto =https --proto-redir =https --fail (or a file:// copy)
URL/repo.idx and .sig into a temporary file in <base>/repos/.
2. Verify the signature with a key listed for that repo (by keynum).
3. Parse. REPO must match. SERIAL must be > the stored one, or equal
with identical bytes. VALID_UNTIL must be in the future.
4. rename() over the stored copy (never written in place: bug 25).
Any failure keeps the old index and refuses the operation in one line:
[x] Index for extra is older than the one already accepted.
Fetch: download FILE into the cache under a temporary name, hash it
while downloading, rename into place only if the hash matches the
index, then write the sidecar FROM THE INDEX and hand the cached file
to the existing install path (which re-hashes it, as today). The cache
is a download optimization, not a trusted store: a cache hit is hashed
again.
As implemented (2026-10-06, step 3; the user chose a refresh flag,
the date-form SERIAL and grepo in C):
- grepo is C (src/grepo.c, libglacier-ng; built from source like
gsign, installed to bin; not in base): `grepo -s KEY.sec -r REPO
[-n SERIAL] [-e DAYS] DIR`. SERIAL = max(previous + 1, today (UTC)
* 100 + 1), e.g. 2026100601; -n must still be above the previous
one. VALID_UNTIL = now + DAYS (default 30, 1..3650). Packages are
sorted by file name; each must be a regular file, its manifest must
say REPO and its name must be NAME-VER.gpkg (VER as the store names
versions, "%d.%d.%d"). An existing DIR/repo.idx must be a
well-formed index of REPO (its SERIAL is the previous one). It is
written to a mkstemp file, signed, and both renamed into place, the
.sig first.
- The index format is section 4's, with DEPS and CONFS as lists of
{ NAME = "x"; VER = "1.0.0"; } (the manifest's PKG_DEPS shape).
gl_index_parse (pkg.c) takes the BYTES the signature was checked
over, never a path, and is strict: exactly REPO, SERIAL > 0,
VALID_UNTIL > 0, PKGS; each package exactly NAME, VER, FILE, SHA256,
SIZE >= 0, DEPS, CONFS; FILE must be exactly "NAME-VER.gpkg"; SHA256
"sha256:" + 64 lowercase hex; no NAME + VER twice. A NUL or an '@'
anywhere is refused before libconfig sees it ('@include').
GL_INDEX_ERR_REPO when REPO is not the repo it was fetched for.
- `syspkg -y` / `gpkg -y` (--refresh) refresh every repos.conf entry
with a url and stop (decision 10d still holds for -f/-u). gpkg
reads the SYSTEM repos.conf and keyring until step 6 (user repos),
and stores into the user's base. No repos.conf, or no entry with a
url: `[x] No remote repositories are configured.` (exit 1).
- <base>/repos is made by gl_scope_state_dir (transaction.c): the
base must be the scope's; repos is created with the scope's mode
(0755 root / 0700 uid) and refused unless it is a real directory
with exactly that owner and mode. Files are 0644 / 0600. -y holds
the scope's lock (gl_begin_transaction on a live context); under
-R the indexes go under the install root, checked with the install
root's OWN keyring (DIR/etc/glacier; changed 2026-10-07, see
"Keyrings under -R" below).
- file:// is read directly (absolute paths only); https:// goes
through `curl -sf --proto =https --proto-redir =https`. 64 MiB at
most (GL_SIGN_MAX_MSG). Every repo is tried; exit 1 if any failed.
- One line per repo:
[✓] Index for base: serial 2026100601, 11 packages.
[i] Index for base is up to date: serial 2026100601.
[x] Cannot fetch the index for base. (a missing .sig too)
[x] Index for base is not signed by a key trusted for base.
[x] Malformed index for base.
[x] Index fetched for base is the index of another repository.
[x] Index for base is older than the one already accepted.
[x] Index for base has the serial of the one already accepted, but other contents.
[x] Index for base has expired.
[x] Malformed /glacier/sys/repos/base.idx. (left for the admin)
- The accepted copy is trusted by where it lives (root's or the
user's private repos dir): it is parsed for its SERIAL, not
re-verified.
- Test: glacier-ng/tests/repo_alpine.sh (103 checks; 18 mutants, 12 C
and 6 Lua, all caught).
+--------------------------------------------+
| 6. -f, -u and dependency closure |
+--------------------------------------------+
-f NAME... (ROADMAP 2.2): like -l, with the file found by name in the
accepted indexes (highest version, first repo by priority). Missing
dependencies are added automatically (ROADMAP 2.3's closure): the
highest version satisfying the minimum, recorded `auto`. The
summary lists them, marked as dependencies. Conflicts and every
2.3 check run as today, before the prompt.
-u (ROADMAP 2.1): every installed package whose repo is configured and
whose index has a higher version is replaced, as -lO does (backups,
KEEP_OLD). Packages from repos not configured are left alone.
`-u NAME...` limits it to those names.
syspkg -f/-u fetch only from system repos; gpkg -f/-u from system and
the user's repos. Base is still refused in user scope (library rule).
As implemented (2026-10-07, step 4; M.fetch / M.upgrade in
glacier-ng/lib/gpkg_common.lua). The user decided three details:
- Which repo: the FIRST repo by repos.conf order that serves NAME, at
its highest version. A lower-priority repo never wins by publishing
a bigger version. -u stays within the installed package's repo.
- A dependency installed at too old a version is UPGRADED in the same
transaction from its own repo (backed up per KEEP_OLD, like -lO),
keeping its auto/explicit mark; the summary shows
"old -> new (dependency)".
- Cache: downloads stay in <base>/cache/REPO while a run is declined
or refused; the files a commit installed are deleted after it.
The rest:
- Every -f/-u runs -y's refresh first; if any repo fails, the
operation is refused (decision 10d). The accepted indexes are then
read from <base>/repos (gl_index_parse again; trusted by location).
- Closure: for each planned package, each DEPS entry of its INDEX
entry is met by a planned package, an installed one at >= VER
(this scope; for a user, the system's index too), else upgraded (an
installed one too old) or added (pick(): first repo serving it,
recorded auto). What can't be met is left to gl_check_plan, which
prints the 2.3 refusal. The summary marks additions
"(dependency)".
- Download: gl_scope_cache_dir (transaction.c) makes and checks
<base>/cache and <base>/cache/REPO as gl_scope_state_dir checks
repos (owner, exact mode, not a symlink). A cached file whose hash
matches the index is reused; otherwise file:// is read, https://
goes through `curl -sf --proto =https --proto-redir =https` on a
pipe; at most SIZE + 1 bytes are ever read, hashed on the way, into
a dot-named temporary, renamed into place only if size and hash
match. 0644 / 0600.
- No sidecar is written: the index's sha256 is handed to
verify_and_copy directly (its `vouched` argument), which re-hashes
while copying into the private dir, as for -l. A base package needs
no .sig of its own: the base index vouches for it.
- The copy's manifest (library-read) must say exactly what the index
entry says: NAME, VER, PKG_REPO = the repo, DEPS and CONFS (same
order, "NAME MAJ.MIN.PAT"). Else, and for a bad hash or size:
[x] extra::evil 1.0.0 does not match its index.
and the cached file is deleted. Nothing to download:
[x] Cannot fetch extra::foo 1.0.0.
No repo serves a name:
[x] foo is not in any repository.
- -f of a name installed at another version needs -O (as -l);
dependency upgrades and -u don't (meta.may_replace). -O still also
means displacing foreign files.
- -u [NAME...]: a named package not installed, or whose repo has no
url, is refused (`[x] foo is not installed.` / `[x] community::foo
is not in any configured repository.`); plain -u skips the latter.
Nothing newer: `[✓] Nothing to do.`, exit 0.
- gpkg: user scope never fetches from base (base indexes are not even
considered, so `[x] core is not in any repository.`); the system's
repos.conf until step 6; indexes and cache in the user's base.
- syspkg -R DIR -f/-u: indexes and cache under DIR, DIR's keyring.
Keyrings under -R (decided 2026-10-07, for glacier-bootstrap
configuring the system's repositories): -y, -f and -u read
DIR/etc/glacier/repos.conf and DIR/etc/glacier/keys, with the same
trust checks, plus: each file's real path must lie inside DIR
(`[x] Refusing P: it resolves outside DIR.`), so a symlink in DIR
can't make the host's file stand in for DIR's. The indexes are DIR's
state and DIR checks them with its own keys from then on, and a host
with no glacier configuration can still bootstrap. A local `-R -l`
base package's .sig is still checked with the HOST's keyring (section
7). Tests: repo_alpine.sh / fetch_alpine.sh give the image its own
keyring; bootstrap_alpine.sh sections 14-16.
- Test: glacier-ng/tests/fetch_alpine.sh (84 checks; 16 Lua mutants
and 1 C mutant, all caught except the download-level hash check,
which is EQUIVALENT: the private copy's re-hash refuses the same
package and the mismatch path deletes the cached file. Kept as
defence in depth: a bad download never sits in the cache under its
final name).
- Known gap: a run killed mid-download leaves its dot-named temporary
in the cache (never read; the next fetch writes its own).
+--------------------------------------------+
| 7. Local installs (decision 1c) |
+--------------------------------------------+
syspkg -l FILE, manifest says base:
FILE.sha256 must match (as today) AND FILE.sha256.sig must verify
with a key listed for base in repos.conf. Otherwise:
[x] base::musl is not signed by a key trusted for base.
syspkg -l, other repos; gpkg -l: unchanged (sidecar only).
glacier-bootstrap -d: carries each package's .sig into its private
copies, so syspkg -R checks it. The keyring checked is the HOST's
/etc/glacier (the build host's trust, as KEEP_OLD is today).
Consequence for ~/Projects/everest: it needs a key; container.sh signs
every base package (gbuild -k); the build host's /etc/glacier/keys and
repos.conf list it. The image ships the PUBLIC key and a repos.conf so
the installed system trusts its own base (since 2026-10-07 in the
everest-repos package, split out of everest-bootscripts).
As implemented (2026-10-06, step 2):
- The repo is the one the LIBRARY reads from the verified copy
(gl_read_pkg_manifest, as gl_install_pkg), never gpkg_common's
pattern-matching read_manifest_fields: a manifest comment
`# PKG_REPO = "extra"` would make that one say extra.
- The sidecar is read ONCE; those bytes are both what the copy's hash
is checked against and what the signature is checked over. So
glacier-bootstrap copies the sidecar's bytes as read (it used to
write a new one from the hash) next to the .sig.
- The key is the one, among those listed for base, whose keynum the
signature names. Any other problem with the signature (missing,
malformed, other bytes, other key) is the one-line refusal above,
before the prompt; one refused package refuses the batch.
- repos.conf is read by gl_conf_repo_keys (pkg.c), strictly: `repos`
is the only setting; each entry has name, keys and optionally url,
nothing else; names and key names follow gl_valid_pkg_ident; a
duplicate name, or a url other than https://X or file://X, is
refused. Any of that: `[x] Malformed /etc/glacier/repos.conf.`
- repos.conf and each listed key, every ancestor of them and of their
real paths, must be root-owned and not group/world-writable:
`[x] Refusing F: P is not root-owned, or is group/world-writable.`
A listed key that is missing (`Cannot read F.`) or not a public key
(`Malformed F.`) stops the run: the admin's configuration is at
fault, not the package. Read only if some package is base.
- gbuild -k KEY.sec signs the sidecar (gsign from $GSIGN_BIN,
build/bin, or PATH), trying the key before building; a build
without -k removes a stale .sig. tests/mkpkg -k likewise.
- Test images (Dockerfile.*) carry a TEST key, glacier-test, listed for
base, its secret at /root/glacier-test.sec. tests/keyring_alpine.sh
covers this section (17 mutants, all caught).
- KEEP_OLD backups are re-packed from the store, so a base backup has
no signature: `syspkg -lO` refuses it until the restore interface
exists (decision 10f).
+--------------------------------------------+
| 8. Where the checks live |
+--------------------------------------------+
Signature verification is C (libglacier_verify.so). The POLICY (which
key for which repo, the base rule, rollback state) lives in the CLIs'
shared module, like hash verification today, so a third-party caller of
gl_install_pkg still isn't checked. ROADMAP 3.2 (move verification into
the library) now covers signatures too.
+--------------------------------------------+
| 9. Order of work |
+--------------------------------------------+
1. Vendored Ed25519 + signify formats in libglacier_verify.so; gsign;
tests including interop with signify (Alpine's `signify` package)
and hostile inputs (bad base64, wrong keynum, truncated files).
2. Keyring + repos.conf + the local base rule (section 7); gbuild -k;
glacier-bootstrap carries .sig; Everest gets a key.
3. Index: grepo; refresh, verify, rollback state. DONE 2026-10-06
(section 5, "As implemented"; syspkg/gpkg -y).
4. -f with closure; then -u. DONE 2026-10-07 (section 6, "As
implemented").
5. glacier-bootstrap -r against an index (pins stay optional extra
assurance).
6. User repos.
Each step is tested in Docker (file:// repos need no network) and
mutation-checked, as usual.
+--------------------------------------------+
| 10. Decisions on the details (2026-10-06) |
+--------------------------------------------+
a. Fetching on Everest: curl plus one TLS library in base. Signatures
make packages authentic regardless; TLS also hides WHAT is fetched
and stops a network attacker serving a stale (still valid) index.
STILL OPEN: which TLS library (BearSSL or LibreSSL); decide when
packaging it. Until then, everything is built and tested with
file:// repos and Alpine's curl.
b. No encrypted secret keys in gsign (no bcrypt_pbkdf). A
passphrase-protected key signs with signify itself.
c. VALID_UNTIL is REQUIRED; an expired index is refused. The
maintainer sets the lifetime; grepo defaults to 30 days.
d. Indexes are refreshed on every -f / -u. Offline, they refuse.
e. Everest's SECRET key lives outside the project tree:
~/.config/everest/keys/everest-2026.sec (EVEREST_KEYS), made by
`everest.sh keygen`, mounted read-only into the build container.
The public key ships in everest-bootscripts (/etc/glacier/keys).
f. Base backups (KEEP_OLD) are refused by `syspkg -lO` for want of a
signature; accepted for now. The restore interface (KEEP_OLD's
"later") can trust backups glacier made in its root-owned backup
dir.
+158 -15
View File
@@ -2,13 +2,14 @@
| Glacier roadmap |
+--------------------------------------------+
What's left to build, in priority order. Status as of 2026-10-04.
What's left to build, in priority order. Status as of 2026-10-07.
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
REPO_DESIGN.txt remote repositories, signing, keyring
GLACIER_BOOTSTRAP.txt glacier-bootstrap plan, prerequisites, decisions
Mark an item [DONE yyyy-mm-dd] rather than deleting it, so the history
@@ -23,7 +24,9 @@ file tracks work.
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
1.1 [DONE 2026-10-05: see CLAUDE.md "Install root / sysroot" and
tests/sysroot_alpine.sh.]
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
@@ -31,20 +34,58 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
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
1.2 [DONE 2026-10-05: -R/--root, strict by default under -R, --strict.]
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).
1.3 [DONE 2026-10-06: glacier-ng/glacier-bootstrap, GLACIER_BOOTSTRAP
section 10; Everest's initramfs is built with it and boots.]
glacier-bootstrap, local mode (GLACIER_BOOTSTRAP.txt sections 1-6).
Template index -> resolve -> verify -> syspkg --root, one
transaction. Never writes into TARGET itself.
[DONE 2026-10-07: repositories. TARGET's repos.conf + keys come
from a package (Everest: everest-repos); bootstrap then runs
syspkg -R TARGET -y, which reads TARGET's own keyring; exit 7 on
a failed fetch. GLACIER_BOOTSTRAP section 10. Later, maybe: an
opt-in copy of the host's repos.conf (pacstrap's mirrorlist).]
1.4 Packages for glacier itself, so an installed system can manage
1.4 [DONE 2026-10-06: built by ~/Projects/everest/scripts/container.sh,
in templates/minimal.template, boot-verified in QEMU; full
regression passes.]
Packages for glacier itself, so an installed system can manage
itself: luajit, libarchive (+ zlib), libconfig, libglacier-ng,
glacier-ng (syspkg, gpkg, gstore), doas.
Decided 2026-10-06:
- base: luajit, zlib, libarchive, libconfig, libglacier-ng,
glacier-ng. extra: doas (replaceable, e.g. by sudo).
- glacier-ng ships syspkg, gpkg and gstore only. glacier-bootstrap,
gbuild, gworld and gsha256sum become a later "glacier-tools"
package in extra (after the release, which brings a real
install Makefile).
- everest-bootscripts ships /etc/doas.conf: `permit :wheel`, plus
`permit nopass :wheel as root cmd /usr/bin/gstore args -n`.
- Layout: scripts + lib/ in usr/lib/glacier-ng/, usr/bin/{syspkg,
gpkg,gstore} symlink into it (realpath lands in the store, lib/
beside it); gpkg moves to the same real-path loading.
As built: zlib 1.3.2, libarchive 3.8.9 (gzip + tar only; every
other codec, crypto library and front-end disabled), libconfig
1.8.2 (no C++), luajit 2.1.1788856981 (commit c6ffc141),
libglacier-ng 4.0.0, glacier-ng 4.0.0, doas 6.8.2 (OpenDoas, no
PAM, no timestamp). The doas.conf rule is given for both `cmd
gstore` and `cmd /usr/bin/gstore`: OpenDoas matches what was typed.
luajit is linked -static-libgcc, since Everest ships no libgcc_s
(see section 6).
1.5 The installer. Undesigned; needs its own design session
1.5 [DEFERRED 2026-10-06 by the user: manual installation is one of
Everest's main features, so the installer comes later. 3.1 is
next.]
[2026-10-07, the user: a manual install works from a live Everest
system or any Linux host; the live system is the preferred,
officially supported way. The user is trying the first disk
install (QEMU) themselves.]
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.
@@ -72,16 +113,35 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
- 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.
waits for 3.1, like -f. [DONE 2026-10-07: REPO_DESIGN
section 6, "As implemented".]
- /etc follows the existing config rules regardless.
2.2 gpkg -f / syspkg -f: MERGE. Spec (2026-10-04): exactly like -l,
2.2 [DONE 2026-10-07: REPO_DESIGN section 6, "As implemented".]
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.
2.3 Dependencies. [DONE 2026-10-05; automatic closure DONE
2026-10-07 with -f/-u (REPO_DESIGN section 6).] Spec (2026-10-05), all three checks in one pass, no override flag,
every problem reported (one line each) before failing:
- A dependency's PKG_VER is a MINIMUM (installed >= PKG_VER).
Matched by name, any repo.
- A user package's dependency may be met by system scope or the
user's own scope. syspkg -x checks system-scope dependents
only.
- Conflicts: both directions (new lists installed, or installed
lists new), by name; PKG_VER is ignored.
- Missing dependencies REFUSE until 3.1; then automatic closure
(GLACIER_BOOTSTRAP 9e).
- Index records auto vs explicit now: <ver>/.glacier-auto in the
store, index line repo::name::version::auto|explicit.
- Messages:
extra::foo requires bar >= 1.2.0, which is not installed.
Removing base::musl breaks extra::foo.
extra::foo conflicts with extra::bar.
- install: refuse if a dependency isn't installed (or in the
same transaction);
- REMOVE: refuse to remove a package others depend on. Today
@@ -89,7 +149,7 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
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.
9e DECIDED 2026-10-05: automatic closure, after 3.1 (see above).
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.
@@ -98,7 +158,9 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
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
2.6 [DONE 2026-10-05: refused in every scope, before the prompt and at
commit (deps.c check_collisions).]
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:
@@ -121,7 +183,14 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
+--------------------------------------------+
3.1 Remote repositories + signing (authenticity, keyring). The agreed
next major item; separate design pass. Unblocks 2.2 and remote
next major item. Design: REPO_DESIGN.txt (2026-10-06; decisions in
sections 1 and 10, order of work section 9). [IN PROGRESS
2026-10-06: steps 1 and 2 DONE (gsign; the local base rule,
repos.conf + keyring, gbuild -k, bootstrap carries .sig, Everest
signs with everest-2026) and 3 (grepo; the signed index;
syspkg/gpkg -y refresh with rollback state) DONE; step 4 (-f with
closure, -u, the cache) DONE 2026-10-07; step 5 (glacier-
bootstrap -r) next, then 6 (user repos).] Unblocks remote
glacier-bootstrap. Until then, GLACIER_BOOTSTRAP's pinned
templates are the safe bridge for remote fetches.
@@ -145,6 +214,10 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
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).
[2026-10-07, the user: Everest will run under glibc as well as
musl, so profiles are a real requirement, not a nicety. The
libc is picked at install time (Gentoo/Void style), one per
system. Open: how packages are kept apart (section 6).]
4.2 Move ~/Projects/everest's per-package gbuild commands into gworld
recipes once 4.1 lands.
@@ -164,18 +237,88 @@ The critical path: from "QEMU boots an initramfs assembled by a script"
- 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).
- The installer (1.5), deferred.
- libgcc (1.4): luajit is built with -static-libgcc because Everest
has no libgcc_s. Claude's call, not yet reviewed; the alternative is
a GCC runtime package in base, which later C++/unwinding packages
would need anyway.
- glibc and musl (4.1): DECIDED 2026-10-07 that the user picks the
libc at install time, like Gentoo's profiles / Void's glibc and musl
ISOs; one libc per system. Still open: how packages are kept apart
(Claude recommends both per-libc repos, as Void, AND a libc field in
manifest + index that the library refuses on a mismatch with the
system's recorded libc).
+--------------------------------------------+
| 7. Done |
+--------------------------------------------+
[DONE 2026-10-07] -f and -u from repositories (3.1 step 4; 2.1's -u,
2.2, 2.3's closure): first repo serving a name wins; missing deps
added auto, too-old ones upgraded; downloads in <base>/cache/REPO,
hashed against the signed index, manifest must match the entry,
deleted after commit. tests/fetch_alpine.sh (84 checks, 17 mutants).
[DONE 2026-10-06] Packages for glacier itself (1.4): zlib, libarchive,
libconfig, luajit, libglacier-ng, glacier-ng in base, doas in extra;
everest-bootscripts 1.1.0 ships /etc/doas.conf. Everest's initramfs
installs all 11 packages; in QEMU, syspkg/gpkg run from the store,
syspkg -x musl is refused (dependents), a wheel user runs doas
gstore -n without a password, then gpkg -l. gpkg loads lib/ from its
realpath. tests/installed_alpine.sh (30 checks) installs glacier in
that layout and runs it from the store. Found on the way: gbuild's
clone warning broke builds from a commit hash (CLAUDE.md bug 28);
syspkg/gstore never checked the ancestors of libglacier-ng's store
dir, which the installed .so's live in (bug 29; both mutations
caught).
[DONE 2026-10-07] glacier-bootstrap configures repositories (1.3):
syspkg -R's -y/-f/-u use TARGET's own repos.conf and keys (each
file's real path must stay inside TARGET); bootstrap runs
syspkg -R T -y when T's repos.conf gives a url (exit 7 on failure,
a warning when none does). Everest: new base package everest-repos
1.0.0 (/etc/glacier/repos.conf + everest-2026.pub), moved out of
everest-bootscripts (now 1.3.0). bootstrap_alpine.sh 100 checks
(6 mutants, all caught); repo/fetch suites updated; full regression
green; Everest rebuilt and booted.
[DONE 2026-10-06] glacier-bootstrap, local mode (1.3): template
(NAME=, PKG=name ver [sha256:pin]; strict), verified private copies
handed to syspkg -R in one transaction, template-level dependency /
conflict / name checks, -n, -t. tests/bootstrap_alpine.sh (77
checks, 11 mutations caught). ~/Projects/everest's initramfs is now
a glacier-installed system (templates/minimal.template), booted in
QEMU: /glacier/sys, merged-/usr links into the store, /etc copied,
a non-root user runs busybox.
[DONE 2026-10-05] syspkg -R/--root DIR + --strict (1.2) and file
collisions (2.6): gl_check_sysroot (root-owned, non-symlink, not
group/world-writable, every ancestor), ctx->strict (a would-be link
refusal fails the transaction before the prompt and at commit; -O
still displaces), two packages shipping one path refused in every
scope. tests/root_alpine.sh (50 checks, 7 mutations caught). gpkg
silently ignored unknown flags (CLAUDE.md bug 27).
[DONE 2026-10-05] sysroot in the library (1.1): *_at contexts and
gl_init_sys_at, install paths in the context with runtime translation
at link/prune/displace, /usr and /etc under the sysroot, user scope
refuses one. tests/sysroot_alpine.sh (33 checks, 8 mutations caught).
Found on the way: live-context removal deleted foreign files
(CLAUDE.md bug 26).
[DONE 2026-10-05] Dependencies and conflicts (2.3): src/deps.c
(gl_check_plan before the prompt; gl_check_txn enforced by
gl_commit_transaction; live-context installs/removals checked inside
gl_install_pkg/gl_remove_pkg), index field 4 auto|explicit +
.glacier-auto + gl_set_pkg_auto, gl_read_pkg_manifest split out of
gl_install_pkg. tests/deps_alpine.sh (41 checks, 8 mutations caught).
Found on the way: the live index was rewritten through a seeded
hardlink (CLAUDE.md bug 25), tests/test_index_abort.lua.
[DONE 2026-10-05] System scope world-readable (found while specifying
2.3): /glacier/sys was root 0700 throughout, so non-root users could
run nothing syspkg installed. Dirs 0755 / index 0644, and created
modes no longer depend on the umask. tests/sys_perms_alpine.sh.
[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
+14
View File
@@ -27,6 +27,9 @@ it. This phase is INTEGRITY ONLY. Authenticity (signing, a trusted
keyring) is a deliberate future phase, tied to remote-repo support —
not being designed in detail now, but a few choices below are made to
stay cheap to extend into later.
[2026-10-06: that phase is REPO_DESIGN.txt. Its first part is in: a
base package installed with syspkg -l needs a signed sidecar
(REPO_DESIGN section 7).]
Everest is a security-focused distro (this is also *why* per-user
package isolation exists in the first place). Two sides of the CIA
@@ -209,6 +212,17 @@ refusing otherwise. The actual privilege boundary is enforced by doas
— a hardened, audited, purpose-built tool for exactly this — not
reimplemented inside syspkg itself.
LATER AMENDMENT (loading, as built): syspkg and gstore run under
`luajit -E`, and pin package.path and every ffi.load to their own
realpath's lib/. Before loading anything as root, each file they load
(every .lua, every .so, including those pulled in through $ORIGIN) and
lib/ itself must be root-owned and not group/world-writable, and so
must every ancestor of the script's realpath AND of each loaded file's
realpath. The second set matters once installed: lib/glacier is a
symlink to /usr/lib/glacier, whose entries link into libglacier-ng's
store directory, a tree the script's own ancestors never reach
(2026-10-06, CLAUDE.md bug 29).
+--------------------------------------------+
| 7. Shared code: gpkg_common.lua |
+6
View File
@@ -27,6 +27,12 @@ root, or shadows real `/usr` binaries."
* **Severity:** High
* **Category:** `authorization-bypass`
* **Confidence:** High
* **Status (2026-10-06):** closed for local installs by recommendation
(b): `syspkg -l` of a package whose manifest says base requires its
sidecar signed by a key `/etc/glacier/repos.conf` lists for base
(REPO_DESIGN.txt section 7); user scope refuses base outright. Remote
packages get (a) with the signed index (REPO_DESIGN section 4). Still
open for a third-party caller of `gl_install_pkg` (ROADMAP 3.2).
**Description:** System-scope symlink placement into `/usr` is gated only
by a package's manifest self-declaring `PKG_REPO="base"`. There is no
+62
View File
@@ -0,0 +1,62 @@
# Security review, 2026-10-06
Scope: every active source file in libglacier-ng (`src/*.c`, `src/*.h`,
not the vendored Monocypher, not the unbuilt early prototypes) and
glacier-ng (`gpkg`, `syspkg`, `gstore`, `glacier-bootstrap`, `gbuild`,
`gworld`, `lib/*.lua`, `tests/mkpkg`, the Dockerfiles). Read line by line;
findings confirmed by running them in Docker where possible; each fix
has a regression test, mutation-checked unless noted.
Tests: `glacier-ng/tests/audit_alpine.sh` (shell, `glacier-alpine_root`)
and `glacier-ng/tests/test_abort_unlocked.lua` (FFI suite).
## Findings and fixes
| # | Severity | Where | Finding | Fix | Test |
|---|---|---|---|---|---|
| 1 | **High** | `gl_install_pkg` | A package could ship top-level `files/.glacier-*` entries, glacier's own store files. As a symlink, `.glacier-fresh` made syspkg `open(O_CREAT\|O_TRUNC)` its target as root: an unsigned extra package truncated `/root/victim` to 0 bytes (reproduced). `.glacier-overwrite` granted the package `-O` without the user asking; `.glacier-manifest` rewrote its saved manifest. | Top-level names starting `.glacier-` are refused (`GL_MARKER_PREFIX`, common.h); the marker writes use `O_NOFOLLOW`. | audit_alpine: reserved names (mutants c3, c4 caught) |
| 2 | Medium | `gl_rmdir_recursive` (common.h) | `lstat` then `opendir` by path: root deleting a tree a user can write to (`gstore -x UID`) could be steered, by swapping a directory for a symlink between the two calls, into emptying the symlink's target. | fd-relative walk: `openat(O_DIRECTORY\|O_NOFOLLOW)`, `fdopendir`, `unlinkat`; the top path must not be a symlink. | audit_alpine: `gstore -x` race, 40 runs (mutant c1 caught 34-38/40) |
| 3 | Medium | `add_to_archive` (istoreutils.c) | Same class: `lstat` then `open`/`opendir` by path, no `O_NOFOLLOW`. Root backing up a user's store (`gstore -b`) could be steered into archiving root-only files. | fd-relative (`fstatat`/`openat(O_NOFOLLOW\|O_NONBLOCK)`/`readlinkat`); the opened descriptor's type is what gets archived. | **No failing-on-old test**: the window is too narrow for a shell race to hit (mutant c2 survived). Verified by review only. |
| 4 | Medium | `gl_gpm2gpkg` (pkg.c) | Manifests come from untrusted archives; libconfig honours `@include "FILE"`, so a manifest could make syspkg read any file as root. | A manifest with a line starting `@include` is refused (read into memory first, 1 MiB cap, no NUL). | audit_alpine (mutant c5 caught) |
| 5 | Low | `gl_gpm2gpkg` | A dependency/conflict entry without `PKG_NAME` ended the list early (later entries silently ignored, and leaked); names were not checked. | Every entry needs a `gl_valid_pkg_ident` name, else the manifest is refused. | audit_alpine (mutants c6, c7 caught) |
| 6 | Low | `gpkg_common.lua` | `read_manifest_fields` pattern-matched the manifest (via `tar` as root) and could disagree with the library: a decoy `// PKG_REPO = "community";` line hid that a package replaced an installed one, skipping the `-O` rule and the KEEP_OLD backup (two versions in the store). The tar extraction itself was not exploitable (GNU and busybox tar strip `..`; checked). | Replaced by `manifest_of`, a wrapper around `gl_read_pkg_manifest`; an unreadable manifest is refused before the prompt. | audit_alpine (old file as mutant l1, caught) |
| 7 | Low | `gstore`, `gpkg` | Text concatenated into `lg_printf` format strings: gstore's `argv[0]` and UID (as root), gpkg `-u`'s operand. | Passed as `%s` arguments; the `-u`/`-f`/`-s` stubs now refuse in one line. | audit_alpine (mutants l2, l3 caught) |
| 8 | Low | `gstore` | A UID above uid_t's range wrapped around in C to another UID. | Refused (`0..4294967294`). | audit_alpine (mutant l4 caught) |
| 9 | Low | `gl_abort_transaction` | Wiped the stage even for a context that never got the lock, i.e. another process's running transaction. | Only the lock holder's abort removes the stage (`GL_TXN_ERR_NO_TXN` otherwise). | test_abort_unlocked.lua (mutant f8 caught) |
| 10 | Low | `gpkg_common.lua` | Lua numbers passed to `lg_printf("%d")` (a double into varargs: undefined behaviour). | `%s` + `tostring`. | none (not observable) |
| 11 | Low | `gl_parse_semver` | `sscanf("%d")` on an untrusted, oversized number is undefined behaviour. | `strtol`, clamped to `0..INT_MAX`. | none (not observable) |
| 12 | Hardening | `is_glacier_symlink` | A plain prefix match: `/glacier/sys/store` also matched `/glacier/sys/store2/...` (only root can create such paths). | The prefix must be followed by `/` or end the target. | none |
| 13 | Bug | `join_with_cwd` (pkg.c) | `strlen(cwd + strlen(path) + 2)`: an out-of-bounds read. Unused. | Removed (see below). | n/a |
| 14 | Hardening | `lg_printf` | No format checking for C callers. | `__attribute__((format(printf, 2, 3)))`; the build is clean under `-Wformat=2 -Werror`. | the build |
Reviewed and found sound: signature code (`sign.c`: strict signify
parsing, secrets wiped, `O_NOFOLLOW|O_EXCL` writes), the index parser,
extraction's tar-slip defences (`is_safe_rel_path`, deferred symlinks,
`crosses_symlink`, `O_NOFOLLOW`), the sysroot trust check, syspkg's and
gstore's hardened loading, the private-copy verification, shell quoting
in every `os.execute`/`io.popen`, `/etc` config handling, `-O` displace
and restore.
## Removed as unused or redundant
- libglacier-ng: the `dag`, `runtime` and `make_conf` modules (nothing
linked them); `gl_pkgver`, `gl_mkdir`, `gl_spawn_pkg`, `gl_cat_dir`,
`gl_cat_dir_and_ver`, `gl_return_dir`, `gl_return_dir_and_ver`,
`gl_free_path`, `gl_free_dir`, `gl_show_pkg_props`, `join_with_cwd`,
`empty`/`null_dep`/`null_conf`, `dir_t`; `gl_parse_index`,
`gl_free_index` (and `struct gindex`), `gl_find_pkg_repo` (duplicate of
`gl_find_pkg_repos`), `gl_sys_istore_exists`, `gl_restore_istore`
(also unsafe: followed symlinks when restoring); `lg_printf_no_newline`,
`lg_syslog`, `lg_log` (its declaration and definition disagreed),
`p2s`; `GL_CONF_PATH`; the broken `install_test_dir` Makefile target.
The marker names, defined twice (deps.c, istoreutils.c), now live once
in common.h; log.c includes log.h instead of redefining its enum.
- glacier-ng: `M.read_manifest_fields`, `M.extract_pkg_info`,
`M.summary`; `install_with_reinstall` is module-local; the matching
`glacier_cdef.lua` declarations.
## Open
- Finding 3 has no test that fails on the old code (see the table).
- Extracted entries keep setuid/setgid bits (known gap, unchanged).
- No size limits on extraction (a decompression bomb fills the disk).
Binary file not shown.
-41
View File
@@ -1,41 +0,0 @@
#ifndef LOG_H_
#define LOG_H_
#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#define _XOPEN_SOURCE 700
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <syslog.h>
#include <time.h>
#include "color.h"
#ifdef __cplusplus
extern "C" {
#endif
typedef enum
{
LG_INFO,
LG_WARN,
LG_ERR,
LG_SUCCESS
}
loglevel_t;
int supports_utf8(void);
const char *p2s(int priority);
void lg_printf(loglevel_t loglevel, const char *fmt, ...);
void lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...);
void lg_syslog(int priority, const char *fmt, ...);
void lg_log(bool use_syslog, FILE *logfile, int priority, const char *fmt, ...);
#ifdef __cplusplus
}
#endif
#endif /* LOG_H_ */
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.
-1
View File
@@ -6,4 +6,3 @@ PREFIX ?= /usr
CFLAGS = -std=c11 -pedantic -O2 -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror -Wno-format-truncation
TEST_DIR = /home/lw/Projects/glacier-ng/lib
+149 -42
View File
@@ -6,6 +6,7 @@
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <linux/limits.h>
#include <stdio.h>
#include <string.h>
@@ -13,13 +14,6 @@
#include <sys/types.h>
#include <unistd.h>
typedef struct
{
const char *path;
mode_t mode;
}
dir_t;
/*
* On-disk layout. Each scope keeps everything it ever creates, renames
* or locks under ONE base directory it owns:
@@ -35,6 +29,21 @@ dir_t;
* an unprivileged user run their own transactions at all.
*/
#define GL_USR_ROOT "/glacier/usr"
/* In a package's version dir: "installed as a dependency, not by name".
* Dot-prefixed like the other store markers, so seeding carries it and
* linking skips it; the index's 4th field is derived from it. */
#define GL_AUTO_MARKER ".glacier-auto"
/* The other files glacier keeps in a version dir (see gl_install_pkg):
* the package's saved manifest, and the one-shot markers for the next
* link pass (this transaction installed it; it may displace, -O). A
* package may not ship any top-level name starting with GL_MARKER_PREFIX:
* these are glacier's, and one shipped as a symlink would redirect
* glacier's own writes. */
#define GL_MARKER_PREFIX ".glacier-"
#define GL_MANIFEST_COPY ".glacier-manifest"
#define GL_FRESH_MARKER ".glacier-fresh"
#define GL_OVERWRITE_MARKER ".glacier-overwrite"
#define GL_SYS_ROOT "/glacier/sys"
static inline void
@@ -78,6 +87,98 @@ gl_usr_base_check(uid_t uid)
return GL_USR_BASE_OK;
}
/* An install root (gl_context_t.sysroot) in canonical form: NULL, "" and
* "/" mean the running system and give ""; anything else must be an
* absolute path with no "." or ".." components, and loses its trailing
* slashes. Room is left for the paths built under it. Returns -1 if
* it's refused. */
static inline int
gl_normalize_sysroot(const char *in, char *out, size_t outsz)
{
out[0] = '\0';
if (!in || in[0] == '\0') { return 0; }
if (in[0] != '/') { return -1; }
size_t len = strlen(in);
while (len > 0 && in[len - 1] == '/') { len--; }
if (len == 0) { return 0; } /* "/" or "//" */
if (len + 256 > outsz || len + 256 > PATH_MAX) { return -1; }
for (size_t i = 0; i < len; ) {
size_t j = i + 1; /* in[i] == '/' */
while (j < len && in[j] != '/') { j++; }
size_t clen = j - i - 1;
const char *comp = in + i + 1;
if (clen == 0 || (clen == 1 && comp[0] == '.') ||
(clen == 2 && comp[0] == '.' && comp[1] == '.')) {
return -1;
}
i = j;
}
memcpy(out, in, len);
out[len] = '\0';
return 0;
}
/* Is a (normalized, non-empty) install root safe for root to write into?
* It and every directory above it must be a real directory (not a
* symlink), owned by root, and not group/world-writable: otherwise a
* user could swap part of the tree for a symlink to the host's /usr
* between two syscalls (IMAGE_DESIGN section 4). Returns 0 if trusted;
* otherwise -1, with the offending path in `bad` and `why` set to
* "a symlink", "not a directory", or "not root-owned, or is
* group/world-writable". */
static inline int
gl_sysroot_check_one(const char *path, char *bad, size_t badsz, const char **why)
{
struct stat st;
if (lstat(path, &st) != 0) {
*why = "missing";
} else if (S_ISLNK(st.st_mode)) {
*why = "a symlink";
} else if (!S_ISDIR(st.st_mode)) {
*why = "not a directory";
} else if (st.st_uid != 0 || (st.st_mode & 022) != 0) {
*why = "not root-owned, or is group/world-writable";
} else {
return 0;
}
snprintf(bad, badsz, "%s", path);
return -1;
}
static inline int
gl_sysroot_trusted(const char *root, char *bad, size_t badsz, const char **why)
{
if (gl_sysroot_check_one("/", bad, badsz, why) != 0) { return -1; }
char prefix[PATH_MAX];
snprintf(prefix, sizeof(prefix), "%s", root);
/* each ancestor, shortest first, then root itself */
for (char *p = prefix + 1; *p; p++) {
if (*p != '/') { continue; }
*p = '\0';
int rc = gl_sysroot_check_one(prefix, bad, badsz, why);
*p = '/';
if (rc != 0) { return -1; }
}
return gl_sysroot_check_one(prefix, bad, badsz, why);
}
/* mkdir -p. Every directory this creates gets exactly `mode`, whatever
* the caller's umask: a system store made under umask 077 would otherwise
* be unreachable for every user but root. Directories that already exist
* are left alone. */
static inline int
gl_mkdir_exact(const char *path, mode_t mode)
{
if (mkdir(path, mode) == 0) {
return chmod(path, mode);
}
return errno == EEXIST ? 0 : -1;
}
static inline int
gl_mkdirp(const char *path, mode_t mode)
{
@@ -88,52 +189,58 @@ gl_mkdirp(const char *path, mode_t mode)
for (char *p = tmp + 1; *p; p++) {
if (*p == '/') {
*p = '\0';
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
if (gl_mkdir_exact(tmp, mode) != 0) {
return -1;
}
*p = '/';
}
}
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
return -1;
}
return 0;
return gl_mkdir_exact(tmp, mode);
}
/* Empties the directory open at `fd` (and closes it). Every step is
* relative to a descriptor opened with O_NOFOLLOW, so nothing is ever
* resolved through a symlink: an entry swapped for a symlink mid-walk
* (root deleting a tree a user can write to, gstore -x) is unlinked
* itself, never followed into its target. */
static inline void
gl_empty_dir_fd(int fd)
{
DIR *dir = fdopendir(fd);
if (!dir) {
close(fd);
return;
}
struct dirent *ent;
while ((ent = readdir(dir))) {
/* Only . and ..: dotfiles are content too (a package's own,
* the store's .displaced tree). */
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
continue;
}
int sub = openat(dirfd(dir), ent->d_name,
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (sub >= 0) {
gl_empty_dir_fd(sub);
unlinkat(dirfd(dir), ent->d_name, AT_REMOVEDIR);
} else {
unlinkat(dirfd(dir), ent->d_name, 0);
}
}
closedir(dir);
}
/* rm -r of a directory. `path` itself must be a directory, not a
* symlink to one (refused, -1). */
static inline int
gl_rmdir_recursive(const char *path)
{
DIR *dir = opendir(path);
if (!dir) { return -1; }
struct dirent *ent;
while ((ent = readdir(dir))) {
/* 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);
struct stat st;
if (lstat(full, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
gl_rmdir_recursive(full);
}
else {
unlink(full);
}
}
closedir(dir);
return rmdir(path);
int fd = open(path, O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
if (fd < 0) { return -1; }
gl_empty_dir_fd(fd);
return rmdir(path);
}
-132
View File
@@ -1,132 +0,0 @@
#define _POSIX_C_SOURCE 200809L
#include <stdio.h>
#include <stdlib.h>
#include "dag.h"
graph_t
*gl_graph_create(void)
{
return calloc(1, sizeof(graph_t));
}
void
gl_graph_free(graph_t *g)
{
if (!g) { return; }
for (size_t i = 0; i < g->node_count; i++) {
free(g->nodes[i]->edges);
free(g->nodes[i]);
}
free(g->nodes);
free(g);
}
graph_node_t
*gl_graph_add_node(graph_t *g, void *data)
{
if (!g) { return NULL; }
graph_node_t *n = calloc(1, sizeof(graph_node_t));
if (!n) { return NULL; }
n->data = data;
graph_node_t **tmp = realloc(g->nodes, sizeof(*g->nodes) * (g->node_count + 1));
if (!tmp) {
free(n);
return NULL;
}
g->nodes = tmp;
g->nodes[g->node_count++] = n;
return n;
}
void
gl_graph_add_edge(graph_t *g, graph_node_t *from, graph_node_t *to)
{
if (!g || !from || !to) { return; }
graph_node_t **tmp = realloc(from->edges, sizeof(*from->edges) * (from->edge_count + 1));
if (!tmp) { return; }
from->edges = tmp;
from->edges[from->edge_count++] = to;
}
static bool
dfs_cycle(graph_node_t *n)
{
n->visited = true;
n->in_stack = true;
for (size_t i = 0; i < n->edge_count; i++) {
graph_node_t *child = n->edges[i];
if (!child->visited && dfs_cycle(child)) {
return true;
}
else if (child->in_stack) {
return true;
}
}
n->in_stack = false;
return false;
}
bool
gl_graph_has_cycle(graph_t *g)
{
if (!g) { return false; }
for (size_t i = 0; i < g->node_count; i++) {
g->nodes[i]->visited = false;
g->nodes[i]->in_stack = false;
}
for (size_t i = 0; i < g->node_count; i++) {
if (!g->nodes[i]->visited && dfs_cycle(g->nodes[i])) {
return true;
}
}
return false;
}
static void
dfs_toposort(graph_node_t *n, void **result, size_t *index)
{
n->visited = true;
for (size_t i = 0; i < n->edge_count; i++) {
if (!n->edges[i]->visited) {
dfs_toposort(n->edges[i], result, index);
}
}
result[(*index)++] = n->data;
}
void
**gl_graph_toposort(graph_t *g, size_t *out_count)
{
if (!g || !out_count) { return NULL; }
if (gl_graph_has_cycle(g)) {
fprintf(stderr, "Error: dependency cycle detected\n");
return NULL;
}
*out_count = g->node_count;
void **order = calloc(g->node_count, sizeof(void *));
if (!order) { return NULL; }
for (size_t i = 0; i < g->node_count; i++) {
g->nodes[i]->visited = false;
}
size_t index = 0;
for (size_t i = 0; i < g->node_count; i++) {
if (!g->nodes[i]->visited) {
dfs_toposort(g->nodes[i], order, (size_t *)&index);
}
}
return order;
}
-34
View File
@@ -1,34 +0,0 @@
#ifndef DAG_H_
#define DAG_H_
#define _POSIX_C_SOURCE 200809L
#include <stdbool.h>
#include <stddef.h>
typedef struct graph_node graph_node_t;
typedef struct graph graph_t;
struct graph_node {
void *data;
graph_node_t **edges;
size_t edge_count;
bool visited;
bool in_stack;
};
struct graph {
graph_node_t **nodes;
size_t node_count;
};
graph_t *graph_create(void);
void graph_free(graph_t *g);
graph_node_t *graph_add_node(graph_t *g, void *data);
void graph_add_edge(graph_t *g, graph_node_t *from, graph_node_t *to);
bool graph_has_cycle(graph_t *g);
void **graph_toposort(graph_t *g, size_t *out_count);
#endif
+727
View File
@@ -0,0 +1,727 @@
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <archive.h>
#include <archive_entry.h>
#include <dirent.h>
#include <errno.h>
#include <unistd.h>
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include "common.h"
#include "deps.h"
#include "istoreutils.h"
#include "log.h"
#include "pkg.h"
/* Where one entry of a package lands once linked: its RUNTIME path (what
* a message names and what two packages are compared by). */
struct dest {
char *path;
bool dir;
};
/* One installed (or about to be installed) package. repo/name/dirname
* come from the store layout (store/<repo>/<name>/<name>-<ver>), or from
* the manifest for a package that isn't installed yet. deps/confs come
* from the manifest; a version dir without one (installed before
* manifests were saved) has none. */
struct rec {
char repo[NAME_MAX + 1];
char name[NAME_MAX + 1];
char dirname[NAME_MAX + 1];
semver_t ver;
gl_pkg_t pkg;
bool touched; /* added or replaced by this transaction */
bool gone; /* removed or replaced: not in the result */
/* For the file checks: where its files come from (a version dir in
* a store, or a .gpkg not installed yet), whether its link pass is
* this transaction's (fresh: it reports refusals), whether that
* pass may displace foreign files (-O), and, once computed, where
* every entry lands. */
char src[PATH_MAX];
bool from_archive;
bool fresh;
bool overwrite;
struct dest *dests;
size_t ndests;
bool dests_loaded;
};
/* What the file checks need to know about the transaction's context. */
struct env {
gl_ctx_scope_t scope;
uid_t uid;
const char *sysroot;
int strict;
char store_rt[PATH_MAX]; /* live store, runtime path: link targets */
char store_at[PATH_MAX]; /* the store being checked, install path */
};
struct set {
struct rec *v;
size_t n;
};
/* A package the transaction removes outright (not replaced). */
struct removed {
char repo[NAME_MAX + 1];
char name[NAME_MAX + 1];
};
static void
set_free(struct set *s)
{
for (size_t i = 0; i < s->n; i++) {
gl_free_pkg(&s->v[i].pkg);
gl_free_semver(&s->v[i].ver);
for (size_t k = 0; k < s->v[i].ndests; k++) { free(s->v[i].dests[k].path); }
free(s->v[i].dests);
}
free(s->v);
s->v = NULL;
s->n = 0;
}
static struct rec *
set_add(struct set *s)
{
struct rec *grown = realloc(s->v, (s->n + 1) * sizeof(*s->v));
if (!grown) { return NULL; }
s->v = grown;
memset(&s->v[s->n], 0, sizeof(*s->v));
return &s->v[s->n++];
}
static int
semver_cmp(const semver_t *a, const semver_t *b)
{
if (a->maj != b->maj) { return a->maj < b->maj ? -1 : 1; }
if (a->min != b->min) { return a->min < b->min ? -1 : 1; }
if (a->pat != b->pat) { return a->pat < b->pat ? -1 : 1; }
return 0;
}
/* Every installed package in `store`. A missing store is empty (nothing
* installed); any other failure to read it is an error, unless `lenient`
* (the system store as seen from user scope: unreadable means "nothing
* there to depend on", and the check then says what's missing). */
static int
load_store(const char *store, struct set *s, bool lenient)
{
DIR *sd = opendir(store);
if (!sd) {
return (errno == ENOENT || lenient) ? 0 : -1;
}
int rc = 0;
struct dirent *re;
while (rc == 0 && (re = readdir(sd))) {
if (re->d_name[0] == '.') { continue; }
char repo_path[PATH_MAX];
snprintf(repo_path, sizeof(repo_path), "%s/%s", store, re->d_name);
DIR *rd = opendir(repo_path);
if (!rd) { continue; } /* not a directory */
struct dirent *ne;
while (rc == 0 && (ne = readdir(rd))) {
if (ne->d_name[0] == '.') { continue; }
char name_path[PATH_MAX];
snprintf(name_path, sizeof(name_path), "%s/%s", repo_path, ne->d_name);
DIR *nd = opendir(name_path);
if (!nd) { continue; }
struct dirent *ve;
while ((ve = readdir(nd))) {
if (ve->d_name[0] == '.') { continue; }
struct rec *r = set_add(s);
if (!r) { rc = -1; break; }
snprintf(r->repo, sizeof(r->repo), "%s", re->d_name);
snprintf(r->name, sizeof(r->name), "%s", ne->d_name);
snprintf(r->dirname, sizeof(r->dirname), "%s", ve->d_name);
snprintf(r->src, sizeof(r->src), "%s/%s", name_path, ve->d_name);
char manifest[PATH_MAX];
snprintf(manifest, sizeof(manifest), "%s/%s/" GL_MANIFEST_COPY,
name_path, ve->d_name);
if (gl_gpm2gpkg(manifest, &r->pkg) != 0) {
gl_free_pkg(&r->pkg);
memset(&r->pkg, 0, sizeof(r->pkg));
}
/* The version is the dir name minus "<name>-". */
size_t prefix = strlen(ne->d_name) + 1;
r->ver = gl_parse_semver(strlen(ve->d_name) > prefix
? ve->d_name + prefix : NULL);
break; /* one version per package */
}
closedir(nd);
}
closedir(rd);
}
closedir(sd);
return rc;
}
static const struct rec *
find_live(const struct set *s, const char *repo, const char *name)
{
for (size_t i = 0; i < s->n; i++) {
if (!s->v[i].gone && strcmp(s->v[i].repo, repo) == 0 &&
strcmp(s->v[i].name, name) == 0) {
return &s->v[i];
}
}
return NULL;
}
/* Is there a package named `name` (any repo) in the result or, from user
* scope, the system? With `min`, it must also be at least that version. */
static bool
provided(const struct set *res, const struct set *sys, const char *name,
const semver_t *min)
{
const struct set *sets[] = { res, sys };
for (int k = 0; k < 2; k++) {
for (size_t i = 0; i < sets[k]->n; i++) {
const struct rec *r = &sets[k]->v[i];
if (r->gone || strcmp(r->name, name) != 0) { continue; }
if (!min || semver_cmp(&r->ver, min) >= 0) { return true; }
}
}
return false;
}
static bool
lists_conflict(const struct rec *r, const char *name)
{
if (!r->pkg.pkg_conflicts) { return false; }
for (const conf_t *c = r->pkg.pkg_conflicts; c->conf_name; c++) {
if (strcmp(c->conf_name, name) == 0) { return true; }
}
return false;
}
/* Does the transaction touch packages named `name`: add or replace one,
* or remove one outright? */
static bool
name_touched(const struct set *res, const struct removed *rm, size_t nrm,
const char *name)
{
for (size_t i = 0; i < res->n; i++) {
if (res->v[i].touched && strcmp(res->v[i].name, name) == 0) { return true; }
}
for (size_t i = 0; i < nrm; i++) {
if (strcmp(rm[i].name, name) == 0) { return true; }
}
return false;
}
static void
fmt_ver(const semver_t *v, char *out, size_t outsz)
{
snprintf(out, outsz, "%d.%d.%d", v->maj, v->min, v->pat);
}
/*
* File checks (ROADMAP 2.6 and 1.2's --strict).
*
* Every entry of a package lands somewhere once linked; dest_of mirrors
* gl_link_pkg exactly: entries under a dot-prefixed name are never
* linked, a leading usr/ is stripped, base-repo packages in system scope
* land in /usr and everything else in the scope's links tree, and in
* system scope the top-level etc/ is copied into /etc.
*/
static bool
has_dot_component(const char *rel)
{
for (const char *c = rel; c; c = strchr(c, '/')) {
if (*c == '/') { c++; }
if (*c == '.') { return true; }
if (!*c) { break; }
}
return false;
}
static int
dest_of(const struct env *e, const char *repo, const char *rel, char *out, size_t outsz)
{
if (e->scope == GL_SCOPE_SYS &&
(strcmp(rel, "etc") == 0 || strncmp(rel, "etc/", 4) == 0)) {
return snprintf(out, outsz, "/%s", rel) >= (int)outsz ? -1 : 0;
}
if (strncmp(rel, "usr/", 4) == 0) { rel += 4; }
char base[PATH_MAX];
if (e->scope == GL_SCOPE_SYS) {
snprintf(base, sizeof(base), "%s", strcmp(repo, "base") == 0
? "/usr" : GL_SYS_ROOT "/links");
} else {
char ub[PATH_MAX];
gl_usr_base(e->uid, ub, sizeof(ub));
snprintf(base, sizeof(base), "%s/links", ub);
}
return snprintf(out, outsz, "%s/%s", base, rel) >= (int)outsz ? -1 : 0;
}
static int
add_dest(struct rec *r, const char *path, bool dir)
{
for (size_t k = 0; k < r->ndests; k++) {
if (strcmp(r->dests[k].path, path) == 0) {
if (!dir) { r->dests[k].dir = false; }
return 0;
}
}
struct dest *grown = realloc(r->dests, (r->ndests + 1) * sizeof(*grown));
if (!grown) { return -1; }
r->dests = grown;
r->dests[r->ndests].path = strdup(path);
if (!r->dests[r->ndests].path) { return -1; }
r->dests[r->ndests].dir = dir;
r->ndests++;
return 0;
}
/* `rel` (and, for an archive entry, its parents, which tar needn't list) */
static int
add_rel(const struct env *e, struct rec *r, const char *rel, bool dir, bool parents)
{
if (rel[0] == '\0' || has_dot_component(rel)) { return 0; }
char d[PATH_MAX];
if (parents) {
char buf[PATH_MAX];
snprintf(buf, sizeof(buf), "%s", rel);
for (char *p = buf; (p = strchr(p, '/')); p++) {
*p = '\0';
if (dest_of(e, r->repo, buf, d, sizeof(d)) != 0 || add_dest(r, d, true) != 0) {
return -1;
}
*p = '/';
}
}
if (dest_of(e, r->repo, rel, d, sizeof(d)) != 0) { return -1; }
return add_dest(r, d, dir);
}
static int
walk_store_dir(const struct env *e, struct rec *r, const char *dir, size_t strip)
{
DIR *d = opendir(dir);
if (!d) { return 0; }
int rc = 0;
struct dirent *ent;
while (rc == 0 && (ent = readdir(d))) {
if (ent->d_name[0] == '.') { continue; }
char full[PATH_MAX];
snprintf(full, sizeof(full), "%s/%s", dir, ent->d_name);
struct stat st;
if (lstat(full, &st) != 0) { continue; }
const char *rel = full + strip;
if (S_ISDIR(st.st_mode)) {
rc = add_rel(e, r, rel, true, false);
if (rc == 0) { rc = walk_store_dir(e, r, full, strip); }
} else if (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode)) {
rc = add_rel(e, r, rel, false, false);
}
}
closedir(d);
return rc;
}
static int
walk_archive(const struct env *e, struct rec *r)
{
char prefix[PATH_MAX];
snprintf(prefix, sizeof(prefix), "%s-%d.%d.%d/files/", r->name,
r->pkg.pkg_ver.maj, r->pkg.pkg_ver.min, r->pkg.pkg_ver.pat);
size_t plen = strlen(prefix);
struct archive *a = archive_read_new();
archive_read_support_filter_gzip(a);
archive_read_support_format_tar(a);
if (archive_read_open_filename(a, r->src, 10240) != ARCHIVE_OK) {
archive_read_free(a);
return -1;
}
int rc = 0;
struct archive_entry *entry;
while (rc == 0 && archive_read_next_header(a, &entry) == ARCHIVE_OK) {
const char *path = archive_entry_pathname(entry);
if (strncmp(path, prefix, plen) == 0) {
char rel[PATH_MAX];
snprintf(rel, sizeof(rel), "%s", path + plen);
size_t len = strlen(rel);
while (len > 0 && rel[len - 1] == '/') { rel[--len] = '\0'; }
mode_t type = archive_entry_filetype(entry);
if (type == AE_IFDIR) {
rc = add_rel(e, r, rel, true, true);
} else if (type == AE_IFREG || type == AE_IFLNK) {
rc = add_rel(e, r, rel, false, true);
}
}
archive_read_data_skip(a);
}
archive_read_free(a);
return rc;
}
static int
load_dests(const struct env *e, struct rec *r)
{
if (r->dests_loaded) { return 0; }
r->dests_loaded = true;
if (r->from_archive) { return walk_archive(e, r); }
return walk_store_dir(e, r, r->src, strlen(r->src) + 1);
}
struct item {
const char *path;
bool dir;
size_t rec;
};
static int
item_cmp(const void *a, const void *b)
{
const struct item *x = a, *y = b;
int c = strcmp(x->path, y->path);
if (c != 0) { return c; }
return x->rec < y->rec ? -1 : x->rec > y->rec;
}
/* Two packages shipping the same path (ROADMAP 2.6): whichever linked
* last used to win silently. Refused when either package is part of
* this transaction; at least one of the two entries must be a file or
* symlink (sharing a directory is normal). */
static int
check_collisions(const struct env *e, struct set *res)
{
size_t n = 0;
for (size_t i = 0; i < res->n; i++) {
if (res->v[i].gone) { continue; }
if (load_dests(e, &res->v[i]) != 0) { return -1; }
n += res->v[i].ndests;
}
if (n == 0) { return 0; }
struct item *items = malloc(n * sizeof(*items));
if (!items) { return -1; }
size_t k = 0;
for (size_t i = 0; i < res->n; i++) {
if (res->v[i].gone) { continue; }
for (size_t j = 0; j < res->v[i].ndests; j++) {
items[k++] = (struct item){ res->v[i].dests[j].path, res->v[i].dests[j].dir, i };
}
}
qsort(items, n, sizeof(*items), item_cmp);
int problems = 0;
for (size_t start = 0; start < n; ) {
size_t end = start + 1;
while (end < n && strcmp(items[end].path, items[start].path) == 0) { end++; }
for (size_t a = start; a < end; a++) {
const struct rec *ra = &res->v[items[a].rec];
if (!ra->touched && !ra->fresh) { continue; }
for (size_t b = start; b < end; b++) {
if (b == a || (items[a].dir && items[b].dir)) { continue; }
const struct rec *rb = &res->v[items[b].rec];
/* two of this transaction's packages: report once */
if ((rb->touched || rb->fresh) && b < a) { continue; }
lg_printf(2, "%s::%s and %s::%s both ship %s.",
ra->repo, ra->name, rb->repo, rb->name, items[a].path);
problems++;
}
}
start = end;
}
free(items);
return problems;
}
/* --strict: what the post-commit link pass would refuse, refused now
* instead, before anything is committed. Same rules as gl_link_pkg, for
* this transaction's packages only (the ones whose refusals it reports).
* User scope never has a foreign file in its links tree (rebuilt on
* every commit), and config in /etc is offered as .new, never refused. */
static int
check_links(const struct env *e, struct set *res)
{
if (!e->strict || e->scope != GL_SCOPE_SYS) { return 0; }
char store_prefix[PATH_MAX];
snprintf(store_prefix, sizeof(store_prefix), "%s/", e->store_rt);
size_t splen = strlen(store_prefix);
int problems = 0;
for (size_t i = 0; i < res->n; i++) {
struct rec *r = &res->v[i];
if (r->gone || !(r->touched || r->fresh)) { continue; }
if (load_dests(e, r) != 0) { return -1; }
for (size_t k = 0; k < r->ndests; k++) {
const struct dest *d = &r->dests[k];
if (strcmp(d->path, "/etc") == 0 || strncmp(d->path, "/etc/", 5) == 0) { continue; }
char at[PATH_MAX];
snprintf(at, sizeof(at), "%s%s", e->sysroot, d->path);
struct stat st;
if (lstat(at, &st) != 0) {
if (errno == ENOTDIR) {
lg_printf(2, "Refusing to overwrite non-glacier path: %s", at);
problems++;
}
continue;
}
bool ours = false;
if (S_ISLNK(st.st_mode)) {
char target[PATH_MAX];
ssize_t tn = readlink(at, target, sizeof(target) - 1);
ours = tn >= 0 && (size_t)tn >= splen &&
strncmp(target, store_prefix, splen) == 0;
}
if (d->dir) {
/* a foreign symlink here (usr/sbin -> bin) is followed,
* like gl_link_pkg does; one of glacier's isn't */
if (S_ISDIR(st.st_mode) || (S_ISLNK(st.st_mode) && !ours)) { continue; }
if (ours) {
lg_printf(2, "Refusing to merge directory %s into another package's symlink", at);
} else {
lg_printf(2, "Refusing to overwrite non-glacier path: %s", at);
}
problems++;
continue;
}
if (ours) { continue; }
if (r->overwrite && (S_ISREG(st.st_mode) || S_ISLNK(st.st_mode))) {
char kept[PATH_MAX];
snprintf(kept, sizeof(kept), "%s/.displaced%s", e->store_at, d->path);
struct stat kst;
if (lstat(kept, &kst) != 0) { continue; } /* -O moves it aside */
}
lg_printf(2, "Refusing to overwrite non-glacier path: %s", at);
problems++;
}
}
return problems;
}
/* The checks themselves, on the result of a transaction. */
static int
check(const struct env *e, struct set *res, const struct set *sys,
const struct removed *rm, size_t nrm)
{
int problems = 0;
char ver[64];
for (size_t i = 0; i < res->n; i++) {
const struct rec *p = &res->v[i];
if (p->gone) { continue; }
const dep_t *deps = p->pkg.pkg_dependencies;
for (const dep_t *d = deps; d && d->dep_name; d++) {
/* An added package: all its deps. An untouched one:
* only deps on something this transaction changes. */
if (!p->touched && !name_touched(res, rm, nrm, d->dep_name)) { continue; }
if (provided(res, sys, d->dep_name, &d->dep_ver)) { continue; }
const struct removed *gone = NULL;
for (size_t k = 0; !p->touched && k < nrm; k++) {
if (strcmp(rm[k].name, d->dep_name) == 0) { gone = &rm[k]; break; }
}
if (gone && !provided(res, sys, d->dep_name, NULL)) {
lg_printf(2, "Removing %s::%s breaks %s::%s.",
gone->repo, gone->name, p->repo, p->name);
} else {
fmt_ver(&d->dep_ver, ver, sizeof(ver));
lg_printf(2, "%s::%s requires %s >= %s, which is not installed.",
p->repo, p->name, d->dep_name, ver);
}
problems++;
}
if (!p->touched) { continue; }
for (size_t j = 0; j < res->n; j++) {
const struct rec *q = &res->v[j];
if (j == i || q->gone) { continue; }
/* Two added packages: report the pair once. */
if (q->touched && j < i) { continue; }
if (lists_conflict(p, q->name) || lists_conflict(q, p->name)) {
lg_printf(2, "%s::%s conflicts with %s::%s.",
p->repo, p->name, q->repo, q->name);
problems++;
}
}
}
int files = check_collisions(e, res);
if (files < 0) { return -1; }
problems += files;
files = check_links(e, res);
if (files < 0) { return -1; }
return problems + files;
}
/* The runtime path of the live store for a context's scope and root. */
static void
env_init(struct env *e, const gl_context_t *ctx, const char *store_at)
{
e->scope = ctx->scope;
e->uid = ctx->uid;
e->sysroot = ctx->sysroot;
e->strict = ctx->strict;
if (ctx->scope == GL_SCOPE_USR) {
char ub[PATH_MAX];
gl_usr_base(ctx->uid, ub, sizeof(ub));
snprintf(e->store_rt, sizeof(e->store_rt), "%s/store", ub);
} else {
snprintf(e->store_rt, sizeof(e->store_rt), "%s/store", GL_SYS_ROOT);
}
snprintf(e->store_at, sizeof(e->store_at), "%s", store_at);
}
/* The system store, which a user-scope transaction's deps may rely on.
* Always the running system's: user scope never has an install root. */
static int
load_sys(gl_ctx_scope_t scope, struct set *sys)
{
if (scope != GL_SCOPE_USR) { return 0; }
return load_store(GL_SYS_ROOT "/store", sys, true);
}
int
gl_check_plan(const gl_context_t *live,
const char *const *add_paths, size_t n_add,
const char *const *remove_ids, size_t n_remove)
{
if (!live) { return -1; }
struct set res = { NULL, 0 }, sys = { NULL, 0 };
struct removed *rm = calloc(n_remove + 1, sizeof(*rm));
size_t nrm = 0;
int rc = -1;
if (!rm || load_store(live->store_path, &res, false) != 0 ||
load_sys(live->scope, &sys) != 0) {
goto out;
}
for (size_t i = 0; i < n_remove; i++) {
const char *sep = strstr(remove_ids[i], "::");
if (!sep) { goto out; }
char repo[NAME_MAX + 1];
snprintf(repo, sizeof(repo), "%.*s", (int)(sep - remove_ids[i]), remove_ids[i]);
struct rec *r = (struct rec *)find_live(&res, repo, sep + 2);
if (!r) { continue; } /* not installed: nothing to break */
r->gone = true;
snprintf(rm[nrm].repo, sizeof(rm[nrm].repo), "%s", r->repo);
snprintf(rm[nrm].name, sizeof(rm[nrm].name), "%s", r->name);
nrm++;
}
for (size_t i = 0; i < n_add; i++) {
gl_pkg_t pkg = {0};
if (gl_read_pkg_manifest(add_paths[i], &pkg) != GL_INSTALL_OK) { goto out; }
/* Same repo::name installed: this replaces it. */
struct rec *old = (struct rec *)find_live(&res, pkg.pkg_repo, pkg.pkg_name);
if (old) { old->gone = true; }
struct rec *r = set_add(&res);
if (!r) { gl_free_pkg(&pkg); goto out; }
snprintf(r->repo, sizeof(r->repo), "%s", pkg.pkg_repo);
snprintf(r->name, sizeof(r->name), "%s", pkg.pkg_name);
r->ver = (semver_t){ pkg.pkg_ver.maj, pkg.pkg_ver.min, pkg.pkg_ver.pat, NULL };
r->pkg = pkg;
r->touched = true;
r->fresh = true;
r->overwrite = live->overwrite != 0;
r->from_archive = true;
snprintf(r->src, sizeof(r->src), "%s", add_paths[i]);
}
struct env e;
env_init(&e, live, live->store_path);
rc = check(&e, &res, &sys, rm, nrm);
out:
set_free(&res);
set_free(&sys);
free(rm);
return rc;
}
int
gl_check_txn(const gl_context_t *stage)
{
if (!stage || stage->mode != GL_CTX_STAGE) { return -1; }
char base[PATH_MAX], live_store[PATH_MAX];
if (stage->scope == GL_SCOPE_USR) {
gl_usr_base(stage->uid, base, sizeof(base));
} else {
snprintf(base, sizeof(base), "%s%s", stage->sysroot, GL_SYS_ROOT);
}
snprintf(live_store, sizeof(live_store), "%s/store", base);
struct set res = { NULL, 0 }, live = { NULL, 0 }, sys = { NULL, 0 };
struct removed *rm = NULL;
size_t nrm = 0;
int rc = -1;
if (load_store(stage->store_path, &res, false) != 0 ||
load_store(live_store, &live, false) != 0 ||
load_sys(stage->scope, &sys) != 0) {
goto out;
}
rm = calloc(live.n + 1, sizeof(*rm));
if (!rm) { goto out; }
/* Touched: in the stage, but not as this exact version in live.
* Fresh: installed by this transaction (a reinstall of the same
* version is fresh but not touched); its link pass is this one's. */
for (size_t i = 0; i < res.n; i++) {
struct rec *r = &res.v[i];
const struct rec *l = find_live(&live, r->repo, r->name);
r->touched = !l || strcmp(l->dirname, r->dirname) != 0;
char marker[PATH_MAX];
struct stat mst;
snprintf(marker, sizeof(marker), "%s/" GL_FRESH_MARKER, r->src);
r->fresh = lstat(marker, &mst) == 0;
snprintf(marker, sizeof(marker), "%s/" GL_OVERWRITE_MARKER, r->src);
r->overwrite = lstat(marker, &mst) == 0;
}
/* Removed: in live, and no longer in the stage under that repo::name. */
for (size_t i = 0; i < live.n; i++) {
if (find_live(&res, live.v[i].repo, live.v[i].name)) { continue; }
snprintf(rm[nrm].repo, sizeof(rm[nrm].repo), "%s", live.v[i].repo);
snprintf(rm[nrm].name, sizeof(rm[nrm].name), "%s", live.v[i].name);
nrm++;
}
struct env e;
env_init(&e, stage, stage->store_path);
rc = check(&e, &res, &sys, rm, nrm);
out:
set_free(&res);
set_free(&live);
set_free(&sys);
free(rm);
return rc;
}
+55
View File
@@ -0,0 +1,55 @@
#ifndef DEPS_H_
#define DEPS_H_
#include <stddef.h>
#include "transaction.h"
/*
* Dependency and conflict checks (ROADMAP 2.3). Built into
* libglacier_istoreutils.so.
*
* Rules:
* - A PKG_DEPS entry {name, ver} is met by any installed package of
* that name (any repo) at version >= ver.
* - In user scope, a dependency may also be met by a system package.
* System scope never looks at user scopes.
* - PKG_CONFS is matched by name, both ways (either package listing
* the other is a conflict); its version is ignored. Only packages in
* the same scope conflict.
* - Only what a transaction CHANGES is checked: the packages it adds
* (deps and conflicts), and the installed packages that depend on
* something it removes or replaces. A store that was already
* inconsistent doesn't block unrelated work.
* - Two packages may not ship the same path (ROADMAP 2.6), compared by
* where each entry lands once linked; sharing a directory is fine.
* Checked whenever either package is part of the transaction.
* - With ctx->strict (syspkg --strict, implied by -R), anything the
* post-commit link pass would refuse (a foreign file in the way, a
* directory over another package's symlink) is refused up front,
* unless -O will move it aside. System scope only: a user's links
* tree is rebuilt on every commit.
* - Every problem is reported with lg_printf, one line each:
* extra::foo requires bar >= 1.2.0, which is not installed.
* Removing base::musl breaks extra::foo.
* extra::foo conflicts with extra::bar.
* extra::foo and extra::bar both ship /glacier/sys/links/bin/x.
* Refusing to overwrite non-glacier path: /usr/bin/x
*
* Both return the number of problems (0 = fine), or -1 if a store or a
* manifest couldn't be read.
*/
/* Before a transaction: `live` is a live context; add_paths are .gpkg
* files to install (a package already installed under the same
* repo::name is replaced); remove_ids are "repo::name" strings. */
int gl_check_plan(const gl_context_t *live,
const char *const *add_paths, size_t n_add,
const char *const *remove_ids, size_t n_remove);
/* At commit: compares the stage against live. gl_commit_transaction
* calls this itself and refuses with GL_TXN_ERR_DEPS, so a caller that
* skips gl_check_plan is still held to the rules. */
int gl_check_txn(const gl_context_t *stage);
#endif
+389
View File
@@ -0,0 +1,389 @@
/*
* grepo: write and sign a repository index (REPO_DESIGN.txt section 4).
* A maintainer tool, like gsign.
*
* grepo -s KEY.sec -r REPO [-n SERIAL] [-e DAYS] DIR
*
* Reads every NAME-VER.gpkg in DIR (its manifest, sha256 and size) and
* writes DIR/repo.idx plus DIR/repo.idx.sig, signed with KEY.sec. Each
* package's manifest must say REPO, and its file name must be
* NAME-VER.gpkg as the manifest names it. The server is then any static
* file host serving DIR.
*
* SERIAL (decided 2026-10-06) is the date form YYYYMMDDNN: the larger
* of the previous DIR/repo.idx's SERIAL + 1 and today (UTC) * 100 + 1.
* -n sets it explicitly; it must still be above the previous one, or
* clients would refuse the index. VALID_UNTIL is now + DAYS days
* (default 30; decision 10c).
*
* Exit 0 on success, 1 otherwise.
*/
#define _POSIX_C_SOURCE 200809L
#define _XOPEN_SOURCE 700
#include <dirent.h>
#include <errno.h>
#include <fcntl.h>
#include <getopt.h>
#include <inttypes.h>
#include <limits.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/stat.h>
#include <time.h>
#include <unistd.h>
#include "istoreutils.h"
#include "log.h"
#include "pkg.h"
#include "sha256.h"
#include "sign.h"
#define DEFAULT_DAYS 30
#define MAX_DAYS 3650
#define USAGE "usage: grepo [-hv] -s KEY.sec -r REPO [-n SERIAL] [-e DAYS] DIR\n"
static const struct option longopts[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'v' },
{ "seckey", required_argument, NULL, 's' },
{ "repo", required_argument, NULL, 'r' },
{ "serial", required_argument, NULL, 'n' },
{ "expire", required_argument, NULL, 'e' },
{ NULL, 0, NULL, 0 }
};
static void
usage(void)
{
fprintf(stderr, USAGE);
}
static void
help(void)
{
printf("grepo: Write and sign a repository index\n");
printf(USAGE);
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 --seckey} KEY.sec", "Sign the index with this secret key");
printf("%-25s %s\n", "{-r --repo} REPO", "Repository the packages in DIR belong to");
printf("%-25s %s\n", "{-n --serial} SERIAL", "Index serial (default: YYYYMMDDNN)");
printf("%-25s Days until the index expires (default: %d)\n", "{-e --expire} DAYS", DEFAULT_DAYS);
printf("\n");
printf("This program is free software.\n");
printf("See the GNU GPL version 3 for details.\n");
}
static int
by_name(const void *a, const void *b)
{
return strcmp(*(char *const *)a, *(char *const *)b);
}
/* sha256 ("sha256:<hex>") and size of a regular file, read once. */
static int
hash_file(const char *path, char sha[72], int64_t *size)
{
int fd = open(path, O_RDONLY | O_NOFOLLOW);
if (fd < 0)
return -1;
gl_sha256_ctx_t h;
gl_sha256_init(&h);
uint8_t buf[65536];
ssize_t n;
int64_t total = 0;
while ((n = read(fd, buf, sizeof buf)) > 0) {
gl_sha256_update(&h, buf, (size_t)n);
total += n;
}
close(fd);
if (n < 0)
return -1;
uint8_t digest[32];
char hex[65];
gl_sha256_final(&h, digest);
gl_sha256_to_hex(digest, hex);
snprintf(sha, 72, "sha256:%s", hex);
*size = total;
return 0;
}
/* Reads a whole file (an index: at most GL_SIGN_MAX_MSG). NULL if it
* doesn't exist (errno ENOENT) or can't be read. */
static char *
slurp(const char *path, size_t *len)
{
FILE *f = fopen(path, "rb");
if (!f)
return NULL;
char *buf = malloc(GL_SIGN_MAX_MSG + 1);
size_t n = buf ? fread(buf, 1, GL_SIGN_MAX_MSG + 1, f) : 0;
int bad = !buf || ferror(f) || n > GL_SIGN_MAX_MSG;
fclose(f);
if (bad) {
free(buf);
errno = EIO;
return NULL;
}
*len = n;
return buf;
}
/* One DEPS/CONFS element. 0 having said why if the name is unsafe. */
static int
relation(FILE *f, const char *path, const char *what, const char *name,
const semver_t *v, int first)
{
if (!gl_valid_pkg_ident(name)) {
lg_printf(LG_ERR, "%s has a %s that is not a safe name.", path, what);
return 0;
}
fprintf(f, "%s { NAME = \"%s\"; VER = \"%d.%d.%d\"; }", first ? "" : ",",
name, v->maj, v->min, v->pat);
return 1;
}
/* One package's PKGS entry, appended to f. 0, or -1 having said why. */
static int
write_entry(FILE *f, const char *dir, const char *file, const char *repo)
{
char path[PATH_MAX];
snprintf(path, sizeof path, "%s/%s", dir, file);
struct stat st;
if (lstat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
lg_printf(LG_ERR, "Refusing %s: not a regular file.", path);
return -1;
}
gl_pkg_t pkg = {0};
if (gl_read_pkg_manifest(path, &pkg) != 0) {
lg_printf(LG_ERR, "Cannot read the manifest of %s.", path);
return -1;
}
int rc = -1;
char ver[64], want[PATH_MAX];
snprintf(ver, sizeof ver, "%d.%d.%d", pkg.pkg_ver.maj, pkg.pkg_ver.min,
pkg.pkg_ver.pat);
snprintf(want, sizeof want, "%s-%s.gpkg", pkg.pkg_name, ver);
if (strcmp(pkg.pkg_repo, repo) != 0) {
lg_printf(LG_ERR, "%s is a %s package, not %s.", path, pkg.pkg_repo, repo);
goto out;
}
if (strcmp(file, want) != 0) {
lg_printf(LG_ERR, "%s must be named %s, as its manifest says.", path, want);
goto out;
}
char sha[72];
int64_t size;
if (hash_file(path, sha, &size) != 0) {
lg_printf(LG_ERR, "Cannot read %s.", path);
goto out;
}
fprintf(f, "\t{ NAME = \"%s\"; VER = \"%s\"; FILE = \"%s\";\n"
"\t SHA256 = \"%s\"; SIZE = %" PRId64 "L;\n\t DEPS = (",
pkg.pkg_name, ver, file, sha, size);
int first = 1;
for (dep_t *d = pkg.pkg_dependencies; d && d->dep_name; d++) {
if (!relation(f, path, "dependency", d->dep_name, &d->dep_ver, first))
goto out;
first = 0;
}
fprintf(f, "%s); CONFS = (", first ? "" : " ");
first = 1;
for (conf_t *k = pkg.pkg_conflicts; k && k->conf_name; k++) {
if (!relation(f, path, "conflict", k->conf_name, &k->conf_ver, first))
goto out;
first = 0;
}
fprintf(f, "%s); }", first ? "" : " ");
rc = 0;
out:
gl_free_pkg(&pkg);
return rc;
}
int
main(int argc, char *argv[])
{
const char *sec = NULL, *repo = NULL;
int64_t serial = 0;
long days = DEFAULT_DAYS;
int c;
while ((c = getopt_long(argc, argv, "hvs:r:n:e:", longopts, NULL)) != -1) {
char *end;
switch (c) {
case 'h':
help();
return 0;
case 'v':
printf("v4.0.0\n");
return 0;
case 's': sec = optarg; break;
case 'r': repo = optarg; break;
case 'n':
errno = 0;
serial = strtoll(optarg, &end, 10);
if (errno || *end || end == optarg || serial <= 0) {
lg_printf(LG_ERR, "SERIAL must be a whole number above 0.");
return 1;
}
break;
case 'e':
errno = 0;
days = strtol(optarg, &end, 10);
if (errno || *end || end == optarg || days < 1 || days > MAX_DAYS) {
lg_printf(LG_ERR, "DAYS must be a whole number from 1 to %d.", MAX_DAYS);
return 1;
}
break;
default:
usage();
return 1;
}
}
if (!sec || !repo || optind != argc - 1) {
usage();
return 1;
}
const char *dir = argv[optind];
if (!gl_valid_pkg_ident(repo)) {
lg_printf(LG_ERR, "Repository name %s is not a safe name ([A-Za-z0-9._-]).", repo);
return 1;
}
int st = gl_sign_check_seckey(sec);
if (st != GL_SIGN_OK) {
lg_printf(LG_ERR, "Secret key %s %s.", sec,
st == GL_SIGN_ERR_ENCRYPTED ? "is encrypted: sign with signify" :
st == GL_SIGN_ERR_FORMAT ? "is malformed" : "cannot be read");
return 1;
}
char idx_path[PATH_MAX], sig_path[PATH_MAX];
if ((size_t)snprintf(idx_path, sizeof idx_path, "%s/repo.idx", dir) >= sizeof idx_path ||
(size_t)snprintf(sig_path, sizeof sig_path, "%s/repo.idx.sig", dir) >= sizeof sig_path) {
lg_printf(LG_ERR, "Path too long: %s", dir);
return 1;
}
/* The previous index, for its SERIAL. The maintainer's own file:
* parsed, not verified, but it must be a well-formed index for
* this repo (a mixed-up DIR is better caught here). */
int64_t prev = 0;
size_t plen;
char *ptext = slurp(idx_path, &plen);
if (!ptext && errno != ENOENT) {
lg_printf(LG_ERR, "Cannot read %s.", idx_path);
return 1;
}
if (ptext) {
gl_index_t old;
gl_index_status_t ist = gl_index_parse(ptext, plen, repo, &old);
free(ptext);
if (ist == GL_INDEX_ERR_REPO) {
lg_printf(LG_ERR, "%s is the index of another repository.", idx_path);
return 1;
}
if (ist != GL_INDEX_OK) {
lg_printf(LG_ERR, "Malformed %s.", idx_path);
return 1;
}
prev = old.serial;
gl_index_free(&old);
}
time_t now = time(NULL);
if (serial == 0) {
struct tm tm;
gmtime_r(&now, &tm);
int64_t today = ((int64_t)(tm.tm_year + 1900) * 10000 +
(tm.tm_mon + 1) * 100 + tm.tm_mday) * 100 + 1;
serial = prev + 1 > today ? prev + 1 : today;
} else if (serial <= prev) {
lg_printf(LG_ERR, "SERIAL must be above %" PRId64 ", the previous index's.", prev);
return 1;
}
DIR *d = opendir(dir);
if (!d) {
lg_printf(LG_ERR, "Cannot open %s: %s", dir, strerror(errno));
return 1;
}
char **names = NULL;
size_t n = 0;
struct dirent *de;
while ((de = readdir(d))) {
size_t l = strlen(de->d_name);
if (de->d_name[0] == '.' || l < 6 || strcmp(de->d_name + l - 5, ".gpkg") != 0)
continue;
char **grown = realloc(names, (n + 1) * sizeof *names);
if (!grown || !(grown[n] = strdup(de->d_name))) {
lg_printf(LG_ERR, "Out of memory.");
return 1;
}
names = grown;
n++;
}
closedir(d);
qsort(names, n, sizeof *names, by_name);
/* Written beside the result, then renamed over it: never in place. */
char tmp_idx[PATH_MAX], tmp_sig[PATH_MAX];
snprintf(tmp_idx, sizeof tmp_idx, "%s/.repo.idx.XXXXXX", dir);
int fd = mkstemp(tmp_idx);
FILE *f = fd < 0 ? NULL : fdopen(fd, "w");
if (!f) {
lg_printf(LG_ERR, "Cannot write in %s: %s", dir, strerror(errno));
return 1;
}
snprintf(tmp_sig, sizeof tmp_sig, "%s.sig", tmp_idx);
fprintf(f, "# Repository index written by grepo. Signed: repo.idx.sig.\n"
"REPO = \"%s\";\nSERIAL = %" PRId64 "L;\nVALID_UNTIL = %" PRId64 "L;\n"
"PKGS = (\n", repo, serial, (int64_t)now + (int64_t)days * 86400);
int rc = 0;
for (size_t i = 0; i < n && rc == 0; i++) {
if (i)
fprintf(f, ",\n");
rc = write_entry(f, dir, names[i], repo);
}
fprintf(f, "%s);\n", n ? "\n" : "");
if (fchmod(fd, 0644) != 0 || fflush(f) != 0 || fsync(fd) != 0)
rc = rc ? rc : (lg_printf(LG_ERR, "Cannot write %s.", tmp_idx), -1);
if (fclose(f) != 0 && rc == 0) {
lg_printf(LG_ERR, "Cannot write %s.", tmp_idx);
rc = -1;
}
for (size_t i = 0; i < n; i++)
free(names[i]);
free(names);
if (rc == 0 && (st = gl_sign_file(sec, tmp_idx, tmp_sig)) != GL_SIGN_OK) {
lg_printf(LG_ERR, "Cannot sign %s.", tmp_idx);
rc = -1;
}
/* The signature first: a client that fetches between the renames
* gets a new .sig with the old index, which fails to verify and is
* refused, never an index accepted under the wrong signature. */
if (rc == 0 && (rename(tmp_sig, sig_path) != 0 || rename(tmp_idx, idx_path) != 0)) {
lg_printf(LG_ERR, "Cannot replace %s: %s", idx_path, strerror(errno));
rc = -1;
}
if (rc != 0) {
unlink(tmp_idx);
unlink(tmp_sig);
return 1;
}
lg_printf(LG_SUCCESS, "Wrote %s: %zu %s, serial %" PRId64 ".", idx_path, n,
n == 1 ? "package" : "packages", serial);
return 0;
}
+40 -4
View File
@@ -1,18 +1,54 @@
#include <getopt.h>
#include <stdio.h>
#include "sha256.h"
#define USAGE "usage: gsha256sum [-hv] FILE\n"
static const struct option longopts[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'v' },
{ NULL, 0, NULL, 0 }
};
static void
help(void)
{
printf("gsha256sum: Print a file's sha256 as a Glacier sidecar line\n");
printf(USAGE);
printf("\n");
printf("%-25s %s\n", "{-h --help}", "Show this message");
printf("%-25s %s\n", "{-v --version}", "Show the current version");
printf("\n");
printf("This program is free software.\n");
printf("See the GNU GPL version 3 for details.\n");
}
int
main(int argc, char *argv[])
{
if (argc != 2) {
fprintf(stderr, "usage: %s FILE\n", argv[0]);
int c;
while ((c = getopt_long(argc, argv, "hv", longopts, NULL)) != -1) {
switch (c) {
case 'h':
help();
return 0;
case 'v':
printf("v4.0.0\n");
return 0;
default:
fprintf(stderr, USAGE);
return 1;
}
}
if (optind != argc - 1) {
fprintf(stderr, USAGE);
return 1;
}
char hex[GL_SHA256_HEX_SIZE];
if (gl_sha256_file(argv[1], hex) != 0) {
perror(argv[1]);
if (gl_sha256_file(argv[optind], hex) != 0) {
perror(argv[optind]);
return 1;
}
+212
View File
@@ -0,0 +1,212 @@
/*
* gsign: make signify-compatible Ed25519 keys and signatures
* (REPO_DESIGN.txt section 2). A build/maintainer tool, like
* gsha256sum.
*
* gsign -G -p KEY.pub -s KEY.sec [-c COMMENT]
* gsign -S -s KEY.sec -m FILE [-x SIGFILE]
* gsign -V -p KEY.pub -m FILE [-x SIGFILE]
*
* SIGFILE defaults to FILE.sig. Exit 0 on success, 1 otherwise.
*/
#define _POSIX_C_SOURCE 200809L
#include <getopt.h>
#include <limits.h>
#include <stdio.h>
#include <string.h>
#include <sys/stat.h>
#include <unistd.h>
#include "log.h"
#include "sign.h"
#define USAGE \
"usage: gsign [-hv]\n" \
" gsign -G -p KEY.pub -s KEY.sec [-c COMMENT]\n" \
" gsign -S -s KEY.sec -m FILE [-x SIGFILE]\n" \
" gsign -V -p KEY.pub -m FILE [-x SIGFILE]\n"
static const struct option longopts[] = {
{ "help", no_argument, NULL, 'h' },
{ "version", no_argument, NULL, 'v' },
{ "generate", no_argument, NULL, 'G' },
{ "sign", no_argument, NULL, 'S' },
{ "verify", no_argument, NULL, 'V' },
{ "pubkey", required_argument, NULL, 'p' },
{ "seckey", required_argument, NULL, 's' },
{ "message", required_argument, NULL, 'm' },
{ "sig", required_argument, NULL, 'x' },
{ "comment", required_argument, NULL, 'c' },
{ NULL, 0, NULL, 0 }
};
static void
usage(void)
{
fprintf(stderr, USAGE);
}
static void
help(void)
{
printf("gsign: Make and check signify-compatible signatures\n");
printf(USAGE);
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", "{-G --generate}", "Generate a new key pair");
printf("%-25s %s\n", "{-S --sign}", "Sign a file");
printf("%-25s %s\n", "{-V --verify}", "Verify a file's signature");
printf("%-25s %s\n", "{-p --pubkey} KEY.pub", "Public key file");
printf("%-25s %s\n", "{-s --seckey} KEY.sec", "Secret key file");
printf("%-25s %s\n", "{-m --message} FILE", "File to sign or verify");
printf("%-25s %s\n", "{-x --sig} SIGFILE", "Signature file (default: FILE.sig)");
printf("%-25s %s\n", "{-c --comment} COMMENT", "Comment for a new key pair (default: gsign)");
printf("\n");
printf("This program is free software.\n");
printf("See the GNU GPL version 3 for details.\n");
}
/* "<what> <path> <problem>." about one file. */
static int
fail(const char *what, const char *path, int st)
{
const char *problem;
switch (st) {
case GL_SIGN_ERR_FORMAT: problem = "is malformed"; break;
case GL_SIGN_ERR_ENCRYPTED: problem = "is encrypted: sign with signify"; break;
case GL_SIGN_ERR_TOOBIG: problem = "is too large"; break;
case GL_SIGN_ERR_EXISTS: problem = "already exists"; break;
case GL_SIGN_ERR_WRITE: problem = "cannot be written"; break;
default: problem = "cannot be read"; break;
}
lg_printf(LG_ERR, "%s %s %s.", what, path, problem);
return 1;
}
int
main(int argc, char *argv[])
{
char mode = 0;
const char *pub = NULL, *sec = NULL, *msg = NULL, *sig = NULL;
const char *comment = "gsign";
int c;
while ((c = getopt_long(argc, argv, "hvGSVp:s:m:x:c:", longopts, NULL)) != -1) {
switch (c) {
case 'h':
help();
return 0;
case 'v':
printf("v4.0.0\n");
return 0;
case 'G': case 'S': case 'V':
if (mode && mode != c) {
usage();
return 1;
}
mode = (char)c;
break;
case 'p': pub = optarg; break;
case 's': sec = optarg; break;
case 'm': msg = optarg; break;
case 'x': sig = optarg; break;
case 'c': comment = optarg; break;
default:
usage();
return 1;
}
}
if (optind != argc) {
usage();
return 1;
}
char sigbuf[PATH_MAX];
if (msg && !sig) {
if ((size_t)snprintf(sigbuf, sizeof sigbuf, "%s.sig", msg) >= sizeof sigbuf) {
lg_printf(LG_ERR, "Path too long: %s", msg);
return 1;
}
sig = sigbuf;
}
int st;
switch (mode) {
case 'G':
if (!pub || !sec || msg) {
usage();
return 1;
}
/* The library refuses these too (O_EXCL); this names which. */
struct stat sb;
if (lstat(sec, &sb) == 0)
return fail("Secret key", sec, GL_SIGN_ERR_EXISTS);
if (lstat(pub, &sb) == 0)
return fail("Public key", pub, GL_SIGN_ERR_EXISTS);
st = gl_sign_generate(pub, sec, comment);
if (st == GL_SIGN_ERR_FORMAT) {
lg_printf(LG_ERR, "Comment must be one line, at most %d characters.",
GL_SIGN_MAX_COMMENT - 11);
return 1;
}
if (st == GL_SIGN_ERR_RANDOM) {
lg_printf(LG_ERR, "Cannot get random bytes for a new key.");
return 1;
}
if (st != GL_SIGN_OK) {
lg_printf(LG_ERR, "Cannot write %s and %s.", pub, sec);
return 1;
}
lg_printf(LG_SUCCESS, "Wrote %s and %s.", pub, sec);
return 0;
case 'S':
if (!sec || !msg || pub) {
usage();
return 1;
}
if ((st = gl_sign_check_seckey(sec)) != GL_SIGN_OK)
return fail("Secret key", sec, st);
st = gl_sign_file(sec, msg, sig);
if (st == GL_SIGN_ERR_WRITE)
return fail("Signature", sig, st);
if (st != GL_SIGN_OK)
return fail("File", msg, st);
lg_printf(LG_SUCCESS, "Signed %s: %s.", msg, sig);
return 0;
case 'V': {
if (!pub || !msg || sec) {
usage();
return 1;
}
gl_sign_pubkey_t pk;
gl_sign_sig_t sg;
if ((st = gl_sign_read_pubkey(pub, &pk)) != GL_SIGN_OK)
return fail("Public key", pub, st);
if ((st = gl_sign_read_sig(sig, &sg)) != GL_SIGN_OK)
return fail("Signature", sig, st);
st = gl_sign_verify_file(pub, msg, sig);
switch (st) {
case GL_SIGN_OK:
lg_printf(LG_SUCCESS, "Signature verified: %s.", msg);
return 0;
case GL_SIGN_ERR_KEYNUM:
lg_printf(LG_ERR, "%s was signed by a different key than %s.", msg, pub);
return 1;
case GL_SIGN_ERR_BAD:
lg_printf(LG_ERR, "Signature does not verify: %s.", msg);
return 1;
default:
return fail("File", msg, st);
}
}
default:
usage();
return 1;
}
}
+366 -502
View File
File diff suppressed because it is too large. Load diff
+9 -26
View File
@@ -5,6 +5,7 @@
#include <stdio.h>
#include <sys/types.h>
#include "pkg.h"
#include "transaction.h"
typedef enum {
@@ -24,12 +25,6 @@ typedef enum {
}
gl_backup_status_t;
typedef enum {
GL_RESTORE_OK = 0,
GL_RESTORE_ERR_OPEN = 1,
GL_RESTORE_ERR_EXTRACT = 2
} gl_restore_status_t;
/*
* GL_INSTALL_OK: installation successful
* GL_INSTALL_ERR_OPEN: unable to open .gpkg file
@@ -37,6 +32,9 @@ typedef enum {
* GL_INSTALL_ERR_EXTRACT: unable to extract files
* GL_INSTALL_ERR_INDEX: unable to update index
* GL_INSTALL_ERR_ALREADY_INSTALLED: package already installed
* GL_INSTALL_ERR_SCOPE: base-repo package under user scope
* GL_INSTALL_ERR_DEPS: unmet dependency or conflict (live context only;
* a staged install is checked at commit, GL_TXN_ERR_DEPS)
*/
typedef enum {
@@ -48,6 +46,7 @@ typedef enum {
GL_INSTALL_ERR_ALREADY_INSTALLED = 5,
GL_INSTALL_SKIPPED = 6,
GL_INSTALL_ERR_SCOPE = 7,
GL_INSTALL_ERR_DEPS = 8, /* live context only; see deps.h */
}
gl_install_status_t;
@@ -56,39 +55,22 @@ typedef enum {
GL_REMOVE_ERR_NOT_FOUND = 1,
GL_REMOVE_ERR_UNLINK = 2,
GL_REMOVE_ERR_INDEX = 3,
GL_REMOVE_ERR_DEPS = 4, /* live context only; see deps.h */
} gl_remove_status_t;
struct gpkg_entry {
char *repo;
char *pkg;
char *ver;
};
struct gindex {
int uid;
int listed;
struct gpkg_entry *entries;
size_t count;
};
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_init_sys_at(const char *sysroot, 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);
void gl_free_index(struct gindex *idx);
int gl_rebuild_index(gl_context_t *ctx);
char *gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name);
typedef struct {
char **repos;
size_t count;
@@ -100,9 +82,10 @@ void gl_free_repo_list(gl_repo_list_t *list);
gl_backup_status_t gl_backup_istore(const char *tar_path, const char *glacier_root,
const char *uid);
gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid);
int gl_read_pkg_manifest(const char *gpkg_path, gl_pkg_t *pkg);
gl_install_status_t gl_install_pkg(gl_context_t *ctx, const char *gpkg_path);
int gl_set_pkg_auto(gl_context_t *ctx, const char *repo, const char *name, bool automatic);
gl_remove_status_t gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo);
int gl_relink_store(gl_context_t *ctx);
+13 -121
View File
@@ -8,19 +8,8 @@
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <syslog.h>
#include <time.h>
#include "color.h"
typedef enum
{
LG_INFO,
LG_WARN,
LG_ERR,
LG_SUCCESS
}
loglevel_t;
#include "log.h"
static int
supports_utf8(void)
@@ -34,26 +23,6 @@ supports_utf8(void)
#endif
}
/* Converts priority integer to string */
const char
*p2s(int priority)
{
switch (priority)
{
case LOG_EMERG: return "EMERG"; break;
case LOG_ALERT: return "ALERT"; break;
case LOG_CRIT: return "CRIT"; break;
case LOG_ERR: return "ERR"; break;
case LOG_WARNING: return "WARN"; break;
case LOG_NOTICE: return "NOTICE"; break;
case LOG_INFO: return "INFO"; break;
case LOG_DEBUG: return "DEBUG"; break;
default: return "UNKNOWN"; break;
}
}
void
lg_printf(loglevel_t loglevel, const char *fmt, ...)
{
@@ -92,104 +61,22 @@ lg_printf(loglevel_t loglevel, const char *fmt, ...)
printf("\n");
}
void
lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...)
{
const char *prefix;
int utf8_ok = supports_utf8();
switch (loglevel) {
case LG_INFO:
prefix = COL_BLUE "[i]" COL_RESET " ";
break;
case LG_WARN:
prefix = COL_YELLOW "[!]" COL_RESET " ";
break;
case LG_ERR:
prefix = COL_RED "[x]" COL_RESET " ";
break;
case LG_SUCCESS:
if (utf8_ok) {
prefix = COL_GREEN "[" "\xE2\x9C\x93" "]" COL_RESET " ";
} else {
prefix = COL_GREEN "[*]" COL_RESET " ";
}
break;
default:
prefix = "[?] ";
}
fputs(prefix, stdout);
va_list args;
va_start(args, fmt);
vprintf(fmt, args);
va_end(args);
}
void
lg_syslog(int priority, const char *fmt, ...)
{
va_list args;
va_start(args, fmt);
vsyslog(priority, fmt, args);
va_end(args);
}
void
lg_log(bool use_syslog, const char *logfile, int priority, const char *fmt, ...)
{
FILE *log = fopen(logfile, "a");
if (!log) {
perror("fopen");
return;
}
time_t now = time(NULL);
struct tm *t = localtime(&now);
char timestamp[20];
strftime(timestamp, sizeof(timestamp), "%Y-%m-%d %H:%M:%S", t);
fprintf(log, "[%s] [%s] ", timestamp, p2s(priority));
va_list args;
va_start(args, fmt);
vfprintf(log, fmt, args);
va_end(args);
fputc('\n', log);
fflush(log);
if (use_syslog) {
va_list args2;
va_start(args2, fmt);
vsyslog(priority, fmt, args2);
va_end(args2);
fclose(log);
}
else {
fclose(log);
return;
}
}
void
lg_gpkg_help(int argc, char *argv[])
{
(void)argc;
printf("%s: Install, update, and remove packages\n", argv[0]);
printf("usage: %s [-hv] [-flux PKG] [-OSV]\n", argv[0]);
printf("usage: %s [-hv] [-y] [-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", "{-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", "{-f --merge} PKG...", "Install packages from a remote repository");
printf("%-25s %s\n", "{-u --update} [PKG...]", "Update system or individual packages");
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", "{-y --refresh}", "Fetch and verify the remote repositories' indexes");
printf("%-25s %s\n", "{-O --overwrite}", "Allow Glacier to overwrite existing 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");
@@ -203,16 +90,21 @@ lg_syspkg_help(int argc, char *argv[])
(void)argc;
printf("%s: Manage the system package index\n", argv[0]);
printf("usage: %s [-hv] [-n] [-lx PKG] [-OSV]\n", argv[0]);
printf("usage: %s [-hv] [-n] [-y] [-flux PKG] [-OSV] [-R DIR] [--strict]\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", "{-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", "{-f --merge} PKG...", "Install packages from a remote repository");
printf("%-25s %s\n", "{-u --update} [PKG...]", "Update system or individual packages");
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", "{-y --refresh}", "Fetch and verify the remote repositories' indexes");
printf("%-25s %s\n", "{-O --overwrite}", "Allow Glacier to overwrite existing files");
printf("%-25s %s\n", "{-S --skip-installed}", "Skip packages that are already up to date");
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
printf("%-25s %s\n", "{-R --root} DIR", "Operate on the system installed under DIR (implies --strict)");
printf("%-25s %s\n", "{--strict}", "Refuse the whole transaction if any path can't be linked");
printf("\n");
printf("This program is free software.\n");
printf("See the GNU GPL version 3 for details.\n");
+6 -9
View File
@@ -7,9 +7,6 @@
#include <stdio.h>
#include <stdarg.h>
#include <stdbool.h>
#include <syslog.h>
#include <time.h>
#include "color.h"
@@ -26,13 +23,13 @@ typedef enum
}
loglevel_t;
int supports_utf8(void);
const char *p2s(int priority);
void lg_printf(loglevel_t loglevel, const char *fmt, ...)
__attribute__((format(printf, 2, 3)));
void lg_printf(loglevel_t loglevel, const char *fmt, ...);
void lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...);
void lg_syslog(int priority, const char *fmt, ...);
void lg_log(bool use_syslog, FILE *logfile, int priority, const char *fmt, ...);
/* The CLIs' -h output. */
void lg_gpkg_help(int argc, char *argv[]);
void lg_syspkg_help(int argc, char *argv[]);
void lg_gstore_help(int argc, char *argv[]);
#ifdef __cplusplus
}
-95
View File
@@ -1,95 +0,0 @@
#define _POSIX_C_SOURcE 200809L
#define _DEFAULT_SOURCE
#define _XOPEN_SOURCE 700
#include <ctype.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define MAX_LINE 1024
static char
*trim(char *s) {
char *end;
while (isspace((unsigned char)*s)) { s++; }
if (*s == 0) { return s; }
end = s + strlen(s) - 1;
while (end > s && isspace((unsigned char)*end)) { *end-- = 0; }
return s;
}
static char
*expand(char *value)
{
size_t cap = strlen(value) + 1;
char *buffer = malloc(cap);
if (!buffer) { return NULL; }
char *out = buffer;
char *p = value;
while (*p) {
if (*p == '$') {
p++;
char var[128], *vp = var;
while (*p && (isalnum((unsigned char)*p) || *p == '_')) {
*vp++ = *p++;
}
*vp = '\0';
char *val = getenv(var);
if (val) {
size_t len = strlen(val);
size_t used = (size_t)(out - buffer);
if (used + len + 1 > cap) {
cap = used + len + 1;
buffer = realloc(buffer, cap);
if (!buffer) { return NULL; }
out = buffer + used;
}
memcpy(out, val, len);
out += len;
}
}
else {
*out++ = *p++;
}
}
*out = '\0';
return buffer;
}
int
parse_makeconf(const char *filename)
{
FILE *make_conf = fopen(filename, "r");
if (!make_conf) {
perror("fopen");
return -1;
}
char line[MAX_LINE];
while (fgets(line, sizeof(line), make_conf)) {
char *p = trim(line);
if (*p == '\0' || *p == '#') { continue; }
char *eq = strchr(p, '=');
if (!eq) { continue; }
*eq = '\0';
char *key = trim(p);
char *val = trim(eq + 1);
char *expanded = expand(val);
if (expanded) {
setenv(key, expanded, 1);
free(expanded);
}
}
fclose(make_conf);
return 0;
}
+167
View File
@@ -0,0 +1,167 @@
Monocypher as a whole is dual-licensed. Choose whichever licence you
want from the two licences listed below.
The first licence is a regular 2-clause BSD licence. The second licence
is the CC-0 from Creative Commons. It is intended to release Monocypher
to the public domain. The BSD licence serves as a fallback option.
See the individual files for specific information about who contributed
to what file during which years. See below for special notes.
Licence 1 (2-clause BSD)
------------------------
Copyright (c) 2017-2023, Loup Vaillant
Copyright (c) 2017-2019, Michael Savage
Copyright (c) 2017-2023, Fabio Scotoni
All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are
met:
1. Redistributions of source code must retain the above copyright
notice, this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright
notice, this list of conditions and the following disclaimer in the
documentation and/or other materials provided with the
distribution.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
Licence 2 (CC-0)
----------------
> CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
> LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
> ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
> INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
> REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
> PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
> THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
> HEREUNDER.
### Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work
of authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without
fear of later claims of infringement build upon, modify, incorporate in
other works, reuse and redistribute as freely as possible in any form
whatsoever and for any purposes, including without limitation commercial
purposes. These owners may contribute to the Commons to promote the
ideal of a free culture and the further production of creative, cultural
and scientific works, or to gain reputation or greater distribution for
their Work in part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or
she is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under
its terms, with knowledge of his or her Copyright and Related Rights in
the Work and the meaning and intended legal effect of CC0 on those
rights.
1. **Copyright and Related Rights.** A Work made available under CC0 may
be protected by copyright and related or neighboring rights
("Copyright and Related Rights"). Copyright and Related Rights
include, but are not limited to, the following:
- the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
- moral rights retained by the original author(s) and/or
performer(s); publicity and privacy rights pertaining to a person's
image or likeness depicted in a Work;
- rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
- rights protecting the extraction, dissemination, use and reuse of
data in a Work;
- database rights (such as those arising under Directive 96/9/EC of
the European Parliament and of the Council of 11 March 1996 on the
legal protection of databases, and under any national
implementation thereof, including any amended or successor version
of such directive); and
- other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. **Waiver.** To the greatest extent permitted by, but not in
contravention of, applicable law, Affirmer hereby overtly, fully,
permanently, irrevocably and unconditionally waives, abandons, and
surrenders all of Affirmer's Copyright and Related Rights and
associated claims and causes of action, whether now known or unknown
(including existing as well as future claims and causes of action),
in the Work (i) in all territories worldwide, (ii) for the maximum
duration provided by applicable law or treaty (including future time
extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"Waiver"). Affirmer makes the Waiver for the benefit of each member
of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal
or equitable action to disrupt the quiet enjoyment of the Work by the
public as contemplated by Affirmer's express Statement of Purpose.
3. **Public License Fallback.** Should any part of the Waiver for any
reason be judged legally invalid or ineffective under applicable law,
then the Waiver shall be preserved to the maximum extent permitted
taking into account Affirmer's express Statement of Purpose. In
addition, to the extent the Waiver is so judged Affirmer hereby
grants to each affected person a royalty-free, non transferable, non
sublicensable, non exclusive, irrevocable and unconditional license
to exercise Affirmer's Copyright and Related Rights in the Work (i)
in all territories worldwide, (ii) for the maximum duration provided
by applicable law or treaty (including future time extensions), (iii)
in any current or future medium and for any number of copies, and
(iv) for any purpose whatsoever, including without limitation
commercial, advertising or promotional purposes (the "License"). The
License shall be deemed effective as of the date CC0 was applied by
Affirmer to the Work. Should any part of the License for any reason
be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the
remainder of the License, and in such case Affirmer hereby affirms
that he or she will not (i) exercise any of his or her remaining
Copyright and Related Rights in the Work or (ii) assert any
associated claims and causes of action with respect to the Work, in
either case contrary to Affirmer's express Statement of Purpose.
4. **Limitations and Disclaimers.**
- No trademark or patent rights held by Affirmer are waived,
abandoned, surrendered, licensed or otherwise affected by this
document.
- Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy,
or the present or absence of errors, whether or not discoverable,
all to the greatest extent permissible under applicable law.
- Affirmer disclaims responsibility for clearing rights of other
persons that may apply to the Work or any use thereof, including
without limitation any person's Copyright and Related Rights in the
Work. Further, Affirmer disclaims responsibility for obtaining any
necessary consents, permissions or other rights required for any
use of the Work.
- Affirmer understands and acknowledges that Creative Commons is not
a party to this document and has no duty or obligation with respect
to this CC0 or use of the Work.
+14
View File
@@ -0,0 +1,14 @@
Monocypher 4.0.3, vendored UNMODIFIED (REPO_DESIGN.txt section 1a).
monocypher.c, monocypher.h from src/
monocypher-ed25519.c, monocypher-ed25519.h from src/optional/
LICENCE.md BSD-2-Clause or CC0
Source: monocypher-4.0.3.tar.gz, fetched 2026-10-06 from both
https://github.com/LoupVaillant/Monocypher/releases/download/4.0.3/
and https://monocypher.org/download/ ; identical, sha512
40904ada5c7ee4f7741733e38b69a30a4b0561cbffba5ffe7c2dce16136d540251ec0d9056ff606510d3b5b708fb8a40db7e0870d4a0b2dc17ba2bfb880f8965
Only Ed25519 (with its SHA-512) is used, through src/sign.c. Never
edit these files: replace them with a newer release, and update this
note.
+503
View File
@@ -0,0 +1,503 @@
// Monocypher version 4.0.3
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#include "monocypher-ed25519.h"
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#endif
/////////////////
/// Utilities ///
/////////////////
#define FOR(i, min, max) for (size_t i = min; i < max; i++)
#define COPY(dst, src, size) FOR(_i_, 0, size) (dst)[_i_] = (src)[_i_]
#define ZERO(buf, size) FOR(_i_, 0, size) (buf)[_i_] = 0
#define WIPE_CTX(ctx) crypto_wipe(ctx , sizeof(*(ctx)))
#define WIPE_BUFFER(buffer) crypto_wipe(buffer, sizeof(buffer))
#define MIN(a, b) ((a) <= (b) ? (a) : (b))
typedef uint8_t u8;
typedef uint64_t u64;
// Returns the smallest positive integer y such that
// (x + y) % pow_2 == 0
// Basically, it's how many bytes we need to add to "align" x.
// Only works when pow_2 is a power of 2.
// Note: we use ~x+1 instead of -x to avoid compiler warnings
static size_t align(size_t x, size_t pow_2)
{
return (~x + 1) & (pow_2 - 1);
}
static u64 load64_be(const u8 s[8])
{
return((u64)s[0] << 56)
| ((u64)s[1] << 48)
| ((u64)s[2] << 40)
| ((u64)s[3] << 32)
| ((u64)s[4] << 24)
| ((u64)s[5] << 16)
| ((u64)s[6] << 8)
| (u64)s[7];
}
static void store64_be(u8 out[8], u64 in)
{
out[0] = (u8)(in >> 56);
out[1] = (u8)(in >> 48);
out[2] = (u8)(in >> 40);
out[3] = (u8)(in >> 32);
out[4] = (u8)(in >> 24);
out[5] = (u8)(in >> 16);
out[6] = (u8)(in >> 8);
out[7] = (u8) in ;
}
static void load64_be_buf (u64 *dst, const u8 *src, size_t size) {
FOR(i, 0, size) { dst[i] = load64_be(src + i*8); }
}
///////////////
/// SHA 512 ///
///////////////
static u64 rot(u64 x, int c ) { return (x >> c) | (x << (64 - c)); }
static u64 ch (u64 x, u64 y, u64 z) { return (x & y) ^ (~x & z); }
static u64 maj(u64 x, u64 y, u64 z) { return (x & y) ^ ( x & z) ^ (y & z); }
static u64 big_sigma0(u64 x) { return rot(x, 28) ^ rot(x, 34) ^ rot(x, 39); }
static u64 big_sigma1(u64 x) { return rot(x, 14) ^ rot(x, 18) ^ rot(x, 41); }
static u64 lit_sigma0(u64 x) { return rot(x, 1) ^ rot(x, 8) ^ (x >> 7); }
static u64 lit_sigma1(u64 x) { return rot(x, 19) ^ rot(x, 61) ^ (x >> 6); }
static const u64 K[80] = {
0x428a2f98d728ae22,0x7137449123ef65cd,0xb5c0fbcfec4d3b2f,0xe9b5dba58189dbbc,
0x3956c25bf348b538,0x59f111f1b605d019,0x923f82a4af194f9b,0xab1c5ed5da6d8118,
0xd807aa98a3030242,0x12835b0145706fbe,0x243185be4ee4b28c,0x550c7dc3d5ffb4e2,
0x72be5d74f27b896f,0x80deb1fe3b1696b1,0x9bdc06a725c71235,0xc19bf174cf692694,
0xe49b69c19ef14ad2,0xefbe4786384f25e3,0x0fc19dc68b8cd5b5,0x240ca1cc77ac9c65,
0x2de92c6f592b0275,0x4a7484aa6ea6e483,0x5cb0a9dcbd41fbd4,0x76f988da831153b5,
0x983e5152ee66dfab,0xa831c66d2db43210,0xb00327c898fb213f,0xbf597fc7beef0ee4,
0xc6e00bf33da88fc2,0xd5a79147930aa725,0x06ca6351e003826f,0x142929670a0e6e70,
0x27b70a8546d22ffc,0x2e1b21385c26c926,0x4d2c6dfc5ac42aed,0x53380d139d95b3df,
0x650a73548baf63de,0x766a0abb3c77b2a8,0x81c2c92e47edaee6,0x92722c851482353b,
0xa2bfe8a14cf10364,0xa81a664bbc423001,0xc24b8b70d0f89791,0xc76c51a30654be30,
0xd192e819d6ef5218,0xd69906245565a910,0xf40e35855771202a,0x106aa07032bbd1b8,
0x19a4c116b8d2d0c8,0x1e376c085141ab53,0x2748774cdf8eeb99,0x34b0bcb5e19b48a8,
0x391c0cb3c5c95a63,0x4ed8aa4ae3418acb,0x5b9cca4f7763e373,0x682e6ff3d6b2b8a3,
0x748f82ee5defb2fc,0x78a5636f43172f60,0x84c87814a1f0ab72,0x8cc702081a6439ec,
0x90befffa23631e28,0xa4506cebde82bde9,0xbef9a3f7b2c67915,0xc67178f2e372532b,
0xca273eceea26619c,0xd186b8c721c0c207,0xeada7dd6cde0eb1e,0xf57d4f7fee6ed178,
0x06f067aa72176fba,0x0a637dc5a2c898a6,0x113f9804bef90dae,0x1b710b35131c471b,
0x28db77f523047d84,0x32caab7b40c72493,0x3c9ebe0a15c9bebc,0x431d67c49c100d4c,
0x4cc5d4becb3e42b6,0x597f299cfc657e2a,0x5fcb6fab3ad6faec,0x6c44198c4a475817
};
static void sha512_compress(crypto_sha512_ctx *ctx)
{
u64 a = ctx->hash[0]; u64 b = ctx->hash[1];
u64 c = ctx->hash[2]; u64 d = ctx->hash[3];
u64 e = ctx->hash[4]; u64 f = ctx->hash[5];
u64 g = ctx->hash[6]; u64 h = ctx->hash[7];
FOR (j, 0, 16) {
u64 in = K[j] + ctx->input[j];
u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in;
u64 t2 = big_sigma0(a) + maj(a, b, c);
h = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
size_t i16 = 0;
FOR(i, 1, 5) {
i16 += 16;
FOR (j, 0, 16) {
ctx->input[j] += lit_sigma1(ctx->input[(j- 2) & 15]);
ctx->input[j] += lit_sigma0(ctx->input[(j-15) & 15]);
ctx->input[j] += ctx->input[(j- 7) & 15];
u64 in = K[i16 + j] + ctx->input[j];
u64 t1 = big_sigma1(e) + ch (e, f, g) + h + in;
u64 t2 = big_sigma0(a) + maj(a, b, c);
h = g; g = f; f = e; e = d + t1;
d = c; c = b; b = a; a = t1 + t2;
}
}
ctx->hash[0] += a; ctx->hash[1] += b;
ctx->hash[2] += c; ctx->hash[3] += d;
ctx->hash[4] += e; ctx->hash[5] += f;
ctx->hash[6] += g; ctx->hash[7] += h;
}
// Write 1 input byte
static void sha512_set_input(crypto_sha512_ctx *ctx, u8 input)
{
size_t word = ctx->input_idx >> 3;
size_t byte = ctx->input_idx & 7;
ctx->input[word] |= (u64)input << (8 * (7 - byte));
}
// Increment a 128-bit "word".
static void sha512_incr(u64 x[2], u64 y)
{
x[1] += y;
if (x[1] < y) {
x[0]++;
}
}
void crypto_sha512_init(crypto_sha512_ctx *ctx)
{
ctx->hash[0] = 0x6a09e667f3bcc908;
ctx->hash[1] = 0xbb67ae8584caa73b;
ctx->hash[2] = 0x3c6ef372fe94f82b;
ctx->hash[3] = 0xa54ff53a5f1d36f1;
ctx->hash[4] = 0x510e527fade682d1;
ctx->hash[5] = 0x9b05688c2b3e6c1f;
ctx->hash[6] = 0x1f83d9abfb41bd6b;
ctx->hash[7] = 0x5be0cd19137e2179;
ctx->input_size[0] = 0;
ctx->input_size[1] = 0;
ctx->input_idx = 0;
ZERO(ctx->input, 16);
}
void crypto_sha512_update(crypto_sha512_ctx *ctx,
const u8 *message, size_t message_size)
{
// Avoid undefined NULL pointer increments with empty messages
if (message_size == 0) {
return;
}
// Align ourselves with word boundaries
if ((ctx->input_idx & 7) != 0) {
size_t nb_bytes = MIN(align(ctx->input_idx, 8), message_size);
FOR (i, 0, nb_bytes) {
sha512_set_input(ctx, message[i]);
ctx->input_idx++;
}
message += nb_bytes;
message_size -= nb_bytes;
}
// Align ourselves with block boundaries
if ((ctx->input_idx & 127) != 0) {
size_t nb_words = MIN(align(ctx->input_idx, 128), message_size) >> 3;
load64_be_buf(ctx->input + (ctx->input_idx >> 3), message, nb_words);
ctx->input_idx += nb_words << 3;
message += nb_words << 3;
message_size -= nb_words << 3;
}
// Compress block if needed
if (ctx->input_idx == 128) {
sha512_incr(ctx->input_size, 1024); // size is in bits
sha512_compress(ctx);
ctx->input_idx = 0;
ZERO(ctx->input, 16);
}
// Process the message block by block
FOR (i, 0, message_size >> 7) { // number of blocks
load64_be_buf(ctx->input, message, 16);
sha512_incr(ctx->input_size, 1024); // size is in bits
sha512_compress(ctx);
ctx->input_idx = 0;
ZERO(ctx->input, 16);
message += 128;
}
message_size &= 127;
if (message_size != 0) {
// Remaining words
size_t nb_words = message_size >> 3;
load64_be_buf(ctx->input, message, nb_words);
ctx->input_idx += nb_words << 3;
message += nb_words << 3;
message_size -= nb_words << 3;
// Remaining bytes
FOR (i, 0, message_size) {
sha512_set_input(ctx, message[i]);
ctx->input_idx++;
}
}
}
void crypto_sha512_final(crypto_sha512_ctx *ctx, u8 hash[64])
{
// Add padding bit
if (ctx->input_idx == 0) {
ZERO(ctx->input, 16);
}
sha512_set_input(ctx, 128);
// Update size
sha512_incr(ctx->input_size, ctx->input_idx * 8);
// Compress penultimate block (if any)
if (ctx->input_idx > 111) {
sha512_compress(ctx);
ZERO(ctx->input, 14);
}
// Compress last block
ctx->input[14] = ctx->input_size[0];
ctx->input[15] = ctx->input_size[1];
sha512_compress(ctx);
// Copy hash to output (big endian)
FOR (i, 0, 8) {
store64_be(hash + i*8, ctx->hash[i]);
}
WIPE_CTX(ctx);
}
void crypto_sha512(u8 hash[64], const u8 *message, size_t message_size)
{
crypto_sha512_ctx ctx;
crypto_sha512_init (&ctx);
crypto_sha512_update(&ctx, message, message_size);
crypto_sha512_final (&ctx, hash);
}
////////////////////
/// HMAC SHA 512 ///
////////////////////
void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx,
const u8 *key, size_t key_size)
{
// hash key if it is too long
if (key_size > 128) {
crypto_sha512(ctx->key, key, key_size);
key = ctx->key;
key_size = 64;
}
// Compute inner key: padded key XOR 0x36
FOR (i, 0, key_size) { ctx->key[i] = key[i] ^ 0x36; }
FOR (i, key_size, 128) { ctx->key[i] = 0x36; }
// Start computing inner hash
crypto_sha512_init (&ctx->ctx);
crypto_sha512_update(&ctx->ctx, ctx->key, 128);
}
void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx,
const u8 *message, size_t message_size)
{
crypto_sha512_update(&ctx->ctx, message, message_size);
}
void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, u8 hmac[64])
{
// Finish computing inner hash
crypto_sha512_final(&ctx->ctx, hmac);
// Compute outer key: padded key XOR 0x5c
FOR (i, 0, 128) {
ctx->key[i] ^= 0x36 ^ 0x5c;
}
// Compute outer hash
crypto_sha512_init (&ctx->ctx);
crypto_sha512_update(&ctx->ctx, ctx->key , 128);
crypto_sha512_update(&ctx->ctx, hmac, 64);
crypto_sha512_final (&ctx->ctx, hmac); // outer hash
WIPE_CTX(ctx);
}
void crypto_sha512_hmac(u8 hmac[64], const u8 *key, size_t key_size,
const u8 *message, size_t message_size)
{
crypto_sha512_hmac_ctx ctx;
crypto_sha512_hmac_init (&ctx, key, key_size);
crypto_sha512_hmac_update(&ctx, message, message_size);
crypto_sha512_hmac_final (&ctx, hmac);
}
////////////////////
/// HKDF SHA 512 ///
////////////////////
void crypto_sha512_hkdf_expand(u8 *okm, size_t okm_size,
const u8 *prk, size_t prk_size,
const u8 *info, size_t info_size)
{
int not_first = 0;
u8 ctr = 1;
u8 blk[64];
while (okm_size > 0) {
size_t out_size = MIN(okm_size, sizeof(blk));
crypto_sha512_hmac_ctx ctx;
crypto_sha512_hmac_init(&ctx, prk , prk_size);
if (not_first) {
// For some reason HKDF uses some kind of CBC mode.
// For some reason CTR mode alone wasn't enough.
// Like what, they didn't trust HMAC in 2010? Really??
crypto_sha512_hmac_update(&ctx, blk , sizeof(blk));
}
crypto_sha512_hmac_update(&ctx, info, info_size);
crypto_sha512_hmac_update(&ctx, &ctr, 1);
crypto_sha512_hmac_final(&ctx, blk);
COPY(okm, blk, out_size);
not_first = 1;
okm += out_size;
okm_size -= out_size;
ctr++;
}
WIPE_BUFFER(blk);
}
void crypto_sha512_hkdf(u8 *okm , size_t okm_size,
const u8 *ikm , size_t ikm_size,
const u8 *salt, size_t salt_size,
const u8 *info, size_t info_size)
{
// Extract
u8 prk[64];
crypto_sha512_hmac(prk, salt, salt_size, ikm, ikm_size);
// Expand
crypto_sha512_hkdf_expand(okm, okm_size, prk, sizeof(prk), info, info_size);
WIPE_BUFFER(prk);
}
///////////////
/// Ed25519 ///
///////////////
void crypto_ed25519_key_pair(u8 secret_key[64], u8 public_key[32], u8 seed[32])
{
u8 a[64];
COPY(a, seed, 32); // a[ 0..31] = seed
crypto_wipe(seed, 32);
COPY(secret_key, a, 32); // secret key = seed
crypto_sha512(a, a, 32); // a[ 0..31] = scalar
crypto_eddsa_trim_scalar(a, a); // a[ 0..31] = trimmed scalar
crypto_eddsa_scalarbase(public_key, a); // public key = [trimmed scalar]B
COPY(secret_key + 32, public_key, 32); // secret key includes public half
WIPE_BUFFER(a);
}
static void hash_reduce(u8 h[32],
const u8 *a, size_t a_size,
const u8 *b, size_t b_size,
const u8 *c, size_t c_size,
const u8 *d, size_t d_size)
{
u8 hash[64];
crypto_sha512_ctx ctx;
crypto_sha512_init (&ctx);
crypto_sha512_update(&ctx, a, a_size);
crypto_sha512_update(&ctx, b, b_size);
crypto_sha512_update(&ctx, c, c_size);
crypto_sha512_update(&ctx, d, d_size);
crypto_sha512_final (&ctx, hash);
crypto_eddsa_reduce(h, hash);
}
static void ed25519_dom_sign(u8 signature[64], const u8 secret_key[64],
const u8 *dom, size_t dom_size,
const u8 *message, size_t message_size)
{
u8 a[64]; // secret scalar and prefix
u8 r[32]; // secret deterministic "random" nonce
u8 h[32]; // publically verifiable hash of the message (not wiped)
u8 R[32]; // first half of the signature (allows overlapping inputs)
const u8 *pk = secret_key + 32;
crypto_sha512(a, secret_key, 32);
crypto_eddsa_trim_scalar(a, a);
hash_reduce(r, dom, dom_size, a + 32, 32, message, message_size, 0, 0);
crypto_eddsa_scalarbase(R, r);
hash_reduce(h, dom, dom_size, R, 32, pk, 32, message, message_size);
COPY(signature, R, 32);
crypto_eddsa_mul_add(signature + 32, h, a, r);
WIPE_BUFFER(a);
WIPE_BUFFER(r);
}
void crypto_ed25519_sign(u8 signature [64], const u8 secret_key[64],
const u8 *message, size_t message_size)
{
ed25519_dom_sign(signature, secret_key, 0, 0, message, message_size);
}
int crypto_ed25519_check(const u8 signature[64], const u8 public_key[32],
const u8 *msg, size_t msg_size)
{
u8 h_ram[32];
hash_reduce(h_ram, signature, 32, public_key, 32, msg, msg_size, 0, 0);
return crypto_eddsa_check_equation(signature, public_key, h_ram);
}
static const u8 domain[34] = "SigEd25519 no Ed25519 collisions\1";
void crypto_ed25519_ph_sign(uint8_t signature[64], const uint8_t secret_key[64],
const uint8_t message_hash[64])
{
ed25519_dom_sign(signature, secret_key, domain, sizeof(domain),
message_hash, 64);
}
int crypto_ed25519_ph_check(const uint8_t sig[64], const uint8_t pk[32],
const uint8_t msg_hash[64])
{
u8 h_ram[32];
hash_reduce(h_ram, domain, sizeof(domain), sig, 32, pk, 32, msg_hash, 64);
return crypto_eddsa_check_equation(sig, pk, h_ram);
}
#ifdef MONOCYPHER_CPP_NAMESPACE
}
#endif
+140
View File
@@ -0,0 +1,140 @@
// Monocypher version 4.0.3
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#ifndef ED25519_H
#define ED25519_H
#include "monocypher.h"
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#elif defined(__cplusplus)
extern "C" {
#endif
////////////////////////
/// Type definitions ///
////////////////////////
// Do not rely on the size or content on any of those types,
// they may change without notice.
typedef struct {
uint64_t hash[8];
uint64_t input[16];
uint64_t input_size[2];
size_t input_idx;
} crypto_sha512_ctx;
typedef struct {
uint8_t key[128];
crypto_sha512_ctx ctx;
} crypto_sha512_hmac_ctx;
// SHA 512
// -------
void crypto_sha512_init (crypto_sha512_ctx *ctx);
void crypto_sha512_update(crypto_sha512_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_sha512_final (crypto_sha512_ctx *ctx, uint8_t hash[64]);
void crypto_sha512(uint8_t hash[64],
const uint8_t *message, size_t message_size);
// SHA 512 HMAC
// ------------
void crypto_sha512_hmac_init(crypto_sha512_hmac_ctx *ctx,
const uint8_t *key, size_t key_size);
void crypto_sha512_hmac_update(crypto_sha512_hmac_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_sha512_hmac_final(crypto_sha512_hmac_ctx *ctx, uint8_t hmac[64]);
void crypto_sha512_hmac(uint8_t hmac[64],
const uint8_t *key , size_t key_size,
const uint8_t *message, size_t message_size);
// SHA 512 HKDF
// ------------
void crypto_sha512_hkdf_expand(uint8_t *okm, size_t okm_size,
const uint8_t *prk, size_t prk_size,
const uint8_t *info, size_t info_size);
void crypto_sha512_hkdf(uint8_t *okm , size_t okm_size,
const uint8_t *ikm , size_t ikm_size,
const uint8_t *salt, size_t salt_size,
const uint8_t *info, size_t info_size);
// Ed25519
// -------
// Signatures (EdDSA with curve25519 + SHA-512)
// --------------------------------------------
void crypto_ed25519_key_pair(uint8_t secret_key[64],
uint8_t public_key[32],
uint8_t seed[32]);
void crypto_ed25519_sign(uint8_t signature [64],
const uint8_t secret_key[64],
const uint8_t *message, size_t message_size);
int crypto_ed25519_check(const uint8_t signature [64],
const uint8_t public_key[32],
const uint8_t *message, size_t message_size);
// Pre-hash variants
void crypto_ed25519_ph_sign(uint8_t signature [64],
const uint8_t secret_key [64],
const uint8_t message_hash[64]);
int crypto_ed25519_ph_check(const uint8_t signature [64],
const uint8_t public_key [32],
const uint8_t message_hash[64]);
#ifdef __cplusplus
}
#endif
#endif // ED25519_H
File diff suppressed because it is too large. Load diff
+321
View File
@@ -0,0 +1,321 @@
// Monocypher version 4.0.3
//
// This file is dual-licensed. Choose whichever licence you want from
// the two licences listed below.
//
// The first licence is a regular 2-clause BSD licence. The second licence
// is the CC-0 from Creative Commons. It is intended to release Monocypher
// to the public domain. The BSD licence serves as a fallback option.
//
// SPDX-License-Identifier: BSD-2-Clause OR CC0-1.0
//
// ------------------------------------------------------------------------
//
// Copyright (c) 2017-2019, Loup Vaillant
// All rights reserved.
//
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions are
// met:
//
// 1. Redistributions of source code must retain the above copyright
// notice, this list of conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright
// notice, this list of conditions and the following disclaimer in the
// documentation and/or other materials provided with the
// distribution.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
// HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
//
// ------------------------------------------------------------------------
//
// Written in 2017-2019 by Loup Vaillant
//
// To the extent possible under law, the author(s) have dedicated all copyright
// and related neighboring rights to this software to the public domain
// worldwide. This software is distributed without any warranty.
//
// You should have received a copy of the CC0 Public Domain Dedication along
// with this software. If not, see
// <https://creativecommons.org/publicdomain/zero/1.0/>
#ifndef MONOCYPHER_H
#define MONOCYPHER_H
#include <stddef.h>
#include <stdint.h>
#ifdef MONOCYPHER_CPP_NAMESPACE
namespace MONOCYPHER_CPP_NAMESPACE {
#elif defined(__cplusplus)
extern "C" {
#endif
// Constant time comparisons
// -------------------------
// Return 0 if a and b are equal, -1 otherwise
int crypto_verify16(const uint8_t a[16], const uint8_t b[16]);
int crypto_verify32(const uint8_t a[32], const uint8_t b[32]);
int crypto_verify64(const uint8_t a[64], const uint8_t b[64]);
// Erase sensitive data
// --------------------
void crypto_wipe(void *secret, size_t size);
// Authenticated encryption
// ------------------------
void crypto_aead_lock(uint8_t *cipher_text,
uint8_t mac [16],
const uint8_t key [32],
const uint8_t nonce[24],
const uint8_t *ad, size_t ad_size,
const uint8_t *plain_text, size_t text_size);
int crypto_aead_unlock(uint8_t *plain_text,
const uint8_t mac [16],
const uint8_t key [32],
const uint8_t nonce[24],
const uint8_t *ad, size_t ad_size,
const uint8_t *cipher_text, size_t text_size);
// Authenticated stream
// --------------------
typedef struct {
uint64_t counter;
uint8_t key[32];
uint8_t nonce[8];
} crypto_aead_ctx;
void crypto_aead_init_x(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[24]);
void crypto_aead_init_djb(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[8]);
void crypto_aead_init_ietf(crypto_aead_ctx *ctx,
const uint8_t key[32], const uint8_t nonce[12]);
void crypto_aead_write(crypto_aead_ctx *ctx,
uint8_t *cipher_text,
uint8_t mac[16],
const uint8_t *ad , size_t ad_size,
const uint8_t *plain_text, size_t text_size);
int crypto_aead_read(crypto_aead_ctx *ctx,
uint8_t *plain_text,
const uint8_t mac[16],
const uint8_t *ad , size_t ad_size,
const uint8_t *cipher_text, size_t text_size);
// General purpose hash (BLAKE2b)
// ------------------------------
// Direct interface
void crypto_blake2b(uint8_t *hash, size_t hash_size,
const uint8_t *message, size_t message_size);
void crypto_blake2b_keyed(uint8_t *hash, size_t hash_size,
const uint8_t *key, size_t key_size,
const uint8_t *message, size_t message_size);
// Incremental interface
typedef struct {
// Do not rely on the size or contents of this type,
// for they may change without notice.
uint64_t hash[8];
uint64_t input_offset[2];
uint64_t input[16];
size_t input_idx;
size_t hash_size;
} crypto_blake2b_ctx;
void crypto_blake2b_init(crypto_blake2b_ctx *ctx, size_t hash_size);
void crypto_blake2b_keyed_init(crypto_blake2b_ctx *ctx, size_t hash_size,
const uint8_t *key, size_t key_size);
void crypto_blake2b_update(crypto_blake2b_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_blake2b_final(crypto_blake2b_ctx *ctx, uint8_t *hash);
// Password key derivation (Argon2)
// --------------------------------
#define CRYPTO_ARGON2_D 0
#define CRYPTO_ARGON2_I 1
#define CRYPTO_ARGON2_ID 2
typedef struct {
uint32_t algorithm; // Argon2d, Argon2i, Argon2id
uint32_t nb_blocks; // memory hardness, >= 8 * nb_lanes
uint32_t nb_passes; // CPU hardness, >= 1 (>= 3 recommended for Argon2i)
uint32_t nb_lanes; // parallelism level (single threaded anyway)
} crypto_argon2_config;
typedef struct {
const uint8_t *pass;
const uint8_t *salt;
uint32_t pass_size;
uint32_t salt_size; // 16 bytes recommended
} crypto_argon2_inputs;
typedef struct {
const uint8_t *key; // may be NULL if no key
const uint8_t *ad; // may be NULL if no additional data
uint32_t key_size; // 0 if no key (32 bytes recommended otherwise)
uint32_t ad_size; // 0 if no additional data
} crypto_argon2_extras;
extern const crypto_argon2_extras crypto_argon2_no_extras;
void crypto_argon2(uint8_t *hash, uint32_t hash_size, void *work_area,
crypto_argon2_config config,
crypto_argon2_inputs inputs,
crypto_argon2_extras extras);
// Key exchange (X-25519)
// ----------------------
// Shared secrets are not quite random.
// Hash them to derive an actual shared key.
void crypto_x25519_public_key(uint8_t public_key[32],
const uint8_t secret_key[32]);
void crypto_x25519(uint8_t raw_shared_secret[32],
const uint8_t your_secret_key [32],
const uint8_t their_public_key [32]);
// Conversion to EdDSA
void crypto_x25519_to_eddsa(uint8_t eddsa[32], const uint8_t x25519[32]);
// scalar "division"
// Used for OPRF. Be aware that exponential blinding is less secure
// than Diffie-Hellman key exchange.
void crypto_x25519_inverse(uint8_t blind_salt [32],
const uint8_t private_key[32],
const uint8_t curve_point[32]);
// "Dirty" versions of x25519_public_key().
// Use with crypto_elligator_rev().
// Leaks 3 bits of the private key.
void crypto_x25519_dirty_small(uint8_t pk[32], const uint8_t sk[32]);
void crypto_x25519_dirty_fast (uint8_t pk[32], const uint8_t sk[32]);
// Signatures
// ----------
// EdDSA with curve25519 + BLAKE2b
void crypto_eddsa_key_pair(uint8_t secret_key[64],
uint8_t public_key[32],
uint8_t seed[32]);
void crypto_eddsa_sign(uint8_t signature [64],
const uint8_t secret_key[64],
const uint8_t *message, size_t message_size);
int crypto_eddsa_check(const uint8_t signature [64],
const uint8_t public_key[32],
const uint8_t *message, size_t message_size);
// Conversion to X25519
void crypto_eddsa_to_x25519(uint8_t x25519[32], const uint8_t eddsa[32]);
// EdDSA building blocks
void crypto_eddsa_trim_scalar(uint8_t out[32], const uint8_t in[32]);
void crypto_eddsa_reduce(uint8_t reduced[32], const uint8_t expanded[64]);
void crypto_eddsa_mul_add(uint8_t r[32],
const uint8_t a[32],
const uint8_t b[32],
const uint8_t c[32]);
void crypto_eddsa_scalarbase(uint8_t point[32], const uint8_t scalar[32]);
int crypto_eddsa_check_equation(const uint8_t signature[64],
const uint8_t public_key[32],
const uint8_t h_ram[32]);
// Chacha20
// --------
// Specialised hash.
// Used to hash X25519 shared secrets.
void crypto_chacha20_h(uint8_t out[32],
const uint8_t key[32],
const uint8_t in [16]);
// Unauthenticated stream cipher.
// Don't forget to add authentication.
uint64_t crypto_chacha20_djb(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[8],
uint64_t ctr);
uint32_t crypto_chacha20_ietf(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[12],
uint32_t ctr);
uint64_t crypto_chacha20_x(uint8_t *cipher_text,
const uint8_t *plain_text,
size_t text_size,
const uint8_t key[32],
const uint8_t nonce[24],
uint64_t ctr);
// Poly 1305
// ---------
// This is a *one time* authenticator.
// Disclosing the mac reveals the key.
// See crypto_lock() on how to use it properly.
// Direct interface
void crypto_poly1305(uint8_t mac[16],
const uint8_t *message, size_t message_size,
const uint8_t key[32]);
// Incremental interface
typedef struct {
// Do not rely on the size or contents of this type,
// for they may change without notice.
uint8_t c[16]; // chunk of the message
size_t c_idx; // How many bytes are there in the chunk.
uint32_t r [4]; // constant multiplier (from the secret key)
uint32_t pad[4]; // random number added at the end (from the secret key)
uint32_t h [5]; // accumulated hash
} crypto_poly1305_ctx;
void crypto_poly1305_init (crypto_poly1305_ctx *ctx, const uint8_t key[32]);
void crypto_poly1305_update(crypto_poly1305_ctx *ctx,
const uint8_t *message, size_t message_size);
void crypto_poly1305_final (crypto_poly1305_ctx *ctx, uint8_t mac[16]);
// Elligator 2
// -----------
// Elligator mappings proper
void crypto_elligator_map(uint8_t curve [32], const uint8_t hidden[32]);
int crypto_elligator_rev(uint8_t hidden[32], const uint8_t curve [32],
uint8_t tweak);
// Easy to use key pair generation
void crypto_elligator_key_pair(uint8_t hidden[32], uint8_t secret_key[32],
uint8_t seed[32]);
#ifdef __cplusplus
}
#endif
#endif // MONOCYPHER_H
+540 -219
View File
@@ -15,63 +15,35 @@
#include "common.h"
#include "pkg.h"
/* null dependencies/conflicts for error throwing */
dep_t null_dep = { "null", {-1, -1, -1, NULL} };
conf_t null_conf = { "null", {-1, -1, -1, NULL} };
char
*join_with_cwd(const char *path)
{
char cwd[PATH_MAX];
if (!getcwd(cwd, sizeof(cwd))) {
perror("getcwd");
return NULL;
}
size_t length = strlen(cwd + strlen(path) + 2);
char *result = malloc(length);
if (!result) { return NULL; }
snprintf(result, length, "%s/%s", cwd, path);
return result;
}
/*
* gl_pkgver
* DESCRIPTION: takes three integers and formats them as a semver_t struct
* PARAMETERS:
* ~ int major (the MAJOR number)
* ~ int minor (the MINOR number)
* ~ int patch (the PATCH number)
* RETURNS:
* ~ a pointer to a semver_t struct
*/
semver_t
gl_pkgver(int major, int minor, int patch)
{
semver_t pkgver = {major, minor, patch, NULL};
return pkgver;
}
/* "MAJ.MIN.PAT[-TAG]". Each number is read with strtol and clamped to
* 0..INT_MAX (a manifest is untrusted: sscanf's %d overflowing is
* undefined); anything missing or unparseable is 0. */
semver_t
gl_parse_semver(const char *str)
{
semver_t v = {0, 0, 0, NULL};
if (!str) { return v; }
char buffer[64];
strncpy(buffer, str, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '\0';
snprintf(buffer, sizeof(buffer), "%s", str);
char *dash = strchr(buffer, '-');
if (dash) {
*dash = '\0';
dash++;
v.tag = strdup(dash);
v.tag = strdup(dash + 1);
}
sscanf(buffer, "%d.%d.%d", &v.maj, &v.min, &v.pat);
int *parts[] = { &v.maj, &v.min, &v.pat };
const char *p = buffer;
for (int i = 0; i < 3 && *p; i++) {
char *end;
errno = 0;
long n = strtol(p, &end, 10);
if (end == p) { break; }
*parts[i] = (errno || n < 0) ? 0 : n > INT_MAX ? INT_MAX : (int)n;
if (*end != '.') { break; }
p = end + 1;
}
return v;
}
@@ -81,47 +53,6 @@ gl_free_semver(semver_t *v)
if (v && v->tag) { free (v->tag); };
}
gl_pkg_t empty = {
"null",
{-1, -1, -1, NULL},
"Null package for internal use. Returned upon an error.",
"null@null.null",
"null",
"null",
&null_dep,
&null_conf
};
int
gl_mkdir(dir_t *dir)
{
if (mkdir(dir -> path, dir -> mode) == 0) {
return 0;
}
if (errno == EEXIST) { return 0; }
perror("libglacier: mkdir");
return -1;
}
gl_pkg_t
*gl_spawn_pkg(char *name, semver_t *ver, char *desc, char *maint, char *copy, char *repo, dep_t *dep, conf_t *conf)
{
gl_pkg_t *new_pkg = malloc(sizeof(gl_pkg_t));
if (!new_pkg) { return NULL; }
new_pkg -> pkg_name = name;
new_pkg -> pkg_ver = *ver;
new_pkg -> pkg_desc = desc;
new_pkg -> pkg_maint = maint;
new_pkg -> pkg_copyright = copy;
new_pkg -> pkg_repo = repo;
new_pkg -> pkg_dependencies = dep;
new_pkg -> pkg_conflicts = conf;
return new_pkg;
}
/*
* gl_valid_pkg_ident
* DESCRIPTION: checks that a package name or repo name is safe to use as a
@@ -151,26 +82,103 @@ gl_valid_pkg_ident(const char *s)
return true;
}
/* A manifest comes out of an untrusted archive. libconfig honours
* `@include "FILE"` at the start of a line, which would have it read any
* file (as root, under syspkg): such a manifest is refused. */
static char *
read_manifest_text(const char *filename)
{
FILE *f = fopen(filename, "rb");
if (!f) { return NULL; }
enum { MAX_MANIFEST = 1024 * 1024 };
char *buf = malloc(MAX_MANIFEST + 1);
size_t n = buf ? fread(buf, 1, MAX_MANIFEST + 1, f) : 0;
bool bad = !buf || ferror(f) || n > MAX_MANIFEST || memchr(buf, '\0', n);
fclose(f);
if (bad) {
free(buf);
return NULL;
}
buf[n] = '\0';
for (const char *line = buf; line; line = strchr(line, '\n')) {
if (*line == '\n') { line++; }
while (*line == ' ' || *line == '\t') { line++; }
if (strncmp(line, "@include", 8) == 0) {
free(buf);
return NULL;
}
}
return buf;
}
/* PKG_DEPS / PKG_CONFS: a list of { PKG_NAME; PKG_VER; }, each name a
* gl_valid_pkg_ident, into a NULL-name-terminated array of
* (name, version) pairs, names and vers parallel. false if malformed. */
static bool
manifest_relations(config_t *cfg, const char *key, char ***names, semver_t **vers)
{
*names = NULL;
*vers = NULL;
config_setting_t *l = config_lookup(cfg, key);
if (!l) { return true; }
if (!config_setting_is_list(l)) { return false; }
int count = config_setting_length(l);
*names = calloc((size_t)count + 1, sizeof(**names));
*vers = calloc((size_t)count + 1, sizeof(**vers));
if (!*names || !*vers) { return false; }
for (int i = 0; i < count; i++) {
config_setting_t *d = config_setting_get_elem(l, (unsigned)i);
const char *name = NULL, *ver = NULL;
config_setting_lookup_string(d, "PKG_NAME", &name);
config_setting_lookup_string(d, "PKG_VER", &ver);
if (!gl_valid_pkg_ident(name) || !((*names)[i] = strdup(name))) {
return false;
}
(*vers)[i] = gl_parse_semver(ver);
}
return true;
}
static void
free_relations(char **names, semver_t *vers)
{
for (size_t i = 0; names && names[i]; i++) {
free(names[i]);
gl_free_semver(&vers[i]);
}
free(names);
free(vers);
}
int
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
{
char *text = read_manifest_text(filename);
if (!text) { return ENOENT; }
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)) {
int parsed = config_read_string(&cfg, text);
free(text);
if (!parsed) {
config_destroy(&cfg);
return ENOENT;
}
/* Validate before anything is strdup'd, so a rejected manifest leaves
* pkg untouched and the caller has nothing to free. */
const char *tmp_str, *name = NULL, *repo = NULL;
char **dnames, **cnames;
semver_t *dvers, *cvers;
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)) {
bool ok = gl_valid_pkg_ident(name) && gl_valid_pkg_ident(repo);
bool rels = manifest_relations(&cfg, "PKG_DEPS", &dnames, &dvers);
rels = manifest_relations(&cfg, "PKG_CONFS", &cnames, &cvers) && rels;
if (!ok || !rels) {
free_relations(dnames, dvers);
free_relations(cnames, cvers);
config_destroy(&cfg);
return EINVAL;
}
@@ -182,102 +190,37 @@ gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
if (config_lookup_string(&cfg, "PKG_COPYRIGHT", &tmp_str)) { pkg->pkg_copyright = 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");
if (deps && config_setting_is_list(deps)) {
int count = config_setting_length(deps);
pkg->pkg_dependencies = calloc((size_t)(count + 1), sizeof(dep_t));
for (int i = 0; i < count; i++) {
config_setting_t *d = config_setting_get_elem(deps, (unsigned int)i);
const char *dep_name = NULL;
const char *dep_ver = NULL;
config_setting_lookup_string(d, "PKG_NAME", &dep_name);
config_setting_lookup_string(d, "PKG_VER", &dep_ver);
pkg->pkg_dependencies[i].dep_name = dep_name ? strdup(dep_name) : NULL;
pkg->pkg_dependencies[i].dep_ver = gl_parse_semver(dep_ver);
if (dnames) {
size_t n = 0;
while (dnames[n]) { n++; }
pkg->pkg_dependencies = calloc(n + 1, sizeof(dep_t));
for (size_t i = 0; pkg->pkg_dependencies && i < n; i++) {
pkg->pkg_dependencies[i].dep_name = dnames[i];
pkg->pkg_dependencies[i].dep_ver = dvers[i];
dnames[i] = NULL;
}
}
confs = config_lookup(&cfg, "PKG_CONFS");
if (confs && config_setting_is_list(confs)) {
int count = config_setting_length(confs);
pkg->pkg_conflicts = calloc((size_t)(count + 1), sizeof(conf_t));
for (int i = 0; i < count; i++) {
config_setting_t *c = config_setting_get_elem(confs, (unsigned int)i);
const char *conf_name = NULL;
const char *conf_ver = NULL;
config_setting_lookup_string(c, "PKG_NAME", &conf_name);
config_setting_lookup_string(c, "PKG_VER", &conf_ver);
pkg->pkg_conflicts[i].conf_name = conf_name ? strdup(conf_name) : NULL;
pkg->pkg_conflicts[i].conf_ver = gl_parse_semver(conf_ver);
if (cnames) {
size_t n = 0;
while (cnames[n]) { n++; }
pkg->pkg_conflicts = calloc(n + 1, sizeof(conf_t));
for (size_t i = 0; pkg->pkg_conflicts && i < n; i++) {
pkg->pkg_conflicts[i].conf_name = cnames[i];
pkg->pkg_conflicts[i].conf_ver = cvers[i];
cnames[i] = NULL;
}
}
free_relations(dnames, dvers);
free_relations(cnames, cvers);
config_destroy(&cfg);
return 0;
}
char
*gl_cat_dir(const dir_t *a, const dir_t *b)
{
size_t length = strlen(a->path) + strlen(b->path) + 2;
char *result = malloc(length);
if (!result) { return NULL; }
snprintf(result, length, "%s/%s", a->path, b-> path);
return result;
}
char
*gl_cat_dir_and_ver(const dir_t *base, const gl_pkg_t *pkg)
{
char version[32];
snprintf(version, sizeof(version), "%d.%d.%d",
pkg->pkg_ver.maj, pkg->pkg_ver.min, pkg->pkg_ver.pat);
size_t length = strlen(base->path) + strlen(pkg->pkg_name) + strlen(version) + 3;
char *result = malloc(length);
if (!result) { return NULL; }
snprintf(result, length, "%s/%s-%s", base->path, pkg->pkg_name, version);
return result;
}
dir_t
gl_return_dir(const dir_t *a, const dir_t *b, mode_t mode)
{
char *path = gl_cat_dir(a, b);
dir_t result = { path, mode };
return result;
}
dir_t
gl_return_dir_and_ver(const dir_t *base, gl_pkg_t *pkg, mode_t mode)
{
char *path = gl_cat_dir_and_ver(base, pkg);
dir_t result = { path, mode };
return result;
}
void
gl_free_path(char *path)
{
free(path);
}
void
gl_free_dir(dir_t *dir)
{
if (dir && dir->path) {
free((char *)dir->path);
dir->path = NULL;
if (!pkg->pkg_name || !pkg->pkg_repo || (dnames && !pkg->pkg_dependencies) ||
(cnames && !pkg->pkg_conflicts)) {
gl_free_pkg(pkg);
memset(pkg, 0, sizeof(*pkg));
return ENOMEM;
}
return 0;
}
void
@@ -310,43 +253,6 @@ gl_free_pkg(gl_pkg_t *pkg)
}
}
void
gl_show_pkg_props(const gl_pkg_t *pkg)
{
if (!pkg) {
printf("(null pkg)\n");
return;
}
printf("Package: %s\n", pkg->pkg_name);
printf("Version: %d.%d.%d\n", pkg->pkg_ver.maj, pkg->pkg_ver.min, pkg->pkg_ver.pat);
printf("Description: %s\n", pkg->pkg_desc);
printf("Maintainer: %s\n", pkg->pkg_maint);
printf("Copyright: %s\n", pkg->pkg_copyright);
if (pkg->pkg_dependencies) {
printf("Dependencies:\n");
dep_t *dep = pkg->pkg_dependencies;
printf("\t~ %s (%d.%d.%d)\n",
dep->dep_name,
dep->dep_ver.maj, dep->dep_ver.min, dep->dep_ver.pat);
}
else {
printf("Dependencies: (none)\n");
}
if (pkg->pkg_conflicts) {
printf("Conflicting packages:\n");
conf_t *conf = pkg->pkg_conflicts;
printf("\t~ %s (%d.%d.%d)\n",
conf->conf_name,
conf->conf_ver.maj, conf->conf_ver.min, conf->conf_ver.pat);
}
else {
printf("Conflicting packages: (none)\n");
}
}
/*
* Read integer `key` from a libconfig file (glacier.conf). Distinguishes
* the failures a caller has to report differently:
@@ -383,3 +289,418 @@ gl_conf_get_int(const char *path, const char *key, int *out)
config_destroy(&cfg);
return GL_CONF_OK;
}
/* A repos.conf url: https:// or file://, then at least one printable,
* non-space ASCII character. http:// and everything else are refused. */
static bool
valid_repo_url(const char *u)
{
const char *rest;
if (strncmp(u, "https://", 8) == 0)
rest = u + 8;
else if (strncmp(u, "file://", 7) == 0)
rest = u + 7;
else
return false;
if (*rest == '\0')
return false;
for (; *rest; rest++)
if (*rest < 0x21 || *rest > 0x7e)
return false;
return true;
}
/*
* Read and check a whole repos.conf (format: gl_conf_repo_keys). On
* GL_CONF_OK, *cfg is open (the caller config_destroy()s it) and *repos
* is its `repos` list; on anything else nothing is left open.
*/
static gl_conf_status_t
read_repos_conf(const char *path, config_t *cfg, config_setting_t **repos)
{
config_init(cfg);
if (!config_read_file(cfg, path)) {
config_destroy(cfg);
return GL_CONF_ERR_READ;
}
config_setting_t *root = config_root_setting(cfg);
config_setting_t *list = config_setting_get_member(root, "repos");
if (config_setting_length(root) != 1 || !list ||
config_setting_type(list) != CONFIG_TYPE_LIST)
goto invalid;
int n = config_setting_length(list);
for (int i = 0; i < n; i++) {
config_setting_t *e = config_setting_get_elem(list, (unsigned)i);
if (config_setting_type(e) != CONFIG_TYPE_GROUP)
goto invalid;
config_setting_t *name = config_setting_get_member(e, "name");
config_setting_t *url = config_setting_get_member(e, "url");
config_setting_t *keys = config_setting_get_member(e, "keys");
if (!name || !keys ||
config_setting_length(e) != (url ? 3 : 2))
goto invalid;
const char *nm = config_setting_get_string(name);
if (!nm || !gl_valid_pkg_ident(nm))
goto invalid;
if (url) {
const char *u = config_setting_get_string(url);
if (!u || !valid_repo_url(u))
goto invalid;
}
for (int j = 0; j < i; j++) {
config_setting_t *prev = config_setting_get_member(
config_setting_get_elem(list, (unsigned)j), "name");
if (strcmp(config_setting_get_string(prev), nm) == 0)
goto invalid;
}
if (config_setting_type(keys) != CONFIG_TYPE_ARRAY &&
config_setting_type(keys) != CONFIG_TYPE_LIST)
goto invalid;
int nk = config_setting_length(keys);
for (int k = 0; k < nk; k++) {
const char *key = config_setting_get_string(
config_setting_get_elem(keys, (unsigned)k));
if (!key || !gl_valid_pkg_ident(key))
goto invalid;
}
}
*repos = list;
return GL_CONF_OK;
invalid:
config_destroy(cfg);
return GL_CONF_ERR_INVALID;
}
/*
* The names of the keys repos.conf trusts for `repo` (REPO_DESIGN.txt
* section 3), '\n'-separated (each followed by '\n') into out.
* GL_CONF_OK out set (possibly "": the repo lists no keys)
* GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig
* GL_CONF_ERR_MISSING the file is valid, but doesn't list `repo`
* GL_CONF_ERR_INVALID anything else than the format below
* GL_CONF_ERR_SPACE the names don't fit in outlen
* The WHOLE file is checked on every call, not only `repo`'s entry, and
* nothing unknown is ignored (a setting that silently never parses is
* CLAUDE.md bug 10's class):
* repos = ( { name = "base"; url = "https://..."; keys = [ "k" ]; }, ... );
* `repos` is the only top-level setting; each entry is a group with a
* name (a gl_valid_pkg_ident, unique), an optional url (valid_repo_url)
* and keys (an array or list of gl_valid_pkg_ident strings).
*/
gl_conf_status_t
gl_conf_repo_keys(const char *path, const char *repo, char *out, size_t outlen)
{
if (!out || outlen == 0)
return GL_CONF_ERR_SPACE;
out[0] = '\0';
config_t cfg;
config_setting_t *repos;
gl_conf_status_t st = read_repos_conf(path, &cfg, &repos);
if (st != GL_CONF_OK)
return st;
st = GL_CONF_ERR_MISSING;
int n = config_setting_length(repos);
for (int i = 0; i < n; i++) {
config_setting_t *e = config_setting_get_elem(repos, (unsigned)i);
if (strcmp(config_setting_get_string(
config_setting_get_member(e, "name")), repo) != 0)
continue;
config_setting_t *keys = config_setting_get_member(e, "keys");
size_t used = 0;
int nk = config_setting_length(keys);
for (int k = 0; k < nk; k++) {
const char *key = config_setting_get_string(
config_setting_get_elem(keys, (unsigned)k));
size_t len = strlen(key);
if (used + len + 2 > outlen) {
out[0] = '\0';
st = GL_CONF_ERR_SPACE;
goto out;
}
memcpy(out + used, key, len);
out[used + len] = '\n';
used += len + 1;
out[used] = '\0';
}
st = GL_CONF_OK;
break;
}
out:
config_destroy(&cfg);
return st;
}
/*
* Every repository repos.conf lists, in its order (= priority), one
* "NAME\tURL\n" line each into out; URL is empty for an entry without
* one. The whole file is checked as by gl_conf_repo_keys.
* GL_CONF_OK out set (possibly "": no repositories)
* GL_CONF_ERR_READ file missing, unreadable, or not valid libconfig
* GL_CONF_ERR_INVALID not the format gl_conf_repo_keys describes
* GL_CONF_ERR_SPACE the lines don't fit in outlen
*/
gl_conf_status_t
gl_conf_repos(const char *path, char *out, size_t outlen)
{
if (!out || outlen == 0)
return GL_CONF_ERR_SPACE;
out[0] = '\0';
config_t cfg;
config_setting_t *repos;
gl_conf_status_t st = read_repos_conf(path, &cfg, &repos);
if (st != GL_CONF_OK)
return st;
size_t used = 0;
int n = config_setting_length(repos);
for (int i = 0; i < n; i++) {
config_setting_t *e = config_setting_get_elem(repos, (unsigned)i);
const char *nm = config_setting_get_string(
config_setting_get_member(e, "name"));
config_setting_t *url = config_setting_get_member(e, "url");
const char *u = url ? config_setting_get_string(url) : "";
int len = snprintf(out + used, outlen - used, "%s\t%s\n", nm, u);
if (len < 0 || (size_t)len >= outlen - used) {
out[0] = '\0';
st = GL_CONF_ERR_SPACE;
break;
}
used += (size_t)len;
}
config_destroy(&cfg);
return st;
}
/* ------------------------------------------------------------------ */
/* Repository indexes (REPO_DESIGN.txt section 4) */
/* ------------------------------------------------------------------ */
/* A version as the store names it: "%d.%d.%d", each part decimal
* without leading zeros and fitting an int. Anything else would name a
* version directory the store never makes. */
static bool
valid_index_ver(const char *v)
{
for (int part = 0; part < 3; part++) {
const char *start = v;
long n = 0;
while (*v >= '0' && *v <= '9') {
n = n * 10 + (*v - '0');
if (n > INT_MAX)
return false;
v++;
}
if (v == start || (*start == '0' && v - start > 1))
return false;
if (part < 2 && *v++ != '.')
return false;
}
return *v == '\0';
}
static bool
valid_sha256_field(const char *s)
{
if (strncmp(s, "sha256:", 7) != 0 || strlen(s) != 7 + 64)
return false;
for (s += 7; *s; s++)
if (!((*s >= '0' && *s <= '9') || (*s >= 'a' && *s <= 'f')))
return false;
return true;
}
/* A group with exactly the members named in `names` (NULL-terminated). */
static bool
has_exactly(config_setting_t *g, const char *const *names)
{
if (!g || config_setting_type(g) != CONFIG_TYPE_GROUP)
return false;
int n = 0;
for (; names[n]; n++)
if (!config_setting_get_member(g, names[n]))
return false;
return config_setting_length(g) == n;
}
static const char *
str_member(config_setting_t *g, const char *name)
{
config_setting_t *m = config_setting_get_member(g, name);
return m && config_setting_type(m) == CONFIG_TYPE_STRING ?
config_setting_get_string(m) : NULL;
}
/* An integer member (int or int64), or false. */
static bool
int_member(config_setting_t *g, const char *name, int64_t *out)
{
config_setting_t *m = config_setting_get_member(g, name);
if (!m || (config_setting_type(m) != CONFIG_TYPE_INT &&
config_setting_type(m) != CONFIG_TYPE_INT64))
return false;
*out = (int64_t)config_setting_get_int64(m);
return true;
}
/* DEPS or CONFS: a list of { NAME; VER; } groups, as "NAME VER\n" lines
* into a new string (*out; "" for an empty list). */
static bool
read_relations(config_setting_t *pkg, const char *name, char **out)
{
static const char *const members[] = { "NAME", "VER", NULL };
config_setting_t *l = config_setting_get_member(pkg, name);
if (!l || config_setting_type(l) != CONFIG_TYPE_LIST)
return false;
int n = config_setting_length(l);
size_t cap = 1, used = 0;
for (int i = 0; i < n; i++) {
config_setting_t *r = config_setting_get_elem(l, (unsigned)i);
const char *nm, *ver;
if (!has_exactly(r, members) || !(nm = str_member(r, "NAME")) ||
!(ver = str_member(r, "VER")) || !gl_valid_pkg_ident(nm) ||
!valid_index_ver(ver))
return false;
cap += strlen(nm) + strlen(ver) + 2;
}
if (!(*out = malloc(cap)))
return false;
(*out)[0] = '\0';
for (int i = 0; i < n; i++) {
config_setting_t *r = config_setting_get_elem(l, (unsigned)i);
used += (size_t)snprintf(*out + used, cap - used, "%s %s\n",
str_member(r, "NAME"), str_member(r, "VER"));
}
return true;
}
void
gl_index_free(gl_index_t *idx)
{
if (!idx)
return;
for (size_t i = 0; i < idx->npkgs; i++) {
gl_index_pkg_t *p = &idx->pkgs[i];
free(p->name);
free(p->ver);
free(p->file);
free(p->sha256);
free(p->deps);
free(p->confs);
}
free(idx->pkgs);
free(idx->repo);
memset(idx, 0, sizeof(*idx));
}
/*
* Parse an index's bytes (text[0..len)) into *out, which is zeroed
* first; free it with gl_index_free on GL_INDEX_OK. The caller checks
* the signature over the SAME bytes first: nothing here is trusted
* otherwise, and nothing here checks SERIAL or VALID_UNTIL against
* anything (that is the caller's rollback/expiry policy).
*
* Strict, like repos.conf: exactly REPO, SERIAL (> 0), VALID_UNTIL
* (> 0) and PKGS at the top; each package exactly NAME, VER, FILE,
* SHA256, SIZE (>= 0), DEPS, CONFS; NAME a gl_valid_pkg_ident; VER as
* the store names versions; FILE exactly "NAME-VER.gpkg"; SHA256
* "sha256:" + 64 lowercase hex; DEPS/CONFS lists of { NAME; VER; }; no
* NAME + VER twice. A NUL or '@' anywhere is refused before parsing:
* an index never has one, and '@' is how libconfig spells @include.
* GL_INDEX_OK, GL_INDEX_ERR_FORMAT, GL_INDEX_ERR_REPO (well formed,
* but REPO is not `repo`), GL_INDEX_ERR_NOMEM
*/
gl_index_status_t
gl_index_parse(const char *text, size_t len, const char *repo, gl_index_t *out)
{
static const char *const top[] = { "REPO", "SERIAL", "VALID_UNTIL", "PKGS", NULL };
static const char *const fields[] = { "NAME", "VER", "FILE", "SHA256",
"SIZE", "DEPS", "CONFS", NULL };
if (!out)
return GL_INDEX_ERR_FORMAT;
memset(out, 0, sizeof(*out));
if (!text || !repo || memchr(text, '\0', len) || memchr(text, '@', len))
return GL_INDEX_ERR_FORMAT;
char *buf = malloc(len + 1);
if (!buf)
return GL_INDEX_ERR_NOMEM;
memcpy(buf, text, len);
buf[len] = '\0';
config_t cfg;
config_init(&cfg);
int ok = config_read_string(&cfg, buf);
free(buf);
if (!ok) {
config_destroy(&cfg);
return GL_INDEX_ERR_FORMAT;
}
gl_index_status_t st = GL_INDEX_ERR_FORMAT;
config_setting_t *root = config_root_setting(&cfg);
const char *r = str_member(root, "REPO");
config_setting_t *pkgs = config_setting_get_member(root, "PKGS");
if (config_setting_length(root) != 4 || !has_exactly(root, top) ||
!r || !gl_valid_pkg_ident(r) ||
!int_member(root, "SERIAL", &out->serial) || out->serial <= 0 ||
!int_member(root, "VALID_UNTIL", &out->valid_until) ||
out->valid_until <= 0 ||
config_setting_type(pkgs) != CONFIG_TYPE_LIST)
goto fail;
size_t n = (size_t)config_setting_length(pkgs);
if (n && !(out->pkgs = calloc(n, sizeof(*out->pkgs)))) {
st = GL_INDEX_ERR_NOMEM;
goto fail;
}
for (size_t i = 0; i < n; i++) {
config_setting_t *e = config_setting_get_elem(pkgs, (unsigned)i);
gl_index_pkg_t *p = &out->pkgs[i];
out->npkgs = i + 1;
const char *nm, *ver, *file, *sha;
if (!has_exactly(e, fields) ||
!(nm = str_member(e, "NAME")) || !gl_valid_pkg_ident(nm) ||
!(ver = str_member(e, "VER")) || !valid_index_ver(ver) ||
!(file = str_member(e, "FILE")) ||
!(sha = str_member(e, "SHA256")) || !valid_sha256_field(sha) ||
!int_member(e, "SIZE", &p->size) || p->size < 0)
goto fail;
char want[PATH_MAX];
snprintf(want, sizeof want, "%s-%s.gpkg", nm, ver);
if (strcmp(file, want) != 0)
goto fail;
for (size_t j = 0; j < i; j++)
if (strcmp(out->pkgs[j].name, nm) == 0 &&
strcmp(out->pkgs[j].ver, ver) == 0)
goto fail;
if (!(p->name = strdup(nm)) || !(p->ver = strdup(ver)) ||
!(p->file = strdup(file)) || !(p->sha256 = strdup(sha))) {
st = GL_INDEX_ERR_NOMEM;
goto fail;
}
if (!read_relations(e, "DEPS", &p->deps) ||
!read_relations(e, "CONFS", &p->confs))
goto fail;
}
if (strcmp(r, repo) != 0) {
st = GL_INDEX_ERR_REPO;
goto fail;
}
if (!(out->repo = strdup(r))) {
st = GL_INDEX_ERR_NOMEM;
goto fail;
}
config_destroy(&cfg);
return GL_INDEX_OK;
fail:
config_destroy(&cfg);
gl_index_free(out);
return st;
}
+38 -24
View File
@@ -7,6 +7,7 @@
#include <libconfig.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <sys/stat.h>
#include "common.h"
@@ -43,44 +44,57 @@ typedef struct {
}
gl_pkg_t;
extern dep_t null_dep;
extern conf_t null_conf;
extern gl_pkg_t empty;
semver_t gl_pkgver(int major, int minor, int patch);
semver_t gl_parse_semver(const char *str);
void gl_free_semver(semver_t *v);
gl_pkg_t *gl_spawn_pkg(
char *name, semver_t *ver, char *desc,
char *maint, char *copy, char *repo,
dep_t *dep, conf_t *conf
);
void gl_show_pkg_props(const gl_pkg_t *pkg);
void gl_free_pkg(gl_pkg_t *pkg);
int gl_mkdir(dir_t *dir);
char *gl_cat_dir(const dir_t *a, const dir_t *b);
char *gl_cat_dir_and_ver(const dir_t *base, const gl_pkg_t *pkg);
dir_t gl_return_dir(const dir_t *a, const dir_t *b, mode_t mode);
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_ERR_TYPE = 3,
GL_CONF_ERR_INVALID = 4,
GL_CONF_ERR_SPACE = 5
} 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);
gl_conf_status_t gl_conf_repo_keys(const char *path, const char *repo,
char *out, size_t outlen);
gl_conf_status_t gl_conf_repos(const char *path, char *out, size_t outlen);
/* A repository index (REPO_DESIGN.txt section 4), as gl_index_parse
* reads it. deps/confs are "NAME VER\n" lines ("" for none). */
typedef struct {
char *name;
char *ver;
char *file;
char *sha256;
int64_t size;
char *deps;
char *confs;
} gl_index_pkg_t;
typedef struct {
char *repo;
int64_t serial;
int64_t valid_until;
size_t npkgs;
gl_index_pkg_t *pkgs;
} gl_index_t;
typedef enum {
GL_INDEX_OK = 0,
GL_INDEX_ERR_FORMAT = 1,
GL_INDEX_ERR_REPO = 2,
GL_INDEX_ERR_NOMEM = 3
} gl_index_status_t;
gl_index_status_t gl_index_parse(const char *text, size_t len,
const char *repo, gl_index_t *out);
void gl_index_free(gl_index_t *idx);
bool gl_valid_pkg_ident(const char *s);
int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg);
char *join_with_cwd(const char *path);
#endif
-45
View File
@@ -1,45 +0,0 @@
#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#define _XOPEN_SOURCE 700
#include <stdbool.h>
#include <stddef.h>
#include <unistd.h>
#include "color.h"
bool
lg_is_root(void)
{
if (getuid() != 0) {
return false;
}
else if (getuid() == 0) {
return true;
}
else {
return false;
}
}
bool
lg_runtime_exists(void)
{
const char *rt_files[] = {
"/etc/glacier/glacier.conf",
"/etc/glacier/make.conf"
};
size_t num_of_files = sizeof(rt_files) / sizeof(rt_files[0]);
for (size_t f_index = 0; f_index < num_of_files; f_index++) {
if (access(rt_files[f_index], F_OK) == 0) {
continue;
}
else {
return false;
}
}
return true;
}
+428
View File
@@ -0,0 +1,428 @@
/*
* gl_sign: Ed25519 signatures in signify's file formats. See sign.h.
*
* Parsing is strict: exactly two lines, the "untrusted comment: "
* prefix, canonical base64 of exactly the expected length, the right
* algorithm bytes. Anything else is GL_SIGN_ERR_FORMAT.
*/
#define _POSIX_C_SOURCE 200809L
#define _DEFAULT_SOURCE
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/random.h>
#include <sys/stat.h>
#include <unistd.h>
#include "sign.h"
#include "monocypher/monocypher.h"
#include "monocypher/monocypher-ed25519.h"
#define COMMENT_PREFIX "untrusted comment: "
#define COMMENT_PREFIX_LEN (sizeof COMMENT_PREFIX - 1)
/* Decoded sizes of the three kinds of file. */
#define PUBKEY_BYTES 42
#define SIG_BYTES 74
#define SECKEY_BYTES 104
/* A key or signature file is a few hundred bytes at most. */
#define MAX_KEYFILE (COMMENT_PREFIX_LEN + GL_SIGN_MAX_COMMENT + 1 + 4 * ((SECKEY_BYTES + 2) / 3) + 1)
/* Reads all of a regular file (at most `max` bytes) into a malloc'd
* buffer. */
static int
read_all(const char *path, size_t max, uint8_t **out, size_t *len)
{
int fd = open(path, O_RDONLY | O_CLOEXEC);
if (fd < 0)
return GL_SIGN_ERR_IO;
struct stat st;
if (fstat(fd, &st) != 0 || !S_ISREG(st.st_mode)) {
close(fd);
return GL_SIGN_ERR_IO;
}
if (st.st_size < 0 || (uintmax_t)st.st_size > max) {
close(fd);
return GL_SIGN_ERR_TOOBIG;
}
size_t size = (size_t)st.st_size;
uint8_t *buf = malloc(size ? size : 1);
if (!buf) {
close(fd);
return GL_SIGN_ERR_IO;
}
size_t got = 0;
while (got < size) {
ssize_t n = read(fd, buf + got, size - got);
if (n < 0 && errno == EINTR)
continue;
if (n <= 0)
break;
got += (size_t)n;
}
/* The file must not have grown or shrunk under us. */
uint8_t extra;
ssize_t more = read(fd, &extra, 1);
close(fd);
if (got != size || more != 0) {
free(buf);
return GL_SIGN_ERR_IO;
}
*out = buf;
*len = size;
return GL_SIGN_OK;
}
static int
b64_value(uint8_t c)
{
if (c >= 'A' && c <= 'Z') return c - 'A';
if (c >= 'a' && c <= 'z') return c - 'a' + 26;
if (c >= '0' && c <= '9') return c - '0' + 52;
if (c == '+') return 62;
if (c == '/') return 63;
return -1;
}
/* Decodes canonical, padded base64 that must yield exactly `outlen`
* bytes. Returns 0 on success. */
static int
b64_decode_exact(const uint8_t *in, size_t n, uint8_t *out, size_t outlen)
{
if (n != (outlen + 2) / 3 * 4)
return -1;
size_t o = 0;
for (size_t i = 0; i < n; i += 4) {
uint32_t t = 0;
size_t pad = 0;
for (size_t j = 0; j < 4; j++) {
uint8_t c = in[i + j];
int v;
if (c == '=') {
/* Padding only in the last quartet's last two places. */
if (i + 4 != n || j < 2)
return -1;
pad++;
v = 0;
} else {
if (pad)
return -1;
v = b64_value(c);
if (v < 0)
return -1;
}
t = (t << 6) | (uint32_t)v;
}
/* Canonical: the bits padding drops must be zero. */
if ((pad == 1 && (t & 0xffu)) || (pad == 2 && (t & 0xffffu)))
return -1;
size_t bytes = 3 - pad;
if (o + bytes > outlen)
return -1;
uint8_t b[3] = { (uint8_t)(t >> 16), (uint8_t)(t >> 8), (uint8_t)t };
memcpy(out + o, b, bytes);
o += bytes;
}
return o == outlen ? 0 : -1;
}
static void
b64_encode(const uint8_t *in, size_t n, char *out)
{
static const char tab[] =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
size_t o = 0;
for (size_t i = 0; i < n; i += 3) {
uint32_t t = (uint32_t)in[i] << 16;
if (i + 1 < n) t |= (uint32_t)in[i + 1] << 8;
if (i + 2 < n) t |= in[i + 2];
out[o++] = tab[(t >> 18) & 63];
out[o++] = tab[(t >> 12) & 63];
out[o++] = i + 1 < n ? tab[(t >> 6) & 63] : '=';
out[o++] = i + 2 < n ? tab[t & 63] : '=';
}
out[o] = '\0';
}
/* Reads a signify file and decodes its payload into out[0..outlen). */
static int
read_signify(const char *path, uint8_t *out, size_t outlen)
{
uint8_t *buf;
size_t len;
int st = read_all(path, MAX_KEYFILE, &buf, &len);
if (st == GL_SIGN_ERR_TOOBIG)
return GL_SIGN_ERR_FORMAT;
if (st != GL_SIGN_OK)
return st;
st = GL_SIGN_ERR_FORMAT;
if (len < COMMENT_PREFIX_LEN || memcmp(buf, COMMENT_PREFIX, COMMENT_PREFIX_LEN) != 0)
goto out;
uint8_t *nl = memchr(buf, '\n', len);
if (!nl || memchr(buf, '\0', (size_t)(nl - buf)))
goto out;
if ((size_t)(nl - buf) - COMMENT_PREFIX_LEN > GL_SIGN_MAX_COMMENT)
goto out;
/* The base64 line runs to a final newline that ends the file. */
uint8_t *b64 = nl + 1;
uint8_t *end = buf + len;
if (b64 >= end || end[-1] != '\n')
goto out;
if (b64_decode_exact(b64, (size_t)(end - 1 - b64), out, outlen) != 0)
goto out;
if (out[0] != 'E' || out[1] != 'd')
goto out;
st = GL_SIGN_OK;
out:
crypto_wipe(buf, len);
free(buf);
return st;
}
int
gl_sign_read_pubkey(const char *path, gl_sign_pubkey_t *out)
{
uint8_t raw[PUBKEY_BYTES];
int st = read_signify(path, raw, sizeof raw);
if (st != GL_SIGN_OK)
return st;
memcpy(out->keynum, raw + 2, GL_SIGN_KEYNUM_SIZE);
memcpy(out->key, raw + 10, sizeof out->key);
return GL_SIGN_OK;
}
int
gl_sign_read_sig(const char *path, gl_sign_sig_t *out)
{
uint8_t raw[SIG_BYTES];
int st = read_signify(path, raw, sizeof raw);
if (st != GL_SIGN_OK)
return st;
memcpy(out->keynum, raw + 2, GL_SIGN_KEYNUM_SIZE);
memcpy(out->sig, raw + 10, sizeof out->sig);
return GL_SIGN_OK;
}
/* sec[0..64) = seed | public key, as signify and Monocypher both lay
* it out. */
static int
read_seckey(const char *path, uint8_t keynum[GL_SIGN_KEYNUM_SIZE], uint8_t sec[64])
{
uint8_t raw[SECKEY_BYTES];
int st = read_signify(path, raw, sizeof raw);
if (st != GL_SIGN_OK)
goto out;
st = GL_SIGN_ERR_FORMAT;
if (raw[2] != 'B' || raw[3] != 'K')
goto out;
uint32_t rounds = (uint32_t)raw[4] << 24 | (uint32_t)raw[5] << 16
| (uint32_t)raw[6] << 8 | raw[7];
if (rounds != 0) {
st = GL_SIGN_ERR_ENCRYPTED;
goto out;
}
/* checksum = the first 8 bytes of SHA-512(seckey) */
uint8_t digest[64];
crypto_sha512(digest, raw + 40, 64);
int ok = memcmp(digest, raw + 24, 8) == 0;
crypto_wipe(digest, sizeof digest);
if (!ok)
goto out;
memcpy(keynum, raw + 32, GL_SIGN_KEYNUM_SIZE);
memcpy(sec, raw + 40, 64);
st = GL_SIGN_OK;
out:
crypto_wipe(raw, sizeof raw);
return st;
}
int
gl_sign_check_seckey(const char *path)
{
uint8_t keynum[GL_SIGN_KEYNUM_SIZE], sec[64];
int st = read_seckey(path, keynum, sec);
crypto_wipe(sec, sizeof sec);
return st;
}
int
gl_sign_verify(const gl_sign_pubkey_t *pub, const gl_sign_sig_t *sig,
const uint8_t *msg, size_t len)
{
if (memcmp(pub->keynum, sig->keynum, GL_SIGN_KEYNUM_SIZE) != 0)
return GL_SIGN_ERR_KEYNUM;
return crypto_ed25519_check(sig->sig, pub->key, msg, len) == 0
? GL_SIGN_OK : GL_SIGN_ERR_BAD;
}
int
gl_sign_verify_file(const char *pubkey_path, const char *msg_path,
const char *sig_path)
{
gl_sign_pubkey_t pub;
gl_sign_sig_t sig;
int st = gl_sign_read_pubkey(pubkey_path, &pub);
if (st != GL_SIGN_OK)
return st;
st = gl_sign_read_sig(sig_path, &sig);
if (st != GL_SIGN_OK)
return st;
/* Cheap mismatch first: no need to read the message. */
if (memcmp(pub.keynum, sig.keynum, GL_SIGN_KEYNUM_SIZE) != 0)
return GL_SIGN_ERR_KEYNUM;
uint8_t *msg;
size_t len;
st = read_all(msg_path, GL_SIGN_MAX_MSG, &msg, &len);
if (st != GL_SIGN_OK)
return st;
st = gl_sign_verify(&pub, &sig, msg, len);
free(msg);
return st;
}
/* Writes "untrusted comment: COMMENT\n" + base64(raw) + "\n" to an fd
* opened with `flags`. */
static int
write_signify(const char *path, int flags, mode_t mode, const char *comment,
const uint8_t *raw, size_t rawlen)
{
char b64[4 * ((SECKEY_BYTES + 2) / 3) + 1];
if (rawlen > SECKEY_BYTES)
return GL_SIGN_ERR_WRITE;
b64_encode(raw, rawlen, b64);
int fd = open(path, O_WRONLY | O_CREAT | O_NOFOLLOW | O_CLOEXEC | flags, mode);
if (fd < 0)
return errno == EEXIST ? GL_SIGN_ERR_EXISTS : GL_SIGN_ERR_WRITE;
/* Exactly `mode`, whatever the umask: a secret key 0600, a public
* key 0644 (users' gpkg reads the system's keys). */
if (fchmod(fd, mode) != 0) {
close(fd);
return GL_SIGN_ERR_WRITE;
}
FILE *f = fdopen(fd, "w");
if (!f) {
close(fd);
return GL_SIGN_ERR_WRITE;
}
int bad = fprintf(f, COMMENT_PREFIX "%s\n%s\n", comment, b64) < 0;
crypto_wipe(b64, sizeof b64);
if (fclose(f) != 0 || bad)
return GL_SIGN_ERR_WRITE;
return GL_SIGN_OK;
}
int
gl_sign_file(const char *seckey_path, const char *msg_path, const char *sig_path)
{
uint8_t keynum[GL_SIGN_KEYNUM_SIZE], sec[64];
int st = read_seckey(seckey_path, keynum, sec);
if (st != GL_SIGN_OK)
return st;
uint8_t *msg;
size_t len;
st = read_all(msg_path, GL_SIGN_MAX_MSG, &msg, &len);
if (st != GL_SIGN_OK) {
crypto_wipe(sec, sizeof sec);
return st;
}
uint8_t raw[SIG_BYTES] = { 'E', 'd' };
memcpy(raw + 2, keynum, GL_SIGN_KEYNUM_SIZE);
crypto_ed25519_sign(raw + 10, sec, msg, len);
crypto_wipe(sec, sizeof sec);
free(msg);
/* "verify with NAME.pub", NAME = the secret key's name minus .sec */
const char *base = strrchr(seckey_path, '/');
base = base ? base + 1 : seckey_path;
size_t blen = strlen(base);
if (blen > 4 && strcmp(base + blen - 4, ".sec") == 0)
blen -= 4;
if (blen > GL_SIGN_MAX_COMMENT - 16)
blen = GL_SIGN_MAX_COMMENT - 16;
char comment[GL_SIGN_MAX_COMMENT + 1];
snprintf(comment, sizeof comment, "verify with %.*s.pub", (int)blen, base);
return write_signify(sig_path, O_TRUNC, 0644, comment, raw, sizeof raw);
}
static int
random_bytes(uint8_t *buf, size_t n)
{
while (n > 0) {
ssize_t got = getrandom(buf, n, 0);
if (got < 0 && errno == EINTR)
continue;
if (got <= 0)
return -1;
buf += got;
n -= (size_t)got;
}
return 0;
}
int
gl_sign_generate(const char *pubkey_path, const char *seckey_path,
const char *comment)
{
if (strchr(comment, '\n') || strlen(comment) > GL_SIGN_MAX_COMMENT - 11)
return GL_SIGN_ERR_FORMAT;
/* rand = seed[32] | keynum[8] | salt[16] */
uint8_t rand[56];
if (random_bytes(rand, sizeof rand) != 0)
return GL_SIGN_ERR_RANDOM;
uint8_t sec[64], pub[32];
crypto_ed25519_key_pair(sec, pub, rand); /* wipes the seed */
uint8_t sraw[SECKEY_BYTES] = { 'E', 'd', 'B', 'K', 0, 0, 0, 0 };
memcpy(sraw + 8, rand + 40, 16); /* salt (unused at 0 rounds) */
uint8_t digest[64];
crypto_sha512(digest, sec, sizeof sec);
memcpy(sraw + 24, digest, 8); /* checksum */
memcpy(sraw + 32, rand + 32, GL_SIGN_KEYNUM_SIZE);
memcpy(sraw + 40, sec, sizeof sec);
uint8_t praw[PUBKEY_BYTES] = { 'E', 'd' };
memcpy(praw + 2, rand + 32, GL_SIGN_KEYNUM_SIZE);
memcpy(praw + 10, pub, sizeof pub);
char c[GL_SIGN_MAX_COMMENT + 1];
snprintf(c, sizeof c, "%s secret key", comment);
int st = write_signify(seckey_path, O_EXCL, 0600, c, sraw, sizeof sraw);
if (st == GL_SIGN_OK) {
snprintf(c, sizeof c, "%s public key", comment);
st = write_signify(pubkey_path, O_EXCL, 0644, c, praw, sizeof praw);
/* Never leave a secret key without its public half. */
if (st != GL_SIGN_OK)
unlink(seckey_path);
}
crypto_wipe(rand, sizeof rand);
crypto_wipe(sec, sizeof sec);
crypto_wipe(sraw, sizeof sraw);
crypto_wipe(digest, sizeof digest);
return st;
}
+82
View File
@@ -0,0 +1,82 @@
#ifndef GL_SIGN_H_
#define GL_SIGN_H_
/*
* gl_sign: Ed25519 signatures in OpenBSD signify's file formats
* (REPO_DESIGN.txt section 2). Private to libglacier_verify.so, like
* sha256.h: no public header is installed.
*
* Every file is two lines, "untrusted comment: ..." and base64 of:
* public key "Ed" | keynum[8] | pubkey[32]
* signature "Ed" | keynum[8] | sig[64]
* secret key "Ed" | "BK" | rounds[4, big-endian] | salt[16] |
* checksum[8] | keynum[8] | seckey[64]
* Only unencrypted secret keys (rounds == 0, `signify -G -n`) are
* read; an encrypted one is GL_SIGN_ERR_ENCRYPTED.
*
* Signatures are pure Ed25519 over the whole message, as signify's, so
* `signify -V -p KEY.pub -m FILE` checks what gl_sign_file makes and
* gl_sign_verify_file checks what signify makes.
*/
#include <stddef.h>
#include <stdint.h>
#define GL_SIGN_KEYNUM_SIZE 8
/* Largest message signed or verified: the whole file is read. */
#define GL_SIGN_MAX_MSG (64u * 1024u * 1024u)
/* Longest comment signify accepts. */
#define GL_SIGN_MAX_COMMENT 1024
typedef enum {
GL_SIGN_OK = 0,
GL_SIGN_ERR_IO = 1, /* a file can't be opened or read */
GL_SIGN_ERR_FORMAT = 2, /* not a well-formed file of the expected kind */
GL_SIGN_ERR_ENCRYPTED = 3, /* a passphrase-protected secret key */
GL_SIGN_ERR_KEYNUM = 4, /* the signature was made by another key */
GL_SIGN_ERR_BAD = 5, /* the signature does not verify */
GL_SIGN_ERR_TOOBIG = 6, /* message larger than GL_SIGN_MAX_MSG */
GL_SIGN_ERR_RANDOM = 7, /* no randomness for a new key */
GL_SIGN_ERR_EXISTS = 8, /* a key file to create already exists */
GL_SIGN_ERR_WRITE = 9, /* a file to write can't be created or written */
} gl_sign_status_t;
typedef struct {
uint8_t keynum[GL_SIGN_KEYNUM_SIZE];
uint8_t key[32];
} gl_sign_pubkey_t;
typedef struct {
uint8_t keynum[GL_SIGN_KEYNUM_SIZE];
uint8_t sig[64];
} gl_sign_sig_t;
int gl_sign_read_pubkey(const char *path, gl_sign_pubkey_t *out);
/* Reads a secret key only to check it (then wipes it). */
int gl_sign_check_seckey(const char *path);
int gl_sign_read_sig(const char *path, gl_sign_sig_t *out);
/* Checks `sig` over msg[0..len) with `pub`: GL_SIGN_ERR_KEYNUM before
* any crypto if the keynums differ, else GL_SIGN_OK or GL_SIGN_ERR_BAD. */
int gl_sign_verify(const gl_sign_pubkey_t *pub, const gl_sign_sig_t *sig,
const uint8_t *msg, size_t len);
/* Reads all three files and verifies. The status names the first
* problem found, in the order: public key, signature, message, check. */
int gl_sign_verify_file(const char *pubkey_path, const char *msg_path,
const char *sig_path);
/* Signs msg_path with the secret key, writing sig_path (replaced if it
* exists, never followed if it's a symlink). The signature's comment
* is "verify with NAME.pub", NAME being the secret key's file name
* without ".sec", as signify writes it. */
int gl_sign_file(const char *seckey_path, const char *msg_path,
const char *sig_path);
/* Makes a new keypair. Neither file may exist (GL_SIGN_ERR_EXISTS).
* The secret key is created 0600 and unencrypted; the comments are
* "COMMENT public key" / "COMMENT secret key". */
int gl_sign_generate(const char *pubkey_path, const char *seckey_path,
const char *comment);
#endif
+200 -29
View File
@@ -12,33 +12,76 @@
#include <unistd.h>
#include "common.h"
#include "deps.h"
#include "istoreutils.h"
#include "log.h"
#include "transaction.h"
/* The one directory a scope owns; everything below derives from it.
* See the layout comment in common.h. */
/* `sysroot` is the install root ("" for the running system, see
* gl_context_t). User scope never has one (set_sysroot refuses it). */
static void
scope_base(gl_ctx_scope_t scope, uid_t uid, char *out)
scope_base(gl_ctx_scope_t scope, uid_t uid, const char *sysroot, 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);
snprintf(out, PATH_MAX, "%s%s", sysroot, GL_SYS_ROOT);
break;
}
}
static void
scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *name, char *out)
scope_subdir(gl_ctx_scope_t scope, uid_t uid, const char *sysroot,
const char *name, char *out)
{
char base[PATH_MAX];
scope_base(scope, uid, base);
scope_base(scope, uid, sysroot, base);
snprintf(out, PATH_MAX, "%s/%s", base, name);
}
int
gl_check_sysroot(const char *sysroot)
{
char root[PATH_MAX];
if (gl_normalize_sysroot(sysroot, root, sizeof(root)) != 0) {
lg_printf(2, "Refusing install root %s: not an absolute path without . or .. components.",
sysroot ? sysroot : "(null)");
return -1;
}
if (root[0] == '\0') { return 0; }
char bad[PATH_MAX];
const char *why = "";
if (gl_sysroot_trusted(root, bad, sizeof(bad), &why) != 0) {
lg_printf(2, "Refusing install root %s: %s is %s.", root, bad, why);
return -1;
}
return 0;
}
/* Store the install root (gl_normalize_sysroot). Only system scope may
* have one (IMAGE_DESIGN section 3): per-user scope has no business in
* an image. */
static int
set_sysroot(gl_context_t *ctx, gl_ctx_scope_t scope, const char *sysroot)
{
ctx->sysroot[0] = '\0';
if (gl_check_sysroot(sysroot) != 0) {
return -1;
}
gl_normalize_sysroot(sysroot, ctx->sysroot, sizeof(ctx->sysroot));
if (scope == GL_SCOPE_USR && ctx->sysroot[0] != '\0') {
lg_printf(2, "Refusing a user-scope context with install root %s.", ctx->sysroot);
ctx->sysroot[0] = '\0';
return -1;
}
return 0;
}
static int
fsync_dir(const char *path)
{
@@ -67,11 +110,11 @@ atomic_swap(const char *live, const char *stage, const char *backup)
}
static void
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
char *store_out, char *links_out)
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, const char *sysroot,
char *index_out, char *store_out, char *links_out)
{
scope_subdir(scope, uid, "index", index_out);
scope_subdir(scope, uid, "store", store_out);
scope_subdir(scope, uid, sysroot, "index", index_out);
scope_subdir(scope, uid, sysroot, "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
@@ -79,7 +122,7 @@ derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
* 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);
scope_subdir(scope, uid, sysroot, "links", links_out);
}
static int
@@ -118,7 +161,9 @@ hardlink_tree(const char *src, const char *dest)
if(lstat(s, &st) != 0) { continue; }
if (S_ISDIR(st.st_mode)) {
if (mkdir(d, st.st_mode & 0777) != 0 && errno != EEXIST) {
/* Exact mode, not umask-filtered: the stage becomes
* live, so seeding must not tighten what it copies. */
if (gl_mkdir_exact(d, st.st_mode & 07777) != 0) {
closedir(dir);
return -1;
}
@@ -148,6 +193,13 @@ hardlink_tree(const char *src, const char *dest)
int
gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
{
return gl_init_live_context_at(ctx, scope, uid, "/");
}
int
gl_init_live_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
const char *sysroot)
{
if (!ctx) {
return -1;
@@ -158,16 +210,28 @@ gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
ctx->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
ctx->link_refusals = 0;
ctx->strict = 0;
if (set_sysroot(ctx, scope, sysroot) != 0) {
return -1;
}
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
ctx->links_path);
snprintf(ctx->root_path, PATH_MAX, "%s/glacier", ctx->sysroot);
derive_base_paths(scope, uid, ctx->sysroot, ctx->index_path,
ctx->store_path, ctx->links_path);
return 0;
}
gl_txn_status_t
gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
{
return gl_init_stage_context_at(ctx, scope, uid, "/");
}
gl_txn_status_t
gl_init_stage_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
const char *sysroot)
{
if (!ctx) {
return GL_TXN_ERR_NO_TXN;
@@ -178,8 +242,13 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
ctx->scope = scope;
ctx->lock_fd = -1;
ctx->overwrite = 0;
ctx->link_refusals = 0;
ctx->strict = 0;
if (set_sysroot(ctx, scope, sysroot) != 0) {
return GL_TXN_ERR_PERM;
}
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
snprintf(ctx->root_path, PATH_MAX, "%s/glacier", ctx->sysroot);
if (scope == GL_SCOPE_USR) {
switch (gl_usr_base_check(uid)) {
@@ -200,7 +269,7 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
}
char stage_base[PATH_MAX - 7];
scope_subdir(scope, uid, "stage", stage_base);
scope_subdir(scope, uid, ctx->sysroot, "stage", stage_base);
snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base);
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
@@ -226,17 +295,23 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
}
}
if (gl_mkdirp(ctx->index_path, 0700) != 0) {
/* chmod, not just mkdir's mode: the stage becomes live on commit,
* and a system store must be 0755 whatever syspkg's umask is. */
mode_t dir_mode = gl_scope_dir_mode(scope);
if (gl_mkdirp(ctx->index_path, dir_mode) != 0 ||
chmod(ctx->index_path, dir_mode) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
if (gl_mkdirp(ctx->store_path, 0700) != 0) {
if (gl_mkdirp(ctx->store_path, dir_mode) != 0 ||
chmod(ctx->store_path, dir_mode) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
if (gl_mkdirp(ctx->links_path, 0700) != 0) {
if (gl_mkdirp(ctx->links_path, dir_mode) != 0 ||
chmod(ctx->links_path, dir_mode) != 0) {
gl_abort_transaction(ctx);
return GL_TXN_ERR_MKDIR;
}
@@ -247,7 +322,7 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
}
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
derive_base_paths(scope, uid, live_index, live_store, live_links);
derive_base_paths(scope, uid, ctx->sysroot, live_index, live_store, live_links);
/* live_links is populated (derive_base_paths always fills all
* three) but intentionally unused below — see the comment on the
* hardlink_tree call. */
@@ -275,6 +350,85 @@ gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
return GL_TXN_OK;
}
/* parent/name into out (PATH_MAX), created with `mode` if missing, then
* refused unless it is a real directory owned by `owner` with exactly
* that mode. 0, or -1 having printed why. */
static int
checked_subdir(const char *parent, const char *name, uid_t owner, mode_t mode,
char *out)
{
struct stat st;
snprintf(out, PATH_MAX, "%s/%s", parent, name);
if (gl_mkdir_exact(out, mode) != 0 && errno != EEXIST) {
lg_printf(2, "Cannot create %s: %s", out, strerror(errno));
return -1;
}
if (lstat(out, &st) != 0 || !S_ISDIR(st.st_mode) || st.st_uid != owner ||
(st.st_mode & 07777) != mode) {
lg_printf(2, "Refusing %s: not a directory owned by UID %u with mode %o.",
out, owner, (unsigned)mode);
return -1;
}
return 0;
}
/*
* <base>/NAME: a directory for state a scope keeps beside its index and
* store ("repos": the accepted repository indexes, REPO_DESIGN.txt
* section 5). The base must already exist and be the scope's (owned by
* root, or by the uid for user scope; no group/world write). NAME is
* created if missing with the scope's mode, and is then refused unless
* it is a real directory owned like the base with exactly that mode.
* The path goes to out (PATH_MAX). 0, or -1 having printed why.
*/
int
gl_scope_state_dir(const gl_context_t *ctx, const char *name, char *out)
{
if (!ctx || !out || !gl_valid_pkg_ident(name)) {
return -1;
}
uid_t owner = ctx->scope == GL_SCOPE_SYS ? 0 : ctx->uid;
mode_t mode = gl_scope_dir_mode(ctx->scope);
char base[PATH_MAX];
scope_base(ctx->scope, ctx->uid, ctx->sysroot, base);
struct stat st;
if (ctx->scope == GL_SCOPE_USR && gl_usr_base_check(ctx->uid) != GL_USR_BASE_OK) {
lg_printf(2, "Cannot open %s: not a private directory owned by UID %u.",
base, ctx->uid);
return -1;
}
if (lstat(base, &st) != 0) {
lg_printf(2, "Cannot open %s: %s", base, strerror(errno));
return -1;
}
if (!S_ISDIR(st.st_mode) || st.st_uid != owner || (st.st_mode & 022)) {
lg_printf(2, "Refusing %s: not a directory owned by UID %u, or group/world-writable.",
base, owner);
return -1;
}
return checked_subdir(base, name, owner, mode, out);
}
/*
* <base>/cache/REPO: where packages fetched from REPO are downloaded
* (REPO_DESIGN.txt section 5). Both levels are made and checked as
* gl_scope_state_dir makes and checks NAME. 0, or -1 having printed why.
*/
int
gl_scope_cache_dir(const gl_context_t *ctx, const char *repo, char *out)
{
char cache[PATH_MAX];
if (!ctx || !out || !gl_valid_pkg_ident(repo) ||
gl_scope_state_dir(ctx, "cache", cache) != 0) {
return -1;
}
return checked_subdir(cache, repo,
ctx->scope == GL_SCOPE_SYS ? 0 : ctx->uid,
gl_scope_dir_mode(ctx->scope), out);
}
gl_txn_status_t
gl_begin_transaction(gl_context_t *ctx)
{
@@ -283,7 +437,7 @@ gl_begin_transaction(gl_context_t *ctx)
}
char lock_path[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "lock", lock_path);
scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "lock", lock_path);
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
if (fd < 0) {
@@ -309,8 +463,19 @@ gl_commit_transaction(gl_context_t *ctx)
return GL_TXN_ERR_NO_TXN;
}
/* Dependencies and conflicts (ROADMAP 2.3). The CLIs check the plan
* before prompting; this holds every other caller of the library to
* the same rules. gl_check_txn reports each problem itself. */
int dep_problems = gl_check_txn(ctx);
if (dep_problems != 0) {
if (dep_problems < 0) {
lg_printf(2, "Cannot read the package store to check dependencies.");
}
return GL_TXN_ERR_DEPS;
}
char stage_base[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "stage", stage_base);
scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "stage", stage_base);
int same = check_same_filesystem(ctx->root_path, stage_base);
if (same < 0) {
@@ -322,12 +487,12 @@ gl_commit_transaction(gl_context_t *ctx)
/* Derive live paths the same way gl_init_live_context would */
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
derive_base_paths(ctx->scope, ctx->uid, live_index, live_store,
live_links);
derive_base_paths(ctx->scope, ctx->uid, ctx->sysroot, live_index,
live_store, live_links);
/* Derive old/backup paths */
char old_base[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "old", old_base);
scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "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);
@@ -363,18 +528,22 @@ gl_commit_transaction(gl_context_t *ctx)
/* fsync the directory the renames happened in */
char base[PATH_MAX];
scope_base(ctx->scope, ctx->uid, base);
scope_base(ctx->scope, ctx->uid, ctx->sysroot, base);
fsync_dir(base);
if (ctx->scope == GL_SCOPE_SYS) {
fsync_dir("/usr"); /* base-repo packages land here instead */
char usr[PATH_MAX];
snprintf(usr, sizeof(usr), "%s/usr", ctx->sysroot);
fsync_dir(usr); /* base-repo packages land here instead */
}
gl_context_t live_ctx;
gl_init_live_context(&live_ctx, ctx->scope, ctx->uid);
gl_init_live_context_at(&live_ctx, ctx->scope, ctx->uid,
ctx->sysroot[0] ? ctx->sysroot : "/");
if (gl_relink_store(&live_ctx) != 0) {
lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid);
}
ctx->link_refusals = live_ctx.link_refusals;
gl_rmdir_recursive(old_base);
@@ -390,13 +559,15 @@ gl_commit_transaction(gl_context_t *ctx)
gl_txn_status_t
gl_abort_transaction(gl_context_t *ctx)
{
if (!ctx) {
/* Only the lock holder's stage is its own to remove: without the
* lock, this would wipe another process's running transaction. */
if (!ctx || ctx->lock_fd < 0) {
return GL_TXN_ERR_NO_TXN;
}
/* Remove stage area — live is untouched */
char stage_uid[PATH_MAX];
scope_subdir(ctx->scope, ctx->uid, "stage", stage_uid);
scope_subdir(ctx->scope, ctx->uid, ctx->sysroot, "stage", stage_uid);
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
+76 -1
View File
@@ -5,6 +5,8 @@
#include <limits.h>
#include <stdbool.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#define GL_UID_NONE ((uid_t)0)
@@ -19,6 +21,22 @@ typedef enum {
GL_SCOPE_SYS
} gl_ctx_scope_t;
/* Modes for what glacier creates in a scope. System scope is
* world-readable, like /usr: its store is where /usr's links point, so
* every user must be able to traverse it, and user-scope dependency
* checks read its index. User scope stays private to its uid. */
static inline mode_t
gl_scope_dir_mode(gl_ctx_scope_t scope)
{
return scope == GL_SCOPE_SYS ? 0755 : 0700;
}
static inline mode_t
gl_scope_file_mode(gl_ctx_scope_t scope)
{
return scope == GL_SCOPE_SYS ? 0644 : 0600;
}
typedef struct {
uid_t uid;
@@ -37,8 +55,53 @@ typedef struct {
* existing non-glacier files at their link destinations; see
* gl_link_pkg. */
int overwrite;
/* Install root (IMAGE_DESIGN section 3), set by the *_at init
* functions: "" for the running system, else e.g. "/mnt/everest".
* The paths above are INSTALL paths, already under it: every
* filesystem operation uses them as they are. What a path MEANS
* once the image is booted (a symlink target, a .displaced key)
* is its runtime path, gl_runtime_path(). System scope only. */
char sysroot[PATH_MAX];
/* Paths the link pass refused to link (a foreign file in the
* way, say), counted only for packages installed in this
* transaction. Zeroed by the init functions; after
* gl_commit_transaction, the post-commit relink's count. A
* non-zero count means the transaction committed but did not
* fully succeed. */
int link_refusals;
/* Set by the caller after gl_init_*_context (which zero it).
* Non-zero: a path the link pass would refuse (a foreign file in
* the way, say) fails the transaction before anything is
* committed, instead of being counted in link_refusals after.
* See deps.h. */
int strict;
} gl_context_t;
/* An install path's runtime meaning: sysroot stripped. A path not under
* the sysroot is returned unchanged. */
static inline const char *
gl_runtime_path(const gl_context_t *ctx, const char *install_path)
{
size_t n = strlen(ctx->sysroot);
if (n > 0 && strncmp(install_path, ctx->sysroot, n) == 0 &&
install_path[n] == '/') {
return install_path + n;
}
return install_path;
}
/* The install path of a runtime path (a symlink's absolute target, say):
* sysroot prepended. Returns -1 if it doesn't fit. */
static inline int
gl_at(const gl_context_t *ctx, const char *runtime_path, char *out, size_t outsz)
{
int n = snprintf(out, outsz, "%s%s", ctx->sysroot, runtime_path);
return (n < 0 || (size_t)n >= outsz) ? -1 : 0;
}
typedef enum {
GL_TXN_OK = 0, /* success */
GL_TXN_ERR_LOCK = 1, /* unable to acquire lock */
@@ -48,13 +111,25 @@ typedef enum {
GL_TXN_ERR_SYNC = 5, /* fsync failed */
GL_TXN_ERR_CLEANUP = 6, /* cleanup failed */
GL_TXN_ERR_NO_TXN = 7, /* aborted without beginning */
GL_TXN_ERR_XDEV = 8 /* stage and live on different filesystems */
GL_TXN_ERR_XDEV = 8, /* stage and live on different filesystems */
GL_TXN_ERR_DEPS = 9 /* unmet dependency, conflict, file collision,
* or (strict) a link refusal: see deps.h */
} gl_txn_status_t;
/* Is `sysroot` acceptable as an install root (absolute, no . or ..,
* trusted: see gl_sysroot_trusted)? Logs why not. 0 if so. "/" is. */
int gl_check_sysroot(const char *sysroot);
int gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid);
int gl_init_live_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
const char *sysroot);
gl_txn_status_t gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid);
gl_txn_status_t gl_init_stage_context_at(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid,
const char *sysroot);
gl_txn_status_t gl_begin_transaction(gl_context_t *ctx);
int gl_scope_state_dir(const gl_context_t *ctx, const char *name, char *out);
int gl_scope_cache_dir(const gl_context_t *ctx, const char *repo, char *out);
gl_txn_status_t gl_commit_transaction(gl_context_t *ctx);
gl_txn_status_t gl_abort_transaction(gl_context_t *ctx);
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.
BIN
View File
Binary file not shown.