survive a failed kernel install and let callers see failed applies - #106
survive a failed kernel install and let callers see failed applies#106cclements wants to merge 1 commit into
Conversation
install_kernel() moves /boot/kernel to /boot/kernel.old and extracts the new set straight into place. When the extraction dies halfway (disk full, I/O error, bad media) the default kernel is left partially populated and the pending marker was already consumed, so nothing will retry -- the next boot strands the box at the loader prompt unless someone walks up to a console and picks kernel.old by hand. Put the old kernel back whenever tar or kldxref fail; keeping the marker instead would be worse, since a retried install would first delete kernel.old and then bury the wreckage on top of it. While here, make consumed-marker applies (-K/-B/-P) exit 2 on failure instead of 1. The boot-time upgrade hook in core uses exit 1 as "nothing pending, try the next stage", which until now was indistinguishable from a stage that destroyed something, letting the loop apply base and packages on top of a broken kernel and reboot. The matching core change stops the loop on exit 2; older core keeps seeing plain non-zero and behaves as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
I’m open to improving this, but not via AI. Nobody ever raised a relevant bug report before and there is at least a partial red herring in this analysis. |
|
Thinking about it I’m not sure what an atomic way to switch would be… does the system boot with kernel.old in place while kernel is not there? We could also extract to kernel.new first and move directories afterwards but also not atomic. There is also the possibility of using „nextboot“ and switching kernel after a successful boot. However, intermittent boot failures or operational issues like premature reset or boot loader usage could prevent the kernel from switching post-boot correctly. Other consideration is whether the partial extract state is even recoverable since it can depend on a system event, not just an operational issue with a single command. @AdSchellevis mentioned this in the write config PR. Cheers, |
install_kernel()moves /boot/kernel to kernel.old, then untars the new setdirectly into place. If tar dies partway through — disk full, I/O error, bad
media — the function exits, but by then the pending marker has already been eaten
and /boot/kernel is a partial stub. Nothing retries, and the loader will pick that
stub by default on the next boot, so a headless machine ends up stuck at the loader
prompt until someone reaches a console and boots kernel.old by hand. The 05-upgrade
hook in core makes this worse than it sounds, because it reboots the moment any
later stage succeeds (see companion PR opnsense/core#10647).
The fix is to put the old kernel back whenever tar or kldxref fail. I looked at
keeping the pending marker for a retry instead, but that is actively dangerous with
the current layout: a second attempt starts by deleting kernel.old — the only good
kernel left — and then moves the wreckage into its place. Restoring immediately is
the safe shape.
Second change: the deferred apply paths (-K/-B/-P) now exit 2 on failure instead
of 1. Exit 1 stays reserved for "no pending marker", which is what the comment in
those branches already promises ("must error out to prevent reboot"). Until now a
caller literally could not tell that apart from an apply that destroyed something,
and core's boot loop used plain success/failure as its only signal. With this, the
matching core change stops the boot sequence on exit 2 instead of stacking base and
packages on top of a broken kernel. Compatibility works out in both directions: old
core with new update treats any non-zero as before, and new core with old update
keeps seeing exit 1 everywhere, so the loop behaves exactly like today.
The FAILURE=2 flag flips on right after the marker is consumed, and the deferred
paths never call fetch_set (that belongs to the lowercase fetch modes), so every
exit_msg it affects really is a botched apply. The base/kernel version-mismatch
abort in the -B path used a bare exit 1, sidestepping exit_msg; it now honours the
flag too, so the boot hook reports the aborted upgrade instead of finishing the
boot without a word.
Known limitation I left alone on purpose: a pkg-level failure inside install_pkgs
(-P) still falls out with exit 0, because the function swallows the result of the
pkg upgrade run. That's pre-existing behaviour, P is the last stage in the boot
sequence, and by that point kernel and base have already applied and rebooted
cleanly, so there is nothing destructive left to protect. Fixing the reporting
there felt like a separate discussion.
🤖 Generated with Claude Code