cve-2026-99999-scanner.git

cve-2026-99999-scanner

~ Checks whether a Linux system is affected by CVE-2026-99999 (PwnEscape) and prints or applies the distribution-specific kernel fix
git clone https://github.com/debuman/cve-2026-99999-scanner.git

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 "$@"