Survive kernel updates on a /boot this small
Some checks failed
build image / build (push) Has been cancelled

A dnf upgrade to 7.1.8 installed the kernel rpm cleanly, then ran /boot out
of space. dracut wrote no initramfs, and because kernel-install stops at the
first failing plugin, 95-c630-devicetree never ran either — leaving a boot
entry with neither an initrd nor a devicetree line, which on this machine can
never boot. dnf reported success and nothing retried.

A kernel costs ~336 MiB here: a 210 MiB hostonly=no initramfs, a 98 MiB
dtb-<kver> directory carrying every board's device tree, plus vmlinuz and
System.map. Three of those cannot fit 1 GiB, so /boot goes to 2 GiB and
installonly_limit drops to 2.

Also fixes the quieter half of the same trap. snd-soc-wsa881x lives outside
the kernel package, so the speakers go silent after any kernel update with
nothing in the logs to explain it. c630-wsa881x rebuilds it and
96-c630-wsa881x.install calls it on each kernel-install add — always exiting
0, since a plugin failure is precisely what caused the damage above.

grub.cfg gains the next_entry one-shot block that has been carried by hand all
along, so testing a kernel costs a power cycle rather than a rescue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011XgGF5wfxLDAybVnNz6eNQ
This commit is contained in:
2026-08-14 16:17:52 +03:00
parent b5c5218b81
commit 477d43aacd
7 changed files with 231 additions and 1 deletions

View File

@@ -365,6 +365,23 @@ if [ -s \$prefix/grubenv ]; then
load_env
fi
# One-shot boot selection. Booting a test kernel on this machine means either
# catching a 5-second menu on a laptop that may not have its display up yet, or
# making the test kernel the permanent default and hoping it boots — which has
# already cost one recovery via rd.break. Instead:
#
# sudo grub2-editenv /boot/grub2/grubenv set next_entry=c630-<kver>
#
# takes effect for exactly one boot and then reverts, so a kernel that hangs
# costs a power cycle rather than a rescue. Entry ids are the BLS file names in
# /boot/loader/entries without the .conf.
if [ "\$next_entry" ] ; then
set default="\$next_entry"
set next_entry=
save_env next_entry
set boot_once=true
fi
blscfg
EOF
: > "$ROOTFS/boot/grub2/grubenv"

View File

@@ -34,7 +34,13 @@
# --- image geometry (MiB) -----------------------------------------------
: "${ESP_SIZE_MIB:=512}"
: "${BOOT_SIZE_MIB:=1024}"
# /boot has to hold whole kernels, and this machine's are fat: the initramfs is
# built hostonly=no (~210 MiB) and kernel-core's dtb directory carries every
# board's device tree (~98 MiB), so an installed kernel costs ~336 MiB. 1024
# fits only two with nothing to spare, and running out mid-update leaves an
# unbootable entry — see overlay/etc/dnf/libdnf5.conf.d/50-c630-installonly.conf.
: "${BOOT_SIZE_MIB:=2048}"
# Total image size. The root partition takes whatever is left, and grows to
# fill the target medium on first boot.

View File

@@ -573,6 +573,27 @@ M=$PWD modules` with `obj-m += snd-soc-wsa881x.o` and `snd-soc-wsa881x-objs :=
wsa881x.o`), installed under `/lib/modules/$(uname -r)/extra/` and picked up
by `depmod -a`, it autoloads on the SoundWire modalias at boot and the
`Lenovo-YOGA-C630-13Q50` card registers, headphone-jack detection included.
Because the module lives outside the kernel package, it has to be rebuilt for
every kernel, and the symptom of forgetting is unhelpful: the speakers simply
go silent after a `dnf upgrade`, with nothing in the logs tying the two
together. `overlay/usr/local/sbin/c630-wsa881x` does the rebuild — it fetches
`wsa881x.c` at the matching upstream tag, builds it against `kernel-devel` and
installs it — and `overlay/etc/kernel/install.d/96-c630-wsa881x.install` calls
it on every `kernel-install add`. That hook always exits 0: `kernel-install`
stops at the first plugin that fails, and a missing compiler costing you the
speakers is a far better outcome than it costing you the `devicetree` line and
therefore the machine. It needs `gcc`, `make`, `kernel-devel` and a network,
none of which the minimal image installs, so on a fresh image run it by hand
once:
```sh
sudo dnf install -y gcc make kernel-devel
sudo c630-wsa881x
```
None of this is necessary once Fedora sets the config; see
[drafts/fedora-wsa881x-request.md](drafts/fedora-wsa881x-request.md).
The UCM profile has been in alsa-ucm-conf since 2020; `alsaucm` itself is in
`alsa-ucm-utils`, so on the minimal image:

View File

@@ -125,6 +125,54 @@ sudo date -u -s "2026-07-27 16:33:00"
Any roughly-correct time will do; it only has to be later than the packages
were signed.
## Kernel updates and a full /boot
A kernel costs about 336 MiB here — a ~210 MiB initramfs (built `hostonly=no`,
so it carries the whole driver and firmware set), a ~98 MiB `dtb-<kver>`
directory because `kernel-core` ships every board's device tree, plus vmlinuz
and System.map. Three of those do not fit a 1 GiB `/boot`, which is why images
now use 2 GiB and `installonly_limit=2`.
If `/boot` does fill up, the failure is quiet and genuinely dangerous:
1. The kernel rpm installs fine — `rpm -V` is clean and the module tree is
complete.
2. `50-dracut.install` then dies on ENOSPC, so **no initramfs is written**.
3. `kernel-install` stops at the first failing plugin, so
`95-c630-devicetree.install` never runs and the boot entry gets **no
`devicetree` line** — which on this machine means it can never boot,
because the UEFI supplies no device tree.
4. `dnf` reports success. Nothing retries, and later `dnf upgrade` runs look
clean because they no longer touch the kernel.
So an update can leave a kernel that is installed, listed in the boot menu, and
unbootable. To check whether it actually completed:
```sh
rpm -q kernel-core # what rpm thinks is installed
ls /boot/initramfs-* # one per kernel, or the update failed
sudo grep -L devicetree /boot/loader/entries/*.conf # any output is unbootable
```
To repair it, free space first, then redo the two steps that were skipped:
```sh
sudo dnf remove kernel-core-<old-version>
sudo dracut --force --kver <kver> /boot/initramfs-<kver>.img
sudo /etc/kernel/install.d/95-c630-devicetree.install add <kver>
```
Both are idempotent. Test the result with a one-shot boot rather than by
changing the default, so a kernel that hangs costs only a power cycle:
```sh
sudo grub2-editenv /boot/grub2/grubenv set next_entry=c630-<kver>
sudo reboot
```
Note that `speakers go silent after a kernel update` is the same event wearing a
different hat — see `c630-wsa881x` in [firmware.md](firmware.md).
## If it does not boot
The kernel command line is the first thing to change. It lives in

View File

@@ -0,0 +1,23 @@
# Keep two kernels, not the stock three.
#
# This machine's initramfs is unusually large — hostonly is off (see
# /etc/dracut.conf.d/10-c630.conf) so every image carries the full driver and
# firmware set, and each installed kernel costs roughly:
#
# initramfs ~210 MiB
# dtb-<kver> ~98 MiB (kernel-core ships every board's device tree)
# vmlinuz ~17 MiB
# System.map ~11 MiB
# ---------------------
# total ~336 MiB
#
# Three of those is ~1008 MiB, which does not fit a 1 GiB /boot. The failure is
# nastier than a full disk: dnf installs the kernel rpm successfully, then
# dracut dies on ENOSPC, and because kernel-install stops at the first failing
# plugin, 95-c630-devicetree never runs either. The result is a boot entry with
# no initrd and no devicetree — unbootable — while dnf reports success and
# never retries.
#
# Two kernels is ~672 MiB, which leaves comfortable headroom.
[main]
installonly_limit=2

View File

@@ -0,0 +1,37 @@
#!/usr/bin/bash
#
# Rebuild the speaker-amplifier module for each new kernel.
#
# snd-soc-wsa881x is not in any Fedora package (see c630-wsa881x), so it lives
# in /lib/modules/<kver>/extra and has to be rebuilt whenever a kernel is
# installed. Without this the speakers go silent the first time you run
# `dnf upgrade`, and nothing in the logs connects the two events.
#
# This runs at 96, after 95-c630-devicetree, because it must not come between
# 90-loaderentry.install — which rewrites the BLS entry — and the hook that
# puts the devicetree line back.
#
# It always exits 0. kernel-install stops at the first plugin that fails, and
# this machine has already been bitten once by that: a full /boot made
# 50-dracut.install fail, which meant 95-c630-devicetree never ran, which left
# a boot entry with neither an initrd nor a devicetree. Silent speakers are
# worth a warning; they are not worth an unbootable machine.
set -u
COMMAND="${1:?}"
KERNEL_VERSION="${2:?}"
[ "$COMMAND" = "add" ] || exit 0
if ! command -v c630-wsa881x >/dev/null; then
exit 0
fi
if ! c630-wsa881x "$KERNEL_VERSION"; then
echo "96-c630-wsa881x: could not build snd-soc-wsa881x for ${KERNEL_VERSION};" \
"the built-in speakers will be silent on that kernel until you run" \
"'sudo c630-wsa881x ${KERNEL_VERSION}'" >&2
fi
exit 0

View File

@@ -0,0 +1,78 @@
#!/usr/bin/bash
#
# Build the speaker-amplifier driver Fedora does not ship.
#
# The C630's two WSA881x amplifiers sit on the WCD9340's SoundWire bus, and
# Fedora's aarch64 kernel has never set CONFIG_SND_SOC_WSA881X — the newer
# WSA883X and WSA884X the ThinkPad X13s needs are both =m, but this one was
# missed. Without it the SoundWire slaves never bind, the machine driver cannot
# find its DAIs, and the sound card sits in deferred probe forever: no card, no
# speakers, no error that points anywhere useful.
#
# sound/soc/codecs/wsa881x.c is one self-contained file with no out-of-tree
# patches, so building it against kernel-devel and dropping it in extra/ is
# enough. Because it lives outside the kernel package, it has to be rebuilt for
# every kernel — hence 96-c630-wsa881x.install, which calls this on each
# kernel-install add. Audio otherwise goes silent the first time you take a
# kernel update, with nothing in the logs to connect the two.
#
# The real fix is Fedora flipping the config; see docs/drafts/.
#
# Usage: c630-wsa881x [kernel-version] (defaults to the running kernel)
set -euo pipefail
KVER="${1:-$(uname -r)}"
BUILD="/lib/modules/${KVER}/build"
DEST="/lib/modules/${KVER}/extra/snd-soc-wsa881x.ko"
if [ -f "$DEST" ]; then
echo "c630-wsa881x: ${DEST} already built"
exit 0
fi
# Refuse rather than half-do it: a partial build leaves no module and no clue.
missing=()
[ -d "$BUILD" ] || missing+=("kernel-devel-${KVER}")
command -v gcc >/dev/null || missing+=(gcc)
command -v make >/dev/null || missing+=(make)
if [ "${#missing[@]}" -gt 0 ]; then
echo "c630-wsa881x: cannot build for ${KVER}, missing: ${missing[*]}" >&2
echo "c630-wsa881x: install them and re-run 'c630-wsa881x ${KVER}'," \
"or the built-in speakers will be silent on that kernel" >&2
exit 1
fi
# Fedora's 7.1.8-200.fc44.aarch64 is upstream 7.1.8. A .0 upstream release is
# tagged v7.1, not v7.1.0, so try both.
base="${KVER%%-*}"
tags=("v${base}")
[ "${base}" != "${base%.0}" ] && tags+=("v${base%.0}")
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
src=""
for tag in "${tags[@]}"; do
url="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/plain/sound/soc/codecs/wsa881x.c?h=${tag}"
if curl -fsSL --retry 3 -o "$work/wsa881x.c" "$url"; then
echo "c630-wsa881x: fetched wsa881x.c at ${tag}"
src="$tag"
break
fi
done
if [ -z "$src" ]; then
echo "c630-wsa881x: could not fetch wsa881x.c for ${KVER} (tried: ${tags[*]})" >&2
echo "c630-wsa881x: needs network; re-run 'c630-wsa881x ${KVER}' once online" >&2
exit 1
fi
cat > "$work/Makefile" <<'EOF'
obj-m += snd-soc-wsa881x.o
snd-soc-wsa881x-objs := wsa881x.o
EOF
make -C "$BUILD" M="$work" modules
install -D -m 0644 "$work/snd-soc-wsa881x.ko" "$DEST"
depmod -a "$KVER"
echo "c630-wsa881x: installed ${DEST}"