From cb6455796539bbe0d507ffcc8052c132b1c78148 Mon Sep 17 00:00:00 2001 From: Steffen Beyer Date: Wed, 1 Apr 2026 16:41:26 +0200 Subject: [PATCH] Integrate FreeBSD jail builds into package path --- docs/PROGRESS.md | 37 +++ .../phase6-jail-build-integration-freebsd.md | 72 ++++ tests/daemon/freebsd-drop-exec.c | 73 +++++ tests/guix/run-phase6-jail-package-build.sh | 310 ++++++++++++++++++ 4 files changed, 492 insertions(+) create mode 100644 docs/reports/phase6-jail-build-integration-freebsd.md create mode 100644 tests/daemon/freebsd-drop-exec.c create mode 100755 tests/guix/run-phase6-jail-package-build.sh diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 5f26617..8351f77 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -1754,3 +1754,40 @@ Current assessment: - Phase 6.1 is now satisfied on the current FreeBSD prototype track - the next step is to move the already validated jail/build-user model into this live package-build path rather than keeping it prototype-only + +## 2026-04-01 — Phase 6.2 completed: jail/build-user isolation integrated into the real package path + +Completed work: + +- added a reusable UID/GID drop helper source: + - `tests/daemon/freebsd-drop-exec.c` +- added a runnable jail-integrated package harness: + - `tests/guix/run-phase6-jail-package-build.sh` +- wrote the Phase 6.2 report: + - `docs/reports/phase6-jail-build-integration-freebsd.md` +- ran the jail-integrated harness successfully and captured metadata under: + - `/tmp/phase6-jail-package-metadata.txt` + +Important findings: + +- a real package build derived from Guix's `hello` definition now runs through the live daemon path inside a FreeBSD jail rather than only through the earlier prototype scripts +- the actual build work inside the jail runs as dropped credentials: + - UID `35001` + - GID `35001` +- the integrated build path required one additional FreeBSD-specific adjustment beyond the earlier prototype: + - the daemon-side host `TMPDIR` path was not automatically valid inside the jail, so the jailed build environment must reset `TMPDIR=/tmp` +- observed metadata confirmed: + - `drv_path=/frx/store/...-hello-2.12.3.drv` + - `out_path=/frx/store/...-hello-2.12.3` + - `runtime_output=Hello, world!` + - `build_uid=35001` + - `build_gid=35001` + - `jail_hostname=fruix-phase6-hello-...` + - `build_mode=freebsd-jail` + - `source_store_path=/frx/store/...-hello-2.12.3.tar.gz` +- the GNU Hello test suite also passed inside the jail-integrated build path + +Current assessment: + +- Phase 6.2 is now satisfied on the current FreeBSD prototype track +- the next step is to validate a minimal user-facing profile installation flow on top of these real store outputs diff --git a/docs/reports/phase6-jail-build-integration-freebsd.md b/docs/reports/phase6-jail-build-integration-freebsd.md new file mode 100644 index 0000000..0633ec2 --- /dev/null +++ b/docs/reports/phase6-jail-build-integration-freebsd.md @@ -0,0 +1,72 @@ +# Phase 6.2: FreeBSD jail and build-user isolation integrated into the live build path + +Date: 2026-04-01 + +## Summary + +This step takes the earlier FreeBSD jail and privilege-drop prototypes and moves them into a real package build submitted through the live Fruix/Guix daemon path. + +Added files: + +- `tests/daemon/freebsd-drop-exec.c` +- `tests/guix/run-phase6-jail-package-build.sh` + +## Validation command + +Run command: + +```sh +METADATA_OUT=/tmp/phase6-jail-package-metadata.txt \ +./tests/guix/run-phase6-jail-package-build.sh +``` + +## What the harness does + +The harness: + +1. reuses the patched Phase 5 checkout/runtime setup +2. fetches GNU Hello `2.12.3` and verifies the expected SHA256 +3. starts the patched daemon on a temporary Unix socket +4. generates a package file that inherits from Guix's real `hello` package definition +5. lowers that package through a FreeBSD-specific build system whose builder: + - compiles `freebsd-drop-exec` + - creates a thin jail root + - mounts a minimal host tool view with explicit `nullfs` mounts + - mounts the source tarball read-only + - mounts per-build writable work/output areas + - enters the jail + - drops to UID/GID `35001:35001` + - runs the GNU Hello configure/build/check/install sequence inside the jail +6. copies the staged result back to the real `/frx/store` output path +7. validates runtime output, jail metadata, and preserved source references + +## Observed results + +Observed metadata included: + +- `drv_path=/frx/store/...-hello-2.12.3.drv` +- `out_path=/frx/store/...-hello-2.12.3` +- `runtime_output=Hello, world!` +- `build_uid=35001` +- `build_gid=35001` +- `jail_hostname=fruix-phase6-hello-...` +- `build_mode=freebsd-jail` +- `source_store_path=/frx/store/...-hello-2.12.3.tar.gz` + +The GNU Hello test suite also passed inside the jail build path. + +## Important findings + +- this is no longer a separate subsystem prototype; a real package build submitted through the daemon now executes under FreeBSD jail isolation with dropped build credentials +- the daemon's host `TMPDIR` path was not automatically meaningful inside the jail, so the integrated build path had to reset `TMPDIR=/tmp` inside the jailed builder environment +- a small helper binary was still needed to perform the post-`jexec` UID/GID drop reliably using numeric build identities on FreeBSD +- the output preserved a direct reference to the source tarball store item, so the integrated jail path still maintains the store-reference expectations established in Phase 6.1 + +## Conclusion + +Phase 6.2 is satisfied on the current FreeBSD prototype track: + +- a real Fruix/Guix package build now runs inside a FreeBSD jail +- the actual build work executes as a dropped numeric build user (`35001:35001`) +- the build succeeds into `/frx/store` +- the previously separate jail/build-user validation work is now connected to the live package-build path diff --git a/tests/daemon/freebsd-drop-exec.c b/tests/daemon/freebsd-drop-exec.c new file mode 100644 index 0000000..37778ba --- /dev/null +++ b/tests/daemon/freebsd-drop-exec.c @@ -0,0 +1,73 @@ +#include +#include +#include +#include +#include +#include +#include + +static void +usage(const char *argv0) +{ + fprintf(stderr, "usage: %s --uid UID --gid GID -- cmd...\n", argv0); +} + +static unsigned long +parse_ulong(const char *text) +{ + char *end = NULL; + unsigned long value; + + errno = 0; + value = strtoul(text, &end, 10); + if (errno != 0 || end == text || *end != '\0') + exit(2); + + return value; +} + +int +main(int argc, char **argv) +{ + uid_t uid = (uid_t)-1; + gid_t gid = (gid_t)-1; + gid_t groups[1]; + int i; + + for (i = 1; i < argc; i++) { + if (strcmp(argv[i], "--uid") == 0 && i + 1 < argc) { + uid = (uid_t)parse_ulong(argv[++i]); + } else if (strcmp(argv[i], "--gid") == 0 && i + 1 < argc) { + gid = (gid_t)parse_ulong(argv[++i]); + } else if (strcmp(argv[i], "--") == 0) { + i++; + break; + } else { + usage(argv[0]); + return 2; + } + } + + if (uid == (uid_t)-1 || gid == (gid_t)-1 || i >= argc) { + usage(argv[0]); + return 2; + } + + groups[0] = gid; + if (setgroups(1, groups) != 0) { + perror("setgroups"); + return 1; + } + if (setgid(gid) != 0) { + perror("setgid"); + return 1; + } + if (setuid(uid) != 0) { + perror("setuid"); + return 1; + } + + execvp(argv[i], &argv[i]); + perror("execvp"); + return 1; +} diff --git a/tests/guix/run-phase6-jail-package-build.sh b/tests/guix/run-phase6-jail-package-build.sh new file mode 100755 index 0000000..e2c1e1e --- /dev/null +++ b/tests/guix/run-phase6-jail-package-build.sh @@ -0,0 +1,310 @@ +#!/bin/sh +set -eu + +repo_root=$(CDPATH= cd -- "$(dirname "$0")/../.." && pwd) +metadata_target=${METADATA_OUT:-} + +cleanup=0 +if [ -n "${WORKDIR:-}" ]; then + workdir=$WORKDIR + mkdir -p "$workdir" +else + workdir=$(mktemp -d /tmp/fruix-phase6-jail-package.XXXXXX) + cleanup=1 +fi +if [ "${KEEP_WORKDIR:-0}" -eq 1 ]; then + cleanup=0 +fi + +setup_metadata=$workdir/setup-metadata.txt +setup_env=$workdir/setup-env.sh +metadata_file=$workdir/phase6-jail-package-metadata.txt +daemon_socket=$workdir/guix-daemon.sock +daemon_log=$workdir/guix-daemon.log +fetch_tarball=$workdir/hello-2.12.3.tar.gz +build_stdout=$workdir/fruix-build.out +build_stderr=$workdir/fruix-build.err +references_log=$workdir/references.log +package_template=$workdir/phase6-jail-hello.scm.in +package_file=$workdir/phase6-jail-hello.scm +helper_source=$repo_root/tests/daemon/freebsd-drop-exec.c + +daemon_pid= +cleanup_workdir() { + if [ -n "$daemon_pid" ]; then + sudo kill "$daemon_pid" >/dev/null 2>&1 || true + wait "$daemon_pid" 2>/dev/null || true + fi + rm -f "$daemon_socket" + if [ "$cleanup" -eq 1 ]; then + rm -rf "$workdir" + fi +} +trap cleanup_workdir EXIT INT TERM + +fetch -o "$fetch_tarball" https://ftp.gnu.org/gnu/hello/hello-2.12.3.tar.gz >/dev/null +[ "$(sha256 -q "$fetch_tarball")" = '0d5f60154382fee10b114a1c34e785d8b1f492073ae2d3a6f7b147687b366aa0' ] || { + echo "unexpected hello tarball hash" >&2 + exit 1 +} + +WORKDIR=$workdir/setup KEEP_WORKDIR=1 \ + METADATA_OUT=$setup_metadata ENV_OUT=$setup_env \ + "$repo_root/tests/guix/setup-phase5-checkout.sh" >"$workdir/setup.log" 2>&1 + +# shellcheck disable=SC1090 +. "$setup_env" + +cd "$PHASE5_BUILDDIR" +export LD_LIBRARY_PATH GUILE_LOAD_PATH GUILE_LOAD_COMPILED_PATH GUILE_EXTENSIONS_PATH +export GUILE_AUTO_COMPILE=0 + +cat >"$package_template" <<'EOF' +(use-modules (gnu packages base) + (guix packages) + (guix build-system) + (guix store) + (guix monads) + (guix gexp) + (guix derivations)) + +(define source-file "__SOURCE_FILE__") +(define helper-source-file "__HELPER_SOURCE_FILE__") +(define source-item + (local-file source-file "hello-2.12.3.tar.gz")) +(define helper-source-item + (local-file helper-source-file "freebsd-drop-exec.c")) + +(define* (phase6-lower name #:key source inputs native-inputs outputs system target #:allow-other-keys) + (bag + (name name) + (system system) + (target target) + (host-inputs `(,@(if source `(("source" ,source)) '()) + ("freebsd-drop-exec-source" ,helper-source-item) + ,@inputs)) + (build-inputs native-inputs) + (outputs outputs) + (build phase6-build) + (arguments `(#:source ,source #:package-name ,name)))) + +(define* (phase6-build name inputs #:key source (outputs '("out")) (system (%current-system)) #:allow-other-keys) + (mlet* %store-monad ((shell (interned-file "/bin/sh" "sh" #:recursive? #t)) + (source* (lower-object source system)) + (helper* (lower-object helper-source-item system)) + (builder (text-file "phase6-jail-hello-builder.sh" +"#!/bin/sh +set -eu +src_tar=\"$1\" +helper_src=\"$2\" +work_root=\"$TMPDIR/phase6-jail-build\" +root=\"$work_root/jail-root\" +build_dir=\"$work_root/build\" +stage_out=\"$work_root/stage-output\" +helper_dir=\"$work_root/helper-dir\" +helper_bin=\"$helper_dir/freebsd-drop-exec\" +jid='' +cleanup() { + set +e + if [ -n \"$jid\" ]; then /usr/sbin/jail -r \"$jid\" >/dev/null 2>&1 || true; fi + /sbin/umount \"$root/dev\" >/dev/null 2>&1 || true + /sbin/umount \"$root/output\" >/dev/null 2>&1 || true + /sbin/umount \"$root/inputs/hello-2.12.3.tar.gz\" >/dev/null 2>&1 || true + /sbin/umount \"$root/work\" >/dev/null 2>&1 || true + /sbin/umount \"$root/tools\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/local/lib\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/local/include\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/local/bin\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/libexec\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/libdata\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/lib\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/include\" >/dev/null 2>&1 || true + /sbin/umount \"$root/usr/bin\" >/dev/null 2>&1 || true + /sbin/umount \"$root/libexec\" >/dev/null 2>&1 || true + /sbin/umount \"$root/lib\" >/dev/null 2>&1 || true + /sbin/umount \"$root/bin\" >/dev/null 2>&1 || true +} +trap cleanup EXIT INT TERM +/bin/mkdir -p \"$root\" \"$build_dir\" \"$stage_out\" \"$helper_dir\" +/usr/bin/cc -Wall -Wextra -std=c11 \"$helper_src\" -o \"$helper_bin\" +/bin/mkdir -p \"$root/tmp\" \"$root/inputs\" \"$root/output\" \"$root/work\" \"$root/tools\" \"$root/etc\" \"$root/dev\" \"$root/usr\" \"$root/usr/local\" +/bin/chmod 1777 \"$root/tmp\" +: > \"$root/inputs/hello-2.12.3.tar.gz\" +printf 'builder:x:35001:35001:builder:/tmp:/bin/sh\\n' > \"$root/etc/passwd\" +printf 'builder:x:35001:\\n' > \"$root/etc/group\" +for p in /bin /lib /libexec /usr/bin /usr/include /usr/lib /usr/libdata /usr/libexec /usr/local/bin /usr/local/include /usr/local/lib; do + /bin/mkdir -p \"$root$p\" + /sbin/mount_nullfs -o ro \"$p\" \"$root$p\" +done +/sbin/mount_nullfs -o ro \"$helper_dir\" \"$root/tools\" +/sbin/mount_nullfs \"$build_dir\" \"$root/work\" +/sbin/mount_nullfs -o ro \"$src_tar\" \"$root/inputs/hello-2.12.3.tar.gz\" +/sbin/mount_nullfs \"$stage_out\" \"$root/output\" +/sbin/mount -t devfs devfs \"$root/dev\" +/usr/sbin/chown 35001:35001 \"$build_dir\" \"$stage_out\" +jail_name=\"fruix-phase6-hello-$$\" +jid=$(/usr/sbin/jail -i -c name=\"$jail_name\" path=\"$root\" host.hostname=\"$jail_name\" persist ip4=disable ip6=disable) +/usr/sbin/jexec \"$jid\" /tools/freebsd-drop-exec --uid 35001 --gid 35001 -- /bin/sh -eu -c ' +export HOME=/tmp +export LC_ALL=C +export TMPDIR=/tmp +mkdir -p /work/toolsbin +ln -sf /usr/local/bin/gmake /work/toolsbin/make +export PATH=/work/toolsbin:/bin:/usr/bin:/usr/local/bin +cd /work +/usr/bin/tar -xf /inputs/hello-2.12.3.tar.gz +cd hello-2.12.3 +export CC=/usr/bin/cc +export CONFIG_SHELL=/bin/sh +export CPPFLAGS=\"-I/usr/local/include\" +export LDFLAGS=\"-L/usr/local/lib -Wl,-rpath,/usr/local/lib\" +./configure --prefix=/output +/usr/local/bin/gmake -j1 +/usr/local/bin/gmake -j1 check +/usr/local/bin/gmake -j1 install +/bin/mkdir -p /output/share/fruix-phase6 +/usr/bin/id -u > /output/share/fruix-phase6/build-uid.txt +/usr/bin/id -g > /output/share/fruix-phase6/build-gid.txt +/bin/hostname > /output/share/fruix-phase6/jail-hostname.txt +' +/bin/mkdir -p \"$out\" +/bin/cp -a \"$stage_out/.\" \"$out/\" +/bin/mkdir -p \"$out/share/fruix-phase6\" +/bin/echo \"$src_tar\" > \"$out/share/fruix-phase6/source-store-path.txt\" +/bin/echo freebsd-jail > \"$out/share/fruix-phase6/build-mode.txt\" +" + (list (if (derivation? source*) + (derivation->output-path source*) + source*) + (if (derivation? helper*) + (derivation->output-path helper*) + helper*))))) + (lambda (store) + (values (derivation store name shell + (list "-e" builder + (if (derivation? source*) + (derivation->output-path source*) + source*) + (if (derivation? helper*) + (derivation->output-path helper*) + helper*)) + #:env-vars '(("HOME" . "/homeless") + ("PATH" . "/bin:/usr/bin:/usr/local/bin:/sbin:/usr/sbin")) + #:inputs `(,@(if (derivation? source*) `((,source*)) '()) + ,@(if (derivation? helper*) `((,helper*)) '()) + (,shell) (,builder)) + #:sources `(,shell ,builder) + #:system system + #:outputs outputs) + store)))) + +(define phase6-build-system + (build-system + (name 'phase6-freebsd-jail-gnu) + (description "Phase 6 FreeBSD jail GNU build system") + (lower phase6-lower))) + +(package/inherit hello + (source source-item) + (build-system phase6-build-system) + (supported-systems (list (%current-system)))) +EOF + +sed \ + -e "s|__SOURCE_FILE__|$fetch_tarball|g" \ + -e "s|__HELPER_SOURCE_FILE__|$helper_source|g" \ + "$package_template" > "$package_file" + +sudo env GUIX_DAEMON_SOCKET=unix://$daemon_socket \ + "$PHASE5_GUIX_DAEMON" --disable-chroot --listen "$daemon_socket" --no-substitutes \ + >"$daemon_log" 2>&1 & +daemon_pid=$! +for _ in 1 2 3 4 5 6 7 8 9 10; do + [ -S "$daemon_socket" ] && break + sleep 1 +done +[ -S "$daemon_socket" ] || { + echo "daemon socket was not created: $daemon_socket" >&2 + exit 1 +} + +GUIX_DAEMON_SOCKET=unix://$daemon_socket \ +./pre-inst-env fruix build -f "$package_file" >"$build_stdout" 2>"$build_stderr" + +out_path=$(awk 'NF { last = $0 } END { print last }' "$build_stdout") +drv_path=$(sed -n 's/^successfully built //p' "$build_stderr" | tail -n 1) +runtime_output=$("$out_path/bin/hello") +source_store_path=$(tr -d '\n' < "$out_path/share/fruix-phase6/source-store-path.txt") +build_uid=$(tr -d '\n' < "$out_path/share/fruix-phase6/build-uid.txt") +build_gid=$(tr -d '\n' < "$out_path/share/fruix-phase6/build-gid.txt") +jail_hostname=$(tr -d '\n' < "$out_path/share/fruix-phase6/jail-hostname.txt") +build_mode=$(tr -d '\n' < "$out_path/share/fruix-phase6/build-mode.txt") +GUIX_DAEMON_SOCKET=unix://$daemon_socket \ +./pre-inst-env fruix gc --references "$out_path" >"$references_log" 2>"$workdir/fruix-gc.err" + +[ -n "$drv_path" ] || { echo "missing derivation path in $build_stderr" >&2; exit 1; } +case "$drv_path" in + /frx/store/*.drv) : ;; + *) echo "unexpected derivation path: $drv_path" >&2; exit 1 ;; +esac +case "$out_path" in + /frx/store/*-hello-2.12.3) : ;; + *) echo "unexpected output path: $out_path" >&2; exit 1 ;; +esac +case "$source_store_path" in + /frx/store/*-hello-2.12.3.tar.gz) : ;; + *) echo "unexpected source store path: $source_store_path" >&2; exit 1 ;; +esac +[ "$runtime_output" = 'Hello, world!' ] || { + echo "unexpected runtime output: $runtime_output" >&2 + exit 1 +} +[ "$build_uid" = '35001' ] || { echo "unexpected build uid: $build_uid" >&2; exit 1; } +[ "$build_gid" = '35001' ] || { echo "unexpected build gid: $build_gid" >&2; exit 1; } +case "$jail_hostname" in + fruix-phase6-hello-*) : ;; + *) echo "unexpected jail hostname: $jail_hostname" >&2; exit 1 ;; +esac +[ "$build_mode" = 'freebsd-jail' ] || { echo "unexpected build mode: $build_mode" >&2; exit 1; } +grep -Fx "$source_store_path" "$references_log" >/dev/null || { + echo "source store path was not preserved as a reference" >&2 + exit 1 +} + +cat >"$metadata_file" <