Only consider real kernel module trees when detecting the latest kernel - #943
Only consider real kernel module trees when detecting the latest kernel#943Cybis320 wants to merge 2 commits into
Conversation
dvida
left a comment
There was a problem hiding this comment.
Reviewed this carefully — the diagnosis is right, the fix is a strict improvement, and the writeup made it easy to verify. What I checked, then one thing I think should go in before merge.
Verified
bash -npasses onbbc4c07.- Reproduced the reported behaviour on a fixture containing
kernel/, three real kernel trees and a version-shaped purged leftover: old code →kernel, this branch →7.0.0-28-generic. Matches your table. - Empty or missing
/lib/modules: the glob doesn't match,dbecomes the literal*, the^[0-9]test drops it,latestis empty,should_rebootreturns 1. Also quieter than the oldls, which wrote to stderr. - Safe under the script's
set -Eeuo pipefail: every test is guarded by|| continue, and the pipeline ends intail -1, which always exits 0. modules.depis a good sentinel —depmodwrites it from the kernel package postinst, and if it were ever missing the failure direction is a missed reboot rather than a spurious one.
I could not reproduce /lib/modules/kernel on my box (no linux-modules-nvidia-* installed), so I'm taking the dpkg -S output at face value. The filter is the right defence regardless of that specific cause.
Finding 1 (major): flavour mixing reproduces the same bug
/lib/modules/ has one directory per installed flavour, not per version, and sort -V | tail -1 crosses flavours happily:
6.12.34+rpt-rpi-v8 vs 6.12.34+rpt-rpi-2712 -> ...-rpi-v8 wins
7.0.0-28-generic vs 7.0.0-28-lowlatency -> ...-lowlatency wins
Both entries survive the new ^[0-9] + modules.dep filter — they're genuine, fully installed kernels, so neither guard applies.
This lands on exactly the platform the fallback exists to serve. Raspberry Pi OS 64-bit ships both linux-image-rpi-v8 and linux-image-rpi-2712 on the same image, so a Pi 5 runs ...-rpi-2712 while latest resolves to ...-rpi-v8: permanent mismatch → one spurious reboot → do_reboot() stamps ...-rpi-v8 → loop guard matches forever → fallback silently dead. Same consequence chain you describe in the problem statement, reached from a different direction, and on RPi OS there's no /var/run/reboot-required to mask it. 32-bit Pi OS is worse — rpi-v6/v7/v7l/v8 can all be installed at once.
The fix is to also require the candidate to share the running kernel's flavour. This parse handles every naming scheme I could find:
uname -r |
flavour |
|---|---|
7.0.0-28-generic |
generic |
6.8.0-51-generic-64k |
generic-64k |
6.1.0-37-arm64 |
arm64 |
6.5.0-1010-raspi |
raspi |
6.12.34+rpt-rpi-2712 |
rpi-2712 |
6.6.51-v8+ |
v8+ |
Finding 2 (minor): inequality where ordering is meant
"$running" != "$latest" treats any difference as "newer kernel pending", so an older latest also triggers a reboot. Reachable if the running kernel's modules tree has been purged, and via the flavour skew above. Worth making it "only reboot if latest sorts strictly after running" while we're in here.
Finding 3 (nit): the loop can be half the size
Putting both tests in the glob is equivalent — same results on all my fixtures, including the no-match case:
for f in /lib/modules/[0-9]*/modules.dep; do
[[ -f "$f" ]] || continue
d="${f%/modules.dep}"; echo "${d##*/}"
doneThe [[ -f "$f" ]] guard is still needed since nullglob isn't set.
Suggested block
All three rolled together. Tested under set -Eeuo pipefail against fixtures for: Pi 5 with both flavours plus kernel/ plus a purged leftover; Ubuntu generic+lowlatency; running kernel purged leaving only an older tree; and empty /lib/modules. All gave the expected verdict.
# Fallback: compare running kernel to latest installed (works on RPi OS / Debian).
local running latest last_target flavour d f
running="$(uname -r)"
# /lib/modules/ is not a clean list of pending kernels. The NVIDIA driver packages
# own a literal /lib/modules/kernel/, purged kernels leave a version-shaped
# directory behind, and every installed *flavour* gets its own tree (Raspberry Pi
# OS ships both rpi-v8 and rpi-2712; Ubuntu can have generic and lowlatency).
# Only a same-flavour, fully installed kernel is a valid reboot target, so keep
# entries that are version-shaped, carry a modules.dep (written by depmod for
# every installed kernel), and end in the running kernel's flavour.
flavour=""
[[ "$running" =~ ^[0-9][^-]*(-[0-9]+)?-(.+)$ ]] && flavour="${BASH_REMATCH[2]}"
latest="$(for f in /lib/modules/[0-9]*/modules.dep; do
[[ -f "$f" ]] || continue
d="${f%/modules.dep}"; d="${d##*/}"
[[ -z "$flavour" || "$d" == *"-$flavour" ]] || continue
echo "$d"
done | sort -V | tail -1)"
# Only reboot when the candidate is strictly newer, so a purged running-kernel
# tree cannot trigger a "downgrade" reboot.
if [[ -n "$latest" && "$running" != "$latest" \
&& "$(printf '%s\n%s\n' "$running" "$latest" | sort -V | tail -1)" == "$latest" ]]; thenEverything below that if — loop guard, stamp, REBOOT_KERNEL_TARGET — stays as you have it.
If you take Finding 1, please extend the out-of-line test matrix with: both rpi-v8 and rpi-2712 present while running rpi-2712 → returns 1; generic and lowlatency present while running generic → returns 1; and only an older tree present while running a newer kernel → returns 1.
/lib/modules/ holds one tree per installed flavour, not per version, and sort -V crosses flavours: 6.12.34+rpt-rpi-v8 sorts above 6.12.34+rpt-rpi-2712, and 7.0.0-28-lowlatency above 7.0.0-28-generic. Both are genuine kernels with a modules.dep, so the version/modules.dep filters let them through. A Pi 5 running rpi-2712 therefore compares itself against the rpi-v8 tree and reports a mismatch on every run - the same failure the kernel/ directory caused, on the platform this fallback exists to serve, where there is no /var/run/reboot-required to mask it. Keep only candidates that end in the running kernel's flavour. Also require the candidate to be strictly newer rather than merely different, so a purged running-kernel tree that leaves only older versions behind cannot trigger a pointless "downgrade" reboot. Fold the version test into the glob while here: /lib/modules/[0-9]*/modules.dep does the same work as the digit and modules.dep checks. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Pushed the three fixes to this branch as 5f86803 — flavour match, strictly-newer guard, and the glob simplification. Your The flavour one is the reason I pushed rather than just commenting: on a Pi 5 the old and new code behave identically, because Verified
Since the repo has no shell test harness and Against the real Two things worth a second pair of eyes, since I can't test either:
Test harness#!/bin/bash
# Out-of-line harness for should_reboot() in GRMSUpdater.sh.
# Extracts the function, redirects /lib/modules, /var/run/reboot-required and uname
# at fixtures, and asserts the return value plus the flagged kernel target.
set -Eeuo pipefail
SRC="/home/dvida/source/RMS/Scripts/MultiCamLinux/GRMSUpdater.sh"
WORK="$(mktemp -d)"
trap 'rm -rf "$WORK"' EXIT
MODDIR="$WORK/lib/modules"
FLAGFILE="$WORK/reboot-required"
# Pull out should_reboot() and rewrite the two absolute paths to point at the fixtures.
awk '/^should_reboot\(\) \{/,/^\}/' "$SRC" \
| sed -e "s#/lib/modules#$MODDIR#g" -e "s#/var/run/reboot-required#$FLAGFILE#g" \
> "$WORK/fn.sh"
grep -q 'should_reboot' "$WORK/fn.sh" || { echo "FAIL: could not extract should_reboot()"; exit 1; }
# shellcheck disable=SC1090
source "$WORK/fn.sh"
RUNNING=""
uname() { [[ "${1:-}" == "-r" ]] && echo "$RUNNING" || command uname "$@"; }
log_message() { LOG="$*"; }
REBOOT_MODE="if-needed"
REBOOT_STAMP_FILE="$WORK/stamp"
REBOOT_KERNEL_TARGET=""
fails=0
pass=0
# make_fixture <mods-with-modules.dep...> -- <bare-dirs...>
make_fixture() {
rm -rf "$MODDIR"; mkdir -p "$MODDIR"
local bare=0
for e in "$@"; do
if [[ "$e" == "--" ]]; then bare=1; continue; fi
mkdir -p "$MODDIR/$e"
(( bare )) || : > "$MODDIR/$e/modules.dep"
done
}
# check <name> <running> <expected-rc> <expected-target>
check() {
local name="$1" running="$2" want_rc="$3" want_target="$4" rc=0
RUNNING="$running"; LOG=""
should_reboot || rc=$?
if [[ "$rc" == "$want_rc" && "${REBOOT_KERNEL_TARGET:-}" == "$want_target" ]]; then
printf 'ok %-52s rc=%s target=[%s]\n' "$name" "$rc" "${REBOOT_KERNEL_TARGET:-}"
(( ++pass ))
else
printf 'FAIL %-52s rc=%s (want %s) target=[%s] (want [%s]) log=%s\n' \
"$name" "$rc" "$want_rc" "${REBOOT_KERNEL_TARGET:-}" "$want_target" "$LOG"
(( ++fails ))
fi
}
rm -f "$REBOOT_STAMP_FILE" "$FLAGFILE"
echo "--- cases from the PR description (expectations unchanged) ---"
make_fixture 6.14.0-37-generic 6.17.0-40-generic 7.0.0-28-generic -- kernel 7.1.0-1-generic
: > "$FLAGFILE"
check "reboot-required present" 7.0.0-28-generic 0 ""
rm -f "$FLAGFILE"
check "running == newest installed (the reported bug)" 7.0.0-28-generic 1 ""
check "running older than newest" 6.14.0-37-generic 0 "7.0.0-28-generic"
echo "7.0.0-28-generic" > "$REBOOT_STAMP_FILE"
check "same, stamp already holds the target (loop guard)" 6.14.0-37-generic 1 ""
rm -f "$REBOOT_STAMP_FILE"
make_fixture
check "empty /lib/modules" 7.0.0-28-generic 1 ""
rm -rf "$MODDIR"
check "missing /lib/modules" 7.0.0-28-generic 1 ""
echo "--- Finding 1: flavour mixing ---"
make_fixture 6.12.34+rpt-rpi-v8 6.12.34+rpt-rpi-2712 6.12.25+rpt-rpi-2712 -- kernel
check "Pi 5: running rpi-2712, newer rpi-v8 present" 6.12.34+rpt-rpi-2712 1 ""
check "Pi 4: running rpi-v8, rpi-2712 also installed" 6.12.34+rpt-rpi-v8 1 ""
check "Pi 5: genuine same-flavour upgrade pending" 6.12.25+rpt-rpi-2712 0 "6.12.34+rpt-rpi-2712"
make_fixture 7.0.0-28-generic 7.0.0-28-lowlatency -- kernel
check "Ubuntu: running generic, lowlatency installed" 7.0.0-28-generic 1 ""
make_fixture 6.8.0-51-generic-64k 6.8.0-51-generic
check "flavour suffix with a dash (generic-64k)" 6.8.0-51-generic-64k 1 ""
make_fixture 6.6.51-v8+ 6.6.62-v8+
check "old RPi naming (6.6.51-v8+)" 6.6.51-v8+ 0 "6.6.62-v8+"
echo "--- Finding 2: only reboot for a strictly newer kernel ---"
make_fixture 6.14.0-37-generic
check "running kernel tree purged, only older left" 7.0.0-28-generic 1 ""
make_fixture 6.14.0-37-generic 7.0.0-28-generic
check "older and newer both present, running older" 6.14.0-37-generic 0 "7.0.0-28-generic"
echo "--- reboot mode passthrough ---"
REBOOT_MODE="always"; check "--reboot always" 7.0.0-28-generic 0 ""
REBOOT_MODE="none"; check "no reboot requested" 6.14.0-37-generic 1 ""
REBOOT_MODE="if-needed"
echo
if (( fails )); then echo "$fails FAILED, $pass passed"; exit 1; fi
echo "all $pass passed" |
|
@Cybis320 can do run a quick test on your end? |
|
Thanks Denis — merged your 5f86803 into The Pi flavour scenario ( On the flavour parse — I went through the naming schemes I could find and it holds for Will report the Ubuntu-machine results here. |
Problem
should_reboot()picked the newest installed kernel with:latest="$(ls /lib/modules/ | sort -V | tail -1)"Not every entry in
/lib/modules/is a kernel version directory. On Ubuntu 24.04 with the NVIDIA driver packages installed, a literalkernel/directory exists there and is owned by dpkg:Since
ksorts after digits,sort -V | tail -1returnskernel, so the comparison on the next line always reports a mismatch. Observed on a live station with nothing actually pending (no/var/run/reboot-required, running kernel already the newest installed):/var/run/reboot-requiredmasks this on Ubuntu, so the fallback only misfires when that file is absent, i.e. exactly on the RPi OS / Debian systems the fallback exists to serve. On a machine with working passwordlesssudo shutdownthe consequence is one spurious reboot, after whichdo_reboot()stampskernelinto the stamp file,last_target == latestholds forever, and the loop guard permanently disables the fallback. One unnecessary reboot, then silent loss of the feature.Deleting
/lib/modules/kernelis not a fix — dpkg owns it and it returns on the next NVIDIA driver update.Fix
Consider only entries that are version-shaped and contain a
modules.dep, whichdepmodwrites into every genuinely installed kernel's directory.sort -Vordering is unchanged, and the loop-guard/stamp logic is untouched — it behaves correctly oncelatestis right.The
modules.depguard also filters half-removed kernels, which are version-shaped and would otherwise trigger the same spurious reboot. Against a directory containing bothkernel/and a purged-kernel leftover:^[0-9]only7.1.0-1-generic(the leftover)^[0-9]+modules.dep7.0.0-28-genericTesting
bash -npasses;shellcheckreports nothing new (the two existing SC2155/SC2106 warnings are elsewhere in the file and pre-existing).should_reboot()was exercised out-of-line with/lib/modules,/var/run/reboot-requiredandunameredirected at a fixture directory containing the layout above:reboot-requiredpresent -> returns 07.0.0-28-generic/lib/modules-> returns 1