Skip to content

The docs name the programs, the compartment and the five kinds of test #623

The docs name the programs, the compartment and the five kinds of test

The docs name the programs, the compartment and the five kinds of test #623

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
schedule:
# Weekly: a pinned kernel's support status changes even when the pin does not (ADR-009).
- cron: '0 6 * * 1'
permissions:
contents: read
jobs:
identity:
name: Commit identity
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
with:
fetch-depth: 0
- name: Every commit is DevomB <Devom.hb@yahoo.com>
run: |
# A pull request's checkout is a synthetic merge commit; check the branch head instead.
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/check-commit-identity.sh "${{ github.event.pull_request.head.sha }}"
else
./tools/check-commit-identity.sh --all
fi
lint:
name: Shell lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Install shellcheck
run: sudo apt-get update -qq && sudo apt-get install -y -qq shellcheck
- name: Syntax check every script
run: |
fail=0
while IFS= read -r f; do
if bash -n "$f"; then
echo " ok $f"
else
echo " FAIL $f"
fail=1
fi
done < <(find . -name '*.sh' -not -path './build/work/*')
exit $fail
- name: Tool fixture suites
run: |
# Exit 77 means the suite cannot run here: it is listed as skipped, never counted as a pass.
fail=0
skipped=""
for t in tools/tests/*.sh tools/tests/*.py; do
echo "=== $t"
# The step runs under bash -e; capture the status without tripping it.
"$t" && rc=0 || rc=$?
if [ "$rc" -eq 77 ]; then
echo " SKIPPED: $t"
skipped="$skipped $t"
elif [ "$rc" -ne 0 ]; then
echo " FAILED: $t"
fail=1
fi
done
if [ -n "$skipped" ]; then
echo
echo "did not run (not passes):$skipped"
fi
exit $fail
- name: Shellcheck
# Every tracked file whose first line names a shell.
run: |
mapfile -t scripts < <(git ls-files | xargs awk 'FNR==1 { if ($0 ~ /^#! ?\/(usr\/)?bin\/(env +)?(ba|da)?sh/) print FILENAME; nextfile }')
echo "${#scripts[@]} shell scripts"
[ "${#scripts[@]}" -ge 100 ] || { echo "the search for scripts found too few: it is broken"; exit 1; }
shellcheck -S warning -x "${scripts[@]}"
- name: Executable bits are recorded
run: |
# A commit from Windows records 100644; a Linux clone then fails with "Permission denied".
fail=0
for f in build/stages/*.sh tools/*.sh tools/tests/*.sh tools/tests/*.py \
tools/image/*.sh tools/image/*.py \
compartments/tests/*.sh tools/git-hooks/*; do
mode=$(git ls-files -s "$f" | awk '{print $1}')
if [ "$mode" != "100755" ]; then
echo " NOT EXECUTABLE ($mode): $f"
fail=1
fi
done
exit $fail
- name: No CRLF in scripts
run: |
# By name or by shebang: tools/kryptik and the run files have no extension.
git ls-files | while IFS= read -r f; do
case "$f" in *.sh|*.rs|*.py|Makefile|*/Makefile) ;; *) [ "$(head -c 2 "$f" 2>/dev/null)" = '#!' ] || continue ;; esac
if grep -qU $'\r' "$f"; then echo "$f"; fi
done > /tmp/crlf
if [ -s /tmp/crlf ]; then
cat /tmp/crlf
echo "CRLF found - scripts will fail on Linux with 'bad interpreter'"
exit 1
fi
echo "no CRLF"
manifest:
name: Source manifest
runs-on: ubuntu-latest
# The Distro workflow's path: a cache's version hashes its path, so any other misses main's copy.
env:
KRYPTIK_SOURCES: /mnt/kryptik/sources
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Manifest resolves
run: |
sudo mkdir -p /mnt/kryptik && sudo chown "$(id -u):$(id -g)" /mnt/kryptik
./tools/fetch-sources.sh --list
# Restores the last set; `make sources` checks it against the lock and fetches what is missing.
- name: Upstream sources (cached by sources.lock)
id: sources
uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: /mnt/kryptik/sources
key: sources-v3-${{ hashFiles('sources.lock') }}
restore-keys: |
sources-
# Fetches the signatures into the cached set for the gate below; this pass is not the verdict.
- name: Fetch and verify the sources
if: steps.sources.outputs.cache-hit != 'true'
run: |
make sources
./tools/prune-sources.sh
./tools/verify-signatures.sh > /dev/null 2>&1 || true
# Not from a pull request: its cache is scoped to the merge ref and would duplicate the set.
- name: Save the sources
if: ${{ github.event_name != 'pull_request' && steps.sources.outputs.cache-hit != 'true' }}
uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5.1.0
with:
path: /mnt/kryptik/sources
key: ${{ steps.sources.outputs.cache-primary-key }}
# Informational, as mirrors go down; a one-byte ranged GET, since some GNU mirrors refuse HEAD.
- name: Every source URL is reachable
continue-on-error: true
run: |
reach() {
curl -fsSL --range 0-0 --retry 3 --retry-delay 2 \
--connect-timeout 20 --max-time 60 -o /dev/null "$1"
}
missing=0
while read -r name ver url _; do
if reach "$url"; then
echo " ok $name $ver"
continue
fi
# Same fallback as fetch-sources.sh: mirrors.kernel.org for GNU.
fallback="${url/https:\/\/ftpmirror.gnu.org/https://mirrors.kernel.org/gnu}"
if [ "$fallback" != "$url" ] && reach "$fallback"; then
echo " ok $name $ver (via fallback mirror)"
else
echo " UNREACHED $name $ver $url"
missing=$((missing + 1))
fi
done < <(./tools/fetch-sources.sh --list)
echo
if [ "$missing" -gt 0 ]; then
echo "$missing source(s) unreachable; investigate if the same one fails across several runs."
else
echo "all sources reachable"
fi
- name: Provenance of unsigned sources
# Strict on push and schedule; informational on pull requests, where a fork gets no token.
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "pull request: informational (no token for fork PRs)"
./tools/verify-provenance.sh
else
./tools/verify-provenance.sh --strict
fi
- name: Provenance inventory
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# One class per source and no total: the classes are different strengths of evidence.
./tools/provenance-inventory.sh
# Each signature must verify against its publisher's key, or tools/source-notes.tsv says why not.
- name: Signatures, strict
run: ./tools/verify-signatures.sh --strict
- name: sources.lock covers the manifest
run: |
fail=0
while read -r name ver url _; do
file=$(basename "$url")
if ! grep -q " $file\$" sources.lock; then
echo " MISSING FROM LOCK: $file"
fail=1
fi
done < <(./tools/fetch-sources.sh --list)
[ $fail -eq 0 ] && echo "sources.lock covers every manifest entry"
exit $fail
compartments:
name: Compartment layer
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: kryptikd unit tests
run: cd compartments/kryptikd && cargo test
# wlproxy and zoneid tests, and the shipped zone files against the identity invariant.
- name: Compositor tests
run: tools/tests/compositor.sh
- name: Zone definitions validate
run: |
cd compartments/kryptikd && cargo build --quiet
./target/debug/kryptikd check --zones ../zones
- name: Allow unprivileged user namespaces
run: |
# Ubuntu 24.04 restricts unprivileged user namespaces, which the zone suites need here.
sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
echo "userns check:"
unshare --user --map-root-user id -u
- name: kryptikd builds for musl
run: |
# The shipped binary is static musl; this catches glibc-only assumptions.
rustup target add x86_64-unknown-linux-musl
cd compartments/kryptikd
cargo build --release --target x86_64-unknown-linux-musl
cargo test --target x86_64-unknown-linux-musl
- name: Isolation exit test (the primitives)
run: |
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/adversarial.sh
- name: Real-launcher suite (the launch path itself)
run: |
# Unprivileged here, so groups K (privileged launch) and M (cgroup limits) report NOT RUN.
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/launcher.sh | tee /tmp/launcher.out
exit "${PIPESTATUS[0]}"
- name: The launcher suite reports its gaps
run: |
# Groups K and M need root: they run in the Distro workflow's zones-test, never here.
if ! grep -q 'PASSED WITH GAPS' /tmp/launcher.out; then
echo "::error::The launcher suite passed cleanly on a runner, where groups K and M cannot run."
echo "They either ran vacuously or were dropped; neither is a pass."
grep -E 'passed,|not run' /tmp/launcher.out || true
exit 1
fi
echo "The suite reported its gaps, as it must on a runner:"
sed -n '/not run (mandatory gaps/,/^$/p' /tmp/launcher.out
- name: What this CI run did and did not establish
if: always()
run: |
cat >> "$GITHUB_STEP_SUMMARY" <<'SUMMARY'
### What a green run here means
| tier | established here? |
|---|---|
| component (unit tests, tool fixtures) | yes |
| developer host (this runner's kernel, unprivileged) | yes |
| **target kernel (root, installed system, userns restricted)** | **no — the Distro workflow's acceptance run** |
The privileged launch path, the cgroup limits, the zone lifecycle
under root and the boot are tested by `make acceptance` on the built
media under KVM, not here.
SUMMARY
- name: The user-facing command
run: |
./compartments/tests/cli.sh
- name: The launch daemon, over its socket
run: |
# A runner has no XDG_RUNTIME_DIR, which the registry and proxy-socket checks need.
uid="$(id -u)"
sudo install -d -m 0700 -o "$uid" -g "$(id -g)" "/run/user/$uid"
export XDG_RUNTIME_DIR="/run/user/$uid"
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/tests/serve.sh
- name: The zone boundary's regression probes
run: |
# Exits with the number of failures; root-only checks say NOT RUN and run in zones-test.
cd compartments/kryptikd && cargo build --quiet
cd ../.. && ./compartments/kryptikd/probes/boundary-checks.sh
kernel:
name: Kernel currency
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@fbc6f3992d24b796d5a048ff273f7fcc4a7b6c09 # v5.1.0
- name: Pinned series are supported upstream
run: ./tools/check-support-status.sh --strict
# Survey, then gate; informational on a pull request, since an upstream release is not its fault.
- name: Pins behind upstream are reviewed
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
./tools/check-source-currency.sh --tsv > pin-survey.tsv
echo "survey: $(wc -l < pin-survey.tsv) sources"
if [ "${{ github.event_name }}" = "pull_request" ]; then
./tools/check-pin-reviews.sh --survey pin-survey.tsv || echo "pull request: informational"
else
./tools/check-pin-reviews.sh --survey pin-survey.tsv
fi
- name: Pinned kernel is longterm and not EOL
run: ./tools/check-kernel-eol.sh
- name: Kernel config fragments validate against pinned source
run: |
# merge_config.sh drops unknown symbols silently; check each against the pinned source.
mkdir -p sources
# Exact names: '^linux ' would also match linux-hardened.
./tools/fetch-sources.sh --list \
| awk '$1 == "linux" || $1 == "linux-hardened" || $1 == "kernel-hardening-checker" {print $3}' \
| while read -r u; do
echo "fetching $(basename "$u")"
curl -fL --no-progress-meter -o "sources/$(basename "$u")" "$u"
done
./tools/validate-kernel-config.sh
./tools/validate-kernel-config.sh --hardened
./tools/validate-kernel-config.sh --boot
- name: Resolved kernel config keeps every fragment line and passes the hardening checker
run: |
# The GCC plugin options exist only with the plugin headers installed.
sudo apt-get install -y --no-install-recommends \
"gcc-$(gcc -dumpversion | cut -d. -f1)-plugin-dev" flex bison
make check-kernel-hardening