The Nucleus kernel in a guest, and the hook revision 1 could not attach¶
Status: recorded observations from milestone M10, reference profile: revision 1 on 2026-09-29, and revision 2, which closes M10, on 2026-10-11
make verify-nucleus-kernel verifies and records a kernel release that
cordanaLLM/nucleus built and published, boots the kernel in a guest, checks
the Aegis kernel requirement against the configuration the kernel reports
from inside the guest, and only then repeats the M19 loads on it.
Revision 2 passes all eighteen cases. The pinned release is
v7.2.8-realtime-lusoris2, and run r20261011T200131-87fa of 2026-10-11
exited 0; Revision 2 records it. Revision 1
passed seventeen: its kernel could not attach a BPF LSM program, and the
sections after that one keep that run and its finding as they were recorded.
Revision 1: seventeen of eighteen (2026-09-29)¶
On 2026-09-29 cordanaLLM/nucleus published the first kernel it built with an
imago.nucleus.kernel-artifact.v1 manifest, release
v7.2.8-realtime-lusoris1. M10's first exit criterion had waited for exactly
that. Run r20260929T195935-62a3 was the first to reach every
case, and passed seventeen of eighteen. Review then tightened the gate: the
signature now binds the tag commit, the release record is written before the
first boot, every case of a stage gates the next stage, cosign is held below
3, and the requirement check refuses unknown keys and checks
abi.module-abi. Run r20260929T203240-505f started after the last edit of
the gate's files and gave the same eighteen outcomes:
PASS nucleus/assets-pinned
PASS nucleus/signature-verified
PASS nucleus/imago-accepts-and-recorded
PASS nucleus/digest-mismatch-refused-before-boot
PASS nucleus/minimum-release-boundary
PASS nucleus/guest-identity-and-config
PASS nucleus/capabilities-read-in-guest-diffed-with-host
PASS nucleus/requirement-satisfied-before-any-load
PASS nucleus/requirement-unsatisfied-rejected
PASS nucleus/requirement-module-boundary
PASS nucleus/load-guest-config-matches-checked
FAIL action_gate/nucleus-loads-and-attaches
PASS action_gate/nucleus-unchecked-pointer-rejected
PASS scx_cake/nucleus-struct-ops-attaches
PASS scx_cake/nucleus-unbounded-rejected
PASS kepler_power/nucleus-tracepoint-attaches
PASS kepler_power/nucleus-missing-tracepoint-reported
PASS kepler_power/nucleus-idle-zero-delta-recorded
The one failure is a finding about the kernel, not a defect in the gate.
The Nucleus kernel's verifier accepts action_gate, but the program cannot
attach to its LSM hook: the kernel is built without CONFIG_FUNCTION_TRACER,
so the BPF trampoline an LSM program attaches through has no entry point to
patch. The Aegis kernel requirement asked for CONFIG_BPF_LSM, which the
kernel has, and not for the trampoline, and M26's own kernel lacked it too.
The finding below records
the controlled comparison that confirms the cause. M10 was therefore not done
on revision 1. Decision D107 then added the trampoline's prerequisite to the
requirement (below),
and nucleus built revision 2 to it.
Revision 2: every case passes¶
Nucleus published v7.2.8-realtime-lusoris2 on 2026-09-29T22:49:49Z, with
the function tracer and the BPF trampoline's direct calls built in.
build/kernel/nucleus-artifact.pin.json pins it, and run
r20261011T200131-87fa, on 2026-10-11, printed eighteen PASS lines and no
FAIL. The release record was written at 18:01:33 UTC, before the first guest
started.
| Field | Recorded value |
|---|---|
kernel.release |
7.2.8-lusoris2-realtime |
kernel.config_digest |
sha256:5dec07f61567f046e923fd862f434f056943ced8dedeb963a7f483c761b24ae8 |
artifact digest (SHA256SUMS) |
sha256:440a656f451783a048ca2210c7dbfa558ec58ba1670c19239cb273f091d2e8a3 |
vmlinuz-7.2.8-lusoris2-realtime |
4cf748d5909cf4c543d27eb86a8cd0513961e41b5937672074ac21d21dc89081 |
| provenance revision | 852be742eb173700d5ef93b0c6f867b855f9c640 |
| manifest, by sha256 | c27a42e85205ef684d90d12fd17c13b048eae4d93d624e5d14785138eb932938 |
| imago | 987b95a432e5c9aac1b4885b97fa2d3ff1b9d311, M09's pin |
What the run read, where it differs from revision 1:
- The signature binds the new tag. cosign verified
SHA256SUMSagainst the workflow identity atrefs/tags/v7.2.8-realtime-lusoris2and the workflow SHA above. It refused a second identity under the same repository and the commit ending in641. - The guest is the published kernel. It reported
7.2.8-lusoris2-realtime,#lusoris2 SMP PREEMPT_RT, and a/proc/config.gzthat hashes to the manifest'skernel.config_digest. - The two rows the host used to supply are now the guest's own.
CONFIG_FUNCTION_TRACERandCONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLSboth readyin the guest. - All fourteen requirement rows are satisfied before any load (D94,
E10-5), and the planted defects are still rejected one by one with
correlation-id aegis-m18-kernel-requirement-0001. action_gateattaches (E10-1). The marker exec reached the ring buffer as one event:filename=/aegis-m10/aegis_exec_prob,comm=aegis_bpf_probe,kind=1,verdict=0,argc=1,cgroup_id=1.
The other outcomes are the ones revision 1 gave: the altered kernel image is
refused before any boot, the 6.12 floor holds, scx_cake attaches and
detaches, kepler_power attaches, and each negative is rejected with M19's
recorded string.
Two changes to the gate ride with the pin, both prompted by the shell scan of
praetorctl at 299d3be, which reports a discarded failure:
- The idle case reads its idle task. PID 1 used to end the task that
occupies the idle cgroup and discard the result. It now prints
IDLE-TASK <case> heldwhen the task was still there as the loader returned, andgoneotherwise. The gate fails the case on anything butheld, and refuses a report without the line: an emptied cgroup reads a zero delta whatever the object does, so a zero is a reading only while the cgroup was occupied. The run readheld. - The probe no longer discards grep's exit status. Status 1, no line naming a symbol, stays a reading. Any higher status ends the probe with exit 2.
The host's tools have moved since the reference values were recorded, and the
run prints both: QEMU 11.1.2 (recorded 11.1.1), bash 5.3.20, mount 2.42.4,
coreutils 9.12, gzip 1.15, clang and llvm-strip 23.1.1 (recorded 22.1.8).
Each is at or above its floor in docs/roadmap/toolchain-admission.md, where
a newer installed version prints beside the recorded one and is not refused.
Nothing here is a release artifact and nothing here qualifies hardware. No package was installed, no module was loaded and no boot entry was written on the host. A pass on this profile is development evidence only.
What is tracked, and what is not¶
| Path | Role |
|---|---|
build/kernel/nucleus-artifact.pin.json |
the release, every asset by sha256 and size, the tag commit, the keyless signer |
tools/verify_nucleus_kernel.py |
the gate, run as make verify-nucleus-kernel; --fetch is make nucleus-kernel-fetch |
tools/nucleus_kernel_check.py |
the D94 check of build/kernel-requirement.json against one configuration, through nucleus's --kernel-config mode |
tools/guest/aegis-nucleus-init.sh |
PID 1 of both guests |
tools/guest/aegis-nucleus-probe.sh |
the sched_ext, BPF LSM and BTF probe, run in the guest and on the host |
bpf/loader/aegis_bpf_probe.c |
M19's loader, with M10's tp-attach mode and --map-set option |
tools/test_nucleus_kernel.py, tools/test_nucleus_kernel_check.py |
the half of the gate that runs inside make verify-all |
Not tracked: the release, the guest trees, the reports, the verifier logs and
each run's summary.json. They live under AEGIS_NUCLEUS_KERNEL_DIR, default
${XDG_CACHE_HOME:-$HOME/.cache}/aegis-nucleus-kernel, one directory per run
id under runs/.
Running it¶
make contract-fetch # M09's imago, if its cache is absent
make nucleus-kernel-fetch # the one networked step
make verify-nucleus-kernel
make nucleus-kernel-fetch downloads the pinned assets with curl, refuses any
whose sha256 or size is not the pin's, and runs cosign verify-blob on
SHA256SUMS. The gate never touches the network: cosign runs --offline
against the bundle's own transparency-log entry.
A host that cannot run the gate prints
SKIP: <reason>; the Nucleus kernel gate did not run. and exits 0: not
Linux (macOS and Windows name their platform), no read-write /dev/kvm, no
/proc/config.gz, a tool missing or outside its admitted range, no M09
contract cache, or no fetched release. cosign is the one tool with a ceiling:
the reference profile carries 2.6.3 and the distribution's 3.1.3, and a
PATH that names 3.1.3 is refused by the gate and by the fetch alike. An
exit 0 is evidence only when the case lines are above it. A cache that is
present but wrong is a FAIL: a symlinked or altered asset fails
nucleus/assets-pinned, observed on 2026-09-29 with a hand-edited
SHA256SUMS. The gate is not part of make verify-all, for the
reasons make verify-latency is not; docs/roadmap/toolchain-admission.md
records every tool it runs.
The order is the contract¶
Nothing boots before the release is verified and recorded (E10-4), and no
M19 object loads before the requirement has been checked against the
configuration the kernel reported from inside the guest (D94, E10-5). Each
stage runs all of its cases, and the next stage starts only when every one of
them passed, the negatives and boundaries included. OrderTests,
ReadbackPhaseTests and LoadPhaseTests in tools/test_nucleus_kernel.py
hold that order inside make verify-all.
- The release. Every cached asset hashes to the pin. cosign verifies
SHA256SUMSagainst the exact signer identity the pin records, with the tag commit and the tag as the certificate's workflow SHA and ref. A second identity under the same repository is refused, and so is a commit that differs in its last digit, so both are load-bearing. imago, the binary M09's fetch built at the commit M09 pins (16f964bfor the runs of 2026-09-29,987b95asince D107), whose sha256 the gate checks against the fetch's identity record, runsimago kernel artifact verifywith the stream, tag and version the pin expects, and returns what M10 records. The record is written to the run'srelease-record.jsonwhen this stage ends. - The readback boot. M23's
boot()intools/verify_latency_fixture.pyruns unchanged, with this kernel image and an M10 initramfs: the emulator argument vector and the kernel command line are M23's, and the initramfs is built through M23'sinstall_programsandarchive_tree. The kernel image is hashed against the pin immediately before each boot, and a mismatch raises before the emulator starts. - The load boot, only when the readback cases passed. PID 1 hashes its
own
/proc/config.gzagain and loads nothing unless the sha256 is the one the gate checked.
E10-4: the release, verified and recorded¶
The values in this section and the next three are revision 1's, from the runs of 2026-09-29. The cases and their order are unchanged on revision 2, whose values are above.
imago accepted the manifest against the downloaded assets. The values it
returned, each equal to the pin, were written to the run's
release-record.json at the end of the release stage. In run
r20260929T203240-505f that file was written at 18:32:41 UTC, before the
readback boot started, and the guest's report is dated three seconds later;
summary.json repeats the values when the run ends.
| Field | Recorded value |
|---|---|
kernel.release |
7.2.8-lusoris1-realtime |
kernel.config_digest |
sha256:e9510d2d1928d4e153b19cf3312c4184ce44b4186ec6fde286e001060ffd731a |
artifact digest (SHA256SUMS) |
sha256:62ceccf9b6730fce5790df8803375b3c1c32f67f1a6ca6bc94f7ba12b2c9c90f |
vmlinuz-7.2.8-lusoris1-realtime |
50ec65c5d6b77de9b14c1f2c0c2f07d668edcff3b44ce1a44196659cdc66d626 |
| provenance revision | 9e050cf8fc2fb62de943229f22ab104dce455e92 |
Both files record all seven artifact digests. imago reports the manifest file itself as an unlisted file beside the assets, which it lists rather than rejects.
The revision is bound to the signature, not only to the pin. The manifest is
not listed in SHA256SUMS, so its provenance.revision field is unsigned.
The Fulcio certificate in SHA256SUMS.bundle does carry the workflow's
commit and ref, and the gate passes --certificate-github-workflow-sha with
the pinned commit and --certificate-github-workflow-ref with
refs/tags/v7.2.8-realtime-lusoris1. cosign verified both. It refused
9e050cf8fc2fb62de943229f22ab104dce455e90, the pinned commit with its last
digit changed, with expected GitHub Workflow SHA not found in certificate.
Negative: a copy of the release whose kernel image differs from the published
one in a single byte, same size, was refused twice, and no guest was started
for it. The gate's pre-boot hash refused it with the two digests, and imago
refused it with
artifacts[vmlinuz-7.2.8-lusoris1-realtime]: digest mismatch.
PreBootRefusalTests holds that the boot path raises before M23's boot()
is called.
Boundary: abi.minimum-release is 6.12. The release rule of
KernelRelease::at_least, applied by nucleus's --kernel-config mode,
admits 6.12 and 6.12.0-lusoris1 and refuses 6.11.999, the highest
release below the floor. The kernel's own 7.2.8-lusoris1-realtime is
admitted, and a kernel reporting 6.11.999 is rejected by the requirement
check with the correlation id named. These are rule evaluations; the only
kernel booted is the pinned one.
The guest's own configuration, and what the host supplied¶
The readback guest reported the release 7.2.8-lusoris1-realtime and
#lusoris1 SMP PREEMPT_RT, and its own /proc/config.gz hashed to
e9510d2d...ffd731a, the manifest's kernel.config_digest. The configuration
checked below is therefore byte for byte the one Nucleus published.
Criterion 6 reads sched_ext, BPF LSM and BTF from the kernel under test and diffs each against the host. The same probe script runs in both places:
| Reading | Guest | Host | Reference profile |
|---|---|---|---|
CONFIG_SCHED_CLASS_EXT |
y |
y |
y |
bpf in the active LSM list |
yes | yes | yes |
/sys/kernel/btf/vmlinux |
present | present | present |
CONFIG_FUNCTION_TRACER (recorded, not required) |
not set | y |
not recorded |
CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS (recorded, not required) |
absent | y |
not recorded |
The guest's active LSM list is capability,lockdown,apparmor,bpf; the host's
is capability,landlock,lockdown,yama,bpf. Each capability is decided from
the guest's reading alone. CapabilityTests holds that a guest without bpf
fails although the host has it (E10-1's boundary), and that a guest without
sched_ext is recorded as a Nucleus requirement defect (E10-2's boundary).
The last two rows are the ones the host silently supplied: they are why the
M19 attach passed on the host and the M10 attach does not. Since D107 the gate
labels the second of them required; decided by the requirement case.
D94: the requirement against the built kernel¶
Since 2026-10-17 tools/nucleus_kernel_check.py hands the configuration to
the pinned nucleus verifier's single-kernel mode (--kernel-config, nucleus
pull request 176) and reads its nucleus.kernel-check-report.v1; until then
tools/kernel_requirement_check.py held a Python copy of the same rules,
and the runs below were decided by that copy. Either way every row of
build/kernel-requirement.json is decided by the M18 rules: built-in by y
only, module by m only, present by either, absent by not set, and an
unobserved symbol by nothing. It also mirrors the identity half of KernelRequirement::unmet:
every listed architecture, the release floor, and abi.module-abi as an
exact release when a payload fixes one. It refuses any key the schema does
not name, as the Rust decoders' deny_unknown_fields does. It does not
decide the capability rows the Rust half reads from a profile. All thirteen
features were satisfied by the configuration the guest printed, and each
symbol's state is in summary.json. The four module
rows, CONFIG_INTEL_RAPL, CONFIG_VFIO, CONFIG_VFIO_PCI and CONFIG_KVM,
read m in the guest.
Negative and boundary, each planted into a copy of the guest's own
configuration and each rejected alone with
correlation-id aegis-m18-kernel-requirement-0001 and the symbol named:
CONFIG_SCHED_CLASS_EXT=n, # CONFIG_BPF_LSM is not set, a removed
CONFIG_HZ_1000 line, CONFIG_BPF_SYSCALL=m against a built-in row, and
CONFIG_KVM=y against a module row.
The runtime probes the requirement rows name for powercap and the IOMMU groups are not read in the guest: QEMU presents neither RAPL counters nor an IOMMU here, so a reading would describe the emulator. D94 is a check of the configuration.
The loads¶
Every load runs as uid 65534 under setpriv with exactly M19's capability
set, -all,+bpf,+perfmon. The objects are M19's sources, compiled by M19's
own gate functions against a vmlinux.h dumped from the Nucleus kernel's
BTF, which the gate extracts from the image it verified. Every verifier log
carries its load's nonce and is kept in full under the run's load/logs/.
| Epic | Positive | Negative | Boundary |
|---|---|---|---|
E10-1 action_gate |
FAIL: loads, load_rc=0, then failed to attach: -EBUSY |
PASS: R7 invalid mem access 'ringbuf_mem_or_null' |
see criterion 6 above; the attach failure carries its recorded reason |
E10-2 scx_cake |
PASS: sched_ext_ops_during=aegis_cake, switch_all=1, still attached after 1 s, disabled (unregistered from user space) |
PASS: The sequence of 8193 jumps is too complex. |
see criterion 6 above |
E10-3 kepler_power |
PASS: sched/sched_switch attached; runtime grew in the root cgroup |
PASS: tracepoint_attach_failed tracepoint=sched/aegis_no_such_tracepoint errno=2 |
PASS: an idle cgroup's delta over 3 s recorded as 0 uJ and 0 ns |
The negatives reuse M19's recorded mutations: the gate deletes the same
marked region and requires the same rejection string, from a log this load
wrote, absent from the unmutated object's log. The kepler value is the
object's declared runtime-times-TDP figure, pseudo_energy_uj_unmeasured,
not a meter reading. The idle cgroup holds one task that enters it after the
attach and sleeps through the interval, so its row exists at both ends and
its delta is a reading of zero, not an absent line. Since 2026-10-11 the
guest also reports whether that task was still there when the loader
returned (Revision 2).
The finding: BPF LSM present but not attachable¶
action_gate loads, and its attach fails:
program=action_gate_exec section=lsm/bprm_check_security type=29 fd_valid=1
load_rc=0
libbpf: prog 'action_gate_exec': failed to attach: -EBUSY
lsm attach failed: Device or resource busy
A BPF LSM program attaches through a BPF trampoline. On x86 the kernel
installs it by patching the five-byte nop that -mfentry leaves at the hook
function's entry, and __bpf_arch_text_poke in
arch/x86/net/bpf_jit_comp.c returns -EBUSY when the bytes there are not
that nop. A kernel built without CONFIG_FUNCTION_TRACER is not compiled
with -mfentry, so the nop is absent whatever CONFIG_BPF_LSM says, and
CONFIG_BPF_LSM does not depend on the function tracer in Kconfig.
That was a hypothesis, and it was checked with a controlled comparison on
2026-09-29. Two kernels were built from M26's verified linux 7.2.5 source and
M26's configuration, one unchanged and one with CONFIG_FTRACE,
CONFIG_FUNCTION_TRACER and CONFIG_DYNAMIC_FTRACE switched on. The
configuration diff is those options and the ones they select, plus
CONFIG_PAHOLE_VERSION, which records the host's pahole and moved from 131
to 132 since M26 built its kernel.
Both booted through the same boot() with the same load initramfs, objects,
loader and capability set. The unchanged kernel failed with the same
-EBUSY; the other attached, and the marker exec reached the ring buffer
(the event line is cut after its first two fields here):
load_rc=0
attached=lsm ringbuf=action_ringbuf
event filename=/aegis-m10/aegis_exec_prob comm=aegis_bpf_probe ...
detached=lsm marker_seen=1
The comparison, its configuration diff and both reports are kept under
control-function-tracer-20260929/ in the gate's cache. It is not part of
the gate: the gate boots only the pinned kernel.
What would close E10-1 is a kernel requirement that states the trampoline
prerequisite and a Nucleus release built to it. The row would name
CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS or CONFIG_FUNCTION_TRACER. That
changes the M18 payload that imago and nucleus both vendor and M09 pins, so it
is a recorded decision for the maintainer, not a change made here; see the
M10 entry in docs/roadmap/README.md.
Since D107: the requirement asks for the trampoline¶
Decision D107, taken by the maintainer on 2026-09-29, adds the row:
build/kernel-requirement.json requires
CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS built-in, probe kernel-config,
required by REQ-P06-05. imago re-vendored the new bytes and nucleus enabled
CONFIG_FTRACE, CONFIG_FUNCTION_TRACER, CONFIG_DYNAMIC_FTRACE and
CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS on every stream, and M09 pins both
(the contract pair); M26's kernel carries the row too
(the Aegis-built kernel). The capability case now labels that
symbol required; decided by the requirement case, and
CONFIG_FUNCTION_TRACER, which the requirement does not name, stays
recorded, not required.
Against the new requirement, v7.2.8-realtime-lusoris1 stops before any
load. Run r20260929T232643-b28e, into a fresh cache that
make nucleus-kernel-fetch filled and with M09's imago at 987b95a, passed
the release stage and the first two readback cases, then:
FAIL nucleus/requirement-satisfied-before-any-load
rejected: correlation-id aegis-m18-kernel-requirement-0001:
CONFIG_DYNAMIC_FTRACE_WITH_DIRECT_CALLS is required built-in
(REQ-P06-05) and the configuration has unobserved
the readback checks did not pass, so no M19 object was loaded
The negative and boundary cases of the requirement stage failed with it: each
plants one defect into the guest's own configuration and expects exactly that
one rejection, and this configuration already carries one. The gate is
unchanged by D107; the kernel is what no longer conforms. Nucleus's ADR-0011,
at 852be74, re-releases all four streams as revision 2,
v7.2.8-realtime-lusoris2 among them; its release list, read on 2026-09-29,
held revision 1 only. M10 closes only when a release built to the new
requirement passes E10-1. That release is revision 2, and it does
(Revision 2).
Two things M19 never exercised¶
M19 loaded scx_cake and never attached it, and loaded kepler_power and
never attached it. Attaching both on the Nucleus kernel found two gaps, each
closed on the loader side and recorded here:
- scx_cake stalled every task until the watchdog ejected it. Its
dispatchwalks as many tiers as the one-row arraycake_tier_budgetholds, and the row reads zero until a loader writes it. Runr20260929T194509-ada2attached it with the zero budget: the guest console recordeddisabled (runnable task stall)after 30 seconds. The loader's new--map-set cake_tier_budget=4writesAEGIS_CAKE_TIERS, read frombpf/aegis_bpf_abi.h, after the load and before the attach. The source is unchanged, so the budget stays a value the verifier does not know and M19's unbounded negative keeps its meaning. - The tracepoint attach needs tracefs access. CAP_PERFMON does not bypass file permissions, and libbpf reads the tracepoint's id from tracefs. PID 1 mounts tracefs group-owned by the loads' gid, 65534.
The loader's tp-attach mode is new: it attaches the tracepoint program, to
the tracepoint its section names or to one given with --tracepoint, reads
the per-cgroup map at both ends of an interval inside the attach window, and
prints a failed attach as tracepoint_attach_failed with its errno. M19's
four modes are unchanged, and tools/test_bpf_objects.py still passes.
What this does not claim¶
- Revision 1 did not close M10: E10-1's positive fails on that kernel, as recorded above. Revision 2 closes it in the register, and nothing beyond it.
- It is development evidence on the reference profile, closing neither the hardware nor the release gate. The M19 host-kernel fixture closes nothing here either: it is exactly the fixture whose host supplied the trampoline.
- No Nucleus kernel has booted on hardware. Both guests are QEMU/KVM guests on the reference profile.
- The guest's userspace, libbpf included, is the host's own; only the kernel is under test.
- No SBOM content is checked; the two SBOMs are verified by digest only.