scanner-cve-2026-99999.sh (72068B)
1 #!/usr/bin/env bash
2 # =============================================================================
3 # scanner-cve-2026-99999.sh (English output · Korean edition: scanner-cve-2026-99999_kr.sh)
4 # CVE-2026-99999 "PwnEscape" — Linux kernel exposure check and patch guidance
5 # -----------------------------------------------------------------------------
6 # Component : kernel IPv4 stack (skb_checksum_help → ip_do_fragment)
7 # Cause : checksum_start/csum_offset supplied through the virtio-net header or
8 # packet sockets is not validated against the IPv4 header, so completing
9 # the checksum can overwrite an already-validated header (IHL) (CWE-20)
10 # Impact : guest VM → host memory disclosure · host crash · root code execution on host
11 # Score : CVSS 3.1 9.3 Critical (kernel CNA) / 8.8 Important (Red Hat)
12 # Introduced: Linux 2.6.27 (f43798c27684)
13 # Fixed by : 6785011f8b16 "ipv4: validate checksum_start before completing checksum"
14 # Workaround: none published — boot a fixed kernel
15 #
16 # Verdicts use vendor advisories as of 2026-10-20. Fixes released later are picked up
17 # by searching the installed kernel package changelog for the CVE / fix commit.
18 #
19 # Usage : bash scanner-cve-2026-99999.sh [-l] [--no-color] [...] (see -h)
20 # Menu after the verdict: 1) patch now (update kernel → reboot)
21 # 2) patch manually later (print commands) 3) exit
22 # When not run from a terminal (cron, pipes) it behaves like option 2
23 # Exit codes: 0 safe / 1 vulnerable (fix available) / 2 fixed kernel installed, reboot needed
24 # 3 vulnerable (no vendor fix yet or end of life) / 4 undetermined
25 # =============================================================================
26
27 # If started with sh/dash, re-run under bash when available (uses bash features such as read -t)
28 if [ -z "${BASH_VERSION:-}" ] && [ -f "$0" ] && command -v bash >/dev/null 2>&1; then
29 exec bash "$0" "$@"
30 fi
31
32 set -u
33
34 CVE="CVE-2026-99999"
35 FIX_TITLE="ipv4: validate checksum_start before completing checksum"
36 FIX_COMMIT="6785011f8b16"
37 FIX_URL="https://git.kernel.org/stable/c/6785011f8b16abf40b1e9d8b2e332232b3e68822"
38 DATA_DATE="2026-10-20"
39 # Changelog entries written before this year cannot contain the fix (CVE reserved 2026-09,
40 # fix committed 2026-10). Changelogs list the newest entry first, so reading stops there.
41 CHANGELOG_CUTOFF_YEAR=2026
42
43 # First fixed release per upstream stable branch (kernel CNA, 2026-10-15). Mainline: 7.3-rc7.
44 UPSTREAM_FIXED="5.10.272 5.15.223 6.1.190 6.6.159 6.12.113 6.18.57 7.2.11"
45 UPSTREAM_TEXT="5.10.272 · 5.15.223 · 6.1.190 · 6.6.159 · 6.12.113 · 6.18.57 · 7.2.11 · 7.3-rc7 or later"
46
47 OS_RELEASE_FILE=/etc/os-release
48 OSR_GIVEN=0
49 KREL=""
50 KVER=""
51 OFFLINE=0
52 USE_COLOR=1
53 LIST_ONLY=0
54 SUDO=""
55 OS_MAJ_NUM=0
56 # Define global state up front so every code path is safe under set -u
57 RUN_DVER=""; VIRT=""; CTR=""; HV_HOST=0; FINAL=""; R_MODE=""; R_PKG=""; R_ADV=""; R_FIXED=""
58 REBOOT_TARGET=""; REBOOT_KREL=""; NB_ALT=""; NEXT_WARN=""; LP_NAME=""; E_INPLACE=0
59
60 # -----------------------------------------------------------------------------
61 # Fixed kernels per vendor
62 # Format: branch|version-release to compare (no dist tag)|fixed package|advisory|package name
63 # -----------------------------------------------------------------------------
64 vendor_table() {
65 case $1 in
66 alma8) echo '4.18|4.18.0-553.174.2|kernel-4.18.0-553.174.2.el8_10|AlmaLinux advisory 2026-10-16, testing repo|kernel' ;;
67 alma9) echo '5.14|5.14.0-687.57.2|kernel-5.14.0-687.57.2.el9_8|AlmaLinux advisory 2026-10-16, testing repo|kernel' ;;
68 alma10) echo '6.12|6.12.0-211.66.2|kernel-6.12.0-211.66.2.el10_2|AlmaLinux advisory 2026-10-16, testing repo|kernel' ;;
69 uek)
70 cat <<'EOF'
71 5.4|5.4.17-2136.360.3.1|kernel-uek-5.4.17-2136.360.3.1 (UEK R6, OL7/8)|ELSA-2026-500397|kernel-uek
72 5.15|5.15.0-325.221.6.4|kernel-uek-5.15.0-325.221.6.4 (UEK R7, OL8/9)|ELSA-2026-500398|kernel-uek
73 6.12|6.12.0-207.111.5.4|kernel-uek-6.12.0-207.111.5.4 (UEK R8, OL9/10)|ELSA-2026-500399|kernel-uek
74 EOF
75 ;;
76 al2)
77 cat <<'EOF'
78 4.14|4.14.355-287.753|kernel-4.14.355-287.753.amzn2|ALAS2-2026-3962|kernel
79 5.4|5.4.302-229.498|kernel-5.4.302-229.498.amzn2|ALAS2KERNEL-5.4-2026-131|kernel
80 5.10|5.10.269-270.1108|kernel-5.10.269-270.1108.amzn2|ALAS2KERNEL-5.10-2026-133|kernel
81 5.15|5.15.221-157.262|kernel-5.15.221-157.262.amzn2|ALAS2KERNEL-5.15-2026-117|kernel
82 EOF
83 ;;
84 al2023)
85 cat <<'EOF'
86 6.1|6.1.188-233.385|kernel-6.1.188-233.385.amzn2023|ALAS2023-2026-3197|kernel
87 6.12|6.12.110-135.201|kernel6.12-6.12.110-135.201.amzn2023|ALAS2023-2026-3196|kernel6.12
88 6.18|6.18.54-121.166|kernel6.18-6.18.54-121.166.amzn2023|ALAS2023-2026-3192|kernel6.18
89 EOF
90 ;;
91 esac
92 }
93
94 # -----------------------------------------------------------------------------
95 # Output helpers
96 # -----------------------------------------------------------------------------
97 setup_colors() {
98 local e
99 if [ "$USE_COLOR" = 1 ] && [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
100 e=$(printf '\033')
101 C_RED="$e[1;31m"; C_GRN="$e[1;32m"; C_YEL="$e[1;33m"
102 C_BLU="$e[1;36m"; C_DIM="$e[2m"; C_OFF="$e[0m"
103 else
104 C_RED=; C_GRN=; C_YEL=; C_BLU=; C_DIM=; C_OFF=
105 fi
106 }
107 section() { printf '\n%s■ %s%s\n' "$C_BLU" "$1" "$C_OFF"; }
108 kv() { printf ' %s: %s\n' "$1" "$2"; }
109 line() { printf ' %s\n' "$*"; }
110 note() { printf ' %s* %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
111 cmd() { printf ' %s%s%s\n' "$C_GRN" "$*" "$C_OFF"; }
112 cmt() { printf ' %s# %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
113 die() { printf 'Error: %s\n' "$*" >&2; exit 4; }
114
115 # Look a command up in PATH once and cache the result
116 have() {
117 local v="HAVE_${1//[!A-Za-z0-9]/_}"
118 eval "case \${$v-} in 1) return 0 ;; 0) return 1 ;; esac"
119 if command -v "$1" >/dev/null 2>&1; then eval "$v=1"; return 0; fi
120 eval "$v=0"; return 1
121 }
122
123 # True if stdin (a newest-first changelog) records the fix.
124 # - the CVE id (not adjacent to other letters/digits) or the "ipv4: validate checksum_start ..." title
125 # - lines containing "revert" never count as the fix
126 # - stops at the first entry date (rpm "* Mon Oct 13 2026 ..." / deb " -- Name Mon, 13 Oct 2026 ...")
127 # older than CHANGELOG_CUTOFF_YEAR → only the recent part of a multi-MB changelog is read
128 # It exits as soon as it matches or stops, so the upstream rpm/gzip also stops (SIGPIPE).
129 has_fix_text() {
130 LC_ALL=C awk -v cve="$CVE" -v title="$FIX_TITLE" -v cutoff="$CHANGELOG_CUTOFF_YEAR" '
131 BEGIN { cve = tolower(cve); title = tolower(title); found = 0 }
132 {
133 l = tolower($0)
134 if ((substr(l, 1, 2) == "* " || substr(l, 1, 4) == " -- ") &&
135 match(l, / (19|20)[0-9][0-9]( |$)/)) {
136 if (substr(l, RSTART + 1, 4) + 0 < cutoff) exit
137 }
138 if (index(l, "revert")) next
139 p = index(l, cve)
140 if (p) {
141 a = (p > 1) ? substr(l, p - 1, 1) : ""
142 b = substr(l, p + length(cve), 1)
143 if (a !~ /[0-9a-z]/ && b !~ /[0-9a-z]/) { found = 1; exit }
144 }
145 if (index(l, title)) { found = 1; exit }
146 }
147 END { exit found ? 0 : 1 }'
148 }
149
150 usage() {
151 cat <<EOF
152 Usage: $0 [options]
153
154 Checks this system's distribution and running kernel for $CVE (PwnEscape).
155 If it is vulnerable, choose one of:
156
157 1. Patch now update the kernel with the distro package manager and reboot
158 2. Patch manually later only print the commands to run
159 3. Exit
160
161 Checking works as a regular user. Option 1 needs root and uses sudo.
162 When not run from a terminal (cron, pipes) no menu is shown; it prints the commands like option 2.
163
164 Options:
165 -l, --list print only the per-distribution fixed-version table
166 --no-color disable colored output
167 --os-release FILE use this os-release file (default: /etc/os-release)
168 --uname-r STRING kernel release to judge (e.g. 'uname -r' from another server)
169 --uname-v STRING kernel build string ('uname -v', used for Debian)
170 -h, --help show this help
171
172 With --uname-r, local checks of this system (changelog, installed kernels,
173 livepatch, virtualization host) are skipped.
174
175 Exit codes:
176 0 safe 1 vulnerable (fix available) 2 fixed kernel installed, reboot needed
177 3 vulnerable (no vendor fix yet or end of life) 4 undetermined
178 EOF
179 }
180
181 parse_args() {
182 while [ $# -gt 0 ]; do
183 case $1 in
184 -l|--list) LIST_ONLY=1 ;;
185 --no-color) USE_COLOR=0 ;;
186 --os-release) [ $# -ge 2 ] || die "--os-release requires a file path"
187 OS_RELEASE_FILE=$2; OSR_GIVEN=1; shift
188 [ -r "$OS_RELEASE_FILE" ] || die "cannot read os-release file: $OS_RELEASE_FILE" ;;
189 --uname-r) [ $# -ge 2 ] || die "--uname-r requires a value"; KREL=$2; OFFLINE=1; shift ;;
190 --uname-v) [ $# -ge 2 ] || die "--uname-v requires a value"; KVER=$2; shift ;;
191 -h|--help) usage; exit 0 ;;
192 *) usage >&2; exit 4 ;;
193 esac
194 shift
195 done
196 # Mixing the real kernel release with a supplied build string would skew the verdict
197 if [ -n "$KVER" ] && [ "$OFFLINE" = 0 ]; then
198 die "--uname-v must be used together with --uname-r"
199 fi
200 }
201
202 # -----------------------------------------------------------------------------
203 # Version comparison (rpm: rpmvercmp, deb: dpkg rules). deb uses dpkg when available.
204 # -----------------------------------------------------------------------------
205 AWK_VERCMP='
206 function numcmp(x, y) {
207 sub(/^0+/, "", x); sub(/^0+/, "", y)
208 if (length(x) != length(y)) return (length(x) < length(y)) ? -1 : 1
209 if (x == y) return 0
210 return (x < y) ? -1 : 1
211 }
212 function splitvr(s, i) {
213 V = s; R = ""
214 for (i = length(s); i > 0; i--)
215 if (substr(s, i, 1) == "-") { V = substr(s, 1, i - 1); R = substr(s, i + 1); return }
216 }
217 function epoch(s) {
218 E = 0; S = s
219 if (match(s, /^[0-9]+:/)) { E = substr(s, 1, RLENGTH - 1); S = substr(s, RLENGTH + 1) }
220 }
221 function rpmvercmp(a, b, sa, sb, r) {
222 if (a == b) return 0
223 while (a != "" || b != "") {
224 sub(/^[^A-Za-z0-9~^]+/, "", a); sub(/^[^A-Za-z0-9~^]+/, "", b)
225 if (substr(a, 1, 1) == "~" || substr(b, 1, 1) == "~") {
226 if (substr(a, 1, 1) != "~") return 1
227 if (substr(b, 1, 1) != "~") return -1
228 a = substr(a, 2); b = substr(b, 2); continue
229 }
230 if (substr(a, 1, 1) == "^" || substr(b, 1, 1) == "^") {
231 if (a == "") return -1
232 if (b == "") return 1
233 if (substr(a, 1, 1) != "^") return 1
234 if (substr(b, 1, 1) != "^") return -1
235 a = substr(a, 2); b = substr(b, 2); continue
236 }
237 if (a == "" || b == "") break
238 if (substr(a, 1, 1) ~ /[0-9]/) {
239 match(a, /^[0-9]+/); sa = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1)
240 if (!match(b, /^[0-9]+/)) return 1
241 sb = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1)
242 r = numcmp(sa, sb)
243 } else {
244 match(a, /^[A-Za-z]+/); sa = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1)
245 if (!match(b, /^[A-Za-z]+/)) return -1
246 sb = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1)
247 r = (sa < sb) ? -1 : (sa > sb) ? 1 : 0
248 }
249 if (r) return r
250 }
251 if (a == "" && b == "") return 0
252 return (a == "") ? -1 : 1
253 }
254 function rpmevr(a, b, ea, eb, va, vb, ra, rb, r) {
255 epoch(a); ea = E; a = S
256 epoch(b); eb = E; b = S
257 r = numcmp(ea, eb); if (r) return r
258 splitvr(a); va = V; ra = R
259 splitvr(b); vb = V; rb = R
260 r = rpmvercmp(va, vb); if (r) return r
261 return rpmvercmp(ra, rb)
262 }
263 function dorder(c) {
264 if (c == "") return 0
265 if (c == "~") return -1
266 if (c ~ /[A-Za-z]/) return index(ASCII, c) + 31
267 return index(ASCII, c) + 31 + 256
268 }
269 function dvercmp(a, b, ca, cb, oa, ob, da, db, r) {
270 while (a != "" || b != "") {
271 while ((a != "" && substr(a, 1, 1) !~ /[0-9]/) || (b != "" && substr(b, 1, 1) !~ /[0-9]/)) {
272 ca = substr(a, 1, 1); if (ca ~ /[0-9]/) ca = ""
273 cb = substr(b, 1, 1); if (cb ~ /[0-9]/) cb = ""
274 oa = dorder(ca); ob = dorder(cb)
275 if (oa != ob) return (oa < ob) ? -1 : 1
276 a = substr(a, 2); b = substr(b, 2)
277 }
278 da = ""; db = ""
279 if (match(a, /^[0-9]+/)) { da = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1) }
280 if (match(b, /^[0-9]+/)) { db = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1) }
281 r = numcmp(da, db); if (r) return r
282 }
283 return 0
284 }
285 function debcmp(a, b, ea, eb, ua, ub, ra, rb, r) {
286 epoch(a); ea = E; a = S
287 epoch(b); eb = E; b = S
288 r = numcmp(ea, eb); if (r) return r
289 splitvr(a); ua = V; ra = R
290 splitvr(b); ub = V; rb = R
291 r = dvercmp(ua, ub); if (r) return r
292 return dvercmp(ra, rb)
293 }
294 function cmp(x, y) { return (mode == "deb") ? debcmp(x, y) : rpmevr(x, y) }
295 BEGIN {
296 for (i = 32; i < 127; i++) ASCII = ASCII sprintf("%c", i)
297 if (op != "pick") { print cmp(a, b); exit }
298 }
299 # op=pick: of the "key<TAB>..." lines, print the one with the largest key above ref (one pass, O(N))
300 {
301 k = $0; sub(/\t.*/, "", k)
302 if (cmp(k, ref) > 0 && (best == "" || cmp(k, best) > 0)) { best = k; bestline = $0 }
303 }
304 END { if (op == "pick" && bestline != "") print bestline }'
305
306 ver_cmp() { # $1=rpm|deb $2 $3 → -1 / 0 / 1
307 if [ "$1" = deb ] && have dpkg; then
308 if dpkg --compare-versions "$2" lt "$3"; then echo -1
309 elif dpkg --compare-versions "$2" gt "$3"; then echo 1
310 else echo 0; fi
311 return
312 fi
313 LC_ALL=C awk -v mode="$1" -v a="$2" -v b="$3" "$AWK_VERCMP"
314 }
315 ver_lt() { [ "$(ver_cmp "$1" "$2" "$3")" = "-1" ]; }
316
317 # "4.18.0-553.1.1" → BRANCH=4.18 (no subprocess)
318 branch_of() {
319 local v=${1%%[!0-9.]*} maj rest
320 BRANCH=""
321 maj=${v%%.*}; rest=${v#*.}
322 [ -n "$maj" ] && [ "$rest" != "$v" ] || return 0
323 rest=${rest%%.*}
324 [ -n "$rest" ] && BRANCH="$maj.$rest"
325 }
326
327 # "4.18.0-553.1.1.el8_10.x86_64+debug" → EVR=4.18.0-553.1.1 (no subprocess)
328 rpm_evr_of_krel() {
329 local k=${1%%+*}
330 case $k in
331 *.x86_64|*.aarch64|*.ppc64le|*.ppc64|*.s390x|*.i[3-6]86|*.armv7hl|*.armv7l|*.riscv64|*.noarch) k=${k%.*} ;;
332 esac
333 case ${k##*.} in el[0-9]*|amzn[0-9]*|fc[0-9]*) k=${k%.*} ;; esac
334 EVR=$k
335 }
336
337 # -----------------------------------------------------------------------------
338 # System information
339 # -----------------------------------------------------------------------------
340 # Read os-release in one pass (handles quotes and CRLF, no subprocess)
341 read_os_release() {
342 local key val cr
343 OS_ID=""; OS_LIKE=""; OS_VER=""; OS_NAME=""; OS_CODENAME=""
344 [ -r "$OS_RELEASE_FILE" ] || OS_RELEASE_FILE=/usr/lib/os-release
345 if [ ! -r "$OS_RELEASE_FILE" ]; then
346 # Old RHEL family (5/6) without os-release: "CentOS release 6.10 (Final)"
347 if [ "$OFFLINE" = 0 ] && [ -r /etc/redhat-release ]; then
348 read -r OS_NAME < /etc/redhat-release
349 OS_ID=rhel
350 case $OS_NAME in *"release "[0-9]*) val=${OS_NAME#*release }; OS_VER=${val%% *} ;; esac
351 fi
352 return 0
353 fi
354 cr=$(printf '\r')
355 local pretty="" name=""
356 while IFS='=' read -r key val || [ -n "$key" ]; do
357 val=${val%"$cr"}
358 case $val in
359 \"*\") val=${val#\"}; val=${val%\"} ;;
360 \'*\') val=${val#\'}; val=${val%\'} ;;
361 esac
362 case $key in
363 ID) OS_ID=$val ;;
364 ID_LIKE) OS_LIKE=$val ;;
365 VERSION_ID) OS_VER=$val ;;
366 PRETTY_NAME) pretty=$val ;;
367 NAME) name=$val ;;
368 VERSION_CODENAME) OS_CODENAME=$val ;;
369 esac
370 done < "$OS_RELEASE_FILE"
371 OS_NAME=${pretty:-$name}
372 }
373
374 detect_os() {
375 read_os_release
376 OS_MAJOR=${OS_VER%%.*}
377 case $OS_MAJOR in ''|*[!0-9]*) OS_MAJ_NUM=0 ;; *) OS_MAJ_NUM=$OS_MAJOR ;; esac
378
379 FAMILY=generic
380 case $KREL in
381 *-Microsoft) FAMILY=wsl1; return ;; # WSL1: a Windows compatibility layer, not a Linux kernel
382 *[Mm]icrosoft*) FAMILY=wsl; return ;;
383 *-pve) FAMILY=proxmox; return ;;
384 esac
385 case $OS_ID in
386 almalinux) case $OS_NAME in *Kitten*) FAMILY=kitten ;; *) FAMILY=alma ;; esac ;;
387 rhel) FAMILY=rhel ;;
388 centos) FAMILY=centos ;;
389 rocky) FAMILY=rocky ;;
390 ol) FAMILY=oracle ;;
391 amzn) FAMILY=amazon ;;
392 fedora) FAMILY=fedora ;;
393 debian) FAMILY=debian ;;
394 ubuntu) FAMILY=ubuntu ;;
395 sles|sled|sles_sap|sle_hpc) FAMILY=sles ;;
396 sle-micro|sl-micro) FAMILY=slemicro ;;
397 opensuse-leap) FAMILY=leap ;;
398 opensuse-tumbleweed|opensuse-slowroll) FAMILY=tumbleweed ;;
399 opensuse-microos|opensuse-aeon|opensuse-kalpa) FAMILY=microos ;;
400 arch|archarm|manjaro|manjaro-arm|endeavouros|cachyos|garuda|artix) FAMILY=arch ;;
401 alpine|postmarketos) FAMILY=alpine ;;
402 gentoo) FAMILY=gentoo ;;
403 *)
404 case " $OS_LIKE " in
405 *" ubuntu "*) FAMILY=ubuntu ;;
406 *" rhel "*|*" centos "*|*" fedora "*)
407 case $KREL in *.el[0-9]*) FAMILY=rhelclone ;; esac ;;
408 *" arch "*) FAMILY=arch ;;
409 *" debian "*) FAMILY=debianlike ;;
410 esac ;;
411 esac
412 }
413
414 family_label() {
415 case $FAMILY in
416 alma) echo "compared with fixed kernels in the AlmaLinux advisory (2026-10-16)" ;;
417 kitten) echo "AlmaLinux Kitten — fix due in the next regular kernel update" ;;
418 rhel) echo "Red Hat advisory (no fix yet) + kernel changelog check" ;;
419 centos|rocky|rhelclone) echo "RHEL-family advisory + kernel changelog check" ;;
420 oracle) echo "compared with fixed kernels in Oracle Linux ELSA advisories (UEK)" ;;
421 amazon) echo "compared with fixed kernels in Amazon Linux ALAS advisories" ;;
422 debian) echo "compared with fixed package versions in the Debian security tracker" ;;
423 debianlike) echo "Debian derivative — Debian rules for Debian kernels, upstream rules otherwise" ;;
424 ubuntu) echo "Ubuntu CVE tracker (no fix yet) + kernel changelog check" ;;
425 proxmox) echo "Proxmox VE kernel — kernel changelog check" ;;
426 sles|slemicro|leap) echo "SUSE security advisory (no fix yet) + kernel changelog check" ;;
427 fedora|arch|tumbleweed|microos|alpine|gentoo) echo "distribution tracks upstream stable — compared with upstream fixed versions" ;;
428 wsl) echo "WSL2 (Microsoft kernel) — compared with upstream fixed versions" ;;
429 wsl1) echo "WSL1 — no Linux kernel involved" ;;
430 *) echo "unknown distribution — compared with upstream fixed versions (indicative only)" ;;
431 esac
432 }
433
434 # Detect virtualization (VIRT) and containers (CTR) with a single systemd-detect-virt call
435 detect_virt_container() {
436 local v=""
437 VIRT=""; CTR=""
438 if [ -f /.dockerenv ]; then CTR=docker
439 elif [ -f /run/.containerenv ]; then CTR=podman
440 elif [ -s /run/systemd/container ]; then read -r CTR < /run/systemd/container
441 elif [ -n "${KUBERNETES_SERVICE_HOST:-}" ]; then CTR=kubernetes
442 elif [ -r /proc/1/environ ] && tr '\0' '\n' < /proc/1/environ 2>/dev/null | grep -q '^container='; then
443 CTR=container
444 fi
445 if have systemd-detect-virt; then
446 v=$(systemd-detect-virt 2>/dev/null) # inside a container the container type is printed first
447 case $v in
448 ''|none) ;;
449 docker|podman|lxc|lxc-libvirt|systemd-nspawn|openvz|rkt|proot|pouch|wsl) [ -n "$CTR" ] || CTR=$v ;;
450 *) VIRT=$v ;;
451 esac
452 else
453 if [ -z "$CTR" ] && grep -qE '/(docker|kubepods|lxc|libpod)' /proc/1/cgroup 2>/dev/null; then
454 CTR=container
455 fi
456 grep -qw hypervisor /proc/cpuinfo 2>/dev/null && VIRT=hypervisor
457 fi
458 [ "$CTR" = wsl ] && CTR=""
459 }
460
461 # Does the kernel package changelog record the fix? (RHEL, SUSE, Oracle, Debian, Ubuntu families)
462 # For rpm, a single 'rpm -qf --changelog' both finds the owning package and prints its changelog.
463 # CL_SRC records where the evidence came from (rpm|deb).
464 changelog_has_fix() {
465 local krel=$1 f
466 CL_SRC=""
467 if have rpm; then
468 for f in "/lib/modules/$krel/vmlinuz" "/boot/vmlinuz-$krel"; do
469 [ -e "$f" ] || continue
470 rpm -qf --changelog "$f" 2>/dev/null | has_fix_text && { CL_SRC=rpm; return 0; }
471 break
472 done
473 fi
474 have gzip || return 1
475 CL_SRC=deb
476 for f in /usr/share/doc/linux-image-"$krel"/changelog.Debian.gz \
477 /usr/share/doc/linux-modules-"$krel"/changelog.Debian.gz \
478 /usr/share/doc/linux-image-unsigned-"$krel"/changelog.Debian.gz \
479 /usr/share/doc/proxmox-kernel-"$krel"*/changelog.Debian.gz; do
480 [ -r "$f" ] || continue
481 gzip -dc "$f" 2>/dev/null | has_fix_text && return 0
482 done
483 return 1
484 }
485
486 # Only kernels that still have a bootable image (skips empty module dirs left by DKMS etc.).
487 # Alpine and Gentoo may not put the release in the image name, so modules.dep is used there.
488 list_installed_krels() {
489 local d k
490 for d in /lib/modules/*/; do
491 k=${d%/}; k=${k##*/}
492 [ "$k" = "*" ] && continue
493 if [ -e "/lib/modules/$k/vmlinuz" ] || [ -e "/boot/vmlinuz-$k" ] ||
494 { case $FAMILY in alpine|gentoo) true ;; *) false ;; esac && [ -e "/lib/modules/$k/modules.dep" ]; }; then
495 printf '%s\n' "$k"
496 fi
497 done
498 }
499
500 # Was the running kernel's image replaced under the same name after boot (same-ABI in-place
501 # update on Debian-like systems)? dpkg keeps the build time as mtime, so compare the install
502 # time (ctime) with the boot time (btime).
503 image_replaced_after_boot() {
504 local f c k v bt=""
505 for f in "/boot/vmlinuz-$1" "/lib/modules/$1/vmlinuz" ""; do [ -n "$f" ] && [ -e "$f" ] && break; done
506 [ -n "$f" ] || return 1
507 c=$(stat -c %Z "$f" 2>/dev/null) || return 1
508 while read -r k v; do [ "$k" = btime ] && { bt=$v; break; }; done < /proc/stat
509 [ -n "$bt" ] && [ "$c" -gt "$bt" ]
510 }
511
512 # If grubby can tell the default boot kernel, put it in DEFAULT_KREL (RHEL, Fedora, Amazon families)
513 default_boot_krel() {
514 local d
515 DEFAULT_KREL=""
516 have grubby || return 1
517 d=$($SUDO grubby --default-kernel 2>/dev/null) || return 1
518 case $d in /boot/vmlinuz-?*) DEFAULT_KREL=${d#/boot/vmlinuz-} ;; *) return 1 ;; esac
519 }
520
521 # Do the image and initramfs of the reboot target actually exist? (otherwise the boot may fail)
522 boot_files_ok() {
523 local k=$1 f img=0 ird=0
524 case $FAMILY in arch|alpine|gentoo|microos|slemicro) return 0 ;; esac # image names carry no release
525 for f in "/boot/vmlinuz-$k" "/lib/modules/$k/vmlinuz" "/boot/Image-$k"; do [ -s "$f" ] && img=1; done
526 for f in "/boot/initramfs-$k.img" "/boot/initrd.img-$k" "/boot/initrd-$k"; do [ -s "$f" ] && ird=1; done
527 [ "$img" = 1 ] && [ "$ird" = 1 ]
528 }
529
530 # The newest kernel installed after the running one (usually the next default boot kernel)
531 # Result: NEW_KREL, NEW_DVER (Debian package version). Candidates are collected with shell
532 # builtins and compared in a single awk run.
533 newest_installed() {
534 local mode=rpm ref k list="" st pkg ver pick tab
535 NEW_KREL=""; NEW_DVER=""
536 tab=$(printf '\t')
537 if [ "$FAMILY" = debian ]; then
538 have dpkg-query || return 1
539 mode=deb; ref=$RUN_DVER
540 while IFS='|' read -r st pkg ver; do
541 case $st in ii*) ;; *) continue ;; esac
542 k=${pkg#linux-image-}
543 [ -e "/lib/modules/$k/modules.dep" ] || continue
544 list="$list$ver$tab$k$tab$ver
545 "
546 done <<EOF
547 $(dpkg-query -W -f='${db:Status-Abbrev}|${Package}|${Version}\n' 'linux-image-*' 2>/dev/null)
548 EOF
549 else
550 rpm_evr_of_krel "$KREL"; ref=$EVR
551 for k in $(list_installed_krels); do
552 [ "$k" = "$KREL" ] && continue
553 [ "$FAMILY" = proxmox ] && case $k in *-pve) ;; *) continue ;; esac # Proxmox kernels only
554 rpm_evr_of_krel "$k"
555 list="$list$EVR$tab$k$tab
556 "
557 done
558 fi
559 [ -n "$list" ] || return 1
560 pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode="$mode" -v ref="$ref" "$AWK_VERCMP")
561 [ -n "$pick" ] || return 1
562 pick=${pick#*"$tab"}
563 NEW_KREL=${pick%%"$tab"*}
564 NEW_DVER=${pick#*"$tab"}
565 }
566
567 # Determine and judge the next boot kernel.
568 # The grubby default when known, otherwise the newest kernel installed after the running one.
569 # Result: NB_KREL / NB_DVER / NB_STATUS (NB_KREL is empty when the next boot is the running kernel)
570 # NB_ALT = a fixed kernel that is installed but is not the default boot kernel
571 next_boot_eval() {
572 NB_KREL=""; NB_DVER=""; NB_STATUS=""; NB_ALT=""
573 newest_installed || { NEW_KREL=""; NEW_DVER=""; }
574 if default_boot_krel; then
575 [ "$DEFAULT_KREL" != "$KREL" ] && NB_KREL=$DEFAULT_KREL
576 else
577 NB_KREL=$NEW_KREL; NB_DVER=$NEW_DVER
578 fi
579 if [ -n "$NB_KREL" ]; then
580 evaluate_kernel "$NB_KREL" "$NB_DVER"; NB_STATUS=$E_STATUS
581 fi
582 if [ "$NB_STATUS" != fixed ] && [ -n "$NEW_KREL" ] && [ "$NEW_KREL" != "$NB_KREL" ]; then
583 evaluate_kernel "$NEW_KREL" "$NEW_DVER"
584 [ "$E_STATUS" = fixed ] && NB_ALT=$NEW_KREL
585 fi
586 return 0
587 }
588
589 # Will a reboot come up on a fixed kernel? → sets REBOOT_TARGET / REBOOT_KREL
590 installed_fix_ready() {
591 REBOOT_TARGET=""; REBOOT_KREL=""
592 next_boot_eval
593 [ "$NB_STATUS" = fixed ] || return 1
594 REBOOT_KREL=$NB_KREL
595 REBOOT_TARGET="$NB_KREL${NB_DVER:+ (package $NB_DVER)}"
596 }
597
598 # Is any kernel livepatch active? (reads sysfs only)
599 livepatch_active() {
600 local f v
601 for f in /sys/kernel/livepatch/*/enabled; do
602 [ -r "$f" ] || continue
603 read -r v < "$f" 2>/dev/null && [ "$v" = 1 ] && return 0
604 done
605 return 1
606 }
607
608 # Has a livepatch applied the fix to the running kernel?
609 # Without an active livepatch the full rpm database query (rpm -qa) is skipped entirely.
610 livepatch_has_fix() {
611 local p rel
612 if livepatch_active; then
613 if have canonical-livepatch && canonical-livepatch status --verbose 2>/dev/null | has_fix_text; then
614 LP_NAME="Canonical Livepatch"; return 0
615 fi
616 if have rpm; then
617 rpm_evr_of_krel "$KREL"; rel=$EVR
618 case $rel in *-default|*-azure|*-rt|*-64kb) rel=${rel%-*} ;; esac
619 # Only the package built for the running kernel (RHEL/SUSE use dots→underscores, Amazon keeps dots)
620 for p in $(rpm -qa 'kpatch-patch*' 'kernel-livepatch-*' 2>/dev/null); do
621 case $p in *-"${rel//./_}"-*|*-"$rel"-*) ;; *) continue ;; esac
622 if rpm -q --changelog "$p" 2>/dev/null | has_fix_text; then LP_NAME=$p; return 0; fi
623 done
624 fi
625 fi
626 if have uptrack-show && uptrack-show 2>/dev/null | has_fix_text; then
627 LP_NAME="Oracle Ksplice"; return 0
628 fi
629 return 1
630 }
631
632 # -----------------------------------------------------------------------------
633 # Verdict — results go into E_STATUS / E_FIXED / E_BASIS / E_ADV / E_PKG / E_MODE
634 # E_STATUS: fixed | vulnerable | pending | eol | unaffected | unknown
635 # -----------------------------------------------------------------------------
636 upstream_eval() { # pure shell pattern parsing, no subprocess
637 local rel=$1 base maj min pat rest rc="" br v fpat
638 E_MODE=upstream
639 base=${rel%%[!0-9.]*}
640 maj=${base%%.*}; rest=${base#*.}
641 min=${rest%%.*}; pat=${rest#*.}; pat=${pat%%.*}
642 [ "$pat" = "$rest" ] && pat=0 # x.y form
643 case $maj:$min:$pat in
644 *[!0-9:]*|:*|*::*|*:) E_STATUS=unknown; E_BASIS="cannot parse kernel version ($rel)"; return ;;
645 esac
646 [ "$rest" = "$base" ] && { E_STATUS=unknown; E_BASIS="cannot parse kernel version ($rel)"; return; }
647 base="$maj.$min.$pat"
648 # rc number: rcN after a non-letter, e.g. "-rc6", ".rc6", Ubuntu mainline "070300rc5"
649 case $rel in
650 *[!A-Za-z]rc[0-9]*) rc=${rel#*[!A-Za-z]rc}; rc=${rc%%[!0-9]*} ;;
651 esac
652 br="$maj.$min"
653
654 if [ "$maj" -lt 2 ] || { [ "$maj" -eq 2 ] && [ "$min" -lt 6 ]; } || { [ "$br" = 2.6 ] && [ "$pat" -lt 27 ]; }; then
655 case $rel in
656 *.el[0-9]*|*.amzn*) # vendor kernels may carry backported networking code under old version numbers
657 E_STATUS=unknown; E_BASIS="$base vendor kernel — version predates 2.6.27 but backports must be checked" ;;
658 *)
659 E_STATUS=unaffected; E_BASIS="$base — older than 2.6.27, where the vulnerable code was introduced" ;;
660 esac
661 return
662 fi
663 if [ "$maj" -gt 7 ] || { [ "$maj" -eq 7 ] && [ "$min" -ge 3 ]; }; then
664 E_FIXED="7.3-rc7"
665 if [ "$br" = 7.3 ] && [ -n "$rc" ] && [ "$rc" -lt 7 ]; then
666 E_STATUS=vulnerable; E_BASIS="7.3-rc$rc < mainline fix point 7.3-rc7"
667 else
668 E_STATUS=fixed; E_BASIS="$base — mainline 7.3-rc7 or later (includes the fix)"
669 fi
670 return
671 fi
672 for v in $UPSTREAM_FIXED; do
673 [ "${v%.*}" = "$br" ] || continue
674 fpat=${v##*.}; E_FIXED=$v
675 if [ "$pat" -ge "$fpat" ]; then
676 E_STATUS=fixed; E_BASIS="$base ≥ upstream fixed version $v"
677 else
678 E_STATUS=vulnerable; E_BASIS="$base < upstream fixed version $v"
679 fi
680 return
681 done
682 E_STATUS=eol; E_BASIS="upstream $br branch is end of life — no fixed release for this branch"
683 }
684
685 nonvendor_eval() { # $1=kernel release $2=reason
686 upstream_eval "$1"
687 E_BASIS="$2 → upstream rules: $E_BASIS"
688 }
689
690 rpm_table_eval() { # $1=kernel release $2=table name
691 local evr br t_br t_cmp t_disp t_adv t_pkg
692 rpm_evr_of_krel "$1"; evr=$EVR
693 branch_of "$evr"; br=$BRANCH
694 while IFS='|' read -r t_br t_cmp t_disp t_adv t_pkg; do
695 [ -n "$t_br" ] && [ "$t_br" = "$br" ] || continue
696 E_FIXED=$t_disp; E_ADV=$t_adv; E_PKG=$t_pkg
697 if ver_lt rpm "$evr" "$t_cmp"; then
698 E_STATUS=vulnerable; E_BASIS="kernel $evr < fixed version $t_cmp"
699 else
700 E_STATUS=fixed; E_BASIS="kernel $evr ≥ fixed version $t_cmp"
701 fi
702 return 0
703 done <<EOF
704 $(vendor_table "$2")
705 EOF
706 return 1
707 }
708
709 deb_eval() { # $1=kernel release $2=Debian package version
710 local v=$2 up br t
711 if [ -z "$v" ]; then
712 if [ "$OFFLINE" = 1 ]; then
713 E_STATUS=unknown
714 E_BASIS="Debian kernels need the package version — pass the target server's 'uname -v' with --uname-v"
715 return
716 fi
717 nonvendor_eval "$1" "not a Debian-packaged kernel"; return
718 fi
719 case $v in *+rpt*|*rpi*) nonvendor_eval "$1" "Raspberry Pi kernel"; return ;; esac
720 up=${v#*:}; branch_of "$up"; br=$BRANCH
721 case $br in
722 6.1) t="6.1.190-1~"; E_FIXED="6.1.190-1"; E_ADV="bookworm-security, DLA-4841-1" ;; # ~ : also covers bullseye-LTS ~deb11u
723 6.12) t="6.12.113-1~"; E_FIXED="6.12.113-1"; E_ADV="trixie-security, DSA-6568-1" ;;
724 7.2) t="7.2.11-1~"; E_FIXED="7.2.11-1"; E_ADV="forky/sid" ;;
725 *)
726 upstream_eval "${up%%-*}"
727 if [ "$E_STATUS" = fixed ] || [ "$E_STATUS" = unaffected ]; then
728 E_BASIS="Debian kernel $v → upstream rules: $E_BASIS"
729 else
730 E_STATUS=eol
731 E_BASIS="kernel branch $br is not tracked by Debian security — end-of-life release (bullseye or older) or non-standard kernel"
732 fi
733 E_MODE=vendor
734 return ;;
735 esac
736 if ver_lt deb "$v" "$t"; then
737 E_STATUS=vulnerable; E_BASIS="Debian kernel $v < fixed version $E_FIXED"
738 else
739 E_STATUS=fixed; E_BASIS="Debian kernel $v ≥ fixed version $E_FIXED"
740 fi
741 }
742
743 # Verdict: decide from vendor tables / upstream versions first; only when that is inconclusive
744 # (no fix yet, end of life, unknown) is the kernel changelog (several MB) checked. Distributions
745 # decided by a table never read a changelog.
746 # E_INPLACE=1: a package with the running kernel's name was replaced by a fixed one after boot → reboot
747 evaluate_kernel() { # $1=kernel release $2=Debian package version (optional)
748 E_INPLACE=0
749 evaluate_by_version "$@"
750 case $E_STATUS in pending|unknown|eol) ;; *) return ;; esac
751 [ "$OFFLINE" = 0 ] || return
752 if changelog_has_fix "$1"; then
753 # A deb changelog belongs to the package on disk — if the running kernel was replaced
754 # after boot, the fix is not in the kernel that is in memory yet
755 if [ "$1" = "$KREL" ] && [ "$CL_SRC" = deb ] && image_replaced_after_boot "$1"; then
756 E_INPLACE=1
757 E_BASIS="$E_BASIS · the package that replaced this kernel after boot contains the fix (reboot needed)"
758 return
759 fi
760 E_STATUS=fixed; E_MODE=changelog
761 E_BASIS="the kernel package changelog records $CVE or the fix commit"
762 else
763 case $FAMILY in
764 rhel|centos|rocky|rhelclone|kitten|oracle|ubuntu|proxmox|sles|slemicro|leap|debian|debianlike)
765 E_BASIS="$E_BASIS · no fix recorded in the kernel changelog either" ;;
766 esac
767 fi
768 }
769
770 evaluate_by_version() {
771 local krel=$1 dver=${2:-}
772 E_STATUS=unknown; E_FIXED=""; E_BASIS=""; E_ADV=""; E_PKG=""; E_MODE=vendor
773
774 # On RPM distributions, a kernel that is not the distro kernel (ELRepo, self-built) uses upstream rules
775 case $FAMILY in
776 alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon)
777 case $krel in
778 *elrepo*) nonvendor_eval "$krel" "ELRepo kernel"; return ;;
779 *.el[0-9]*|*.amzn[0-9]*) ;;
780 *) nonvendor_eval "$krel" "not the distribution kernel"; return ;;
781 esac ;;
782 esac
783
784 case $FAMILY in
785 alma)
786 case $OS_MAJ_NUM in
787 8|9|10) rpm_table_eval "$krel" "alma$OS_MAJ_NUM" ||
788 nonvendor_eval "$krel" "not the default kernel branch of AlmaLinux $OS_MAJ_NUM" ;;
789 *) E_STATUS=eol; E_BASIS="end-of-life AlmaLinux release ($OS_VER)" ;;
790 esac ;;
791 kitten)
792 E_STATUS=pending
793 E_BASIS="AlmaLinux Kitten gets the fix in the next regular kernel update (no separate build)" ;;
794 rhel|centos|rocky|rhelclone)
795 if { [ "$FAMILY" = rhel ] && [ "$OS_MAJ_NUM" -lt 7 ]; } ||
796 { [ "$FAMILY" = centos ] && [ "$OS_MAJ_NUM" -le 8 ]; }; then
797 E_STATUS=eol; E_BASIS="end-of-life release (${OS_NAME:-$OS_ID $OS_VER}) — no fixed kernel will ship"
798 elif [ "$FAMILY" = rhel ]; then
799 E_STATUS=pending; E_BASIS="Red Hat: RHEL 7/8/9/10 affected, no fixed kernel released (as of $DATA_DATE)"
800 else
801 E_STATUS=pending; E_BASIS="no fixed RHEL kernel yet — none published for this distribution either (as of $DATA_DATE)"
802 fi ;;
803 oracle)
804 case $krel in
805 *uek*)
806 rpm_table_eval "$krel" uek || {
807 rpm_evr_of_krel "$krel"; branch_of "$EVR"
808 E_STATUS=pending
809 E_BASIS="no advisory for this UEK release (${BRANCH:-?}) — move to UEK R6 or later"
810 } ;;
811 *)
812 E_STATUS=pending
813 E_BASIS="no advisory for RHCK (Red Hat compatible kernel) (as of $DATA_DATE) — UEK is fixed" ;;
814 esac ;;
815 amazon)
816 case $OS_VER in
817 2|2023)
818 rpm_table_eval "$krel" "al$OS_VER" ||
819 { E_STATUS=unknown; E_BASIS="kernel branch not covered by ALAS advisories ($krel)"; } ;;
820 *)
821 E_STATUS=eol; E_BASIS="Amazon Linux 1 is end of life — no advisory" ;;
822 esac ;;
823 debian)
824 deb_eval "$krel" "$dver" ;;
825 debianlike)
826 if [ -n "$dver" ]; then deb_eval "$krel" "$dver"
827 else nonvendor_eval "$krel" "distribution's own kernel"; fi ;;
828 ubuntu)
829 E_STATUS=pending
830 E_BASIS="Ubuntu CVE tracker: every kernel package 'Needs evaluation' — no fix released (as of $DATA_DATE)" ;;
831 proxmox)
832 E_STATUS=unknown
833 E_BASIS="no published fix information for the Proxmox VE kernel (based on the Ubuntu kernel)" ;;
834 sles|slemicro)
835 case $FAMILY:$OS_VER in
836 sles:11*|sles:12|sles:12.[0-4]|sles:15|sles:15.[1-3]|slemicro:5.[0-2])
837 E_STATUS=eol; E_BASIS="end-of-life SUSE release ($OS_VER) — not proactively patched" ;;
838 *)
839 E_STATUS=pending; E_BASIS="SUSE advisory: kernel-default etc. 'Affected', state 'Pending' — no fix released (as of $DATA_DATE)" ;;
840 esac ;;
841 leap)
842 case $OS_MAJ_NUM in
843 15|42) E_STATUS=eol; E_BASIS="openSUSE Leap $OS_VER is end of life" ;;
844 *) E_STATUS=pending; E_BASIS="SUSE advisory: Leap 16 'Affected', state 'Pending' — no fix released (as of $DATA_DATE)" ;;
845 esac ;;
846 fedora|arch|tumbleweed|microos|alpine|gentoo|wsl)
847 upstream_eval "$krel" ;;
848 wsl1)
849 E_STATUS=unaffected; E_BASIS="WSL1 has no Linux kernel; Windows handles the system calls" ;;
850 *)
851 upstream_eval "$krel"
852 [ "$E_STATUS" = eol ] && E_STATUS=unknown
853 E_BASIS="estimated from upstream versions (check distribution backports): $E_BASIS" ;;
854 esac
855 }
856
857 status_label() {
858 case $1 in
859 fixed) printf '%sSAFE — running a fixed kernel%s' "$C_GRN" "$C_OFF" ;;
860 unaffected) printf '%sNOT AFFECTED — this kernel does not contain the vulnerable code%s' "$C_GRN" "$C_OFF" ;;
861 livepatched) printf '%sSAFE (temporary) — fix applied by livepatch%s' "$C_GRN" "$C_OFF" ;;
862 reboot) printf '%sREBOOT NEEDED — fixed kernel installed, still running the old one%s' "$C_YEL" "$C_OFF" ;;
863 vulnerable) printf '%sVULNERABLE — a fixed kernel is available, update needed%s' "$C_RED" "$C_OFF" ;;
864 pending) printf '%sVULNERABLE — no fixed kernel from the distribution yet (as of %s)%s' "$C_RED" "$DATA_DATE" "$C_OFF" ;;
865 eol) printf '%sVULNERABLE — end-of-life release/branch, no fixed kernel%s' "$C_RED" "$C_OFF" ;;
866 *) printf '%sUNDETERMINED — check manually%s' "$C_YEL" "$C_OFF" ;;
867 esac
868 }
869
870 # -----------------------------------------------------------------------------
871 # Exposure — the main target is a virtualization host running untrusted guests
872 # -----------------------------------------------------------------------------
873 read_sysctl() { # $1=file → SYSCTL_VAL (empty if missing, no subprocess)
874 SYSCTL_VAL=""
875 [ -r "$1" ] && read -r SYSCTL_VAL < "$1" 2>/dev/null
876 return 0
877 }
878
879 check_exposure() {
880 local any=0 n procs v mods="" m _
881 HV_HOST=0
882 if [ -e /dev/kvm ]; then
883 line "- /dev/kvm present: this system can run KVM virtual machines"; any=1
884 fi
885 # Read /proc/modules only once
886 if [ -r /proc/modules ]; then
887 while read -r m _; do mods="$mods $m"; done < /proc/modules
888 fi
889 case "$mods " in *" vhost_net "*)
890 line "- vhost_net module loaded: virtio-net backend in use"; any=1; HV_HOST=1 ;;
891 esac
892 case "$mods " in *" tun "*|*" tap "*|*" macvtap "*)
893 line "- tun/tap module loaded"; any=1 ;;
894 esac
895 set -- /sys/class/net/*/tun_flags
896 if [ -e "$1" ]; then n=$#; else n=0; fi
897 if [ "$n" -gt 0 ]; then
898 line "- $n TUN/TAP interface(s) present"; any=1
899 fi
900 # A single grep reads /proc/*/comm directly; awk removes duplicates and joins
901 # kvm = Proxmox VM process, vhost-<pid> = virtio-net vhost worker thread
902 procs=$(grep -hE '^(qemu-system|qemu-kvm|kvm$|vhost-[0-9]|firecracker|cloud-hyperviso|crosvm|libvirtd|virtqemud|VBoxHeadless|VirtualBoxVM|vmware-vmx)' \
903 /proc/[0-9]*/comm 2>/dev/null | awk '{ sub(/^vhost-[0-9]+$/, "vhost") } !s[$0]++ { printf "%s ", $0 }')
904 if [ -n "$procs" ]; then
905 line "- virtualization processes running: $procs"; any=1; HV_HOST=1
906 fi
907 if [ "$FAMILY" = proxmox ] && [ "$HV_HOST" = 0 ]; then
908 line "- Proxmox VE node"; any=1; HV_HOST=1
909 fi
910 [ "$any" = 1 ] || line "- no sign of a virtualization host (no KVM, TUN/TAP or VM processes)"
911 if [ "$HV_HOST" = 1 ]; then
912 line "${C_RED}→ Looks like a virtualization host. It is the direct target of guest→host escape: patch it first.${C_OFF}"
913 fi
914 [ -n "$VIRT" ] && line "- This system is a VM guest ($VIRT). Patching the host kernel is up to the host operator/provider."
915
916 if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then
917 read_sysctl /proc/sys/kernel/unprivileged_userns_clone; v=$SYSCTL_VAL
918 if [ "$v" = 0 ]; then line "- unprivileged user namespaces: blocked (kernel.unprivileged_userns_clone=0)"
919 else line "- unprivileged user namespaces: allowed (kernel.unprivileged_userns_clone=$v)"; fi
920 elif [ -r /proc/sys/user/max_user_namespaces ]; then
921 read_sysctl /proc/sys/user/max_user_namespaces; v=$SYSCTL_VAL
922 if [ "$v" = 0 ]; then line "- user namespaces: blocked (user.max_user_namespaces=0)"
923 else line "- user namespaces: allowed (user.max_user_namespaces=$v)"; fi
924 fi
925 read_sysctl /proc/sys/kernel/apparmor_restrict_unprivileged_userns
926 if [ "$SYSCTL_VAL" = 1 ]; then
927 line " (AppArmor restricts unprivileged namespace privileges)"
928 fi
929 note "The namespace lines are informational. An unprivileged user who gains CAP_NET_ADMIN/CAP_NET_RAW inside a namespace may reach the TUN/TAP and packet-socket paths, which could matter for local attacks (an assumption), but no vendor has published this as a mitigation."
930 }
931
932 # -----------------------------------------------------------------------------
933 # Remediation
934 # -----------------------------------------------------------------------------
935 remedy_eol() {
936 line "No fixed kernel will be released for this release/kernel branch. $1"
937 }
938
939 remedy_custom() {
940 line "Judged as a kernel that is not the distribution kernel (ELRepo, self-built, ...)."
941 line "Apply the kernel provider's update or move to an upstream stable fixed version or later:"
942 line " $UPSTREAM_TEXT"
943 line " Fix commit: $FIX_COMMIT ($FIX_URL)"
944 cmt "The kernel CNA recommends the latest stable kernel over cherry-picking single commits"
945 cmt "ELRepo example: sudo dnf --enablerepo=elrepo-kernel update 'kernel-ml*' 'kernel-lt*'"
946 cmt "Or boot the distribution kernel again and apply the distribution patch"
947 }
948
949 remedy_rhel_like() { # $1=status
950 local pm=dnf core=kernel-core
951 case $OS_MAJ_NUM in [1-7]) pm=yum; core=kernel ;; esac
952 if [ "$1" = eol ]; then
953 remedy_eol "Move to a supported release (RHEL, Rocky, AlmaLinux 8/9/10, CentOS Stream 9/10)."
954 return
955 fi
956 line "No fixed kernel has been released as of $DATA_DATE. (Red Hat: no mitigation available)"
957 cmt "1) Check whether a fix has been released"
958 cmd "sudo $pm updateinfo list --cve $CVE"
959 cmd "rpm -q --changelog $core | grep -iE '$CVE|checksum_start'"
960 cmt "2) Once released, apply it and reboot"
961 cmd "sudo $pm update --cve $CVE # or: sudo $pm update 'kernel*'"
962 cmd "sudo reboot"
963 if [ "$FAMILY" = rhel ]; then
964 cmt "If Red Hat ships a kpatch livepatch, it can be applied without a reboot"
965 cmd "sudo $pm install \"kpatch-patch = \$(uname -r)\""
966 fi
967 }
968
969 remedy_family() { # $1=status
970 local st=$1 pm
971 case $FAMILY in
972 alma)
973 if [ "$st" = eol ]; then remedy_eol "Upgrade to a supported AlmaLinux (8.10 / 9 / 10)."; return; fi
974 line "Fixed kernel: ${R_FIXED:-see the AlmaLinux advisory} or later"
975 cmt "1) First try the regular repositories"
976 cmd "sudo dnf clean metadata && sudo dnf update 'kernel*'"
977 cmt "2) If it is not there yet, install from the testing repo (AlmaLinux advisory of 2026-10-16)"
978 cmd "sudo dnf install -y almalinux-release-testing"
979 cmd "sudo dnf update 'kernel*' --enablerepo=almalinux-testing"
980 cmd "sudo dnf remove -y almalinux-release-testing # clean up the testing repo"
981 cmt "3) Reboot and confirm the fixed version with 'uname -r'"
982 cmd "sudo reboot"
983 ;;
984 kitten)
985 line "AlmaLinux Kitten will include the fix in its next regular kernel update."
986 cmd "sudo dnf update 'kernel*'"
987 cmd "sudo reboot"
988 ;;
989 rhel|centos|rocky|rhelclone)
990 remedy_rhel_like "$st"
991 ;;
992 oracle)
993 pm=dnf; case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
994 case $KREL in
995 *uek*)
996 if [ "$st" = vulnerable ]; then
997 line "Fixed kernel: $R_FIXED or later ($R_ADV)"
998 cmd "sudo $pm update kernel-uek"
999 cmd "sudo reboot"
1000 cmt "With a Ksplice subscription, apply without a reboot: sudo uptrack-upgrade -y"
1001 else
1002 line "There is no advisory for the UEK release in use. Switch to a fixed UEK."
1003 line " UEK R6 5.4.17-2136.360.3.1 (OL7/8) · UEK R7 5.15.0-325.221.6.4 (OL8/9) · UEK R8 6.12.0-207.111.5.4 (OL9/10)"
1004 cmd "sudo $pm install kernel-uek"
1005 cmd "sudo reboot"
1006 fi ;;
1007 *)
1008 line "There is no fix for RHCK (Red Hat compatible kernel) yet. UEK is already fixed, so switching to UEK is the fastest route."
1009 cmd "sudo $pm install kernel-uek"
1010 cmd "sudo grubby --info=ALL | grep -E '^kernel' # find the installed UEK path"
1011 cmd "sudo grubby --set-default /boot/vmlinuz-<UEK version>"
1012 cmd "sudo reboot"
1013 cmt "If you must stay on RHCK, check for a release: sudo $pm updateinfo list --cve $CVE"
1014 ;;
1015 esac ;;
1016 amazon)
1017 case $OS_VER in
1018 2)
1019 if [ -n "$R_ADV" ]; then
1020 line "Fixed kernel: $R_FIXED or later ($R_ADV)"
1021 cmd "sudo yum update --advisory $R_ADV # or: sudo yum update kernel"
1022 else
1023 cmd "sudo yum update kernel"
1024 fi
1025 cmd "sudo reboot" ;;
1026 2023)
1027 if [ -n "$R_ADV" ]; then
1028 line "Fixed kernel: $R_FIXED or later ($R_ADV)"
1029 cmd "sudo dnf update $R_PKG --releasever 2023.12.20261012"
1030 cmt "or: sudo dnf update --advisory $R_ADV --releasever 2023.12.20261012"
1031 else
1032 cmd "sudo dnf update 'kernel*' --releasever latest"
1033 fi
1034 cmd "sudo reboot" ;;
1035 *)
1036 remedy_eol "Move to Amazon Linux 2023." ;;
1037 esac
1038 note "AWS states that the EC2, Lambda and Fargate host infrastructure is not affected. The steps above are for this instance's own kernel."
1039 ;;
1040 debian)
1041 if [ "$st" = eol ]; then
1042 remedy_eol "Upgrade to bookworm (12) or later, or look at extended support such as Freexian ELTS."
1043 return
1044 fi
1045 line "Fixed kernel: $R_FIXED or later ($R_ADV)"
1046 cmt "1) Make sure the security repository (<codename>-security) is enabled"
1047 cmd "grep -rhE '^[^#].*security' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null"
1048 cmt "2) Update and reboot"
1049 cmd "sudo apt update"
1050 cmd "sudo apt full-upgrade"
1051 cmt "If the kernel metapackage is missing: sudo apt install linux-image-\$(dpkg --print-architecture)"
1052 case $RUN_DVER in *bpo*)
1053 cmt "Using a backports kernel: sudo apt install -t ${OS_CODENAME:-<codename>}-backports linux-image-\$(dpkg --print-architecture)" ;;
1054 esac
1055 cmd "sudo reboot"
1056 cmt "3) After the reboot, check that 'Debian <version>' in 'uname -v' is the fixed version or later"
1057 ;;
1058 debianlike)
1059 line "Update the distribution kernel to the latest version. (fixed in upstream ${R_FIXED:-stable} or later)"
1060 cmd "sudo apt update && sudo apt full-upgrade"
1061 cmd "sudo reboot"
1062 ;;
1063 ubuntu)
1064 line "No fixed Ubuntu kernel has been released as of $DATA_DATE. (every kernel 'Needs evaluation')"
1065 cmt "1) Check whether a fix has been released (Ubuntu Pro client)"
1066 cmd "pro fix $CVE --dry-run"
1067 cmt "or https://ubuntu.com/security/$CVE"
1068 cmt "2) If you use Livepatch, check whether it has been applied"
1069 cmd "sudo canonical-livepatch status --verbose | grep -A2 $CVE"
1070 cmt "3) Once released, apply it and reboot"
1071 cmd "sudo apt update && sudo apt full-upgrade"
1072 cmd "sudo reboot"
1073 ;;
1074 proxmox)
1075 line "Whether the Proxmox VE kernel (based on the Ubuntu kernel) is fixed must be checked in Proxmox announcements."
1076 cmd "pveversion -v | grep -E 'proxmox-kernel|pve-manager'"
1077 cmd "apt changelog proxmox-kernel-\$(uname -r)-signed 2>/dev/null | grep -iE '$CVE|checksum_start'"
1078 cmt "When a fixed kernel is out (pve-enterprise or pve-no-subscription repository)"
1079 cmd "sudo apt update && sudo apt full-upgrade"
1080 cmt "Migrate or shut down the guests, then reboot"
1081 cmd "sudo reboot"
1082 ;;
1083 sles|slemicro|leap)
1084 if [ "$st" = eol ]; then
1085 if [ "$FAMILY" = leap ]; then remedy_eol "Upgrade to openSUSE Leap 16."
1086 else remedy_eol "Upgrade to a supported service pack (including LTSS)."; fi
1087 return
1088 fi
1089 line "No fixed SUSE kernel has been released as of $DATA_DATE. (state 'Pending')"
1090 cmt "1) Check whether a fix has been released"
1091 cmd "sudo zypper refresh && sudo zypper list-patches --cve=$CVE"
1092 cmt "2) Once released, apply it and reboot"
1093 if [ "$FAMILY" = slemicro ]; then cmd "sudo transactional-update patch"
1094 else cmd "sudo zypper patch --cve=$CVE"; fi
1095 cmd "sudo reboot"
1096 cmt "With SLE Live Patching, a kernel-livepatch patch can be applied without a reboot"
1097 ;;
1098 fedora)
1099 if [ "$st" = eol ]; then
1100 line "This kernel branch is no longer supported upstream. Update the kernel, and if the Fedora release is end of life, move to a supported release."
1101 cmd "sudo dnf upgrade --refresh"
1102 cmd "sudo dnf system-upgrade download --releasever=<new release>"
1103 return
1104 fi
1105 line "Fixed in: upstream $R_FIXED or later"
1106 cmd "sudo dnf upgrade --refresh 'kernel*'"
1107 cmt "If it is not in the stable repository yet, install from updates-testing"
1108 cmd "sudo dnf upgrade --refresh --enablerepo=updates-testing 'kernel*'"
1109 cmd "sudo reboot"
1110 ;;
1111 arch)
1112 line "Fixed in: linux ${R_FIXED:-7.2.11} or later / linux-lts 6.12.113 · 6.18.57 or later"
1113 cmd "sudo pacman -Syu"
1114 cmd "sudo reboot"
1115 ;;
1116 tumbleweed)
1117 line "Fixed in: upstream ${R_FIXED:-stable} or later"
1118 cmd "sudo zypper refresh && sudo zypper dup"
1119 cmd "sudo reboot"
1120 ;;
1121 microos)
1122 cmd "sudo transactional-update dup"
1123 cmd "sudo reboot"
1124 ;;
1125 alpine)
1126 line "Fixed in: upstream ${R_FIXED:-stable} or later (linux-lts / linux-virt)"
1127 cmd "sudo apk update && sudo apk upgrade"
1128 cmd "sudo reboot"
1129 [ "$st" = eol ] && cmt "If the kernel branch is end of life, the Alpine release itself must be upgraded"
1130 ;;
1131 gentoo)
1132 line "Fixed in: upstream ${R_FIXED:-stable} or later"
1133 cmd "sudo emerge --sync"
1134 cmd "sudo emerge --ask --update --oneshot sys-kernel/gentoo-kernel-bin # or gentoo-kernel"
1135 cmt "If you build from gentoo-sources, rebuild from a fixed version or later"
1136 cmd "sudo reboot"
1137 ;;
1138 wsl)
1139 line "The WSL2 kernel is shipped by Microsoft. Run this in Windows PowerShell:"
1140 cmd "wsl --update"
1141 cmd "wsl --shutdown"
1142 note "The WSL2 host is Windows (Hyper-V), so the 'VM → Linux host escape' scenario does not apply. Only the WSL kernel itself was judged, using upstream versions."
1143 ;;
1144 *)
1145 line "Check your distribution vendor's security advisory and update the kernel."
1146 line "For a self-built kernel, move to an upstream stable fixed version or later:"
1147 line " $UPSTREAM_TEXT"
1148 line " Fix commit: $FIX_COMMIT ($FIX_URL)"
1149 ;;
1150 esac
1151 }
1152
1153 print_remedy() { # $1=final status
1154 local st=$1
1155 case $st in
1156 fixed|unaffected)
1157 line "${C_GRN}No further action needed.${C_OFF}"
1158 [ -n "$VIRT" ] && note "This system is a VM guest ($VIRT). Check separately whether the host (cloud/hosting provider) kernel is patched."
1159 return ;;
1160 livepatched)
1161 line "A livepatch ($LP_NAME) has applied the fix to the running kernel."
1162 line "At the next maintenance window, install the fixed kernel package and reboot."
1163 return ;;
1164 reboot)
1165 line "A fixed kernel is already installed: $REBOOT_TARGET"
1166 line "It takes effect after a reboot."
1167 [ "$HV_HOST" = 1 ] && cmt "This is a virtualization host. Migrate or shut down the guest VMs before rebooting."
1168 cmd "sudo reboot"
1169 cmt "Run this script again after the reboot to confirm"
1170 return ;;
1171 esac
1172
1173 if [ "$R_MODE" = upstream ]; then
1174 case $FAMILY in
1175 fedora|arch|tumbleweed|microos|alpine|gentoo|wsl|generic|debianlike) remedy_family "$st" ;;
1176 debian) FAMILY=debianlike remedy_family "$st" ;;
1177 *) remedy_custom ;;
1178 esac
1179 else
1180 remedy_family "$st"
1181 fi
1182
1183 printf '\n'
1184 note "There is no official workaround (sysctl, module blacklisting, ...). Booting a fixed kernel is the only fix."
1185 if [ "$HV_HOST" = 1 ]; then
1186 note "Patching only the guest OS kernels does not protect the host. Migrate or shut down the guests before rebooting."
1187 fi
1188 case $st in pending|eol|unknown)
1189 note "Until patched, limit the creation of untrusted workloads (VMs, microVMs, sandboxes) and keep them away from sensitive systems. (This lowers risk; it is not a fix)" ;;
1190 esac
1191 [ -n "$VIRT" ] && note "This system is a VM guest ($VIRT). Also check whether the host (cloud/hosting provider) is patched."
1192 }
1193
1194 print_refs() {
1195 line "https://www.cve.org/CVERecord?id=$CVE"
1196 line "https://nvd.nist.gov/vuln/detail/$CVE"
1197 line "https://pwn.ai/blog/pwnescape"
1198 line "https://almalinux.org/blog/2026-10-16-pwnescape-cve-2026-99999/"
1199 line "https://www.penligent.ai/hackinglabs/cve-2026-99999/"
1200 line "https://news.hada.io/topic?id=35568"
1201 case $FAMILY in
1202 debian|debianlike) line "https://security-tracker.debian.org/tracker/$CVE" ;;
1203 ubuntu|proxmox) line "https://ubuntu.com/security/$CVE" ;;
1204 alma|kitten|rhel|centos|rocky|rhelclone)
1205 line "https://access.redhat.com/security/cve/cve-2026-99999" ;;
1206 oracle) line "https://linux.oracle.com/cve/$CVE.html" ;;
1207 amazon) line "https://explore.alas.aws.amazon.com/$CVE.html" ;;
1208 sles|slemicro|leap|tumbleweed|microos)
1209 line "https://www.suse.com/security/cve/$CVE.html" ;;
1210 *) line "$FIX_URL" ;;
1211 esac
1212 }
1213
1214 print_refs_section() {
1215 section "References"
1216 print_refs
1217 note "Verdict data as of $DATA_DATE. Vendor advisories keep changing, so check the latest ones too."
1218 }
1219
1220 show_manual() {
1221 section "Remediation (manual)"
1222 print_remedy "$FINAL"
1223 print_refs_section
1224 }
1225
1226 # -----------------------------------------------------------------------------
1227 # Automatic patching (menu option 1)
1228 # -----------------------------------------------------------------------------
1229 run() {
1230 printf ' %s$ %s%s\n' "$C_DIM" "$*" "$C_OFF"
1231 "$@"
1232 }
1233
1234 setup_sudo() {
1235 SUDO=""
1236 [ "$(id -u)" = 0 ] && return 0
1237 if command -v sudo >/dev/null 2>&1; then SUDO=sudo; return 0; fi
1238 line "${C_RED}Root privileges are required but sudo is not available. Run this again as root.${C_OFF}"
1239 return 1
1240 }
1241
1242 can_autopatch() {
1243 case $FAMILY in wsl|wsl1) return 1 ;; esac # rebooting inside WSL never boots a distribution kernel
1244 [ "$FINAL" = reboot ] && return 0
1245 [ "$FINAL" = eol ] && return 1
1246 case $FAMILY in
1247 debian|debianlike|fedora|arch|tumbleweed|microos|alpine) return 0 ;;
1248 alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon|ubuntu|proxmox|sles|slemicro|leap)
1249 [ "$R_MODE" != upstream ] ;; # ELRepo / self-built kernels are not patched automatically
1250 *) return 1 ;;
1251 esac
1252 }
1253
1254 # zypper exit codes 100-103 are informational (updates available, reboot needed, ...), treat as success
1255 zypper_ok() { [ "$1" -eq 0 ] || { [ "$1" -ge 100 ] && [ "$1" -le 103 ]; }; }
1256 zypper_run() { # kernel packages only: proceed without asking
1257 local rc
1258 run $SUDO zypper --non-interactive "$@"; rc=$?
1259 zypper_ok "$rc"
1260 }
1261 zypper_ask() { # full system upgrade (dup): may remove packages, so the user confirms
1262 local rc
1263 run $SUDO zypper "$@"; rc=$?
1264 zypper_ok "$rc"
1265 }
1266
1267 # Upgrade only the installed linux-* packages (metapackages pull in the new ABI kernel).
1268 # - --no-remove: stop if other packages would have to be removed
1269 # - 'apt-get install' marks the named packages as manually installed, so restore the ones that
1270 # were automatic, letting 'apt autoremove' keep cleaning up old kernels
1271 # - Proxmox: dist-upgrade as Proxmox recommends, without -y so the user reviews any removals
1272 patch_apt() {
1273 local pkgs="" autos="" st pkg a meta=0
1274 local opts="-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold"
1275 run $SUDO apt-get update || return 1
1276 if [ "$FAMILY" = proxmox ]; then
1277 run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get $opts dist-upgrade
1278 return
1279 fi
1280 while read -r st pkg; do
1281 case $st in ii*) ;; *) continue ;; esac
1282 pkgs="$pkgs $pkg"
1283 case $pkg in linux-image-[a-z]*) meta=1 ;; esac
1284 done <<EOF
1285 $(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'linux-*' 2>/dev/null)
1286 EOF
1287 if [ -z "$pkgs" ]; then
1288 line "No installed linux-* kernel packages found."; return 1
1289 fi
1290 while read -r a; do
1291 case "$pkgs " in *" $a "*) autos="$autos $a" ;; esac
1292 done <<EOF
1293 $(apt-mark showauto 2>/dev/null)
1294 EOF
1295 run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install --only-upgrade $pkgs || return 1
1296 [ -n "$autos" ] && run $SUDO apt-mark auto $autos >/dev/null
1297 # On Debian without a kernel metapackage the new ABI kernel never arrives, so install it
1298 if [ "$FAMILY" = debian ] && [ "$meta" = 0 ]; then
1299 run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install "linux-image-$(dpkg --print-architecture)"
1300 fi
1301 }
1302
1303 # The AlmaLinux advisory method. The testing repo is enabled only for this run, and the repo
1304 # package is removed afterwards so regular updates never mix in testing packages.
1305 patch_alma() {
1306 local rc
1307 run $SUDO dnf clean metadata && run $SUDO dnf -y update 'kernel*' || return 1
1308 installed_fix_ready && return 0
1309 line "The fixed kernel is not in the regular repositories yet; installing from almalinux-testing. (AlmaLinux advisory method)"
1310 run $SUDO dnf -y install almalinux-release-testing || return 1
1311 run $SUDO dnf -y update 'kernel*' --enablerepo=almalinux-testing; rc=$?
1312 run $SUDO dnf -y remove almalinux-release-testing ||
1313 note "Could not remove almalinux-release-testing. Check /etc/yum.repos.d/ to make sure the testing repo is not enabled."
1314 return $rc
1315 }
1316
1317 # Full system upgrades (pacman -Syu, zypper dup, apk upgrade, transactional-update dup) may remove
1318 # or replace packages, so their confirmation prompt is kept. Kernel-only upgrades use -y.
1319 patch_now() {
1320 local pm=dnf
1321 case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
1322 case $FAMILY in
1323 alma) patch_alma ;;
1324 kitten|rhel|centos|rocky|rhelclone|oracle)
1325 run $SUDO $pm -y update 'kernel*' ;;
1326 amazon)
1327 if [ "$OS_VER" = 2 ]; then run $SUDO yum -y update kernel
1328 else run $SUDO dnf -y update "${R_PKG:-kernel}" --releasever=latest; fi ;;
1329 fedora) run $SUDO dnf -y upgrade --refresh 'kernel*' ;;
1330 debian|debianlike|ubuntu|proxmox) patch_apt ;;
1331 sles|leap) zypper_run refresh && zypper_run update "kernel-${KREL##*-}" ;;
1332 slemicro) run $SUDO transactional-update --non-interactive patch ;;
1333 microos) run $SUDO transactional-update --interactive dup ;;
1334 tumbleweed) zypper_run refresh && zypper_ask dup ;;
1335 arch) run $SUDO pacman -Syu ;;
1336 alpine) run $SUDO apk -U --interactive upgrade ;;
1337 *) return 1 ;;
1338 esac
1339 }
1340
1341 # With too little free space in /boot, creating the new kernel/initramfs can half-fail while the
1342 # default boot entry still changes
1343 boot_space_ok() {
1344 local avail
1345 have df || return 0
1346 avail=$(df -Pk /boot 2>/dev/null | awk 'NR == 2 { print $4 }')
1347 case $avail in ''|*[!0-9]*) return 0 ;; esac
1348 [ "$avail" -ge 102400 ] && return 0
1349 line "${C_RED}Only $((avail / 1024))MB free in /boot; installing a new kernel may fail, so patching stops here.${C_OFF}"
1350 cmt "Remove old kernels, e.g.: sudo dnf remove --oldinstallonly kernel / sudo apt autoremove --purge"
1351 return 1
1352 }
1353
1354 # After transactional-update: does the next boot snapshot (btrfs default subvolume) contain a fixed kernel?
1355 snapshot_fix_ready() {
1356 local out n root k list="" tab pick
1357 REBOOT_TARGET=""; REBOOT_KREL=""
1358 have btrfs || return 1
1359 out=$($SUDO btrfs subvolume get-default / 2>/dev/null) || return 1
1360 case $out in */.snapshots/*/snapshot*) ;; *) return 1 ;; esac
1361 n=${out##*/.snapshots/}; n=${n%%/*}
1362 case $n in ''|*[!0-9]*) return 1 ;; esac
1363 root=/.snapshots/$n/snapshot
1364 if [ "$FAMILY" = slemicro ]; then
1365 $SUDO rpm --root "$root" -q --changelog "kernel-${KREL##*-}" 2>/dev/null | has_fix_text || return 1
1366 else
1367 tab=$(printf '\t')
1368 for k in $($SUDO ls "$root/usr/lib/modules" 2>/dev/null); do list="$list$k$tab$k
1369 "; done
1370 pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode=rpm -v ref="" "$AWK_VERCMP")
1371 k=${pick%%"$tab"*}
1372 [ -n "$k" ] || return 1
1373 upstream_eval "$k"; [ "$E_STATUS" = fixed ] || return 1
1374 fi
1375 REBOOT_TARGET="snapshot $n"
1376 }
1377
1378 # After patching, will a reboot come up on a fixed kernel? (in-place replacement first, then next boot kernel)
1379 reboot_fix_ready() {
1380 case $FAMILY in slemicro|microos) snapshot_fix_ready; return ;; esac
1381 evaluate_kernel "$KREL" "$RUN_DVER"
1382 if [ "$E_INPLACE" = 1 ]; then
1383 REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (package replaced under the same name after boot)"
1384 return 0
1385 fi
1386 installed_fix_ready
1387 }
1388
1389 show_default_kernel() {
1390 local d
1391 have grubby || return 0
1392 d=$($SUDO grubby --default-kernel 2>/dev/null) || return 0
1393 [ -n "$d" ] && kv "Default boot kernel" "$d"
1394 }
1395
1396 # Without grubby, when GRUB_DEFAULT is not 0 (first entry = newest kernel) the next boot kernel is uncertain
1397 grub_default_custom() {
1398 local l v=""
1399 have grubby && return 1
1400 [ -r /etc/default/grub ] || return 1
1401 while IFS= read -r l; do case $l in GRUB_DEFAULT=*) v=${l#GRUB_DEFAULT=} ;; esac; done < /etc/default/grub
1402 v=${v#\"}; v=${v%\"}; v=${v#\'}; v=${v%\'}
1403 case $v in ''|0) return 1 ;; esac
1404 GRUB_DEFAULT_VAL=$v
1405 }
1406
1407 do_reboot() {
1408 local ans grub_custom=0
1409 grub_default_custom && grub_custom=1
1410 # On a virtualization host, with a GRUB setup that makes the next boot kernel uncertain, or in a
1411 # shell without timed input (read -t), ask for a typed 'yes' instead of a countdown
1412 if [ "$HV_HOST" = 1 ] || [ "$grub_custom" = 1 ] || [ -z "${BASH_VERSION:-}" ]; then
1413 if [ "$HV_HOST" = 1 ]; then
1414 line "${C_YEL}This is a virtualization host. Rebooting stops every running VM.${C_OFF}"
1415 line "If possible, migrate the guests to another host or shut them down first."
1416 fi
1417 if [ "$grub_custom" = 1 ]; then
1418 line "${C_YEL}GRUB_DEFAULT=$GRUB_DEFAULT_VAL, so the next boot may not use the newest kernel. Check the boot menu settings.${C_OFF}"
1419 fi
1420 printf ' Type yes to reboot now: '
1421 read -r ans || ans=""
1422 if [ "$ans" != yes ]; then
1423 line "Reboot cancelled. Run 'sudo reboot' when you are ready."
1424 return 1
1425 fi
1426 else
1427 printf ' Rebooting in 10 seconds. Press Enter to cancel... '
1428 # Reboot only when the timer runs out (exit code > 128). Enter, Ctrl-D (EOF) or errors cancel.
1429 read -r -t 10 ans
1430 if [ $? -le 128 ]; then
1431 printf '\n'
1432 line "Reboot cancelled. Run 'sudo reboot' when you are ready."
1433 return 1
1434 fi
1435 printf '\n'
1436 fi
1437 line "Rebooting. Run this script again after boot to confirm the result."
1438 run $SUDO reboot
1439 }
1440
1441 auto_patch() {
1442 setup_sudo || { show_manual; return; }
1443 if [ "$FINAL" != reboot ]; then
1444 section "Patching"
1445 note "Updating kernel packages. sudo may ask for your password."
1446 boot_space_ok || { show_manual; return; }
1447 if ! patch_now; then
1448 line "${C_RED}The package update failed. Check the output above and continue with the manual steps below.${C_OFF}"
1449 show_manual
1450 return
1451 fi
1452 if reboot_fix_ready; then
1453 FINAL=reboot
1454 line "${C_GRN}Fixed kernel confirmed for the next boot: $REBOOT_TARGET${C_OFF}"
1455 else
1456 line "The update finished, but the fix for $CVE could not be confirmed in the next boot kernel. Not rebooting."
1457 if [ -n "${NB_ALT:-}" ]; then
1458 line "The fixed kernel $NB_ALT is installed but is not the default boot kernel:"
1459 cmd "sudo grubby --set-default /boot/vmlinuz-$NB_ALT && sudo reboot"
1460 else
1461 line "The distribution may not have released a fixed kernel yet."
1462 fi
1463 show_manual
1464 return
1465 fi
1466 fi
1467 # Never reboot into a kernel whose boot files are missing (dracut/initramfs generation failure etc.)
1468 if [ -n "${REBOOT_KREL:-}" ] && ! boot_files_ok "$REBOOT_KREL"; then
1469 line "${C_RED}The kernel image or initramfs for $REBOOT_KREL is missing, so the automatic reboot stops here.${C_OFF}"
1470 cmt "Rebuild the initramfs: sudo dracut -f --kver $REBOOT_KREL / sudo update-initramfs -u -k $REBOOT_KREL"
1471 return
1472 fi
1473 section "Reboot"
1474 show_default_kernel
1475 do_reboot
1476 }
1477
1478 choose_action() {
1479 local choice label
1480 case $FINAL in
1481 reboot) label="Reboot now (apply the installed fixed kernel)" ;;
1482 pending|unknown) label="Try updating now (install and reboot if a fixed kernel is out)" ;;
1483 *) label="Patch now (install the fixed kernel and reboot)" ;;
1484 esac
1485 can_autopatch || label="Patch now — automatic patching is not supported on this system"
1486
1487 section "Choose the next step"
1488 if [ -n "$VIRT" ]; then
1489 line "${C_YEL}This system is a VM guest ($VIRT). Patching the guest does not protect the host,${C_OFF}"
1490 line "${C_YEL}so run this script on the host OS as well and patch it there.${C_OFF}"
1491 printf '\n'
1492 fi
1493 line "1. $label"
1494 line "2. Patch manually later (show the commands)"
1495 line "3. Exit"
1496 while :; do
1497 printf '\n Choice [1-3]: '
1498 read -r choice || { printf '\n'; return; }
1499 case $choice in
1500 1)
1501 if can_autopatch; then
1502 auto_patch
1503 else
1504 line "This distribution/kernel cannot be patched automatically. Follow the manual steps below."
1505 show_manual
1506 fi
1507 return ;;
1508 2) show_manual; return ;;
1509 3) line "Exiting."; return ;;
1510 *) line "Enter 1, 2 or 3." ;;
1511 esac
1512 done
1513 }
1514
1515 print_header() {
1516 printf '%s' "$C_BLU"
1517 printf '==================================================================\n'
1518 printf ' %s (PwnEscape) Linux kernel vulnerability scanner\n' "$CVE"
1519 printf ' Missing IPv4 checksum_start validation → guest VM can escape to the host\n'
1520 printf ' CVSS 9.3 Critical · every kernel since 2.6.27 · fix commit %s\n' "$FIX_COMMIT"
1521 printf '==================================================================\n'
1522 printf '%s' "$C_OFF"
1523 }
1524
1525 print_table_rows() { # $1=table name $2=row prefix
1526 local br cmp disp adv pkg
1527 while IFS='|' read -r br cmp disp adv pkg; do
1528 [ -n "$br" ] && line "$2$disp or later ($adv)"
1529 done <<EOF
1530 $(vendor_table "$1")
1531 EOF
1532 }
1533
1534 print_matrix() {
1535 printf '%s%s status and fixed versions by distribution (as of %s)%s\n' "$C_BLU" "$CVE" "$DATA_DATE" "$C_OFF"
1536 section "Upstream kernel (kernel.org)"
1537 line "Affected: every kernel since 2.6.27 / fix commit $FIX_COMMIT"
1538 line "Fixed: $UPSTREAM_TEXT"
1539 line "Other branches (5.4, 6.7-6.11, 6.13-6.17, 7.0, 7.1, ...) are end of life — move to a supported branch"
1540 section "AlmaLinux — fixed (2026-10-16, almalinux-testing repo)"
1541 print_table_rows alma8 "8: "
1542 print_table_rows alma9 "9: "
1543 print_table_rows alma10 "10: "
1544 line "Kitten 10: included in the next regular kernel update (no separate build)"
1545 section "Red Hat Enterprise Linux 7 / 8 / 9 / 10 — affected · no fix yet"
1546 line "Red Hat CVSS 8.8 (Important), no mitigation, both kernel and kernel-rt affected"
1547 section "Rocky Linux · CentOS Stream · other RHEL compatibles — no fixed kernel published"
1548 line "Expected to follow the RHEL fix (including Oracle Linux RHCK)"
1549 section "Oracle Linux UEK — fixed (2026-10-16)"
1550 print_table_rows uek ""
1551 section "Amazon Linux 2 — fixed"
1552 print_table_rows al2 ""
1553 section "Amazon Linux 2023 — fixed"
1554 print_table_rows al2023 ""
1555 line "AWS states that the EC2 · Lambda · Fargate host infrastructure is not affected"
1556 section "Debian"
1557 line "bookworm (12): 6.1.190-1 or later (DLA-4841-1, bookworm-security)"
1558 line "trixie (13): 6.12.113-1 or later (DSA-6568-1, trixie-security)"
1559 line "forky / sid: 7.2.11-1 or later"
1560 line "bullseye (11) and older: not tracked by Debian security (end of life)"
1561 section "Ubuntu — no fix yet"
1562 line "14.04 - 26.04: every kernel package (linux, -aws, -azure, -gcp, -oracle, -hwe, ...) 'Needs evaluation'"
1563 section "SUSE / openSUSE"
1564 line "SLES 12 SP5 · 15 SP4-SP7 · 16.x, SLE Micro 5.3-5.5 · 6.x, Leap 16: affected · no fix yet (Pending)"
1565 line "openSUSE Leap 15.x, SLES 15 SP1-SP3, SLE Micro 5.0-5.2: end of life"
1566 section "Fedora · Arch · openSUSE Tumbleweed · Alpine · Gentoo, ..."
1567 line "Distribution kernels track upstream stable, so the upstream fixed versions above apply"
1568 printf '\n'
1569 note "Vendor advisories keep changing. Check each distribution's security tracker for the latest status."
1570 }
1571
1572 # -----------------------------------------------------------------------------
1573 main() {
1574 parse_args "$@"
1575 setup_colors
1576 if [ "$LIST_ONLY" = 1 ]; then print_matrix; exit 0; fi
1577
1578 if [ "$OFFLINE" = 0 ]; then
1579 # Read the same values as uname -r / -v straight from /proc (no subprocess)
1580 if [ -r /proc/sys/kernel/osrelease ] && [ -r /proc/sys/kernel/version ]; then
1581 read -r KREL < /proc/sys/kernel/osrelease
1582 read -r KVER < /proc/sys/kernel/version
1583 else
1584 [ "$(uname -s)" = Linux ] ||
1585 die "Run this on Linux. (To judge another server's values use --uname-r / --uname-v / --os-release)"
1586 KREL=$(uname -r); KVER=$(uname -v)
1587 fi
1588 fi
1589 RUN_DVER=""
1590 case $KVER in *" Debian "*) RUN_DVER=${KVER#* Debian }; RUN_DVER=${RUN_DVER%% *} ;; esac
1591
1592 detect_os
1593 VIRT=""; CTR=""; HV_HOST=0
1594 [ "$OFFLINE" = 0 ] && detect_virt_container
1595 case $FAMILY in wsl|wsl1) VIRT="" ;; esac # the WSL host is Windows, so the 'patch the Linux host' advice does not apply
1596 # The running Debian kernel version comes only from uname -v. The package version on disk may have changed since boot.
1597
1598 print_header
1599 section "System information"
1600 kv "Distribution" "${OS_NAME:-unknown} [${OS_ID:-?} ${OS_VER:-?}]"
1601 kv "Running kernel" "$KREL"
1602 [ -n "$RUN_DVER" ] && kv "Debian kernel package" "$RUN_DVER"
1603 if [ "$OFFLINE" = 1 ]; then
1604 kv "Check mode" "judging supplied values (local checks skipped)"
1605 elif [ "$FAMILY" = wsl ]; then
1606 kv "Virtualization" "WSL2 (running on Windows Hyper-V)"
1607 elif [ -n "$VIRT" ]; then
1608 kv "Virtualization" "VM guest ($VIRT)"
1609 else
1610 kv "Virtualization" "physical server or not detected"
1611 fi
1612 kv "Method" "$(family_label)"
1613
1614 if [ -n "$CTR" ]; then
1615 section "Verdict"
1616 kv "Status" "$(status_label unknown)"
1617 line "Running inside a container ($CTR). Containers share the host kernel, so the"
1618 line "container image's distribution cannot be used for the verdict. Run this on the host."
1619 line "If you cannot reach the host directly, judge the host's values instead:"
1620 cmd "bash $0 --uname-r '<host uname -r>' --uname-v '<host uname -v>' --os-release <copy of the host os-release>"
1621 exit 4
1622 fi
1623
1624 evaluate_kernel "$KREL" "$RUN_DVER"
1625 R_STATUS=$E_STATUS R_FIXED=$E_FIXED R_BASIS=$E_BASIS R_ADV=$E_ADV R_PKG=$E_PKG R_MODE=$E_MODE R_INPLACE=$E_INPLACE
1626 FINAL=$R_STATUS; LP_NAME=""; REBOOT_TARGET=""; REBOOT_KREL=""; NEXT_WARN=""; NB_ALT=""
1627 # WSL never boots a distribution kernel, so installed-kernel and livepatch checks are meaningless there
1628 if [ "$OFFLINE" = 0 ] && [ "$FAMILY" != wsl ] && [ "$FAMILY" != wsl1 ]; then
1629 case $R_STATUS in
1630 vulnerable|pending|eol|unknown)
1631 if livepatch_has_fix; then
1632 FINAL=livepatched
1633 elif [ "$R_INPLACE" = 1 ]; then
1634 FINAL=reboot; REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (package replaced under the same name after boot)"
1635 elif installed_fix_ready; then
1636 FINAL=reboot
1637 fi ;;
1638 fixed)
1639 # Safe now, but if the next boot kernel is not fixed, the system becomes vulnerable again on reboot
1640 next_boot_eval
1641 if [ -n "$NB_KREL" ]; then
1642 case $NB_STATUS in fixed|unaffected) ;; *) NEXT_WARN=$NB_KREL ;; esac
1643 fi ;;
1644 esac
1645 fi
1646
1647 section "Verdict"
1648 kv "Status" "$(status_label "$FINAL")"
1649 kv "Basis" "$R_BASIS"
1650 [ -n "$R_FIXED" ] && kv "Fixed version" "$R_FIXED${R_ADV:+ ($R_ADV)}"
1651 [ "$FINAL" = livepatched ] && kv "Livepatch" "$LP_NAME"
1652 [ "$FINAL" = reboot ] && kv "Fixed kernel for next boot" "$REBOOT_TARGET"
1653 [ -n "$NEXT_WARN" ] &&
1654 kv "Warning" "${C_YEL}The next boot kernel $NEXT_WARN is not fixed. Rebooting may make the system vulnerable again.${C_OFF}"
1655 if [ "$FINAL" != reboot ] && [ -n "$NB_ALT" ]; then
1656 kv "Note" "the fixed kernel $NB_ALT is installed but is not the default boot kernel → sudo grubby --set-default /boot/vmlinuz-$NB_ALT"
1657 fi
1658
1659 if [ "$OFFLINE" = 0 ]; then
1660 section "Exposure"
1661 check_exposure
1662 fi
1663
1664 case $FINAL in
1665 fixed|unaffected|livepatched)
1666 section "Remediation"
1667 print_remedy "$FINAL"
1668 print_refs_section ;;
1669 *)
1670 # Show the menu only when both input and output are a terminal (with output redirected to a
1671 # file the prompt would be invisible and the script would seem to hang)
1672 if [ "$OFFLINE" = 0 ] && [ -t 0 ] && [ -t 1 ]; then choose_action; else show_manual; fi ;;
1673 esac
1674
1675 case $FINAL in
1676 fixed|unaffected|livepatched) exit 0 ;;
1677 vulnerable) exit 1 ;;
1678 reboot) exit 2 ;;
1679 pending|eol) exit 3 ;;
1680 *) exit 4 ;;
1681 esac
1682 }
1683
1684 main "$@"