10/14
This commit is contained in:
1 parent
ee4bcc3b77
commit
ae54f74070
53 files changed
+8902
-1410
No files matched your search
@@ -0,0 +1,2 @@
|
|||||||
|
# Build outputs (make; make libglacier-ng)
|
||||||
|
/build/
|
||||||
@@ -3,7 +3,8 @@
|
|||||||
Context for Claude Code sessions on libglacier-ng: the package manager
|
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
|
for Everest Linux. Kept as a handoff, so a fresh session starts with full
|
||||||
continuity instead of re-deriving everything. Last full rewrite:
|
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
|
## Start here
|
||||||
|
|
||||||
@@ -15,15 +16,231 @@ continuity instead of re-deriving everything. Last full rewrite:
|
|||||||
- `SECURITY_DESIGN.txt`: integrity verification, scope separation,
|
- `SECURITY_DESIGN.txt`: integrity verification, scope separation,
|
||||||
refusal matrix, syspkg's privilege model.
|
refusal matrix, syspkg's privilege model.
|
||||||
- `PACKAGE_SCOPES.txt`: layout, link routing, `/etc` config handling.
|
- `PACKAGE_SCOPES.txt`: layout, link routing, `/etc` config handling.
|
||||||
- `IMAGE_DESIGN.txt`: PROPOSAL for system images (bootstrap OUTPUT,
|
- `IMAGE_DESIGN.txt`: system images (bootstrap OUTPUT). Sections 3
|
||||||
the sysroot library change). Open decisions are in section 9.
|
(sysroot) and 4 (`syspkg --root`) are IMPLEMENTED (milestones 1-2);
|
||||||
- `GLACIER_BOOTSTRAP.txt`: PLAN for `glacier-bootstrap`. Prerequisites
|
the rest is still a proposal. Open decisions are in section 9.
|
||||||
are in section 7, decisions in section 9 (9c and 9d are decided).
|
- `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.
|
- `SECURITY_REVIEW_2026-07-16.md`: older review findings.
|
||||||
|
|
||||||
Nothing from the 2026-10-04 session is committed. libglacier-ng is on
|
Nothing from the 2026-10-04 through 2026-10-07 sessions is
|
||||||
branch `dev` with a large uncommitted diff (`src/`, the docs). Commit only
|
committed. The user said "do not commit yet" (2026-10-05), and on
|
||||||
when the user asks.
|
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
|
## The three projects
|
||||||
|
|
||||||
@@ -31,11 +248,12 @@ when the user asks.
|
|||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `~/Projects/libglacier-ng` | C library, design docs, this file | yes (`dev`) |
|
| `~/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/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
|
Nothing prebuilt is kept in either repo (the host-built `.so`s in
|
||||||
won't load on musl. Every Alpine image and test builds libglacier-ng from
|
`glacier-ng/lib/glacier/` were removed 2026-10-06). Every image and test
|
||||||
source instead.
|
builds libglacier-ng from source; the Dockerfiles create
|
||||||
|
`/app/lib/glacier/` with the musl `.so`s.
|
||||||
|
|
||||||
## What this project is
|
## 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]`
|
- The pre-trust-check `die()` in syspkg/gstore reproduces the `[x]`
|
||||||
format byte for byte, because it can't load `libglacier_log.so`
|
format byte for byte, because it can't load `libglacier_log.so`
|
||||||
before that file has been verified.
|
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
|
- **No hint messages.** Don't print lines that point users to other flags
|
||||||
or commands (for example "use -lO to ..."). The user called these
|
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
|
clutter. A refusal states the fact, in one line. The user also removed a
|
||||||
`Run doas gstore -n ...` follow-up hint from gstore themselves.
|
`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
|
- **Ask before settling a design choice that is the user's to make**, and
|
||||||
give a recommendation. They have answered every such question promptly.
|
give a recommendation. They have answered every such question promptly.
|
||||||
Don't silently pick a layout or semantics.
|
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
|
defence in a throwaway copy and confirm the test fails ("mutation
|
||||||
check"). Several defences here are verified that way.
|
check"). Several defences here are verified that way.
|
||||||
- Use doas, not sudo. syspkg is never setuid.
|
- 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)
|
## 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
|
- "Backed up" means the old package's files are compressed into a
|
||||||
`.gpkg`. A restore interface comes later.
|
`.gpkg`. A restore interface comes later.
|
||||||
- **`gpkg -u` / `syspkg -u` upgrade from a REMOTE repository**, and
|
- **`gpkg -u` / `syspkg -u` upgrade from a REMOTE repository**, and
|
||||||
`gpkg -f` is `-l` but fetched by name from a remote repository. Both wait
|
`gpkg -f` is `-l` but fetched by name from a remote repository. Done
|
||||||
for remote repos + signing (ROADMAP 3.1). Don't implement them against
|
2026-10-07 (3.1 step 4). Never against local files.
|
||||||
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
|
- `everest-bootscripts` is the name of the base `/etc` package. Its source
|
||||||
lives in `~/Projects/everest/pkgsrc/`.
|
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
|
## Core architecture
|
||||||
|
|
||||||
**`gl_context_t`** (transaction.h) is the central abstraction. Fields:
|
**`gl_context_t`** (transaction.h) is the central abstraction. Fields, in
|
||||||
`uid`, `root_path`, `index_path`, `store_path`, `links_path`, `mode`
|
order: `uid`, `root_path`, `index_path`, `store_path`, `links_path`,
|
||||||
(`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR`/`GL_SCOPE_SYS`),
|
`mode` (`GL_CTX_LIVE`/`GL_CTX_STAGE`), `scope`
|
||||||
`lock_fd`, and `overwrite` (last field, zeroed by the init functions).
|
(`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
|
**Layout.** Each scope owns ONE base directory holding
|
||||||
`{index,store,links,stage,old,lock,backup}`. Every create, rename and lock
|
`{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 |
|
| Scope | Base dir | Ownership |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `GL_SCOPE_USR` | `/glacier/usr/<uid>/` | uid, mode 0700, inside a root-owned 0755 `/glacier/usr` |
|
| `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
|
- The helpers `gl_usr_base`, `gl_usr_base_check` and the layout comment
|
||||||
are in `common.h`.
|
are in `common.h`.
|
||||||
@@ -126,11 +445,22 @@ a transaction does stays inside it.
|
|||||||
live package inside a transaction needs no special casing. Seeding
|
live package inside a transaction needs no special casing. Seeding
|
||||||
copies dotfiles: only `.` and `..` are skipped.
|
copies dotfiles: only `.` and `..` are skipped.
|
||||||
- A stage on another filesystem fails at seeding with `GL_TXN_ERR_XDEV`.
|
- A stage on another filesystem fails at seeding with `GL_TXN_ERR_XDEV`.
|
||||||
- **Commit:** rename-based promotion (stage → live, previous live → `old/`),
|
- **Commit:** first `gl_check_txn` (deps, conflicts, collisions, and
|
||||||
then fsync the base. Then `gl_relink_store` + `gl_rebuild_index`
|
strict link refusals if `ctx->strict`); any problem → nothing changes,
|
||||||
regenerate links and index from a *fresh live context* and rescan the
|
`GL_TXN_ERR_DEPS`. Then rename-based promotion (stage → live, previous
|
||||||
WHOLE store (every commit relinks every package, so link-time logic must
|
live → `old/`), then fsync the base. Then `gl_relink_store` regenerates
|
||||||
be idempotent). `old/` is then deleted.
|
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`):
|
**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-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-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 |
|
| `<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 |
|
| `<store>/.etc-state/<path>` | the last version of each `/etc` file a package offered |
|
||||||
|
|
||||||
**Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax).
|
**Configuration.** `/etc/glacier/glacier.conf` (libconfig syntax).
|
||||||
@@ -189,8 +520,20 @@ possibly shared:
|
|||||||
plus `O_NOFOLLOW`).
|
plus `O_NOFOLLOW`).
|
||||||
- Any entry with `..` or an absolute path refuses the whole package.
|
- Any entry with `..` or an absolute path refuses the whole package.
|
||||||
- A failed extraction removes its half-made version directory.
|
- A failed extraction removes its half-made version directory.
|
||||||
- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`. Every
|
- **Recursive deletes.** `gl_rmdir_recursive` skips only `.`/`..`, and
|
||||||
caller passes a glacier-only path; keep it that way. `gl_delete_user`
|
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
|
lstat-checks its target first, because a symlinked base would otherwise
|
||||||
have its *target* emptied.
|
have its *target* emptied.
|
||||||
- **Wide wrappers.** Don't add convenience wrappers that silently touch a
|
- **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`):
|
**Version replacement** (`-lO`, `M.install` in `gpkg_common.lua`):
|
||||||
1. Installed entries are read from the live index's `current` file
|
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
|
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
|
`-O` it's refused; with `-O`, `keep_old()` reads `KEEP_OLD`, and the
|
||||||
summary shows `old -> new`.
|
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
|
- `gl_init_sys` runs it before writing the index. The index marks `-n` as
|
||||||
complete, so a second `syspkg -n` is refused: `Cannot create
|
complete, so a second `syspkg -n` is refused: `Cannot create
|
||||||
/glacier/sys: already exists`.
|
/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.
|
**Package symlinks** are extracted verbatim and linked like files.
|
||||||
Hardlink, device and fifo entries are skipped. `add_to_archive` (used by
|
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 |
|
| 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` |
|
| `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...` `-x NAME...` `-O` `-S` `-V` `-h` `-v` | root via doas; `-h`/`-v` without root |
|
| `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` (stub) `-V` `-h` `-v` | `-n` needs root (direct, or `doas gstore -n` for yourself) |
|
| `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
|
- **gpkg:** modifiers `-O`/`-S`/`-V` are applied in a pre-pass, so their
|
||||||
position doesn't matter (`-lO` == `-Ol`). It finds its user via
|
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, `-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
|
- **syspkg as non-root:** `-n`/`-l`/`-x` are refused after argument
|
||||||
parsing with `Cannot open system index: Permission denied`.
|
parsing with `Cannot open system index: Permission denied`.
|
||||||
- **gstore, `-n` without a UID, under doas:** acts for the invoking user
|
- **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.
|
installed fails before the prompt.
|
||||||
- **Hardened loading** (syspkg and gstore): `#!/usr/bin/luajit -E`,
|
- **Hardened loading** (syspkg and gstore): `#!/usr/bin/luajit -E`,
|
||||||
`package.path` and every `ffi.load` pinned to their own `realpath`, and
|
`package.path` and every `ffi.load` pinned to their own `realpath`, and
|
||||||
when euid is 0, every ancestor directory and every loaded `.lua`/`.so`
|
when euid is 0, every loaded `.lua`/`.so`, and every ancestor of the
|
||||||
must be root-owned and not group/world-writable. The preamble is
|
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.
|
DUPLICATED in both files on purpose: change one, change both.
|
||||||
- **gpkg** uses the older CWD-relative convention (`require("lib/...")`,
|
- **gpkg** (2026-10-06) loads `lib/` from its own realpath, like
|
||||||
`ffi.load("./lib/glacier/...")`): run it from its own directory.
|
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
|
- **Declarations:** `lib/glacier_cdef.lua` is the single source of truth
|
||||||
for every `ffi.cdef`. `tests/glacier_ffi.lua` is only a wrapper around
|
for every `ffi.cdef`. `tests/glacier_ffi.lua` is only a wrapper around
|
||||||
it. Declaration ORDER matters: a declaration using `gl_context_t` must
|
it. Declaration ORDER matters: a declaration using `gl_context_t` must
|
||||||
@@ -303,7 +738,9 @@ backups and `gstore -b`) archives symlinks.
|
|||||||
## Build tooling
|
## Build tooling
|
||||||
|
|
||||||
- **gbuild** (bash, the only copy is `glacier-ng/gbuild`):
|
- **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
|
`-v`/`--version` prints the version, `--verbose` turns on verbose
|
||||||
output.
|
output.
|
||||||
- Autodetects the build system, with autotools checked BEFORE a bare
|
- Autodetects the build system, with autotools checked BEFORE a bare
|
||||||
@@ -320,15 +757,29 @@ backups and `gstore -b`) archives symlinks.
|
|||||||
(`Dockerfile.build`).
|
(`Dockerfile.build`).
|
||||||
- **Kernel version:** linux `6.12.112`; plain `v6.12` doesn't build with
|
- **Kernel version:** linux `6.12.112`; plain `v6.12` doesn't build with
|
||||||
GCC 15.
|
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
|
## libglacier-ng build
|
||||||
|
|
||||||
- **Separate `.so` per module** (`libglacier_log`, `_pkg`, `_istoreutils`,
|
- **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.
|
third-party program links only what it needs.
|
||||||
- **The combined `libglacier-ng.so`** (`make libglacier-ng`) exists only
|
- **The combined `libglacier-ng.so`** (`make libglacier-ng`) exists only
|
||||||
for the FFI tests.
|
for the FFI tests.
|
||||||
- **`libglacier_verify.so`** is private: no public header.
|
- **`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`
|
- **Warnings:** `config.mk` builds with `-Werror`. `-Wno-format-truncation`
|
||||||
is set because that warning is noise; `-Wstringop-truncation` stays
|
is set because that warning is noise; `-Wstringop-truncation` stays
|
||||||
fatal.
|
fatal.
|
||||||
@@ -340,7 +791,7 @@ backups and `gstore -b`) archives symlinks.
|
|||||||
Run everything in throwaway containers. Several suites write to `/usr`,
|
Run everything in throwaway containers. Several suites write to `/usr`,
|
||||||
`/etc` and `/glacier`.
|
`/etc` and `/glacier`.
|
||||||
|
|
||||||
**FFI suite** (`glacier-ng/tests/*.lua`; 8 files, all passing):
|
**FFI suite** (`glacier-ng/tests/*.lua`; 11 files, all passing):
|
||||||
```sh
|
```sh
|
||||||
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
|
docker run --rm --cap-add SYS_ADMIN -v ~/Projects/glacier-ng:/src:ro \
|
||||||
-v ~/Projects/libglacier-ng:/libsrc:ro alpine:latest sh -c '
|
-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 |
|
| 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` |
|
| `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:
|
Build the `glacier-alpine_root` image with:
|
||||||
```sh
|
```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
|
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}`.
|
**Interactive images:** `./run_docker.sh Dockerfile.{alpine,alpine_root,arch,debian}`.
|
||||||
`run_docker.sh` passes the `libglacier` build context. `alpine`, `arch`
|
`run_docker.sh` passes the `libglacier` build context. `alpine`, `arch`
|
||||||
and `debian` default to user `glacier` (uid 1000, in wheel); `alpine_root`
|
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`;
|
- root-owned `/app`;
|
||||||
- no `/glacier` until `gstore -n` / `syspkg -n`;
|
- no `/glacier` until `gstore -n` / `syspkg -n`;
|
||||||
- the doas rule plus `permit nopass glacier`;
|
- 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.
|
- `glacier.conf` installed.
|
||||||
|
|
||||||
**Boot test:** `cd ~/Projects/everest && ./everest.sh boot` boots QEMU
|
**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
|
## Bug history: so these don't get reintroduced
|
||||||
|
|
||||||
@@ -430,6 +936,99 @@ All fixed in the current tree.
|
|||||||
context every retry reported `ALREADY_INSTALLED`.
|
context every retry reported `ALREADY_INSTALLED`.
|
||||||
23. Backups written in the same second were pruned in the wrong order
|
23. Backups written in the same second were pruned in the wrong order
|
||||||
(whole-second mtimes).
|
(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
|
## Conventions
|
||||||
|
|
||||||
|
|||||||
+76
-16
@@ -2,9 +2,10 @@
|
|||||||
| glacier-bootstrap |
|
| glacier-bootstrap |
|
||||||
+--------------------------------------------+
|
+--------------------------------------------+
|
||||||
|
|
||||||
STATUS: PLAN. Nothing here is implemented. Section 9 lists the
|
STATUS: LOCAL MODE IMPLEMENTED 2026-10-06 (glacier-ng/glacier-bootstrap,
|
||||||
decisions still open; everything else is a recommendation to argue
|
tests/bootstrap_alpine.sh; see section 10 for how it differs from the
|
||||||
with, not a settled design.
|
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
|
glacier-bootstrap takes a template index and builds a chrootable
|
||||||
Everest system in a target directory, from either a local directory of
|
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)
|
3. fetch into a private cache; verify every hash (section 5)
|
||||||
4. doas syspkg --root TARGET -n create the tree
|
4. doas syspkg --root TARGET -n create the tree
|
||||||
5. doas syspkg --root TARGET --strict -l PKG... ONE transaction
|
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
|
Step 5 is one transaction on purpose: TARGET ends up either the whole
|
||||||
system or an empty tree, never half a system.
|
system or an empty tree, never half a system.
|
||||||
@@ -100,7 +102,6 @@ per package, so packages can carry pins:
|
|||||||
|
|
||||||
# templates/minimal.template
|
# templates/minimal.template
|
||||||
NAME=minimal
|
NAME=minimal
|
||||||
SYS_PROFILE=x86_64-musl
|
|
||||||
PKG=filesystem 1.0.0
|
PKG=filesystem 1.0.0
|
||||||
PKG=musl 1.2.5 sha256:3a5f...
|
PKG=musl 1.2.5 sha256:3a5f...
|
||||||
PKG=busybox 1.37.0 sha256:91c0...
|
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
|
- A pin is optional for -d (if present, it must ALSO match the
|
||||||
sidecar) and required for -r until signed repos exist.
|
sidecar) and required for -r until signed repos exist.
|
||||||
- Strict parsing: unknown keys, a duplicate PKG name, a malformed pin
|
- 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.
|
10: a field silently never parsing, with a default that looks right.
|
||||||
- A `glacier-bootstrap --pin PKGDIR TEMPLATE` helper that fills pins in
|
- A `glacier-bootstrap --pin PKGDIR TEMPLATE` helper that fills pins in
|
||||||
from local sidecars is the obvious way to produce a pinned template
|
from local sidecars is the obvious way to produce a pinned template
|
||||||
@@ -131,8 +134,10 @@ doesn't.)
|
|||||||
Recommendation: CHECK, don't auto-resolve.
|
Recommendation: CHECK, don't auto-resolve.
|
||||||
|
|
||||||
- Every PKG_DEPS entry of every listed package must itself be listed,
|
- Every PKG_DEPS entry of every listed package must itself be listed,
|
||||||
with a matching version. Otherwise error: "busybox needs musl 1.2.5,
|
at that version or later (decided 2026-10-06: the ROADMAP 2.3 rule, so
|
||||||
which the template does not list."
|
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).
|
- No two listed packages may conflict (PKG_CONFS).
|
||||||
- Each manifest's PKG_NAME/PKG_VER must match its template line. This
|
- 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.
|
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.
|
section 4). Pins mean nothing until syspkg actually checks sidecars.
|
||||||
2. [DONE] Symlink extraction + linking, with the write-through-symlink
|
2. [DONE] Symlink extraction + linking, with the write-through-symlink
|
||||||
defense (6a). Needed for anything to run.
|
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.
|
/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
|
refusal fails the transaction), and the skeleton on -n (6b) [the
|
||||||
skeleton is DONE; --root and --strict are not].
|
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).
|
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
|
MILESTONE: `glacier-bootstrap -t -d pkgs templates/minimal.template
|
||||||
/mnt/everest` succeeds, `chroot /mnt/everest /bin/sh` gives a
|
/mnt/everest` succeeds, `chroot /mnt/everest /bin/sh` gives a
|
||||||
working shell, and the host is byte-identical before and after.
|
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
|
a. IMAGE_DESIGN.txt section 9a still stands: images are bootstrap
|
||||||
OUTPUT, never install INPUT. Everything here assumes yes.
|
OUTPUT, never install INPUT. Everything here assumes yes.
|
||||||
b. Template format (section 3): one PKG= line per package with an
|
b. DECIDED 2026-10-06: NAME= plus one `PKG=name version [sha256:hex]`
|
||||||
optional pin, or something else.
|
per package; no SYS_PROFILE until manifests record one (section 3).
|
||||||
c. DECIDED 2026-10-04: option 1, copy on install, .new on conflict.
|
c. DECIDED 2026-10-04: option 1, copy on install, .new on conflict.
|
||||||
Implemented in istoreutils.c (install_config / prune_config): a
|
Implemented in istoreutils.c (install_config / prune_config): a
|
||||||
system-scope package's top-level etc/ is copied into /etc, never
|
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
|
d. DECIDED 2026-10-04: syspkg -n creates the top-level tree itself
|
||||||
(6b). Implemented as gl_create_fs_tree(root, verbose), called by
|
(6b). Implemented as gl_create_fs_tree(root, verbose), called by
|
||||||
gl_init_sys before the index is written.
|
gl_init_sys before the index is written.
|
||||||
e. Dependency handling (section 4): check-only, as recommended, or
|
e. DECIDED 2026-10-05: automatic closure, once there is a source to
|
||||||
automatic closure.
|
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
|
f. Pinning helper: part of glacier-bootstrap (--pin) or of gworld
|
||||||
(which already knows what it just built).
|
(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
@@ -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
|
exist" check has to become "does T + target exist". The same goes for
|
||||||
restore_displaced and for anything else that follows a link.
|
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
|
- gl_context_t gains `char sysroot[PATH_MAX]` ("" == "/"; appended
|
||||||
at the end, matched in lib/glacier_cdef.lua and
|
at the end, matched in lib/glacier_cdef.lua and
|
||||||
tests/glacier_ffi.lua).
|
tests/glacier_ffi.lua).
|
||||||
@@ -126,6 +129,12 @@ Proposed shape:
|
|||||||
wrappers passing "/", so every current caller is unchanged.
|
wrappers passing "/", so every current caller is unchanged.
|
||||||
- One helper, gl_at(ctx, runtime_path, out), used for EVERY
|
- One helper, gl_at(ctx, runtime_path, out), used for EVERY
|
||||||
filesystem operation. Runtime paths stay what's stored and compared.
|
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
|
- GL_BASE_LINKS_DEST resolves to sysroot + "/usr". Never the host's
|
||||||
/usr when a sysroot is set.
|
/usr when a sysroot is set.
|
||||||
|
|
||||||
@@ -239,9 +248,13 @@ Consequences of /usr starting empty that the design has to own:
|
|||||||
| 8. Milestones |
|
| 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.
|
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).
|
3. A `filesystem` base package (section 7).
|
||||||
4. images/minimal.image + a driver script (gworld -> syspkg --root
|
4. images/minimal.image + a driver script (gworld -> syspkg --root
|
||||||
-> finalize). Milestone: `chroot T /bin/sh` works, and
|
-> finalize). Milestone: `chroot T /bin/sh` works, and
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
include config.mk
|
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"
|
@echo "libglacier-ng has finished building"
|
||||||
|
|
||||||
static: prepare log_static pkg_static istoreutils_static libglacier.a
|
static: prepare log_static pkg_static istoreutils_static libglacier.a
|
||||||
@@ -15,12 +15,14 @@ pkg_static:
|
|||||||
|
|
||||||
istoreutils_static: pkg_static
|
istoreutils_static: pkg_static
|
||||||
$(CC) $(CFLAGS) -c src/istoreutils.c -o build/lib/tmp/istoreutils.o
|
$(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:
|
libglacier.a:
|
||||||
ar rcs build/lib/libglacier.a \
|
ar rcs build/lib/libglacier.a \
|
||||||
build/lib/tmp/log.o \
|
build/lib/tmp/log.o \
|
||||||
build/lib/tmp/pkg.o \
|
build/lib/tmp/pkg.o \
|
||||||
build/lib/tmp/istoreutils.o
|
build/lib/tmp/istoreutils.o \
|
||||||
|
build/lib/tmp/deps.o
|
||||||
|
|
||||||
prepare:
|
prepare:
|
||||||
mkdir -p build
|
mkdir -p build
|
||||||
@@ -38,19 +40,30 @@ log:
|
|||||||
log_test:
|
log_test:
|
||||||
$(CC) $(CFLAGS) -shared -fPIC src/log.c -o build/lib/shared/libglacier_log.so
|
$(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:
|
sha256:
|
||||||
$(CC) $(CFLAGS) src/sha256.c -c -o build/lib/tmp/sha256.o
|
$(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:
|
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
|
-o build/lib/shared/libglacier_verify.so
|
||||||
|
|
||||||
gsha256sum: prepare
|
gsha256sum: prepare
|
||||||
$(CC) $(CFLAGS) src/gsha256sum.c src/sha256.c -o build/bin/gsha256sum
|
$(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
|
transaction: log istoreutils
|
||||||
$(CC) $(CFLAGS) -shared -fPIC src/transaction.c \
|
$(CC) $(CFLAGS) -shared -fPIC src/transaction.c \
|
||||||
-Lbuild/lib/shared -lglacier_istoreutils -lglacier_log \
|
-Lbuild/lib/shared -lglacier_istoreutils -lglacier_log \
|
||||||
@@ -58,7 +71,7 @@ transaction: log istoreutils
|
|||||||
-o build/lib/shared/libglacier_transaction.so
|
-o build/lib/shared/libglacier_transaction.so
|
||||||
|
|
||||||
istoreutils: pkg
|
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 \
|
-Lbuild/lib/shared -lglacier_log -lglacier_pkg -larchive \
|
||||||
-Wl,-rpath,'$$ORIGIN' \
|
-Wl,-rpath,'$$ORIGIN' \
|
||||||
-o build/lib/shared/libglacier_istoreutils.so
|
-o build/lib/shared/libglacier_istoreutils.so
|
||||||
@@ -69,21 +82,12 @@ pkg:
|
|||||||
-Wl,-rpath,'$$ORIGIN' \
|
-Wl,-rpath,'$$ORIGIN' \
|
||||||
-o build/lib/shared/libglacier_pkg.so
|
-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
|
libglacier-ng: prepare
|
||||||
$(CC) $(CFLAGS) -shared -fPIC \
|
$(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 \
|
-larchive -lconfig \
|
||||||
-o build/lib/shared/libglacier-ng.so
|
-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:
|
install:
|
||||||
mkdir -p $(PREFIX)/lib/glacier/
|
mkdir -p $(PREFIX)/lib/glacier/
|
||||||
install build/lib/shared/libglacier_log.so $(PREFIX)/lib/glacier/ -m 755
|
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_pkg.so $(PREFIX)/lib/glacier -m 755
|
||||||
install build/lib/shared/libglacier_verify.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/gsha256sum $(PREFIX)/bin/ -m 755
|
||||||
|
install build/bin/gsign $(PREFIX)/bin/ -m 755
|
||||||
|
install build/bin/grepo $(PREFIX)/bin/ -m 755
|
||||||
|
|
||||||
install_static:
|
install_static:
|
||||||
mkdir -p $(PREFIX)/lib/glacier
|
mkdir -p $(PREFIX)/lib/glacier
|
||||||
install build/lib/libglacier.a $(PREFIX)/lib/glacier/ -m 644
|
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:
|
clean:
|
||||||
rm -rf build
|
rm -rf build
|
||||||
@@ -35,6 +35,13 @@ SYS (GL_SCOPE_SYS)
|
|||||||
System-wide packages, not owned by any particular uid:
|
System-wide packages, not owned by any particular uid:
|
||||||
/glacier/sys/{index,store,links,stage,old,lock}
|
/glacier/sys/{index,store,links,stage,old,lock}
|
||||||
Symlink destination depends on the package's repo (see section 3).
|
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
|
Requires root. gpkg enforces this via geteuid() before it will
|
||||||
even attempt a system-scope operation.
|
even attempt a system-scope operation.
|
||||||
|
|
||||||
|
|||||||
+446
@@ -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
@@ -2,13 +2,14 @@
|
|||||||
| Glacier roadmap |
|
| 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
|
Design detail lives in the companion docs; this file says WHAT and in
|
||||||
what order, and links to WHERE:
|
what order, and links to WHERE:
|
||||||
|
|
||||||
SECURITY_DESIGN.txt integrity, scope separation, refusal matrix
|
SECURITY_DESIGN.txt integrity, scope separation, refusal matrix
|
||||||
PACKAGE_SCOPES.txt layout, link routing, /etc config handling
|
PACKAGE_SCOPES.txt layout, link routing, /etc config handling
|
||||||
IMAGE_DESIGN.txt system images as bootstrap output, sysroot
|
IMAGE_DESIGN.txt system images as bootstrap output, sysroot
|
||||||
|
REPO_DESIGN.txt remote repositories, signing, keyring
|
||||||
GLACIER_BOOTSTRAP.txt glacier-bootstrap plan, prerequisites, decisions
|
GLACIER_BOOTSTRAP.txt glacier-bootstrap plan, prerequisites, decisions
|
||||||
|
|
||||||
Mark an item [DONE yyyy-mm-dd] rather than deleting it, so the history
|
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"
|
The critical path: from "QEMU boots an initramfs assembled by a script"
|
||||||
(~/Projects/everest) to "glacier installs a system".
|
(~/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
|
hardcoded to /glacier and /usr. Needs separate install-root
|
||||||
(where bytes are written) and runtime-root (what symlink targets
|
(where bytes are written) and runtime-root (what symlink targets
|
||||||
say). The trap: prune_stale_links follows absolute links, so a
|
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
|
filesystem. Required test: an image build leaves the host's /usr
|
||||||
and /glacier byte-identical.
|
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
|
root-owned and not group/world-writable) and --strict (any link
|
||||||
refusal fails the transaction; an image's /usr starts empty).
|
refusal fails the transaction; an image's /usr starts empty).
|
||||||
gl_create_fs_tree already takes a root prefix.
|
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
|
Template index -> resolve -> verify -> syspkg --root, one
|
||||||
transaction. Never writes into TARGET itself.
|
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,
|
itself: luajit, libarchive (+ zlib), libconfig, libglacier-ng,
|
||||||
glacier-ng (syspkg, gpkg, gstore), doas.
|
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
|
(SECURITY_DESIGN section 10). Partitioning, a bootloader or an
|
||||||
EFI-stub kernel on the ESP, copying the kernel there (a symlink
|
EFI-stub kernel on the ESP, copying the kernel there (a symlink
|
||||||
won't do on FAT), a disk image.
|
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
|
- A restore interface for backups: later. (A backup can already
|
||||||
be reinstalled with -lO.)
|
be reinstalled with -lO.)
|
||||||
- `gpkg -u` / `syspkg -u` = upgrade from a REMOTE repository;
|
- `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.
|
- /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
|
except the package comes from a remote repository by name
|
||||||
instead of from a local file. Same summary, verification, scope
|
instead of from a local file. Same summary, verification, scope
|
||||||
rules and transaction. Depends on 3.1 (remote repositories +
|
rules and transaction. Depends on 3.1 (remote repositories +
|
||||||
signing): until then there is nothing safe to fetch from.
|
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
|
- install: refuse if a dependency isn't installed (or in the
|
||||||
same transaction);
|
same transaction);
|
||||||
- REMOVE: refuse to remove a package others depend on. Today
|
- 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;
|
the system. Highest-value item in this section;
|
||||||
- conflicts: refuse to install a package that conflicts with
|
- conflicts: refuse to install a package that conflicts with
|
||||||
an installed one.
|
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
|
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.
|
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
|
deletes it. Keep it (or the last N), plus a command to return to
|
||||||
it. Interacts with 2.1's KEEP_OLD.
|
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.
|
path — whichever links last silently wins. Detect and refuse.
|
||||||
|
|
||||||
2.7 glacier.conf parsing. [DONE 2026-10-04 for integers:
|
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
|
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
|
glacier-bootstrap. Until then, GLACIER_BOOTSTRAP's pinned
|
||||||
templates are the safe bridge for remote fetches.
|
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
|
exists in the tree (only one gbuild, in glacier-ng). Today the
|
||||||
target libc is decided by where gbuild runs (~/Projects/everest
|
target libc is decided by where gbuild runs (~/Projects/everest
|
||||||
builds in Alpine for musl).
|
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
|
4.2 Move ~/Projects/everest's per-package gbuild commands into gworld
|
||||||
recipes once 4.1 lands.
|
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 /
|
- Images are only ever bootstrap OUTPUT (IMAGE_DESIGN 9a /
|
||||||
GLACIER_BOOTSTRAP 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).
|
- Where the pinning helper lives (GLACIER_BOOTSTRAP 9f).
|
||||||
- 2.1's open questions a-d (upgrade backups).
|
- 2.1's open questions a-d (upgrade backups).
|
||||||
- extra vs community: should they ever behave differently?
|
- 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 |
|
| 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,
|
[DONE 2026-10-04] Version replacement + backups (2.1): -lO replaces,
|
||||||
KEEP_OLD backups, gl_backup_pkg / gl_prune_backups / gl_conf_get_int,
|
KEEP_OLD backups, gl_backup_pkg / gl_prune_backups / gl_conf_get_int,
|
||||||
the manifest saved in the store (.glacier-manifest), add_to_archive
|
the manifest saved in the store (.glacier-manifest), add_to_archive
|
||||||
|
|||||||
@@ -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 —
|
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
|
not being designed in detail now, but a few choices below are made to
|
||||||
stay cheap to extend into later.
|
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
|
Everest is a security-focused distro (this is also *why* per-user
|
||||||
package isolation exists in the first place). Two sides of the CIA
|
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
|
— a hardened, audited, purpose-built tool for exactly this — not
|
||||||
reimplemented inside syspkg itself.
|
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 |
|
| 7. Shared code: gpkg_common.lua |
|
||||||
|
|||||||
@@ -27,6 +27,12 @@ root, or shadows real `/usr` binaries."
|
|||||||
* **Severity:** High
|
* **Severity:** High
|
||||||
* **Category:** `authorization-bypass`
|
* **Category:** `authorization-bypass`
|
||||||
* **Confidence:** High
|
* **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
|
**Description:** System-scope symlink placement into `/usr` is gated only
|
||||||
by a package's manifest self-declaring `PKG_REPO="base"`. There is no
|
by a package's manifest self-declaring `PKG_REPO="base"`. There is no
|
||||||
|
|||||||
@@ -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.
@@ -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.
@@ -6,4 +6,3 @@ PREFIX ?= /usr
|
|||||||
|
|
||||||
CFLAGS = -std=c11 -pedantic -O2 -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror -Wno-format-truncation
|
CFLAGS = -std=c11 -pedantic -O2 -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror -Wno-format-truncation
|
||||||
|
|
||||||
TEST_DIR = /home/lw/Projects/glacier-ng/lib
|
|
||||||
+141
-34
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
#include <dirent.h>
|
#include <dirent.h>
|
||||||
#include <errno.h>
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
#include <linux/limits.h>
|
#include <linux/limits.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
@@ -13,13 +14,6 @@
|
|||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
#include <unistd.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
|
* On-disk layout. Each scope keeps everything it ever creates, renames
|
||||||
* or locks under ONE base directory it owns:
|
* or locks under ONE base directory it owns:
|
||||||
@@ -35,6 +29,21 @@ dir_t;
|
|||||||
* an unprivileged user run their own transactions at all.
|
* an unprivileged user run their own transactions at all.
|
||||||
*/
|
*/
|
||||||
#define GL_USR_ROOT "/glacier/usr"
|
#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"
|
#define GL_SYS_ROOT "/glacier/sys"
|
||||||
|
|
||||||
static inline void
|
static inline void
|
||||||
@@ -78,6 +87,98 @@ gl_usr_base_check(uid_t uid)
|
|||||||
return GL_USR_BASE_OK;
|
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
|
static inline int
|
||||||
gl_mkdirp(const char *path, mode_t mode)
|
gl_mkdirp(const char *path, mode_t mode)
|
||||||
{
|
{
|
||||||
@@ -88,51 +189,57 @@ gl_mkdirp(const char *path, mode_t mode)
|
|||||||
for (char *p = tmp + 1; *p; p++) {
|
for (char *p = tmp + 1; *p; p++) {
|
||||||
if (*p == '/') {
|
if (*p == '/') {
|
||||||
*p = '\0';
|
*p = '\0';
|
||||||
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
|
if (gl_mkdir_exact(tmp, mode) != 0) {
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
*p = '/';
|
*p = '/';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
|
return gl_mkdir_exact(tmp, mode);
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static inline int
|
/* Empties the directory open at `fd` (and closes it). Every step is
|
||||||
gl_rmdir_recursive(const char *path)
|
* 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 = opendir(path);
|
DIR *dir = fdopendir(fd);
|
||||||
if (!dir) { return -1; }
|
if (!dir) {
|
||||||
|
close(fd);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
struct dirent *ent;
|
struct dirent *ent;
|
||||||
while ((ent = readdir(dir))) {
|
while ((ent = readdir(dir))) {
|
||||||
/* Only . and .. — skipping every dotfile left any directory
|
/* Only . and ..: dotfiles are content too (a package's own,
|
||||||
* containing one (a package's own dotfiles, the store's
|
* the store's .displaced tree). */
|
||||||
* .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) {
|
if (strcmp(ent->d_name, ".") == 0 || strcmp(ent->d_name, "..") == 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
int sub = openat(dirfd(dir), ent->d_name,
|
||||||
char full[PATH_MAX];
|
O_RDONLY | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC);
|
||||||
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
|
if (sub >= 0) {
|
||||||
|
gl_empty_dir_fd(sub);
|
||||||
struct stat st;
|
unlinkat(dirfd(dir), ent->d_name, AT_REMOVEDIR);
|
||||||
if (lstat(full, &st) != 0) { continue; }
|
} else {
|
||||||
|
unlinkat(dirfd(dir), ent->d_name, 0);
|
||||||
if (S_ISDIR(st.st_mode)) {
|
|
||||||
gl_rmdir_recursive(full);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
unlink(full);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
closedir(dir);
|
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)
|
||||||
|
{
|
||||||
|
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);
|
return rmdir(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -1,18 +1,54 @@
|
|||||||
|
#include <getopt.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
|
|
||||||
#include "sha256.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
|
int
|
||||||
main(int argc, char *argv[])
|
main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
if (argc != 2) {
|
int c;
|
||||||
fprintf(stderr, "usage: %s FILE\n", argv[0]);
|
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;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
char hex[GL_SHA256_HEX_SIZE];
|
char hex[GL_SHA256_HEX_SIZE];
|
||||||
if (gl_sha256_file(argv[1], hex) != 0) {
|
if (gl_sha256_file(argv[optind], hex) != 0) {
|
||||||
perror(argv[1]);
|
perror(argv[optind]);
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+212
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
+361
-497
File diff suppressed because it is too large.
Load diff
+9
-26
@@ -5,6 +5,7 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#include "pkg.h"
|
||||||
#include "transaction.h"
|
#include "transaction.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
@@ -24,12 +25,6 @@ typedef enum {
|
|||||||
}
|
}
|
||||||
gl_backup_status_t;
|
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_OK: installation successful
|
||||||
* GL_INSTALL_ERR_OPEN: unable to open .gpkg file
|
* 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_EXTRACT: unable to extract files
|
||||||
* GL_INSTALL_ERR_INDEX: unable to update index
|
* GL_INSTALL_ERR_INDEX: unable to update index
|
||||||
* GL_INSTALL_ERR_ALREADY_INSTALLED: package already installed
|
* 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 {
|
typedef enum {
|
||||||
@@ -48,6 +46,7 @@ typedef enum {
|
|||||||
GL_INSTALL_ERR_ALREADY_INSTALLED = 5,
|
GL_INSTALL_ERR_ALREADY_INSTALLED = 5,
|
||||||
GL_INSTALL_SKIPPED = 6,
|
GL_INSTALL_SKIPPED = 6,
|
||||||
GL_INSTALL_ERR_SCOPE = 7,
|
GL_INSTALL_ERR_SCOPE = 7,
|
||||||
|
GL_INSTALL_ERR_DEPS = 8, /* live context only; see deps.h */
|
||||||
}
|
}
|
||||||
gl_install_status_t;
|
gl_install_status_t;
|
||||||
|
|
||||||
@@ -56,39 +55,22 @@ typedef enum {
|
|||||||
GL_REMOVE_ERR_NOT_FOUND = 1,
|
GL_REMOVE_ERR_NOT_FOUND = 1,
|
||||||
GL_REMOVE_ERR_UNLINK = 2,
|
GL_REMOVE_ERR_UNLINK = 2,
|
||||||
GL_REMOVE_ERR_INDEX = 3,
|
GL_REMOVE_ERR_INDEX = 3,
|
||||||
|
GL_REMOVE_ERR_DEPS = 4, /* live context only; see deps.h */
|
||||||
} gl_remove_status_t;
|
} 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_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_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_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_prune_backups(const char *dir, int keep);
|
||||||
|
|
||||||
int gl_init_user(uid_t uid, bool isVerbose);
|
int gl_init_user(uid_t uid, bool isVerbose);
|
||||||
int gl_init_sys(bool isVerbose);
|
int gl_init_sys(bool isVerbose);
|
||||||
|
int gl_init_sys_at(const char *sysroot, bool isVerbose);
|
||||||
int gl_create_fs_tree(const char *root, bool isVerbose);
|
int gl_create_fs_tree(const char *root, bool isVerbose);
|
||||||
int gl_delete_user(uid_t uid, bool isVerbose);
|
int gl_delete_user(uid_t uid, bool isVerbose);
|
||||||
|
|
||||||
int gl_parse_index(struct gindex *idx, FILE *f);
|
|
||||||
void gl_free_index(struct gindex *idx);
|
|
||||||
|
|
||||||
int gl_rebuild_index(gl_context_t *ctx);
|
int gl_rebuild_index(gl_context_t *ctx);
|
||||||
|
|
||||||
char *gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name);
|
|
||||||
|
|
||||||
typedef struct {
|
typedef struct {
|
||||||
char **repos;
|
char **repos;
|
||||||
size_t count;
|
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,
|
gl_backup_status_t gl_backup_istore(const char *tar_path, const char *glacier_root,
|
||||||
const char *uid);
|
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);
|
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);
|
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);
|
int gl_relink_store(gl_context_t *ctx);
|
||||||
|
|||||||
@@ -8,19 +8,8 @@
|
|||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <string.h>
|
#include <string.h>
|
||||||
#include <syslog.h>
|
|
||||||
#include <time.h>
|
|
||||||
|
|
||||||
#include "color.h"
|
#include "log.h"
|
||||||
|
|
||||||
typedef enum
|
|
||||||
{
|
|
||||||
LG_INFO,
|
|
||||||
LG_WARN,
|
|
||||||
LG_ERR,
|
|
||||||
LG_SUCCESS
|
|
||||||
}
|
|
||||||
loglevel_t;
|
|
||||||
|
|
||||||
static int
|
static int
|
||||||
supports_utf8(void)
|
supports_utf8(void)
|
||||||
@@ -34,26 +23,6 @@ supports_utf8(void)
|
|||||||
#endif
|
#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
|
void
|
||||||
lg_printf(loglevel_t loglevel, const char *fmt, ...)
|
lg_printf(loglevel_t loglevel, const char *fmt, ...)
|
||||||
{
|
{
|
||||||
@@ -92,104 +61,22 @@ lg_printf(loglevel_t loglevel, const char *fmt, ...)
|
|||||||
printf("\n");
|
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
|
void
|
||||||
lg_gpkg_help(int argc, char *argv[])
|
lg_gpkg_help(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
(void)argc;
|
(void)argc;
|
||||||
|
|
||||||
printf("%s: Install, update, and remove packages\n", argv[0]);
|
printf("%s: Install, update, and remove packages\n", argv[0]);
|
||||||
printf("usage: %s [-hv] [-flux PKG] [-OSV]\n", argv[0]);
|
printf("usage: %s [-hv] [-y] [-flux PKG] [-OSV]\n", argv[0]);
|
||||||
printf("\n");
|
printf("\n");
|
||||||
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
||||||
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
||||||
printf("%-25s %s\n", "{-l --local} FILE...", "Install local package files");
|
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", "{-f --merge} PKG...", "Install packages from a remote repository");
|
||||||
printf("%-25s %s\n", "{-u --update} PKG...", "Upgrade installed packages (not yet implemented)");
|
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", "{-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", "{-S --skip-installed}", "Skip packages that are already up to date");
|
||||||
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
||||||
printf("\n");
|
printf("\n");
|
||||||
@@ -203,16 +90,21 @@ lg_syspkg_help(int argc, char *argv[])
|
|||||||
(void)argc;
|
(void)argc;
|
||||||
|
|
||||||
printf("%s: Manage the system package index\n", argv[0]);
|
printf("%s: Manage the system package index\n", argv[0]);
|
||||||
printf("usage: %s [-hv] [-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("\n");
|
||||||
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
printf("%-25s %s\n", "{-h --help}", "Show this message");
|
||||||
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
printf("%-25s %s\n", "{-v --version}", "Show the current version");
|
||||||
printf("%-25s %s\n", "{-n --new}", "Create the system index and store");
|
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", "{-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", "{-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", "{-S --skip-installed}", "Skip packages that are already up to date");
|
||||||
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
printf("%-25s %s\n", "{-V --verbose}", "Log events more verbosely");
|
||||||
|
printf("%-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("\n");
|
||||||
printf("This program is free software.\n");
|
printf("This program is free software.\n");
|
||||||
printf("See the GNU GPL version 3 for details.\n");
|
printf("See the GNU GPL version 3 for details.\n");
|
||||||
|
|||||||
@@ -7,9 +7,6 @@
|
|||||||
|
|
||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <stdarg.h>
|
#include <stdarg.h>
|
||||||
#include <stdbool.h>
|
|
||||||
#include <syslog.h>
|
|
||||||
#include <time.h>
|
|
||||||
|
|
||||||
#include "color.h"
|
#include "color.h"
|
||||||
|
|
||||||
@@ -26,13 +23,13 @@ typedef enum
|
|||||||
}
|
}
|
||||||
loglevel_t;
|
loglevel_t;
|
||||||
|
|
||||||
int supports_utf8(void);
|
void lg_printf(loglevel_t loglevel, const char *fmt, ...)
|
||||||
const char *p2s(int priority);
|
__attribute__((format(printf, 2, 3)));
|
||||||
|
|
||||||
void lg_printf(loglevel_t loglevel, const char *fmt, ...);
|
/* The CLIs' -h output. */
|
||||||
void lg_printf_no_newline(loglevel_t loglevel, const char *fmt, ...);
|
void lg_gpkg_help(int argc, char *argv[]);
|
||||||
void lg_syslog(int priority, const char *fmt, ...);
|
void lg_syspkg_help(int argc, char *argv[]);
|
||||||
void lg_log(bool use_syslog, FILE *logfile, int priority, const char *fmt, ...);
|
void lg_gstore_help(int argc, char *argv[]);
|
||||||
|
|
||||||
#ifdef __cplusplus
|
#ifdef __cplusplus
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
|
||||||
}
|
|
||||||
@@ -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.
|
||||||
@@ -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.
|
||||||
@@ -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
|
||||||
@@ -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
@@ -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
|
||||||
@@ -15,45 +15,9 @@
|
|||||||
#include "common.h"
|
#include "common.h"
|
||||||
#include "pkg.h"
|
#include "pkg.h"
|
||||||
|
|
||||||
/* null dependencies/conflicts for error throwing */
|
/* "MAJ.MIN.PAT[-TAG]". Each number is read with strtol and clamped to
|
||||||
dep_t null_dep = { "null", {-1, -1, -1, NULL} };
|
* 0..INT_MAX (a manifest is untrusted: sscanf's %d overflowing is
|
||||||
conf_t null_conf = { "null", {-1, -1, -1, NULL} };
|
* undefined); anything missing or unparseable is 0. */
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
semver_t
|
semver_t
|
||||||
gl_parse_semver(const char *str)
|
gl_parse_semver(const char *str)
|
||||||
{
|
{
|
||||||
@@ -61,17 +25,25 @@ gl_parse_semver(const char *str)
|
|||||||
if (!str) { return v; }
|
if (!str) { return v; }
|
||||||
|
|
||||||
char buffer[64];
|
char buffer[64];
|
||||||
strncpy(buffer, str, sizeof(buffer) - 1);
|
snprintf(buffer, sizeof(buffer), "%s", str);
|
||||||
buffer[sizeof(buffer) - 1] = '\0';
|
|
||||||
|
|
||||||
char *dash = strchr(buffer, '-');
|
char *dash = strchr(buffer, '-');
|
||||||
if (dash) {
|
if (dash) {
|
||||||
*dash = '\0';
|
*dash = '\0';
|
||||||
dash++;
|
v.tag = strdup(dash + 1);
|
||||||
v.tag = strdup(dash);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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;
|
return v;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,47 +53,6 @@ gl_free_semver(semver_t *v)
|
|||||||
if (v && v->tag) { free (v->tag); };
|
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
|
* gl_valid_pkg_ident
|
||||||
* DESCRIPTION: checks that a package name or repo name is safe to use as a
|
* 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;
|
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
|
int
|
||||||
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
|
gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg)
|
||||||
{
|
{
|
||||||
|
char *text = read_manifest_text(filename);
|
||||||
|
if (!text) { return ENOENT; }
|
||||||
|
|
||||||
config_t cfg;
|
config_t cfg;
|
||||||
config_setting_t *deps, *confs;
|
|
||||||
const char *tmp_str;
|
|
||||||
const char *name = NULL, *repo = NULL;
|
|
||||||
|
|
||||||
config_init(&cfg);
|
config_init(&cfg);
|
||||||
|
int parsed = config_read_string(&cfg, text);
|
||||||
if (!config_read_file(&cfg, filename)) {
|
free(text);
|
||||||
|
if (!parsed) {
|
||||||
config_destroy(&cfg);
|
config_destroy(&cfg);
|
||||||
return ENOENT;
|
return ENOENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Validate before anything is strdup'd, so a rejected manifest leaves
|
/* Validate before anything is strdup'd, so a rejected manifest leaves
|
||||||
* pkg untouched and the caller has nothing to free. */
|
* 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_NAME", &name);
|
||||||
config_lookup_string(&cfg, "PKG_REPO", &repo);
|
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);
|
config_destroy(&cfg);
|
||||||
return EINVAL;
|
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_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); }
|
if (config_lookup_string(&cfg, "PKG_VER", &tmp_str)) { pkg->pkg_ver = gl_parse_semver(tmp_str); }
|
||||||
|
|
||||||
deps = config_lookup(&cfg, "PKG_DEPS");
|
if (dnames) {
|
||||||
if (deps && config_setting_is_list(deps)) {
|
size_t n = 0;
|
||||||
int count = config_setting_length(deps);
|
while (dnames[n]) { n++; }
|
||||||
pkg->pkg_dependencies = calloc((size_t)(count + 1), sizeof(dep_t));
|
pkg->pkg_dependencies = calloc(n + 1, sizeof(dep_t));
|
||||||
|
for (size_t i = 0; pkg->pkg_dependencies && i < n; i++) {
|
||||||
for (int i = 0; i < count; i++) {
|
pkg->pkg_dependencies[i].dep_name = dnames[i];
|
||||||
config_setting_t *d = config_setting_get_elem(deps, (unsigned int)i);
|
pkg->pkg_dependencies[i].dep_ver = dvers[i];
|
||||||
const char *dep_name = NULL;
|
dnames[i] = 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 (cnames) {
|
||||||
confs = config_lookup(&cfg, "PKG_CONFS");
|
size_t n = 0;
|
||||||
if (confs && config_setting_is_list(confs)) {
|
while (cnames[n]) { n++; }
|
||||||
int count = config_setting_length(confs);
|
pkg->pkg_conflicts = calloc(n + 1, sizeof(conf_t));
|
||||||
pkg->pkg_conflicts = calloc((size_t)(count + 1), sizeof(conf_t));
|
for (size_t i = 0; pkg->pkg_conflicts && i < n; i++) {
|
||||||
|
pkg->pkg_conflicts[i].conf_name = cnames[i];
|
||||||
for (int i = 0; i < count; i++) {
|
pkg->pkg_conflicts[i].conf_ver = cvers[i];
|
||||||
config_setting_t *c = config_setting_get_elem(confs, (unsigned int)i);
|
cnames[i] = NULL;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
free_relations(dnames, dvers);
|
||||||
|
free_relations(cnames, cvers);
|
||||||
config_destroy(&cfg);
|
config_destroy(&cfg);
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
char
|
if (!pkg->pkg_name || !pkg->pkg_repo || (dnames && !pkg->pkg_dependencies) ||
|
||||||
*gl_cat_dir(const dir_t *a, const dir_t *b)
|
(cnames && !pkg->pkg_conflicts)) {
|
||||||
{
|
gl_free_pkg(pkg);
|
||||||
size_t length = strlen(a->path) + strlen(b->path) + 2;
|
memset(pkg, 0, sizeof(*pkg));
|
||||||
char *result = malloc(length);
|
return ENOMEM;
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void
|
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
|
* Read integer `key` from a libconfig file (glacier.conf). Distinguishes
|
||||||
* the failures a caller has to report differently:
|
* 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);
|
config_destroy(&cfg);
|
||||||
return GL_CONF_OK;
|
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;
|
||||||
|
}
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
#include <libconfig.h>
|
#include <libconfig.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
#include <stddef.h>
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
@@ -43,44 +44,57 @@ typedef struct {
|
|||||||
}
|
}
|
||||||
gl_pkg_t;
|
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);
|
semver_t gl_parse_semver(const char *str);
|
||||||
void gl_free_semver(semver_t *v);
|
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);
|
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 {
|
typedef enum {
|
||||||
GL_CONF_OK = 0,
|
GL_CONF_OK = 0,
|
||||||
GL_CONF_ERR_READ = 1,
|
GL_CONF_ERR_READ = 1,
|
||||||
GL_CONF_ERR_MISSING = 2,
|
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;
|
} 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_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);
|
bool gl_valid_pkg_ident(const char *s);
|
||||||
int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg);
|
int gl_gpm2gpkg(const char *filename, gl_pkg_t *pkg);
|
||||||
char *join_with_cwd(const char *path);
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
@@ -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
@@ -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
@@ -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
@@ -12,33 +12,76 @@
|
|||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
|
|
||||||
#include "common.h"
|
#include "common.h"
|
||||||
|
#include "deps.h"
|
||||||
#include "istoreutils.h"
|
#include "istoreutils.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "transaction.h"
|
#include "transaction.h"
|
||||||
|
|
||||||
/* The one directory a scope owns; everything below derives from it.
|
/* The one directory a scope owns; everything below derives from it.
|
||||||
* See the layout comment in common.h. */
|
* 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
|
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) {
|
switch (scope) {
|
||||||
case GL_SCOPE_USR:
|
case GL_SCOPE_USR:
|
||||||
gl_usr_base(uid, out, PATH_MAX);
|
gl_usr_base(uid, out, PATH_MAX);
|
||||||
break;
|
break;
|
||||||
case GL_SCOPE_SYS:
|
case GL_SCOPE_SYS:
|
||||||
snprintf(out, PATH_MAX, "%s", GL_SYS_ROOT);
|
snprintf(out, PATH_MAX, "%s%s", sysroot, GL_SYS_ROOT);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static void
|
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];
|
char base[PATH_MAX];
|
||||||
scope_base(scope, uid, base);
|
scope_base(scope, uid, sysroot, base);
|
||||||
snprintf(out, PATH_MAX, "%s/%s", base, name);
|
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
|
static int
|
||||||
fsync_dir(const char *path)
|
fsync_dir(const char *path)
|
||||||
{
|
{
|
||||||
@@ -67,11 +110,11 @@ atomic_swap(const char *live, const char *stage, const char *backup)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
|
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, const char *sysroot,
|
||||||
char *store_out, char *links_out)
|
char *index_out, char *store_out, char *links_out)
|
||||||
{
|
{
|
||||||
scope_subdir(scope, uid, "index", index_out);
|
scope_subdir(scope, uid, sysroot, "index", index_out);
|
||||||
scope_subdir(scope, uid, "store", store_out);
|
scope_subdir(scope, uid, sysroot, "store", store_out);
|
||||||
/* For system scope this is the DEFAULT links location — for
|
/* For system scope this is the DEFAULT links location — for
|
||||||
* packages the operator explicitly installs under system scope that
|
* packages the operator explicitly installs under system scope that
|
||||||
* aren't part of the base system. Base-repo packages get routed to
|
* 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
|
* 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
|
* something the context can decide up front. See GL_BASE_REPO in
|
||||||
* istoreutils.c. */
|
* istoreutils.c. */
|
||||||
scope_subdir(scope, uid, "links", links_out);
|
scope_subdir(scope, uid, sysroot, "links", links_out);
|
||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@@ -118,7 +161,9 @@ hardlink_tree(const char *src, const char *dest)
|
|||||||
if(lstat(s, &st) != 0) { continue; }
|
if(lstat(s, &st) != 0) { continue; }
|
||||||
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
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);
|
closedir(dir);
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
@@ -148,6 +193,13 @@ hardlink_tree(const char *src, const char *dest)
|
|||||||
|
|
||||||
int
|
int
|
||||||
gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
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) {
|
if (!ctx) {
|
||||||
return -1;
|
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->scope = scope;
|
||||||
ctx->lock_fd = -1;
|
ctx->lock_fd = -1;
|
||||||
ctx->overwrite = 0;
|
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);
|
snprintf(ctx->root_path, PATH_MAX, "%s/glacier", ctx->sysroot);
|
||||||
derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
|
derive_base_paths(scope, uid, ctx->sysroot, ctx->index_path,
|
||||||
ctx->links_path);
|
ctx->store_path, ctx->links_path);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
gl_txn_status_t
|
gl_txn_status_t
|
||||||
gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
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) {
|
if (!ctx) {
|
||||||
return GL_TXN_ERR_NO_TXN;
|
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->scope = scope;
|
||||||
ctx->lock_fd = -1;
|
ctx->lock_fd = -1;
|
||||||
ctx->overwrite = 0;
|
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) {
|
if (scope == GL_SCOPE_USR) {
|
||||||
switch (gl_usr_base_check(uid)) {
|
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];
|
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->index_path, PATH_MAX, "%s/index", stage_base);
|
||||||
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
|
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
|
||||||
@@ -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);
|
gl_abort_transaction(ctx);
|
||||||
return GL_TXN_ERR_MKDIR;
|
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);
|
gl_abort_transaction(ctx);
|
||||||
return GL_TXN_ERR_MKDIR;
|
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);
|
gl_abort_transaction(ctx);
|
||||||
return GL_TXN_ERR_MKDIR;
|
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];
|
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
|
/* live_links is populated (derive_base_paths always fills all
|
||||||
* three) but intentionally unused below — see the comment on the
|
* three) but intentionally unused below — see the comment on the
|
||||||
* hardlink_tree call. */
|
* 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;
|
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_txn_status_t
|
||||||
gl_begin_transaction(gl_context_t *ctx)
|
gl_begin_transaction(gl_context_t *ctx)
|
||||||
{
|
{
|
||||||
@@ -283,7 +437,7 @@ gl_begin_transaction(gl_context_t *ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
char lock_path[PATH_MAX];
|
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);
|
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
|
||||||
if (fd < 0) {
|
if (fd < 0) {
|
||||||
@@ -309,8 +463,19 @@ gl_commit_transaction(gl_context_t *ctx)
|
|||||||
return GL_TXN_ERR_NO_TXN;
|
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];
|
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);
|
int same = check_same_filesystem(ctx->root_path, stage_base);
|
||||||
if (same < 0) {
|
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 */
|
/* Derive live paths the same way gl_init_live_context would */
|
||||||
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
|
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
|
||||||
derive_base_paths(ctx->scope, ctx->uid, live_index, live_store,
|
derive_base_paths(ctx->scope, ctx->uid, ctx->sysroot, live_index,
|
||||||
live_links);
|
live_store, live_links);
|
||||||
|
|
||||||
/* Derive old/backup paths */
|
/* Derive old/backup paths */
|
||||||
char old_base[PATH_MAX];
|
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];
|
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
|
||||||
snprintf(old_index, PATH_MAX, "%s/index", old_base);
|
snprintf(old_index, PATH_MAX, "%s/index", old_base);
|
||||||
@@ -363,18 +528,22 @@ gl_commit_transaction(gl_context_t *ctx)
|
|||||||
|
|
||||||
/* fsync the directory the renames happened in */
|
/* fsync the directory the renames happened in */
|
||||||
char base[PATH_MAX];
|
char base[PATH_MAX];
|
||||||
scope_base(ctx->scope, ctx->uid, base);
|
scope_base(ctx->scope, ctx->uid, ctx->sysroot, base);
|
||||||
fsync_dir(base);
|
fsync_dir(base);
|
||||||
if (ctx->scope == GL_SCOPE_SYS) {
|
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_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) {
|
if (gl_relink_store(&live_ctx) != 0) {
|
||||||
lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid);
|
lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid);
|
||||||
}
|
}
|
||||||
|
ctx->link_refusals = live_ctx.link_refusals;
|
||||||
|
|
||||||
gl_rmdir_recursive(old_base);
|
gl_rmdir_recursive(old_base);
|
||||||
|
|
||||||
@@ -390,13 +559,15 @@ gl_commit_transaction(gl_context_t *ctx)
|
|||||||
gl_txn_status_t
|
gl_txn_status_t
|
||||||
gl_abort_transaction(gl_context_t *ctx)
|
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;
|
return GL_TXN_ERR_NO_TXN;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Remove stage area — live is untouched */
|
/* Remove stage area — live is untouched */
|
||||||
char stage_uid[PATH_MAX];
|
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 */
|
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
|
||||||
|
|
||||||
|
|||||||
+76
-1
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
#include <limits.h>
|
#include <limits.h>
|
||||||
#include <stdbool.h>
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
||||||
#define GL_UID_NONE ((uid_t)0)
|
#define GL_UID_NONE ((uid_t)0)
|
||||||
@@ -19,6 +21,22 @@ typedef enum {
|
|||||||
GL_SCOPE_SYS
|
GL_SCOPE_SYS
|
||||||
} gl_ctx_scope_t;
|
} 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 {
|
typedef struct {
|
||||||
uid_t uid;
|
uid_t uid;
|
||||||
|
|
||||||
@@ -37,8 +55,53 @@ typedef struct {
|
|||||||
* existing non-glacier files at their link destinations; see
|
* existing non-glacier files at their link destinations; see
|
||||||
* gl_link_pkg. */
|
* gl_link_pkg. */
|
||||||
int overwrite;
|
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;
|
} 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 {
|
typedef enum {
|
||||||
GL_TXN_OK = 0, /* success */
|
GL_TXN_OK = 0, /* success */
|
||||||
GL_TXN_ERR_LOCK = 1, /* unable to acquire lock */
|
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_SYNC = 5, /* fsync failed */
|
||||||
GL_TXN_ERR_CLEANUP = 6, /* cleanup failed */
|
GL_TXN_ERR_CLEANUP = 6, /* cleanup failed */
|
||||||
GL_TXN_ERR_NO_TXN = 7, /* aborted without beginning */
|
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;
|
} 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(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(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);
|
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_commit_transaction(gl_context_t *ctx);
|
||||||
gl_txn_status_t gl_abort_transaction(gl_context_t *ctx);
|
gl_txn_status_t gl_abort_transaction(gl_context_t *ctx);
|
||||||
|
|
||||||
|
|||||||
BIN
Binary file not shown.
Reference in new issue
Block a user