Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 35 additions & 13 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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"
Expand All @@ -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:
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -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

Expand Down Expand Up @@ -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
Expand Down
27 changes: 11 additions & 16 deletions DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`
Expand Down
55 changes: 27 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down
Loading