7/19
This commit is contained in:
261
CLAUDE.md
Normal file
261
CLAUDE.md
Normal file
@@ -0,0 +1,261 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
Context for Claude Code sessions on this repo (libglacier-ng, the package
|
||||||
|
manager for the Everest/Glacier Linux distribution). This file exists
|
||||||
|
because there's no built-in way to hand off a claude.ai planning
|
||||||
|
conversation into Claude Code — this is that handoff, written down.
|
||||||
|
|
||||||
|
For the symlink-routing / package-scope design specifically, see
|
||||||
|
`PACKAGE_SCOPES.txt` at the repo root — that doc is the source of truth
|
||||||
|
for that topic and isn't duplicated here.
|
||||||
|
|
||||||
|
## What this project is
|
||||||
|
|
||||||
|
A from-scratch Linux distribution (Everest, built on the Glacier
|
||||||
|
package-management model) with its own package manager
|
||||||
|
(`libglacier-ng` + `gpkg`/`gstore` CLI tools) and its own build
|
||||||
|
tooling (`gbuild`/`gworld`) for bootstrapping a set of packages up to
|
||||||
|
a minimal bootable system.
|
||||||
|
|
||||||
|
## Core architecture
|
||||||
|
|
||||||
|
**`gl_context_t`** (transaction.h) is the central abstraction: `uid`,
|
||||||
|
`root_path`, `index_path`, `store_path`, `links_path`, `mode`
|
||||||
|
(`GL_CTX_LIVE` / `GL_CTX_STAGE`), `scope` (`GL_SCOPE_USR` /
|
||||||
|
`GL_SCOPE_SYS`), `lock_fd`.
|
||||||
|
|
||||||
|
**Transaction lifecycle**: `gl_init_live_context` / `gl_init_stage_context`
|
||||||
|
→ `gl_commit_transaction` or `gl_abort_transaction`. Staged installs
|
||||||
|
hardlink-seed `index`/`store` from live at stage-init time (copy-on-write,
|
||||||
|
so removing a live package inside a transaction doesn't require special
|
||||||
|
casing). The `links` tree is deliberately **not** seeded — see below.
|
||||||
|
|
||||||
|
**Commit** does atomic rename-based promotion (stage → live, with the
|
||||||
|
previous live moved to `old/` as a rollback point), fsyncs the parent
|
||||||
|
dirs, then calls `gl_relink_store` to regenerate symlinks and
|
||||||
|
`gl_rebuild_index` to regenerate the index — both from a fresh
|
||||||
|
`GL_CTX_LIVE` context, not the just-committed stage context.
|
||||||
|
|
||||||
|
**Scopes**:
|
||||||
|
- `GL_SCOPE_USR`: per-uid, `/glacier/usr/{index,store,links}/<uid>`.
|
||||||
|
- `GL_SCOPE_SYS`: system-wide, `/glacier/sys/{index,store}`. Symlink
|
||||||
|
destination depends on the package's **repo** — see `PACKAGE_SCOPES.txt`.
|
||||||
|
Short version: `repo == "base"` → `/usr`; anything else →
|
||||||
|
`/glacier/sys/links`. This is decided per-package (in `gl_link_pkg` /
|
||||||
|
`gl_relink_store`), not baked into the context.
|
||||||
|
|
||||||
|
**Repos**: `base` (minimal working system, boot-critical by definition —
|
||||||
|
this is a repo-level classification, not a per-package flag), `extra`
|
||||||
|
(important but not required), `community` (everything else). `extra`
|
||||||
|
and `community` currently behave identically — see open items.
|
||||||
|
|
||||||
|
## Safety mechanisms (read before touching links/symlink code)
|
||||||
|
|
||||||
|
`ctx->links_path` is **not always glacier-exclusive territory** —
|
||||||
|
for system-scope `base` packages it's literally `/usr`, which holds
|
||||||
|
plenty of content glacier has no business touching. Everything that
|
||||||
|
reads or writes a links destination has to treat it as "possibly
|
||||||
|
shared":
|
||||||
|
|
||||||
|
- `is_glacier_symlink(path, store_prefix)` (istoreutils.c) is the one
|
||||||
|
source of truth for "did glacier create this." True only if `path`
|
||||||
|
is a symlink whose target lives under `store_prefix`. Never touch a
|
||||||
|
path this returns false for.
|
||||||
|
- `gl_link_pkg` will not overwrite an existing path unless
|
||||||
|
`is_glacier_symlink` says it's safe to replace.
|
||||||
|
- `gl_relink_store` prunes stale symlinks (`prune_stale_links`) rather
|
||||||
|
than wiping and rebuilding the whole links directory — a full
|
||||||
|
`rm -rf` of `ctx->links_path` would be catastrophic once that path
|
||||||
|
can be `/usr`. It prunes **both** possible system-scope destinations
|
||||||
|
(`/glacier/sys/links` and `/usr`) since a single pass can't know in
|
||||||
|
advance whether any base-repo packages are involved.
|
||||||
|
- The stage links tree is never hardlink-seeded — it's genuinely
|
||||||
|
unused (`gl_link_pkg` only ever runs against `GL_CTX_LIVE`;
|
||||||
|
`gl_relink_store` always builds its own fresh live context). Seeding
|
||||||
|
it would mean hardlinking all of `/usr` on every staged system
|
||||||
|
transaction, and risking `EXDEV` if `/usr` and the stage area are on
|
||||||
|
different filesystems.
|
||||||
|
|
||||||
|
## Bug history (so it doesn't get relitigated or reintroduced)
|
||||||
|
|
||||||
|
Roughly chronological, all already fixed in the current tree:
|
||||||
|
|
||||||
|
1. **`gl_abort_transaction` never actually released the lock file** — a
|
||||||
|
copy-paste bug wrote into the wrong variable. Fixed by moving off
|
||||||
|
`O_EXCL`-based locking entirely: `gl_begin_transaction` now just
|
||||||
|
`open(O_CREAT)` + `flock()`, and lock files are never `unlink()`'d
|
||||||
|
(the kernel releases `flock()` automatically on process death, even
|
||||||
|
`SIGKILL`; explicit unlinking has its own TOCTOU race with a second
|
||||||
|
process creating a new inode at the same path).
|
||||||
|
2. **`EXDEV` wasn't checked before renaming** — `gl_commit_transaction`
|
||||||
|
now `stat()`s both sides and fails cleanly with `GL_TXN_ERR_XDEV`
|
||||||
|
before attempting any renames if stage and live aren't on the same
|
||||||
|
filesystem.
|
||||||
|
3. **Segfault on install**, root cause: the Makefile's `transaction`
|
||||||
|
target didn't link `libglacier_transaction.so` against
|
||||||
|
`libglacier_istoreutils.so`/`libglacier_log.so`, even though
|
||||||
|
`transaction.c` calls `gl_relink_store()` and `lg_printf()`. Fixed
|
||||||
|
by adding those `-l` flags and an explicit `transaction: log
|
||||||
|
istoreutils` prerequisite (build order matters now).
|
||||||
|
4. **Symlinks going stale after commit** — `gl_link_pkg` used to run
|
||||||
|
during staged installs too, embedding the *stage* path as the
|
||||||
|
symlink target. Fixed: `gl_link_pkg` only runs for `GL_CTX_LIVE`;
|
||||||
|
links are fully regenerated post-commit by `gl_relink_store`.
|
||||||
|
5. **Dangling symlinks never cleaned up on removal** — see "Safety
|
||||||
|
mechanisms" above; this is what `prune_stale_links` fixes.
|
||||||
|
6. **`GL_TXN_ERR_RENAME` on every commit after the first** — leftover
|
||||||
|
`old/` directory from a previous commit made the next commit's
|
||||||
|
`rename()` fail with `ENOTEMPTY`. Fixed: `gl_commit_transaction`
|
||||||
|
wipes `old_base` if present before using it.
|
||||||
|
7. **`-Wstringop-truncation` errors** (real bug, not noise) — four
|
||||||
|
`strncpy(dst, ctx->foo_path, PATH_MAX - 1)` calls that could leave
|
||||||
|
`dst` unterminated. Fixed by switching to `snprintf(dst,
|
||||||
|
sizeof(dst), "%s", ...)`, which always terminates.
|
||||||
|
(`-Wformat-truncation`, separately, is suppressed via
|
||||||
|
`-Wno-error=format-truncation` in `config.mk` — those warnings are
|
||||||
|
about genuinely-safe `snprintf` truncation, not a real bug.)
|
||||||
|
8. **`gbuild` build-system autodetection** checked for a bare
|
||||||
|
`Makefile` before `configure`/`configure.ac`. Broke musl (and any
|
||||||
|
project with a hand-rolled, non-autotools build system that ships
|
||||||
|
both) since its `Makefile` is non-functional without `configure`
|
||||||
|
generating `config.mak` first. Fixed by reordering the checks.
|
||||||
|
9. **Index only stored `repo::pkgname`, no version.** Now
|
||||||
|
`repo::pkgname::version` (`gl_rebuild_index` looks up the
|
||||||
|
`<pkgname>-<version>` store subdirectory). `struct gpkg_entry` in
|
||||||
|
`istoreutils.h` needs a `char *ver;` field — this was a manual
|
||||||
|
header edit, double check it's actually present.
|
||||||
|
10. **`gworld` recipe parser silently dropped `SYS_PROFILE`** — the
|
||||||
|
key-matching regex only matched uppercase letters, not
|
||||||
|
underscores. Fixed to `^([%u_][%u%d_]*)=(.*)$`. Caught by an
|
||||||
|
actual precedence test, not code review — worth remembering that
|
||||||
|
class of bug (a field silently never parsing, falling back to a
|
||||||
|
default that happens to look right) is easy to miss by inspection
|
||||||
|
alone.
|
||||||
|
|
||||||
|
## CLI additions
|
||||||
|
|
||||||
|
- `gpkg -s` / `--system`: operate against `GL_SCOPE_SYS` instead of a
|
||||||
|
uid's tree. Requires root (checked via `geteuid()`). Must appear
|
||||||
|
*before* `-l`/`-x` in a grouped flag string (e.g. `-sl`, not `-ls`) —
|
||||||
|
flags are handled in parse order, same constraint `-V`/`-S` already
|
||||||
|
had.
|
||||||
|
- `gstore -N` / `--new-system`: calls `gl_init_sys()`. Requires root.
|
||||||
|
- `gbuild -P` / `--profile PROFILE`: system profile, `ARCH-LIBC[-FEATURE...]`
|
||||||
|
(e.g. `x86_64-musl`, `x86_64-glibc-multilib`). Derives a cross-compile
|
||||||
|
prefix (translating `glibc`→`gnu` per real GNU triplet convention),
|
||||||
|
detects native-vs-cross against the build host, and only pre-fills
|
||||||
|
`-t`/`-x` defaults for whichever the user didn't already set
|
||||||
|
explicitly. Features export as `GL_FEATURE_<NAME>=1` env vars for
|
||||||
|
custom `-b`/`-i` commands to branch on — gbuild itself doesn't
|
||||||
|
hardcode per-feature behavior, that's intentional.
|
||||||
|
|
||||||
|
## Build system
|
||||||
|
|
||||||
|
`Makefile` builds separate `.so`s per module
|
||||||
|
(`libglacier_log.so`, `libglacier_pkg.so`, `libglacier_istoreutils.so`,
|
||||||
|
`libglacier_transaction.so`, `libglacier_dag.so`) — **this split is
|
||||||
|
deliberate Unix-philosophy design**, not an accident: a third-party
|
||||||
|
glacier-compatible program should be able to link only
|
||||||
|
`libglacier_log.so` without pulling in everything else. `gpkg`/`gstore`
|
||||||
|
load the specific per-module `.so`s they need directly, matching this.
|
||||||
|
|
||||||
|
There's also a `libglacier-ng` Makefile target producing a **combined**
|
||||||
|
`.so` — this exists *solely* for the Lua FFI test harness's
|
||||||
|
convenience (one thing to `ffi.load()` instead of juggling rpath
|
||||||
|
chains in a throwaway test process). It is **not** in `all` and **not**
|
||||||
|
installed. Don't reach for it outside `tests/` — that's a sign
|
||||||
|
something should be split-loaded instead.
|
||||||
|
|
||||||
|
`config.mk` holds `CFLAGS`, including `-Wno-error=format-truncation`
|
||||||
|
(see bug #7 above for why that specific warning is suppressed and
|
||||||
|
others aren't).
|
||||||
|
|
||||||
|
## Lua tooling
|
||||||
|
|
||||||
|
- `gpkg`, `gstore`: LuaJIT + FFI, calling directly into the `.so`s.
|
||||||
|
- `lib/glacier_cdef.lua`: single shared source of truth for every
|
||||||
|
`ffi.cdef()` declaration, used by `gpkg`, `gstore`, and the test
|
||||||
|
suite. This used to be duplicated three ways, which is exactly what
|
||||||
|
let a stale signature drift silently into `gpkg` at one point — if
|
||||||
|
you're editing a C function signature, this file needs the matching
|
||||||
|
update, and it's the *only* place that needs it now.
|
||||||
|
- `tests/*.lua`: LuaJIT FFI test suite exercising real filesystem/process
|
||||||
|
behavior (`fork`+`SIGKILL` for lock crash-recovery, real `mount()`
|
||||||
|
for the `EXDEV` check, etc.) — not mocked. Run via:
|
||||||
|
```sh
|
||||||
|
cd tests
|
||||||
|
make -C .. libglacier-ng # builds the test-only combined .so
|
||||||
|
GLACIER_TEST_CONFIRM=yes LIBGLACIER_SO=$(pwd)/../build/lib/shared/libglacier-ng.so luajit run_all.lua
|
||||||
|
```
|
||||||
|
`test_exdev.lua` needs `--cap-add=SYS_ADMIN` in Docker to actually
|
||||||
|
exercise the mount path; it skips cleanly (not a failure) without it.
|
||||||
|
|
||||||
|
## Build automation (bootstrap toolchain)
|
||||||
|
|
||||||
|
- `gbuild` (bash): single-package builder. Autodetects build system
|
||||||
|
(cmake/meson/cargo/go/autotools/make), supports cross-compilation
|
||||||
|
(`-t`/`-x`, now also `-P` for profiles).
|
||||||
|
- `gworld` (Lua): batch orchestrator. Reads `recipes/*.recipe`,
|
||||||
|
topologically sorts by `DEPS`, invokes `gbuild` once per package in
|
||||||
|
order. `-s SYSROOT` gives every package in the batch a shared
|
||||||
|
staging dir — relies on `gbuild -p` never wiping its target, so
|
||||||
|
headers/libs accumulate across the batch for free.
|
||||||
|
- Recipe format: `NAME VER REPO URL/REF-or-SRC DEPS BUILD INSTALL
|
||||||
|
FLAGS SYS_PROFILE` (plain `key=value`, see `recipes/musl.recipe` for
|
||||||
|
a real example).
|
||||||
|
- Real multi-pass toolchain bootstraps (LFS-style — gcc built twice,
|
||||||
|
etc.) should use separate recipes per pass (`gcc-pass1`,
|
||||||
|
`gcc-pass2`), publishing only the final pass under the real
|
||||||
|
name/repo. The DAG models build steps, not abstract packages.
|
||||||
|
- **`grootstrap` needs a rebuild, not yet done.** The old design (raw
|
||||||
|
`tar` extraction into a target dir + chroot + register everything
|
||||||
|
under an arbitrary bootstrap uid) predates the `GL_SCOPE_SYS` +
|
||||||
|
base-repo design and is now more complicated than necessary. Once
|
||||||
|
`gpkg`/`gstore` themselves exist on the target (still an unsolved
|
||||||
|
bootstrapping-the-bootstrapper problem — how do they get there
|
||||||
|
before a working package manager exists to install them with),
|
||||||
|
every package including `base` ones can go through the same `gpkg
|
||||||
|
-s -l` path; the repo alone routes symlinks correctly. No more
|
||||||
|
special-casing base packages during bootstrap.
|
||||||
|
|
||||||
|
## Known open items / stubs
|
||||||
|
|
||||||
|
- `gpkg -f` / `-u` (merge/update) are unimplemented stubs. `-f`
|
||||||
|
references an undefined `uid` variable (should be `uidn`) — will
|
||||||
|
nil-concatenation-error the moment it's actually implemented.
|
||||||
|
- `gpkg -x`'s confirmation summary shows placeholder `unknown::pkg
|
||||||
|
0.0.0 (unknown)` since it has no local manifest to read version/repo
|
||||||
|
from for a bare package name. Would need `resolve_repo` called
|
||||||
|
before building the summary, not just before the removal itself.
|
||||||
|
- `gpkg -x` doesn't print a completion message on success (`-l` does:
|
||||||
|
"Completed with no errors."). Cosmetic inconsistency.
|
||||||
|
- Reinstalling an up-to-date package double-logs ("Installed X" +
|
||||||
|
"staged from X" for the same event).
|
||||||
|
- `extra` vs `community` repos don't currently diverge in behavior —
|
||||||
|
both just mean "not base" for symlink-routing purposes.
|
||||||
|
- `gl_relink_store`/`gl_rebuild_index` are full rescans of the store on
|
||||||
|
every commit — fine at current scale, worth revisiting if large
|
||||||
|
transactions become common (a dirty-set of touched packages instead
|
||||||
|
of full rescans).
|
||||||
|
- Package manifests don't record which `SYS_PROFILE` they were built
|
||||||
|
under — deliberately not added yet (would need a `pkg.c` /
|
||||||
|
`gl_gpm2gpkg` change), flagged as a future option, not a gap that
|
||||||
|
needs fixing now.
|
||||||
|
- A stray nested `lib/glacier/lib/glacier/` directory was spotted once
|
||||||
|
in a `tree` listing — looked like a leftover `make install`
|
||||||
|
artifact, not in any load path, harmless but worth a `rm -rf`
|
||||||
|
eventually.
|
||||||
|
|
||||||
|
## Conventions worth knowing before editing
|
||||||
|
|
||||||
|
- C source generally uses tabs for indentation, though the codebase is
|
||||||
|
inconsistent about it in places (some blocks use spaces). ALWAYS USE TABS WHEN POSSIBLE.
|
||||||
|
- `PATH_MAX`-sized stack buffers + `snprintf` is the standard pattern
|
||||||
|
for path construction throughout; prefer it over `strncpy` for
|
||||||
|
anything copying into a fixed buffer (see bug #7).
|
||||||
|
- Don't add convenience wrappers that silently touch a broader
|
||||||
|
filesystem scope than what's asked — this codebase has already been
|
||||||
|
bitten twice by exactly that class of mistake (`gl_relink_store`'s
|
||||||
|
wipe, `gl_link_pkg`'s blind `unlink`), both only becoming dangerous
|
||||||
|
once a path assumption (`links_path` is glacier-exclusive) quietly
|
||||||
|
stopped holding.
|
||||||
31
Makefile
31
Makefile
@@ -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
|
.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
|
||||||
|
|
||||||
all: prepare log log_test rt pkg istoreutils make_conf dag
|
all: prepare log log_test rt pkg transaction istoreutils make_conf dag sha256 libglacier_verify gsha256sum
|
||||||
@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
|
||||||
@@ -29,6 +29,7 @@ prepare:
|
|||||||
mkdir -p build/lib/tmp
|
mkdir -p build/lib/tmp
|
||||||
mkdir -p build/lib/shared
|
mkdir -p build/lib/shared
|
||||||
mkdir -p build/include/glacier
|
mkdir -p build/include/glacier
|
||||||
|
mkdir -p build/bin
|
||||||
|
|
||||||
log:
|
log:
|
||||||
$(CC) $(CFLAGS) src/log.c -c -o build/lib/tmp/log.o
|
$(CC) $(CFLAGS) src/log.c -c -o build/lib/tmp/log.o
|
||||||
@@ -40,6 +41,22 @@ log_test:
|
|||||||
rt:
|
rt:
|
||||||
$(CC) $(CFLAGS) src/runtime.c -c -o build/lib/tmp/runtime.o
|
$(CC) $(CFLAGS) src/runtime.c -c -o build/lib/tmp/runtime.o
|
||||||
|
|
||||||
|
sha256:
|
||||||
|
$(CC) $(CFLAGS) src/sha256.c -c -o build/lib/tmp/sha256.o
|
||||||
|
|
||||||
|
libglacier_verify:
|
||||||
|
$(CC) $(CFLAGS) -shared -fPIC src/sha256.c \
|
||||||
|
-o build/lib/shared/libglacier_verify.so
|
||||||
|
|
||||||
|
gsha256sum: prepare
|
||||||
|
$(CC) $(CFLAGS) src/gsha256sum.c src/sha256.c -o build/bin/gsha256sum
|
||||||
|
|
||||||
|
transaction: log istoreutils
|
||||||
|
$(CC) $(CFLAGS) -shared -fPIC src/transaction.c \
|
||||||
|
-Lbuild/lib/shared -lglacier_istoreutils -lglacier_log \
|
||||||
|
-Wl,-rpath,'$$ORIGIN' \
|
||||||
|
-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 \
|
||||||
-Lbuild/lib/shared -lglacier_log -lglacier_pkg -larchive \
|
-Lbuild/lib/shared -lglacier_log -lglacier_pkg -larchive \
|
||||||
@@ -55,6 +72,12 @@ pkg:
|
|||||||
make_conf:
|
make_conf:
|
||||||
$(CC) $(CFLAGS) src/make_conf.c -c -o build/lib/tmp/make_conf.o
|
$(CC) $(CFLAGS) src/make_conf.c -c -o build/lib/tmp/make_conf.o
|
||||||
|
|
||||||
|
libglacier-ng: prepare
|
||||||
|
$(CC) $(CFLAGS) -shared -fPIC \
|
||||||
|
src/log.c src/pkg.c src/transaction.c src/istoreutils.c \
|
||||||
|
-larchive -lconfig \
|
||||||
|
-o build/lib/shared/libglacier-ng.so
|
||||||
|
|
||||||
dag:
|
dag:
|
||||||
$(CC) $(CFLAGS) -shared -fPIC src/dag.c \
|
$(CC) $(CFLAGS) -shared -fPIC src/dag.c \
|
||||||
-Lbuild/lib/shared -lglacier_log \
|
-Lbuild/lib/shared -lglacier_log \
|
||||||
@@ -65,8 +88,10 @@ 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
|
||||||
install build/lib/shared/libglacier_istoreutils.so $(PREFIX)/lib/glacier/ -m 755
|
install build/lib/shared/libglacier_istoreutils.so $(PREFIX)/lib/glacier/ -m 755
|
||||||
|
install build/lib/shared/libglacier_transaction.so $(PREFIX)/lib/glacier/ -m 755
|
||||||
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_dag.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_static:
|
install_static:
|
||||||
mkdir -p $(PREFIX)/lib/glacier
|
mkdir -p $(PREFIX)/lib/glacier
|
||||||
|
|||||||
177
PACKAGE_SCOPES.txt
Normal file
177
PACKAGE_SCOPES.txt
Normal file
@@ -0,0 +1,177 @@
|
|||||||
|
+------------------------------------+
|
||||||
|
| Package scopes and symlink routing |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
This document records the design decisions behind how glacier packages
|
||||||
|
are scoped, how their repo determines where their symlinks land, and
|
||||||
|
what "boot-critical" means in this system. It exists because these
|
||||||
|
rules aren't obvious from reading the code alone, and getting them
|
||||||
|
wrong (particularly the symlink-placement rule) has real consequences
|
||||||
|
up to and including destroying a live /usr.
|
||||||
|
|
||||||
|
|
||||||
|
+------------------------------------+
|
||||||
|
| 1. The two scopes |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
USR (GL_SCOPE_USR)
|
||||||
|
Per-uid packages. Index/store/links all live under a specific
|
||||||
|
uid's tree:
|
||||||
|
/glacier/usr/index/<uid>
|
||||||
|
/glacier/usr/store/<uid>
|
||||||
|
/glacier/usr/links/<uid>
|
||||||
|
Symlinks always land under /glacier/usr/links/<uid>/{bin,...}.
|
||||||
|
There is no base-repo exception for USR scope. Nothing installed
|
||||||
|
under a per-user scope is ever considered part of the base system,
|
||||||
|
so nothing here ever touches /usr.
|
||||||
|
|
||||||
|
SYS (GL_SCOPE_SYS)
|
||||||
|
System-wide packages, not owned by any particular uid:
|
||||||
|
/glacier/sys/index
|
||||||
|
/glacier/sys/store
|
||||||
|
Symlink destination depends on the package's repo (see section 3).
|
||||||
|
Requires root. gpkg enforces this via geteuid() before it will
|
||||||
|
even attempt a system-scope operation.
|
||||||
|
|
||||||
|
|
||||||
|
+------------------------------------+
|
||||||
|
| 2. Repo semantics |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
A package's repo (PKG_REPO in its manifest) is not just a label for
|
||||||
|
organizing the store — under system scope, it is the one thing that
|
||||||
|
decides where that package's activation symlinks physically land.
|
||||||
|
|
||||||
|
base
|
||||||
|
Packages that make up a minimal working system. ALL packages
|
||||||
|
in this repo are considered boot-critical BY DEFINITION — this
|
||||||
|
is a repo-level classification, not a per-package flag. A
|
||||||
|
package doesn't have to be individually essential for booting
|
||||||
|
to count; if it's filed under base, it's treated as part of
|
||||||
|
the base system, full stop.
|
||||||
|
|
||||||
|
extra
|
||||||
|
Software that may be important but is not necessary to run
|
||||||
|
Everest, nor to install it.
|
||||||
|
|
||||||
|
community
|
||||||
|
Everything else.
|
||||||
|
|
||||||
|
NOTE: extra and community are currently NOT distinguished by the
|
||||||
|
library itself — both simply mean "not base," and both route to
|
||||||
|
the same default system-scope links location. If they need to
|
||||||
|
diverge further later (different confirmation prompts, different
|
||||||
|
trust handling, whatever), that is a deliberate future change, not
|
||||||
|
something already implemented.
|
||||||
|
|
||||||
|
|
||||||
|
+------------------------------------+
|
||||||
|
| 3. Symlink placement rule |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
scope == USR -> /glacier/usr/links/<uid>
|
||||||
|
scope == SYS && repo == "base" -> /usr (*)
|
||||||
|
scope == SYS && repo != "base" -> /glacier/sys/links
|
||||||
|
|
||||||
|
(*) This is the base-system exception. Packages that are part of the
|
||||||
|
minimal working system get their symlinks placed directly into
|
||||||
|
/usr/bin, /usr/lib, etc. — real FHS locations — so that anything
|
||||||
|
with a hardcoded path (init scripts, systemd units, shebang lines)
|
||||||
|
works without needing glacier-aware PATH setup.
|
||||||
|
|
||||||
|
Everything an operator installs under system scope that ISN'T
|
||||||
|
part of the base system stays isolated under /glacier/sys/links,
|
||||||
|
same as it always has. The base system is the exception to the
|
||||||
|
norm, not the other way around.
|
||||||
|
|
||||||
|
This decision is made per-package, at the point where a package's repo
|
||||||
|
is actually known (inside gl_link_pkg / gl_unlink_pkg / the walk loop
|
||||||
|
in gl_relink_store) — NOT baked into the context at creation time.
|
||||||
|
gl_context_t's links_path field always holds the DEFAULT destination
|
||||||
|
for that scope; the /usr override is computed separately per package.
|
||||||
|
|
||||||
|
|
||||||
|
+------------------------------------+
|
||||||
|
| 4. Why /usr as a target is safe |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
/usr is not glacier-exclusive territory the way /glacier/sys/links is.
|
||||||
|
It holds plenty of content glacier has no business touching. Every
|
||||||
|
place that reads or writes into a links destination therefore treats
|
||||||
|
that destination as "possibly shared, not owned":
|
||||||
|
|
||||||
|
- is_glacier_symlink(path, store_prefix) is the single source of
|
||||||
|
truth for "did glacier itself create this." It reports true only
|
||||||
|
if `path` is a symlink whose target lives under the relevant
|
||||||
|
store path. Anything else — a real file, a directory, a foreign
|
||||||
|
symlink pointing somewhere else entirely — is never touched.
|
||||||
|
|
||||||
|
- gl_link_pkg refuses to overwrite an existing path unless
|
||||||
|
is_glacier_symlink says it's safe to replace. It will not clobber
|
||||||
|
a foreign file that happens to occupy the same path.
|
||||||
|
|
||||||
|
- gl_relink_store no longer wipes and rebuilds its links directory
|
||||||
|
wholesale (that was safe only when the target was glacier-only
|
||||||
|
territory). It prunes ONLY stale symlinks glacier itself created
|
||||||
|
(identified via is_glacier_symlink, removed only if their store
|
||||||
|
target no longer exists) and leaves everything else alone.
|
||||||
|
|
||||||
|
- A single system-scope relink pass prunes BOTH possible
|
||||||
|
destinations (/glacier/sys/links and /usr), since it can't know in
|
||||||
|
advance whether any base-repo packages are involved without
|
||||||
|
checking both.
|
||||||
|
|
||||||
|
- The stage tree's links directory is no longer hardlink-seeded at
|
||||||
|
all. It was always unused (gl_link_pkg only ever runs against a
|
||||||
|
LIVE context; gl_relink_store always builds its own fresh live
|
||||||
|
context rather than touching a staged one) — and now that live
|
||||||
|
links_path can be /usr, seeding it would mean hardlinking the
|
||||||
|
entire /usr tree on every staged system transaction, and risking
|
||||||
|
EXDEV outright if /usr and the stage area are on different
|
||||||
|
filesystems.
|
||||||
|
|
||||||
|
|
||||||
|
+------------------------------------+
|
||||||
|
| 5. Updating base-system packages |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
Base-repo packages are tracked in the system index like anything else,
|
||||||
|
and updating them works exactly the same way updating any other
|
||||||
|
package does — same staged transaction, same commit/rollback. No
|
||||||
|
separate mechanism was built for this, because none was needed:
|
||||||
|
|
||||||
|
gpkg -s newmusl.gpkg
|
||||||
|
|
||||||
|
...updates musl in place under system scope, symlinks and all,
|
||||||
|
whether it was originally installed by a bootstrap tool or by a normal
|
||||||
|
`gpkg -s` call later. There is no meaningful distinction between
|
||||||
|
"how it originally got there" and "how you update it."
|
||||||
|
|
||||||
|
Replacing something as fundamental as libc on a LIVE running system is
|
||||||
|
safe under this model specifically because of POSIX unlink semantics:
|
||||||
|
a process that already has a shared object open keeps working off the
|
||||||
|
old inode even after the symlink swap happens. Only newly-spawned
|
||||||
|
processes after the swap see the new version. This falls directly out
|
||||||
|
of the atomic-rename commit design already in place — nothing extra
|
||||||
|
was added to make it work.
|
||||||
|
|
||||||
|
|
||||||
|
+------------------------------------+
|
||||||
|
| 6. Open items |
|
||||||
|
+------------------------------------+
|
||||||
|
|
||||||
|
- grootstrap has not been rebuilt against this design yet. The
|
||||||
|
simplification this design enables: once gpkg/gstore themselves
|
||||||
|
exist on the target (still a bootstrapping problem that needs
|
||||||
|
solving separately), EVERY package — base included — can go
|
||||||
|
through the same `gpkg -s -l` path. There is no longer a need for
|
||||||
|
a separate raw-extraction special-case for base packages
|
||||||
|
specifically; the repo alone routes them to /usr correctly.
|
||||||
|
|
||||||
|
- extra vs community currently behave identically (see section 2).
|
||||||
|
Whether they should diverge, and how, is undecided.
|
||||||
|
|
||||||
|
- The bootstrapping-a-bootstrapper problem (getting gpkg/gstore onto
|
||||||
|
a target filesystem before there's a working package manager to
|
||||||
|
install them with) is still unsolved and orthogonal to everything
|
||||||
|
in this document.
|
||||||
379
SECURITY_DESIGN.txt
Normal file
379
SECURITY_DESIGN.txt
Normal file
@@ -0,0 +1,379 @@
|
|||||||
|
+--------------------------------------------+
|
||||||
|
| Package integrity & scope security |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
This document records design decisions made in one planning session,
|
||||||
|
covering package integrity verification and the hard separation
|
||||||
|
between per-user and system-scope package tools. NONE OF THIS IS
|
||||||
|
IMPLEMENTED YET. It exists so implementation can start from a settled
|
||||||
|
plan instead of re-deriving it.
|
||||||
|
|
||||||
|
Companion document: PACKAGE_SCOPES.txt (repo/scope symlink routing —
|
||||||
|
referenced but not repeated here).
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 1. Scope of this phase |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
Two different guarantees are in play, and they are NOT the same thing:
|
||||||
|
|
||||||
|
integrity - has this file been corrupted or altered
|
||||||
|
authenticity - did this actually come from who it claims to be
|
||||||
|
|
||||||
|
A hash alone only ever proves integrity. If the hash travels with the
|
||||||
|
file from the same untrusted source, tampering both together defeats
|
||||||
|
it. This phase is INTEGRITY ONLY. Authenticity (signing, a trusted
|
||||||
|
keyring) is a deliberate future phase, tied to remote-repo support —
|
||||||
|
not being designed in detail now, but a few choices below are made to
|
||||||
|
stay cheap to extend into later.
|
||||||
|
|
||||||
|
Everest is a security-focused distro (this is also *why* per-user
|
||||||
|
package isolation exists in the first place). Two sides of the CIA
|
||||||
|
triad — confidentiality is out of scope here, but integrity and
|
||||||
|
(eventually) authenticity are both treated as first-class, not
|
||||||
|
best-effort.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 2. Hash algorithm: SHA-256 |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
Decision: SHA-256, not SHA-512.
|
||||||
|
|
||||||
|
Rationale (SHA-512 was seriously considered, not dismissed by default):
|
||||||
|
- SHA-512's usual advantage (faster on 64-bit CPUs, due to 64-bit
|
||||||
|
word operations) does not hold across this project's actual
|
||||||
|
target set. Everest explicitly supports multiple architectures
|
||||||
|
via SYS_PROFILE, including 32-bit ones — on a 32-bit target,
|
||||||
|
SHA-512 is SLOWER than SHA-256 (64-bit math has to be emulated).
|
||||||
|
- SHA-256's 128-bit collision resistance is not a meaningful
|
||||||
|
weakness for this use case. There is no realistic attack path
|
||||||
|
where 128 bits is the bottleneck for "did this archive get
|
||||||
|
corrupted or tampered with."
|
||||||
|
- SHA-256 is what every comparable tool already defaults to (apk,
|
||||||
|
pacman, dpkg), and is the simpler implementation to vendor and
|
||||||
|
audit (smaller constant tables, no 64-bit rotates).
|
||||||
|
|
||||||
|
Escape hatch, explicitly kept open: hash values are namespaced as
|
||||||
|
"sha256:<hex>", never a bare hex string, specifically so a future
|
||||||
|
algorithm change is additive, not a format migration. "sha256 can
|
||||||
|
always be added to base if needed" — i.e. if the vendored
|
||||||
|
implementation is ever found lacking, the fallback is depending on a
|
||||||
|
real sha256sum binary once one exists in the base repo, not a redesign
|
||||||
|
of the sidecar format.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 3. What gets hashed, and how |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
WHAT: whole-archive only, for now. Per-file hashing (and the
|
||||||
|
eventual `gpkg --verify`-style installed-file audit it would enable)
|
||||||
|
is explicitly deferred, not rejected.
|
||||||
|
|
||||||
|
WHERE: a sidecar file next to the .gpkg —
|
||||||
|
|
||||||
|
musl-1.2.5.gpkg
|
||||||
|
musl-1.2.5.gpkg.sha256 <- contains exactly: sha256:<hex>
|
||||||
|
|
||||||
|
Sidecar was chosen over embedding in the manifest specifically because
|
||||||
|
a whole-archive hash cannot cleanly be embedded inside the file it is
|
||||||
|
hashing without excluding the hash field itself from the computation.
|
||||||
|
This also generalizes naturally to a future remote repo index that
|
||||||
|
just lists hashes for everything it serves.
|
||||||
|
|
||||||
|
IMPLEMENTATION SHAPE: one shared vendored implementation, compiled
|
||||||
|
two different ways, so there is exactly one place the algorithm
|
||||||
|
itself is ever implemented:
|
||||||
|
|
||||||
|
- src/sha256.c / src/sha256.h
|
||||||
|
Streaming API (init/update/final), not just "hash this file" —
|
||||||
|
chosen now specifically so per-file hashing later reuses the
|
||||||
|
same primitive without a rewrite.
|
||||||
|
|
||||||
|
- gpkg / syspkg side:
|
||||||
|
Compiled into a PRIVATE .so (no public header, not part of the
|
||||||
|
split-library public API surface). Loaded directly via FFI from
|
||||||
|
gpkg_common.lua (see section 6). Private for now because this is
|
||||||
|
new, unaudited code; promoting it to a real public
|
||||||
|
libglacier_crypto.so is the natural path once per-file hashing
|
||||||
|
is built and the API has proven itself.
|
||||||
|
|
||||||
|
- gbuild side:
|
||||||
|
Compiled into a tiny standalone CLI helper, `gsha256sum FILE`,
|
||||||
|
printing `sha256:<hex>` to stdout. gbuild shells out to it the
|
||||||
|
same way it already shells out to git/tar/make. This keeps
|
||||||
|
gbuild from depending on the host having sha256sum or openssl
|
||||||
|
installed, while still not needing gbuild to link against the
|
||||||
|
private .so directly (gbuild is a bash script, not something
|
||||||
|
that can drive FFI).
|
||||||
|
|
||||||
|
gbuild change: after producing the .gpkg archive, hash it with
|
||||||
|
gsha256sum and write the sidecar alongside it. This is not optional
|
||||||
|
groundwork — the moment verification is turned on, EVERYTHING
|
||||||
|
becomes a "missing sidecar" failure until this exists, so the two
|
||||||
|
changes (gbuild writes sidecars / gpkg+syspkg check them) ship as
|
||||||
|
ONE atomic change, not sequential phases.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 4. Refusal policy |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
Missing sidecar and hash mismatch are treated IDENTICALLY. There is no
|
||||||
|
soft "unverified, proceed anyway by default" tier — absence of proof
|
||||||
|
is treated the same as proof of tampering.
|
||||||
|
|
||||||
|
repo == "base" -> ALWAYS refuse. No flag, no
|
||||||
|
scope, no exception. Ever.
|
||||||
|
scope == SYS (via syspkg) -> ALWAYS refuse, for ANY repo,
|
||||||
|
including extra/community.
|
||||||
|
No flag overrides this.
|
||||||
|
scope == USR && repo != base -> refuse by default. The ONLY
|
||||||
|
place a bypass flag could ever
|
||||||
|
apply, once one exists.
|
||||||
|
|
||||||
|
No bypass flag exists yet. One may be added later, but ONLY for the
|
||||||
|
USR-scope/non-base case above — never for base, never for system
|
||||||
|
scope, regardless of how the flag is invoked.
|
||||||
|
|
||||||
|
Net effect, stated as a real security property: there is no flag
|
||||||
|
combination, present or future, that can install a corrupted base
|
||||||
|
package or a corrupted system-scope package.
|
||||||
|
|
||||||
|
HOOK POINT: gpkg's read_manifest_fields already resolves a package's
|
||||||
|
repo before any transaction machinery runs (it's needed for the
|
||||||
|
existing confirmation summary). Verification belongs right there,
|
||||||
|
before gl_init_stage_context is ever called. A failure aborts that
|
||||||
|
package the same way any other install error already does today — no
|
||||||
|
new failure-handling pattern needed.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 5. Tool separation: gpkg / gstore / syspkg |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
gpkg and gstore are BOTH strictly per-user tools. Neither has, or will
|
||||||
|
ever have, any code path into GL_SCOPE_SYS. This is a reversal of
|
||||||
|
earlier work in this project (gpkg briefly gained a -s/--system flag
|
||||||
|
and gstore briefly gained -N/--new-system) — both need to come back
|
||||||
|
out. Recorded here explicitly so it isn't missed during
|
||||||
|
implementation: removing capability that already exists in the
|
||||||
|
current tree, not just "don't add it."
|
||||||
|
|
||||||
|
syspkg is a new, separate binary — the SOLE tool with any system-scope
|
||||||
|
capability at all: install, remove, AND store creation/init (what
|
||||||
|
gstore -N used to do). One tool to secure, one tool to audit, one
|
||||||
|
place the privilege boundary has to be enforced.
|
||||||
|
|
||||||
|
Why store creation belongs in syspkg and not "the installer calling
|
||||||
|
gl_init_sys() directly": that would create a second, independent,
|
||||||
|
unaudited entry point into system-scope mutation — the exact problem
|
||||||
|
already solved for install/remove. syspkg owning ALL system-scope
|
||||||
|
mutation, with nothing else able to touch it, is the actual point.
|
||||||
|
|
||||||
|
gstore's existing UID bounds-check behavior (hard-refuses < 1000, even
|
||||||
|
under doas, even as UID 0) is confirmed already working correctly and
|
||||||
|
is being preserved exactly as-is — gstore stays permanently
|
||||||
|
user-scope-only; the fix is removing -N, not touching the bounds
|
||||||
|
check.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 6. syspkg privilege model |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
syspkg is always run as root. It is invoked via doas (or sudo) —
|
||||||
|
NEVER made setuid. This was an explicit rejected alternative, not an
|
||||||
|
unconsidered default:
|
||||||
|
|
||||||
|
A setuid LuaJIT interpreter is a real anti-pattern. It would inherit
|
||||||
|
environment variables, library search paths, and require() resolution
|
||||||
|
from whatever untrusted shell invoked it — all of which become
|
||||||
|
privilege-escalation surface the instant the process is running as
|
||||||
|
root. This is the same class of problem that has made setuid
|
||||||
|
scripts (shell, Perl, anything with a runtime) a long-standing
|
||||||
|
security no-go.
|
||||||
|
|
||||||
|
syspkg's own responsibility is limited to checking geteuid() == 0 and
|
||||||
|
refusing otherwise. The actual privilege boundary is enforced by doas
|
||||||
|
— a hardened, audited, purpose-built tool for exactly this — not
|
||||||
|
reimplemented inside syspkg itself.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 7. Shared code: gpkg_common.lua |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
gpkg and syspkg need nearly identical machinery: manifest reading, the
|
||||||
|
summary/confirmation prompt, staged-transaction handling
|
||||||
|
(gl_init_stage_context -> install/remove loop -> commit/abort), and
|
||||||
|
the hash-verification check from section 4. All of that lives in
|
||||||
|
lib/gpkg_common.lua, required by both.
|
||||||
|
|
||||||
|
What stays DISTINCT per-binary:
|
||||||
|
- Which scope each one is even capable of requesting. gpkg's code
|
||||||
|
simply never constructs a GL_SCOPE_SYS context — not "doesn't
|
||||||
|
expose a flag for it," does not have the capability in its code
|
||||||
|
path at all.
|
||||||
|
- The privilege model (only syspkg is ever invoked with elevation).
|
||||||
|
|
||||||
|
This split means the refusal matrix in section 4 lives ONCE in the
|
||||||
|
shared module — it does not need to be written twice or kept in sync
|
||||||
|
across two files. syspkg is simply the only caller that can ever
|
||||||
|
reach the scope == SYS branch of it.
|
||||||
|
|
||||||
|
This decision exists partly because of history in this project: a
|
||||||
|
previously-duplicated ffi.cdef block across three files caused a real,
|
||||||
|
hard-to-diagnose bug. Not repeating that shape here on purpose.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 8. Base repo cannot install to USR |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
A package in the base repo can NEVER be installed under GL_SCOPE_USR
|
||||||
|
(per-user scope). This is very unlikely to be attempted in practice,
|
||||||
|
but is being hardcoded as a real invariant anyway, not left as an
|
||||||
|
assumption.
|
||||||
|
|
||||||
|
ENFORCEMENT LEVEL: the library itself (gl_install_pkg), not any CLI.
|
||||||
|
scope == GL_SCOPE_USR && repo == GL_BASE_REPO refuses unconditionally,
|
||||||
|
regardless of which caller asked for it.
|
||||||
|
|
||||||
|
Rationale: the whole point of the split-.so architecture is that a
|
||||||
|
third-party tool can be built directly against libglacier-ng without
|
||||||
|
going through gpkg/syspkg at all. A rule that only one CLI happens to
|
||||||
|
respect is not a real guarantee. This must be a property of the
|
||||||
|
package system itself.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 9. syspkg self-update |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
syspkg will itself ship as a base-repo package (it has to, to be part
|
||||||
|
of the minimal system) and therefore has to be able to update itself
|
||||||
|
while running. Resolved the same way the earlier libc-live-update
|
||||||
|
question was resolved (see PACKAGE_SCOPES.txt section 5): POSIX
|
||||||
|
unlink() semantics already make this safe — a running process keeps
|
||||||
|
its old inode open even after the symlink swap, only newly-spawned
|
||||||
|
processes see the new version. No special self-replace mechanism
|
||||||
|
needed; this falls directly out of the existing atomic-rename commit
|
||||||
|
design.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 10. Open: the installer / grootstrap |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
STATUS: genuinely undecided, not just unimplemented. The actual
|
||||||
|
process of installing Everest end-to-end is, in the author's own
|
||||||
|
words, "kind of elusive" right now. grootstrap's rework is explicitly
|
||||||
|
BLOCKED on this getting its own dedicated design pass — not something
|
||||||
|
to force an answer on tonight.
|
||||||
|
|
||||||
|
What IS clear:
|
||||||
|
- grootstrap (or whatever the installer turns out to be) cannot be
|
||||||
|
correctly rebuilt until syspkg, gpkg_common.lua, and hash
|
||||||
|
verification all exist — every package a bootstrap installs is
|
||||||
|
base-repo, the strictest tier, so the installer is the FIRST real
|
||||||
|
consumer of all three at once.
|
||||||
|
- Whatever places syspkg itself onto a target filesystem, before
|
||||||
|
syspkg exists there to enforce anything, is definitionally outside
|
||||||
|
the "corrupted base package can never install" guarantee. This is
|
||||||
|
an honest edge, not a flaw — it only closes once the installer's
|
||||||
|
actual shape is designed. Live/install media is presumably the
|
||||||
|
root of trust at that point (if you trust what you booted from,
|
||||||
|
you trust what it lays down) — but this has not been decided, only
|
||||||
|
named as the shape of the problem.
|
||||||
|
|
||||||
|
REFERENCE MATERIAL (from an old installation guide at
|
||||||
|
everestlinux.org/install, read only through its "Syncing the Build
|
||||||
|
Environment" section — earlier material only, deliberately not
|
||||||
|
consulting anything past that point. Describes a previous,
|
||||||
|
possibly-outdated vision of the install process; treat as INPUT to the
|
||||||
|
future design pass, not as decisions already made. No system image
|
||||||
|
tarball concept appears anywhere in this range — that idea is
|
||||||
|
discarded entirely, not merely unmentioned):
|
||||||
|
|
||||||
|
- "system mountpoint" — a target directory (example used:
|
||||||
|
/mnt/everest, with a SYS_MNT env var pointing at it) that the new
|
||||||
|
system's root gets built at, kept separate from the host doing the
|
||||||
|
building. Conceptually the same thing grootstrap's TARGET_DIR
|
||||||
|
already is.
|
||||||
|
|
||||||
|
- Partition layout described around that mountpoint: an EFI system
|
||||||
|
partition at .../boot, an OPTIONAL dedicated partition for
|
||||||
|
/glacier itself (suggested minimum 15 GB), an optional /home
|
||||||
|
partition, and the remainder as root. The optional standalone
|
||||||
|
glacier partition is worth noting specifically — it implies
|
||||||
|
/glacier was originally conceived as something that could be its
|
||||||
|
own filesystem, separate from root, not necessarily just a
|
||||||
|
directory tree living on the root filesystem. Not a decision,
|
||||||
|
just a detail worth not losing.
|
||||||
|
|
||||||
|
- "template index" — a named, downloadable list of packages
|
||||||
|
defining what a given install should contain (the guide's own
|
||||||
|
words: "provides a list of packages which can be merged into a
|
||||||
|
build environment, thereby creating a semi-functional system").
|
||||||
|
The guide frames CHOOSING a template index around a specific set
|
||||||
|
of questions: does the install need 32-bit libraries, will it run
|
||||||
|
proprietary software that can't be recompiled, does it need a
|
||||||
|
specific C library for the hardware, is SELinux wanted, is a
|
||||||
|
non-default init system wanted. Those are essentially the exact
|
||||||
|
axes SYS_PROFILE already encodes (ARCH-LIBC[-FEATURE...]) — this
|
||||||
|
is a real correspondence, not a coincidence to ignore. Worth
|
||||||
|
deciding whether a template index simply IS a SYS_PROFILE plus a
|
||||||
|
recipes/ set under current design, or a separate downstream
|
||||||
|
concept (e.g. a pre-resolved list of already-built package
|
||||||
|
references to fetch, rather than a list of things to build from
|
||||||
|
source). The guide also warns that switching template
|
||||||
|
indexes/profiles after install requires rebuilding most of the
|
||||||
|
system — consistent with SYS_PROFILE being a foundational,
|
||||||
|
not-meant-to-change-casually choice in the current design too.
|
||||||
|
|
||||||
|
- "glacier-bootstrap PATH_TO_TEMPLATE_INDEX /mnt/everest" — a
|
||||||
|
referenced (possibly not-yet-existing-in-current-form) tool
|
||||||
|
described as taking "the specified template index, and
|
||||||
|
bootstrap[ping] a system at the specified directory, using the
|
||||||
|
packages listed within the template index as a guide." This is
|
||||||
|
package-list-driven bootstrapping, not image-based — it lines up
|
||||||
|
with what grootstrap is already trying to be (extract/install a
|
||||||
|
defined set of packages into a target directory), not with
|
||||||
|
anything tarball-shaped.
|
||||||
|
|
||||||
|
None of the above is a decision. It's what exists to react to when the
|
||||||
|
installer gets its own design session — scoped deliberately to only
|
||||||
|
the early, environment-setup half of the old guide for now.
|
||||||
|
|
||||||
|
|
||||||
|
+--------------------------------------------+
|
||||||
|
| 11. Build order for tomorrow |
|
||||||
|
+--------------------------------------------+
|
||||||
|
|
||||||
|
Dependency order, not necessarily literal implementation order within
|
||||||
|
a day, but grootstrap specifically cannot start until the first three
|
||||||
|
exist:
|
||||||
|
|
||||||
|
1. src/sha256.c / src/sha256.h (no dependencies)
|
||||||
|
2. gsha256sum CLI (needs 1)
|
||||||
|
3. private hash .so for gpkg/syspkg (needs 1)
|
||||||
|
4. gbuild: write sidecar on build (needs 2)
|
||||||
|
5. lib/gpkg_common.lua (shared logic extraction)
|
||||||
|
6. gl_install_pkg: base->USR refusal (library-level, needs nothing
|
||||||
|
above, can happen any time)
|
||||||
|
7. gpkg: remove -s/--system entirely (needs 5, to not duplicate
|
||||||
|
logic while trimming it)
|
||||||
|
8. gstore: remove -N/--new-system entirely
|
||||||
|
9. syspkg: new binary — install/remove/init under GL_SCOPE_SYS,
|
||||||
|
doas-only, no setuid (needs 1, 3, 5)
|
||||||
|
10. grootstrap rework BLOCKED — needs 4, 9, AND a
|
||||||
|
separate installer design
|
||||||
|
pass (section 10). Do not
|
||||||
|
start until that exists.
|
||||||
|
|
||||||
|
Verify-downloads-from-remote-repos (authenticity, signing, keyring)
|
||||||
|
is the explicitly agreed NEXT major item after everything above is
|
||||||
|
implemented — not part of this phase.
|
||||||
130
SECURITY_REVIEW_2026-07-16.md
Normal file
130
SECURITY_REVIEW_2026-07-16.md
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
# Security Review — libglacier-ng (dev branch, working tree)
|
||||||
|
|
||||||
|
Date: 2026-07-16
|
||||||
|
Scope: uncommitted working-tree changes (Makefile, config.mk, src/common.h,
|
||||||
|
src/istoreutils.{c,h}, new src/transaction.{c,h}). The `dev` branch had no
|
||||||
|
new commits vs. `origin/dev`, so the actual diff under review is the
|
||||||
|
unstaged/untracked working-tree state.
|
||||||
|
|
||||||
|
All three findings below share one root cause: package manifests
|
||||||
|
(`PKG_NAME` / `PKG_REPO`, read from `manifest.gpm.cfg` inside the `.gpkg`
|
||||||
|
archive being installed) are fully attacker-controlled and are never
|
||||||
|
validated anywhere in the codebase. A full-repo search for
|
||||||
|
`signature|verify|trusted|checksum|sha256|gpg|allowlist` returned zero
|
||||||
|
hits — there is no package authenticity or provenance check at all.
|
||||||
|
|
||||||
|
This PR's changes (`GL_SCOPE_SYS` / `gl_init_sys`, and the new
|
||||||
|
`GL_BASE_REPO` → `/usr` symlink routing) newly plug that untrusted data
|
||||||
|
into root-privileged, system-wide install paths, which is what escalates
|
||||||
|
these from "attacker writes into their own store" to "attacker writes as
|
||||||
|
root, or shadows real `/usr` binaries."
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 1 — Authorization bypass via self-declared "base" repo
|
||||||
|
|
||||||
|
* **File:** `src/istoreutils.c:742` (also `:859`, `:1404`)
|
||||||
|
* **Severity:** High
|
||||||
|
* **Category:** `authorization-bypass`
|
||||||
|
* **Confidence:** High
|
||||||
|
|
||||||
|
**Description:** System-scope symlink placement into `/usr` is gated only
|
||||||
|
by a package's manifest self-declaring `PKG_REPO="base"`. There is no
|
||||||
|
check that the package actually originated from a trusted/vetted base
|
||||||
|
repository — the classification is just a string the package author
|
||||||
|
wrote into their own manifest.
|
||||||
|
|
||||||
|
**Exploit scenario:** An operator runs `gpkg -s malicious.gpkg` (a
|
||||||
|
normal, documented system-scope install per `PACKAGE_SCOPES.txt`) on a
|
||||||
|
package whose `manifest.gpm.cfg` the attacker fully controls. Setting
|
||||||
|
`PKG_REPO="base"` causes `gl_link_pkg` (istoreutils.c:742) and
|
||||||
|
`gl_relink_store` (istoreutils.c:1404) to route the package's symlinks
|
||||||
|
into `GL_BASE_LINKS_DEST` (`/usr`) instead of the isolated
|
||||||
|
`/glacier/sys/links` tree — the same privileged, real-FHS locations init
|
||||||
|
scripts, systemd units, and shebang lines trust. Combined with Finding 3
|
||||||
|
(unsanitized `PKG_NAME`), the attacker can target specific paths such as
|
||||||
|
`/usr/bin/sudo`.
|
||||||
|
|
||||||
|
**Recommendation:** Don't trust the manifest's self-declared repo for
|
||||||
|
privilege routing. Either (a) determine repo/trust from the source the
|
||||||
|
package was fetched from (signed repo metadata, not embedded manifest
|
||||||
|
fields), or (b) require a separate signature/checksum check before a
|
||||||
|
package is allowed to claim `base` classification and land in `/usr`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 2 — Tar-slip path traversal during archive extraction
|
||||||
|
|
||||||
|
* **File:** `src/istoreutils.c:1242` (loop starting ~1234, `dest_path` built at 1249)
|
||||||
|
* **Severity:** High
|
||||||
|
* **Category:** `path-traversal`
|
||||||
|
* **Confidence:** High
|
||||||
|
|
||||||
|
**Description:** `gl_install_pkg` extracts the package tar with a
|
||||||
|
hand-rolled `open()`/`mkdir()` loop instead of libarchive's
|
||||||
|
`archive_write_disk` (which offers `ARCHIVE_EXTRACT_SECURE_NODOTDOT` /
|
||||||
|
`SECURE_SYMLINKS` protections). The per-entry relative path (`rel_path`,
|
||||||
|
istoreutils.c:1242) is taken directly from the tar entry name after
|
||||||
|
stripping a known prefix, and is concatenated into `dest_path` via
|
||||||
|
`snprintf` with no rejection of `..` segments or absolute paths.
|
||||||
|
|
||||||
|
**Exploit scenario:** Because the strip prefix is
|
||||||
|
`"<pkg_name>-<ver>/files/"` and `pkg_name` is itself attacker-controlled
|
||||||
|
(pulled verbatim from the manifest, no validation), the attacker fully
|
||||||
|
controls the prefix match and can name a tar entry
|
||||||
|
`"<pkg_name>-<ver>/files/../../../../etc/cron.d/pwn"`. After prefix
|
||||||
|
stripping this becomes `../../../../etc/cron.d/pwn`, written wherever the
|
||||||
|
installing process can write. This code path is reachable for both
|
||||||
|
per-uid and (newly, via this PR) root-privileged system-scope installs,
|
||||||
|
so the same bug now yields root-level arbitrary file write.
|
||||||
|
|
||||||
|
**Recommendation:** Reject any entry path containing `..` components or a
|
||||||
|
leading `/` before extraction, or switch to `archive_write_disk` with
|
||||||
|
`ARCHIVE_EXTRACT_SECURE_NODOTDOT | ARCHIVE_EXTRACT_SECURE_SYMLINKS |
|
||||||
|
ARCHIVE_EXTRACT_SECURE_NOABSOLUTEPATHS` set.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Finding 3 — Path traversal via unsanitized PKG_NAME / PKG_REPO in store paths
|
||||||
|
|
||||||
|
* **File:** `src/istoreutils.c:1196` (manifest parsing: `src/pkg.c:149,153`)
|
||||||
|
* **Severity:** Medium–High
|
||||||
|
* **Category:** `path-traversal`
|
||||||
|
* **Confidence:** High
|
||||||
|
|
||||||
|
**Description:** `gl_gpm2gpkg` (`pkg.c:149,153`) reads `PKG_NAME` and
|
||||||
|
`PKG_REPO` straight out of the archive's manifest config with
|
||||||
|
`config_lookup_string` + `strdup`, no validation. `gl_install_pkg` then
|
||||||
|
builds the package's store directory as `"%s/%s/%s"` from
|
||||||
|
`ctx->store_path`, `pkg.pkg_repo`, `pkg.pkg_name` (istoreutils.c:1196)
|
||||||
|
and `mkdir -p`s it.
|
||||||
|
|
||||||
|
**Exploit scenario:** A manifest with `PKG_REPO="../../../../tmp"` (or a
|
||||||
|
`PKG_NAME` containing `../` segments) causes the package directory —
|
||||||
|
and, via the extraction loop right after (Finding 2), arbitrary files —
|
||||||
|
to be created outside `ctx->store_path` entirely. Previously this only
|
||||||
|
affected a single user's own per-uid store; with this PR's new
|
||||||
|
`GL_SCOPE_SYS` (`ctx->store_path == "/glacier/sys/store"`, root-owned),
|
||||||
|
the same bug now lets an untrusted package write anywhere root can
|
||||||
|
write.
|
||||||
|
|
||||||
|
**Recommendation:** Validate `PKG_NAME` and `PKG_REPO` against an
|
||||||
|
allowlist pattern (e.g. `^[A-Za-z0-9._-]+$`) immediately after parsing
|
||||||
|
in `gl_gpm2gpkg`, rejecting the manifest outright if either field
|
||||||
|
contains `/`, `..`, or is empty.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Note — unrelated working-tree cleanup
|
||||||
|
|
||||||
|
The working tree also contains what look like leftover failed-patch
|
||||||
|
artifacts, not part of the reviewed logic but worth cleaning up before
|
||||||
|
committing:
|
||||||
|
|
||||||
|
```
|
||||||
|
istoreutils.c.diff
|
||||||
|
istoreutils.h.diff
|
||||||
|
src/istoreutils.c.back
|
||||||
|
src/istoreutils.c.rej
|
||||||
|
src/istoreutils.h.back
|
||||||
|
```
|
||||||
BIN
build/bin/gsha256sum
Executable file
BIN
build/bin/gsha256sum
Executable file
Binary file not shown.
BIN
build/lib/shared/libglacier-ng.so
Executable file
BIN
build/lib/shared/libglacier-ng.so
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/lib/shared/libglacier_transaction.so
Executable file
BIN
build/lib/shared/libglacier_transaction.so
Executable file
Binary file not shown.
BIN
build/lib/shared/libglacier_verify.so
Executable file
BIN
build/lib/shared/libglacier_verify.so
Executable file
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
BIN
build/lib/tmp/sha256.o
Normal file
BIN
build/lib/tmp/sha256.o
Normal file
Binary file not shown.
@@ -4,6 +4,6 @@
|
|||||||
|
|
||||||
PREFIX ?= /usr
|
PREFIX ?= /usr
|
||||||
|
|
||||||
CFLAGS = -std=c11 -pedantic -O2 -flto -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror
|
CFLAGS = -std=c11 -pedantic -O2 -Wall -Wextra -Wshadow -Wformat=2 -Wconversion -Wpedantic -Werror -Wno-format-truncation
|
||||||
|
|
||||||
TEST_DIR = /home/lw/Projects/glacier-ng/lib
|
TEST_DIR = /home/lw/Projects/glacier-ng/lib
|
||||||
|
|||||||
67
src/common.h
67
src/common.h
@@ -1,17 +1,17 @@
|
|||||||
#ifndef COMMON_H_
|
#ifndef COMMON_H_
|
||||||
#define COMMON_H_
|
#define COMMON_H_
|
||||||
|
|
||||||
#ifndef _POSIX_C_SOURCE
|
|
||||||
#define _POSIX_C_SOURCE 200809L
|
#define _POSIX_C_SOURCE 200809L
|
||||||
#endif
|
|
||||||
#ifndef _DEFAULT_SOURCE
|
|
||||||
#define _DEFAULT_SOURCE
|
|
||||||
#endif
|
|
||||||
#ifndef _XOPEN_SOURCE
|
|
||||||
#define _XOPEN_SOURCE 700
|
#define _XOPEN_SOURCE 700
|
||||||
#endif
|
|
||||||
|
|
||||||
|
#include <dirent.h>
|
||||||
|
#include <errno.h>
|
||||||
|
#include <linux/limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
typedef struct
|
typedef struct
|
||||||
{
|
{
|
||||||
@@ -20,4 +20,57 @@ typedef struct
|
|||||||
}
|
}
|
||||||
dir_t;
|
dir_t;
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
gl_mkdirp(const char *path, mode_t mode)
|
||||||
|
{
|
||||||
|
char tmp[PATH_MAX];
|
||||||
|
strncpy(tmp, path, sizeof(tmp));
|
||||||
|
tmp[sizeof(tmp) - 1] = '\0';
|
||||||
|
|
||||||
|
for (char *p = tmp + 1; *p; p++) {
|
||||||
|
if (*p == '/') {
|
||||||
|
*p = '\0';
|
||||||
|
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
*p = '/';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static inline int
|
||||||
|
gl_rmdir_recursive(const char *path)
|
||||||
|
{
|
||||||
|
DIR *dir = opendir(path);
|
||||||
|
if (!dir) { return -1; }
|
||||||
|
|
||||||
|
struct dirent *ent;
|
||||||
|
while ((ent = readdir(dir))) {
|
||||||
|
if (ent->d_name[0] == '.') { continue; }
|
||||||
|
|
||||||
|
char full[PATH_MAX];
|
||||||
|
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (lstat(full, &st) != 0) { continue; }
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
gl_rmdir_recursive(full);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
unlink(full);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
return rmdir(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
21
src/gsha256sum.c
Normal file
21
src/gsha256sum.c
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
#include <stdio.h>
|
||||||
|
|
||||||
|
#include "sha256.h"
|
||||||
|
|
||||||
|
int
|
||||||
|
main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
if (argc != 2) {
|
||||||
|
fprintf(stderr, "usage: %s FILE\n", argv[0]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
char hex[GL_SHA256_HEX_SIZE];
|
||||||
|
if (gl_sha256_file(argv[1], hex) != 0) {
|
||||||
|
perror(argv[1]);
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
printf("sha256:%s\n", hex);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@
|
|||||||
#include "istoreutils.h"
|
#include "istoreutils.h"
|
||||||
#include "log.h"
|
#include "log.h"
|
||||||
#include "pkg.h"
|
#include "pkg.h"
|
||||||
|
#include "transaction.h"
|
||||||
|
|
||||||
static const dir_t usr_index = { "/glacier/usr/index", 0700 };
|
static const dir_t usr_index = { "/glacier/usr/index", 0700 };
|
||||||
static const dir_t sys_index = { "/glacier/sys/index", 0700 };
|
static const dir_t sys_index = { "/glacier/sys/index", 0700 };
|
||||||
@@ -29,6 +30,17 @@ static const dir_t usr_store = { "/glacier/usr/store", 0700 };
|
|||||||
static const dir_t sys_store = { "/glacier/sys/store", 0700 };
|
static const dir_t sys_store = { "/glacier/sys/store", 0700 };
|
||||||
|
|
||||||
static const dir_t usr_links = { "/glacier/usr/links", 0700 };
|
static const dir_t usr_links = { "/glacier/usr/links", 0700 };
|
||||||
|
static const dir_t sys_links = { "/glacier/sys/links", 0700 };
|
||||||
|
|
||||||
|
/* Base-system exception: packages in the "base" repo under system
|
||||||
|
* scope get their symlinks placed directly in /usr rather than the
|
||||||
|
* default isolated /glacier/sys/links, so boot/init/scripts that
|
||||||
|
* hardcode /usr/bin, /usr/lib, etc. work without glacier-aware PATH
|
||||||
|
* setup. Everything else installed under system scope (an operator
|
||||||
|
* running `gpkg -s` on something that isn't part of the base system)
|
||||||
|
* stays under /glacier/sys/links. See gl_link_pkg / gl_relink_store. */
|
||||||
|
#define GL_BASE_REPO "base"
|
||||||
|
#define GL_BASE_LINKS_DEST "/usr"
|
||||||
|
|
||||||
bool
|
bool
|
||||||
gl_usr_istore_exists(index_store_t index_or_store, int uid)
|
gl_usr_istore_exists(index_store_t index_or_store, int uid)
|
||||||
@@ -115,6 +127,36 @@ create_index(const char *ind_path, uid_t uid)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool
|
||||||
|
is_dir(const char *d)
|
||||||
|
{
|
||||||
|
struct stat st;
|
||||||
|
return stat(d, &st) == 0 && S_ISDIR(st.st_mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* is_glacier_symlink
|
||||||
|
* Returns true only if `path` is a symlink whose target lives under
|
||||||
|
* `store_prefix` — i.e. a symlink glacier itself created, as opposed
|
||||||
|
* to some unrelated file, directory, or foreign symlink that happens
|
||||||
|
* to occupy that path. This distinction only matters once a scope's
|
||||||
|
* links_path can alias a directory glacier doesn't exclusively own
|
||||||
|
* (e.g. /usr for system-scope packages) rather than always being an
|
||||||
|
* isolated glacier-only tree. Everywhere this is used, the rule is the
|
||||||
|
* same: if this returns false, never touch the path — not overwrite
|
||||||
|
* it, not prune it, nothing.
|
||||||
|
*/
|
||||||
|
static bool
|
||||||
|
is_glacier_symlink(const char *path, const char *store_prefix)
|
||||||
|
{
|
||||||
|
char target[PATH_MAX];
|
||||||
|
ssize_t n = readlink(path, target, sizeof(target) - 1);
|
||||||
|
if (n < 0) { return false; } /* not a symlink, or doesn't exist */
|
||||||
|
target[n] = '\0';
|
||||||
|
return strncmp(target, store_prefix, strlen(store_prefix)) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
int
|
int
|
||||||
gl_init_user(uid_t uid, bool isVerbose)
|
gl_init_user(uid_t uid, bool isVerbose)
|
||||||
{
|
{
|
||||||
@@ -253,6 +295,46 @@ gl_init_user(uid_t uid, bool isVerbose)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
gl_init_sys(bool isVerbose)
|
||||||
|
{
|
||||||
|
const char *paths[] = {
|
||||||
|
sys_index.path,
|
||||||
|
sys_store.path,
|
||||||
|
sys_links.path,
|
||||||
|
};
|
||||||
|
mode_t modes[] = {
|
||||||
|
sys_index.mode,
|
||||||
|
sys_store.mode,
|
||||||
|
sys_links.mode,
|
||||||
|
};
|
||||||
|
|
||||||
|
for (int i = 0; i < 3; i++) {
|
||||||
|
if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); }
|
||||||
|
|
||||||
|
if (mkdir(paths[i], modes[i]) == -1) {
|
||||||
|
switch (errno) {
|
||||||
|
case EEXIST:
|
||||||
|
break;
|
||||||
|
case EACCES:
|
||||||
|
lg_printf(2, "Cannot create %s: permission denied", paths[i]);
|
||||||
|
return 1;
|
||||||
|
case ENOSPC:
|
||||||
|
lg_printf(2, "Cannot create %s: no space left on device", paths[i]);
|
||||||
|
return 1;
|
||||||
|
case EROFS:
|
||||||
|
lg_printf(2, "Cannot create %s: read-only filesystem", paths[i]);
|
||||||
|
return 1;
|
||||||
|
default:
|
||||||
|
lg_printf(2, "Cannot create %s: mkdir failed", paths[i]);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
static void
|
static void
|
||||||
trim(char *s)
|
trim(char *s)
|
||||||
{
|
{
|
||||||
@@ -313,17 +395,34 @@ gl_parse_index(struct gindex *idx, FILE *f)
|
|||||||
|
|
||||||
char *first = strstr(line, "::");
|
char *first = strstr(line, "::");
|
||||||
if (!first) { continue; }
|
if (!first) { continue; }
|
||||||
|
|
||||||
*first = 0;
|
*first = 0;
|
||||||
|
|
||||||
char *repo = line;
|
char *repo = line;
|
||||||
char *pkg = first + 2;
|
char *rest = first + 2;
|
||||||
|
|
||||||
|
char *second = strstr(rest, "::");
|
||||||
|
char *pkg;
|
||||||
|
char *ver;
|
||||||
|
if (second) {
|
||||||
|
*second = 0;
|
||||||
|
pkg = rest;
|
||||||
|
ver = second + 2;
|
||||||
|
} else {
|
||||||
|
/* Older index files may only have repo::pkg with no
|
||||||
|
* version field — tolerate that rather than reject it. */
|
||||||
|
pkg = rest;
|
||||||
|
ver = "unknown";
|
||||||
|
}
|
||||||
|
|
||||||
trim(repo);
|
trim(repo);
|
||||||
trim(pkg);
|
trim(pkg);
|
||||||
|
trim(ver);
|
||||||
|
|
||||||
struct gpkg_entry ent;
|
struct gpkg_entry ent;
|
||||||
ent.repo = dupstr(repo);
|
ent.repo = dupstr(repo);
|
||||||
ent.pkg = dupstr(pkg);
|
ent.pkg = dupstr(pkg);
|
||||||
if (!ent.repo || !ent.pkg) { return -1; }
|
ent.ver = dupstr(ver);
|
||||||
|
if (!ent.repo || !ent.pkg || !ent.ver) { return -1; }
|
||||||
|
|
||||||
struct gpkg_entry *tmp =
|
struct gpkg_entry *tmp =
|
||||||
realloc(idx->entries, sizeof(*idx->entries) * (idx->count + 1));
|
realloc(idx->entries, sizeof(*idx->entries) * (idx->count + 1));
|
||||||
@@ -341,80 +440,19 @@ gl_free_index(struct gindex *idx)
|
|||||||
for (size_t i = 0; i < idx->count; i++) {
|
for (size_t i = 0; i < idx->count; i++) {
|
||||||
free(idx->entries[i].repo);
|
free(idx->entries[i].repo);
|
||||||
free(idx->entries[i].pkg);
|
free(idx->entries[i].pkg);
|
||||||
|
free(idx->entries[i].ver);
|
||||||
}
|
}
|
||||||
|
|
||||||
free(idx->entries);
|
free(idx->entries);
|
||||||
idx->entries = NULL;
|
idx->entries = NULL;
|
||||||
idx->count = 0;
|
idx->count = 0;
|
||||||
}
|
}
|
||||||
/*
|
|
||||||
int
|
int
|
||||||
gl_rebuild_index(uid_t uid, const char *out_dir)
|
gl_rebuild_index(gl_context_t *ctx)
|
||||||
{
|
{
|
||||||
char store_path[PATH_MAX];
|
if (!ctx) { return -1; }
|
||||||
snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid);
|
|
||||||
|
|
||||||
DIR *store_dir = opendir(store_path);
|
DIR *store_dir = opendir(ctx->store_path);
|
||||||
if (!store_dir) return -1;
|
|
||||||
|
|
||||||
FILE *out = fopen(out_dir, "w");
|
|
||||||
if (!out) return -1;
|
|
||||||
|
|
||||||
size_t listed = 0;
|
|
||||||
|
|
||||||
struct dirent *repo_ent;
|
|
||||||
while ((repo_ent = readdir(store_dir))) {
|
|
||||||
if (repo_ent->d_name[0] == '.') { continue; }
|
|
||||||
|
|
||||||
char repo_path[PATH_MAX];
|
|
||||||
snprintf(repo_path, sizeof(repo_path), "%s/%s",
|
|
||||||
store_path, repo_ent->d_name);
|
|
||||||
|
|
||||||
struct stat rst;
|
|
||||||
if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
DIR *repo_dir = opendir(repo_path);
|
|
||||||
if (!repo_dir) continue;
|
|
||||||
|
|
||||||
struct dirent *pkg_ent;
|
|
||||||
while ((pkg_ent = readdir(repo_dir))) {
|
|
||||||
if (pkg_ent->d_name[0] == '.') { continue; }
|
|
||||||
|
|
||||||
char pkg_path[PATH_MAX];
|
|
||||||
snprintf(pkg_path, sizeof(pkg_path), "%s/%s",
|
|
||||||
repo_path, pkg_ent->d_name);
|
|
||||||
|
|
||||||
struct stat pst;
|
|
||||||
if (stat(pkg_path, &pst) != 0 || !S_ISDIR(pst.st_mode)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
fprintf(out, "%s::%s\n",
|
|
||||||
repo_ent->d_name,
|
|
||||||
pkg_ent->d_name);
|
|
||||||
listed++;
|
|
||||||
}
|
|
||||||
closedir(repo_dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
rewind(out);
|
|
||||||
fprintf(out, "uid = %d\n", uid);
|
|
||||||
|
|
||||||
closedir(store_dir);
|
|
||||||
fclose(out);
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
*/
|
|
||||||
|
|
||||||
int
|
|
||||||
gl_rebuild_index(uid_t uid, const char *out_dir)
|
|
||||||
{
|
|
||||||
char store_path[PATH_MAX];
|
|
||||||
snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid);
|
|
||||||
|
|
||||||
DIR *store_dir = opendir(store_path);
|
|
||||||
if (!store_dir) return -1;
|
if (!store_dir) return -1;
|
||||||
|
|
||||||
/* Write to a new timestamped index file */
|
/* Write to a new timestamped index file */
|
||||||
@@ -422,7 +460,8 @@ gl_rebuild_index(uid_t uid, const char *out_dir)
|
|||||||
make_index_name(fname, sizeof(fname));
|
make_index_name(fname, sizeof(fname));
|
||||||
|
|
||||||
char new_index_path[PATH_MAX];
|
char new_index_path[PATH_MAX];
|
||||||
snprintf(new_index_path, sizeof(new_index_path), "%s/%s", out_dir, fname);
|
snprintf(new_index_path, sizeof(new_index_path), "%s/%s",
|
||||||
|
ctx->index_path, fname);
|
||||||
|
|
||||||
FILE *out = fopen(new_index_path, "w");
|
FILE *out = fopen(new_index_path, "w");
|
||||||
if (!out) {
|
if (!out) {
|
||||||
@@ -438,7 +477,7 @@ gl_rebuild_index(uid_t uid, const char *out_dir)
|
|||||||
|
|
||||||
char repo_path[PATH_MAX];
|
char repo_path[PATH_MAX];
|
||||||
snprintf(repo_path, sizeof(repo_path), "%s/%s",
|
snprintf(repo_path, sizeof(repo_path), "%s/%s",
|
||||||
store_path, repo_ent->d_name);
|
ctx->store_path, repo_ent->d_name);
|
||||||
|
|
||||||
struct stat rst;
|
struct stat rst;
|
||||||
if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) {
|
if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) {
|
||||||
@@ -461,9 +500,33 @@ gl_rebuild_index(uid_t uid, const char *out_dir)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(out, "%s::%s\n",
|
/* One version dir per package name, named
|
||||||
|
* "<pkg_name>-<version>" (see gl_cat_dir_and_ver
|
||||||
|
* in pkg.c, which is what created it). Strip the
|
||||||
|
* known "<pkg_name>-" prefix to recover just the
|
||||||
|
* version part — safe even if pkg_name itself
|
||||||
|
* contains dashes, since we already know its
|
||||||
|
* exact length from pkg_ent->d_name. */
|
||||||
|
char pkg_ver[64] = "unknown";
|
||||||
|
DIR *ver_dir = opendir(pkg_path);
|
||||||
|
if (ver_dir) {
|
||||||
|
struct dirent *ver_ent;
|
||||||
|
while ((ver_ent = readdir(ver_dir))) {
|
||||||
|
if (ver_ent->d_name[0] == '.') { continue; }
|
||||||
|
size_t prefix_len = strlen(pkg_ent->d_name) + 1;
|
||||||
|
if (strlen(ver_ent->d_name) > prefix_len) {
|
||||||
|
snprintf(pkg_ver, sizeof(pkg_ver), "%s",
|
||||||
|
ver_ent->d_name + prefix_len);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
closedir(ver_dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
fprintf(out, "%s::%s::%s\n",
|
||||||
repo_ent->d_name,
|
repo_ent->d_name,
|
||||||
pkg_ent->d_name);
|
pkg_ent->d_name,
|
||||||
|
pkg_ver);
|
||||||
listed++;
|
listed++;
|
||||||
}
|
}
|
||||||
closedir(repo_dir);
|
closedir(repo_dir);
|
||||||
@@ -476,14 +539,15 @@ gl_rebuild_index(uid_t uid, const char *out_dir)
|
|||||||
if (!tmp_in) return -1;
|
if (!tmp_in) return -1;
|
||||||
|
|
||||||
char tmp_path[PATH_MAX];
|
char tmp_path[PATH_MAX];
|
||||||
snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX", out_dir);
|
snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX",
|
||||||
|
ctx->index_path);
|
||||||
int tmp_fd = mkstemp(tmp_path);
|
int tmp_fd = mkstemp(tmp_path);
|
||||||
if (tmp_fd < 0) { fclose(tmp_in); return -1; }
|
if (tmp_fd < 0) { fclose(tmp_in); return -1; }
|
||||||
|
|
||||||
FILE *tmp_out = fdopen(tmp_fd, "w");
|
FILE *tmp_out = fdopen(tmp_fd, "w");
|
||||||
if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; }
|
if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; }
|
||||||
|
|
||||||
fprintf(tmp_out, "uid = %d\n", uid);
|
fprintf(tmp_out, "uid = %d\n", ctx->uid);
|
||||||
fprintf(tmp_out, "listed = %zu\n\n", listed);
|
fprintf(tmp_out, "listed = %zu\n\n", listed);
|
||||||
|
|
||||||
char line[512];
|
char line[512];
|
||||||
@@ -494,15 +558,15 @@ gl_rebuild_index(uid_t uid, const char *out_dir)
|
|||||||
fclose(tmp_out);
|
fclose(tmp_out);
|
||||||
|
|
||||||
rename(tmp_path, new_index_path);
|
rename(tmp_path, new_index_path);
|
||||||
chown(new_index_path, uid, (gid_t)-1);
|
chown(new_index_path, ctx->uid, (gid_t)-1);
|
||||||
|
|
||||||
/* Update the "current" symlink to point at the new index */
|
/* Update the "current" symlink to point at the new index */
|
||||||
char link_path[PATH_MAX];
|
char link_path[PATH_MAX];
|
||||||
snprintf(link_path, sizeof(link_path), "%s/current", out_dir);
|
snprintf(link_path, sizeof(link_path), "%s/current", ctx->index_path);
|
||||||
unlink(link_path);
|
unlink(link_path);
|
||||||
|
|
||||||
if (symlink(fname, link_path) != 0) { return -1; }
|
if (symlink(fname, link_path) != 0) { return -1; }
|
||||||
lchown(link_path, uid, (gid_t)-1);
|
lchown(link_path, ctx->uid, (gid_t)-1);
|
||||||
|
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -672,62 +736,14 @@ gl_backup_istore(const char *tar_path,
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
gl_mkdirp(const char *path, mode_t mode)
|
gl_unlink_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo)
|
||||||
{
|
|
||||||
char tmp[PATH_MAX];
|
|
||||||
strncpy(tmp, path, sizeof(tmp));
|
|
||||||
tmp[sizeof(tmp) - 1] = '\0';
|
|
||||||
|
|
||||||
for (char *p = tmp + 1; *p; p++) {
|
|
||||||
if (*p == '/') {
|
|
||||||
*p = '\0';
|
|
||||||
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
*p = '/';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mkdir(tmp, mode) == -1 && errno != EEXIST) {
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
int
|
|
||||||
gl_rmdir_recursive(const char *path)
|
|
||||||
{
|
|
||||||
DIR *dir = opendir(path);
|
|
||||||
if (!dir) { return -1; }
|
|
||||||
|
|
||||||
struct dirent *ent;
|
|
||||||
while ((ent = readdir(dir))) {
|
|
||||||
if (ent->d_name[0] == '.') { continue; }
|
|
||||||
|
|
||||||
char full[PATH_MAX];
|
|
||||||
snprintf(full, sizeof(full), "%s/%s", path, ent->d_name);
|
|
||||||
|
|
||||||
struct stat st;
|
|
||||||
if (lstat(full, &st) != 0) { continue; }
|
|
||||||
|
|
||||||
if (S_ISDIR(st.st_mode)) {
|
|
||||||
gl_rmdir_recursive(full);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
unlink(full);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
closedir(dir);
|
|
||||||
return rmdir(path);
|
|
||||||
}
|
|
||||||
|
|
||||||
static int
|
|
||||||
gl_unlink_pkg(const char *pkg_store_final, uid_t uid)
|
|
||||||
{
|
{
|
||||||
char links_base[PATH_MAX];
|
char links_base[PATH_MAX];
|
||||||
snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid);
|
if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) {
|
||||||
|
snprintf(links_base, sizeof(links_base), "%s", GL_BASE_LINKS_DEST);
|
||||||
|
} else {
|
||||||
|
snprintf(links_base, sizeof(links_base), "%s", ctx->links_path);
|
||||||
|
}
|
||||||
|
|
||||||
size_t strip_len = strlen(pkg_store_final);
|
size_t strip_len = strlen(pkg_store_final);
|
||||||
|
|
||||||
@@ -781,12 +797,14 @@ gl_unlink_pkg(const char *pkg_store_final, uid_t uid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
gl_remove_status_t
|
gl_remove_status_t
|
||||||
gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid)
|
gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
|
||||||
{
|
{
|
||||||
|
if (!ctx) { return GL_REMOVE_ERR_NOT_FOUND; }
|
||||||
|
|
||||||
/* find the package base directory in the store */
|
/* find the package base directory in the store */
|
||||||
char pkg_store_base[PATH_MAX];
|
char pkg_store_base[PATH_MAX];
|
||||||
snprintf(pkg_store_base, sizeof(pkg_store_base),
|
snprintf(pkg_store_base, sizeof(pkg_store_base),
|
||||||
"%s/%d/%s/%s", usr_store.path, uid, pkg_repo, pkg_name);
|
"%s/%s/%s", ctx->store_path, pkg_repo, pkg_name);
|
||||||
|
|
||||||
struct stat st;
|
struct stat st;
|
||||||
if (stat(pkg_store_base, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
if (stat(pkg_store_base, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||||
@@ -811,10 +829,15 @@ gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid)
|
|||||||
return GL_REMOVE_ERR_NOT_FOUND;
|
return GL_REMOVE_ERR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* remove symlinks from links tree */
|
/* remove symlinks from links tree. GL_CTX_STAGE: skip — the
|
||||||
if (gl_unlink_pkg(pkg_store_final, uid) != 0) {
|
* stage links tree isn't seeded (links are fully derived from
|
||||||
|
* the store post-commit by relink_store), so there's nothing
|
||||||
|
* there to unlink. */
|
||||||
|
if (ctx->mode == GL_CTX_LIVE) {
|
||||||
|
if (gl_unlink_pkg(ctx, pkg_store_final, pkg_repo) != 0) {
|
||||||
return GL_REMOVE_ERR_UNLINK;
|
return GL_REMOVE_ERR_UNLINK;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* remove package from store */
|
/* remove package from store */
|
||||||
if (gl_rmdir_recursive(pkg_store_base) != 0) {
|
if (gl_rmdir_recursive(pkg_store_base) != 0) {
|
||||||
@@ -822,10 +845,7 @@ gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* rebuild index */
|
/* rebuild index */
|
||||||
char index_dir[PATH_MAX];
|
if (gl_rebuild_index(ctx) != 0) {
|
||||||
snprintf(index_dir, sizeof(index_dir), "%s/%d", usr_index.path, uid);
|
|
||||||
|
|
||||||
if (gl_rebuild_index(uid, index_dir) != 0) {
|
|
||||||
return GL_REMOVE_ERR_INDEX;
|
return GL_REMOVE_ERR_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -833,10 +853,14 @@ gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid)
|
|||||||
}
|
}
|
||||||
|
|
||||||
static int
|
static int
|
||||||
gl_link_pkg(const char *pkg_store_final, uid_t uid)
|
gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final, const char *pkg_repo)
|
||||||
{
|
{
|
||||||
char links_base[PATH_MAX];
|
char links_base[PATH_MAX];
|
||||||
snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid);
|
if (ctx->scope == GL_SCOPE_SYS && strcmp(pkg_repo, GL_BASE_REPO) == 0) {
|
||||||
|
snprintf(links_base, sizeof(links_base), "%s", GL_BASE_LINKS_DEST);
|
||||||
|
} else {
|
||||||
|
snprintf(links_base, sizeof(links_base), "%s", ctx->links_path);
|
||||||
|
}
|
||||||
|
|
||||||
size_t strip_len = strlen(pkg_store_final);
|
size_t strip_len = strlen(pkg_store_final);
|
||||||
|
|
||||||
@@ -895,7 +919,17 @@ gl_link_pkg(const char *pkg_store_final, uid_t uid)
|
|||||||
gl_mkdirp(parent, 0700);
|
gl_mkdirp(parent, 0700);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* remove existing symlink if present */
|
/* Only ever replace an existing entry if it's a
|
||||||
|
* symlink we created ourselves. See
|
||||||
|
* is_glacier_symlink's comment for why this
|
||||||
|
* check exists at all. */
|
||||||
|
struct stat existing_st;
|
||||||
|
if (lstat(link_path, &existing_st) == 0 &&
|
||||||
|
!is_glacier_symlink(link_path, ctx->store_path)) {
|
||||||
|
lg_printf(1,
|
||||||
|
"Refusing to overwrite non-glacier path: %s",
|
||||||
|
link_path);
|
||||||
|
} else {
|
||||||
unlink(link_path);
|
unlink(link_path);
|
||||||
|
|
||||||
/* symlink store path -> links tree */
|
/* symlink store path -> links tree */
|
||||||
@@ -906,7 +940,8 @@ gl_link_pkg(const char *pkg_store_final, uid_t uid)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
lchown(link_path, uid, (gid_t)-1);
|
lchown(link_path, ctx->uid, (gid_t)-1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -949,6 +984,78 @@ gl_delete_user(uid_t uid, bool isVerbose)
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
char
|
||||||
|
*gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name)
|
||||||
|
{
|
||||||
|
char store_path[PATH_MAX];
|
||||||
|
snprintf(store_path, sizeof(store_path), "%s", ctx->store_path);
|
||||||
|
|
||||||
|
DIR *uid_dir = opendir(store_path);
|
||||||
|
if (!uid_dir) { return NULL; }
|
||||||
|
|
||||||
|
struct dirent *repo_entry;
|
||||||
|
while ((repo_entry = readdir(uid_dir)) != NULL) {
|
||||||
|
if (repo_entry->d_name[0] == '.') { continue; }
|
||||||
|
|
||||||
|
char pkg_path[PATH_MAX];
|
||||||
|
snprintf(pkg_path, sizeof(pkg_path), "%s/%s/%s", store_path, repo_entry->d_name, pkg_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(pkg_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||||
|
char *repo = strdup(repo_entry->d_name);
|
||||||
|
closedir(uid_dir);
|
||||||
|
return repo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(uid_dir);
|
||||||
|
return NULL;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_repo_list_t
|
||||||
|
gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name)
|
||||||
|
{
|
||||||
|
gl_repo_list_t result = { NULL, 0 };
|
||||||
|
|
||||||
|
char store_path[PATH_MAX];
|
||||||
|
snprintf(store_path, sizeof(store_path), "%s", ctx->store_path);
|
||||||
|
|
||||||
|
DIR *uid_dir = opendir(store_path);
|
||||||
|
if (!uid_dir) { return result; }
|
||||||
|
|
||||||
|
struct dirent *repo_entry;
|
||||||
|
while ((repo_entry = readdir(uid_dir)) != NULL) {
|
||||||
|
if (repo_entry->d_name[0] == '.') { continue; }
|
||||||
|
|
||||||
|
char pkg_path[PATH_MAX];
|
||||||
|
snprintf(pkg_path, sizeof(pkg_path),
|
||||||
|
"%s/%s/%s", store_path, repo_entry->d_name, pkg_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(pkg_path, &st) == 0 && S_ISDIR(st.st_mode)) {
|
||||||
|
char **tmp = realloc(result.repos,
|
||||||
|
(result.count + 1) * sizeof(char *));
|
||||||
|
if (!tmp) { break; }
|
||||||
|
result.repos = tmp;
|
||||||
|
result.repos[result.count] = strdup(repo_entry->d_name);
|
||||||
|
result.count++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(uid_dir);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
gl_free_repo_list(gl_repo_list_t *list)
|
||||||
|
{
|
||||||
|
if (!list) { return; }
|
||||||
|
for (size_t i = 0; i < list->count; i++) { free(list->repos[i]); }
|
||||||
|
free(list->repos);
|
||||||
|
list->repos = NULL;
|
||||||
|
list->count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
gl_restore_status_t
|
gl_restore_status_t
|
||||||
gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid)
|
gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid)
|
||||||
{
|
{
|
||||||
@@ -1011,8 +1118,9 @@ gl_restore_istore(const char *tar_path, const char *glacier_root, const char *ui
|
|||||||
}
|
}
|
||||||
|
|
||||||
gl_install_status_t
|
gl_install_status_t
|
||||||
gl_install_pkg(const char *gpkg_path, uid_t uid)
|
gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
|
||||||
{
|
{
|
||||||
|
if (!ctx) { return GL_INSTALL_ERR_OPEN; }
|
||||||
char original_cwd[PATH_MAX];
|
char original_cwd[PATH_MAX];
|
||||||
if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) {
|
if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) {
|
||||||
return GL_INSTALL_ERR_OPEN;
|
return GL_INSTALL_ERR_OPEN;
|
||||||
@@ -1086,7 +1194,7 @@ gl_install_pkg(const char *gpkg_path, uid_t uid)
|
|||||||
|
|
||||||
/* construct the directory for the package in the store */
|
/* construct the directory for the package in the store */
|
||||||
snprintf(pkg_store_base, sizeof(pkg_store_base),
|
snprintf(pkg_store_base, sizeof(pkg_store_base),
|
||||||
"%s/%d/%s/%s", usr_store.path, uid, pkg.pkg_repo, pkg.pkg_name);
|
"%s/%s/%s", ctx->store_path, pkg.pkg_repo, pkg.pkg_name);
|
||||||
|
|
||||||
snprintf(pkg_store_final, sizeof(pkg_store_final),
|
snprintf(pkg_store_final, sizeof(pkg_store_final),
|
||||||
"%s/%s-%d.%d.%d", pkg_store_base, pkg.pkg_name,
|
"%s/%s-%d.%d.%d", pkg_store_base, pkg.pkg_name,
|
||||||
@@ -1186,15 +1294,13 @@ gl_install_pkg(const char *gpkg_path, uid_t uid)
|
|||||||
archive_read_free(a2);
|
archive_read_free(a2);
|
||||||
|
|
||||||
/* now the index will be updated */
|
/* now the index will be updated */
|
||||||
|
/* Resolve "current" symlink to confirm an index exists to rebuild
|
||||||
/* now the index will be updated */
|
* against. The actual write happens in gl_rebuild_index below —
|
||||||
char index_dir[PATH_MAX];
|
* anything written here would just be overwritten by that call,
|
||||||
snprintf(index_dir, sizeof(index_dir),
|
* since it creates a fresh timestamped index file and repoints
|
||||||
"%s/%d", usr_index.path, uid);
|
* "current" at it rather than editing this one in place. */
|
||||||
|
|
||||||
/* Resolve "current" symlink to find the actual index file to append to */
|
|
||||||
char current_link[PATH_MAX];
|
char current_link[PATH_MAX];
|
||||||
snprintf(current_link, sizeof(current_link), "%s/current", index_dir);
|
snprintf(current_link, sizeof(current_link), "%s/current", ctx->index_path);
|
||||||
|
|
||||||
char current_target[PATH_MAX];
|
char current_target[PATH_MAX];
|
||||||
ssize_t len = readlink(current_link, current_target, sizeof(current_target) - 1);
|
ssize_t len = readlink(current_link, current_target, sizeof(current_target) - 1);
|
||||||
@@ -1204,31 +1310,148 @@ gl_install_pkg(const char *gpkg_path, uid_t uid)
|
|||||||
}
|
}
|
||||||
current_target[len] = '\0';
|
current_target[len] = '\0';
|
||||||
|
|
||||||
char index_path[PATH_MAX];
|
if (gl_rebuild_index(ctx) != 0) {
|
||||||
snprintf(index_path, sizeof(index_path), "%s/%s", index_dir, current_target);
|
|
||||||
|
|
||||||
FILE *idx = fopen(index_path, "a");
|
|
||||||
if (!idx) {
|
|
||||||
gl_free_pkg(&pkg);
|
gl_free_pkg(&pkg);
|
||||||
return GL_INSTALL_ERR_INDEX;
|
return GL_INSTALL_ERR_INDEX;
|
||||||
}
|
}
|
||||||
|
|
||||||
fprintf(idx, "%s::%s\n", pkg.pkg_repo, pkg.pkg_name);
|
if (ctx->mode == GL_CTX_LIVE) {
|
||||||
fclose(idx);
|
if (gl_link_pkg(ctx, pkg_store_final, pkg.pkg_repo) != 0) {
|
||||||
|
|
||||||
if (gl_rebuild_index(uid, index_dir) != 0) {
|
|
||||||
gl_free_pkg(&pkg);
|
|
||||||
return GL_INSTALL_ERR_INDEX;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (gl_link_pkg(pkg_store_final, uid) != 0) {
|
|
||||||
gl_free_pkg(&pkg);
|
gl_free_pkg(&pkg);
|
||||||
return GL_INSTALL_ERR_EXTRACT;
|
return GL_INSTALL_ERR_EXTRACT;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
/* GL_CTX_STAGE: skip. A symlink written here would target
|
||||||
|
* ctx->store_path, which is the stage path — it goes stale the
|
||||||
|
* moment gl_commit_transaction renames the store into its live
|
||||||
|
* location. The links tree is fully derived from the store, so
|
||||||
|
* it's regenerated wholesale post-commit instead of tracked
|
||||||
|
* incrementally here (see relink_store in transaction.c). */
|
||||||
|
|
||||||
gl_free_pkg(&pkg);
|
gl_free_pkg(&pkg);
|
||||||
chdir(original_cwd);
|
chdir(original_cwd);
|
||||||
return GL_INSTALL_OK;
|
return GL_INSTALL_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* prune_stale_links
|
||||||
|
* Recursively removes only the symlinks glacier itself created (per
|
||||||
|
* is_glacier_symlink) whose store target no longer exists — i.e. links
|
||||||
|
* left behind by a package that's since been removed. Directories,
|
||||||
|
* regular files, and symlinks glacier didn't create are never touched,
|
||||||
|
* recursed into for directories aside. This must stay this
|
||||||
|
* conservative because links_path is not always glacier-exclusive
|
||||||
|
* territory: for system-scope packages it can be /usr itself, which
|
||||||
|
* holds plenty of content glacier has no business deleting.
|
||||||
|
*/
|
||||||
|
static void
|
||||||
|
prune_stale_links(const char *dir_path, const char *store_prefix)
|
||||||
|
{
|
||||||
|
DIR *dir = opendir(dir_path);
|
||||||
|
if (!dir) { return; }
|
||||||
|
|
||||||
|
struct dirent *ent;
|
||||||
|
while ((ent = readdir(dir))) {
|
||||||
|
if (ent->d_name[0] == '.') { continue; }
|
||||||
|
|
||||||
|
char full[PATH_MAX];
|
||||||
|
snprintf(full, sizeof(full), "%s/%s", dir_path, ent->d_name);
|
||||||
|
|
||||||
|
struct stat lst;
|
||||||
|
if (lstat(full, &lst) != 0) { continue; }
|
||||||
|
|
||||||
|
if (S_ISLNK(lst.st_mode)) {
|
||||||
|
if (!is_glacier_symlink(full, store_prefix)) {
|
||||||
|
continue; /* not ours - never touch */
|
||||||
|
}
|
||||||
|
struct stat target_st;
|
||||||
|
if (stat(full, &target_st) != 0 && errno == ENOENT) {
|
||||||
|
/* our symlink, but its target is gone */
|
||||||
|
unlink(full);
|
||||||
|
}
|
||||||
|
} else if (S_ISDIR(lst.st_mode)) {
|
||||||
|
prune_stale_links(full, store_prefix);
|
||||||
|
}
|
||||||
|
/* regular files: never ours, never touched */
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
gl_relink_store(gl_context_t *ctx)
|
||||||
|
{
|
||||||
|
/* This function's contract is "derive the links tree entirely
|
||||||
|
* from the store" (same as gl_rebuild_index does for the index) -
|
||||||
|
* but "derive fully" now means prune-then-refresh, not
|
||||||
|
* wipe-then-rebuild. links_path may be a directory glacier
|
||||||
|
* doesn't exclusively own (e.g. /usr for system scope), so a
|
||||||
|
* recursive delete of the whole tree is off the table; only
|
||||||
|
* glacier's own stale symlinks get removed. gl_mkdirp is
|
||||||
|
* non-destructive (mkdir tolerates EEXIST) so this is still safe
|
||||||
|
* to call even if links_path already exists. */
|
||||||
|
if (gl_mkdirp(ctx->links_path, 0700) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
prune_stale_links(ctx->links_path, ctx->store_path);
|
||||||
|
|
||||||
|
/* Base-repo packages under system scope route to /usr instead of
|
||||||
|
* ctx->links_path (see GL_BASE_REPO) — prune there too, since a
|
||||||
|
* package that WAS in the base repo and got removed/moved leaves
|
||||||
|
* its stale symlink in /usr, not in ctx->links_path. Only relevant
|
||||||
|
* for system scope; /usr is never a valid destination for
|
||||||
|
* per-uid packages. */
|
||||||
|
if (ctx->scope == GL_SCOPE_SYS) {
|
||||||
|
prune_stale_links(GL_BASE_LINKS_DEST, ctx->store_path);
|
||||||
|
}
|
||||||
|
|
||||||
|
DIR *repo_dir = opendir(ctx->store_path);
|
||||||
|
if (!repo_dir) return (errno == ENOENT) ? 0 : -1;
|
||||||
|
|
||||||
|
struct dirent *repo_ent;
|
||||||
|
while ((repo_ent = readdir(repo_dir))) {
|
||||||
|
if (repo_ent->d_name[0] == '.') continue;
|
||||||
|
|
||||||
|
char repo_path[PATH_MAX];
|
||||||
|
snprintf(repo_path, sizeof(repo_path), "%s/%s",
|
||||||
|
ctx->store_path, repo_ent->d_name);
|
||||||
|
if (!is_dir(repo_path)) continue;
|
||||||
|
|
||||||
|
DIR *name_dir = opendir(repo_path);
|
||||||
|
if (!name_dir) continue;
|
||||||
|
|
||||||
|
struct dirent *name_ent;
|
||||||
|
while ((name_ent = readdir(name_dir))) {
|
||||||
|
if (name_ent->d_name[0] == '.') continue;
|
||||||
|
|
||||||
|
char name_path[PATH_MAX];
|
||||||
|
snprintf(name_path, sizeof(name_path), "%s/%s",
|
||||||
|
repo_path, name_ent->d_name);
|
||||||
|
if (!is_dir(name_path)) continue;
|
||||||
|
|
||||||
|
/* one version dir per package name today */
|
||||||
|
DIR *ver_dir = opendir(name_path);
|
||||||
|
if (!ver_dir) continue;
|
||||||
|
|
||||||
|
struct dirent *ver_ent;
|
||||||
|
while ((ver_ent = readdir(ver_dir))) {
|
||||||
|
if (ver_ent->d_name[0] == '.') continue;
|
||||||
|
|
||||||
|
char ver_path[PATH_MAX];
|
||||||
|
snprintf(ver_path, sizeof(ver_path), "%s/%s",
|
||||||
|
name_path, ver_ent->d_name);
|
||||||
|
if (!is_dir(ver_path)) continue;
|
||||||
|
|
||||||
|
if (gl_link_pkg(ctx, ver_path, repo_ent->d_name) != 0) {
|
||||||
|
closedir(ver_dir); closedir(name_dir);
|
||||||
|
closedir(repo_dir);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
closedir(ver_dir);
|
||||||
|
}
|
||||||
|
closedir(name_dir);
|
||||||
|
}
|
||||||
|
closedir(repo_dir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
1365
src/istoreutils.c.back
Normal file
1365
src/istoreutils.c.back
Normal file
File diff suppressed because it is too large
Load Diff
302
src/istoreutils.c.rej
Normal file
302
src/istoreutils.c.rej
Normal file
@@ -0,0 +1,302 @@
|
|||||||
|
--- istoreutils.c.orig 2026-06-01 19:43:00.493773073 +0000
|
||||||
|
+++ istoreutils.c.new 2026-06-01 19:43:00.573469330 +0000
|
||||||
|
@@ -21,6 +21,7 @@
|
||||||
|
#include "istoreutils.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "pkg.h"
|
||||||
|
+#include "transaction.h"
|
||||||
|
|
||||||
|
static const dir_t usr_index = { "/glacier/usr/index", 0700 };
|
||||||
|
static const dir_t sys_index = { "/glacier/sys/index", 0700 };
|
||||||
|
@@ -29,6 +30,7 @@
|
||||||
|
static const dir_t sys_store = { "/glacier/sys/store", 0700 };
|
||||||
|
|
||||||
|
static const dir_t usr_links = { "/glacier/usr/links", 0700 };
|
||||||
|
+static const dir_t sys_links = { "/glacier/sys/links", 0700 };
|
||||||
|
|
||||||
|
bool
|
||||||
|
gl_usr_istore_exists(index_store_t index_or_store, int uid)
|
||||||
|
@@ -253,6 +255,46 @@
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
+int
|
||||||
|
+gl_init_sys(bool isVerbose)
|
||||||
|
+{
|
||||||
|
+ const char *paths[] = {
|
||||||
|
+ sys_index.path,
|
||||||
|
+ sys_store.path,
|
||||||
|
+ sys_links.path,
|
||||||
|
+ };
|
||||||
|
+ mode_t modes[] = {
|
||||||
|
+ sys_index.mode,
|
||||||
|
+ sys_store.mode,
|
||||||
|
+ sys_links.mode,
|
||||||
|
+ };
|
||||||
|
+
|
||||||
|
+ for (int i = 0; i < 3; i++) {
|
||||||
|
+ if (isVerbose) { lg_printf(0, "Creating %s", paths[i]); }
|
||||||
|
+
|
||||||
|
+ if (mkdir(paths[i], modes[i]) == -1) {
|
||||||
|
+ switch (errno) {
|
||||||
|
+ case EEXIST:
|
||||||
|
+ break;
|
||||||
|
+ case EACCES:
|
||||||
|
+ lg_printf(2, "Cannot create %s: permission denied", paths[i]);
|
||||||
|
+ return 1;
|
||||||
|
+ case ENOSPC:
|
||||||
|
+ lg_printf(2, "Cannot create %s: no space left on device", paths[i]);
|
||||||
|
+ return 1;
|
||||||
|
+ case EROFS:
|
||||||
|
+ lg_printf(2, "Cannot create %s: read-only filesystem", paths[i]);
|
||||||
|
+ return 1;
|
||||||
|
+ default:
|
||||||
|
+ lg_printf(2, "Cannot create %s: mkdir failed", paths[i]);
|
||||||
|
+ return -1;
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+ }
|
||||||
|
+
|
||||||
|
+ return 0;
|
||||||
|
+}
|
||||||
|
+
|
||||||
|
static void
|
||||||
|
trim(char *s)
|
||||||
|
{
|
||||||
|
@@ -409,12 +451,11 @@
|
||||||
|
*/
|
||||||
|
|
||||||
|
int
|
||||||
|
-gl_rebuild_index(uid_t uid, const char *out_dir)
|
||||||
|
+gl_rebuild_index(gl_context_t *ctx)
|
||||||
|
{
|
||||||
|
- char store_path[PATH_MAX];
|
||||||
|
- snprintf(store_path, sizeof(store_path), "%s/%d", usr_store.path, uid);
|
||||||
|
+ if (!ctx) { return -1; }
|
||||||
|
|
||||||
|
- DIR *store_dir = opendir(store_path);
|
||||||
|
+ DIR *store_dir = opendir(ctx->store_path);
|
||||||
|
if (!store_dir) return -1;
|
||||||
|
|
||||||
|
/* Write to a new timestamped index file */
|
||||||
|
@@ -422,7 +463,8 @@
|
||||||
|
make_index_name(fname, sizeof(fname));
|
||||||
|
|
||||||
|
char new_index_path[PATH_MAX];
|
||||||
|
- snprintf(new_index_path, sizeof(new_index_path), "%s/%s", out_dir, fname);
|
||||||
|
+ snprintf(new_index_path, sizeof(new_index_path), "%s/%s",
|
||||||
|
+ ctx->index_path, fname);
|
||||||
|
|
||||||
|
FILE *out = fopen(new_index_path, "w");
|
||||||
|
if (!out) {
|
||||||
|
@@ -438,7 +480,7 @@
|
||||||
|
|
||||||
|
char repo_path[PATH_MAX];
|
||||||
|
snprintf(repo_path, sizeof(repo_path), "%s/%s",
|
||||||
|
- store_path, repo_ent->d_name);
|
||||||
|
+ ctx->store_path, repo_ent->d_name);
|
||||||
|
|
||||||
|
struct stat rst;
|
||||||
|
if (stat(repo_path, &rst) != 0 || !S_ISDIR(rst.st_mode)) {
|
||||||
|
@@ -476,14 +518,15 @@
|
||||||
|
if (!tmp_in) return -1;
|
||||||
|
|
||||||
|
char tmp_path[PATH_MAX];
|
||||||
|
- snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX", out_dir);
|
||||||
|
+ snprintf(tmp_path, sizeof(tmp_path), "%s/.index_tmp_XXXXXX",
|
||||||
|
+ ctx->index_path);
|
||||||
|
int tmp_fd = mkstemp(tmp_path);
|
||||||
|
if (tmp_fd < 0) { fclose(tmp_in); return -1; }
|
||||||
|
|
||||||
|
FILE *tmp_out = fdopen(tmp_fd, "w");
|
||||||
|
if (!tmp_out) { close(tmp_fd); fclose(tmp_in); return -1; }
|
||||||
|
|
||||||
|
- fprintf(tmp_out, "uid = %d\n", uid);
|
||||||
|
+ fprintf(tmp_out, "uid = %d\n", ctx->uid);
|
||||||
|
fprintf(tmp_out, "listed = %zu\n\n", listed);
|
||||||
|
|
||||||
|
char line[512];
|
||||||
|
@@ -494,15 +537,15 @@
|
||||||
|
fclose(tmp_out);
|
||||||
|
|
||||||
|
rename(tmp_path, new_index_path);
|
||||||
|
- chown(new_index_path, uid, (gid_t)-1);
|
||||||
|
+ chown(new_index_path, ctx->uid, (gid_t)-1);
|
||||||
|
|
||||||
|
/* Update the "current" symlink to point at the new index */
|
||||||
|
char link_path[PATH_MAX];
|
||||||
|
- snprintf(link_path, sizeof(link_path), "%s/current", out_dir);
|
||||||
|
+ snprintf(link_path, sizeof(link_path), "%s/current", ctx->index_path);
|
||||||
|
unlink(link_path);
|
||||||
|
|
||||||
|
if (symlink(fname, link_path) != 0) { return -1; }
|
||||||
|
- lchown(link_path, uid, (gid_t)-1);
|
||||||
|
+ lchown(link_path, ctx->uid, (gid_t)-1);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
@@ -672,10 +715,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
-gl_unlink_pkg(const char *pkg_store_final, uid_t uid)
|
||||||
|
+gl_unlink_pkg(gl_context_t *ctx, const char *pkg_store_final)
|
||||||
|
{
|
||||||
|
char links_base[PATH_MAX];
|
||||||
|
- snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid);
|
||||||
|
+ strncpy(links_base, ctx->links_path, PATH_MAX - 1);
|
||||||
|
|
||||||
|
size_t strip_len = strlen(pkg_store_final);
|
||||||
|
|
||||||
|
@@ -729,12 +772,14 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_remove_status_t
|
||||||
|
-gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid)
|
||||||
|
+gl_remove_pkg(gl_context_t *ctx, const char *pkg_name, const char *pkg_repo)
|
||||||
|
{
|
||||||
|
+ if (!ctx) { return GL_REMOVE_ERR_NOT_FOUND; }
|
||||||
|
+
|
||||||
|
/* find the package base directory in the store */
|
||||||
|
char pkg_store_base[PATH_MAX];
|
||||||
|
snprintf(pkg_store_base, sizeof(pkg_store_base),
|
||||||
|
- "%s/%d/%s/%s", usr_store.path, uid, pkg_repo, pkg_name);
|
||||||
|
+ "%s/%s/%s", ctx->store_path, pkg_repo, pkg_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(pkg_store_base, &st) != 0 || !S_ISDIR(st.st_mode)) {
|
||||||
|
@@ -760,7 +805,7 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
/* remove symlinks from links tree */
|
||||||
|
- if (gl_unlink_pkg(pkg_store_final, uid) != 0) {
|
||||||
|
+ if (gl_unlink_pkg(ctx, pkg_store_final) != 0) {
|
||||||
|
return GL_REMOVE_ERR_UNLINK;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -770,10 +815,7 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
/* rebuild index */
|
||||||
|
- char index_dir[PATH_MAX];
|
||||||
|
- snprintf(index_dir, sizeof(index_dir), "%s/%d", usr_index.path, uid);
|
||||||
|
-
|
||||||
|
- if (gl_rebuild_index(uid, index_dir) != 0) {
|
||||||
|
+ if (gl_rebuild_index(ctx) != 0) {
|
||||||
|
return GL_REMOVE_ERR_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -781,10 +823,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
-gl_link_pkg(const char *pkg_store_final, uid_t uid)
|
||||||
|
+gl_link_pkg(gl_context_t *ctx, const char *pkg_store_final)
|
||||||
|
{
|
||||||
|
char links_base[PATH_MAX];
|
||||||
|
- snprintf(links_base, sizeof(links_base), "%s/%d", usr_links.path, uid);
|
||||||
|
+ strncpy(links_base, ctx->links_path, PATH_MAX - 1);
|
||||||
|
|
||||||
|
size_t strip_len = strlen(pkg_store_final);
|
||||||
|
|
||||||
|
@@ -854,7 +896,7 @@
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
- lchown(link_path, uid, (gid_t)-1);
|
||||||
|
+ lchown(link_path, ctx->uid, (gid_t)-1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@@ -898,11 +940,10 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
char
|
||||||
|
-*gl_find_pkg_repo(const char *pkg_name, uid_t uid)
|
||||||
|
+*gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name)
|
||||||
|
{
|
||||||
|
char store_path[PATH_MAX];
|
||||||
|
- snprintf(store_path, sizeof(store_path),
|
||||||
|
-"/glacier/usr/store/%u", uid);
|
||||||
|
+ strncpy(store_path, ctx->store_path, PATH_MAX - 1);
|
||||||
|
|
||||||
|
DIR *uid_dir = opendir(store_path);
|
||||||
|
if (!uid_dir) { return NULL; }
|
||||||
|
@@ -927,13 +968,12 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_repo_list_t
|
||||||
|
-gl_find_pkg_repos(const char *pkg_name, uid_t uid)
|
||||||
|
+gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name)
|
||||||
|
{
|
||||||
|
gl_repo_list_t result = { NULL, 0 };
|
||||||
|
|
||||||
|
char store_path[PATH_MAX];
|
||||||
|
- snprintf(store_path, sizeof(store_path),
|
||||||
|
- "/glacier/usr/store/%u", uid);
|
||||||
|
+ strncpy(store_path, ctx->store_path, PATH_MAX - 1);
|
||||||
|
|
||||||
|
DIR *uid_dir = opendir(store_path);
|
||||||
|
if (!uid_dir) { return result; }
|
||||||
|
@@ -1033,8 +1073,9 @@
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_install_status_t
|
||||||
|
-gl_install_pkg(const char *gpkg_path, uid_t uid)
|
||||||
|
+gl_install_pkg(gl_context_t *ctx, const char *gpkg_path)
|
||||||
|
{
|
||||||
|
+ if (!ctx) { return GL_INSTALL_ERR_OPEN; }
|
||||||
|
char original_cwd[PATH_MAX];
|
||||||
|
if (getcwd(original_cwd, sizeof(original_cwd)) == NULL) {
|
||||||
|
return GL_INSTALL_ERR_OPEN;
|
||||||
|
@@ -1108,7 +1149,7 @@
|
||||||
|
|
||||||
|
/* construct the directory for the package in the store */
|
||||||
|
snprintf(pkg_store_base, sizeof(pkg_store_base),
|
||||||
|
- "%s/%d/%s/%s", usr_store.path, uid, pkg.pkg_repo, pkg.pkg_name);
|
||||||
|
+ "%s/%s/%s", ctx->store_path, pkg.pkg_repo, pkg.pkg_name);
|
||||||
|
|
||||||
|
snprintf(pkg_store_final, sizeof(pkg_store_final),
|
||||||
|
"%s/%s-%d.%d.%d", pkg_store_base, pkg.pkg_name,
|
||||||
|
@@ -1208,15 +1249,9 @@
|
||||||
|
archive_read_free(a2);
|
||||||
|
|
||||||
|
/* now the index will be updated */
|
||||||
|
-
|
||||||
|
- /* now the index will be updated */
|
||||||
|
- char index_dir[PATH_MAX];
|
||||||
|
- snprintf(index_dir, sizeof(index_dir),
|
||||||
|
- "%s/%d", usr_index.path, uid);
|
||||||
|
-
|
||||||
|
/* Resolve "current" symlink to find the actual index file to append to */
|
||||||
|
char current_link[PATH_MAX];
|
||||||
|
- snprintf(current_link, sizeof(current_link), "%s/current", index_dir);
|
||||||
|
+ snprintf(current_link, sizeof(current_link), "%s/current", ctx->index_path);
|
||||||
|
|
||||||
|
char current_target[PATH_MAX];
|
||||||
|
ssize_t len = readlink(current_link, current_target, sizeof(current_target) - 1);
|
||||||
|
@@ -1227,7 +1262,8 @@
|
||||||
|
current_target[len] = '\0';
|
||||||
|
|
||||||
|
char index_path[PATH_MAX];
|
||||||
|
- snprintf(index_path, sizeof(index_path), "%s/%s", index_dir, current_target);
|
||||||
|
+ snprintf(index_path, sizeof(index_path), "%s/%s",
|
||||||
|
+ ctx->index_path, current_target);
|
||||||
|
|
||||||
|
FILE *idx = fopen(index_path, "a");
|
||||||
|
if (!idx) {
|
||||||
|
@@ -1238,12 +1274,12 @@
|
||||||
|
fprintf(idx, "%s::%s\n", pkg.pkg_repo, pkg.pkg_name);
|
||||||
|
fclose(idx);
|
||||||
|
|
||||||
|
- if (gl_rebuild_index(uid, index_dir) != 0) {
|
||||||
|
+ if (gl_rebuild_index(ctx) != 0) {
|
||||||
|
gl_free_pkg(&pkg);
|
||||||
|
return GL_INSTALL_ERR_INDEX;
|
||||||
|
}
|
||||||
|
|
||||||
|
- if (gl_link_pkg(pkg_store_final, uid) != 0) {
|
||||||
|
+ if (gl_link_pkg(ctx, pkg_store_final) != 0) {
|
||||||
|
gl_free_pkg(&pkg);
|
||||||
|
return GL_INSTALL_ERR_EXTRACT;
|
||||||
|
}
|
||||||
@@ -5,6 +5,8 @@
|
|||||||
#include <stdio.h>
|
#include <stdio.h>
|
||||||
#include <sys/types.h>
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#include "transaction.h"
|
||||||
|
|
||||||
typedef enum {
|
typedef enum {
|
||||||
INDEX,
|
INDEX,
|
||||||
STORE
|
STORE
|
||||||
@@ -58,6 +60,7 @@ typedef enum {
|
|||||||
struct gpkg_entry {
|
struct gpkg_entry {
|
||||||
char *repo;
|
char *repo;
|
||||||
char *pkg;
|
char *pkg;
|
||||||
|
char *ver;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct gindex {
|
struct gindex {
|
||||||
@@ -71,21 +74,33 @@ bool gl_usr_istore_exists(index_store_t index_or_store, int uid);
|
|||||||
bool gl_sys_istore_exists(index_store_t index_or_store);
|
bool gl_sys_istore_exists(index_store_t index_or_store);
|
||||||
|
|
||||||
int gl_init_user(uid_t uid, bool isVerbose);
|
int gl_init_user(uid_t uid, bool isVerbose);
|
||||||
|
int gl_init_sys(bool isVerbose);
|
||||||
int gl_delete_user(uid_t uid, bool isVerbose);
|
int gl_delete_user(uid_t uid, bool isVerbose);
|
||||||
|
|
||||||
int gl_parse_index(struct gindex *idx, FILE *f);
|
int gl_parse_index(struct gindex *idx, FILE *f);
|
||||||
void gl_free_index(struct gindex *idx);
|
void gl_free_index(struct gindex *idx);
|
||||||
|
|
||||||
int gl_rebuild_index(uid_t uid, const char *out_dir);
|
int gl_rebuild_index(gl_context_t *ctx);
|
||||||
|
|
||||||
char *gl_find_pkg_repo(const char *pkg_name, uid_t uid);
|
char *gl_find_pkg_repo(gl_context_t *ctx, const char *pkg_name);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char **repos;
|
||||||
|
size_t count;
|
||||||
|
}
|
||||||
|
gl_repo_list_t;
|
||||||
|
|
||||||
|
gl_repo_list_t gl_find_pkg_repos(gl_context_t *ctx, const char *pkg_name);
|
||||||
|
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);
|
gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid);
|
||||||
|
|
||||||
gl_install_status_t gl_install_pkg(const char *gpkg_path, uid_t uid);
|
gl_install_status_t gl_install_pkg(gl_context_t *ctx, const char *gpkg_path);
|
||||||
gl_remove_status_t gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid);
|
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);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
101
src/istoreutils.h.back
Normal file
101
src/istoreutils.h.back
Normal file
@@ -0,0 +1,101 @@
|
|||||||
|
#ifndef ISTOREUTILS_H_
|
||||||
|
#define ISTOREUTILS_H_
|
||||||
|
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
INDEX,
|
||||||
|
STORE
|
||||||
|
}
|
||||||
|
index_store_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_BACKUP_OK = 0,
|
||||||
|
GL_BACKUP_ERR_ALLOC = 1,
|
||||||
|
GL_BACKUP_ERR_OPEN = 2,
|
||||||
|
GL_BACKUP_ERR_ADD_INDEX = 3,
|
||||||
|
GL_BACKUP_ERR_ADD_STORE = 4,
|
||||||
|
GL_BACKUP_ERR_ADD_LINKS = 5,
|
||||||
|
GL_BACKUP_ERR_CLOSE = 6
|
||||||
|
}
|
||||||
|
gl_backup_status_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_RESTORE_OK = 0,
|
||||||
|
GL_RESTORE_ERR_OPEN = 1,
|
||||||
|
GL_RESTORE_ERR_EXTRACT = 2
|
||||||
|
} gl_restore_status_t;
|
||||||
|
|
||||||
|
/*
|
||||||
|
* GL_INSTALL_OK: installation successful
|
||||||
|
* GL_INSTALL_ERR_OPEN: unable to open .gpkg file
|
||||||
|
* GL_INSTALL_ERR_MANIFEST: manifest is missing or unparseable
|
||||||
|
* GL_INSTALL_ERR_EXTRACT: unable to extract files
|
||||||
|
* GL_INSTALL_ERR_INDEX: unable to update index
|
||||||
|
* GL_INSTALL_ERR_ALREADY_INSTALLED: package already installed
|
||||||
|
*/
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_INSTALL_OK = 0,
|
||||||
|
GL_INSTALL_ERR_OPEN = 1,
|
||||||
|
GL_INSTALL_ERR_MANIFEST = 2,
|
||||||
|
GL_INSTALL_ERR_EXTRACT = 3,
|
||||||
|
GL_INSTALL_ERR_INDEX = 4,
|
||||||
|
GL_INSTALL_ERR_ALREADY_INSTALLED = 5,
|
||||||
|
GL_INSTALL_SKIPPED = 6,
|
||||||
|
}
|
||||||
|
gl_install_status_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_REMOVE_OK = 0,
|
||||||
|
GL_REMOVE_ERR_NOT_FOUND = 1,
|
||||||
|
GL_REMOVE_ERR_UNLINK = 2,
|
||||||
|
GL_REMOVE_ERR_INDEX = 3,
|
||||||
|
} gl_remove_status_t;
|
||||||
|
|
||||||
|
struct gpkg_entry {
|
||||||
|
char *repo;
|
||||||
|
char *pkg;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct gindex {
|
||||||
|
int uid;
|
||||||
|
int listed;
|
||||||
|
struct gpkg_entry *entries;
|
||||||
|
size_t count;
|
||||||
|
};
|
||||||
|
|
||||||
|
bool gl_usr_istore_exists(index_store_t index_or_store, int uid);
|
||||||
|
bool gl_sys_istore_exists(index_store_t index_or_store);
|
||||||
|
|
||||||
|
int gl_init_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(uid_t uid, const char *out_dir);
|
||||||
|
|
||||||
|
char *gl_find_pkg_repo(const char *pkg_name, uid_t uid);
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
char **repos;
|
||||||
|
size_t count;
|
||||||
|
}
|
||||||
|
gl_repo_list_t;
|
||||||
|
|
||||||
|
gl_repo_list_t gl_find_pkg_repos(const char *pkg_name, uid_t uid);
|
||||||
|
void gl_free_repo_list(gl_repo_list_t *list);
|
||||||
|
|
||||||
|
gl_backup_status_t gl_backup_istore(const char *tar_path, const char *glacier_root,
|
||||||
|
const char *uid);
|
||||||
|
gl_restore_status_t gl_restore_istore(const char *tar_path, const char *glacier_root, const char *uid);
|
||||||
|
|
||||||
|
gl_install_status_t gl_install_pkg(const char *gpkg_path, uid_t uid);
|
||||||
|
gl_remove_status_t gl_remove_pkg(const char *pkg_name, const char *pkg_repo, uid_t uid);
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#endif
|
||||||
203
src/sha256.c
Normal file
203
src/sha256.c
Normal file
@@ -0,0 +1,203 @@
|
|||||||
|
/*
|
||||||
|
* gl_sha256 — vendored SHA-256 (FIPS 180-4).
|
||||||
|
* See sha256.h for API rationale.
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
|
||||||
|
#include "sha256.h"
|
||||||
|
|
||||||
|
#define ROTRIGHT(a, b) (((a) >> (b)) | ((a) << (32 - (b))))
|
||||||
|
|
||||||
|
#define CH(x, y, z) (((x) & (y)) ^ (~(x) & (z)))
|
||||||
|
#define MAJ(x, y, z) (((x) & (y)) ^ ((x) & (z)) ^ ((y) & (z)))
|
||||||
|
#define EP0(x) (ROTRIGHT(x, 2) ^ ROTRIGHT(x, 13) ^ ROTRIGHT(x, 22))
|
||||||
|
#define EP1(x) (ROTRIGHT(x, 6) ^ ROTRIGHT(x, 11) ^ ROTRIGHT(x, 25))
|
||||||
|
#define SIG0(x) (ROTRIGHT(x, 7) ^ ROTRIGHT(x, 18) ^ ((x) >> 3))
|
||||||
|
#define SIG1(x) (ROTRIGHT(x, 17) ^ ROTRIGHT(x, 19) ^ ((x) >> 10))
|
||||||
|
|
||||||
|
static const uint32_t k[64] = {
|
||||||
|
0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5,
|
||||||
|
0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
|
||||||
|
0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3,
|
||||||
|
0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
|
||||||
|
0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc,
|
||||||
|
0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
|
||||||
|
0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7,
|
||||||
|
0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
|
||||||
|
0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13,
|
||||||
|
0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
|
||||||
|
0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3,
|
||||||
|
0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
|
||||||
|
0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5,
|
||||||
|
0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
|
||||||
|
0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208,
|
||||||
|
0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
|
||||||
|
};
|
||||||
|
|
||||||
|
static void
|
||||||
|
sha256_transform(gl_sha256_ctx_t *ctx, const uint8_t data[64])
|
||||||
|
{
|
||||||
|
uint32_t m[64];
|
||||||
|
uint32_t i, j;
|
||||||
|
|
||||||
|
for (i = 0, j = 0; i < 16; ++i, j += 4) {
|
||||||
|
m[i] = ((uint32_t)data[j] << 24) |
|
||||||
|
((uint32_t)data[j + 1] << 16) |
|
||||||
|
((uint32_t)data[j + 2] << 8) |
|
||||||
|
((uint32_t)data[j + 3]);
|
||||||
|
}
|
||||||
|
for (; i < 64; ++i) {
|
||||||
|
m[i] = SIG1(m[i - 2]) + m[i - 7] + SIG0(m[i - 15]) + m[i - 16];
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t a = ctx->state[0];
|
||||||
|
uint32_t b = ctx->state[1];
|
||||||
|
uint32_t c = ctx->state[2];
|
||||||
|
uint32_t d = ctx->state[3];
|
||||||
|
uint32_t e = ctx->state[4];
|
||||||
|
uint32_t f = ctx->state[5];
|
||||||
|
uint32_t g = ctx->state[6];
|
||||||
|
uint32_t h = ctx->state[7];
|
||||||
|
|
||||||
|
for (i = 0; i < 64; ++i) {
|
||||||
|
uint32_t t1 = h + EP1(e) + CH(e, f, g) + k[i] + m[i];
|
||||||
|
uint32_t t2 = EP0(a) + MAJ(a, b, c);
|
||||||
|
h = g;
|
||||||
|
g = f;
|
||||||
|
f = e;
|
||||||
|
e = d + t1;
|
||||||
|
d = c;
|
||||||
|
c = b;
|
||||||
|
b = a;
|
||||||
|
a = t1 + t2;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->state[0] += a;
|
||||||
|
ctx->state[1] += b;
|
||||||
|
ctx->state[2] += c;
|
||||||
|
ctx->state[3] += d;
|
||||||
|
ctx->state[4] += e;
|
||||||
|
ctx->state[5] += f;
|
||||||
|
ctx->state[6] += g;
|
||||||
|
ctx->state[7] += h;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
gl_sha256_init(gl_sha256_ctx_t *ctx)
|
||||||
|
{
|
||||||
|
ctx->datalen = 0;
|
||||||
|
ctx->bitlen = 0;
|
||||||
|
ctx->state[0] = 0x6a09e667;
|
||||||
|
ctx->state[1] = 0xbb67ae85;
|
||||||
|
ctx->state[2] = 0x3c6ef372;
|
||||||
|
ctx->state[3] = 0xa54ff53a;
|
||||||
|
ctx->state[4] = 0x510e527f;
|
||||||
|
ctx->state[5] = 0x9b05688c;
|
||||||
|
ctx->state[6] = 0x1f83d9ab;
|
||||||
|
ctx->state[7] = 0x5be0cd19;
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
gl_sha256_update(gl_sha256_ctx_t *ctx, const uint8_t *data, size_t len)
|
||||||
|
{
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
for (i = 0; i < len; ++i) {
|
||||||
|
ctx->data[ctx->datalen] = data[i];
|
||||||
|
ctx->datalen++;
|
||||||
|
if (ctx->datalen == 64) {
|
||||||
|
sha256_transform(ctx, ctx->data);
|
||||||
|
ctx->bitlen += 512;
|
||||||
|
ctx->datalen = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
gl_sha256_final(gl_sha256_ctx_t *ctx, uint8_t digest[GL_SHA256_DIGEST_SIZE])
|
||||||
|
{
|
||||||
|
size_t i = ctx->datalen;
|
||||||
|
|
||||||
|
if (ctx->datalen < 56) {
|
||||||
|
ctx->data[i++] = 0x80;
|
||||||
|
while (i < 56) {
|
||||||
|
ctx->data[i++] = 0x00;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ctx->data[i++] = 0x80;
|
||||||
|
while (i < 64) {
|
||||||
|
ctx->data[i++] = 0x00;
|
||||||
|
}
|
||||||
|
sha256_transform(ctx, ctx->data);
|
||||||
|
memset(ctx->data, 0, 56);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->bitlen += (uint64_t)ctx->datalen * 8;
|
||||||
|
ctx->data[63] = (uint8_t)(ctx->bitlen);
|
||||||
|
ctx->data[62] = (uint8_t)(ctx->bitlen >> 8);
|
||||||
|
ctx->data[61] = (uint8_t)(ctx->bitlen >> 16);
|
||||||
|
ctx->data[60] = (uint8_t)(ctx->bitlen >> 24);
|
||||||
|
ctx->data[59] = (uint8_t)(ctx->bitlen >> 32);
|
||||||
|
ctx->data[58] = (uint8_t)(ctx->bitlen >> 40);
|
||||||
|
ctx->data[57] = (uint8_t)(ctx->bitlen >> 48);
|
||||||
|
ctx->data[56] = (uint8_t)(ctx->bitlen >> 56);
|
||||||
|
sha256_transform(ctx, ctx->data);
|
||||||
|
|
||||||
|
for (i = 0; i < 4; ++i) {
|
||||||
|
uint32_t shift = 24 - (uint32_t)i * 8;
|
||||||
|
digest[i] = (uint8_t)((ctx->state[0] >> shift) & 0xffU);
|
||||||
|
digest[i + 4] = (uint8_t)((ctx->state[1] >> shift) & 0xffU);
|
||||||
|
digest[i + 8] = (uint8_t)((ctx->state[2] >> shift) & 0xffU);
|
||||||
|
digest[i + 12] = (uint8_t)((ctx->state[3] >> shift) & 0xffU);
|
||||||
|
digest[i + 16] = (uint8_t)((ctx->state[4] >> shift) & 0xffU);
|
||||||
|
digest[i + 20] = (uint8_t)((ctx->state[5] >> shift) & 0xffU);
|
||||||
|
digest[i + 24] = (uint8_t)((ctx->state[6] >> shift) & 0xffU);
|
||||||
|
digest[i + 28] = (uint8_t)((ctx->state[7] >> shift) & 0xffU);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void
|
||||||
|
gl_sha256_to_hex(const uint8_t digest[GL_SHA256_DIGEST_SIZE], char *out)
|
||||||
|
{
|
||||||
|
static const char hexchars[] = "0123456789abcdef";
|
||||||
|
size_t i;
|
||||||
|
|
||||||
|
for (i = 0; i < GL_SHA256_DIGEST_SIZE; i++) {
|
||||||
|
out[i * 2] = hexchars[(digest[i] >> 4) & 0x0fU];
|
||||||
|
out[i * 2 + 1] = hexchars[digest[i] & 0x0fU];
|
||||||
|
}
|
||||||
|
out[GL_SHA256_DIGEST_SIZE * 2] = '\0';
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
gl_sha256_file(const char *path, char *hex_out)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_sha256_ctx_t ctx;
|
||||||
|
gl_sha256_init(&ctx);
|
||||||
|
|
||||||
|
uint8_t buf[65536];
|
||||||
|
size_t n;
|
||||||
|
while ((n = fread(buf, 1, sizeof(buf), f)) > 0) {
|
||||||
|
gl_sha256_update(&ctx, buf, n);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ferror(f)) {
|
||||||
|
fclose(f);
|
||||||
|
errno = EIO;
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
|
||||||
|
uint8_t digest[GL_SHA256_DIGEST_SIZE];
|
||||||
|
gl_sha256_final(&ctx, digest);
|
||||||
|
gl_sha256_to_hex(digest, hex_out);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
26
src/sha256.h
Normal file
26
src/sha256.h
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
#ifndef GL_SHA256_H_
|
||||||
|
#define GL_SHA256_H_
|
||||||
|
|
||||||
|
#include <stddef.h>
|
||||||
|
#include <stdint.h>
|
||||||
|
|
||||||
|
#define GL_SHA256_DIGEST_SIZE 32
|
||||||
|
#define GL_SHA256_HEX_SIZE (GL_SHA256_DIGEST_SIZE * 2 + 1)
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uint32_t state[8];
|
||||||
|
uint64_t bitlen;
|
||||||
|
uint8_t data[64];
|
||||||
|
size_t datalen;
|
||||||
|
}
|
||||||
|
gl_sha256_ctx_t;
|
||||||
|
|
||||||
|
void gl_sha256_init(gl_sha256_ctx_t *ctx);
|
||||||
|
void gl_sha256_update(gl_sha256_ctx_t *ctx, const uint8_t *data, size_t len);
|
||||||
|
void gl_sha256_final(gl_sha256_ctx_t *ctx, uint8_t digest[GL_SHA256_DIGEST_SIZE]);
|
||||||
|
|
||||||
|
void gl_sha256_to_hex(const uint8_t digest[GL_SHA256_DIGEST_SIZE], char *out);
|
||||||
|
|
||||||
|
int gl_sha256_file(const char *path, char *hex_out);
|
||||||
|
|
||||||
|
#endif
|
||||||
420
src/transaction.c
Normal file
420
src/transaction.c
Normal file
@@ -0,0 +1,420 @@
|
|||||||
|
#include <linux/limits.h>
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include <errno.h>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdio.h>
|
||||||
|
#include <string.h>
|
||||||
|
#include <sys/file.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include "common.h"
|
||||||
|
#include "istoreutils.h"
|
||||||
|
#include "log.h"
|
||||||
|
#include "transaction.h"
|
||||||
|
|
||||||
|
#define GL_STAGE_BASE "/glacier/usr/stage"
|
||||||
|
#define GL_LOCK_BASE "/glacier/usr/lock"
|
||||||
|
#define GL_OLD_BASE "/glacier/usr/old"
|
||||||
|
|
||||||
|
#define GL_USR_INDEX "/glacier/usr/index"
|
||||||
|
#define GL_USR_STORE "/glacier/usr/store"
|
||||||
|
#define GL_USR_LINKS "/glacier/usr/links"
|
||||||
|
|
||||||
|
#define GL_SYS_INDEX "/glacier/sys/index"
|
||||||
|
#define GL_SYS_STORE "/glacier/sys/store"
|
||||||
|
#define GL_SYS_LINKS "/glacier/sys/links"
|
||||||
|
|
||||||
|
static int
|
||||||
|
fsync_dir(const char *path)
|
||||||
|
{
|
||||||
|
int fd = open(path, O_RDONLY | O_DIRECTORY);
|
||||||
|
if (fd < 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
int r = fsync(fd);
|
||||||
|
close(fd);
|
||||||
|
return r;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
atomic_swap(const char *live, const char *stage, const char *backup)
|
||||||
|
{
|
||||||
|
if (rename(live, backup) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rename(stage, live) != 0) {
|
||||||
|
rename(backup, live);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
static void
|
||||||
|
derive_base_paths(gl_ctx_scope_t scope, uid_t uid, char *index_out,
|
||||||
|
char *store_out, char *links_out)
|
||||||
|
{
|
||||||
|
switch (scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
snprintf(index_out, PATH_MAX, "/glacier/usr/index/%u", uid);
|
||||||
|
snprintf(store_out, PATH_MAX, "/glacier/usr/store/%u", uid);
|
||||||
|
snprintf(links_out, PATH_MAX, "/glacier/usr/links/%u", uid);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
strncpy(index_out, "/glacier/sys/index", PATH_MAX - 1);
|
||||||
|
strncpy(store_out, "/glacier/sys/store", PATH_MAX - 1);
|
||||||
|
/* This is the DEFAULT system-scope links location — for
|
||||||
|
* packages the operator explicitly installs under system
|
||||||
|
* scope that aren't part of the base system. Base-repo
|
||||||
|
* packages get routed to /usr instead, but that's a
|
||||||
|
* per-package decision made in gl_link_pkg/gl_relink_store
|
||||||
|
* (which know a package's repo), not something the context
|
||||||
|
* can decide up front. See GL_BASE_REPO in istoreutils.c. */
|
||||||
|
strncpy(links_out, "/glacier/sys/links", PATH_MAX - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
check_same_filesystem(const char *a, const char *b)
|
||||||
|
{
|
||||||
|
struct stat sa, sb;
|
||||||
|
if (stat(a, &sa) != 0 || stat(b, &sb) != 0) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return sa.st_dev == sb.st_dev;
|
||||||
|
}
|
||||||
|
|
||||||
|
static int
|
||||||
|
hardlink_tree(const char *src, const char *dest)
|
||||||
|
{
|
||||||
|
DIR *dir = opendir(src);
|
||||||
|
if (!dir) {
|
||||||
|
return (errno == ENOENT) ? 0 : -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
struct dirent *ent;
|
||||||
|
while ((ent = readdir(dir))) {
|
||||||
|
if (ent->d_name[0] == '.') { continue; }
|
||||||
|
|
||||||
|
char s[PATH_MAX], d[PATH_MAX];
|
||||||
|
snprintf(s, sizeof(s), "%s/%s", src, ent->d_name);
|
||||||
|
snprintf(d, sizeof(d), "%s/%s", dest, ent->d_name);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if(lstat(s, &st) != 0) { continue; }
|
||||||
|
|
||||||
|
if (S_ISDIR(st.st_mode)) {
|
||||||
|
if (mkdir(d, st.st_mode & 0777) != 0 && errno != EEXIST) {
|
||||||
|
closedir(dir);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (hardlink_tree(s, d) != 0) {
|
||||||
|
closedir(dir);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (S_ISLNK(st.st_mode)) {
|
||||||
|
char target[PATH_MAX];
|
||||||
|
ssize_t n = readlink(s, target, sizeof(target) - 1);
|
||||||
|
if (n < 0) { continue; }
|
||||||
|
target[n] = '\0';
|
||||||
|
symlink(target, d);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
if (link(s, d) != 0 && errno != EEXIST) {
|
||||||
|
closedir(dir);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
closedir(dir);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
int
|
||||||
|
gl_init_live_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
||||||
|
{
|
||||||
|
if (!ctx) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->uid = uid;
|
||||||
|
ctx->mode = GL_CTX_LIVE;
|
||||||
|
ctx->scope = scope;
|
||||||
|
ctx->lock_fd = -1;
|
||||||
|
|
||||||
|
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
|
||||||
|
derive_base_paths(scope, uid, ctx->index_path, ctx->store_path,
|
||||||
|
ctx->links_path);
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_txn_status_t
|
||||||
|
gl_init_stage_context(gl_context_t *ctx, gl_ctx_scope_t scope, uid_t uid)
|
||||||
|
{
|
||||||
|
if (!ctx) {
|
||||||
|
return GL_TXN_ERR_NO_TXN;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->uid = uid;
|
||||||
|
ctx->mode = GL_CTX_STAGE;
|
||||||
|
ctx->scope = scope;
|
||||||
|
ctx->lock_fd = -1;
|
||||||
|
|
||||||
|
strncpy(ctx->root_path, "/glacier", PATH_MAX - 1);
|
||||||
|
|
||||||
|
char stage_base[PATH_MAX - 7];
|
||||||
|
|
||||||
|
switch (scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
snprintf(stage_base, sizeof(stage_base), "/glacier/usr/stage/%u", uid);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
strncpy(stage_base, "/glacier/sys/stage", sizeof(stage_base));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
snprintf(ctx->index_path, PATH_MAX, "%s/index", stage_base);
|
||||||
|
snprintf(ctx->store_path, PATH_MAX, "%s/store", stage_base);
|
||||||
|
snprintf(ctx->links_path, PATH_MAX, "%s/links", stage_base);
|
||||||
|
|
||||||
|
struct stat st;
|
||||||
|
if (stat(stage_base, &st) == 0) {
|
||||||
|
if (gl_rmdir_recursive(stage_base) != 0) {
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl_mkdirp(GL_LOCK_BASE, 0700) != 0 && errno != EEXIST) {
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_txn_status_t lock_status = gl_begin_transaction(ctx);
|
||||||
|
if (lock_status != GL_TXN_OK) {
|
||||||
|
return lock_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl_mkdirp(ctx->index_path, 0700) != 0) {
|
||||||
|
gl_abort_transaction(ctx);
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl_mkdirp(ctx->store_path, 0700) != 0) {
|
||||||
|
gl_abort_transaction(ctx);
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl_mkdirp(ctx->links_path, 0700) != 0) {
|
||||||
|
gl_abort_transaction(ctx);
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chown(stage_base, uid, (gid_t)-1) != 0) {
|
||||||
|
gl_abort_transaction(ctx);
|
||||||
|
return GL_TXN_ERR_PERM;
|
||||||
|
}
|
||||||
|
|
||||||
|
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
|
||||||
|
derive_base_paths(scope, uid, live_index, live_store, live_links);
|
||||||
|
/* live_links is populated (derive_base_paths always fills all
|
||||||
|
* three) but intentionally unused below — see the comment on the
|
||||||
|
* hardlink_tree call. */
|
||||||
|
|
||||||
|
/* Only index and store need copy-on-write seeding — the staged
|
||||||
|
* links tree is never actually read from or written to (gl_link_pkg
|
||||||
|
* skips GL_CTX_STAGE entirely; gl_relink_store always builds its
|
||||||
|
* own fresh live context rather than touching ctx->links_path of a
|
||||||
|
* stage context). Seeding it was always wasted work; now that
|
||||||
|
* live_links can be /usr for system scope, it would also be
|
||||||
|
* actively dangerous — hardlinking the entire /usr tree on every
|
||||||
|
* transaction, and risking EXDEV outright if /usr and the stage
|
||||||
|
* area are on different filesystems. */
|
||||||
|
if (hardlink_tree(live_index, ctx->index_path) != 0 ||
|
||||||
|
hardlink_tree(live_store, ctx->store_path) != 0) {
|
||||||
|
gl_abort_transaction(ctx);
|
||||||
|
return GL_TXN_ERR_MKDIR;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GL_TXN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_txn_status_t
|
||||||
|
gl_begin_transaction(gl_context_t *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx) {
|
||||||
|
return GL_TXN_ERR_NO_TXN;
|
||||||
|
}
|
||||||
|
|
||||||
|
char lock_path[PATH_MAX];
|
||||||
|
switch (ctx->scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
snprintf(lock_path, PATH_MAX, "%s/%u.lock", GL_LOCK_BASE,
|
||||||
|
ctx->uid);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
snprintf(lock_path, PATH_MAX, "%s/sys.lock", GL_LOCK_BASE);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int fd = open(lock_path, O_WRONLY | O_CREAT, 0600);
|
||||||
|
if (fd < 0) {
|
||||||
|
return GL_TXN_ERR_LOCK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (flock(fd, LOCK_EX | LOCK_NB) != 0) {
|
||||||
|
close(fd);
|
||||||
|
return GL_TXN_ERR_LOCK;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx->lock_fd = fd;
|
||||||
|
return GL_TXN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_txn_status_t
|
||||||
|
gl_commit_transaction(gl_context_t *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx || ctx->lock_fd < 0) {
|
||||||
|
return GL_TXN_ERR_NO_TXN;
|
||||||
|
}
|
||||||
|
if (ctx->mode != GL_CTX_STAGE) {
|
||||||
|
return GL_TXN_ERR_NO_TXN;
|
||||||
|
}
|
||||||
|
|
||||||
|
char stage_base[PATH_MAX];
|
||||||
|
switch (ctx->scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
snprintf(stage_base, PATH_MAX, "/glacier/usr/stage/%u",
|
||||||
|
ctx->uid);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
strncpy(stage_base, "/glacier/sys/stage", PATH_MAX - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
int same = check_same_filesystem(ctx->root_path, stage_base);
|
||||||
|
if (same < 0) {
|
||||||
|
return GL_TXN_ERR_RENAME; /* failed to stat either side */
|
||||||
|
}
|
||||||
|
if (!same) {
|
||||||
|
return GL_TXN_ERR_XDEV;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Derive live paths the same way gl_init_live_context would */
|
||||||
|
char live_index[PATH_MAX], live_store[PATH_MAX], live_links[PATH_MAX];
|
||||||
|
derive_base_paths(ctx->scope, ctx->uid, live_index, live_store,
|
||||||
|
live_links);
|
||||||
|
|
||||||
|
/* Derive old/backup paths */
|
||||||
|
char old_base[PATH_MAX];
|
||||||
|
switch (ctx->scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
snprintf(old_base, PATH_MAX, "/glacier/usr/old/%u", ctx->uid);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
strncpy(old_base, "/glacier/sys/old", PATH_MAX - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
char old_index[PATH_MAX], old_store[PATH_MAX], old_links[PATH_MAX];
|
||||||
|
snprintf(old_index, PATH_MAX, "%s/index", old_base);
|
||||||
|
snprintf(old_store, PATH_MAX, "%s/store", old_base);
|
||||||
|
snprintf(old_links, PATH_MAX, "%s/links", old_base);
|
||||||
|
|
||||||
|
struct stat old_st;
|
||||||
|
if (stat(old_base, &old_st) == 0) {
|
||||||
|
if (gl_rmdir_recursive(old_base) != 0) {
|
||||||
|
return GL_TXN_ERR_RENAME;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (gl_mkdirp(old_base, 0700) != 0) {
|
||||||
|
return GL_TXN_ERR_RENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (atomic_swap(live_index, ctx->index_path, old_index) != 0) {
|
||||||
|
return GL_TXN_ERR_RENAME;
|
||||||
|
}
|
||||||
|
if (atomic_swap(live_store, ctx->store_path, old_store) != 0) {
|
||||||
|
rename(live_index, ctx->index_path);
|
||||||
|
rename(old_index, live_index);
|
||||||
|
return GL_TXN_ERR_RENAME;
|
||||||
|
}
|
||||||
|
if (atomic_swap(live_links, ctx->links_path, old_links) != 0) {
|
||||||
|
rename(live_store, ctx->store_path);
|
||||||
|
rename(old_store, live_store);
|
||||||
|
rename(live_index, ctx->index_path);
|
||||||
|
rename(old_index, live_index);
|
||||||
|
return GL_TXN_ERR_RENAME;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* fsync the parent directories, not the uid subdirs */
|
||||||
|
switch (ctx->scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
fsync_dir("/glacier/usr/index");
|
||||||
|
fsync_dir("/glacier/usr/store");
|
||||||
|
fsync_dir("/glacier/usr/links");
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
fsync_dir("/glacier/sys/index");
|
||||||
|
fsync_dir("/glacier/sys/store");
|
||||||
|
fsync_dir("/glacier/sys/links");
|
||||||
|
fsync_dir("/usr"); /* base-repo packages land here instead */
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_context_t live_ctx;
|
||||||
|
gl_init_live_context(&live_ctx, ctx->scope, ctx->uid);
|
||||||
|
|
||||||
|
if (gl_relink_store(&live_ctx) != 0) {
|
||||||
|
lg_printf(2, "Post-commit relink incomplete for uid %u", ctx->uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_rmdir_recursive(old_base);
|
||||||
|
|
||||||
|
/* Remove stage base */
|
||||||
|
gl_rmdir_recursive(stage_base);
|
||||||
|
|
||||||
|
close(ctx->lock_fd);
|
||||||
|
ctx->lock_fd = -1;
|
||||||
|
|
||||||
|
return GL_TXN_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_txn_status_t
|
||||||
|
gl_abort_transaction(gl_context_t *ctx)
|
||||||
|
{
|
||||||
|
if (!ctx) {
|
||||||
|
return GL_TXN_ERR_NO_TXN;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Remove stage area — live is untouched */
|
||||||
|
char stage_uid[PATH_MAX];
|
||||||
|
switch (ctx->scope) {
|
||||||
|
case GL_SCOPE_USR:
|
||||||
|
snprintf(stage_uid, PATH_MAX, "/glacier/usr/stage/%u",
|
||||||
|
ctx->uid);
|
||||||
|
break;
|
||||||
|
case GL_SCOPE_SYS:
|
||||||
|
strncpy(stage_uid, "/glacier/sys/stage", PATH_MAX - 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
gl_rmdir_recursive(stage_uid); /* best-effort; ignore errors */
|
||||||
|
|
||||||
|
/* Release lock */
|
||||||
|
|
||||||
|
if (ctx->lock_fd >= 0) {
|
||||||
|
close(ctx->lock_fd);
|
||||||
|
ctx->lock_fd = -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return GL_TXN_OK;
|
||||||
|
}
|
||||||
55
src/transaction.h
Normal file
55
src/transaction.h
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
#ifndef TRANSACTION_H_
|
||||||
|
#define TRANSACTION_H_
|
||||||
|
|
||||||
|
#define _POSIX_C_SOURCE 200809L
|
||||||
|
|
||||||
|
#include <limits.h>
|
||||||
|
#include <stdbool.h>
|
||||||
|
#include <sys/types.h>
|
||||||
|
|
||||||
|
#define GL_UID_NONE ((uid_t)0)
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_CTX_LIVE,
|
||||||
|
GL_CTX_STAGE
|
||||||
|
} gl_ctx_mode_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_SCOPE_USR,
|
||||||
|
GL_SCOPE_SYS
|
||||||
|
} gl_ctx_scope_t;
|
||||||
|
|
||||||
|
typedef struct {
|
||||||
|
uid_t uid;
|
||||||
|
|
||||||
|
char root_path[PATH_MAX];
|
||||||
|
char index_path[PATH_MAX];
|
||||||
|
char store_path[PATH_MAX];
|
||||||
|
char links_path[PATH_MAX];
|
||||||
|
|
||||||
|
gl_ctx_mode_t mode;
|
||||||
|
gl_ctx_scope_t scope;
|
||||||
|
|
||||||
|
int lock_fd;
|
||||||
|
} gl_context_t;
|
||||||
|
|
||||||
|
typedef enum {
|
||||||
|
GL_TXN_OK = 0, /* success */
|
||||||
|
GL_TXN_ERR_LOCK = 1, /* unable to acquire lock */
|
||||||
|
GL_TXN_ERR_MKDIR = 2, /* unable to create txn stage */
|
||||||
|
GL_TXN_ERR_PERM = 3, /* permission error */
|
||||||
|
GL_TXN_ERR_RENAME = 4, /* atomic swap failed */
|
||||||
|
GL_TXN_ERR_SYNC = 5, /* fsync failed */
|
||||||
|
GL_TXN_ERR_CLEANUP = 6, /* cleanup failed */
|
||||||
|
GL_TXN_ERR_NO_TXN = 7, /* aborted without beginning */
|
||||||
|
GL_TXN_ERR_XDEV = 8 /* stage and live on different filesystems */
|
||||||
|
} gl_txn_status_t;
|
||||||
|
|
||||||
|
int gl_init_live_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_begin_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);
|
||||||
|
|
||||||
|
#endif
|
||||||
Reference in New Issue
Block a user