From b94403c70b4b97d4f47e5c64e791f73dce3cc081 Mon Sep 17 00:00:00 2001 From: Manuel de Brito Fontes Date: Thu, 12 Nov 2026 01:06:17 -0300 Subject: [PATCH] kernel: the console's lockdown lines are said, and compat.yml boots the lockdown check The README names both lines a guest's console prints at boot, `bpf-restrict-fs: Failed to load BPF object` and `use of bpf to read kernel RAM is restricted`: lockdown refusing bpf_probe_read_kernel to systemd, which costs the guest's units `RestrictFileSystems=`. Kept on purpose; integrity would let root read the kernel again. `task boot:lockdown` ran only by hand. compat.yml runs it after boot:compat, on this change's kernel, so it runs on every kernel change on main and before every release. boot:here kept only the binaries it replaces and dropped the release's e2fsprogs with them; it now removes only QEMU, so the test's debugfs is the release's, beside its image. Refs #231 Co-Authored-By: Claude Opus 5.5 --- .github/workflows/compat.yml | 11 ++++++++++- boot/Taskfile.yml | 8 +++++--- kernel/README.md | 21 ++++++++++++++++++--- 3 files changed, 33 insertions(+), 7 deletions(-) diff --git a/.github/workflows/compat.yml b/.github/workflows/compat.yml index 4dd248f..f8e3dc9 100644 --- a/.github/workflows/compat.yml +++ b/.github/workflows/compat.yml @@ -3,7 +3,8 @@ name: Compat # Whether a checkpoint the newest release saved resumes on this change's machine, and whether the # fingerprint still accounts for every ROM the machine maps (boot:compat, boot:roms): after every # change on main to what a resumed guest is loaded into - the machine's definition, QEMU, the -# kernel, the firmware and their pins - and before every release. +# kernel, the firmware and their pins - and before every release. And whether that kernel, booted, +# is locked down (boot:lockdown). # # On the KVM fleet (spin-stack/ci), because it boots: the newest release is unpacked as the machine # before, and the machine here is that tree with this change's QEMU, firmware and kernel built over @@ -29,6 +30,8 @@ on: - kernel/** - boot/compat_test.go - boot/roms_test.go + - boot/lockdown_test.go + - boot/testdata/lockdown-check.sh - versions.yaml - .github/workflows/compat.yml workflow_dispatch: @@ -91,3 +94,9 @@ jobs: - name: A checkpoint of the release before resumes here run: go tool task boot:compat + + # This change's kernel, booted: lockdown at confidentiality that root cannot lower, the BPF + # LSM, no /dev/mem or /proc/kcore. kernel/Dockerfile checks the config; this checks the + # kernel that came out of it. + - name: The kernel is locked down + run: go tool task boot:lockdown diff --git a/boot/Taskfile.yml b/boot/Taskfile.yml index 8b3ecf0..f41be33 100644 --- a/boot/Taskfile.yml +++ b/boot/Taskfile.yml @@ -66,7 +66,9 @@ tasks: kernel and spin-machine in place of its own, in _output/compat/here. Both are exported to the steps after in CI (SPIN_COMPAT_PREVIOUS, SPIN_MACHINE_OUTPUT). Needs a built QEMU, kernel and tools. - # The base image is the release's: a checkpoint resumes with the disks it was saved with. + # The base image is the release's: a checkpoint resumes with the disks it was saved with. So + # are the e2fsprogs tools that edit a copy of it (boot:lockdown's debugfs), which is why bin/ + # loses only what this tree replaces; the release's TCG QEMU goes with its QEMU. cmds: - task: previous - | @@ -74,8 +76,8 @@ tasks: previous=$(cat {{.OUTPUT_ABS}}/compat/previous) here={{.OUTPUT_ABS}}/compat/here rm -rf "$here" && cp -as "$previous" "$here" - rm -rf "$here/bin" "$here/qemu" "$here/kernel" - mkdir -p "$here/bin" "$here/kernel" + rm -rf "$here"/bin/qemu-* "$here/qemu" "$here/kernel" + mkdir -p "$here/kernel" cp {{.OUTPUT_ABS}}/bin/qemu-system-x86_64 {{.OUTPUT_ABS}}/bin/qemu-img {{.OUTPUT_ABS}}/bin/spin-machine "$here/bin/" cp -r {{.OUTPUT_ABS}}/qemu "$here/qemu" cp {{.OUTPUT_ABS}}/kernel/vmlinux "$here/kernel/vmlinux" diff --git a/kernel/README.md b/kernel/README.md index f85de6d..09a2aba 100644 --- a/kernel/README.md +++ b/kernel/README.md @@ -55,12 +55,27 @@ The guest's root is not the kernel's. The kernel runs two LSMs, `lockdown` and ` it, root reads or rewrites the kernel those programs live in. What enforcing the policy is for, and how, is the consumer's. That this kernel does both is -what `kernel/Dockerfile` checks after olddefconfig, and `task boot:lockdown` asks a booted guest. +what `kernel/Dockerfile` checks after olddefconfig, and `task boot:lockdown` asks a booted guest: +`compat.yml` runs it on every kernel change on main and before every release. Lockdown at confidentiality also takes away the tenant's kernel tracing: kprobes, `bpf_probe_read_kernel` and perf on the kernel. User-space tracing and networking eBPF are -untouched. systemd notices at boot: it logs `use of bpf to read kernel RAM is restricted` and -carries on, and logins are unaffected (`task boot:logind`). +untouched, and so is a BPF LSM program that reads no kernel memory. + +systemd notices at boot, and the console shows two lines that read like missing protection +and are lockdown working: + +``` +bpf-restrict-fs: Failed to load BPF object: Invalid argument +Lockdown: init: use of bpf to read kernel RAM is restricted; see man kernel_lockdown.7 +``` + +The second is lockdown refusing `bpf_probe_read_kernel` to PID 1. The first is what that costs: +systemd's `RestrictFileSystems=` program reads each file's filesystem type out of kernel memory +with that helper, the verifier rejects it, and systemd carries on without it. So a unit in the +guest cannot use `RestrictFileSystems=`; it is not enforced, and systemd does not say so per +unit. That is the price of confidentiality, kept on purpose: integrity would give it back by +letting root read the kernel's memory again. Logins are unaffected (`task boot:logind`). Measured 2026-10-01, `task boot:initcalls` with `SPIN_KERNEL_B` set to the kernel without these options: no cost that can be told from noise. Over 20 interleaved boots each, kernel start to