From 89e4f2941b13a33f0238a3df6d780e3b0bf02b21 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 27 Aug 2026 23:16:50 +0200 Subject: [PATCH 1/9] fix(ci): the two workflows broken by environment drift, with the real causes Issue #22 documented these with suspects; the raw log archive gave the actual answers, and both were environment changes dated to the breakage window. THE VM MATRIX: docker refuses to create from a CMD-less image 'Error response from daemon: no command specified'. The ci-kernels images are file carriers with no CMD, and the docker in runner images since 2026-08-23 refuses docker create without a command - all five matrix entries died identically at Fetch the kernel. create never executes the command, so a trailing placeholder word satisfies it. The other docker create in this file targets debian:bookworm-slim, which defines a CMD, and is untouched. SELINUX: one interface, two names, headers that know only one checkmodule: syntax error at token files_runtime_filetrans. Fedora's selinux-policy renamed files_pid_filetrans to files_runtime_filetrans with the /var/run -> /run migration, but the policy-devel headers in BOTH current CI containers define only the pid spelling, and an unknown interface is a hard parse error. m4 ifdef picks whichever the build host defines, so the module now compiles on either side of the rename. This PR's own CI is the proof: rhel.yml and ebpf.yml both trigger on the paths it touches. --- .github/workflows/ebpf.yml | 7 ++++++- packaging/selinux/colony_firewall.te | 10 ++++++++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index 70f9b85..adbd4ca 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -325,7 +325,12 @@ jobs: - name: Fetch the kernel run: | set -euo pipefail - docker create --name k "${{ matrix.image }}" + # The trailing word is load-bearing. ci-kernels images are file + # carriers with no CMD, and the docker shipped in runner images since + # 2026-08-23 refuses `docker create` without a command ("no command + # specified") - which broke all five matrix entries at once. create + # never executes the command, so any word satisfies it. + docker create --name k "${{ matrix.image }}" unused docker cp k:/boot/vmlinuz ./vmlinuz docker rm k ls -l ./vmlinuz diff --git a/packaging/selinux/colony_firewall.te b/packaging/selinux/colony_firewall.te index 2ca7bad..8b9a822 100644 --- a/packaging/selinux/colony_firewall.te +++ b/packaging/selinux/colony_firewall.te @@ -225,7 +225,17 @@ logging_log_filetrans(colony_firewalld_t, colony_firewall_log_t, { dir file }) manage_dirs_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) manage_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) manage_sock_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) +# Two names for one interface, picked at build time. Fedora's selinux-policy +# renamed files_pid_filetrans to files_runtime_filetrans when /var/run became +# /run, but the policy-devel headers in both current CI containers (rocky-9 +# AND fedora-latest) still define only the pid spelling - an unknown interface +# is a hard parse error, so naming just one breaks on the other side of the +# rename. m4's ifdef sees whichever the build host defines. +ifdef(`files_runtime_filetrans', ` files_runtime_filetrans(colony_firewalld_t, colony_firewall_runtime_t, { dir file sock_file }) +', ` +files_pid_filetrans(colony_firewalld_t, colony_firewall_runtime_t, { dir file sock_file }) +') ######################################## # From f1131307b2722d8aec04cbb6f3d76579bfa7f59d Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 27 Aug 2026 23:32:30 +0200 Subject: [PATCH 2/9] =?UTF-8?q?fix(ci):=20the=20second=20layer=20of=20both?= =?UTF-8?q?=20failures=20=E2=80=94=20the=20.if=20file,=20and=20the=20name?= =?UTF-8?q?=20length?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The first pass moved each error instead of clearing it, which is progress wearing failure's clothes, and both follow-ups are instructive. selinux: the .te compiled past line 228 and died at 268 - on OUR OWN colony_firewall_stream_connect interface, whose expansion in the .if file calls files_search_runtime. Same rename family, same fix, two more sites. The .if had never been grepped: the caller was searched, the definition was not, and m4 expansion is where the definition's body meets the compiler. vm matrix: the placeholder command got docker past 'no command specified' straight into 'Invalid container name (k)' - the same runner-image docker now rejects single-character container names (the regex in its own error message requires two). kernelsrc, three sites. Two environment breakages stacked on one command line, discovered one per run because each error masks the next. --- .github/workflows/ebpf.yml | 11 +++++++---- packaging/selinux/colony_firewall.if | 10 ++++++++++ 2 files changed, 17 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index adbd4ca..0629289 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -329,10 +329,13 @@ jobs: # carriers with no CMD, and the docker shipped in runner images since # 2026-08-23 refuses `docker create` without a command ("no command # specified") - which broke all five matrix entries at once. create - # never executes the command, so any word satisfies it. - docker create --name k "${{ matrix.image }}" unused - docker cp k:/boot/vmlinuz ./vmlinuz - docker rm k + # never executes the command, so any word satisfies it. The same + # docker also began rejecting single-character container names + # ("Invalid container name (k)"), hence the longer name - two + # environment breakages stacked on one line. + docker create --name kernelsrc "${{ matrix.image }}" unused + docker cp kernelsrc:/boot/vmlinuz ./vmlinuz + docker rm kernelsrc ls -l ./vmlinuz file ./vmlinuz || true diff --git a/packaging/selinux/colony_firewall.if b/packaging/selinux/colony_firewall.if index d65bcde..d798ae6 100644 --- a/packaging/selinux/colony_firewall.if +++ b/packaging/selinux/colony_firewall.if @@ -30,7 +30,13 @@ interface(`colony_firewall_stream_connect',` type colony_firewall_runtime_t; ') + # Same rename family as files_runtime_filetrans in the .te: current CI + # containers' policy-devel headers define only the pids spelling. + ifdef(`files_search_runtime', ` files_search_runtime($1) + ', ` + files_search_pids($1) + ') stream_connect_pattern($1, colony_firewall_runtime_t, colony_firewall_runtime_t, colony_firewalld_t) ') @@ -93,7 +99,11 @@ interface(`colony_firewall_admin',` files_search_var_lib($1) admin_pattern($1, colony_firewall_var_lib_t) + ifdef(`files_search_runtime', ` files_search_runtime($1) + ', ` + files_search_pids($1) + ') admin_pattern($1, colony_firewall_runtime_t) logging_search_logs($1) From 2b531de6b567a381bea61b74ac74ab15a5a658e2 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Thu, 27 Aug 2026 23:50:54 +0200 Subject: [PATCH 3/9] fix(ci): an initramfs for kernels with no ext4, and plain pid spelling for m4's sake THE VM MATRIX NEVER WORKED, AND THE GUEST FINALLY SAID WHY No filesystem could mount root, tried: vfat msdos iso9660 The pinned ci-kernels have no ext4 at all. The workflow's comment claimed 'virtio_blk + ext4 - both built in'; nobody had checked, and no run of this matrix has ever been green - issue #22's 'broken since Aug 25' timeline was this author's own unverified inference. The rootfs is now a full cpio initramfs: the kernel unpacks it into ramfs and runs /init, which requires no filesystem driver whatsoever. The earlier revision rejected 'an initramfs' believing it implied 9p/virtiofs modules - backwards: a self-contained initramfs is what removes every driver requirement. Layer three of the docker create saga stands: placeholder command, longer container name. SELINUX: THE CLEVER FIX BROKE M4 ITSELF m4:colony_firewall.te:250: non-numeric argument to builtin 'incr' Wrapping an interface call in ifdef arguments breaks refpolicy's line-counting machinery. Both CI containers define the classic pid spelling, so the .te and .if now just use it - files_pid_filetrans, files_search_pids - with a note saying why. If a future policy drops the compat spelling the error will name the line plainly, which beats clever. --- .github/workflows/ebpf.yml | 29 +++++++++++++++++----------- packaging/selinux/colony_firewall.if | 11 +---------- packaging/selinux/colony_firewall.te | 18 ++++++++--------- 3 files changed, 27 insertions(+), 31 deletions(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index 0629289..90ab416 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -307,7 +307,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y protobuf-compiler libnfnetlink-dev \ - libnetfilter-queue-dev qemu-system-x86 e2fsprogs + libnetfilter-queue-dev qemu-system-x86 cpio - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -339,12 +339,16 @@ jobs: ls -l ./vmlinuz file ./vmlinuz || true - # docker export -> mke2fs -d builds the image from a directory, so the - # test binary and the object are just files placed there beforehand. - # That removes the guest-transport problem entirely, and needs only - # virtio_blk + ext4 in the guest - both built in. virtiofs and 9p are - # MODULES on Ubuntu mainline builds, so any design that mounts the host - # rootfs that way needs an initramfs; this one does not. + # docker export -> cpio builds a full-rootfs initramfs, so the guest + # needs NO filesystem driver at all: the kernel unpacks it into ramfs + # and runs /init. This replaced an ext4 disk image after the guest's own + # panic message listed what these kernels can actually mount - + # "No filesystem could mount root, tried: vfat msdos iso9660". The + # previous comment here claimed ext4 was built in; nobody had checked, + # and the matrix had never once been green. An earlier revision also + # rejected "an initramfs" on the belief it implied 9p/virtiofs modules - + # backwards: a self-contained initramfs is exactly what removes every + # driver requirement. - name: Build the guest rootfs run: | set -euo pipefail @@ -381,8 +385,11 @@ jobs: INIT chmod +x rootfs/init - # Slack for the binary; the base image is ~80 MiB. - mke2fs -t ext4 -d rootfs -F rootfs.ext4 2G + # newc is the format the kernel's initramfs unpacker speaks; + # --owner=0:0 because the runner user's uid must not leak into the + # guest's idea of who owns /init. gzip -1: this is unpacked once. + (cd rootfs && find . | cpio -o -H newc --owner=0:0 | gzip -1) > rootfs.cpio.gz + ls -lh rootfs.cpio.gz - name: Boot and run run: | @@ -391,8 +398,8 @@ jobs: timeout 600 qemu-system-x86_64 \ -nographic -no-reboot -m 2G -smp 2 \ -kernel ./vmlinuz \ - -drive file=rootfs.ext4,if=virtio,format=raw \ - -append "root=/dev/vda rw init=/init console=ttyS0 panic=1" \ + -initrd rootfs.cpio.gz \ + -append "rdinit=/init console=ttyS0 panic=1" \ | tee guest.log ELAPSED=$(( $(date +%s) - START )) diff --git a/packaging/selinux/colony_firewall.if b/packaging/selinux/colony_firewall.if index d798ae6..76b0b7c 100644 --- a/packaging/selinux/colony_firewall.if +++ b/packaging/selinux/colony_firewall.if @@ -30,13 +30,8 @@ interface(`colony_firewall_stream_connect',` type colony_firewall_runtime_t; ') - # Same rename family as files_runtime_filetrans in the .te: current CI - # containers' policy-devel headers define only the pids spelling. - ifdef(`files_search_runtime', ` - files_search_runtime($1) - ', ` + # Classic spelling on purpose; see the .te's files_pid_filetrans note. files_search_pids($1) - ') stream_connect_pattern($1, colony_firewall_runtime_t, colony_firewall_runtime_t, colony_firewalld_t) ') @@ -99,11 +94,7 @@ interface(`colony_firewall_admin',` files_search_var_lib($1) admin_pattern($1, colony_firewall_var_lib_t) - ifdef(`files_search_runtime', ` - files_search_runtime($1) - ', ` files_search_pids($1) - ') admin_pattern($1, colony_firewall_runtime_t) logging_search_logs($1) diff --git a/packaging/selinux/colony_firewall.te b/packaging/selinux/colony_firewall.te index 8b9a822..d354361 100644 --- a/packaging/selinux/colony_firewall.te +++ b/packaging/selinux/colony_firewall.te @@ -225,17 +225,15 @@ logging_log_filetrans(colony_firewalld_t, colony_firewall_log_t, { dir file }) manage_dirs_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) manage_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) manage_sock_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) -# Two names for one interface, picked at build time. Fedora's selinux-policy -# renamed files_pid_filetrans to files_runtime_filetrans when /var/run became -# /run, but the policy-devel headers in both current CI containers (rocky-9 -# AND fedora-latest) still define only the pid spelling - an unknown interface -# is a hard parse error, so naming just one breaks on the other side of the -# rename. m4's ifdef sees whichever the build host defines. -ifdef(`files_runtime_filetrans', ` -files_runtime_filetrans(colony_firewalld_t, colony_firewall_runtime_t, { dir file sock_file }) -', ` +# The pid spelling, deliberately. Fedora renamed files_pid_filetrans to +# files_runtime_filetrans with the /var/run -> /run migration, but the +# policy-devel headers in both CI containers (rocky-9 AND fedora-latest) +# define only the classic name, and an unknown interface is a hard parse +# error. An ifdef around the interface was tried and broke m4 itself - +# refpolicy's line-counting machinery does not survive an interface call +# nested in ifdef arguments. If a future policy drops the compat spelling, +# the error will name this line plainly; that beats clever. files_pid_filetrans(colony_firewalld_t, colony_firewall_runtime_t, { dir file sock_file }) -') ######################################## # From b53c3c3ccd8a9b3684eb5586018c9be07d15addc Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Fri, 28 Aug 2026 00:07:57 +0200 Subject: [PATCH 4/9] =?UTF-8?q?fix(ci):=20round=20four=20=E2=80=94=20the?= =?UTF-8?q?=20init=20speaks=20and=20exits,=20the=20policy=20owes=20nothing?= =?UTF-8?q?=20to=20headers?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit VM: the register dump told the whole story. A docker-export rootfs has no device nodes, so /init ran with no stdout - an entire round executed invisibly - and then died at 127 on poweroff, which bookworm-slim does not ship. devtmpfs is now mounted first and the console re-exec'd before anything else prints; and exiting IS the shutdown: panic=1 reboots when PID 1 dies, -no-reboot turns that into a qemu exit, and the CFC_EXIT marker is already out by then. selinux: three rounds, three different failures, all inside interface machinery this module does not control - runtime spelling unknown, pid spelling's deprecation shim leaking file:line literals into the stream, ifdef breaking m4's line counter. The module now uses raw allow and type_transition statements against var_run_t, which is a *type*, aliased across every rename, and owes nothing to any header's health. A diagnostic grep of files.if ships with the job so a fifth round, if any, starts from ground truth instead of another guess. --- .github/workflows/ebpf.yml | 15 +++++++++++++-- .github/workflows/rhel.yml | 4 ++++ packaging/selinux/colony_firewall.if | 12 +++++++++--- packaging/selinux/colony_firewall.te | 24 +++++++++++++++--------- 4 files changed, 41 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index 90ab416..91ac33b 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -365,10 +365,17 @@ jobs: # Minimal PID 1. Everything the loader touches has to be mounted here: # tracefs for the tracepoint id/format files, cgroup2 for the DNS # program's attach point, sysfs for /sys/kernel/btf/vmlinux. + # + # /dev FIRST, then grab the console. A docker-export rootfs has no + # device nodes (tar cannot mknod unprivileged), so until devtmpfs is + # mounted this script runs with no stdout at all - an entire round of + # this matrix executed invisibly and died on its last line, and the + # only witness was exit_group(127) in the panic's register dump. + mount -t devtmpfs devtmpfs /dev + exec > /dev/console 2>&1 set -x mount -t proc proc /proc mount -t sysfs sysfs /sys - mount -t devtmpfs devtmpfs /dev mount -t tmpfs tmpfs /tmp mount -t tracefs tracefs /sys/kernel/tracing 2>/dev/null || \ mount -t debugfs debugfs /sys/kernel/debug @@ -381,7 +388,11 @@ jobs: CFC_EBPF_OBJECT=/cfc-ebpf.o /cfc-test --ignored --nocapture \ --test-threads=1 loads_and_attaches echo "CFC_EXIT=$?" - poweroff -f + # No poweroff: bookworm-slim ships no init system, so the binary does + # not exist - it was the 127 that killed PID 1 above. Exiting is the + # shutdown mechanism here: panic=1 reboots when init dies, and qemu's + # -no-reboot turns that reboot into an exit. The marker is already out. + exit 0 INIT chmod +x rootfs/init diff --git a/.github/workflows/rhel.yml b/.github/workflows/rhel.yml index f5ac57e..78db13b 100644 --- a/.github/workflows/rhel.yml +++ b/.github/workflows/rhel.yml @@ -71,6 +71,10 @@ jobs: run: | set -eux cd packaging/selinux + # Ground truth first: if the build below fails, this output is what + # ends four rounds of guessing what these headers actually define. + grep -n "interface(\`files_search_pids\|interface(\`files_search_runtime\|interface(\`files_pid_filetrans\|interface(\`files_runtime_filetrans" \ + /usr/share/selinux/devel/include/kernel/files.if || true make -f /usr/share/selinux/devel/Makefile colony_firewall.pp ls -l colony_firewall.pp diff --git a/packaging/selinux/colony_firewall.if b/packaging/selinux/colony_firewall.if index 76b0b7c..2f08c68 100644 --- a/packaging/selinux/colony_firewall.if +++ b/packaging/selinux/colony_firewall.if @@ -30,8 +30,11 @@ interface(`colony_firewall_stream_connect',` type colony_firewall_runtime_t; ') - # Classic spelling on purpose; see the .te's files_pid_filetrans note. - files_search_pids($1) + # Raw statement; see the .te's var_run_t note for why no files_* interface. + gen_require(` + type var_run_t; + ') + allow $1 var_run_t:dir search; stream_connect_pattern($1, colony_firewall_runtime_t, colony_firewall_runtime_t, colony_firewalld_t) ') @@ -94,7 +97,10 @@ interface(`colony_firewall_admin',` files_search_var_lib($1) admin_pattern($1, colony_firewall_var_lib_t) - files_search_pids($1) + gen_require(` + type var_run_t; + ') + allow $1 var_run_t:dir search; admin_pattern($1, colony_firewall_runtime_t) logging_search_logs($1) diff --git a/packaging/selinux/colony_firewall.te b/packaging/selinux/colony_firewall.te index d354361..dd7b2d5 100644 --- a/packaging/selinux/colony_firewall.te +++ b/packaging/selinux/colony_firewall.te @@ -225,15 +225,21 @@ logging_log_filetrans(colony_firewalld_t, colony_firewall_log_t, { dir file }) manage_dirs_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) manage_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) manage_sock_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_firewall_runtime_t) -# The pid spelling, deliberately. Fedora renamed files_pid_filetrans to -# files_runtime_filetrans with the /var/run -> /run migration, but the -# policy-devel headers in both CI containers (rocky-9 AND fedora-latest) -# define only the classic name, and an unknown interface is a hard parse -# error. An ifdef around the interface was tried and broke m4 itself - -# refpolicy's line-counting machinery does not survive an interface call -# nested in ifdef arguments. If a future policy drops the compat spelling, -# the error will name this line plainly; that beats clever. -files_pid_filetrans(colony_firewalld_t, colony_firewall_runtime_t, { dir file sock_file }) +# Raw statements, not an interface, and the history earns the verbosity. +# The runtime spelling (files_runtime_filetrans) is an unknown token in both +# CI containers' devel headers; the pid spelling exists but its deprecation +# shim leaks a literal file:line into the m4 stream and breaks the parse; +# and wrapping either in ifdef broke m4's own line counting. Three rounds, +# three different failures, all inside interface machinery this module does +# not control. var_run_t is a *type*, aliased across every rename, and raw +# allow/type_transition statements owe nothing to any header's health. +gen_require(` + type var_run_t; +') +allow colony_firewalld_t var_run_t:dir { getattr search open read write add_name remove_name }; +type_transition colony_firewalld_t var_run_t:dir colony_firewall_runtime_t; +type_transition colony_firewalld_t var_run_t:file colony_firewall_runtime_t; +type_transition colony_firewalld_t var_run_t:sock_file colony_firewall_runtime_t; ######################################## # From 235426278ce19b315d1dd6388ecf18ac7f9d94ef Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Fri, 28 Aug 2026 00:22:18 +0200 Subject: [PATCH 5/9] =?UTF-8?q?fix(ci):=20round=20five=20=E2=80=94=20a=20g?= =?UTF-8?q?uest=20glibc=20that=20can=20load=20the=20binary,=20and=20zero?= =?UTF-8?q?=20m4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The console fix paid off immediately: the guest now narrates its own failure. Every mount succeeds, the marker prints, init exits cleanly - and the test binary cannot load: built on the ubuntu-noble runner against glibc 2.39, refused by bookworm's 2.36. trixie-slim's glibc covers it; the guest userland must be at least as new as the build host. And the fourth selinux failure was mine, said plainly by the error: my multi-line gen_require unbalanced m4's quoting. Round five removes m4 from the equation entirely - a native require { type var_run_t; } block, which is checkmodule syntax, not macro expansion. The ground-truth grep shipped last round stays: if a sixth failure exists, it will name itself. --- .github/workflows/ebpf.yml | 7 ++++++- packaging/selinux/colony_firewall.if | 8 ++------ packaging/selinux/colony_firewall.te | 4 ++-- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index 91ac33b..49edd0f 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -353,7 +353,12 @@ jobs: run: | set -euo pipefail mkdir -p rootfs - docker create --name rfs debian:bookworm-slim + # trixie, not bookworm, and the constraint is the builder's glibc: + # the test binary is compiled on the ubuntu-noble runner (glibc 2.39) + # and bookworm's 2.36 refuses to load it - the guest said so plainly + # once it had a console: "version GLIBC_2.39 not found". The guest + # userland must be at least as new as the build host's. + docker create --name rfs debian:trixie-slim docker export rfs | tar -C rootfs -xf - docker rm rfs diff --git a/packaging/selinux/colony_firewall.if b/packaging/selinux/colony_firewall.if index 2f08c68..6d13cb1 100644 --- a/packaging/selinux/colony_firewall.if +++ b/packaging/selinux/colony_firewall.if @@ -31,9 +31,8 @@ interface(`colony_firewall_stream_connect',` ') # Raw statement; see the .te's var_run_t note for why no files_* interface. - gen_require(` - type var_run_t; - ') + # The type is required by the .te's own require block; callers outside this + # module must require var_run_t themselves. allow $1 var_run_t:dir search; stream_connect_pattern($1, colony_firewall_runtime_t, colony_firewall_runtime_t, colony_firewalld_t) ') @@ -97,9 +96,6 @@ interface(`colony_firewall_admin',` files_search_var_lib($1) admin_pattern($1, colony_firewall_var_lib_t) - gen_require(` - type var_run_t; - ') allow $1 var_run_t:dir search; admin_pattern($1, colony_firewall_runtime_t) diff --git a/packaging/selinux/colony_firewall.te b/packaging/selinux/colony_firewall.te index dd7b2d5..968ffb0 100644 --- a/packaging/selinux/colony_firewall.te +++ b/packaging/selinux/colony_firewall.te @@ -233,9 +233,9 @@ manage_sock_files_pattern(colony_firewalld_t, colony_firewall_runtime_t, colony_ # three different failures, all inside interface machinery this module does # not control. var_run_t is a *type*, aliased across every rename, and raw # allow/type_transition statements owe nothing to any header's health. -gen_require(` +require { type var_run_t; -') +} allow colony_firewalld_t var_run_t:dir { getattr search open read write add_name remove_name }; type_transition colony_firewalld_t var_run_t:dir colony_firewall_runtime_t; type_transition colony_firewalld_t var_run_t:file colony_firewall_runtime_t; From bb2a5f237cfe2f5ed7e605221b8257fc60b96225 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Fri, 28 Aug 2026 00:38:55 +0200 Subject: [PATCH 6/9] =?UTF-8?q?fix(ci):=20round=20six=20=E2=80=94=20one=20?= =?UTF-8?q?possessive=20apostrophe,=20and=20a=20loopback=20nobody=20raised?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit selinux: the wreckage of rounds three through five - deprecation shims apparently leaking file:line literals, gen_require unbalancing m4 - was one bug, and it was mine: refpolicy disables the m4 comment character, so inside the backtick quotes of an interface body an apostrophe terminates the quote. My comments said the te-apostrophe-s, twice. Every exotic symptom downstream was quoting poisoned at that character. The comments are reworded, the raw statements stay - they are proven independent of header health - and a warning comment now stands where the trap is. vm: the test finally ran and failed honestly - ENETUNREACH sending to 127.0.0.1, because loopback was never up: trixie-slim ships neither ip nor ifconfig and the init line failed into its own || true. A static busybox from the runner brings lo up and owes the guest libc nothing. --- .github/workflows/ebpf.yml | 9 +++++++-- packaging/selinux/colony_firewall.if | 10 +++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index 49edd0f..b26c5b9 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -307,7 +307,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y protobuf-compiler libnfnetlink-dev \ - libnetfilter-queue-dev qemu-system-x86 cpio + libnetfilter-queue-dev qemu-system-x86 cpio busybox-static - uses: Swatinem/rust-cache@6323deb102c322ba6fcbdcafc7e3dddab59af2b6 # v2.9.2 - uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: @@ -363,6 +363,11 @@ jobs: docker rm rfs install -D -m0755 "${{ steps.bin.outputs.path }}" rootfs/cfc-test + # Static busybox from the runner: trixie-slim ships neither ip nor + # ifconfig, so the init line that brings loopback up was failing into + # its own `|| true` - and the DNS probe then got ENETUNREACH on + # 127.0.0.1. A static binary owes the guest libc nothing. + install -D -m0755 "$(command -v busybox)" rootfs/busybox install -D -m0644 obj/cfc-ebpf.o rootfs/cfc-ebpf.o cat > rootfs/init <<'INIT' @@ -385,7 +390,7 @@ jobs: mount -t tracefs tracefs /sys/kernel/tracing 2>/dev/null || \ mount -t debugfs debugfs /sys/kernel/debug mount -t cgroup2 cgroup2 /sys/fs/cgroup - ip link set lo up 2>/dev/null || ifconfig lo up 2>/dev/null || true + /busybox ip link set lo up echo "guest glibc: $(ldd --version 2>&1 | head -1)" echo "guest kernel: $(uname -r)" diff --git a/packaging/selinux/colony_firewall.if b/packaging/selinux/colony_firewall.if index 6d13cb1..4615485 100644 --- a/packaging/selinux/colony_firewall.if +++ b/packaging/selinux/colony_firewall.if @@ -30,9 +30,13 @@ interface(`colony_firewall_stream_connect',` type colony_firewall_runtime_t; ') - # Raw statement; see the .te's var_run_t note for why no files_* interface. - # The type is required by the .te's own require block; callers outside this - # module must require var_run_t themselves. + # Raw statement; see the var_run_t note in the te file for why no files_* + # interface is used. The type is required by that same require block; + # callers outside this module must require var_run_t themselves. + # NO APOSTROPHES in comments inside interface bodies: refpolicy disables + # the m4 comment character, so inside the backtick quotes of a body an + # apostrophe TERMINATES the quote. One possessive apostrophe here cost + # three CI rounds of increasingly exotic-looking m4 wreckage. allow $1 var_run_t:dir search; stream_connect_pattern($1, colony_firewall_runtime_t, colony_firewall_runtime_t, colony_firewalld_t) ') From 91eff7eac793dd81ff39a7897bf753c0c09d3d22 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Fri, 28 Aug 2026 00:59:41 +0200 Subject: [PATCH 7/9] fix(test): the verified-insns assertion assumed a 5.16+ kernel The 5.10 guest finally ran the suite and failed honestly: the test asserted verified_insns is never empty, but bpf_prog_info.verified_insns only exists since kernel 5.16 - verifier_cost() even documents returning None on older kernels, two screens above the assertion that ignored it. The assertion now checks the running kernel first, so it still catches the loader silently losing the counts on kernels that do provide them. Also in that guest log, a note read '... enforcement cannot be pinned and will stop when this daemon does; it will stop when this daemon does': the inner bpffs error and its wrapper each appended the same consequence. The inner error now states the fact only; the wrapper owns the consequence for every error path, not just that one. --- crates/cfc-daemon/src/ebpf/enforce.rs | 7 +++++-- crates/cfc-daemon/src/ebpf/loader.rs | 17 +++++++++++++---- 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/cfc-daemon/src/ebpf/enforce.rs b/crates/cfc-daemon/src/ebpf/enforce.rs index 8a93262..57e81a4 100644 --- a/crates/cfc-daemon/src/ebpf/enforce.rs +++ b/crates/cfc-daemon/src/ebpf/enforce.rs @@ -578,9 +578,12 @@ fn is_bpffs(path: &Path) -> io::Result { pub(super) fn prepare() -> anyhow::Result { let root = Path::new(BPFFS); if !is_bpffs(root).with_context(|| format!("stat {BPFFS}"))? { + // States the fact only. The caller in the loader wraps every error + // from here with the consequence ("cannot be pinned; it will stop + // when this daemon does") - repeating it here printed the clause + // twice in one note. return Err(anyhow!( - "{BPFFS} is not a bpffs mount (mount -t bpf bpffs {BPFFS}); \ - enforcement cannot be pinned and will stop when this daemon does" + "{BPFFS} is not a bpffs mount (mount -t bpf bpffs {BPFFS})" )); } let ns = root.join(PIN_NAMESPACE); diff --git a/crates/cfc-daemon/src/ebpf/loader.rs b/crates/cfc-daemon/src/ebpf/loader.rs index 53da4f3..728a930 100644 --- a/crates/cfc-daemon/src/ebpf/loader.rs +++ b/crates/cfc-daemon/src/ebpf/loader.rs @@ -1622,10 +1622,19 @@ mod tests { for (program, insns) in &report.verified_insns { println!("verified_insns: {program} = {insns}"); } - assert!( - !report.verified_insns.is_empty(), - "this kernel reports verified instruction counts; they should be recorded" - ); + // `bpf_prog_info.verified_insns` exists since kernel 5.16; before + // that, an empty report is the correct answer, not a recording + // failure. The assertion exists to catch the loader silently losing + // the counts on kernels that do provide them. + let kernel_reports_counts = aya::util::KernelVersion::current() + .map(|v| v >= aya::util::KernelVersion::new(5, 16, 0)) + .unwrap_or(true); + if kernel_reports_counts { + assert!( + !report.verified_insns.is_empty(), + "this kernel reports verified instruction counts; they should be recorded" + ); + } assert_within_verifier_budget(&report.verified_insns); println!("dns_capture = {}", report.dns_capture); assert!( From b37217d84ea4b9fa5442c10a0c77d09fbacf1e0a Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Fri, 28 Aug 2026 01:16:04 +0200 Subject: [PATCH 8/9] fix(ci): the guest verdict was already the marker - stop letting qemu overrule it On 5.10 the suite now passes inside the guest (CFC_EXIT=0), and the job still failed: under set -euo pipefail the step died on qemu's own exit code. The shutdown is a deliberate panic - init exits under panic=1 and -no-reboot turns the reset into a qemu exit - and how qemu reports that reset varies by guest kernel: the 6.x reboot path exits 0, the 5.10 one exits 1, for the same healthy shutdown. The step already refused to trust qemu on the other side (a boot failure must not read as a pass, hence the marker); now it also refuses on this side. A hang or a crashed qemu is still caught: either way the marker never got printed. --- .github/workflows/ebpf.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index b26c5b9..7ed3236 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -416,13 +416,21 @@ jobs: run: | set -euo pipefail START=$(date +%s) + # qemu's exit code is not the verdict; the CFC_EXIT marker below is. + # The shutdown here is a deliberate panic (init exits under panic=1) + # and how qemu reports the resulting reset request varies by guest + # kernel: the 6.x reboot path exits 0, the 5.10 one exits 1, for the + # same healthy shutdown. A hang or a crashed qemu is still caught - + # either way the marker never got printed. + QEMU_RC=0 timeout 600 qemu-system-x86_64 \ -nographic -no-reboot -m 2G -smp 2 \ -kernel ./vmlinuz \ -initrd rootfs.cpio.gz \ -append "rdinit=/init console=ttyS0 panic=1" \ - | tee guest.log + | tee guest.log || QEMU_RC=$? ELAPSED=$(( $(date +%s) - START )) + echo "qemu exit: ${QEMU_RC} (informational)" # A guest that never printed the marker did not run the test - a # boot failure must not read as a pass. From f5e4e6cdc366b638f50b0cf031a58148f17cf646 Mon Sep 17 00:00:00 2001 From: MotherSphere Date: Fri, 28 Aug 2026 01:31:18 +0200 Subject: [PATCH 9/9] fix(ci): the summary grep assumed verified-insn counts exist - same bug, second home On 5.10 the suite passes, qemu exits 0, and the step still died with no message: the step-summary block greps guest.log for verified_insns lines, kernels before 5.16 report none, grep exits 1 on no matches, and under pipefail as the last command of the brace group that took the whole step down two lines before exit CODE. This is the identical assumption the round-7 test fix removed, living in the workflow instead of the test: no counts is what an old kernel reporting honestly looks like. Swept the workflows for the class: every other unguarded grep either always matches or is a deliberate assertion. --- .github/workflows/ebpf.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ebpf.yml b/.github/workflows/ebpf.yml index 7ed3236..615da22 100644 --- a/.github/workflows/ebpf.yml +++ b/.github/workflows/ebpf.yml @@ -447,9 +447,15 @@ jobs: echo "|---|---|" echo "| exit | ${CODE} |" echo "| boot+test wall clock | ${ELAPSED}s |" + # `|| true`: kernels before 5.16 report no verified-insn counts, + # so on 5.10 this grep matches nothing - which under pipefail was + # exit 1, and as the last command of this group it took the whole + # step down after the suite had already passed. Same lesson as the + # test assertion this round fixed: no counts is what an old kernel + # reporting honestly looks like. grep -o 'verified_insns: [^ ]* = [0-9]*' guest.log | while read -r _ p _ n; do echo "| \`${p}\` verified insns | ${n} |" - done + done || true } >> "${GITHUB_STEP_SUMMARY}" # Wall clock, not just instruction count. They are different