Pick an installed Linux kernel on Ubuntu and make it stick: hold its packages
so apt can't reap it, make it the GRUB default, and build its initramfs if
one is missing.
Ubuntu makes it easy to boot an older kernel once and surprisingly fiddly to
keep one. The menu entry indices shift every time a kernel is added or
removed, /etc/default/grub gets replaced by package upgrades, and
unattended-upgrades will happily autoremove the kernel you were relying on.
This script does the three things that actually make a pin durable, in the
order that works.
Tested on Ubuntu 24.04 and 26.04. Should be fine on Debian and Mint.
sudo install -m755 pin-kernel /usr/local/sbin/pin-kernelNo dependencies beyond what a stock Ubuntu install already has: bash 4+,
awk, dpkg-query, apt-mark, update-grub, and either dracut or
initramfs-tools.
pin-kernel [OPTIONS] [KERNEL-VERSION]
KERNEL-VERSION is a full release (6.17.0-9-generic) or any unambiguous
substring of one (6.17.0-9, -lowlatency). Omit it and you get a numbered
picker.
$ sudo pin-kernel -l
# kernel status
1 6.17.0-9-generic running, grub default
2 6.17.0-5-generic
3 6.14.0-27-generic held, no initramfsThis lists only kernels GRUB has a working menu entry for and whose
vmlinuz is still on disk — not every linux-image-* package dpkg remembers.
$ sudo pin-kernel -n 6.14
==> Kernel: 6.14.0-27-generic
menuentry: gnulinux-advanced-2f9a…>gnulinux-6.14.0-27-generic-advanced-2f9a…
packages: linux-headers-6.14.0-27 linux-headers-6.14.0-27-generic
linux-image-6.14.0-27-generic linux-modules-6.14.0-27-generic
initramfs: will build with dracut
[dry-run] dracut --force /boot/initrd.img-6.14.0-27-generic 6.14.0-27-generic
[dry-run] apt-mark hold linux-headers-6.14.0-27 …
[dry-run] would write GRUB_DEFAULT='gnulinux-advanced-2f9a…>…'
==> Dry run complete — nothing was changed.--dry-run and --list don't need root.
sudo pin-kernel 6.14.0-27-genericThen reboot. Or test the choice without committing to it first:
sudo grub-reboot "gnulinux-advanced-…>gnulinux-6.14.0-27-generic-advanced-…"
sudo reboot(the script prints the exact command at the end). A one-shot grub-reboot
reverts by itself if the kernel doesn't come up, which is the safer way to
try an unfamiliar kernel on a remote box.
sudo pin-kernel --revertRemoves the GRUB override, releases the holds it created, and regenerates
grub.cfg. The newest installed kernel takes over again.
| Option | Effect |
|---|---|
-l, --list |
List bootable kernels and exit. No root needed. |
-y, --yes |
Skip the confirmation prompt. |
-n, --dry-run |
Print every action, change nothing. No root needed. |
--no-hold |
Set the GRUB default but leave apt marks alone. |
--no-grub |
Hold the packages but don't touch the boot default. |
--rebuild-initrd |
Rebuild the initramfs even if one already exists. |
--revert |
Undo a previous pin. |
-h, --help |
Usage. |
Exit status is 0 on success, 1 on any error (no match, ambiguous match,
not root, no GRUB config, missing initramfs tooling).
1. Build the initramfs first. grub-mkconfig only writes an initrd line
for images it can actually see. Regenerating the initramfs after
update-grub gives you a menu entry that boots to a kernel panic. The script
only does this when /boot/initrd.img-<version> is missing or zero-length,
unless you force it. It probes for dracut and falls back to
update-initramfs -c -k — relevant now that Ubuntu ships dracut on some
images while older installs still use initramfs-tools.
2. Hold the packages. Everything ending in the full release
(linux-image-…, linux-modules-…, linux-modules-extra-…,
linux-headers-…) plus the flavour-independent linux-headers-<abi>. Each
gets apt-mark manual as well as apt-mark hold: manual takes it out of
the autoremove candidate pool, hold makes apt refuse to remove or upgrade it
even if something tries.
3. Set the GRUB default, written to
/etc/default/grub.d/99-pinned-kernel.cfg rather than into
/etc/default/grub. Ubuntu's grub-mkconfig sources everything in that
directory, and using a drop-in means a grub-common upgrade that replaces the
main config file can't silently drop your pin. If the local grub-mkconfig
turns out not to read the drop-in directory, the script says so and edits
/etc/default/grub instead, after taking a timestamped backup.
The value written is a menu entry ID path, not an index:
GRUB_DEFAULT='gnulinux-advanced-2f9a…>gnulinux-6.14.0-27-generic-advanced-2f9a…'
Indices renumber whenever kernels come and go; entry titles are localized and change wording between releases. The IDs are stable and derive from the kernel version and root filesystem UUID.
- A pin is permanent until you revert it. New kernels will keep installing,
and GRUB will keep ignoring them. That's the point, but it means you can sit
on an unpatched kernel without noticing. Run
pin-kernel -loccasionally. - Held packages block related upgrades.
apt upgradewill report packages kept back once a newer kernel meta-package wants to pull in something your hold conflicts with. Expected, not broken. - systemd-boot systems aren't supported. If
/boot/grub/grub.cfgdoesn't exist the script exits and points you atbootctl set-default. - Hand-built kernels installed outside dpkg get the GRUB and initramfs steps, but the hold step warns and skips — there are no packages to hold.
- Secure Boot: an unsigned kernel won't boot with Secure Boot on, and this script won't tell you that in advance. It only checks that the image exists.
- Encrypted root / unusual storage: if you regenerate an initramfs with a different tool than the one that built the others, verify it includes your crypt and LVM modules before you rely on it.
The menu doesn't appear at boot, so I can't fall back.
Set a visible menu in /etc/default/grub and rerun update-grub:
GRUB_TIMEOUT_STYLE=menu
GRUB_TIMEOUT=5
I pinned a kernel and it didn't take. Check what actually landed in the generated config:
grep -m1 '^set default' /boot/grub/grub.cfg
sudo grub-editenv listA leftover saved_entry in grubenv from an earlier GRUB_DEFAULT=saved
setup will override things. Clear it with
sudo grub-editenv /boot/grub/grubenv unset saved_entry.
I'm locked out after a reboot.
Boot the installer in live mode, mount the root filesystem, chroot in, and run
pin-kernel --revert — or just delete
/etc/default/grub.d/99-pinned-kernel.cfg and run update-grub.