diff --git a/modules/kernel/README.md b/modules/kernel/README.md index c96e91a1c..388d0664a 100644 --- a/modules/kernel/README.md +++ b/modules/kernel/README.md @@ -56,7 +56,7 @@ built for the fallback kernel too) after its boot-entries service, so the entry ## Microcode in every initramfs (novox/hq issue 1977) Before this, `amd-ucode` was installed by hand on two machines and no initramfs carried it, so a machine booted from -its default entry ran the CPU's older microcode (ace: revision 0x0a201016 while the package held 0x0a20102e). The module +its default entry ran the CPU's older microcode (one machine: revision 0x0a201016 while the package held 0x0a20102e). The module declares the packages and makes mkinitcpio's own `microcode` hook part of every image, so every boot entry loads it, a default entry written by hand included, and no entry is edited. The fallback entry's own microcode lines stay (a second early archive is harmless). @@ -65,8 +65,8 @@ early archive is harmless). |---|---|---| | packages | `amd-ucode`, `intel-ucode` | both on every machine; the `autodetect` hook keeps only the running CPU's microcode in the default image | | file | `/etc/mkinitcpio.conf.d/mesh-microcode.conf` | read by mkinitcpio after `mkinitcpio.conf`: appends `microcode` to `HOOKS` unless it names it | -| file | `${dir:state}/initramfs` | the script, `apply`: when the drop-in differs from the one the images were last built with (`${dir:state}/initramfs-built`) it runs `mkinitcpio -P` and then records the drop-in; otherwise it says "as it was". A failed mkinitcpio fails the unit, says so and records nothing | -| file | `/etc/systemd/system/mesh-kernel-initramfs.service` | oneshot, sandboxed: the file system read-only but for `/boot` and the module's state, a private `/tmp`, no home, no network | +| file | `${dir:state}/initramfs` | the script, `apply`: when the drop-in differs from the one the images were last built with (`${dir:state}/initramfs-built`) it runs `mkinitcpio -P` and then records the drop-in; otherwise it says "as it was". Before building it takes a preset's `ALL_config="/etc/mkinitcpio.conf"` line out (that line makes mkinitcpio skip `mkinitcpio.conf.d`; a copy of the preset goes to `${dir:state}/taken-over/`; another `ALL_config` is left and named); after building, an image without `kernel/x86/microcode/` in its first 64 KiB fails the unit, which names it. A failed mkinitcpio fails the unit, says so and records nothing | +| file | `/etc/systemd/system/mesh-kernel-initramfs.service` | oneshot, sandboxed: the file system read-only but for `/boot`, `/etc/mkinitcpio.d` and the module's state, a private `/tmp`, no home, no network | | 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 diff --git a/modules/kernel/cmd/kernel-tools/microcode_test.go b/modules/kernel/cmd/kernel-tools/microcode_test.go index e976adf09..75029fd7f 100644 --- a/modules/kernel/cmd/kernel-tools/microcode_test.go +++ b/modules/kernel/cmd/kernel-tools/microcode_test.go @@ -58,30 +58,35 @@ func TestAC2TheDropInAddsMicrocodeToHooksOnce(t *testing.T) { } } -type initBed struct{ script, dropin, state, runs, bin string } +type initBed struct{ script, dropin, state, runs, bin, presets, boot string } func newInitBed(t *testing.T, mkinitcpioExit string) initBed { t.Helper() dir := t.TempDir() b := initBed{script: filepath.Join(dir, "initramfs"), dropin: filepath.Join(dir, "mesh-microcode.conf"), state: filepath.Join(dir, "state"), - runs: filepath.Join(dir, "runs"), bin: filepath.Join(dir, "bin")} - for _, d := range []string{b.state, b.bin} { + runs: filepath.Join(dir, "runs"), bin: filepath.Join(dir, "bin"), presets: filepath.Join(dir, "presets"), boot: filepath.Join(dir, "boot")} + for _, d := range []string{b.state, b.bin, b.presets, b.boot} { if err := os.MkdirAll(d, 0o755); err != nil { t.Fatal(err) } } content := strings.ReplaceAll(manifest(t).resource(t, "initramfs").text(t), "${dir:state}", b.state) - if strings.Contains(content, "${dir") { + if strings.Contains(content, "${") { t.Fatal("a substitution is left in the script") } write(t, b.script, content, 0o755) write(t, b.dropin, "HOOKS+=(microcode)\n", 0o644) - write(t, filepath.Join(b.bin, "mkinitcpio"), "#!/bin/sh\necho \"$@\" >>"+b.runs+"\nexit "+mkinitcpioExit+"\n", 0o755) + // the stand-in mkinitcpio writes an image whose early archive holds microcode. + b.mkinitcpio(t, "kernel/x86/microcode/AuthenticAMD.bin", mkinitcpioExit) return b } +func (b initBed) mkinitcpio(t *testing.T, early, exit string) { + write(t, filepath.Join(b.bin, "mkinitcpio"), "#!/bin/sh\necho \"$@\" >>"+b.runs+"\nprintf '070701%s\\0' "+early+" >"+b.boot+"/initramfs-linux.img\nexit "+exit+"\n", 0o755) +} + func (b initBed) run(t *testing.T) (string, error) { - cmd := exec.Command("sh", b.script, "apply", b.dropin, b.state) + cmd := exec.Command("sh", b.script, "apply", b.dropin, b.state, b.presets, b.boot) cmd.Env = append(os.Environ(), "PATH="+b.bin+string(os.PathListSeparator)+os.Getenv("PATH")) out, err := cmd.CombinedOutput() return string(out), err @@ -148,3 +153,38 @@ func TestAC5KernelBootNamesTheRunningMicrocodeAndWhichImagesCarryIt(t *testing.T t.Fatalf("unreadable: %+v", got) } } + +// 1977/AC7 +func TestAC7ADefaultConfigPresetIsTakenOverAndAnotherIsLeftAndNamed(t *testing.T) { + b := newInitBed(t, "0") + preset := "ALL_kver=\"/boot/vmlinuz-linux\"\nALL_config=\"/etc/mkinitcpio.conf\"\nPRESETS=('default')\n" + write(t, filepath.Join(b.presets, "linux.preset"), preset, 0o644) + other := "ALL_config=\"/etc/other.conf\"\nPRESETS=('default')\n" + write(t, filepath.Join(b.presets, "linux-lts.preset"), other, 0o644) + out, err := b.run(t) + if err != nil { + t.Fatalf("%v %s", err, out) + } + if got, _ := os.ReadFile(filepath.Join(b.presets, "linux.preset")); string(got) != "ALL_kver=\"/boot/vmlinuz-linux\"\nPRESETS=('default')\n" { + t.Fatalf("preset: %q", got) + } + if got, _ := os.ReadFile(filepath.Join(b.state, "taken-over", "linux.preset")); string(got) != preset { + t.Fatalf("copy: %q", got) + } + if got, _ := os.ReadFile(filepath.Join(b.presets, "linux-lts.preset")); string(got) != other || !strings.Contains(out, "linux-lts.preset sets ALL_config=\"/etc/other.conf\"") { + t.Fatalf("other: %q %s", got, out) + } +} + +// 1977/AC8 +func TestAC8AnImageWithoutMicrocodeFailsTheUnitNamesItAndRecordsNothing(t *testing.T) { + b := newInitBed(t, "0") + b.mkinitcpio(t, "early/other.bin", "0") + out, err := b.run(t) + if err == nil || !strings.Contains(out, "initramfs-linux.img") { + t.Fatalf("%v %s", err, out) + } + if _, err := os.Stat(filepath.Join(b.state, "initramfs-built")); !os.IsNotExist(err) { + t.Fatalf("recorded: %v", err) + } +} diff --git a/modules/kernel/files/initramfs.sh b/modules/kernel/files/initramfs.sh index 92982f467..d7d82c185 100644 --- a/modules/kernel/files/initramfs.sh +++ b/modules/kernel/files/initramfs.sh @@ -2,18 +2,28 @@ # The initramfs images as the mesh builds them (module kernel, novox/hq issue 1977). Run by # mesh-kernel-initramfs.service: # -# initramfs apply [ [ []]] +# initramfs apply [ [ [ []]]] # # `apply` builds every image again (mkinitcpio -P) when the drop-in differs from the one the images were last built # with, kept in the state as `initramfs-built`, and records the drop-in only after mkinitcpio succeeded. Later -# kernel upgrades build the images through the distribution's own hook, with the drop-in in place. The arguments -# are only for the module's tests. +# kernel upgrades build the images through the distribution's own hook, with the drop-in in place. +# +# A preset that names the default configuration (ALL_config="/etc/mkinitcpio.conf") makes mkinitcpio pass -c and skip +# mkinitcpio.conf.d, so the drop-in is never read: before building, that line is taken out of the preset, a copy of +# the preset kept in the state's taken-over directory (novox/hq ADR 0508 ยง10's way with a file the mesh did not +# write); any other ALL_config is left and named. After building, every image must hold microcode in its first +# 64 KiB (the early uncompressed archive), else the unit fails and records nothing. The arguments are only for the +# module's tests. set -eu dropin=/etc/mkinitcpio.conf.d/mesh-microcode.conf state='${dir:state}' [ $# -ge 2 ] && dropin=$2 [ $# -ge 3 ] && state=$3 +presets=/etc/mkinitcpio.d +boot=/boot +[ $# -ge 4 ] && presets=$4 +[ $# -ge 5 ] && boot=$5 [ $# -ge 1 ] && [ "$1" = apply ] || { echo "usage: initramfs apply [ []]" >&2 @@ -27,10 +37,35 @@ if [ -f "$state/initramfs-built" ] && cmp -s "$dropin" "$state/initramfs-built"; echo "initramfs: the images were built with this drop-in, as it was" exit 0 fi +for preset in "$presets"/*.preset; do + [ -f "$preset" ] || continue + if grep -qx 'ALL_config="/etc/mkinitcpio.conf"' "$preset"; then + mkdir -p "$state/taken-over" + cp "$preset" "$state/taken-over/$(basename "$preset")" + grep -vx 'ALL_config="/etc/mkinitcpio.conf"' "$preset" >"$preset.new" || true + cat "$preset.new" >"$preset" + rm -f "$preset.new" + echo "initramfs: $preset named the default configuration, which skips mkinitcpio.conf.d; that line is taken out, a copy is in $state/taken-over" + elif grep -q '^ALL_config=' "$preset"; then + echo "initramfs: $preset sets $(grep '^ALL_config=' "$preset" | head -n 1), so the microcode drop-in may not be read; left as it is" + fi +done if ! mkinitcpio -P; then echo "initramfs: mkinitcpio failed; nothing recorded, the next start tries again" >&2 exit 1 fi +missing= +for img in "$boot"/initramfs-*.img; do + [ -e "$img" ] || continue + if ! head -c 65536 "$img" >"$state/image-head.tmp" || ! grep -aq 'kernel/x86/microcode/' "$state/image-head.tmp"; then + missing="$missing $(basename "$img")" + fi +done +rm -f "$state/image-head.tmp" +if [ -n "$missing" ]; then + echo "initramfs: mkinitcpio ran, but no microcode is in the early archive of:$missing; nothing recorded" >&2 + exit 1 +fi cp "$dropin" "$state/initramfs-built.new" mv "$state/initramfs-built.new" "$state/initramfs-built" echo "initramfs: the images are built with the microcode drop-in" diff --git a/modules/kernel/files/mesh-kernel-initramfs.service b/modules/kernel/files/mesh-kernel-initramfs.service index 95df9ba3f..c1a2515c3 100644 --- a/modules/kernel/files/mesh-kernel-initramfs.service +++ b/modules/kernel/files/mesh-kernel-initramfs.service @@ -10,9 +10,9 @@ Wants=local-fs.target Type=oneshot RemainAfterExit=yes ExecStart=${dir:state}/initramfs apply -# It writes the boot partition and its own state; mkinitcpio keeps its work in a private /tmp. +# It writes the boot partition, the presets and its own state; mkinitcpio keeps its work in a private /tmp. ProtectSystem=strict -ReadWritePaths=/boot ${dir:state} +ReadWritePaths=/boot /etc/mkinitcpio.d ${dir:state} PrivateTmp=yes ProtectHome=yes PrivateNetwork=yes