From e404e2e08d68de9dfacf677fa64cac664b858336 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 1 Apr 2026 11:46:23 +0200 Subject: [PATCH] Prototype FreeBSD store management --- docs/PROGRESS.md | 107 ++++++++ .../reports/phase2-freebsd-store-prototype.md | 189 ++++++++++++++ tests/store/run-freebsd-store-prototype.sh | 234 ++++++++++++++++++ 3 files changed, 530 insertions(+) create mode 100644 docs/reports/phase2-freebsd-store-prototype.md create mode 100755 tests/store/run-freebsd-store-prototype.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index cb552b2..5c7f9fa 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -972,3 +972,110 @@ Next recommended step: 2. if possible, use outputs or dependency relationships realistic enough to model how a future FreeBSD Guix daemon would retain referenced store items 3. continue carrying the separate Guix checkout runtime blocker: - investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version` + +## 2026-04-01 — Phase 2.3 completed: `/frx/store` prototype validated on FreeBSD + +Completed work: + +- added a runnable `/frx/store` prototype harness: + - `tests/store/run-freebsd-store-prototype.sh` +- wrote the Phase 2.3 report: + - `docs/reports/phase2-freebsd-store-prototype.md` +- created and exercised the operator-requested `/frx` layout on-host: + - `/frx/store` + - `/frx/var` + - `/frx/etc` + - `/frx/var/fruix/gcroots` +- created a store group for the prototype path: + - `fruixbuild` +- ran the store prototype successfully and captured metadata under: + - `/tmp/freebsd-store-prototype-metadata.txt` + +Important findings: + +- the current host now has a working `/frx/store` prototype owned as: + - `root:fruixbuild` + with mode: + - `drwxrwxr-t` +- the prototype successfully created content-addressed demo store items under `/frx/store` using hash-based names +- the demo item set included: + - rooted greeting data + - a rooted app referencing that data through an absolute store path + - an unrooted orphan item intended for collection +- an unprivileged user (`nobody`) could: + - read store data + - execute the demo app from the store +- the same unprivileged user could not: + - create files directly in `/frx/store` + and the observed failure was: + - `Permission denied` +- the prototype GC logic followed rooted references successfully: + - with a GC root present, the app and its referenced data survived while the orphan item was collected + - after removing the GC root, the remaining demo items were collected as well +- the demo store returned to an empty state after the second GC pass, so the host is left with the `/frx` skeleton but without lingering prototype payloads + +Current assessment: + +- Phase 2.3 is now satisfied on the current FreeBSD prototype track +- the core store assumptions needed for a FreeBSD Guix-daemon design have practical validation now: + - `/frx/store` path viability + - root-controlled mutation + - unprivileged read access + - immutable absolute store references + - root-managed GC roots and mark/sweep retention behavior +- remaining gaps are now above this architectural layer rather than below it: + - real derivation registration + - SQLite-backed store metadata + - daemon RPC integration + - actual package lowering/build submission using these mechanisms + +## 2026-04-01 — Phase 2 completed on the current FreeBSD prototype track + +Phase 2 is now considered complete for the active FreeBSD amd64 prototype path. + +Why this milestone is satisfied: + +- **Phase 2.1** success criteria were met: + - a detailed jail-first build-isolation design was produced + - a runnable prototype successfully executed a build command in a restricted FreeBSD jail +- **Phase 2.2** success criteria were met: + - a concrete C privilege-dropping implementation was added + - build-user credential drop, inability to regain root, and concurrent cross-build isolation were demonstrated +- **Phase 2.3** success criteria were met on the prototype track: + - a working `/frx/store` equivalent was established + - content-addressed demo store items were created and consumed + - unprivileged read vs. privileged write behavior was validated + - garbage-collection behavior over rooted references was demonstrated + +Important scope note: + +- this completes the **core daemon architecture adaptation** milestone, not a full Guix-daemon port +- the separate real-checkout blocker from Phase 1 remains relevant for later integration work: + - `./pre-inst-env guix --version` still fails with `Wrong type to apply: #` +- however, that runtime issue no longer blocks the specific Phase 2 architectural deliverables because the jail, privilege, and store assumptions have now been validated independently on FreeBSD + +Recent commits: + +- `e380e88` — `Add FreeBSD Guile verification harness` +- `cd721b1` — `Update progress after Guile verification` +- `27916cb` — `Diagnose Guile subprocess crash on FreeBSD` +- `02f7a7f` — `Validate local Guile fix on FreeBSD` +- `4aebea4` — `Add native GNU Hello FreeBSD build harness` +- `c944cdb` — `Validate Guix builder phases on FreeBSD` +- `0a2e48e` — `Validate GNU which builder phases on FreeBSD` +- `245a47d` — `Document gaps to real Guix FreeBSD builds` +- `d62e9b0` — `Investigate Guix derivation generation on FreeBSD` +- `c0a85ed` — `Build local Guile-GnuTLS on FreeBSD` +- `15b9037` — `Build local Guile-Git on FreeBSD` +- `47d31e8` — `Build local Guile-JSON on FreeBSD` +- `d82195b` — `Advance Guix checkout on FreeBSD` +- `9bf3d30` — `Document FreeBSD syscall mapping` +- `7621798` — `Prototype FreeBSD jail build isolation` +- `d65b2af` — `Prototype FreeBSD build user isolation` + +Next recommended step: + +1. begin Phase 3.1 by adapting Guix build-system expectations to the now-validated jail/privilege/store model on FreeBSD +2. carry forward the concrete real-checkout runtime blocker for later integration work: + - investigate the `leave-on-EPIPE` failure in `./pre-inst-env guix --version` +3. continue using `/frx/store` rather than `/gnu/store` for FreeBSD store experiments diff --git a/docs/reports/phase2-freebsd-store-prototype.md b/docs/reports/phase2-freebsd-store-prototype.md new file mode 100644 index 0000000..b3a6194 --- /dev/null +++ b/docs/reports/phase2-freebsd-store-prototype.md @@ -0,0 +1,189 @@ +# Phase 2.3: `/frx/store` prototype for FreeBSD + +Date: 2026-04-01 + +## Summary + +This step establishes a working FreeBSD prototype for the Guix store concept under the operator-requested `/frx` prefix. + +Added file: + +- `tests/store/run-freebsd-store-prototype.sh` + +The prototype creates and validates: + +- `/frx/store` +- `/frx/var` +- `/frx/etc` +- `/frx/var/fruix/gcroots` + +It then installs a few content-addressed demo store items, verifies that an unprivileged user can read and execute them but cannot write to the store, and exercises a simple reference-following garbage-collection pass. + +## Run command + +```sh +METADATA_OUT=/tmp/freebsd-store-prototype-metadata.txt \ +./tests/store/run-freebsd-store-prototype.sh +``` + +## Store layout and permission model + +Observed store metadata: + +- store root: `/frx/store` +- store state root: `/frx/var/fruix` +- gc roots: `/frx/var/fruix/gcroots` +- store group: `fruixbuild` +- observed store root mode: `drwxrwxr-t` +- observed store root ownership: `root:fruixbuild` + +This matches the intended high-level Guix model closely enough for the prototype track: + +- daemon/root can create and remove store items +- ordinary users can traverse and read store items +- ordinary users cannot write new store entries directly + +## Content-addressed demo items + +The prototype creates three demo store items using hashes derived from explicit manifests: + +1. **greeting data** + - path shape: `/frx/store/-demo-greeting-data` + - contains `share/greeting.txt` + +2. **hello app** + - path shape: `/frx/store/-demo-hello-app` + - contains `bin/demo-hello` + - references the greeting-data store path through an absolute store reference + +3. **orphan data** + - path shape: `/frx/store/-demo-orphan-data` + - intentionally left unrooted so GC should collect it + +Each store item also carries prototype metadata files: + +- `.fruix-demo` +- `.references` + +The `.references` file is used by the prototype GC pass to retain transitive dependencies. + +## Readability and write protection checks + +The prototype validates store access as the unprivileged `nobody` user. + +Observed successful unprivileged reads/execution: + +```text +hello-from-frx-store +hello-from-frx-store +``` + +Those two lines come from: + +- directly reading the data file +- executing the app script, which reads the referenced data store path + +Observed failed unprivileged write attempt: + +```text +touch: /frx/store/should-not-write: Permission denied +``` + +This demonstrates the critical store property needed for later daemon work: + +- store contents are consumable by unprivileged users +- store mutation remains daemon/root controlled + +## Garbage-collection prototype + +### Rooted GC pass + +The prototype creates a GC root symlink: + +- `/frx/var/fruix/gcroots/demo-root` -> `demo-hello-app` + +The GC algorithm then: + +1. walks GC roots +2. marks the rooted item reachable +3. follows `.references` transitively +4. removes demo items not in the reachable set + +Observed store listing after the rooted GC pass: + +```text +/frx/store/5aa68380f937120deaf9befe3390a563a3631672e2ab5d19b5498e2c288d7277-demo-hello-app +/frx/store/d182f3489191dae9d4b1768e0e7afbfae0143b6d08750a058589437ccd5e0af3-demo-greeting-data +``` + +Result: + +- rooted app retained +- referenced greeting data retained +- orphan data removed + +### Unrooted GC pass + +The prototype then removes the GC root and reruns the same mark-and-sweep logic. + +Observed store listing after the second GC pass: + +```text + +``` + +Result: + +- app removed +- transitive dependency removed +- demo store returned to an empty state + +## What this demonstrates for the FreeBSD port + +### 1. `/frx/store` works as a practical stand-in for `/gnu/store` + +The operator-requested `/frx` prefix is now exercised as a real on-host store root with: + +- correct directory creation +- permissions resembling Guix store expectations +- content-addressed path naming +- shared read access +- root-controlled mutation + +### 2. Store references can be modeled directly on FreeBSD + +The `demo-hello-app` item refers to the exact absolute path of its dependency under `/frx/store`, showing that the usual Guix model of immutable absolute store references maps cleanly onto FreeBSD path semantics. + +### 3. Garbage collection can be expressed with ordinary FreeBSD filesystem primitives + +No Linux-specific kernel mechanism was required for the prototype GC behavior. A root/daemon process can manage roots, references, and deletion using normal filesystem traversal and metadata files. + +## Limitations versus real Guix + +This is still a prototype, not full Guix store integration. It does **not** yet provide: + +- real derivation registration +- SQLite-backed store metadata +- daemon RPC interfaces +- narinfo/substitute handling +- signature verification +- graft handling +- real package database integration + +However, it does validate the central FreeBSD-side store assumptions needed before deeper integration: + +- `/frx/store` can exist with an appropriate permission model +- absolute immutable store paths are viable +- root-managed GC roots and dependency retention are viable +- unprivileged consumers can read/execute store content without being able to mutate it + +## Conclusion + +Phase 2.3 is satisfied on the current prototype track: + +- a working `/frx/store` equivalent now exists on the host +- content-addressed demo store items were created successfully +- unprivileged readability and write protection were validated +- reference-preserving garbage collection was demonstrated + +With Phase 2.1, 2.2, and 2.3 now all satisfied on the prototype track, the project has completed the core FreeBSD Guix-daemon architecture adaptation milestone needed before moving on to build-system adaptation work. diff --git a/tests/store/run-freebsd-store-prototype.sh b/tests/store/run-freebsd-store-prototype.sh new file mode 100755 index 0000000..d740fed --- /dev/null +++ b/tests/store/run-freebsd-store-prototype.sh @@ -0,0 +1,234 @@ +#!/bin/sh +set -eu + +store_root=${STORE_ROOT:-/frx/store} +state_root=${STATE_ROOT:-/frx/var} +sysconf_root=${SYSCONF_ROOT:-/frx/etc} +fruix_state_dir=$state_root/fruix +fruix_gcroots_dir=$fruix_state_dir/gcroots +store_group=${STORE_GROUP:-fruixbuild} +workdir=${WORKDIR:-$(mktemp -d /tmp/fruix-store-prototype.XXXXXX)} +cleanup_workdir=1 +metadata_file=$workdir/freebsd-store-prototype-metadata.txt +access_read_out=$workdir/access-read.out +access_write_err=$workdir/access-write.err +first_gc_listing=$workdir/first-gc-listing.txt +second_gc_listing=$workdir/second-gc-listing.txt +store_root_stat=$workdir/store-root.stat +gcroots_stat=$workdir/gcroots.stat + +if [ -n "${WORKDIR:-}" ]; then + cleanup_workdir=0 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup_workdir=0 +fi + +cleanup() { + set +e + if [ "$cleanup_workdir" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup EXIT INT TERM + +ensure_store_group() { + if ! pw groupshow "$store_group" >/dev/null 2>&1; then + if ! sudo pw groupadd "$store_group" -g 35000 >/dev/null 2>&1; then + sudo pw groupadd "$store_group" >/dev/null + fi + group_created=yes + else + group_created=no + fi + group_gid=$(pw groupshow "$store_group" | awk -F: '{print $3}') +} + +clean_demo_items() { + sudo find "$store_root" -mindepth 2 -maxdepth 2 -name .fruix-demo -print 2>/dev/null | + while IFS= read -r marker; do + [ -n "$marker" ] || continue + sudo rm -rf "$(dirname "$marker")" + done + sudo rm -f "$fruix_gcroots_dir/demo-root" +} + +install_store_item() { + item_name=$1 + stage_dir=$2 + item_hash=$3 + destination=$store_root/${item_hash}-${item_name} + + sudo rm -rf "$destination" + sudo mkdir -p "$destination" + sudo cp -a "$stage_dir/." "$destination/" + sudo find "$destination" -type d -exec chmod 0555 {} + + sudo find "$destination" -type f -exec chmod 0444 {} + + if [ -d "$destination/bin" ]; then + sudo find "$destination/bin" -type f -exec chmod 0555 {} + + fi + printf '%s\n' "$destination" +} + +mark_reachable() { + item=$1 + [ -n "$item" ] || return 0 + [ -d "$item" ] || return 0 + if grep -Fxq "$item" "$reachable_file" 2>/dev/null; then + return 0 + fi + printf '%s\n' "$item" >> "$reachable_file" + if [ -f "$item/.references" ]; then + while IFS= read -r ref; do + [ -n "$ref" ] || continue + mark_reachable "$ref" + done < "$item/.references" + fi +} + +run_gc() { + reachable_file=$1 + : > "$reachable_file" + if [ -d "$fruix_gcroots_dir" ]; then + for root in "$fruix_gcroots_dir"/*; do + [ -L "$root" ] || continue + mark_reachable "$(readlink -f "$root")" + done + fi + + sudo find "$store_root" -mindepth 2 -maxdepth 2 -name .fruix-demo -print | + while IFS= read -r marker; do + item=$(dirname "$marker") + if ! grep -Fxq "$item" "$reachable_file" 2>/dev/null; then + sudo rm -rf "$item" + fi + done +} + +mkdir -p "$workdir/stage" +ensure_store_group + +sudo install -d -m 0755 -o root -g wheel /frx +sudo install -d -m 1775 -o root -g "$store_group" "$store_root" +sudo install -d -m 0755 -o root -g wheel "$state_root" "$sysconf_root" "$fruix_state_dir" "$fruix_gcroots_dir" + +clean_demo_items +sudo stat -f '%Su %Sg %OLp %Sp' "$store_root" > "$store_root_stat" +sudo stat -f '%Su %Sg %OLp %Sp' "$fruix_gcroots_dir" > "$gcroots_stat" + +data_stage=$workdir/stage/greeting-data +app_stage=$workdir/stage/hello-app +orphan_stage=$workdir/stage/orphan-data +mkdir -p "$data_stage/share" "$app_stage/bin" "$orphan_stage/share" + +printf 'hello-from-frx-store\n' > "$data_stage/share/greeting.txt" +printf 'demo-item\n' > "$data_stage/.fruix-demo" +: > "$data_stage/.references" +data_manifest=$(printf 'name=demo-greeting-data\nfile=share/greeting.txt\ncontent=%s\n' "hello-from-frx-store") +data_hash=$(printf '%s' "$data_manifest" | sha256 -q) +data_path=$(install_store_item demo-greeting-data "$data_stage" "$data_hash") + +cat > "$app_stage/bin/demo-hello" < "$app_stage/.references" +printf 'demo-item\n' > "$app_stage/.fruix-demo" +app_manifest=$(printf 'name=demo-hello-app\nfile=bin/demo-hello\nref=%s\n' "$data_path") +app_hash=$(printf '%s' "$app_manifest" | sha256 -q) +app_path=$(install_store_item demo-hello-app "$app_stage" "$app_hash") + +printf 'this-should-be-collected\n' > "$orphan_stage/share/orphan.txt" +: > "$orphan_stage/.references" +printf 'demo-item\n' > "$orphan_stage/.fruix-demo" +orphan_manifest=$(printf 'name=demo-orphan-data\nfile=share/orphan.txt\ncontent=%s\n' "this-should-be-collected") +orphan_hash=$(printf '%s' "$orphan_manifest" | sha256 -q) +orphan_path=$(install_store_item demo-orphan-data "$orphan_stage" "$orphan_hash") + +sudo ln -sfn "$app_path" "$fruix_gcroots_dir/demo-root" + +sudo -u nobody /bin/sh -eu -c " + cat '$data_path/share/greeting.txt' + '$app_path/bin/demo-hello' +" > "$access_read_out" + +set +e +sudo -u nobody /bin/sh -eu -c "touch '$store_root/should-not-write'" >/dev/null 2> "$access_write_err" +write_rc=$? +set -e +if [ "$write_rc" -eq 0 ]; then + echo "unexpected unprivileged store write succeeded" >&2 + exit 1 +fi + +reachable_after_first_gc=$workdir/reachable-first.txt +run_gc "$reachable_after_first_gc" +find "$store_root" -maxdepth 1 -mindepth 1 | sort > "$first_gc_listing" + +if [ -d "$orphan_path" ]; then + echo "orphan store item survived rooted GC unexpectedly" >&2 + exit 1 +fi +if [ ! -d "$app_path" ] || [ ! -d "$data_path" ]; then + echo "reachable store items missing after rooted GC" >&2 + exit 1 +fi + +sudo rm -f "$fruix_gcroots_dir/demo-root" +reachable_after_second_gc=$workdir/reachable-second.txt +run_gc "$reachable_after_second_gc" +find "$store_root" -maxdepth 1 -mindepth 1 | sort > "$second_gc_listing" + +if [ -d "$app_path" ] || [ -d "$data_path" ]; then + echo "unrooted store items survived GC unexpectedly" >&2 + exit 1 +fi + +cat > "$metadata_file" <