LinuxLXV · Rolling Kernel UpgradesVerification
Proving a kernel campaign worked - CVEs, backports and vulnerability state
What you'll learn
- Explain why a version-string comparison produces false positives against a backporting distribution
- Map a CVE to a vendor kernel build using changelogs and advisory identifiers
- Read /sys/devices/system/cpu/vulnerabilities and state what it does and does not cover
- Produce a per-host campaign record that a scanner disagreeing with you cannot overturn
Prerequisites
Verified against Ubuntu 24.04 LTS · Debian 12 (Bookworm) · RHEL 9.x · Rocky Linux 9.x · AlmaLinux 9.x · Linux kernel 6.1 LTS / 6.6 LTS · systemd 255+ · OpenSSH 8.7p1 (RHEL 9) / 9.6p1 (Ubuntu 24.04) · nftables 1.0.x · chrony 4.x · Pacemaker 2.1.x · Corosync 3.1.x · 2026-08-11
The campaign is finished. Forty nodes have been drained, patched, rebooted, validated and returned. Somebody now has to answer a question from a security or compliance function: is the fleet patched for CVE-2025-XXXXX?
That question is harder to answer well than the campaign was to run, and the two most obvious answers are both wrong. “The package is installed” is wrong because installed is not running. “The kernel is version 5.14.0” is wrong for a reason that catches out almost everyone the first time.
Version strings do not mean what the scanner thinks
Enterprise distributions do not ship the upstream kernel that the CVE was fixed in. They take a kernel from years ago, keep its version number for the lifetime of the release, and backport individual fixes into it.
A Red Hat Enterprise Linux 9 host reports 5.14.0-427.el9. The
upstream fix for a given CVE may have landed in 6.6. Both facts
are true simultaneously, because Red Hat backported the patch
into their 5.14.0 tree and expressed that in the package release
field — the -427 — not in the version.
The consequence is that a scanner reasoning “this host runs 5.14, the fix is in 6.6, therefore vulnerable” flags every RHEL 9 host in the estate, permanently, and will continue to do so after you upgrade because the version never changes. The same applies to Ubuntu LTS, SLES and Debian stable.
Mapping a CVE to a build
The changelog is the authoritative local record. It is on every host, it does not need a network, and it names the CVE explicitly.
$ # RHEL family - the running kernel's own changelog
rpm -q --changelog "kernel-$(uname -r)" | grep -i 'CVE-2025-' | head
# Debian family - the installed kernel package changelog
dpkg-query -W -f='${binary:Package}\n' 'linux-image-*' | grep "$(uname -r)"
zgrep -i 'CVE-2025-' "/usr/share/doc/linux-image-$(uname -r)/changelog.Debian.gz" | head- redhat: fix CVE-2025-XXXXX (Foo Bar) [RHEL-98765] {CVE-2025-XXXXX}
- net: sched: fix CVE-2025-YYYYY (Baz Qux) [RHEL-98766] {CVE-2025-YYYYY}
linux-image-6.8.0-51-generic
* CVE-2025-XXXXX
- net/sched: reject invalid parent handleIllustrative output
Note the deliberate detail in the RHEL command:
kernel-$(uname -r), not kernel. Querying the package name
alone returns the newest installed kernel, which on a host
awaiting its reboot is precisely the kernel that is not running
— the same installed-versus-running confusion the rollback
lesson opens with.
On Ubuntu with a Pro subscription there is a direct tool that resolves the CVE against the vendor data and reports the state of this host:
sudo pro fix CVE-2025-XXXXX
It reports whether the host is affected, whether a fix is available, and whether it is already applied — including fixes delivered by livepatch rather than by a reboot, which is the case the next section is about.
What /sys/devices/system/cpu/vulnerabilities tells you
For the CPU-side speculative execution issues, the kernel publishes its own assessment at runtime. This is the strongest form of evidence available, because it is not a version comparison at all — it is the running kernel stating what it has detected and what it has enabled.
$ grep -r . /sys/devices/system/cpu/vulnerabilities//sys/devices/system/cpu/vulnerabilities/meltdown:Not affected
/sys/devices/system/cpu/vulnerabilities/mds:Not affected
/sys/devices/system/cpu/vulnerabilities/spectre_v1:Mitigation: usercopy/swapgs barriers and __user pointer sanitization
/sys/devices/system/cpu/vulnerabilities/spectre_v2:Mitigation: Enhanced / Automatic IBRS; IBPB: conditional; STIBP: disabled
/sys/devices/system/cpu/vulnerabilities/spec_store_bypass:Mitigation: Speculative Store Bypass disabled via prctl
/sys/devices/system/cpu/vulnerabilities/retbleed:Not affected
/sys/devices/system/cpu/vulnerabilities/srbds:Not affected
/sys/devices/system/cpu/vulnerabilities/old_microcode:Not affectedIllustrative output
Two things this output is often over-read for.
It covers hardware vulnerabilities only. Speculative execution, microarchitectural data sampling, and related CPU issues. A use-after-free in the network stack does not appear here and never will. This file set is a strong answer to one class of question and silent on the rest of the kernel.
A missing entry is a signal. old_microcode and the newer
entries only exist on kernels that know about those issues. A
fleet where some hosts have an entry and others do not is a
fleet running two kernel generations — which the campaign was
supposed to eliminate, and which is worth reconciling.
Live-patched hosts break every simple check
A host that took a live kernel patch is the case that defeats both of the obvious verification methods at once:
uname -rreports the old kernel, because the host has not rebooted. A check keyed on the running version says vulnerable.- The kernel package on disk may also be the old one, because the fix arrived as a patch module rather than as a new kernel. A check keyed on installed packages also says vulnerable.
The host is, in fact, fixed. The evidence is in the live patching state, and it has to be collected separately:
# RHEL family
sudo kpatch list
# Ubuntu
sudo canonical-livepatch status --verbose
This is the strongest argument for the policy in
linux-live-kernel-patching-concepts: live patching buys time
against the CVE deadline, and the reboot still happens on the
normal schedule. A fleet that live-patches and never reboots
accumulates hosts whose true state cannot be read from any
single command, and whose live patch is silently discarded the
next time anything reboots them.
The campaign record
Collect this per host, at the end of the campaign, and keep it. It is what you hand to an auditor and what you check the scanner’s output against.
#!/usr/bin/env bash
# /usr/local/sbin/kernel-campaign-record
set -euo pipefail
printf 'host=%s\n' "$(hostname -f)"
printf 'running_kernel=%s\n' "$(uname -r)"
printf 'newest_installed=%s\n' "$(ls -1v /boot/vmlinuz-* | tail -1 | sed 's|.*/vmlinuz-||')"
printf 'boot_time=%s\n' "$(uptime -s)"
printf 'cmdline=%s\n' "$(cat /proc/cmdline)"
printf 'vulnerable_entries=%s\n' \
"$(grep -l Vulnerable /sys/devices/system/cpu/vulnerabilities/* 2>/dev/null | wc -l)"
if command -v kpatch >/dev/null; then
printf 'livepatch=%s\n' "$(kpatch list | tr '\n' ' ')"
fi
The field that closes the campaign is the comparison between
running_kernel and newest_installed. Equal means the host is
done. Different means the host has the patch installed and is
still running the old kernel — the single most common way a
campaign is reported complete while a third of the fleet is
unprotected.
# Fleet-wide, from the control node
ansible -i /etc/ansible/prod all -m ansible.builtin.command \
-a "/usr/local/sbin/kernel-campaign-record" \
| grep -E 'running_kernel|newest_installed'
Knowledge check
Knowledge check · 4 questions
Q1. A scanner reports every RHEL 9 host as vulnerable to a CVE fixed upstream in 6.6, because the hosts report kernel 5.14.0. What is happening?
Q2. A "Not affected" entry in /sys/devices/system/cpu/vulnerabilities means the kernel has mitigated that issue.
Q3. Which checks belong in a kernel campaign close-out record? Select all that apply.
Q4. A host took a live kernel patch for a critical CVE and has not rebooted. What do uname -r and the installed package version report, and where is the evidence that the host is fixed?
Passing score: 75%. Answers are checked in this browser.