Skip to content

The Aegis-built kernel, and what the guest said about it

Status: recorded observations from milestone M26, reference profile, 2026-09-13; rebuilt on 2026-09-29 for D107; retired on 2026-10-17 by D112

Retired by D112 (2026-10-17). Nucleus now publishes the kernel this page built. Its release v7.2.8-realtime-lusoris2, pinned in build/kernel/nucleus-artifact.pin.json, passed every M10 case on 2026-10-11 (the Nucleus kernel gate), and on 2026-10-17 the M23 latency fixture booted it in place of this build (the latency fixture). build/kernel/source.pin.json, tools/verify_kernel_build.py, tools/test_kernel_build.py and tools/guest/aegis-readback-init.sh were removed with that decision, and make verify-kernel no longer exists. The two fragments stay, bound to the schema by crates/aegis-fabrica-defs/tests/kernel_fragment.rs. The rest of this page is the record of what the retired gate did; the files it quotes are in the history up to commit 47e4544.

Decision D70 puts kernel construction in this repository while Nucleus is a scaffold, exactly as D56 keeps image-definition validation here while Imago is a scaffold: a repository cannot defer construction to a producer that cannot yet produce. This page records what was built, how the source was verified, what the guest reported from inside itself, and what a pass here does not mean.

Nothing on this page is a release artifact. No image was packaged, nothing was signed, nothing was installed, no bootloader entry was written, no module was loaded on the host, and no device was touched. Construction returns to Nucleus, which owns it under docs/integration/stack.md, once Nucleus returns real artifacts against the contract M09 pinned; the kernel result is M10's under D92. Until then this is development evidence on one workstation, and it closes no boot, hardware or release gate.

What is tracked, and what is not

Since D112 the pin, the gate and the guest init this section listed are removed; the table names what stays.

Path Role
build/kernel/10-base-support.config the Kconfig and guest-boot prerequisites; states no product requirement
build/kernel/50-aegis-requirement.config the product requirement, byte-identical to what KernelRequirement::config_fragment renders
build/kernel/nucleus-artifact.pin.json the Nucleus kernel the gates boot since D112

Not tracked, and deliberately so: the source tree, the object tree, the produced .config, bzImage, the modules, the initramfs and the guest console. They live under AEGIS_KERNEL_BUILD_DIR, default ${XDG_CACHE_HOME:-$HOME/.cache}/aegis-kernel. A full .config is 5,545 lines of which fourteen are the requirement (5,520 and thirteen before D107); tracking it would bury the requirement in the base configuration and make every upstream default a reviewed decision.

The pinned source, and how it was verified

linux-7.2.5, the stable release current at the revision date, from https://cdn.kernel.org/pub/linux/kernel/v7.x/linux-7.2.5.tar.xz.

$ sha256sum linux-7.2.5.tar.xz
55ddf0df8325d9dad96fcff7bd93977d22e3f50af06527572af59b77c7632b78  linux-7.2.5.tar.xz

$ grep linux-7.2.5.tar.xz sha256sums.asc
55ddf0df8325d9dad96fcff7bd93977d22e3f50af06527572af59b77c7632b78  linux-7.2.5.tar.xz

$ gpg --verify sha256sums.asc
gpg: Good signature from "Kernel.org checksum autosigner <autosigner@kernel.org>"
Primary key fingerprint: B886 8C80 BA62 A1FF FAF5  FDA9 632D 3A06 589D A6B1

$ xz -dc linux-7.2.5.tar.xz | gpg --verify linux-7.2.5.tar.sign -
gpg: Good signature from "Greg Kroah-Hartman <gregkh@linuxfoundation.org>"
Primary key fingerprint: 647F 2865 4894 E3BD 4571  99BE 38DB BDC8 6092 693E

Both keys are fetched once into a keyring under the build directory. Neither is certified by a local trust path, so gpg also prints its "not certified with a trusted signature" warning; the pin asserts the fingerprint, not a web of trust. The gate refuses a source whose signature is good but made by some other key.

CONFIG_PREEMPT_RT needs no out-of-tree patch here, and that was read out of the pinned source rather than assumed: kernel/Kconfig.preempt carries config PREEMPT_RT with depends on EXPERT && ARCH_SUPPORTS_RT && !COMPILE_TEST, and arch/x86/Kconfig selects ARCH_SUPPORTS_RT.

The configuration: a fragment on a named base

The base is x86_64_defconfig. The two tracked fragments are applied in order by the kernel's own scripts/kconfig/merge_config.sh -m, then resolved with make olddefconfig.

50-aegis-requirement.config is generated, not written. It is what KernelRequirement::config_fragment renders from build/kernel-requirement.json -- one assignment per feature row with its requirement identifier on the line above -- and crates/aegis-fabrica-defs/tests/kernel_fragment.rs fails if the tracked file and the renderer's output differ by a byte. That is what makes the M18 schema the source of the requirement rather than a description of a hand-written file.

10-base-support.config carries only prerequisites, each with the Kconfig dependency it satisfies named above it: CONFIG_EXPERT for PREEMPT_RT, BPF_JIT/SECURITY/SECURITYFS/KPROBES/PERF_EVENTS for BPF_LSM, FTRACE/FUNCTION_TRACER/DYNAMIC_FTRACE for DYNAMIC_FTRACE_WITH_DIRECT_CALLS (since D107), the "Debug information" choice for DEBUG_INFO_BTF, PCI/IOSF_MBI for INTEL_RAPL, an IOMMU driver so the non-selectable IOMMU_API can be y, IKCONFIG_PROC for the read-back, the serial and initrd options for the guest, and CONFIG_LOCALVERSION="-aegis-m26" with LOCALVERSION_AUTO off. It assigns no symbol the payload demands, and a test fails if it ever does -- otherwise a hand-written file could satisfy a schema row while the generated fragment said nothing.

What was built

$ make O=<build>/out/positive -j32 bzImage modules
Kernel: arch/x86/boot/bzImage is ready  (#1)

69 seconds on the reference profile's 32 threads, from a freshly configured tree. The artifacts are a 16,438,272-byte bzImage and 19 modules, among them vfio.ko, vfio-pci.ko, vfio_iommu_type1.ko, intel_rapl_common.ko and intel_rapl_msr.ko -- the module-state rows of the payload, compiled rather than merely configured.

One honest detail about CONFIG_KVM=m: the produced configuration carries it, which is what the requirement row asks about, but no kvm.ko is produced. arch/x86/kvm/Kconfig makes the module that actually gets built KVM_X86, def_tristate KVM if (KVM_INTEL != n || KVM_AMD != n), and x86_64_defconfig selects neither vendor module. The row is satisfied as a Kconfig state; a KVM module object is not part of this evidence.

The read-back, from inside the guest

The guest is a minimal initramfs booted by qemu-system-x86_64 -enable-kvm -cpu max -kernel <bzImage> -initrd <cpio>. Its /init mounts its own /proc, and reports on a second serial line so that a kernel printk cannot splice itself into the configuration being reported. CONFIG_IKCONFIG_PROC is why /proc/config.gz exists at all: what the guest prints is the configuration compiled into the image that is executing the script, not a file from the build tree.

The guest's own output, verbatim, first five lines and last two:

AEGIS-M26-BEGIN
AEGIS-M26-UNAME-R 7.2.5-aegis-m26
AEGIS-M26-UNAME-V #1 SMP PREEMPT_RT Sun Sep 13 18:32:20 CEST 2026
AEGIS-M26-UNAME-M x86_64
AEGIS-M26-CONFIG-BEGIN
...
AEGIS-M26-CONFIG-END
AEGIS-M26-END

and the required options as the guest printed them, with the line number each sits on in the guest's own dump:

 49:CONFIG_LOCALVERSION="-aegis-m26"
134:CONFIG_BPF_SYSCALL=y
140:CONFIG_BPF_LSM=y
147:CONFIG_PREEMPT_RT=y
153:CONFIG_SCHED_CLASS_EXT=y
190:CONFIG_IKCONFIG_PROC=y
238:CONFIG_CGROUP_BPF=y
503:CONFIG_HZ_1000=y
504:CONFIG_HZ=1000
740:CONFIG_KVM=m
4146:CONFIG_VFIO=m
4159:CONFIG_VFIO_PCI=m
4328:CONFIG_IOMMU_API=y
4482:CONFIG_POWERCAP=y
4484:CONFIG_INTEL_RAPL=m
5197:CONFIG_DEBUG_INFO_BTF=y

The host's own kernel at the time was 7.2.4-1-cachyos with # CONFIG_PREEMPT_RT is not set. Three separate things therefore have to hold before the read-back passes, and each is checked:

  1. the release the guest reports equals the release this build produced (include/config/kernel.release, 7.2.5-aegis-m26) and is not the host's;
  2. every requirement row holds in the text the guest printed: thirteen on 2026-09-13, fourteen since D107;
  3. that text is the produced .config byte for byte -- 5,520 lines. A read-back that came from anywhere but the running guest kernel could not reproduce it.

The guest then powers itself off through magic SysRq (reboot: Power down on the console), so the gate's deadline is a failure signal rather than the normal exit path.

The cases, and what each proves

make verify-kernel runs seven cases. None of them suppresses a failure and none uses || true. Each is its own unit: a run that fails two of them reports two failures. Once the gate is past its toolchain check it runs all seven, with one exception: kernel/readback-negative-missing-option needs the host's own /proc/config.gz and is skipped, by name, on a kernel that publishes none. The skip takes nothing else with it -- kernel/readback-boundary-host-kernel reads only uname -r, so it still runs and is still counted.

Case What it does Outcome
kernel/positive the two tracked fragments on x86_64_defconfig every requirement row satisfied
kernel/negative-contradictory appends # CONFIG_PREEMPT_RT is not set refused: CONFIG_PREEMPT_RT: REQ-P07-01 requires built-in, observed 'n'
kernel/boundary-builtin-where-module appends CONFIG_VFIO=y where the schema demands a module refused: CONFIG_VFIO: REQ-P03-06 requires module, observed 'y'
kernel/boundary-module-where-builtin appends CONFIG_BPF_SYSCALL=m where the schema demands built-in kconfig resolved the request to 'n'; refused, naming CONFIG_BPF_SYSCALL and the two rows that fell with it
kernel/readback-positive boots the built image, reads /proc/config.gz in the guest release, rows and byte-identity all hold
kernel/readback-negative-missing-option the same check over the host's own /proc/config.gz refused: CONFIG_PREEMPT_RT: REQ-P07-01 requires built-in, observed 'n'
kernel/readback-boundary-host-kernel the host's release through the identity check refused twice: not the built release, and it is the host's own

The fourth case is the interesting one and is worth stating plainly, because it is the failure mode this whole gate exists for. Every symbol the payload demands built-in is a bool in the pinned source, so =m is not a state any of them can reach. kconfig neither honours the line nor complains about it: it resolves CONFIG_BPF_SYSCALL to n. A fragment asking for a module therefore yields a kernel without the feature rather than one carrying it as a module, and CONFIG_BPF_LSM and CONFIG_SCHED_CLASS_EXT disappear with it. Nothing in the build says so; only reading the produced configuration back does.

Rebuilt for D107 (2026-09-29)

Decision D107 adds CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS, built-in and required by REQ-P06-05, to build/kernel-requirement.json, because a BPF LSM program attaches through a BPF trampoline and CONFIG_BPF_LSM does not depend on the function tracer that trampoline needs (the Nucleus kernel gate). 50-aegis-requirement.config was rendered again from the payload and carries the row after CONFIG_BPF_LSM, fourteen assignments in all.

The symbol has no prompt, and the chain it follows from was read out of the pinned source, linux-7.2.5 hashed against the pin: in kernel/trace/Kconfig it is a def_bool that depends on DYNAMIC_FTRACE_WITH_REGS || DYNAMIC_FTRACE_WITH_ARGS and on HAVE_DYNAMIC_FTRACE_WITH_DIRECT_CALLS, both WITH_ symbols are def_bools over DYNAMIC_FTRACE, DYNAMIC_FTRACE depends on FUNCTION_TRACER, and FUNCTION_TRACER has no default and sits inside if FTRACE; under config X86, arch/x86/Kconfig selects every HAVE_ symbol involved. x86_64_defconfig sets CONFIG_DEBUG_KERNEL=y, which turns FTRACE on by default, and no FUNCTION_TRACER. 10-base-support.config therefore gains CONFIG_FTRACE=y, CONFIG_FUNCTION_TRACER=y and CONFIG_DYNAMIC_FTRACE=y, the three options M10's controlled comparison switched on, and tools/test_kernel_build.py requires all three. The fragment's comment also states what BPF_LSM itself depends on: FTRACE, through BPF_EVENTS, which sits inside if FTRACE, but none of FUNCTION_TRACER, DYNAMIC_FTRACE or DYNAMIC_FTRACE_WITH_DIRECT_CALLS (kernel/bpf/Kconfig).

Without them the row does not survive. The same defconfig, merge and olddefconfig, run by hand over the pinned tree with the support fragment of before D107 and the new requirement fragment, produced CONFIG_FTRACE=y, # CONFIG_FUNCTION_TRACER is not set and no DYNAMIC_FTRACE line at all, and the gate's own check refused it with CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS: REQ-P06-05 requires built-in, observed unrecorded.

With them it does. make verify-kernel ran into a fresh build directory, downloading the tarball and verifying its sha256 and signature again (from 21:13:22 to 21:15:52 UTC), then over the same tree (21:23:38 to 21:26:30 UTC), and once more over it after the last edit of any file the gate reads, which narrowed the support fragment's comment on what BPF_LSM depends on and changed no assignment, and after this change was rebased onto main at a28be4e (21:53:55 to 21:57:34 UTC); a direct python3 tools/verify_kernel_build.py between the first two gave the same. Each read 14 requirement rows, passed all seven cases with the outcomes in the table above and exited 0. The last two each built a 16,954,368-byte bzImage and 19 modules as 7.2.5-aegis-m26, in 142 and 126 seconds, and in each the guest's own /proc/config.gz was byte-identical to the produced .config: 5,545 lines, sha256 90e08c39b2bcbacb257cb15e86d4e5d0fd82270105cd4b9d0c80d29f52088585. The tracer, as the guest printed it, with the line each sits on in its dump:

5396:CONFIG_FTRACE=y
5399:CONFIG_FUNCTION_TRACER=y
5404:CONFIG_DYNAMIC_FTRACE=y
5406:CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS=y

The rebuild reads the configuration back; it attaches nothing. That a kernel built from M26's source with these three options attaches action_gate is M10's controlled comparison, not a case of this gate.

Scope, and what a pass here does not mean

  • Development evidence on the reference profile recorded in planning/hardware-profile.json. It qualifies no hardware.
  • It is not a substitute for the Nucleus contract. M09 pinned the consumption contract against the real producer that reads the payloads, and under D92 a Nucleus-built kernel and its digests are M10's to record; this milestone builds a kernel locally because that producer is a scaffold.
  • No boot gate is closed. The guest boot here is a configuration read-back, not a measured boot: no UKI, no Secure Boot, no TPM measurement, no bootctl status evidence.
  • No release gate is closed. Nothing is packaged, signed or published.
  • The guest's userspace is the host's own bash, mount, uname, gzip and sleep with their shared-library closure. Only the kernel under test is built here; the initramfs is transport, and the read-back would be equally valid with any userspace that can read /proc/config.gz.
  • No latency, jitter or determinism figure is claimed. CONFIG_PREEMPT_RT is confirmed as a configuration state and uname -v reports PREEMPT_RT; measuring what that buys is milestone M23.
  • A host that cannot run the gate has two outcomes, and they say different things. A missing tool, or one below the floor the pinned source declares, prints SKIP: <reason>; the kernel build gate did not run. and exits 0 -- the same convention verify-systemd and verify-mkosi use -- so an exit 0 from make verify-kernel is evidence only when the case lines are above it. A host that has the toolchain but cannot obtain the pinned source, with no network and no cached tarball, prints FAIL: the gate could not run: and exits 1 instead: an unverifiable source is a refusal, not a skip.
  • make verify-all does not run this gate, and the omission is deliberate. See the comment on the verify-kernel target in the Makefile: it downloads 160 MB, extracts 1.4 GB and compiles a kernel, and the CI runner has no kernel toolchain, no /dev/kvm and no emulator, so wiring it in would add a step that can only skip. What CI does re-run is the binding that matters everywhere: crates/aegis-fabrica-defs/tests/kernel_fragment.rs fails if the tracked fragment stops being what the M18 schema renders, and tools/test_kernel_build.py fails if the pin, the fragments or the recorded toolchain drift apart. CI never compiles a kernel and never boots one.