Files
fips/.github/workflows/package-freebsd.yml
T
Johnathan Corgan 1149da9e06 Run the TCP blackhole tests on FreeBSD
The rx-stall tests and the background connect timeout test need a local
TCP address whose SYNs go unanswered. They build it from a listener whose
accept queue is full, which Linux, macOS and Windows leave unanswered.
FreeBSD answers such a SYN from its syncache and then resets the
connection, so the helper panicked and eleven tests failed in the FreeBSD
package build on every branch.

On FreeBSD the helper now fills by replacing the listener with a bound
socket that does not listen, and drains by listening on it. That address
is silent only with net.inet.tcp.blackhole=2 and blackhole_local=1, which
also stop every refused connect on the host, so the FreeBSD package
workflow runs the tests in two passes: everything else with the settings
off, then the blackhole tests alone with them on. A connect that is not
silent still fails the helper, naming the settings.

Once the helper worked, three tests failed on FreeBSD because they took
one read() as one frame: FreeBSD delivered the link announce that follows
a msg2 in a read of its own, where Linux returned the two together. The
tests now read each frame by its length with the transport's own frame
reader, and drain the announces after the handshake.
2026-10-05 14:39:19 +00:00

394 lines
16 KiB
YAML

name: FreeBSD Package
on:
push:
branches:
- master
- maint
- next
tags:
- "v*"
pull_request:
workflow_dispatch:
env:
CARGO_TERM_COLOR: always
jobs:
determine-versioning:
runs-on: ubuntu-latest
outputs:
freebsd_package_version: ${{ steps.freebsd_version.outputs.freebsd_package_version }}
freebsd_pkg_file_version: ${{ steps.freebsd_version.outputs.freebsd_pkg_file_version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Derive FreeBSD package version
id: freebsd_version
shell: bash
run: |
: ${GITHUB_OUTPUT:=/tmp/github_output}
BASE_VERSION=$(grep '^version' Cargo.toml | head -1 | sed 's/.*"\(.*\)"/\1/')
if [[ "$GITHUB_REF" == refs/tags/* ]]; then
VERSION="${GITHUB_REF_NAME#v}"
else
BRANCH=$(echo "$GITHUB_REF_NAME" | sed 's|[^A-Za-z0-9]|.|g; s/\.\{2,\}/./g; s/^\.//; s/\.$//')
HEIGHT=$(git rev-list --count HEAD)
HASH=$(git rev-parse --short HEAD)
if [[ -z "$BRANCH" ]]; then
BRANCH="ref"
fi
VERSION="${BASE_VERSION}+${BRANCH}.${HEIGHT}.${HASH}"
fi
# build-pkg.sh maps '-' and '+' to '.' (neither is allowed in a
# pkg version); derive the same mapping here so later steps can
# assert the exact artifact filename.
PKG_FILE_VERSION=$(printf '%s' "$VERSION" | tr -- '+-' '..')
echo "freebsd_package_version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "freebsd_pkg_file_version=${PKG_FILE_VERSION}" >> "$GITHUB_OUTPUT"
build:
name: Build FreeBSD package (x86_64)
# No GitHub-hosted FreeBSD runners exist; build inside a KVM-accelerated
# FreeBSD VM on the Linux runner. The release must track the .pkg ABI
# major (FreeBSD:15:amd64) — pkg on other majors refuses the package.
runs-on: ubuntu-latest
needs: determine-versioning
# Successful runs of this job take 8 to 11 minutes. Five consecutive runs
# in August 2026 instead sat in the VM step for 70, 190, 360, 360 and 360
# minutes and ended cancelled, the last three at GitHub's own six-hour job
# ceiling. Nothing here bounded them. This bound is deliberately loose
# enough that a slow-but-working run still passes, and tight enough that a
# stall fails in half an hour instead of burning a runner for six.
timeout-minutes: 30
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Set SOURCE_DATE_EPOCH from git
run: echo "SOURCE_DATE_EPOCH=$(git log -1 --format=%ct)" >> "$GITHUB_ENV"
- name: Build and smoke-install in FreeBSD VM
uses: vmactions/freebsd-vm@f0552d3b69211736abd97f02ff3d4674c56b73b1 # v1
env:
FREEBSD_PACKAGE_VERSION: ${{ needs.determine-versioning.outputs.freebsd_package_version }}
with:
release: "15.1"
usesh: true
sync: rsync
copyback: true
mem: 6144
envs: "SOURCE_DATE_EPOCH CARGO_TERM_COLOR FREEBSD_PACKAGE_VERSION"
prepare: |
pkg install -y curl
run: |
set -e
# rustup rather than the ports rust: rust-toolchain.toml pins
# the toolchain, and rustup honors the pin on first cargo use.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \
| sh -s -- -y --default-toolchain none --profile minimal
. "$HOME/.cargo/env"
cargo build --release
# The only place the FreeBSD cfg arms' unit tests ever run in
# CI — the main CI matrix runs no FreeBSD job, and a release build
# compiles no #[cfg(test)] code (AF-prefix strip round-trips,
# platform module, config path gates).
#
# Bounded, because libtest has no per-test deadline and this job
# runs plain `cargo test` rather than nextest, so one test that
# blocks on a syscall holds the whole binary open with nothing to
# end it. Six runs in August 2026 did exactly that. The bound is on
# the suite rather than on the job so the failure is a named step
# failure at fifteen minutes instead of the job ceiling at thirty,
# and `timeout` leaves the test binary's stdout untouched up to the
# kill: that stdout is what carries libtest's "has been running for
# over N seconds" lines, which are what name the blocked test.
#
# This bounds the damage; it does not fix anything. A test that can
# block for ever is a defect at the test, and the ones this suite
# has are bounded where they are written.
#
# The tests that need a TCP address whose SYNs go unanswered
# (`testutil::Blackhole`) run in a second pass. FreeBSD resets a
# connect to a full accept queue, so it gets that address only
# from the kernel's blackhole settings, and those also stop every
# refused connect on the host, which other tests need. So the
# first pass skips them and the second runs only them, with the
# settings on.
BLACKHOLE_TESTS="node::tests::rx_stall:: transport::tcp::tests::background_connect_timeout_is_counted"
skips=""
for t in $BLACKHOLE_TESTS; do skips="$skips --skip $t"; done
if ! timeout -s KILL 900 cargo test -- $skips; then
echo "FAIL: cargo test failed, or did not finish within its 900s bound." >&2
echo " If the output above stops mid-run, look for libtest's" >&2
echo " 'has been running for over' lines: they name the test" >&2
echo " that blocked, and the tests that never reported at all" >&2
echo " are the rest of the answer." >&2
exit 1
fi
bh_was=$(sysctl -n net.inet.tcp.blackhole)
bh_local_was=$(sysctl -n net.inet.tcp.blackhole_local)
sysctl net.inet.tcp.blackhole=2 net.inet.tcp.blackhole_local=1
bh_rc=0
timeout -s KILL 300 cargo test --lib -- $BLACKHOLE_TESTS || bh_rc=$?
sysctl net.inet.tcp.blackhole="$bh_was" net.inet.tcp.blackhole_local="$bh_local_was"
if [ "$bh_rc" -ne 0 ]; then
echo "FAIL: the blackhole pass failed, or did not finish within its 300s bound." >&2
exit 1
fi
packaging/freebsd/build-pkg.sh \
--version "$FREEBSD_PACKAGE_VERSION" \
--no-build
# Smoke-install the package in the VM: files land where the
# rc.d scripts and DNS integration expect them, and the
# binaries link against this release's base libraries.
PKG=$(ls deploy/fips-*-freebsd-*.pkg)
pkg add "$PKG"
for bin in fips fipsctl fipstop; do
test -x "/usr/local/bin/$bin" || { echo "FAIL: missing /usr/local/bin/$bin"; exit 1; }
if ldd "/usr/local/bin/$bin" | grep "not found"; then
echo "FAIL: unresolved shared libraries in $bin"; exit 1
fi
done
test -x /usr/local/etc/rc.d/fips
test -x /usr/local/etc/rc.d/fips_dns
test -f /usr/local/etc/fips/fips.yaml.sample
test -f /usr/local/etc/fips/hosts.sample
# The manifest post-install script must have copied the
# samples into place (install-if-absent semantics).
test -f /usr/local/etc/fips/fips.yaml
test -f /usr/local/etc/fips/hosts
# fips.yaml may hold a node private key (nsec:); it must not
# be world-readable — Debian and macOS both install it 0600.
for f in /usr/local/etc/fips/fips.yaml /usr/local/etc/fips/fips.yaml.sample; do
mode=$(stat -f %Lp "$f")
if [ "$mode" != "600" ]; then
echo "FAIL: $f mode is $mode, expected 600"; exit 1
fi
done
# post-install must create the control-socket access group.
pw groupshow fips >/dev/null || { echo "FAIL: fips group missing"; exit 1; }
test -x /usr/local/libexec/fips/fips-dns-setup
# The package's newsyslog entry must rotate the daemon log and
# make daemon(8) reopen it. The rc script starts daemon(8) with
# -H and records the supervisor's pid in daemon.pid, which the
# entry signals; without -H the supervisor keeps writing into
# the rotated file.
LOG=/var/log/fips.log
ENTRY=/usr/local/etc/newsyslog.conf.d/fips.conf
test -f "$ENTRY" || { echo "FAIL: missing $ENTRY"; exit 1; }
# Dry run of the stock configuration: the entry is reached only
# through newsyslog.conf's include of newsyslog.conf.d.
if ! newsyslog -nv 2>&1 | grep -q "$LOG"; then
echo "FAIL: the stock newsyslog configuration does not cover $LOG"
newsyslog -nv 2>&1 || true
exit 1
fi
service fips onestart
i=0
until [ -s /var/run/fips/daemon.pid ] && [ -s "$LOG" ]; do
i=$((i + 1))
if [ "$i" -gt 30 ]; then
echo "FAIL: no daemon.pid or empty $LOG 30s after start"
ls -l /var/run/fips "$LOG" 2>&1 || true
cat "$LOG" 2>&1 || true
exit 1
fi
sleep 1
done
sup_pid=$(cat /var/run/fips/daemon.pid)
# Inode numbers a process holds open, from fstat's INUM column.
open_inodes() { fstat -p "$1" 2>/dev/null | awk 'NR > 1 && $6 ~ /^[0-9]+$/ { print $6 }'; }
before=$(stat -f %i "$LOG")
# -F rotates regardless of size; -f reads the shipped entry alone.
newsyslog -F -f "$ENTRY"
after=$(stat -f %i "$LOG")
if [ "$after" = "$before" ]; then
echo "FAIL: newsyslog -F did not rotate $LOG"; ls -li "$LOG"*; exit 1
fi
if ! ls "$LOG".0* >/dev/null 2>&1; then
echo "FAIL: no rotated generation of $LOG"; ls -l "$LOG"*; exit 1
fi
i=0
until open_inodes "$sup_pid" | grep -qx "$after"; do
i=$((i + 1))
if [ "$i" -gt 10 ]; then
echo "FAIL: daemon(8) pid $sup_pid did not reopen $LOG after rotation"
fstat -p "$sup_pid" || true
exit 1
fi
sleep 1
done
if open_inodes "$sup_pid" | grep -qx "$before"; then
echo "FAIL: daemon(8) pid $sup_pid still holds the rotated $LOG open"; exit 1
fi
service fips onestop
echo "==> log rotation PASSED"
pkg info fips
echo "==> pkg smoke-install PASSED"
# SHA-256 sidecar computed inside the VM; the host verifies the
# bytes again after the rsync copyback, so corruption across
# the VM handoff is detected before upload.
( cd deploy && sha256 -q "$(basename "$PKG")" \
| { read -r h; printf '%s %s\n' "$h" "$(basename "$PKG")"; } \
> "$(basename "$PKG").sha256" )
# The whole workspace is rsynced back to the host; drop the
# build tree so the copyback moves megabytes, not gigabytes.
rm -rf target
- name: Resolve FreeBSD asset path
id: freebsd-assets
shell: bash
run: |
: ${GITHUB_OUTPUT:=/tmp/github_output}
set -euo pipefail
# build-pkg.sh names the package from the derived version and the
# pkg ABI arch; assert the exact name so a naming regression fails
# here instead of colliding on the release page.
EXPECTED="deploy/fips-${{ needs.determine-versioning.outputs.freebsd_pkg_file_version }}-freebsd-amd64.pkg"
if [[ ! -f "$EXPECTED" ]]; then
echo "Expected package $EXPECTED was not produced" >&2
echo "deploy/ contains:" >&2
ls -la deploy >&2 || true
exit 1
fi
echo "pkg=$EXPECTED" >> "$GITHUB_OUTPUT"
- name: Verify .pkg integrity across the VM handoff
shell: bash
run: |
set -euo pipefail
PKG="${{ steps.freebsd-assets.outputs.pkg }}"
sidecar="${PKG}.sha256"
if [[ ! -f "$sidecar" ]]; then
echo "FAIL: missing SHA-256 sidecar for $(basename "$PKG")" >&2
exit 1
fi
expected=$(awk '{print $1}' "$sidecar")
actual=$(sha256sum "$PKG" | awk '{print $1}')
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $(basename "$PKG") SHA-256 mismatch across the VM copyback" >&2
echo " expected (FreeBSD VM): $expected" >&2
echo " actual (host): $actual" >&2
exit 1
fi
echo "PASS: $(basename "$PKG") matches the in-VM SHA-256 ($actual)"
- name: Upload artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fips_${{ needs.determine-versioning.outputs.freebsd_package_version }}_x86_64_freebsd
path: |
${{ steps.freebsd-assets.outputs.pkg }}
${{ steps.freebsd-assets.outputs.pkg }}.sha256
retention-days: 30
- name: Build summary
run: |
echo "Build Summary for freebsd/x86_64:"
echo " Package: ${{ steps.freebsd-assets.outputs.pkg }}"
release:
name: Publish FreeBSD assets to GitHub Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download FreeBSD artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: dist
merge-multiple: true
- name: Validate .pkg bytes before publishing
shell: bash
run: |
set -euo pipefail
cd dist
pkgs=$(find . -maxdepth 1 -type f -name '*.pkg' | LC_ALL=C sort)
if [[ -z "$pkgs" ]]; then
echo "FAIL: no .pkg artifacts were downloaded" >&2
exit 1
fi
fail=0
while IFS= read -r pkg; do
base=$(basename "$pkg")
sidecar="${pkg}.sha256"
if [[ ! -f "$sidecar" ]]; then
echo "FAIL: missing SHA-256 sidecar for $base" >&2
fail=1
continue
fi
expected=$(awk '{print $1}' "$sidecar")
actual=$(sha256sum "$pkg" | awk '{print $1}')
if [[ "$expected" != "$actual" ]]; then
echo "FAIL: $base SHA-256 mismatch on the bytes about to be published" >&2
echo " expected (FreeBSD VM): $expected" >&2
echo " actual (downloaded): $actual" >&2
fail=1
continue
fi
echo "PASS: $base matches the in-VM SHA-256 ($actual)"
done <<<"$pkgs"
if [[ "$fail" -ne 0 ]]; then
echo "==> pre-publish .pkg verification FAILED; not publishing" >&2
exit 1
fi
echo "==> pre-publish .pkg verification PASSED"
- name: Generate FreeBSD release checksums
run: |
cd dist
find . -maxdepth 1 -type f -name '*.pkg' -printf '%P\n' \
| LC_ALL=C sort \
| xargs sha256sum \
> checksums-freebsd.txt
- name: Wait for tag release
env:
GH_TOKEN: ${{ github.token }}
run: |
for attempt in $(seq 1 20); do
if gh release view "${GITHUB_REF_NAME}" --repo "${GITHUB_REPOSITORY}" >/dev/null 2>&1; then
exit 0
fi
echo "Release ${GITHUB_REF_NAME} not available yet; waiting..."
sleep 15
done
echo "Timed out waiting for release ${GITHUB_REF_NAME}" >&2
exit 1
- name: Upload FreeBSD assets
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${GITHUB_REF_NAME}" \
dist/*.pkg \
dist/checksums-freebsd.txt \
--clobber \
--repo "${GITHUB_REPOSITORY}"