From 62c0eb90cb867f3795a47eca7721f203ca5a676d Mon Sep 17 00:00:00 2001 From: jochens Date: Thu, 12 Nov 2026 20:54:34 +0100 Subject: [PATCH] kernel: kernel_boot reads microcode from each initramfs image (issue 1977, L6) Replaces the state record read (root's 0700) with images_with_microcode per /boot/initramfs-*.img: true when the first 64 KiB hold a path under kernel/x86/microcode/; an unreadable image goes to image_errors with its error. Test: 1977/AC5. Builds: L6 (issue 1977 note 3) --- modules/kernel/README.md | 6 +-- modules/kernel/cmd/kernel-tools/boot.go | 44 ++++++++++++++----- modules/kernel/cmd/kernel-tools/main.go | 2 +- .../kernel/cmd/kernel-tools/microcode_test.go | 27 +++++++++--- 4 files changed, 57 insertions(+), 22 deletions(-) diff --git a/modules/kernel/README.md b/modules/kernel/README.md index 2da9207db..c96e91a1c 100644 --- a/modules/kernel/README.md +++ b/modules/kernel/README.md @@ -70,7 +70,7 @@ early archive is harmless). | service | `mesh-kernel-initramfs.service` | running and enabled; restarted when the unit, the script or the drop-in changes. Later kernel upgrades build their images through the distribution's own hook, with the drop-in in place | The module does not reboot: a machine loads the new microcode at its next boot. `kernel_boot` says the revision the CPU -runs now and whether the images were built with the drop-in. +runs now and which images carry microcode. ## Kernel modules at boot, and the running kernel's modules kept @@ -92,13 +92,13 @@ module says so in its health and takes no image build there meanwhile. | tool | | answers | |---|---|---| -| `kernel_boot` | r | the loader (`systemd-boot`, `grub` or `none`), the menu's entries, the kernels in `/boot`, the entry booted (systemd-boot) or the kernel image booted (GRUB), whether the fallback kernel is on the menu with its kernel in `/boot`, the room free on `/boot` in KiB, the microcode revision the CPU runs (`/proc/cpuinfo`) and whether the initramfs images were built with the microcode drop-in (read from `/var/lib/mesh-kernel/initramfs-built`) | +| `kernel_boot` | r | the loader (`systemd-boot`, `grub` or `none`), the menu's entries, the kernels in `/boot`, the entry booted (systemd-boot) or the kernel image booted (GRUB), whether the fallback kernel is on the menu with its kernel in `/boot`, the room free on `/boot` in KiB, the microcode revision the CPU runs (`/proc/cpuinfo`) and, per `/boot/initramfs-*.img`, whether the image's first 64 KiB (the early uncompressed cpio) hold a path under `kernel/x86/microcode/` (`images_with_microcode`); an image that cannot be read is named with its error in `image_errors`, never counted as false | | `kernel_huge_pages` | r | the mode, defrag and shared-memory mode; each per-size control and what `inherit` resolves to; huge pages by kind from `/proc/meminfo` (MiB); the units holding the most file pages in huge folios (`memory.stat` `file_thp`) with their dirty and writeback bytes, khugepaged's collapses and the bytes their cgroup wrote (`io.stat`, the most any one device saw: a mirror and its members each count the same write) | ## Tests `go test ./...`: the tool over a stand-in tree holding the control node's own files of 2026-10-14; the -manifest's shape (the default, the order, the unit and its sandbox, reload, restart and health); the kernel modules loaded at boot (`tun`) and the package and boot service that keep the running kernel's modules; the boot-entries script run with `sh` over stand-in roots (a systemd-boot entries directory, a GRUB menu, a hook that copies kernels to one fixed name): the entry from the booted command line, a menu generated again only when the kernels changed, no loader, no room, the taken-over hook; `kernel_boot` over the same roots; and the microcode drop-in sourced by bash, the initramfs script run with `sh` over a stand-in mkinitcpio (once, again on a changed drop-in, a failure recording nothing) and `kernel_boot`'s microcode answer; and the huge-pages script as the mesh +manifest's shape (the default, the order, the unit and its sandbox, reload, restart and health); the kernel modules loaded at boot (`tun`) and the package and boot service that keep the running kernel's modules; the boot-entries script run with `sh` over stand-in roots (a systemd-boot entries directory, a GRUB menu, a hook that copies kernels to one fixed name): the entry from the booted command line, a menu generated again only when the kernels changed, no loader, no room, the taken-over hook; `kernel_boot` over the same roots; and the microcode drop-in sourced by bash, the initramfs script run with `sh` over a stand-in mkinitcpio (once, again on a changed drop-in, a failure recording nothing) and `kernel_boot`'s microcode answer (stand-in cpio images, one unreadable); and the huge-pages script as the mesh writes it, run with `sh` against a stand-in directory: it sets and reads back, keeps what it found once, rotates the journal once and not for `always` nor while the machine starts, goes straight to a new value on a reload without rewriting what it found, gives back what it found and then keeps nothing, refuses a value the kernel diff --git a/modules/kernel/cmd/kernel-tools/boot.go b/modules/kernel/cmd/kernel-tools/boot.go index 8ebfd49e0..603d17e07 100644 --- a/modules/kernel/cmd/kernel-tools/boot.go +++ b/modules/kernel/cmd/kernel-tools/boot.go @@ -4,6 +4,8 @@ package main // whether the fallback kernel is on the menu (novox/hq ADR 0508 ยง9, issue 1841). Reads only. import ( + "bytes" + "io" "os" "path/filepath" "regexp" @@ -25,28 +27,48 @@ type BootState struct { BootFreeKiB int64 `json:"boot_free_kib"` // MicrocodeRevision is the revision the CPU runs, the first "microcode" line of /proc/cpuinfo ("" when it has none). MicrocodeRevision string `json:"microcode_revision"` - // ImagesBuiltWithMicrocode is true when the initramfs images were last built with the microcode drop-in as it is now - // (novox/hq issue 1977). - ImagesBuiltWithMicrocode bool `json:"images_built_with_microcode"` + // ImagesWithMicrocode is, per /boot/initramfs-*.img, whether the image's early uncompressed cpio (its first 64 KiB) + // holds a path under kernel/x86/microcode/ (novox/hq issue 1977). + ImagesWithMicrocode map[string]bool `json:"images_with_microcode"` + // ImageErrors names each image that could not be read and why: such an image is in no verdict above. + ImageErrors []ImageError `json:"image_errors"` } -// stateDir is the module's own directory on the machine, where the initramfs script records the drop-in it built with. -const stateDir = "var/lib/mesh-kernel" - -const microcodeDropIn = "etc/mkinitcpio.conf.d/mesh-microcode.conf" +// ImageError is an initramfs image that could not be read. +type ImageError struct { + Image string `json:"image"` + Error string `json:"error"` +} var cpuMicrocode = regexp.MustCompile(`(?m)^microcode\s*:\s*(\S+)`) -// microcode reads the revision the CPU runs and whether the images carry the drop-in as it is now. +// microcode reads the revision the CPU runs and which initramfs images carry microcode. func (m *Machine) microcode(b *BootState) { + b.ImagesWithMicrocode = map[string]bool{} + b.ImageErrors = []ImageError{} if raw, err := m.read("proc/cpuinfo"); err == nil { if r := cpuMicrocode.FindStringSubmatch(raw); r != nil { b.MicrocodeRevision = r[1] } } - built, err := m.read(stateDir + "/initramfs-built") - now, err2 := m.read(microcodeDropIn) - b.ImagesBuiltWithMicrocode = err == nil && err2 == nil && built == now + images, _ := filepath.Glob(m.path("boot/initramfs-*.img")) + sort.Strings(images) + for _, img := range images { + name := filepath.Base(img) + f, err := os.Open(img) + if err != nil { + b.ImageErrors = append(b.ImageErrors, ImageError{name, err.Error()}) + continue + } + head := make([]byte, 64*1024) + n, err := io.ReadFull(f, head) + f.Close() + if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF { + b.ImageErrors = append(b.ImageErrors, ImageError{name, err.Error()}) + continue + } + b.ImagesWithMicrocode[name] = bytes.Contains(head[:n], []byte("kernel/x86/microcode/")) + } } var grubKernel = regexp.MustCompile(`(?m)^\s*linux\s+(?:/boot)?/(vmlinuz-\S+)`) diff --git a/modules/kernel/cmd/kernel-tools/main.go b/modules/kernel/cmd/kernel-tools/main.go index e93c4efb5..e7e729574 100644 --- a/modules/kernel/cmd/kernel-tools/main.go +++ b/modules/kernel/cmd/kernel-tools/main.go @@ -52,7 +52,7 @@ func tools(m *Machine) []stdio.Tool { "(systemd-boot, read from the EFI variable it sets, grub, or none), the menu's entries, the kernels in /boot, the entry " + "the machine booted (systemd-boot) or the kernel image it booted (grub), whether the fallback kernel linux-lts is on " + "the menu with its kernel in /boot, and the room free on /boot in KiB (the fallback kernel needs about 72 MB), the microcode revision the CPU runs " + - "(/proc/cpuinfo) and whether the initramfs images were built with the microcode drop-in (novox/hq issue 1977). " + + "(/proc/cpuinfo) and, per /boot/initramfs-*.img, whether its early cpio carries microcode (images_with_microcode; an unreadable image is named in image_errors) (novox/hq issue 1977). " + "Replaces ls /boot, bootctl list, reading grub.cfg and df /boot. (r)", Input: schema(map[string]any{}), Run: func(args map[string]any) (any, error) { return m.Boot() }, diff --git a/modules/kernel/cmd/kernel-tools/microcode_test.go b/modules/kernel/cmd/kernel-tools/microcode_test.go index 144e1d31c..e976adf09 100644 --- a/modules/kernel/cmd/kernel-tools/microcode_test.go +++ b/modules/kernel/cmd/kernel-tools/microcode_test.go @@ -123,15 +123,28 @@ func TestAC4AFailedMkinitcpioFailsTheUnitAndRecordsNothing(t *testing.T) { } // 1977/AC5 -func TestAC5KernelBootNamesTheRunningMicrocodeAndWhetherTheImagesCarryTheDropIn(t *testing.T) { +func TestAC5KernelBootNamesTheRunningMicrocodeAndWhichImagesCarryIt(t *testing.T) { root := t.TempDir() put(t, root, "proc/cpuinfo", "processor\t: 0\nmicrocode\t: 0x0a201016\nmodel name\t: x\n") - put(t, root, "etc/mkinitcpio.conf.d/mesh-microcode.conf", "HOOKS+=(microcode)\n") - if got := bootOf(t, root); got.MicrocodeRevision != "0x0a201016" || got.ImagesBuiltWithMicrocode { - t.Fatalf("no record: %+v", got) + // a stand-in early cpio: a name field, then padding, as the first member of an initramfs. + put(t, root, "boot/initramfs-linux.img", "070701000000kernel/x86/microcode/AuthenticAMD.bin\x00\x00"+strings.Repeat("z", 100)) + put(t, root, "boot/initramfs-linux-lts.img", "070701000000early/other.bin\x00"+strings.Repeat("z", 100)) + got := bootOf(t, root) + if got.MicrocodeRevision != "0x0a201016" || len(got.ImagesWithMicrocode) != 2 || + !got.ImagesWithMicrocode["initramfs-linux.img"] || got.ImagesWithMicrocode["initramfs-linux-lts.img"] || len(got.ImageErrors) != 0 { + t.Fatalf("%+v", got) } - put(t, root, "var/lib/mesh-kernel/initramfs-built", "HOOKS+=(microcode)\n") - if got := bootOf(t, root); got.MicrocodeRevision != "0x0a201016" || !got.ImagesBuiltWithMicrocode { - t.Fatalf("recorded: %+v", got) + if os.Getuid() == 0 { + t.Skip("root reads an unreadable file") + } + bad := filepath.Join(root, "boot/initramfs-linux-fallback.img") + put(t, root, "boot/initramfs-linux-fallback.img", "x") + if err := os.Chmod(bad, 0); err != nil { + t.Fatal(err) + } + got = bootOf(t, root) + if _, in := got.ImagesWithMicrocode["initramfs-linux-fallback.img"]; in || len(got.ImageErrors) != 1 || + got.ImageErrors[0].Image != "initramfs-linux-fallback.img" || got.ImageErrors[0].Error == "" { + t.Fatalf("unreadable: %+v", got) } }