ADR-0008: Signed Kernel Sources and a Resolved, Survival-Checked Kernel Configuration¶
Date: 2026-09-29
Status¶
Accepted (2026-09-29)
This ADR supersedes two parts of accepted ADRs, and amends ADR-0007:
- ADR-0001, decision item 1: a stream no longer declares
tarball_url; it declares asource(section 1), andarchitecturesis an object that carries each architecture's build data (section 5). Items 2 and 3 stand. - ADR-0002, decision items 1 and 2: there is no
kconfig/base.config, and the fragments are not composed on their own; they are applied on top of the architecture's defconfig and resolved (section 4). Item 3 stands. - ADR-0007, section 4: the
resolvedevidence level it announced comes from a resolved.config, not from a built kernel (section 6).
Context¶
Issue #18 is the real build. Its first half is everything before the compiler runs: a kernel
source this forge can prove, and a configuration that holds what the fragments ask for. Neither
existed. versions.json carried one tarball_url per stream and nothing that proved what it
pointed at, and three of the four URLs were stale: kernel.org publishes 7.3-rc5, 7.2.8 and
6.18.54, while the manifest pinned 7.3-rc2, 7.2.4 and 6.18.50. The fourth,
projects/rt/7.2/linux-7.2-rt.tar.xz, returns HTTP 404; the rt directory holds
patch-7.2-rt5 and its patch series, based on v7.2.
kernel.org signs its sources in two ways (https://www.kernel.org/signature.html):
- A release tarball has a detached
.tar.signover the uncompressed tar, checked withxz -cd linux-X.tar.xz | gpg --verify linux-X.tar.sign -. Stable and longterm tarballs are signed by Greg Kroah-Hartman (647F28654894E3BD457199BE38DBBDC86092693E); 7.2.8 and 6.18.54 are. - A release candidate has no signed tarball:
releases.jsonlistspgp: nullfor mainline, and thegit.kernel.orgsnapshot is generated on request. Its tag intorvalds/linux.gitis signed by Linus Torvalds (ABAF11C65A2970B130ABE3C479BE3E4300411886). - Each release directory also has
sha256sums.asc, signed by the checksum autosigner (B8868C80BA62A1FFFAF5FDA9632D3A06589DA6B1). signature.html says it is a mirror check and does not replace a developer signature.
The fragments had never been resolved against a kernel tree. Merged on their own and passed to
make olddefconfig, they produce a kernel without EXT4_FS, NET, MODULES or EFI_STUB,
because a fragment names what differs from a base, not a whole configuration. Applied on top of
each architecture's defconfig and resolved against the pinned sources, they lost, silently:
DEFAULT_MQ_DEADLINE on every leg (no tree has that symbol); VIRTIO_FS on x86_64 (unset) and
arm64 (capped at m); TCP_CONG_BBR and DEFAULT_BBR on arm64 and riscv64; KVM_GUEST on arm64;
and RANDOMIZE_BASE on riscv64 from 7.2 on. The kernel's own scripts/kconfig/merge_config.sh
only prints a warning for a value that did not survive.
Alternatives considered¶
- Sources. A
sha256pin alone (trust on first use) proves the bytes did not change after the pin, not who published them; it is the only option for a release-candidate snapshot, which has no signature.sha256sums.ascis signed, but by an automated key that kernel.org itself ranks below the developer signatures. The rt patch on top of v7.2 was the third option forrealtime: it carries fixes for arm (32-bit), powerpc, i915 and the 8250 serial driver, none for x86, arm64 or riscv, and it is based on v7.2, not on the 7.2.y stable releases. - Base configuration. A distribution configuration (Ubuntu's) plus the fragments builds thousands of modules this forge does not need, and it is not published per release in a form the build can pin. The fragments alone resolve to a kernel that cannot boot a disk.
- Survival.
merge_config.shwithout-mrunsalldefconfigand warns; a strict mode of it (-s) refuses a fragment that redefines a value, which is the layering this forge relies on (realtime.configafter the architecture fragment), and still does not refuse a dropped value.
Decision¶
1. Every stream names a signed source, and nothing is used before it is proven¶
versions.json streams.<stream>.source is one of two kinds (versions.schema.json):
tarball:urlof the.tar.xz,signature_urlof its.tar.sign, thesha256of the.tar.xz, andsigners.git-tag: therepository, thetag, thecommitit points at, andsigners.
signers lists the primary-key fingerprints whose signature is accepted for that source.
scripts/fetch-kernel-source.sh --stream=<stream> --dest=<dir> proves the source, writes the tree,
and checks the tree's release; any failure refuses with exit status 1 and leaves no tree behind:
tarball: the.tar.xzmatches the pinnedsha256, thenxz -cd <tar.xz> | gpgv --keyring <keyring> <tar.sign> -succeeds. Both are required: the signature proves who published the release, the pin proves it is the one reviewed here.git-tag:git fetch --depth=1of the one tag,git verify-tagin a throwaway GnuPG home, the tag object names the tag, and the tag points at the pinned commit. The tree is exported withgit archive.- In both cases the GnuPG status must carry a good signature from an unexpired, unrevoked key, and
every
VALIDSIGmust name, as its primary-key fingerprint, a listed signer. The keyring or GnuPG home holds the listed signers' keys and no others. - The tree's top-level
Makefilemust report the releaseversions.jsonnames for the stream.
bleeding is a git-tag source; mainstream, lts and realtime are tarball sources signed
by Greg Kroah-Hartman.
2. The keys are in the repository¶
keys/<FINGERPRINT>.asc holds the armored, minimal export of each key, fetched through the
kernel.org Web Key Directory as signature.html instructs. The autosigner key is kept to check
sha256sums.asc when a tarball pin is bumped and is no stream's signer. gpgv does not read an
armored keyring, so the fetch script dearmors the listed keys into a binary one. keys/README.md
records the provenance and how to refresh a key.
3. realtime is the mainline stable release with the in-tree PREEMPT_RT¶
realtime pins the same 7.2.8 tarball as mainstream; kconfig/streams/realtime.config sets
PREEMPT_RT, HZ_1000 and EXPERT. PREEMPT_RT depends on EXPERT && ARCH_SUPPORTS_RT &&
!COMPILE_TEST, and x86, arm64 and riscv select ARCH_SUPPORTS_RT in 6.18.54, 7.2.8 and
7.3-rc5 alike, so all three realtime legs resolve with it. No rt patch is applied.
4. The configuration is resolved against the source¶
scripts/merge-config.sh --stream=<stream> --arch=<arch> --source-tree=<verified tree>:
make ARCH=<kernel_arch> CROSS_COMPILE=<cross_compile> <base_config>: the architecture's in-tree defconfig (x86_64_defconfig, ordefconfigfor arm64 and riscv64);scripts/kconfig/merge_config.sh -mof the tree, withsecurity-hardened.config, the architecture fragment and the stream fragment, in the order ADR-0007 and the declared merge use;make olddefconfig;- the survival check (section 5);
output/kernel-<stream>-<arch>.config, which carries no timestamp. The build directory (--build-dir, theO=directory) is where the compile will run.
5. Every requested value must survive¶
scripts/kconfig_survival.py reads the fragments in merge order with the grammar the declared
merge uses, the last line for a symbol winning, and compares every requested CONFIG_X=value and
every # CONFIG_X is not set with the resolved .config. A value that differs, or a symbol the
resolved .config does not record, fails the resolution, and the report names the symbol, the
requested value, the resolved value and the fragment. An unset request is met only by the
resolved # CONFIG_X is not set line. The check is never relaxed to make a leg pass: a symbol
an architecture cannot have moves to that architecture's fragment, and a dependency the base
does not carry is declared next to what needs it.
Resolving all twelve legs against the pinned sources required these fragment changes:
DEFAULT_MQ_DEADLINE removed (no such symbol; block/elevator.c makes mq-deadline the default
for single-queue devices); FUSE_FS=y for VIRTIO_FS=y on x86_64 and arm64;
TCP_CONG_ADVANCED=y for BBR, which only the x86_64 defconfig sets; KVM_GUEST removed from
arm64, where it does not exist; RELOCATABLE=y on riscv64, on which RANDOMIZE_BASE depends
since Linux 7.2.
6. The requirement verifier gains the resolved evidence level¶
verify-requirements.yml keeps the declared check, then resolves: verify_kernel_requirement.py
--plan lists every stream with every architecture versions.json builds, all twelve legs, a
job per stream fetches and verifies the source in an ubuntu:26.04 container and resolves each
architecture, and the verifier decides each document again with --resolved-config
STREAM:ARCH=PATH. The plan does not depend on the documents: resolving is also the survival
check of the fragments, and a leg no document binds or lists still ships a kernel. At this
level a bound stream is judged on its resolved .config; a bound stream and listed architecture
without one is an error, not a pass. A resolved .config is accepted only for the leg its
Kconfig header names (# Linux/<ARCH> <release> Kernel Configuration, ARCH being the make
ARCH); the header does not tell mainstream from realtime, which share a release. Unbound streams stay declared, as information. Nothing is
compiled; abi.module-abi still fails closed until a kernel is built.
7. The toolchain is part of the resolution¶
versions.json architectures.<arch> carries kernel_arch, base_config and cross_compile.
The prefix is used on every host, the native one included (Ubuntu names its native compiler
x86_64-linux-gnu-gcc too), so symbols that depend on the compiler, such as the
CC_HAS_* probes and CC_VERSION_TEXT, resolve against the compiler that builds. pahole is
required, because DEBUG_INFO_BTF depends on PAHOLE_VERSION >= 122 and SCHED_CLASS_EXT on
BTF.
Consequences¶
Positive¶
- A source is used only when kernel.org's signature and this repository's pin agree. A compromised mirror, a replaced tarball and a moved tag are all refused.
- A fragment line that configures nothing fails at resolution time, on every leg, instead of shipping a kernel without it. The requirement gate checks the configuration a build will use.
- The
realtimestream builds from a release that exists and receives stable fixes.
Negative¶
- Bumping a stream now takes a
sha256or a commit and, for a new signer, a key;tests/test_manifest.pychecks the source against the version, not against kernel.org. - The
bleedingfetch transfers about 290 MB of git objects, and each tarball about 150 MB; a release candidate has no smaller signed form.verify-requirements.ymlcaches the download (fetch-kernel-source.sh --work, never the tree), keyed on the pinnedsha256or commit, and the script verifies a reused download exactly like a fresh one, so the cache saves the transfer and is never trusted. A cache entry that fails verification fails the job until it is deleted. mainstreamandrealtimeresolve to the samemake kernelrelease(7.2.8). The build must set a localversion per stream before both are packaged.
Neutral¶
- The resolved
.configrecords the compiler (CONFIG_CC_VERSION_TEXT,CONFIG_GCC_VERSION), so its digest changes with the toolchain. publish-release.ymlstill publishes the declared merge askernel-<stream>.configuntil the build replaces it with the resolved one.
Compliance¶
tests/test_fetch_kernel_source.py: a throwaway key, a fake tarball and a local repository with signed tags; a good tarball and a good tag are written, and a wrongsha256, a signature over other content, a missing signature, a signature by an unlisted key, a key file holding another key, a tree of another release, an unknown stream, a tag at another commit, a tag signed by an unlisted key, an unsigned tag and a ref pointing at the signed tag object of another tag are refused with no tree left behind.tests/test_kconfig_survival.py: the survival rules on synthetic configurations, andscripts/merge-config.sh --source-treeagainst a stand-in tree, including the refusal of a dropped request and of a tree of another release, each of which removes the configuration an earlier run wrote.tests/test_manifest.py: every source names its stream's release, every signer has a key, a release candidate comes from a tag; no kernel version is written in a test.tests/test_verify_kernel_requirement.py: the resolved level, missing resolved evidence, a resolved.configwhose header names another leg, and the plan of all twelve legs..github/workflows/verify-requirements.yml: resolves all twelve stream and architecture legs from their verified sources on each pull request and push that touches the fragments, the scripts,keys/orversions.json, and weekly.
References¶
- nucleus issue #18 (the real build), PR #35 (ADR-0007, the requirement gate).
- Decisions recorded for issue #18: all twelve stream and architecture legs are built; the
realtimesource is the mainline stable 7.2.y release withCONFIG_EXPERTandCONFIG_PREEMPT_RT, without the rt patch; thebleedingsource is the latest mainline release candidate through its signed tag andgit verify-tag; the base configuration is the architecture defconfig with the fragments and a survival check. - https://www.kernel.org/signature.html, https://www.kernel.org/releases.json (2026-09-29).
- Linux 7.2.8:
scripts/kconfig/merge_config.sh,kernel/Kconfig.preempt,arch/{x86,arm64,riscv}/Kconfig(ARCH_SUPPORTS_RT, riscvRANDOMIZE_BASE),net/ipv4/Kconfig(TCP_CONG_ADVANCED),fs/fuse/Kconfig(VIRTIO_FS),block/elevator.c. - GnuPG
doc/DETAILS(VALIDSIG,GOODSIG,EXPKEYSIG,REVKEYSIG).