Files
fips/.github/workflows/package-linux.yml
T
Johnathan Corgan 867f5f81b4 build: produce every Linux artifact in a pinned container and check its floor
The released .deb installs cleanly on Debian 12 and Ubuntu 22.04 and the
daemon then cannot start, with the loader reporting GLIBC_2.39 not found.
Three binaries are affected, fips, fipstop and fips-gateway; fipsctl runs,
which is why it stayed quiet, since an install checked by running fipsctl
gets a clean answer while the daemon is dead. It is not a v0.5.0 regression:
every release artifact from v0.3.0 onward carries the same floor and the same
unversioned dependency.

The cause is the build machine. Rust's standard library references
pidfd_spawnp and pidfd_getpid as weak undefined symbols behind a runtime
check, so a binary should fall back where the C library lacks them. Linking
against a C library that has them records a hard version dependency instead,
and the loader refuses the image on that entry alone. No Rust changed here.

One script now produces the Linux artifacts. It builds in a container pinned
to the oldest distribution still supported for free by its distributor, named
with the floor in packaging/build-floor.env, and runs the floor check on the
package it produced, so every producer is gated rather than one workflow. The
floor guard reads readelf's Version needs section: the obvious objdump
formulation returns 2.2.5 for the shipped fips and would have passed every
affected release.

The script prints the package path as the only thing on its stdout, which is
what lets a caller take it without parsing, and it takes --features. Both
required care. The container's own stdout reaches the caller, so the build
runs with its output on stderr; without that, a caller using a plain command
substitution captures four lines of build chatter along with the path. And a
feature build must keep the +<features> marker that distinguishes it from the
default build of the same commit, or dpkg sees two packages at one version and
a revert silently no-ops. The version is derived on the host, because the
image has no git and the source is mounted read-only, so build-deb.sh now
applies that marker to an explicit version as well as to one it derives.

Both runners now build once through that script and install the artifact.
The five deb-install legs previously built their own package each, so one CI
run performed five complete release builds and four were waste; they now live
in a job of their own that downloads one built package, which also stops the
rest of the integration matrix waiting on it. The parity guard read one
hardcoded job and now sweeps every job's matrix. The release workflow builds
both architectures through the same script, and the systemd tarball takes its
binaries out of that package instead of from a second, unchecked set on the
runner, then is floor-checked after the strip.

Cargo.toml derives the dependency with $auto rather than stating a bare libc6
that nothing can fail. Note the ordering this implies for any pipeline that
builds on a current distribution: until it builds through this script, its
packages will declare libc6 (>= 2.39).

Measured: the container build produces four binaries at 2.34, and one artifact
passes all five distributions, 95 checks, in about two minutes. The floor
check fails the released 0.5.0 package on three binaries and passes this one.
A profiling build produces fips_0.5.1~dev+git<date>.<sha>+profiling-1_amd64.deb.
2026-09-05 23:34:05 +00:00

260 lines
9.5 KiB
YAML

name: Linux 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:
linux_package_version: ${{ steps.linux_version.outputs.linux_package_version }}
steps:
- uses: actions/checkout@d23441a48e516b6c34aea4fa41551a30e30af803 # v6
with:
fetch-depth: 0
- name: Derive Linux package version
id: linux_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/\.\.+/./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
echo "linux_package_version=${VERSION}" >> "$GITHUB_OUTPUT"
build:
name: Build Linux artifacts (${{ matrix.artifact_arch }})
runs-on: ${{ matrix.os }}
needs: determine-versioning
# Both legs build in the same pinned container. Nothing passes --platform,
# so the arm runner resolves the arm64 variant of the base image and builds
# natively; the floor check runs on that package too, so an aarch64 build
# above the floor fails the leg rather than shipping. What the runner
# supplies is Docker and the checkout -- neither leg compiles on the host.
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
artifact_arch: x86_64
deb_arch: amd64
- os: ubuntu-24.04-arm
artifact_arch: aarch64
deb_arch: arm64
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"
# The host no longer compiles anything: the container carries the
# toolchain and the build dependencies. llvm is here only for llvm-strip,
# which build-tarball.sh uses on the binaries recovered from the package.
- name: Install host packaging tools
run: sudo apt-get update && sudo apt-get install -y --no-install-recommends llvm
# Build in the pinned container rather than on the runner. The runner's
# glibc is what put a GLIBC_2.39 requirement into every Linux artifact
# from v0.3.0 onward, so the package installed cleanly and then could not
# load on Debian 12 or Ubuntu 22.04. packaging/build-floor.env declares
# the base image and the floor; the script builds there and runs
# testing/check-glibc-floor.sh on the package it produced, so a build that
# would ship an unloadable binary fails here instead of at the user.
#
# This is the same script ci.yml and a local run call, so the package that
# passes the five-distro suite is built the way this one is.
- name: Build Debian package in the pinned container
id: deb
shell: bash
run: |
set -euo pipefail
: ${GITHUB_OUTPUT:=/tmp/github_output}
packaging/debian/build-deb-container.sh \
--version "${{ needs.determine-versioning.outputs.linux_package_version }}" \
--output-dir deploy \
| tee /tmp/build-deb-container.log
# The script prints the package path as its last line of stdout.
# Only stdout is captured; its diagnostics go to stderr and straight
# to the job log, so nothing can land after the path.
DEB_FILE=$(tail -n 1 /tmp/build-deb-container.log)
if [[ ! -f "$DEB_FILE" ]]; then
echo "build-deb-container.sh did not name a package: '$DEB_FILE'" >&2
exit 1
fi
case "$DEB_FILE" in
*_${{ matrix.deb_arch }}.deb) ;;
*)
echo "Package $DEB_FILE is not ${{ matrix.deb_arch }}" >&2
exit 1
;;
esac
# Record it relative to the checkout: upload-artifact derives the
# archive layout from the common ancestor of its paths, and an
# absolute path here would nest the package under directories the
# release job's dist/*.deb glob does not look in.
echo "deb=${DEB_FILE#"$PWD"/}" >> "$GITHUB_OUTPUT"
# The container writes its target directory to a Docker volume, so the
# runner's target/release is empty. Recover the four binaries from the
# package instead: they are the container-built ones, so the tarball ships
# what the package ships rather than a second, runner-built set that the
# floor check never saw and that no package manager would refuse.
- name: Stage container-built binaries for the tarball
shell: bash
run: |
set -euo pipefail
UNPACK=$(mktemp -d)
dpkg-deb -x "${{ steps.deb.outputs.deb }}" "$UNPACK"
mkdir -p target/release
for bin in fips fipsctl fipstop fips-gateway; do
if [[ ! -f "$UNPACK/usr/bin/$bin" ]]; then
echo "Package is missing usr/bin/$bin" >&2
exit 1
fi
install -m 0755 "$UNPACK/usr/bin/$bin" "target/release/$bin"
done
rm -rf "$UNPACK"
- name: Build systemd tarball
env:
STRIP: llvm-strip
run: |
packaging/systemd/build-tarball.sh \
--version "${{ needs.determine-versioning.outputs.linux_package_version }}" \
--arch "${{ matrix.artifact_arch }}" \
--no-build
# The tarball has no package manager to refuse it, so nothing at install
# time would notice a bad floor. Check the binaries out of the finished
# tarball, after the strip, rather than trusting that they are the same
# objects the package check already passed.
- name: Check the tarball against the declared glibc floor
shell: bash
run: |
set -euo pipefail
TARBALL="deploy/fips-${{ needs.determine-versioning.outputs.linux_package_version }}-linux-${{ matrix.artifact_arch }}.tar.gz"
UNPACK=$(mktemp -d)
tar -xzf "$TARBALL" -C "$UNPACK"
testing/check-glibc-floor.sh \
"$UNPACK"/*/fips \
"$UNPACK"/*/fipsctl \
"$UNPACK"/*/fipstop \
"$UNPACK"/*/fips-gateway
rm -rf "$UNPACK"
- name: Resolve Linux asset paths
id: linux-assets
shell: bash
run: |
: ${GITHUB_OUTPUT:=/tmp/github_output}
TARBALL="deploy/fips-${{ needs.determine-versioning.outputs.linux_package_version }}-linux-${{ matrix.artifact_arch }}.tar.gz"
if [[ ! -f "$TARBALL" ]]; then
echo "Missing tarball: $TARBALL" >&2
exit 1
fi
echo "tarball=$TARBALL" >> "$GITHUB_OUTPUT"
echo "deb=${{ steps.deb.outputs.deb }}" >> "$GITHUB_OUTPUT"
- name: SHA-256 hashes
run: |
echo "==> Linux release assets:"
sha256sum \
"${{ steps.linux-assets.outputs.tarball }}" \
"${{ steps.linux-assets.outputs.deb }}"
- name: Upload artifact (GitHub only)
if: ${{ env.ACT != 'true' }}
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
with:
name: fips_${{ needs.determine-versioning.outputs.linux_package_version }}_${{ matrix.artifact_arch }}_linux
path: |
${{ steps.linux-assets.outputs.tarball }}
${{ steps.linux-assets.outputs.deb }}
retention-days: 30
- name: Build Summary
run: |
echo "Build Summary for linux/${{ matrix.artifact_arch }}:"
echo " Tarball: ${{ steps.linux-assets.outputs.tarball }}"
echo " Debian: ${{ steps.linux-assets.outputs.deb }}"
release:
name: Publish Linux assets to GitHub Release
runs-on: ubuntu-latest
needs: build
if: startsWith(github.ref, 'refs/tags/')
permissions:
contents: write
steps:
- name: Download Linux artifacts
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
with:
path: dist
merge-multiple: true
- name: Generate Linux release checksums
run: |
cd dist
find . -maxdepth 1 -type f \( -name '*.deb' -o -name '*.tar.gz' \) -printf '%P\n' \
| LC_ALL=C sort \
| xargs sha256sum \
> checksums-linux.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 Linux assets
env:
GH_TOKEN: ${{ github.token }}
run: |
gh release upload "${GITHUB_REF_NAME}" \
dist/*.deb \
dist/*.tar.gz \
dist/checksums-linux.txt \
--clobber \
--repo "${GITHUB_REPOSITORY}"