Build igloo.ko with nix, against kernel derivations - #100
Merged
Conversation
Adds a nix build path alongside the Docker one. Same artifact, same
igloo_driver.tar.gz layout; the difference is what a module is built
*against*.
The Docker path downloads kernel-devel-all.tar.gz from a linux_builder
release, unpacks it, and builds against whatever is in that directory.
Two problems, only one of them obvious:
1. Nothing ties the unpacked tree to the kernel the module is inserted
into. The pin was a release TAG -- a label, not an identity. A module
is loadable only by the exact kernel build it was compiled against:
modversion CRCs come from that kernel's headers, and the kernel's
.config itself holds options Kconfig resolves BY PROBING THE COMPILER
(CONFIG_STACKPROTECTOR_PER_TASK, CONFIG_INIT_STACK_*, the CC_HAS_*
family). The ABI moves when the toolchain moves, with byte-identical
sources and byte-identical configs.
Not hypothetical: pairing v0.0.96 with linux_builder's nix kernels put
129 of 221 CRCs out of agreement. The only symptom was "igloo:
disagrees about version of symbol module_layout" and a guest panic,
and nothing in either release recorded what it was built against.
2. A nix-built kernel-devel tree is not consumable from a non-nix
container at all. It ships prebuilt host tools linked against the Nix
store -- scripts/basic/fixdep requests an interpreter under
/nix/store -- so inside embedded-toolchains every target died within
seconds of make starting. That is not a bug in the tarball; it is what
"prebuilt host tools" means.
Taking linux_builder as a flake input makes the kernel derivation the
build input. ARCH, CROSS_COMPILE and the compiler all come out of the
kernel's passthru via kernelsmith's buildModule, so a mismatched
(kernel, module) pair is not representable -- "built against a different
kernel" is a different store path, not a runtime panic.
What lands:
flake.nix linux_builder as an input; the 19-cell matrix is
DERIVED from its kernel outputs rather than restated,
so build.sh's hand-maintained TARGETS/VERSIONS lists
can no longer drift into silent "SKIPPED (No kernel
config)" lines
nix/module.nix the buildModule call (wrapper entry: src/Makefile
does python3 codegen before re-entering kbuild)
nix/artifacts.nix ISF and stripped .ko as separate derivations off the
unstripped module, so the extract-then-strip ordering
_in_container_build.sh relies on cannot be reordered
into a bug. dwarf2json is linux_builder's pinned fork,
shared rather than re-derived
nix/release.nix the tarball, reproducible, plus BUILT_AGAINST.txt --
which now names each kernel's STORE PATH, i.e. the ABI
itself, not a tag that two different ABIs can share
nix/shape.nix every module's ELF class/byte order/machine must match
what its target name claims. The Docker build has no
such check and collapses every powerpc* target onto
ARCH=powerpc; linux_builder shipped two bugs of this
exact shape before adding the equivalent
.github/workflows/nix.yml
PR gate (one cell per version, through to the shipped
artifacts), full matrix on demand and on a nixdev_*
tag, which cuts a PRERELEASE. Deliberately not dev_*:
that already triggers build.yml, and both pipelines
would race version-increment for the same vX.Y.Z
build.sh and the Docker workflows are untouched and still work; this is
additive while both release paths exist.
Three additions on top of the nix build path.
nix/modversions.nix -- assert every module's modversion CRCs agree with
its kernel's Module.symvers.
Building against a kernel derivation makes the right INPUTS structural.
It does not prove the OUTPUT is right, which is a different claim: kbuild
happily emits a module with no __versions section at all if
CONFIG_MODVERSIONS is off in a config, and nothing else here would
notice. Either way the symptom is identical and arrives inside a guest --
"igloo: disagrees about version of symbol module_layout", then a panic.
That shipped once (129 of 221 CRCs out of agreement) and was
undiscoverable until boot.
All 19 cells pass, 198-225 symbols each, zero mismatches and zero
symbols absent.
The section is read by parsing the ELF directly rather than shelling out
to objcopy: nixpkgs' binutils is built for the host target and
`objcopy --dump-section` fails outright on several machines in this
matrix, and it also writes its temp file beside the input, which is in
the read-only store. Note MODULE_NAME_LEN is (64 - sizeof(Elf_Addr)), so
a modversion entry is 64 bytes on every arch -- parsing it as crc + 56
misreads every 32-bit module.
flake.nix -- `lib.<system>.buildFor { kernel, version, target }`.
The seam for where the driver is going: fewer changes carried as kernel
patches, more of the delta in the module, eventually igloo.ko loading
into a stock upstream kernel. Anything that can hand over a buildKernel
derivation can build a matching module without this repo enumerating that
kernel anywhere.
Also fixes a real bug in the emptiness guard added in the previous
commit: it was a standalone `_ = lib.assertMsg ...` let-binding, and an
unreferenced binding is never forced, so it would never have fired.
Folded into kernelCells itself.
build.sh -- refuse a NIX-BUILT kernel-devel tree, loudly and up front.
Its prebuilt host tools are linked against the Nix store
(scripts/basic/fixdep asks for an interpreter under /nix/store), which
does not exist in the Ubuntu toolchain image, so every target dies within
seconds of make starting with an error that names fixdep and explains
nothing. Detect it and point at `nix build .#igloo_driver` instead.
And while in there: rewrite_mount referenced $PENGUIN_HOST_MOUNT_FROM
unguarded under `set -u`, so it aborted with "unbound variable" whenever
the variable was unset -- every local build, i.e. exactly the case its own
comment says is a no-op. `./build.sh` has been unusable locally as a
result.
nixdev_0.1.0's 4.10/x86_64 kernel did not boot -- binutils >= 2.31 emits R_X86_64_PLT32, which Linux only learned in 4.16. A module is only as good as the kernel it is paired with, so this pin has to move with the kernels rather than lag them. Also picks up kernelsmith's $dev slimming (kernelsmith#4), which changes what an `M=` build reads out of the kernel-devel tree. This repo is the consumer that exercises that path, and its matrix is what confirms it: all 19 cells build, modversions and shape checks pass.
Three changes, and the second forces the third. 1. linux-builder: nixdev_0.1.1 -> v4.0.1. A real release rather than a prerelease: v4.0.1 is the first linux_builder release cut by its nix path and the first whose kernels are all boot-tested. 2. The Docker path is removed -- build.yml, test.yml, build.sh, _in_container_build.sh. It did not fall out of favour, it became UNRUNNABLE. It consumed linux_builder's `releases/latest/download/kernel-devel-all.tar.gz`, and as of v4.0.1 that asset is nix-built: its prebuilt host tools are linked against /nix/store, so they cannot execute in the Ubuntu toolchain image. Every target fails within seconds of `make` starting. build.sh's own guard detected exactly this and refused, which is the only reason the failure was legible. Not a bug in the tarball -- it is what "prebuilt host tools" means, and it is why a nix linux_builder forces a nix igloo_driver. 3. So release duty moves to nix.yml, which is now the only pipeline: the full matrix also runs on a push to main, then version-increment and a real vX.Y.Z release. Releases are therefore cut by the job that already proved every module built, the shapes match, and the CRCs agree. That last part is the point of the whole exercise. v0.0.96 was Docker-built against different kernels and disagreed on 129 of 221 modversion CRCs when paired with nix kernels -- which a guest reports as "igloo: disagrees about version of symbol module_layout", at load time, in a running rehost. Modules are now compiled against the kernel DERIVATIONS they will ship beside, so that pairing is not expressible. nix.yml carried a comment warning that its release must NEVER be a full one, since `releases/latest` is the newest non-prerelease and a full release here "would silently become the driver everything picks up". That is now the intent rather than the hazard, and the comment says so instead of being deleted. Verified against v4.0.1: 19 cells build, modversions and shape checks pass, and BUILT_AGAINST.txt records linux_builder b87fad4 -- the v4.0.1 commit. README and docs/building.md rewritten around the nix path, both recording why the Docker path is gone rather than quietly dropping it. NOTE: the deleted test.yml contained a plaintext registry credential. It is still in git history and should be rotated independently of this change.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Moves igloo_driver to nix.
build.shand the Docker workflows are untouched and still work — this is additive while both release paths exist.Why
A module is loadable only by the exact kernel build it was compiled against. Its modversion CRCs come from that kernel's headers, and the kernel's
.configitself holds options Kconfig resolves by probing the compiler (CONFIG_STACKPROTECTOR_PER_TASK,CONFIG_INIT_STACK_*, theCC_HAS_*family). The ABI moves when the toolchain moves, with byte-identical sources and byte-identical config files.The Docker path downloads
kernel-devel-all.tar.gzfrom a linux_builder release, unpacks it, and builds against whatever is in that directory. Two problems, only one obvious:Nothing ties the unpacked tree to the kernel the module is inserted into. The pin was a release tag — a label, not an identity. Pairing v0.0.96 with linux_builder's nix kernels put 129 of 221 CRCs out of agreement; the only symptom was
igloo: disagrees about version of symbol module_layoutand a guest panic, and nothing in either release recorded what it was built against.A nix-built kernel-devel tree is not consumable from a non-nix container at all. It ships prebuilt host tools linked against the Nix store —
scripts/basic/fixdeprequests an interpreter under/nix/store— so insideembedded-toolchainsevery target died within seconds ofmakestarting (run 31767370803). That is not a bug in the tarball; it is what "prebuilt host tools" means.Taking linux_builder as a flake input makes the kernel derivation the build input.
ARCH,CROSS_COMPILEand the compiler all come out of the kernel'spassthruvia kernelsmith'sbuildModule, so a mismatched (kernel, module) pair is not representable — "built against a different kernel" is a different store path, not a runtime panic.What lands
flake.nixbuild.sh's hand-maintainedTARGETS/VERSIONSlists can no longer drift into silentSKIPPED (No kernel config)lines. Also exposeslib.<system>.buildFor(below).nix/module.nixbuildModulecall.entry = "wrapper"becausesrc/Makefileruns python3 codegen before re-entering kbuild.nix/artifacts.nix.koas separate derivations off the unstripped module, so the extract-then-strip ordering_in_container_build.shrelies on cannot be reordered into a bug.dwarf2jsonis linux_builder's pinned fork, shared rather than re-derived — the kernel ISFs and the module ISFs in a release are now produced by one identical tool, which was previously true only by coincidence.nix/release.nixBUILT_AGAINST.txt— which names each kernel's store path, i.e. the ABI itself, not a tag two different ABIs can share.nix/shape.nixpowerpc*target ontoARCH=powerpc; linux_builder shipped two bugs of exactly this shape before adding the equivalent.nix/modversions.nixModule.symvers(below)..github/workflows/nix.ymlnixdev_*tag, which cuts a prerelease.build.shset -ufix — see below.Verification
nix build .#igloo_driverproduces all 19 cells (7.1 MB):kernels/<version>/igloo.ko.<target>+.json.xz, the same layout the Docker build produces, so penguin'smk-igloo-static.nixconsumes it unchanged.nix flake check— all 38 checks pass:That last check is the point of the exercise: 222 of 222 CRCs agree where the old pairing had 129 of 221 disagreeing.
Building against a kernel derivation makes the right inputs structural. It does not prove the output is right — kbuild happily emits a module with no
__versionssection at all ifCONFIG_MODVERSIONSis off in a config, and nothing else here would notice.nix/modversions.nixcloses that, and is the check that would have caught the incident that motivated this PR.Two implementation notes worth a reviewer's eye:
objcopy: nixpkgs' binutils is built for the host target andobjcopy --dump-sectionfails outright on several machines in this matrix.MODULE_NAME_LENis(64 - sizeof(Elf_Addr)), so a modversion entry is 64 bytes on every arch. Parsing it ascrc + 56misreads every 32-bit module (found the hard way).Also in here
build.sh'srewrite_mountreferenced$PENGUIN_HOST_MOUNT_FROMunguarded underset -u, so it aborted withunbound variablewhenever that variable was unset — every local build, i.e. exactly the case its own comment says is a no-op../build.shhas been unusable locally as a result. Fixed with${VAR:-}.Where this is going
lib.<system>.buildFor { kernel, version, target }buildsigloo.koagainst an arbitrary kernelsmith-built kernel, not just one of linux_builder's cells:That is the seam for the direction this is heading — fewer changes carried as kernel patches, more of the delta in the module, eventually igloo.ko loading into a stock upstream kernel. Anything that can hand over a
buildKernelderivation can build a matching module today, without this repo enumerating that kernel anywhere.Follow-ups (not in this PR)
inputs.linux-builderis pinned to thenixdev_0.1.0prerelease. Repin once Build kernels from upstream tarballs + an IGLOO patch series, via Nix/kernelsmith linux_builder#59 lands.nixdev_*tag here, then point nix: take kernels from linux_builder's flake, not a release tarball penguin#932'sinputs.igloo-driverat the resulting release (or, better, at.#kernelsfrom this flake — it is exactly the tree penguin'smk-igloo-static.nixwants, with no tar/untar round trip).