From 4301f9760a42fd1baf8293109a25cb8bdc82000b Mon Sep 17 00:00:00 2001 From: Anastassios Nanos Date: Fri, 30 Oct 2026 18:42:00 +0200 Subject: [PATCH 1/2] feat(bundle): Take the guest initrd from hull-assets Since hull-assets 0.1.11 its initrd is urunit's single binary, built from the urunit commit that urunc 3c00b69 pins. That is the urunc the bundle builds. The bundle now keeps hull-assets' container-initrd next to its kernel and stops building urunit and its own initrd. - scripts/check-assets.sh reads sources.urunc.ref from the signed hull-assets bundle.json and fails the build unless it is URUNC_REF. hull-assets records no urunit commit yet. When it does, the check requires the commit urunc pins in its urunit.pin. - The build still reads urunit.pin from the urunc checkout and records URUNIT_REPO and URUNIT_REF in pins.env. URUNIT_BRANCH and URUNIT_PINNED are gone, and setting URUNIT_REF, URUNIT_BRANCH or URUNIT_REPO is refused. - share/guest/bundle.json keeps ref and urunit, sets built_by to hull-assets and names the digest each file came from. INITRD_SOURCE in pins.env is that digest too. - CI checks that the kernel and the initrd are the layers of the hull-assets manifest pins.env names, and that ASSETS_URUNC_REF is the pinned urunc. - tests/check-assets.sh covers the check. tests/source-pins.sh now fails at the first download and checks that docker runs only for the urunc build. Signed-off-by: Anastassios Nanos --- .github/workflows/ci.yml | 48 ++++++--- .github/workflows/release.yml | 8 +- scripts/build-bundle.sh | 197 ++++++++++++++-------------------- scripts/check-assets.sh | 57 ++++++++++ tests/check-assets.sh | 82 ++++++++++++++ tests/source-pins.sh | 93 ++++++++-------- 6 files changed, 308 insertions(+), 177 deletions(-) create mode 100644 scripts/check-assets.sh create mode 100644 tests/check-assets.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8c0da43..02cecb0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: run: | set -eu sh -n install.sh - sh -n scripts/build-bundle.sh scripts/fetch-hvi.sh + sh -n scripts/build-bundle.sh scripts/fetch-hvi.sh scripts/check-assets.sh # The wrappers and uninstaller are generated into the tarball by # build-bundle.sh, so lint the text it will write rather than waiting # to find out on a host. @@ -53,7 +53,7 @@ jobs: - name: shellcheck run: | sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck - shellcheck -s sh install.sh scripts/build-bundle.sh scripts/fetch-hvi.sh /tmp/brig-uninstall.sh \ + shellcheck -s sh install.sh scripts/build-bundle.sh scripts/fetch-hvi.sh scripts/check-assets.sh /tmp/brig-uninstall.sh \ /tmp/brig-rootless-setup.sh /tmp/brig-ctl tests/*.sh - name: Script tests @@ -69,8 +69,8 @@ jobs: sh "$t" done - # Build the real bundle on each arch's native runner: urunc, urunit and the - # initrd from source (never a released urunc), the guest kernel pulled, the + # Build the real bundle on each arch's native runner: urunc from source (never + # a released urunc), the guest kernel and initrd pulled from hull-assets, the # configs generated, everything packed. Native, so no qemu. A smoke test of the # whole build path -- not a reproducibility gate, since a source-built urunc is # not byte-for-byte identical across builds. @@ -108,7 +108,8 @@ jobs: --variant generic-boot ${{ matrix.flag }} --out dist # The published record must be the one in the tree, and it must match the - # kernel and initrd the tree carries. pins.env names the kernel by digest. + # kernel and initrd the tree carries. pins.env names the hull-assets + # bundle both came from by digest, and both must be that bundle's bytes. - name: Check the boot-asset digests run: | set -eu @@ -125,6 +126,25 @@ jobs: esac grep -Eq '^KERNEL_SOURCE=.+@sha256:[0-9a-f]{64}$' dist/*.pins.env \ || { echo "pins.env does not name the kernel by digest"; exit 1; } + src="$(sed -n 's/^KERNEL_SOURCE=//p' dist/*.pins.env)" + grep -qx "INITRD_SOURCE=$src" dist/*.pins.env \ + || { echo "pins.env does not name the initrd as $src"; exit 1; } + # Each layer of the signed manifest is one file, named by its title, + # and its digest is the file's sha256. + o="$(echo "$d"/*/bin/oras)" + "$o" manifest fetch "${src%%:*}@${src##*@}" > "$d/manifest.json" + while read -r sum name; do + want="$(jq -r --arg n "$name" \ + '.layers[] | select(.annotations["org.opencontainers.image.title"] == $n) | .digest' \ + "$d/manifest.json")" + [ "$want" = "sha256:$sum" ] \ + || { echo "$name is sha256:$sum, but $src has '$want'"; exit 1; } + echo "$name: $want, as in $src" + done < "$g/SHA256SUMS" + cat "$g/bundle.json" + jq -e --arg s "$src" '.built_by == "hull-assets" and .kernel.source == $s and .initrd.source == $s' \ + "$g/bundle.json" > /dev/null \ + || { echo "bundle.json does not name $src as the source of both files"; exit 1; } # install.sh retargets a user install by rewriting the layout the bundle # was generated with, which it reads from here. @@ -143,14 +163,17 @@ jobs: esac grep -q "^ROOTLESS=$want$" "$p" \ || { echo "pins.env does not say ROOTLESS=$want"; exit 1; } - # urunc and urunit are built from pinned commits, and the build - # fails if the checkout is anything else. This checks the record: the - # commits in pins.env are the ones the script pins. + # urunc is built from a pinned commit, and the build fails if the + # checkout is anything else. This checks the record: the commit in + # pins.env is the one the script pins, and the guest assets were + # built from it. want="$(sed -n 's/^URUNC_REF_DEFAULT=\([0-9a-f]\{40\}\)$/\1/p' scripts/build-bundle.sh)" [ -n "$want" ] || { echo "no URUNC_REF_DEFAULT in build-bundle.sh"; exit 1; } grep -E "^URUNC_(REF|PINNED)=" "$p" grep -qx "URUNC_REF=$want" "$p" || { echo "pins.env does not carry URUNC_REF=$want"; exit 1; } grep -qx "URUNC_PINNED=true" "$p" || { echo "pins.env does not say URUNC_PINNED=true"; exit 1; } + grep -qx "ASSETS_URUNC_REF=$want" "$p" \ + || { echo "pins.env does not carry ASSETS_URUNC_REF=$want"; exit 1; } # hvi is fetched from the release the script pins; fetch-hvi.sh # fails the build unless its signature and sums check out. grep -E "^HVI_(VERSION|COMMIT)=" "$p" @@ -159,17 +182,16 @@ jobs: [ -n "$want" ] || { echo "no $k in build-bundle.sh"; exit 1; } grep -qx "$k=$want" "$p" || { echo "pins.env does not carry $k=$want"; exit 1; } done - # urunit has no pin of its own here: the build reads it from the - # pinned urunc commit's packaging/container-initrd/urunit.pin. Read - # that file at the recorded urunc commit and compare. + # urunit is not built here. pins.env records the commit the pinned + # urunc pins in packaging/container-initrd/urunit.pin. Read that file + # at the recorded urunc commit and compare. urunc_repo="$(sed -n 's/^URUNC_REPO=//p' "$p")" urunc_ref="$(sed -n 's/^URUNC_REF=//p' "$p")" want="$(curl -fsSL "https://raw.githubusercontent.com/$urunc_repo/$urunc_ref/packaging/container-initrd/urunit.pin" \ | sed -n 's/^URUNIT_REF=\([0-9a-f]\{40\}\)$/\1/p')" [ -n "$want" ] || { echo "$urunc_repo@$urunc_ref has no urunit.pin naming a commit"; exit 1; } - grep -E "^URUNIT_(REF|PINNED)=" "$p" + grep -E "^URUNIT_(REPO|REF)=" "$p" grep -qx "URUNIT_REF=$want" "$p" || { echo "pins.env does not carry URUNIT_REF=$want, urunc's pin"; exit 1; } - grep -qx "URUNIT_PINNED=true" "$p" || { echo "pins.env does not say URUNIT_PINNED=true"; exit 1; } - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2 with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 4173d0b..7478fbf 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,8 @@ # Build, sign and publish the self-contained brig bundles. # # Each architecture is built on a native runner -- amd64 on ubuntu-24.04, arm64 -# on ubuntu-24.04-arm -- so the urunc/urunit/initrd builds run native rather than -# under qemu. A final job collects both, checksums and signs them, and publishes +# on ubuntu-24.04-arm -- so the urunc build runs native rather than under +# qemu. A final job collects both, checksums and signs them, and publishes # one release. name: release @@ -64,8 +64,8 @@ jobs: - name: Build the ${{ matrix.arch }} ${{ matrix.rootless }} bundle env: # Pin every timestamp in the archives. The fetched components are - # byte-identical per pin; the source-built urunc, urunit and initrd - # are not guaranteed bit-for-bit across builds. + # byte-identical per pin; the source-built urunc is not guaranteed + # bit-for-bit across builds. SOURCE_DATE_EPOCH: '0' run: | set -eu diff --git a/scripts/build-bundle.sh b/scripts/build-bundle.sh index 55d5f12..76952b6 100755 --- a/scripts/build-bundle.sh +++ b/scripts/build-bundle.sh @@ -9,11 +9,11 @@ # The tarball is a complete /var/lib/brig/data tree: brig and brigd (and brig-broker, # from a brig release that carries it), cosign and oras, the # private-containerd stack (containerd, ctr, runc, nerdctl, CNI), the microVM -# monitors, the urunc runtime, the guest kernel and a brig-built initrd, plus -# every config file, systemd unit, wrapper and the uninstaller. Most components -# are upstream release artifacts, hvi among them. Three are built here because -# no release ships them: urunc (from urunc-dev/urunc), urunit (from -# NOFireAI/urunit) and the container-initrd (assembled from urunc and urunit). +# monitors, the urunc runtime, the guest kernel and initrd, plus every config +# file, systemd unit, wrapper and the uninstaller. Most components are upstream +# release artifacts, hvi among them. The kernel and the initrd come from the +# signed hull-assets bundle. urunc is built here from urunc-dev/urunc, because +# no urunc release carries what brig needs. # # scripts/build-bundle.sh --arch amd64 --version v0.1.0 --out dist # scripts/build-bundle.sh --arch arm64 --version v0.1.0 --variant generic-boot @@ -23,10 +23,10 @@ # because a node install driven by root never uses any of it, and a user # install cannot work without it. # -# docker is required (the urunc and urunit builds run in containers). Building -# the arm64 pieces on an amd64 runner (or vice versa) needs binfmt/qemu -# registered, because the static urunc binary and urunit are CGO/C and are -# built native inside a target-arch container (docker run --platform). +# docker is required (the urunc build runs in a container). Building the arm64 +# pieces on an amd64 runner (or vice versa) needs binfmt/qemu registered, +# because the static urunc binary is CGO and is built native inside a +# target-arch container (docker run --platform). # # Produces, under --out: # brig-standalone--linux-.tar.gz (the install tarball) @@ -97,8 +97,8 @@ done # bundle is cut for. BRIG_VERSION=latest still works for a local build. BRIG_VERSION="${BRIG_VERSION:-v0.4.1}" BRIG_REPO="${BRIG_REPO:-brig-sh/brig}" -# urunc and urunit have no release that carries what brig needs, so they are -# built from source, and the source is pinned to one commit each. A branch tip +# urunc has no release that carries what brig needs, so it is built from +# source, and the source is pinned to one commit. A branch tip # moves with every push, so it is not a release input: two builds of one bundle # version would carry different code. Moving a pin is a reviewed one-line # change to a *_REF_DEFAULT below, plus its *_BRANCH_DEFAULT when the commit @@ -127,11 +127,9 @@ URUNC_REPO="${URUNC_REPO:-urunc-dev/urunc}" URUNC_BRANCH_DEFAULT=feat/linux-hvi-v050 URUNC_REF_DEFAULT=3c00b69ff9ac1b8bb260975cdc63d18a7043558b URUNC_GO_IMAGE="${URUNC_GO_IMAGE:-golang:1.26.8}" -# urunit: the one binary of the initrd: early init, supervisor and exec agent. -# urunc pins it in packaging/container-initrd/urunit.pin, read after the urunc -# checkout below, so the initrd always matches the shim that talks to it. -# URUNIT_REPO and URUNIT_REF override the pin. URUNIT_REF= builds a branch -# tip, so it needs URUNIT_BRANCH too. +# urunit, the one binary of the initrd, is not built here: the initrd comes from +# hull-assets. pins.env records the urunit commit that urunc pins in +# packaging/container-initrd/urunit.pin. # hvi: the monitor brig boots a generic container on. It is the # brig-sh/hvi-vmm release hull ships on macOS, static against musl. The tag, # its commit and the sha256 of each tarball describe one release, so a bump @@ -145,36 +143,22 @@ HVI_SHA256_LINUX_ARM64=277c0cfbd9a669f79e9819b35b327a9d815128e181215fa46618acca0 if [ -n "${URUNC_BRANCH:-}" ] && [ "$URUNC_BRANCH" != "$URUNC_BRANCH_DEFAULT" ] && [ -z "${URUNC_REF+set}" ]; then fatal "URUNC_BRANCH=$URUNC_BRANCH is set but URUNC_REF is not. Set URUNC_REF= to build that branch's tip, or URUNC_REF= to build one commit from it." fi -if [ -n "${URUNIT_BRANCH:-}" ] && [ -z "${URUNIT_REF+set}" ]; then - fatal "URUNIT_BRANCH=$URUNIT_BRANCH is set but URUNIT_REF is not. Set URUNIT_REF= to build that branch's tip, or URUNIT_REF= to build one commit from it." -fi -if [ -n "${URUNIT_REF+set}" ] && [ -z "$URUNIT_REF" ] && [ -z "${URUNIT_BRANCH:-}" ]; then - fatal "URUNIT_REF= builds a branch tip, but URUNIT_BRANCH names no branch. Set URUNIT_BRANCH= too." +if [ -n "${URUNIT_REF+set}" ] || [ -n "${URUNIT_BRANCH+set}" ] || [ -n "${URUNIT_REPO+set}" ]; then + fatal "urunit is no longer built here: URUNIT_REPO, URUNIT_REF and URUNIT_BRANCH do nothing. The initrd comes from hull-assets; move ASSETS_VERSION to a release built from the urunit you want." fi if [ -n "${HVI_REF+set}" ] || [ -n "${HVI_BRANCH+set}" ]; then fatal "hvi is no longer built from source: HVI_REF and HVI_BRANCH do nothing. Move HVI_VERSION and its pins to another release instead." fi URUNC_BRANCH="${URUNC_BRANCH:-$URUNC_BRANCH_DEFAULT}" URUNC_REF="${URUNC_REF-$URUNC_REF_DEFAULT}" -# Empty when urunit comes from urunc's pin: urunit.pin names no branch. -URUNIT_BRANCH="${URUNIT_BRANCH:-}" -# Unset takes urunc's pin, after the urunc checkout. Set and empty builds the -# tip of URUNIT_BRANCH. -URUNIT_FROM_PIN=false -if [ -z "${URUNIT_REF+set}" ]; then - URUNIT_FROM_PIN=true - URUNIT_REF="" -fi # The checkout compares HEAD with the ref, so a ref is a full SHA: a short one, # a tag or a branch name would never match. -for r in "URUNC_REF=$URUNC_REF" "URUNIT_REF=$URUNIT_REF"; do - v="${r#*=}" - [ -n "$v" ] || continue - case "$v" in - *[!0-9a-f]*) fatal "${r%%=*}='$v' is not a commit: pin a full 40-character SHA" ;; +if [ -n "$URUNC_REF" ]; then + case "$URUNC_REF" in + *[!0-9a-f]*) fatal "URUNC_REF='$URUNC_REF' is not a commit: pin a full 40-character SHA" ;; esac - [ "${#v}" -eq 40 ] || fatal "${r%%=*}='$v' is not a commit: pin a full 40-character SHA" -done + [ "${#URUNC_REF}" -eq 40 ] || fatal "URUNC_REF='$URUNC_REF' is not a commit: pin a full 40-character SHA" +fi # The monitors and virtiofsd come from one monitors-build release. The release # is tagged vX.Y.Z and names its per-arch tarball after the monitor versions it # carries. monitors-build publishes no checksum file, so each tarball is pinned @@ -259,7 +243,7 @@ GITHUB="https://github.com" NAME="brig-standalone-$VERSION-linux-$ARCH" [ "$ROOTLESS" = true ] && NAME="brig-standalone-$VERSION-rootless-linux-$ARCH" TMP_DIR="$(mktemp -d)" -# The urunc/urunit/initrd builds run in docker as root and leave root-owned files +# The urunc build runs in docker as root and leaves root-owned files # (vendored deps, dist/, apt caches) under TMP_DIR. A non-root builder -- a CI # runner -- cannot rm those, and a failing cleanup would fail the whole build # after the tarball was already produced. So reclaim them via docker if a plain @@ -1018,61 +1002,32 @@ else info " built urunc $URUNC_REF" fi -# ---- urunit + the brig container-initrd (generic-boot / introspection) ------- -# brig builds its own initrd rather than shipping hull-assets': it execs into a -# guest through the agent in urunit, which has to match the urunc shim's -# protocol, so urunit is the commit the same urunc checkout ($src) pins. -URUNIT_PINNED="" -if [ "$VARIANT" = "stock" ]; then - # stock boots the unikernel's own init, so there is no urunit to build. - URUNIT_REF="" -else - command -v docker >/dev/null 2>&1 || fatal "docker is required to build the brig initrd" - [ -d "${src:-}" ] || fatal "the urunc checkout is needed to build the initrd (build urunc from source)" - usrc="$TMP_DIR/urunit-src" - if [ "$URUNIT_FROM_PIN" = true ]; then - pin="$src/packaging/container-initrd/urunit.pin" - [ -f "$pin" ] || fatal "$URUNC_REPO@$URUNC_REF has no urunit.pin; set URUNIT_REPO and URUNIT_REF" - pin_repo="$(sed -n 's/^URUNIT_REPO=//p' "$pin" | head -1)" - pin_repo="${pin_repo#https://github.com/}" - URUNIT_REPO="${URUNIT_REPO:-${pin_repo%.git}}" - URUNIT_REF="$(sed -n 's/^URUNIT_REF=//p' "$pin" | head -1)" - case "$URUNIT_REF" in - *[!0-9a-f]*|"") fatal "urunit.pin names '$URUNIT_REF', not a commit" ;; - esac - [ "${#URUNIT_REF}" -eq 40 ] || fatal "urunit.pin names '$URUNIT_REF', not a full 40-character SHA" - fi - URUNIT_REPO="${URUNIT_REPO:-NOFireAI/urunit}" - # checkout_source puts the repo after https://github.com/. +# ---- urunit: the commit urunc pins (generic-boot / introspection) ----------- +# The initrd comes from hull-assets below, and urunit is the whole of it. The +# exec agent in urunit has to match the urunc shim's protocol. So the urunit +# commit brig expects is the one the urunc checkout ($src) pins, and the guest +# assets are checked against it. +URUNIT_REPO="" +URUNIT_REF="" +if [ "$VARIANT" != "stock" ]; then + [ -d "${src:-}" ] || fatal "the urunc checkout is needed to read its urunit pin (build urunc from source)" + pin="$src/packaging/container-initrd/urunit.pin" + [ -f "$pin" ] || fatal "$URUNC_REPO@$URUNC_REF has no packaging/container-initrd/urunit.pin" + URUNIT_REPO="$(sed -n 's/^URUNIT_REPO=//p' "$pin" | head -1)" + URUNIT_REPO="${URUNIT_REPO#https://github.com/}" + URUNIT_REPO="${URUNIT_REPO%.git}" + # It goes into pins.env, which install.sh reads with awk -F=. case "$URUNIT_REPO" in */*/*|/*|*/|*[!A-Za-z0-9._/-]*) fatal "URUNIT_REPO='$URUNIT_REPO' is not / on GitHub" ;; */*) ;; *) fatal "URUNIT_REPO='$URUNIT_REPO' is not / on GitHub" ;; esac - URUNIT_PINNED=true - [ -n "$URUNIT_REF" ] || URUNIT_PINNED=false - ut_commit="$(checkout_source "$URUNIT_REPO" "$URUNIT_BRANCH" "$URUNIT_REF" "$usrc")" - pinned_note urunit "$URUNIT_REPO" "$URUNIT_BRANCH" "$URUNIT_REF" "$ut_commit" - URUNIT_REF="$ut_commit" - info "building urunit $URUNIT_REF (linux/$ARCH)" - docker run --rm --platform "linux/$ARCH" -v "$usrc":/u -w /u alpine:3.20 \ - sh -c "apk add --no-cache build-base linux-headers make musl-dev >/dev/null && make static" \ - || fatal "urunit build failed" - [ -s "$usrc/dist/urunit_static" ] || fatal "no urunit_static produced by the build" - - # The single-binary layout: /urunit, with /init and /vz-init linked to it. - # URUNIT hands the script the binary built above, so it neither builds - # urunit itself nor needs a container tool inside this one. - info "building the brig container-initrd (urunit $URUNIT_REF)" - docker run --rm --platform "linux/$ARCH" \ - -v "$src":/app -v "$usrc/dist/urunit_static":/urunit-static:ro \ - -w /app -e HOME=/tmp -e URUNIT=/urunit-static -e TARGET_ARCH="$ARCH_UNAME" "$URUNC_GO_IMAGE" \ - sh -c "apt-get update >/dev/null 2>&1 && apt-get install -y cpio >/dev/null 2>&1 && git config --global --add safe.directory /app && ./packaging/container-initrd/build-container-initrd.sh /app/dist/container-initrd" \ - || fatal "container-initrd build failed" - [ -s "$src/dist/container-initrd" ] || fatal "no container-initrd produced" - cp "$src/dist/container-initrd" "$DL/container-initrd" - chmod 0644 "$DL/container-initrd" - info " built container-initrd ($(stat -c%s "$DL/container-initrd") bytes)" + URUNIT_REF="$(sed -n 's/^URUNIT_REF=//p' "$pin" | head -1)" + case "$URUNIT_REF" in + *[!0-9a-f]*|"") fatal "urunit.pin names '$URUNIT_REF', not a commit" ;; + esac + [ "${#URUNIT_REF}" -eq 40 ] || fatal "urunit.pin names '$URUNIT_REF', not a full 40-character SHA" + info "urunit: urunc $URUNC_REF pins ${URUNIT_REPO:-urunit}@$URUNIT_REF" fi # ---- brig + brigd ------------------------------------------------------------ @@ -1240,13 +1195,11 @@ fi chmod 0755 "$STAGE"/bin/* # ---- guest boot assets (generic-boot / introspection) ------------------------ -# The kernel comes from hull-assets on both arches, pulled with the oras we just -# bundled. The tag is resolved once, the cosign we just bundled checks the -# signature on the digest it names, and that digest is what is pulled, so the -# kernel is the one hull-assets signed. The initrd is not taken from there: -# hull-assets' initrd carries hull's agent, so it is thrown away and the -# brig-built one above takes its place. A fresh bundle.json records the urunc -# the agent was built against, so the installer can check coherence. +# The kernel and the initrd come from hull-assets on both arches, pulled with +# the oras we just bundled. The tag is resolved once, the cosign we just bundled +# checks the signature on the digest it names, and that digest is what is +# pulled. So both files are the ones hull-assets signed, and so is the +# bundle.json that says which urunc they were built from. ASSETS_URUNC_REF="" KERNEL_SOURCE="" KERNEL_FROM_ASSETS=false @@ -1264,28 +1217,41 @@ if [ "$VARIANT" != "stock" ]; then "$a_ref@$a_digest" 2>&1 > /dev/null)" \ || fatal "the signature on $ASSETS_REPO@$a_digest did not verify against $ASSETS_SIGNER: $(printf '%s\n' "$a_cosign" | tail -n 1)" - info "fetching the guest kernel $ASSETS_REPO:$a_tag ($a_digest, signature verified) with oras" + info "fetching the guest kernel and initrd $ASSETS_REPO:$a_tag ($a_digest, signature verified) with oras" ( cd "$STAGE/share/guest" && "$STAGE/bin/oras" pull "$a_ref@$a_digest" ) \ || fatal "could not pull $ASSETS_REPO@$a_digest" KERNEL_SOURCE="$a_ref:$a_tag@$a_digest" KERNEL_FROM_ASSETS=true - [ -s "$STAGE/share/guest/$kernel" ] || fatal "no $kernel in the guest assets" - - # Replace hull's initrd with the brig-built one. - [ -f "$DL/container-initrd" ] || fatal "the brig container-initrd was not built" - cp "$DL/container-initrd" "$STAGE/share/guest/container-initrd" - magic="$(od -An -tx1 -N2 "$STAGE/share/guest/container-initrd" | tr -d ' \n')" + g="$STAGE/share/guest" + [ -s "$g/$kernel" ] || fatal "no $kernel in the guest assets" + [ -s "$g/container-initrd" ] || fatal "no container-initrd in the guest assets" + magic="$(od -An -tx1 -N2 "$g/container-initrd" | tr -d ' \n')" [ "$magic" = "1f8b" ] && fatal "container-initrd is gzipped; it must stay uncompressed" - ASSETS_URUNC_REF="$URUNC_REF" - cat > "$STAGE/share/guest/bundle.json" < "$g/bundle.json" < "$STAGE/share/guest/SHA256SUMS" - info " kernel from $KERNEL_SOURCE, initrd built for brig (urunc $URUNC_REF)" + ( cd "$g" && sha256sum -- "$kernel" container-initrd ) > "$g/SHA256SUMS" + info " kernel and initrd from $KERNEL_SOURCE (urunc $URUNC_REF)" fi # ---- config files, systemd units, wrappers ----------------------------------- @@ -1533,13 +1499,11 @@ URUNC_REPO=$URUNC_REPO URUNC_BRANCH=$URUNC_BRANCH URUNC_REF=$URUNC_REF URUNC_PINNED=$URUNC_PINNED -URUNIT_REPO=$([ "$VARIANT" = "stock" ] && echo "" || echo "$URUNIT_REPO") -URUNIT_BRANCH=$([ "$VARIANT" = "stock" ] && echo "" || echo "$URUNIT_BRANCH") +URUNIT_REPO=$URUNIT_REPO URUNIT_REF=$URUNIT_REF -URUNIT_PINNED=$URUNIT_PINNED HVI_VERSION=$([ "$HVI_INSTALLED" = true ] && echo "$HVI_VERSION" || echo "") HVI_COMMIT=$([ "$HVI_INSTALLED" = true ] && echo "$HVI_COMMIT" || echo "") -INITRD_SOURCE=$([ "$VARIANT" = "stock" ] && echo "" || echo "built:$URUNC_REF") +INITRD_SOURCE=$KERNEL_SOURCE PREFIX=$PREFIX DATA_DIR=$DATA_DIR RUN_DIR=$RUN_DIR @@ -1565,15 +1529,12 @@ PINS if [ "$URUNC_PINNED" = false ]; then echo "# WARNING: urunc is the tip of $URUNC_BRANCH at build time, not a pinned commit." >> "$STAGE/pins.env" fi -if [ "$URUNIT_PINNED" = false ]; then - echo "# WARNING: urunit is the tip of $URUNIT_BRANCH at build time, not a pinned commit." >> "$STAGE/pins.env" -fi # Deterministic tar: sorted names, one fixed timestamp, root-owned, fixed # directory and file modes, no extended headers. Fetched components are -# byte-identical per pin; the urunc binary and the initrd are byte-identical only -# if their builds are reproducible (fixed commit, SOURCE_DATE_EPOCH, same -# toolchain image), so a bundle's reproducibility rests on those builds too. +# byte-identical per pin; the urunc binary is byte-identical only if its build +# is reproducible (fixed commit, SOURCE_DATE_EPOCH, same toolchain image), so a +# bundle's reproducibility rests on that build too. info "packing" find "$STAGE" -type d -exec chmod 0755 {} + find "$STAGE" -type f ! -perm -u+x -exec chmod 0644 {} + diff --git a/scripts/check-assets.sh b/scripts/check-assets.sh new file mode 100644 index 0000000..bd3746d --- /dev/null +++ b/scripts/check-assets.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# +# Check that a hull-assets bundle was built from the urunc this bundle carries. +# +# scripts/check-assets.sh +# +# The initrd in a hull-assets bundle is urunit, and urunit's exec agent has to +# speak the protocol of the urunc shim. hull-assets records the urunc it was +# built from as sources.urunc.ref in its bundle.json. That ref must be +# . When bundle.json also records sources.urunit.ref, it must be +# , the commit urunc pins in packaging/container-initrd/urunit.pin. +# +# On success it prints the recorded urunc ref and the recorded urunit ref, or +# "-" for no urunit record. The bundle.json read here is the one in the signed +# artifact, so build-bundle.sh calls this after cosign checked +# the digest it pulled. + +set -eu + +die() { + echo "check-assets: $*" >&2 + exit 1 +} + +[ $# -eq 3 ] || die "usage: $0 " +json="$1" +urunc_ref="$2" +urunit_ref="$3" + +[ -s "$json" ] || die "$json is missing, so the initrd cannot be matched to urunc $urunc_ref" + +# sources..ref, from the one-object-per-source layout hull-assets writes. +# jq is not a build requirement, so sed reads it. +source_ref() { + tr -d '\n' < "$json" \ + | sed -n 's/.*"'"$1"'"[[:space:]]*:[[:space:]]*{[^}]*"ref"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' +} + +got="$(source_ref urunc)" +[ -n "$got" ] || die "$json records no sources.urunc.ref, so the initrd cannot be matched to urunc $urunc_ref" +[ "$got" = "$urunc_ref" ] || die "the guest assets were built from urunc $got, but this bundle carries urunc $urunc_ref. + The initrd's exec agent must match the urunc shim. Move ASSETS_VERSION to a hull-assets + release built from urunc $urunc_ref, or move URUNC_REF_DEFAULT to $got." + +# A urunit source whose ref the sed cannot read is refused. Treating it as +# absent would turn the urunit check off for any layout the sed does not model. +got_urunit="$(source_ref urunit)" +if [ -n "$got_urunit" ]; then + [ "$got_urunit" = "$urunit_ref" ] \ + || die "the guest assets carry urunit $got_urunit, but urunc $urunc_ref pins urunit $urunit_ref" +elif tr -d '\n' < "$json" | grep -q '"urunit"[[:space:]]*:'; then + die "$json records a urunit source whose ref could not be read, so the initrd cannot be matched to urunit $urunit_ref" +else + echo "check-assets: $json records no urunit commit; matched on urunc $got alone" >&2 +fi + +echo "$got ${got_urunit:--}" diff --git a/tests/check-assets.sh b/tests/check-assets.sh new file mode 100644 index 0000000..a3cdbd5 --- /dev/null +++ b/tests/check-assets.sh @@ -0,0 +1,82 @@ +#!/bin/sh +# +# Run scripts/check-assets.sh against hull-assets bundle.json files written here, +# and check that it passes only assets built from the urunc the bundle carries. + +set -eu + +here="$(cd "$(dirname "$0")/.." && pwd)" + +fail() { echo "FAIL: $*" >&2; exit 1; } +ok() { echo "ok - $*"; } + +T="$(mktemp -d)" +trap 'rm -rf "$T"' EXIT + +urunc=3c00b69ff9ac1b8bb260975cdc63d18a7043558b +urunit=48a61bd480eb57871204523312127b06d0a05363 +other=1111111111111111111111111111111111111111 + +# assets : write the bundle.json hull-assets 0.1.11 ships, +# with the given sources object. +assets() { + cat > "$T/bundle.json" < "$T/out" 2> "$T/err" +} + +refused() { + if check; then + cat "$T/out" "$T/err" >&2 + fail "$2 was accepted" + fi + grep -q "$1" "$T/err" || { cat "$T/err" >&2; fail "$2 was refused, but not with '$1'"; } +} + +# 1. The bundle.json of hull-assets 0.1.11: urunc matches, no urunit recorded. +assets "\"urunc\": { \"repo\": \"https://github.com/urunc-dev/urunc\", \"ref\": \"$urunc\" }" +check || { cat "$T/err" >&2; fail "matching assets were refused"; } +[ "$(cat "$T/out")" = "$urunc -" ] || fail "printed '$(cat "$T/out")', not the urunc ref and no urunit" +grep -q "records no urunit commit" "$T/err" || fail "a bundle.json without urunit was not reported" +ok "assets built from this urunc pass, and a missing urunit ref is reported" + +# 2. Assets built from another urunc are refused. +assets "\"urunc\": { \"repo\": \"https://github.com/urunc-dev/urunc\", \"ref\": \"$other\" }" +refused "built from urunc $other, but this bundle carries urunc $urunc" "assets from another urunc" +ok "assets built from another urunc are refused" + +# 3. A recorded urunit must be the one urunc pins. +assets "\"urunc\": { \"repo\": \"https://github.com/urunc-dev/urunc\", \"ref\": \"$urunc\" }, + \"urunit\": { \"repo\": \"https://github.com/NOFireAI/urunit\", \"ref\": \"$other\" }" +refused "carry urunit $other, but urunc $urunc pins urunit $urunit" "assets with another urunit" +assets "\"urunc\": { \"repo\": \"https://github.com/urunc-dev/urunc\", \"ref\": \"$urunc\" }, + \"urunit\": { \"repo\": \"https://github.com/NOFireAI/urunit\", \"ref\": \"$urunit\" }" +check || { cat "$T/err" >&2; fail "assets with the pinned urunit were refused"; } +if grep -q "records no urunit commit" "$T/err"; then fail "a recorded urunit was reported missing"; fi +[ "$(cat "$T/out")" = "$urunc $urunit" ] || fail "printed '$(cat "$T/out")', not both refs" +ok "a recorded urunit must be the one urunc pins" + +# 3b. A urunit source whose ref cannot be read is refused, not taken as absent. +assets "\"urunc\": { \"repo\": \"https://github.com/urunc-dev/urunc\", \"ref\": \"$urunc\" }, + \"urunit\": { \"repo\": \"https://github.com/NOFireAI/urunit\", \"build\": { \"arch\": \"amd64\" }, \"ref\": \"$other\" }" +refused "records a urunit source whose ref could not be read" "a urunit source the check cannot read" +ok "an unreadable urunit source is refused" + +# 4. No urunc recorded, or no bundle.json at all, is refused. +assets "\"kernel-config\": { \"ref\": \"$urunc\" }" +refused "records no sources.urunc.ref" "a bundle.json without a urunc ref" +rm -f "$T/bundle.json" +refused "is missing" "a missing bundle.json" +ok "assets that do not say which urunc they were built from are refused" diff --git a/tests/source-pins.sh b/tests/source-pins.sh index c40c003..912242d 100755 --- a/tests/source-pins.sh +++ b/tests/source-pins.sh @@ -1,10 +1,11 @@ #!/bin/sh # -# Run build-bundle.sh against a stub git and docker, and check which urunc and -# urunit commits it asks for. Nothing is fetched: the stub git records the ref -# it was asked to fetch, answers rev-parse with it and plants the urunit.pin -# the build reads from the urunc checkout; the stub docker fakes the urunc -# build and fails the urunit build, which ends the run. +# Run build-bundle.sh against a stub git, docker and curl, and check which urunc +# commit it asks for and which urunit commit it records. Nothing is fetched: the +# stub git records the ref it was asked to fetch, answers rev-parse with it and +# plants the urunit.pin the build reads from the urunc checkout; the stub docker +# fakes the urunc build; the stub curl fails the first download, which ends the +# run. # # Needs the tools build-bundle.sh checks for (curl, tar, sha256sum, awk, sed). @@ -23,7 +24,7 @@ urunc_branch="$(sed -n 's/^URUNC_BRANCH_DEFAULT=\(.*\)$/\1/p' "$here/scripts/bui [ "${#urunc_pin}" -eq 40 ] || fail "no URUNC_REF_DEFAULT in build-bundle.sh" tip=1111111111111111111111111111111111111111 # The urunit commit the stub urunc checkout pins in its urunit.pin. The build -# has no URUNIT_REF_DEFAULT of its own: it reads that file. +# has no urunit pin of its own: it reads that file. urunit_pin=3333333333333333333333333333333333333333 T="$(mktemp -d)" @@ -33,7 +34,7 @@ mkdir -p "$T/stub" # git: log every call. fetch remembers what it was asked for, and rev-parse # HEAD answers with it, or with $STUB_HEAD, or with $tip for a branch. A # checkout of the urunc source plants urunit.pin, as the real commit carries -# one; STUB_NO_PIN=1 leaves it out, and STUB_PIN_REPO replaces its repo. +# one; STUB_NO_PIN=1 leaves it out. cat > "$T/stub/git" <> "$T/git.log" @@ -66,12 +67,14 @@ esac exit 0 STUB -# docker: the urunc build writes the binaries the script copies out; the -# urunit build fails. -cat > "$T/stub/docker" <<'STUB' +# docker: the urunc build writes the binaries the script copies out. Anything +# else is logged and fails: urunit and the initrd are no longer built. +cat > "$T/stub/docker" <> "$T/docker.log" +STUB +cat >> "$T/stub/docker" <<'STUB' case "$*" in - *"apk add"*) exit 1 ;; *"make static"*) prev="" for a in "$@"; do @@ -85,54 +88,57 @@ case "$*" in esac exit 1 STUB +# curl: every download fails, so the run ends at the first one after urunc. +printf '#!/bin/sh\nexit 22\n' > "$T/stub/curl" chmod 0755 "$T/stub"/* # build [VAR=value...]: run build-bundle.sh with those variables, output in # $T/out.log. URUNC_* and URUNIT_* start unset. build() { : > "$T/git.log" + : > "$T/docker.log" : > "$T/fetched" - # Every run ends in a failure, by design: at the stub urunit build or at - # the check under test. - env -u URUNC_REF -u URUNC_BRANCH -u URUNIT_REF -u URUNIT_BRANCH -u HVI_REF -u HVI_BRANCH \ + # Every run ends in a failure, by design: at the first download or at the + # check under test. + env -u URUNC_REF -u URUNC_BRANCH -u URUNIT_REF -u URUNIT_BRANCH -u URUNIT_REPO -u HVI_REF -u HVI_BRANCH \ PATH="$T/stub:$PATH" "$@" \ sh "$here/scripts/build-bundle.sh" --arch amd64 --version test --out "$T/dist" \ > "$T/out.log" 2>&1 || true } -# 1. Defaults: urunc is fetched at its pinned commit, by SHA; urunit at the -# commit the urunc checkout's urunit.pin names. +# 1. Defaults: urunc is fetched at its pinned commit, by SHA. urunit is not +# fetched or built; the build reads the commit urunc's urunit.pin names. build grep -qx "git -C .*/urunc-src fetch -q --depth 1 origin $urunc_pin" "$T/git.log" \ || { cat "$T/git.log" "$T/out.log" >&2; fail "urunc was not fetched at its pin"; } -grep -qx "git -C .*/urunit-src fetch -q --depth 1 origin $urunit_pin" "$T/git.log" \ - || { cat "$T/git.log" "$T/out.log" >&2; fail "urunit was not fetched at urunc's pin"; } -grep -q "urunit: checked out NOFireAI/urunit@$urunit_pin (pinned" "$T/out.log" \ - || { cat "$T/out.log" >&2; fail "the build log does not name the pinned urunit"; } grep -q "urunc: checked out urunc-dev/urunc@$urunc_pin (pinned" "$T/out.log" \ || { cat "$T/out.log" >&2; fail "the build log does not name the pinned urunc"; } +grep -q "urunit: urunc $urunc_pin pins NOFireAI/urunit@$urunit_pin" "$T/out.log" \ + || { cat "$T/out.log" >&2; fail "the build log does not name urunc's urunit pin"; } +grep -q "failed to download" "$T/out.log" \ + || { cat "$T/out.log" >&2; fail "the build did not get past the urunc build"; } +if grep -q "urunit-src" "$T/git.log"; then fail "urunit was fetched"; fi +grep -q "make static" "$T/docker.log" || fail "the urunc build did not run" +if grep -qv "make static" "$T/docker.log"; then + cat "$T/docker.log" >&2; fail "docker ran for something other than the urunc build" +fi if grep -q -e "git clone" -e "WARNING" "$T/git.log" "$T/out.log"; then fail "a pinned build cloned a branch or warned" fi -ok "a default build fetches urunc and urunit at their pinned commits" - -# 1b. URUNIT_REF overrides urunc's pin for one build, and a urunc commit with no -# urunit.pin is refused rather than guessed. -other=4444444444444444444444444444444444444444 -build URUNIT_REF=$other -grep -qx "git -C .*/urunit-src fetch -q --depth 1 origin $other" "$T/git.log" \ - || { cat "$T/git.log" "$T/out.log" >&2; fail "URUNIT_REF did not override urunc's pin"; } +ok "a default build fetches urunc at its pin and builds no urunit or initrd" + +# 1b. A urunc commit with no urunit.pin is refused, and so is a pin that names +# no full commit. build STUB_NO_PIN=1 -grep -q "has no urunit.pin" "$T/out.log" \ +grep -q "has no packaging/container-initrd/urunit.pin" "$T/out.log" \ || { cat "$T/out.log" >&2; fail "a urunc commit without urunit.pin was not refused"; } -ok "URUNIT_REF overrides the pin, and a urunc without one is refused" +ok "a urunc without a urunit pin is refused" -# 1c. A urunit.pin whose repo is not / on GitHub is refused before -# urunit is fetched. +# 1c. A urunit.pin whose repo is not / on GitHub is refused: the +# repo goes into pins.env. build STUB_PIN_REPO=git@github.com:NOFireAI/urunit.git grep -q "URUNIT_REPO='git@github.com:NOFireAI/urunit' is not / on GitHub" "$T/out.log" \ || { cat "$T/out.log" >&2; fail "a urunit.pin with an ssh URL was not refused"; } -if grep -q "urunit-src" "$T/git.log"; then fail "urunit was fetched from a refused repo"; fi ok "a urunit.pin repo that is not / is refused" # 2. A checkout that is not the pinned commit fails the build. @@ -156,21 +162,24 @@ grep -q "URUNC_BRANCH=feat/something-else is set but URUNC_REF is not" "$T/out.l [ ! -s "$T/git.log" ] || fail "git ran for a refused build" ok "a branch without a ref is refused" -# 4b. URUNIT_REF= with no branch to build the tip of is refused, and so is the -# hvi source build that no longer exists. -build URUNIT_REF= -grep -q "URUNIT_REF= builds a branch tip, but URUNIT_BRANCH names no branch" "$T/out.log" \ - || { cat "$T/out.log" >&2; fail "URUNIT_REF= without a branch was not refused"; } -[ ! -s "$T/git.log" ] || fail "git ran for a refused build" +# 4b. The urunit build knobs are gone, so setting one is refused, and so is +# the hvi source build that no longer exists. +for v in URUNIT_REF=4444444444444444444444444444444444444444 URUNIT_REF= \ + URUNIT_BRANCH=main URUNIT_REPO=NOFireAI/urunit; do + build "$v" + grep -q "urunit is no longer built here" "$T/out.log" \ + || { cat "$T/out.log" >&2; fail "$v was not refused"; } + [ ! -s "$T/git.log" ] || fail "git ran for a refused build ($v)" +done build HVI_REF=24dc2c5f643ae23551ceb158600676c33636a21f grep -q "hvi is no longer built from source" "$T/out.log" \ || { cat "$T/out.log" >&2; fail "HVI_REF was not refused"; } [ ! -s "$T/git.log" ] || fail "git ran for a refused build" -ok "URUNIT_REF= without a branch, and HVI_REF, are refused" +ok "URUNIT_REF, URUNIT_BRANCH, URUNIT_REPO and HVI_REF are refused" # 5. A ref that is not a full SHA is refused before anything is fetched. -build URUNIT_REF=urunit_agent -grep -q "URUNIT_REF='urunit_agent' is not a commit" "$T/out.log" \ +build URUNC_REF=urunc_agent +grep -q "URUNC_REF='urunc_agent' is not a commit" "$T/out.log" \ || { cat "$T/out.log" >&2; fail "a branch name as a ref was not refused"; } build URUNC_REF=0818ff1 grep -q "URUNC_REF='0818ff1' is not a commit" "$T/out.log" \ From 331f4781cac984d99e58d8300a57c3c27528bbc2 Mon Sep 17 00:00:00 2001 From: Anastassios Nanos Date: Fri, 30 Oct 2026 18:42:02 +0200 Subject: [PATCH 2/2] docs(bundle): Say the initrd comes from hull-assets The README, DESIGN.md and docs/variants.md said the build assembles its own initrd from urunit. They now say the kernel and the initrd come from one signed hull-assets bundle, and that the build refuses a bundle built from another urunc. Signed-off-by: Anastassios Nanos --- DESIGN.md | 27 +++++++----------- README.md | 55 ++++++++++++++++++----------------- docs/variants.md | 74 ++++++++++++++++++++++-------------------------- 3 files changed, 72 insertions(+), 84 deletions(-) diff --git a/DESIGN.md b/DESIGN.md index 2c759ff..ac724e7 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -32,23 +32,18 @@ now done and is what `install.sh` and `scripts/build-bundle.sh` produce. the host. The installer builds nothing, runs no compiler and no docker, and fetches nothing else. Because the config files carry the fixed layout, the paths above are not configurable at install time. -- **Why three things are built (at release).** Most components are upstream release +- **Why urunc is built (at release).** Most components are upstream release artifacts, hvi among them (`brig-sh/hvi-vmm` `v0.1.0-rc.1`, cosign-verified). - Three have no upstream release and are compiled in the build: urunc - (from `urunc-dev/urunc` at commit `3c00b69`, on - `feat/linux-hvi-v050`, CGO static, so native per arch under - qemu/binfmt), urunit (from `NOFireAI/urunit` at the commit urunc's - `urunit.pin` names, C static) and the brig `container-initrd`. The sources are - pinned to a commit, not to a branch: a branch tip moves with every push, so it - is not a release input. The README's build section says how to move a pin. - brig execs into a guest through the agent in urunit, whose protocol must match - the urunc shim, so hull-assets' prebuilt initrd is not usable; the build - assembles a brig initrd of urunit alone, the early init, supervisor and agent - in one static binary, at the commit the shim's urunc pins. The kernel is still fetched, not built: from - `ghcr.io/nofireai/hull-assets` on both arches, by digest, once its cosign - signature checks out. - Because urunc and the initrd ship in one tarball built together, their agent - commit matches by construction. + urunc has no release that carries what brig needs, so the build compiles it + from `urunc-dev/urunc` at commit `3c00b69`, on `feat/linux-hvi-v050` (CGO + static, so native per arch under qemu/binfmt). The source is pinned to a + commit, since a branch tip moves with every push. The README's build section + says how to move the pin. + The kernel and the initrd are fetched from `ghcr.io/nofireai/hull-assets` on + both arches, by digest, once its cosign signature checks out. The initrd is + urunit alone, and its agent has to match the urunc shim. hull-assets records + the urunc it built from in its signed `bundle.json`, and the build refuses a + bundle whose urunc is not the one in the tarball. - **The installer's own job** is the host wiring the tarball cannot carry: create the state and socket dirs, provision the device-mapper pool when the kernel supports it (brig defaults to overlayfs; `CONTAINERD_SNAPSHOTTER=devmapper` diff --git a/README.md b/README.md index 78e9621..4bdec81 100644 --- a/README.md +++ b/README.md @@ -277,16 +277,14 @@ the install does not. Running `build-bundle.sh` by hand to cross-build the other architecture needs qemu/binfmt instead of a native runner. `build-bundle.sh` fetches the components that have upstream releases and builds -the three that do not: +urunc, which has none: | Component | Source | In the tarball | | --- | --- | --- | | brig, brigd, brig-broker | `brig-sh/brig` release | fetched, verified against its signed `checksums.txt` | | urunc, containerd-shim-urunc-v2 | built from `urunc-dev/urunc` at commit `3c00b69` (branch `feat/linux-hvi-v050`) | CGO-static, built in a Go container | -| urunit | built from `NOFireAI/urunit` at the commit urunc's `packaging/container-initrd/urunit.pin` names (`48a61bd`, branch `stack/10-acpi-power-button`) | C-static; the whole initrd | | hvi | `brig-sh/hvi-vmm` release `v0.1.0-rc.1` (commit `65570c7`), the one hull ships on macOS | fetched, musl-static; its `SHA256SUMS` cosign-verified against hvi-vmm's release workflow, the tarball pinned by sha256 | -| container-initrd | built from the above | assembled for brig, not fetched | -| guest kernel | `ghcr.io/nofireai/hull-assets` | fetched by digest once its cosign signature checks out; the kernel hull and brig boot | +| guest kernel, container-initrd | `ghcr.io/nofireai/hull-assets` `0.1.11` | fetched by digest once its cosign signature checks out; the initrd is urunit. urunc's `packaging/container-initrd/urunit.pin` names `48a61bd`; hull-assets `0.1.11` does not record its urunit, so the bundle records that commit as `urunit_expected`. The build refuses a hull-assets bundle built from another urunc | | monitors, virtiofsd | `urunc-dev/monitors-build` | fetched, pinned by sha256 | | containerd, runc, nerdctl, CNI | upstream releases | fetched, upstream checksums | | cosign | `sigstore/cosign` | fetched, pinned by sha256 | @@ -295,16 +293,15 @@ the three that do not: Then it generates the config files, systemd units, `brig-ctl` and the uninstaller, and packs the whole `/var/lib/brig/data` tree. -### urunc and urunit are pinned to a commit +### urunc is pinned to a commit A branch tip is not a release input. It moves with every push, so two builds of one bundle version would carry different code. So `build-bundle.sh` pins -urunc to one commit, `URUNC_REF_DEFAULT`, and takes urunit from the commit that -urunc pins in `packaging/container-initrd/urunit.pin`, so the initrd always -matches the shim that talks to it. The build fetches each commit by its SHA, -fails if the checkout is anything else, and records it in `pins.env` as -`URUNC_REF` and `URUNIT_REF`. CI checks that record on every bundle it builds. -`URUNIT_REPO` and `URUNIT_REF` override the urunit pin for a one-off build. +urunc to one commit, `URUNC_REF_DEFAULT`. The build fetches that commit by its +SHA, fails if the checkout is anything else, and records it in `pins.env` as +`URUNC_REF`. It also records `URUNIT_REF`, the urunit commit that urunc pins in +`packaging/container-initrd/urunit.pin`. CI checks that record on every bundle +it builds. The urunc pin is `feat/linux-hvi-v050`: `feat/unchanged_containers` with the exec fixes, the hvi monitor, urunit's single binary as the initrd, the @@ -338,26 +335,28 @@ $ URUNC_REF= URUNC_BRANCH=feat/unchanged_containers scripts/build-bundle.sh --ar ``` A branch given without a ref is refused, since it is unclear which of the two -was meant. +was meant. A tip build also has to match the guest assets (below), so it fails +unless `ASSETS_VERSION` names a hull-assets release built from that tip. -### Why the initrd is brig's own, not hull-assets' +### The initrd comes from hull-assets, checked against urunc brig execs into a guest through the exec agent in urunit, whose wire protocol -(`pkg/agentproto`) is a contract with the urunc shim. The two agree when urunit -is the commit the urunc checkout pins, and hull-assets' prebuilt initrd may -carry another. So `build-bundle.sh` builds a brig initrd with urunc's own -`packaging/container-initrd` tooling: `/urunit`, one static binary that is the -early init, the supervisor and the agent, with `/init` and `/vz-init` linked to -it. The urunc binary and the initrd's agent ship in one tarball, so they always -match. Only the kernel is fetched rather than built. It is generic, -and comes from hull-assets on both arches, checked against its signature. - -Two properties of the initrd are checked when the build packs it and again when -the runtime boots it, because each fails in a way that does not name itself: -`container-initrd` must not be compressed (the runtime appends the container's -argv, environment and network identity to a copy of it, and a gzipped one accepts -the append and boots without it), and urunit must be built for the target -architecture, or the kernel cannot run init. +(`pkg/agentproto`) is a contract with the urunc shim. Since hull-assets +`0.1.11`, its initrd is urunit's single binary: `/urunit`, the early init, the +supervisor and the agent, with `/init`, `/vz-init` and `/urunit-agent` linked to +it. hull-assets builds it from the urunit commit that urunc pins, and records +that urunc in the `bundle.json` it signs, as `sources.urunc.ref`. + +So `build-bundle.sh` takes the kernel and the initrd from the same signed +hull-assets bundle. `scripts/check-assets.sh` refuses the bundle unless +`sources.urunc.ref` is the urunc this tarball carries. hull-assets does not +record its urunit commit yet. When it does, the check also requires it to be the +one urunc pins. + +The initrd must not be compressed. The runtime appends the container's argv, +environment and network identity to a copy of it, and a gzipped one accepts the +append and boots without it. The build checks that when it packs the initrd, +and the runtime checks it again at boot. The boot assets land in `share/guest/` and the launcher points brig at them through `BRIG_BOOT_ASSETS`, so brig uses the packed assets rather than fetching diff --git a/docs/variants.md b/docs/variants.md index 1a4fb61..65048a1 100644 --- a/docs/variants.md +++ b/docs/variants.md @@ -60,13 +60,11 @@ involved; only a hand cross-build on one host falls back to qemu/binfmt. The result, urunc and its shim, goes into the tarball. `install.sh` never builds; it unpacks what the build produced. -### The kernel is fetched, the initrd is built for brig +### The kernel and the initrd are fetched -The two files a generic boot names are the guest kernel and the initrd, and they -come from different places. - -**The kernel** is fetched from hull-assets on both arches, the OCI artifact hull -and brig already boot from, one tag per platform, pulled with the bundled `oras`: +The two files a generic boot names are the guest kernel and the initrd. Both are +fetched from hull-assets on both arches, the OCI artifact hull and brig already +boot from, one tag per platform, pulled with the bundled `oras`: ``` ghcr.io/nofireai/hull-assets:-linux- immutable @@ -77,38 +75,35 @@ The build resolves the `` tag once. The bundled `cosign` checks the signature on the digest it names, against the identity brig checks: the `build-assets.yml` workflow in `NOFireAI/hull-assets`. The build then pulls that digest. The artifact's layers are the files themselves, each named by its -`org.opencontainers.image.title`, so the kernel's sha256 is its layer digest in +`org.opencontainers.image.title`, so each file's sha256 is its layer digest in the signed manifest. The kernel is generic and not brig-specific. On arm64 it is the same file hull -boots on macOS. `pins.env` records it as `KERNEL_SOURCE`, by the digest the build -pulled. `share/guest/SHA256SUMS` holds the sha256 of the kernel and the initrd, -and each release publishes it as `.boot-assets.sha256`: see the README. - -**The initrd is not taken from hull-assets.** brig execs into a guest through the -exec agent in urunit, whose wire protocol (`pkg/agentproto`) is a contract with -the urunc shim. The two agree when urunit is the commit the shim's urunc pins in -`packaging/container-initrd/urunit.pin`, and hull-assets' prebuilt initrd may -carry another. So the build assembles a brig initrd, with urunc's -`packaging/container-initrd/build-container-initrd.sh`, from one binary: - -- `/urunit`, built static from `NOFireAI/urunit` at the commit urunc pins. It is - the early init (`/init` and `/vz-init` are links to it), the PID 1 supervisor - and the exec agent. - -urunc and the initrd ship in one tarball, built together, so their agent commit -matches by construction. The installer just fetches and unpacks the tarball; it -never builds an initrd. - -**Two properties of the initrd are checked**, at build time and again when the -runtime boots it, because each fails in a way that does not name itself: - -- `container-initrd` must not be compressed. The runtime appends the container's - argv, environment and network identity to a copy of it, and concatenated cpio - works only while both halves are uncompressed. A gzipped one accepts the append - and boots without it. -- `urunit` must be built for this architecture. Under the wrong one the kernel - cannot run init and panics. +boots on macOS. `pins.env` records the bundle as `KERNEL_SOURCE` and +`INITRD_SOURCE`, by the digest the build pulled. `share/guest/SHA256SUMS` holds +the sha256 of the kernel and the initrd, and each release publishes it as +`.boot-assets.sha256`: see the README. + +**The initrd has to match urunc.** brig execs into a guest through the exec +agent in urunit, whose wire protocol (`pkg/agentproto`) is a contract with the +urunc shim. Since hull-assets `0.1.11`, its initrd is one binary: + +- `/urunit`, static, from `NOFireAI/urunit` at the commit urunc pins in + `packaging/container-initrd/urunit.pin`. It is the early init (`/init`, + `/vz-init` and `/urunit-agent` are links to it), the PID 1 supervisor and the + exec agent. + +hull-assets records the urunc it built from in its signed `bundle.json`, as +`sources.urunc.ref`. `scripts/check-assets.sh` refuses the bundle unless that is +the urunc this tarball carries. hull-assets does not record its urunit commit +yet; when it does, the check requires the commit urunc pins. hull-assets' own +build checks that urunit is built for the target architecture. The installer +just fetches and unpacks the tarball; it never builds an initrd. + +**The initrd must not be compressed**, and the build and the runtime both check +it. The runtime appends the container's argv, environment and network identity +to a copy of it, and concatenated cpio works only while both halves are +uncompressed. A gzipped one accepts the append and boots without it. The annotations that carry the pair are `com.urunc.unikernel.bootKernel` and `com.urunc.unikernel.bootInitrd`, pointing into `share/guest/`. hull takes the @@ -166,11 +161,10 @@ refuses a stock binary, rather than printing a table of zeroes. 3. Settled: the guest kernel is fetched from `hull-assets`, by the same tags hull uses, and checked against its signature; the runtime is built from `urunc-dev/urunc` at a pinned commit - (`3c00b69`, on `feat/linux-hvi-v050`); and the initrd is built for - brig from `NOFireAI/urunit` at the commit that urunc pins, with urunc's own - `packaging/container-initrd`, so its agent matches the shim. Both are - commits because a branch tip moves with every push; the README's build - section says how to move one. + (`3c00b69`, on `feat/linux-hvi-v050`); and the initrd comes from the same + hull-assets bundle as the kernel, which the build refuses unless it was built + from that urunc. urunc is pinned to a commit because a branch tip moves with + every push; the README's build section says how to move it. 4. Settled: `build-bundle.sh` fetches hvi from the `brig-sh/hvi-vmm` release hull ships on macOS (`v0.1.0-rc.1`). `scripts/fetch-hvi.sh` checks the cosign signature on its `SHA256SUMS`, the pinned sha256 of the tarball and