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_kr.sh (75661B)

      1 #!/usr/bin/env bash
      2 # =============================================================================
      3 #  scanner-cve-2026-99999_kr.sh  (한국어 출력판 · 영어판: scanner-cve-2026-99999.sh)
      4 #  CVE-2026-99999 "PwnEscape" — 리눅스 커널 취약점 영향 점검 및 패치 안내
      5 # -----------------------------------------------------------------------------
      6 #  구성요소 : 커널 IPv4 스택 (skb_checksum_help → ip_do_fragment)
      7 #  원인     : virtio-net 헤더·패킷 소켓이 넘긴 checksum_start/csum_offset 을
      8 #             IPv4 헤더와 대조하지 않아, 체크섬 완료 시 이미 검증된 헤더(IHL)를
      9 #             덮어씀 (CWE-20)
     10 #  영향     : 게스트 VM → 호스트 메모리 유출 · 호스트 크래시 · 호스트 root 코드 실행
     11 #  점수     : CVSS 3.1 9.3 Critical (커널 CNA) / 8.8 Important (Red Hat)
     12 #  도입     : Linux 2.6.27 (f43798c27684)
     13 #  수정     : 6785011f8b16 "ipv4: validate checksum_start before completing checksum"
     14 #  우회책   : 공식 우회책 없음 — 수정된 커널로 교체하고 재부팅해야 함
     15 #
     16 #  판정 데이터는 2026-10-20 시점의 각 벤더 공지 기준이다. 그 이후 출시된 패치는
     17 #  설치된 커널 패키지 changelog 에서 CVE/수정 커밋을 찾아 보완 판정한다.
     18 #
     19 #  사용법   : bash scanner-cve-2026-99999_kr.sh [-l] [--no-color] [...]   (-h 참고)
     20 #             판정 후 메뉴: 1) 지금 패치(커널 업데이트 → 재부팅)
     21 #                           2) 나중에 수동 패치(명령어 출력)  3) 종료
     22 #             터미널이 아닌 곳(cron·파이프)에서 실행하면 메뉴 없이 2번처럼 동작
     23 #  종료 코드: 0 안전 / 1 취약(패치 있음) / 2 수정 커널 설치됨·재부팅 필요
     24 #             3 취약(벤더 패치 미출시 또는 지원 종료) / 4 판단 불가
     25 # =============================================================================
     26 
     27 # sh/dash 로 실행했어도 bash 가 있으면 bash 로 다시 실행 (read -t 등 bash 기능 사용)
     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 항목에는 수정이 있을 수 없다 (CVE 예약 2026-09, 수정 커밋 2026-10).
     40 # changelog 는 최신 항목부터 나오므로 이보다 오래된 항목에 닿으면 읽기를 멈춘다.
     41 CHANGELOG_CUTOFF_YEAR=2026
     42 
     43 # 업스트림 stable 브랜치별 최초 수정 버전 (커널 CNA, 2026-10-15). 메인라인은 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 이상"
     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 # 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 # 벤더별 수정 커널
     62 #   형식: 브랜치|비교용 버전-릴리스(dist 태그 제외)|수정 패키지|공지|패키지 이름
     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 공지 2026-10-16, testing 저장소|kernel' ;;
     67     alma9)  echo '5.14|5.14.0-687.57.2|kernel-5.14.0-687.57.2.el9_8|AlmaLinux 공지 2026-10-16, testing 저장소|kernel' ;;
     68     alma10) echo '6.12|6.12.0-211.66.2|kernel-6.12.0-211.66.2.el10_2|AlmaLinux 공지 2026-10-16, testing 저장소|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 # 출력 도우미
     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 '오류: %s\n' "$*" >&2; exit 4; }
    114 
    115 # 명령 존재 여부를 한 번만 PATH 에서 찾고 결과를 캐시
    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 # 표준 입력(최신 항목이 먼저 나오는 changelog)에 수정 기록이 있으면 참.
    124 #  - CVE 번호(앞뒤가 영숫자가 아닌 경우만) 또는 "ipv4: validate checksum_start ..." 커밋 제목
    125 #  - Revert 가 들어간 줄은 수정으로 치지 않음
    126 #  - 항목 날짜(rpm "* Mon Oct 13 2026 ..." / deb " -- 이름  Mon, 13 Oct 2026 ...")가
    127 #    CHANGELOG_CUTOFF_YEAR 보다 오래되면 거기서 멈춘다 → 수 MB changelog 도 최근 부분만 읽음
    128 #  일치하거나 멈추면 바로 끝나므로 앞단 rpm/gzip 도 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 사용법: $0 [옵션]
    153 
    154 이 시스템의 배포판과 실행 중인 커널을 확인해 $CVE (PwnEscape) 영향 여부를
    155 판정합니다. 취약하면 다음 중 하나를 고릅니다:
    156 
    157   1. 지금 패치       배포판 패키지 관리자로 커널을 업데이트하고 재부팅
    158   2. 나중에 수동 패치  실행할 명령어만 출력
    159   3. 종료
    160 
    161 점검은 일반 사용자로도 됩니다. 1번은 root 권한이 필요하며 sudo 로 실행합니다.
    162 터미널이 아닌 곳(cron·파이프)에서 실행하면 메뉴 없이 2번처럼 명령어만 출력합니다.
    163 
    164 옵션:
    165   -l, --list             배포판별 수정 버전 현황표만 출력
    166       --no-color         색상 없이 출력
    167       --os-release 파일  os-release 파일 지정 (기본: /etc/os-release)
    168       --uname-r 문자열   커널 릴리스 지정 (다른 서버의 'uname -r' 값으로 판정)
    169       --uname-v 문자열   커널 빌드 문자열 지정 ('uname -v', Debian 판정에 사용)
    170   -h, --help             도움말
    171 
    172   --uname-r 를 지정하면 이 시스템에 대한 로컬 검사(changelog, 설치된 커널,
    173   라이브패치, 가상화 호스트 여부)는 건너뜁니다.
    174 
    175 종료 코드:
    176   0 안전   1 취약(패치 있음)   2 수정 커널 설치됨·재부팅 필요
    177   3 취약(벤더 패치 미출시 또는 지원 종료)   4 판단 불가
    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 에 파일 경로가 필요합니다"
    187                     OS_RELEASE_FILE=$2; OSR_GIVEN=1; shift
    188                     [ -r "$OS_RELEASE_FILE" ] || die "os-release 파일을 읽을 수 없습니다: $OS_RELEASE_FILE" ;;
    189       --uname-r)    [ $# -ge 2 ] || die "--uname-r 에 값이 필요합니다"; KREL=$2; OFFLINE=1; shift ;;
    190       --uname-v)    [ $# -ge 2 ] || die "--uname-v 에 값이 필요합니다"; KVER=$2; shift ;;
    191       -h|--help)    usage; exit 0 ;;
    192       *)            usage >&2; exit 4 ;;
    193     esac
    194     shift
    195   done
    196   # 실제 커널 릴리스와 지정한 빌드 문자열이 섞이면 판정이 틀어진다
    197   if [ -n "$KVER" ] && [ "$OFFLINE" = 0 ]; then
    198     die "--uname-v 는 --uname-r 와 함께 지정해야 합니다"
    199   fi
    200 }
    201 
    202 # -----------------------------------------------------------------------------
    203 # 버전 비교 (rpm: rpmvercmp, deb: dpkg 규칙). dpkg 가 있으면 deb 는 dpkg 사용.
    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: "비교키<TAB>..." 줄 가운데 ref 보다 크고 가장 큰 줄 하나를 출력 (한 번의 실행으로 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 (프로세스 생성 없음)
    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 (프로세스 생성 없음)
    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 # 시스템 정보 수집
    339 # -----------------------------------------------------------------------------
    340 # os-release 를 한 번에 읽는다 (따옴표·CRLF 처리, 프로세스 생성 없음)
    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     # os-release 가 없는 옛 RHEL 계열(5/6): "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: 리눅스 커널이 아니라 Windows 의 호환 계층
    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 "AlmaLinux 공지(2026-10-16)의 수정 커널 버전과 비교" ;;
    417     kitten)        echo "AlmaLinux Kitten — 정기 커널 업데이트로 수정 예정" ;;
    418     rhel)          echo "Red Hat 공지(패치 미출시) + 커널 changelog 검사" ;;
    419     centos|rocky|rhelclone) echo "RHEL 계열 공지 + 커널 changelog 검사" ;;
    420     oracle)        echo "Oracle Linux ELSA 공지(UEK)의 수정 커널 버전과 비교" ;;
    421     amazon)        echo "Amazon Linux ALAS 공지의 수정 커널 버전과 비교" ;;
    422     debian)        echo "Debian 보안 트래커의 수정 패키지 버전과 비교" ;;
    423     debianlike)    echo "Debian 파생 — Debian 커널이면 Debian 기준, 아니면 업스트림 기준" ;;
    424     ubuntu)        echo "Ubuntu CVE 트래커(패치 미출시) + 커널 changelog 검사" ;;
    425     proxmox)       echo "Proxmox VE 커널 — 커널 changelog 검사" ;;
    426     sles|slemicro|leap) echo "SUSE 보안 공지(패치 미출시) + 커널 changelog 검사" ;;
    427     fedora|arch|tumbleweed|microos|alpine|gentoo) echo "업스트림 stable 기반 배포판 — 업스트림 수정 버전과 비교" ;;
    428     wsl)           echo "WSL2 (Microsoft 커널) — 업스트림 수정 버전과 비교" ;;
    429     wsl1)          echo "WSL1 — 리눅스 커널을 쓰지 않음" ;;
    430     *)             echo "알 수 없는 배포판 — 업스트림 수정 버전과 비교(참고용)" ;;
    431   esac
    432 }
    433 
    434 # 가상화(VIRT)와 컨테이너(CTR)를 systemd-detect-virt 한 번으로 판별
    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)   # 컨테이너 안이면 컨테이너 종류가 우선 출력됨
    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 # 커널 패키지 changelog 에 수정 기록이 있는지 (RHEL·SUSE·Oracle·Debian·Ubuntu 계열)
    462 # rpm 은 'rpm -qf --changelog' 한 번으로 소유 패키지 조회와 changelog 출력을 함께 한다.
    463 #   CL_SRC 에 근거 종류(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 # 부팅 가능한 커널 이미지가 남아 있는 커널만 (DKMS 등이 남긴 빈 모듈 디렉터리 제외).
    487 # Alpine·Gentoo 는 이미지 이름에 커널 릴리스가 없을 수 있어 modules.dep 로 판단.
    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 # 실행 중인 커널 이미지가 부팅 뒤 같은 이름으로 교체됐는지 (Debian 계열 같은 ABI 제자리 업데이트).
    501 # dpkg 는 파일 mtime 을 빌드 시각으로 두므로 설치 시각인 ctime 을 부팅 시각(btime)과 비교한다.
    502 image_replaced_after_boot() {
    503   local f c k v bt=""
    504   for f in "/boot/vmlinuz-$1" "/lib/modules/$1/vmlinuz" ""; do [ -n "$f" ] && [ -e "$f" ] && break; done
    505   [ -n "$f" ] || return 1
    506   c=$(stat -c %Z "$f" 2>/dev/null) || return 1
    507   while read -r k v; do [ "$k" = btime ] && { bt=$v; break; }; done < /proc/stat
    508   [ -n "$bt" ] && [ "$c" -gt "$bt" ]
    509 }
    510 
    511 # grubby 로 다음 부팅 기본 커널을 알 수 있으면 DEFAULT_KREL 에 담는다 (RHEL·Fedora·Amazon 계열)
    512 default_boot_krel() {
    513   local d
    514   DEFAULT_KREL=""
    515   have grubby || return 1
    516   d=$($SUDO grubby --default-kernel 2>/dev/null) || return 1
    517   case $d in /boot/vmlinuz-?*) DEFAULT_KREL=${d#/boot/vmlinuz-} ;; *) return 1 ;; esac
    518 }
    519 
    520 # 재부팅 대상 커널의 이미지와 initramfs 가 실제로 있는지 (없으면 부팅 실패 위험)
    521 boot_files_ok() {
    522   local k=$1 f img=0 ird=0
    523   case $FAMILY in arch|alpine|gentoo|microos|slemicro) return 0 ;; esac   # 이미지 이름에 릴리스가 없음
    524   for f in "/boot/vmlinuz-$k" "/lib/modules/$k/vmlinuz" "/boot/Image-$k"; do [ -s "$f" ] && img=1; done
    525   for f in "/boot/initramfs-$k.img" "/boot/initrd.img-$k" "/boot/initrd-$k"; do [ -s "$f" ] && ird=1; done
    526   [ "$img" = 1 ] && [ "$ird" = 1 ]
    527 }
    528 
    529 # 실행 중인 커널보다 새로 설치된 커널 가운데 가장 최신 것(보통 다음 부팅 기본 커널)
    530 #   결과: NEW_KREL, NEW_DVER(Debian 패키지 버전). 후보 수집은 셸 내장으로, 비교는 awk 한 번으로.
    531 newest_installed() {
    532   local mode=rpm ref k list="" st pkg ver pick tab
    533   NEW_KREL=""; NEW_DVER=""
    534   tab=$(printf '\t')
    535   if [ "$FAMILY" = debian ]; then
    536     have dpkg-query || return 1
    537     mode=deb; ref=$RUN_DVER
    538     while IFS='|' read -r st pkg ver; do
    539       case $st in ii*) ;; *) continue ;; esac
    540       k=${pkg#linux-image-}
    541       [ -e "/lib/modules/$k/modules.dep" ] || continue
    542       list="$list$ver$tab$k$tab$ver
    543 "
    544     done <<EOF
    545 $(dpkg-query -W -f='${db:Status-Abbrev}|${Package}|${Version}\n' 'linux-image-*' 2>/dev/null)
    546 EOF
    547   else
    548     rpm_evr_of_krel "$KREL"; ref=$EVR
    549     for k in $(list_installed_krels); do
    550       [ "$k" = "$KREL" ] && continue
    551       [ "$FAMILY" = proxmox ] && case $k in *-pve) ;; *) continue ;; esac   # Proxmox 커널만
    552       rpm_evr_of_krel "$k"
    553       list="$list$EVR$tab$k$tab
    554 "
    555     done
    556   fi
    557   [ -n "$list" ] || return 1
    558   pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode="$mode" -v ref="$ref" "$AWK_VERCMP")
    559   [ -n "$pick" ] || return 1
    560   pick=${pick#*"$tab"}
    561   NEW_KREL=${pick%%"$tab"*}
    562   NEW_DVER=${pick#*"$tab"}
    563 }
    564 
    565 # 다음 부팅 커널을 정하고 판정한다.
    566 #   grubby 로 기본 커널을 알 수 있으면 그것, 아니면 실행 중인 커널보다 새로 설치된 커널 중 최신 것.
    567 #   결과: NB_KREL / NB_DVER / NB_STATUS (다음 부팅이 지금 커널과 같으면 NB_KREL 비움)
    568 #         NB_ALT = 수정된 커널이 설치돼 있지만 기본 부팅 커널이 아닌 경우 그 커널
    569 next_boot_eval() {
    570   NB_KREL=""; NB_DVER=""; NB_STATUS=""; NB_ALT=""
    571   newest_installed || { NEW_KREL=""; NEW_DVER=""; }
    572   if default_boot_krel; then
    573     [ "$DEFAULT_KREL" != "$KREL" ] && NB_KREL=$DEFAULT_KREL
    574   else
    575     NB_KREL=$NEW_KREL; NB_DVER=$NEW_DVER
    576   fi
    577   if [ -n "$NB_KREL" ]; then
    578     evaluate_kernel "$NB_KREL" "$NB_DVER"; NB_STATUS=$E_STATUS
    579   fi
    580   if [ "$NB_STATUS" != fixed ] && [ -n "$NEW_KREL" ] && [ "$NEW_KREL" != "$NB_KREL" ]; then
    581     evaluate_kernel "$NEW_KREL" "$NEW_DVER"
    582     [ "$E_STATUS" = fixed ] && NB_ALT=$NEW_KREL
    583   fi
    584   return 0
    585 }
    586 
    587 # 재부팅하면 수정된 커널로 부팅되는지 → REBOOT_TARGET / REBOOT_KREL 설정
    588 installed_fix_ready() {
    589   REBOOT_TARGET=""; REBOOT_KREL=""
    590   next_boot_eval
    591   [ "$NB_STATUS" = fixed ] || return 1
    592   REBOOT_KREL=$NB_KREL
    593   REBOOT_TARGET="$NB_KREL${NB_DVER:+ (패키지 $NB_DVER)}"
    594 }
    595 
    596 # 활성화된 커널 라이브패치가 있는지 (sysfs 만 읽음)
    597 livepatch_active() {
    598   local f v
    599   for f in /sys/kernel/livepatch/*/enabled; do
    600     [ -r "$f" ] || continue
    601     read -r v < "$f" 2>/dev/null && [ "$v" = 1 ] && return 0
    602   done
    603   return 1
    604 }
    605 
    606 # 실행 중인 커널에 라이브패치로 수정이 적용됐는지.
    607 # 활성 라이브패치가 없으면 rpm 데이터베이스 전체 조회(rpm -qa)를 아예 하지 않는다.
    608 livepatch_has_fix() {
    609   local p rel
    610   if livepatch_active; then
    611     if have canonical-livepatch && canonical-livepatch status --verbose 2>/dev/null | has_fix_text; then
    612       LP_NAME="Canonical Livepatch"; return 0
    613     fi
    614     if have rpm; then
    615       rpm_evr_of_krel "$KREL"; rel=$EVR
    616       case $rel in *-default|*-azure|*-rt|*-64kb) rel=${rel%-*} ;; esac
    617       # 실행 커널용 패키지만 정확히 (RHEL/SUSE 는 점→밑줄, Amazon 은 점 그대로)
    618       for p in $(rpm -qa 'kpatch-patch*' 'kernel-livepatch-*' 2>/dev/null); do
    619         case $p in *-"${rel//./_}"-*|*-"$rel"-*) ;; *) continue ;; esac
    620         if rpm -q --changelog "$p" 2>/dev/null | has_fix_text; then LP_NAME=$p; return 0; fi
    621       done
    622     fi
    623   fi
    624   if have uptrack-show && uptrack-show 2>/dev/null | has_fix_text; then
    625     LP_NAME="Oracle Ksplice"; return 0
    626   fi
    627   return 1
    628 }
    629 
    630 # -----------------------------------------------------------------------------
    631 # 판정 — 결과는 E_STATUS / E_FIXED / E_BASIS / E_ADV / E_PKG / E_MODE 에 담긴다
    632 #   E_STATUS: fixed | vulnerable | pending | eol | unaffected | unknown
    633 # -----------------------------------------------------------------------------
    634 upstream_eval() {  # 프로세스 생성 없이 셸 패턴으로만 해석
    635   local rel=$1 base maj min pat rest rc="" br v fpat
    636   E_MODE=upstream
    637   base=${rel%%[!0-9.]*}
    638   maj=${base%%.*}; rest=${base#*.}
    639   min=${rest%%.*}; pat=${rest#*.}; pat=${pat%%.*}
    640   [ "$pat" = "$rest" ] && pat=0          # x.y 형식
    641   case $maj:$min:$pat in
    642     *[!0-9:]*|:*|*::*|*:) E_STATUS=unknown; E_BASIS="커널 버전 형식을 해석할 수 없음 ($rel)"; return ;;
    643   esac
    644   [ "$rest" = "$base" ] && { E_STATUS=unknown; E_BASIS="커널 버전 형식을 해석할 수 없음 ($rel)"; return; }
    645   base="$maj.$min.$pat"
    646   # rc 번호: "-rc6", ".rc6", Ubuntu mainline "070300rc5" 처럼 영문자가 아닌 문자 뒤의 rcN
    647   case $rel in
    648     *[!A-Za-z]rc[0-9]*) rc=${rel#*[!A-Za-z]rc}; rc=${rc%%[!0-9]*} ;;
    649   esac
    650   br="$maj.$min"
    651 
    652   if [ "$maj" -lt 2 ] || { [ "$maj" -eq 2 ] && [ "$min" -lt 6 ]; } || { [ "$br" = 2.6 ] && [ "$pat" -lt 27 ]; }; then
    653     case $rel in
    654       *.el[0-9]*|*.amzn*)   # 벤더 커널은 옛 버전 번호에 새 네트워크 코드를 백포트했을 수 있음
    655         E_STATUS=unknown; E_BASIS="$base 벤더 커널 — 버전 번호는 2.6.27 이전이지만 백포트 여부 확인 필요" ;;
    656       *)
    657         E_STATUS=unaffected; E_BASIS="$base — 취약 코드가 들어온 2.6.27 이전 커널" ;;
    658     esac
    659     return
    660   fi
    661   if [ "$maj" -gt 7 ] || { [ "$maj" -eq 7 ] && [ "$min" -ge 3 ]; }; then
    662     E_FIXED="7.3-rc7"
    663     if [ "$br" = 7.3 ] && [ -n "$rc" ] && [ "$rc" -lt 7 ]; then
    664       E_STATUS=vulnerable; E_BASIS="7.3-rc$rc < 메인라인 수정 시점 7.3-rc7"
    665     else
    666       E_STATUS=fixed; E_BASIS="$base — 메인라인 7.3-rc7 이후 커널(수정 포함)"
    667     fi
    668     return
    669   fi
    670   for v in $UPSTREAM_FIXED; do
    671     [ "${v%.*}" = "$br" ] || continue
    672     fpat=${v##*.}; E_FIXED=$v
    673     if [ "$pat" -ge "$fpat" ]; then
    674       E_STATUS=fixed; E_BASIS="$base ≥ 업스트림 수정 버전 $v"
    675     else
    676       E_STATUS=vulnerable; E_BASIS="$base < 업스트림 수정 버전 $v"
    677     fi
    678     return
    679   done
    680   E_STATUS=eol; E_BASIS="업스트림 $br 브랜치는 지원 종료(EOL) — 이 브랜치용 수정 버전이 나오지 않음"
    681 }
    682 
    683 nonvendor_eval() {  # $1=커널 릴리스 $2=사유
    684   upstream_eval "$1"
    685   E_BASIS="$2 → 업스트림 기준: $E_BASIS"
    686 }
    687 
    688 rpm_table_eval() {  # $1=커널 릴리스 $2=테이블 이름
    689   local evr br t_br t_cmp t_disp t_adv t_pkg
    690   rpm_evr_of_krel "$1"; evr=$EVR
    691   branch_of "$evr"; br=$BRANCH
    692   while IFS='|' read -r t_br t_cmp t_disp t_adv t_pkg; do
    693     [ -n "$t_br" ] && [ "$t_br" = "$br" ] || continue
    694     E_FIXED=$t_disp; E_ADV=$t_adv; E_PKG=$t_pkg
    695     if ver_lt rpm "$evr" "$t_cmp"; then
    696       E_STATUS=vulnerable; E_BASIS="커널 $evr < 수정 버전 $t_cmp"
    697     else
    698       E_STATUS=fixed; E_BASIS="커널 $evr ≥ 수정 버전 $t_cmp"
    699     fi
    700     return 0
    701   done <<EOF
    702 $(vendor_table "$2")
    703 EOF
    704   return 1
    705 }
    706 
    707 deb_eval() {  # $1=커널 릴리스 $2=Debian 패키지 버전
    708   local v=$2 up br t
    709   if [ -z "$v" ]; then
    710     if [ "$OFFLINE" = 1 ]; then
    711       E_STATUS=unknown
    712       E_BASIS="Debian 커널은 패키지 버전이 필요합니다 — 대상 서버의 'uname -v' 값을 --uname-v 로 지정하세요"
    713       return
    714     fi
    715     nonvendor_eval "$1" "Debian 패키지 커널이 아님"; return
    716   fi
    717   case $v in *+rpt*|*rpi*) nonvendor_eval "$1" "Raspberry Pi 커널"; return ;; esac
    718   up=${v#*:}; branch_of "$up"; br=$BRANCH
    719   case $br in
    720     6.1)  t="6.1.190-1~";  E_FIXED="6.1.190-1"; E_ADV="bookworm-security, DLA-4841-1" ;;  # ~ : bullseye-LTS 의 ~deb11u 포함
    721     6.12) t="6.12.113-1~"; E_FIXED="6.12.113-1"; E_ADV="trixie-security, DSA-6568-1" ;;
    722     7.2)  t="7.2.11-1~";   E_FIXED="7.2.11-1"; E_ADV="forky/sid" ;;
    723     *)
    724       upstream_eval "${up%%-*}"
    725       if [ "$E_STATUS" = fixed ] || [ "$E_STATUS" = unaffected ]; then
    726         E_BASIS="Debian 커널 $v → 업스트림 기준: $E_BASIS"
    727       else
    728         E_STATUS=eol
    729         E_BASIS="Debian 보안 추적 대상이 아닌 커널 브랜치($br) — 지원 종료 릴리스(bullseye 이하) 또는 비표준 커널"
    730       fi
    731       E_MODE=vendor
    732       return ;;
    733   esac
    734   if ver_lt deb "$v" "$t"; then
    735     E_STATUS=vulnerable; E_BASIS="Debian 커널 $v < 수정 버전 $E_FIXED"
    736   else
    737     E_STATUS=fixed; E_BASIS="Debian 커널 $v ≥ 수정 버전 $E_FIXED"
    738   fi
    739 }
    740 
    741 # 판정: 먼저 벤더 표·업스트림 버전으로 판정하고, 그것으로 결론이 안 날 때(미출시·지원 종료·불명)만
    742 # 커널 changelog(수 MB)를 검사한다. 표로 결론이 나는 배포판은 changelog 를 전혀 읽지 않는다.
    743 #   E_INPLACE=1: 실행 중인 커널과 같은 이름의 패키지가 부팅 뒤 수정본으로 교체됨 → 재부팅 필요
    744 evaluate_kernel() {  # $1=커널 릴리스 $2=Debian 패키지 버전(선택)
    745   E_INPLACE=0
    746   evaluate_by_version "$@"
    747   case $E_STATUS in pending|unknown|eol) ;; *) return ;; esac
    748   [ "$OFFLINE" = 0 ] || return
    749   if changelog_has_fix "$1"; then
    750     # deb 계열 changelog 는 디스크에 있는 패키지의 것 — 실행 중 커널이 부팅 뒤 교체됐다면
    751     # 그 수정은 아직 메모리의 커널에 들어 있지 않다
    752     if [ "$1" = "$KREL" ] && [ "$CL_SRC" = deb ] && image_replaced_after_boot "$1"; then
    753       E_INPLACE=1
    754       E_BASIS="$E_BASIS · 부팅 뒤 같은 이름으로 교체된 커널 패키지에는 수정이 있음(재부팅 필요)"
    755       return
    756     fi
    757     E_STATUS=fixed; E_MODE=changelog
    758     E_BASIS="커널 패키지 changelog 에 $CVE 또는 수정 커밋이 기록되어 있음"
    759   else
    760     case $FAMILY in
    761       rhel|centos|rocky|rhelclone|kitten|oracle|ubuntu|proxmox|sles|slemicro|leap|debian|debianlike)
    762         E_BASIS="$E_BASIS · 커널 changelog 에도 수정 기록 없음" ;;
    763     esac
    764   fi
    765 }
    766 
    767 evaluate_by_version() {
    768   local krel=$1 dver=${2:-}
    769   E_STATUS=unknown; E_FIXED=""; E_BASIS=""; E_ADV=""; E_PKG=""; E_MODE=vendor
    770 
    771   # RPM 계열에서 배포판 기본 커널이 아닌 경우(ELRepo·직접 빌드)는 업스트림 기준
    772   case $FAMILY in
    773     alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon)
    774       case $krel in
    775         *elrepo*)                 nonvendor_eval "$krel" "ELRepo 커널"; return ;;
    776         *.el[0-9]*|*.amzn[0-9]*) ;;
    777         *)                       nonvendor_eval "$krel" "배포판 기본 커널이 아님"; return ;;
    778       esac ;;
    779   esac
    780 
    781   case $FAMILY in
    782     alma)
    783       case $OS_MAJ_NUM in
    784         8|9|10) rpm_table_eval "$krel" "alma$OS_MAJ_NUM" ||
    785                   nonvendor_eval "$krel" "AlmaLinux $OS_MAJ_NUM 기본 커널 브랜치가 아님" ;;
    786         *)      E_STATUS=eol; E_BASIS="지원 종료된 AlmaLinux 릴리스 ($OS_VER)" ;;
    787       esac ;;
    788     kitten)
    789       E_STATUS=pending
    790       E_BASIS="AlmaLinux Kitten 은 별도 빌드 없이 다음 정기 커널 업데이트에 수정이 포함될 예정" ;;
    791     rhel|centos|rocky|rhelclone)
    792       if { [ "$FAMILY" = rhel ] && [ "$OS_MAJ_NUM" -lt 7 ]; } ||
    793          { [ "$FAMILY" = centos ] && [ "$OS_MAJ_NUM" -le 8 ]; }; then
    794         E_STATUS=eol; E_BASIS="지원 종료된 릴리스 (${OS_NAME:-$OS_ID $OS_VER}) — 수정 커널이 나오지 않음"
    795       elif [ "$FAMILY" = rhel ]; then
    796         E_STATUS=pending; E_BASIS="Red Hat: RHEL 7/8/9/10 영향 받음, 수정 커널 미출시 ($DATA_DATE 기준)"
    797       else
    798         E_STATUS=pending; E_BASIS="RHEL 수정 커널 미출시 — 이 배포판도 공개된 수정 커널 없음 ($DATA_DATE 기준)"
    799       fi ;;
    800     oracle)
    801       case $krel in
    802         *uek*)
    803           rpm_table_eval "$krel" uek || {
    804             rpm_evr_of_krel "$krel"; branch_of "$EVR"
    805             E_STATUS=pending
    806             E_BASIS="이 UEK 릴리스(${BRANCH:-?})용 수정 공지 없음 — UEK R6 이상으로 전환 필요"
    807           } ;;
    808         *)
    809           E_STATUS=pending
    810           E_BASIS="RHCK(Red Hat 호환 커널)용 수정 공지 없음 ($DATA_DATE 기준) — UEK 는 수정됨" ;;
    811       esac ;;
    812     amazon)
    813       case $OS_VER in
    814         2|2023)
    815           rpm_table_eval "$krel" "al$OS_VER" ||
    816             { E_STATUS=unknown; E_BASIS="ALAS 공지에 없는 커널 브랜치 ($krel)"; } ;;
    817         *)
    818           E_STATUS=eol; E_BASIS="Amazon Linux 1 은 지원 종료 — 수정 공지 없음" ;;
    819       esac ;;
    820     debian)
    821       deb_eval "$krel" "$dver" ;;
    822     debianlike)
    823       if [ -n "$dver" ]; then deb_eval "$krel" "$dver"
    824       else nonvendor_eval "$krel" "배포판 자체 커널"; fi ;;
    825     ubuntu)
    826       E_STATUS=pending
    827       E_BASIS="Ubuntu CVE 트래커: 모든 커널 패키지 'Needs evaluation' — 패치 미출시 ($DATA_DATE 기준)" ;;
    828     proxmox)
    829       E_STATUS=unknown
    830       E_BASIS="Proxmox VE 커널(Ubuntu 커널 기반)의 공개된 수정 정보 없음" ;;
    831     sles|slemicro)
    832       case $FAMILY:$OS_VER in
    833         sles:11*|sles:12|sles:12.[0-4]|sles:15|sles:15.[1-3]|slemicro:5.[0-2])
    834           E_STATUS=eol; E_BASIS="지원 종료된 SUSE 릴리스 ($OS_VER) — 사전 패치 대상 아님" ;;
    835         *)
    836           E_STATUS=pending; E_BASIS="SUSE 공지: kernel-default 등 'Affected', 상태 'Pending' — 패치 미출시 ($DATA_DATE 기준)" ;;
    837       esac ;;
    838     leap)
    839       case $OS_MAJ_NUM in
    840         15|42) E_STATUS=eol; E_BASIS="openSUSE Leap $OS_VER 은 지원 종료" ;;
    841         *)     E_STATUS=pending; E_BASIS="SUSE 공지: Leap 16 'Affected', 상태 'Pending' — 패치 미출시 ($DATA_DATE 기준)" ;;
    842       esac ;;
    843     fedora|arch|tumbleweed|microos|alpine|gentoo|wsl)
    844       upstream_eval "$krel" ;;
    845     wsl1)
    846       E_STATUS=unaffected; E_BASIS="WSL1 은 리눅스 커널 없이 Windows 가 시스템 호출을 처리함" ;;
    847     *)
    848       upstream_eval "$krel"
    849       [ "$E_STATUS" = eol ] && E_STATUS=unknown
    850       E_BASIS="업스트림 기준 추정(배포판 백포트 여부 확인 필요): $E_BASIS" ;;
    851   esac
    852 }
    853 
    854 status_label() {
    855   case $1 in
    856     fixed)       printf '%s안전 — 수정된 커널로 실행 중%s' "$C_GRN" "$C_OFF" ;;
    857     unaffected)  printf '%s영향 없음 — 이 커널에는 취약한 코드가 없음%s' "$C_GRN" "$C_OFF" ;;
    858     livepatched) printf '%s안전(임시) — 라이브패치로 수정 적용됨%s' "$C_GRN" "$C_OFF" ;;
    859     reboot)      printf '%s재부팅 필요 — 수정 커널은 설치됐지만 이전 커널로 실행 중%s' "$C_YEL" "$C_OFF" ;;
    860     vulnerable)  printf '%s취약 — 수정 커널이 나와 있음, 업데이트 필요%s' "$C_RED" "$C_OFF" ;;
    861     pending)     printf '%s취약 — 배포판 수정 커널 미출시 (%s 기준)%s' "$C_RED" "$DATA_DATE" "$C_OFF" ;;
    862     eol)         printf '%s취약 — 지원 종료(EOL) 릴리스/브랜치, 수정 커널 없음%s' "$C_RED" "$C_OFF" ;;
    863     *)           printf '%s판단 불가 — 수동 확인 필요%s' "$C_YEL" "$C_OFF" ;;
    864   esac
    865 }
    866 
    867 # -----------------------------------------------------------------------------
    868 # 노출 위험 요소 — 주 공격 대상은 신뢰할 수 없는 게스트를 돌리는 가상화 호스트
    869 # -----------------------------------------------------------------------------
    870 read_sysctl() {  # $1=파일 → SYSCTL_VAL (없으면 빈 값, 프로세스 생성 없음)
    871   SYSCTL_VAL=""
    872   [ -r "$1" ] && read -r SYSCTL_VAL < "$1" 2>/dev/null
    873   return 0
    874 }
    875 
    876 check_exposure() {
    877   local any=0 n procs v mods="" m _
    878   HV_HOST=0
    879   if [ -e /dev/kvm ]; then
    880     line "- /dev/kvm 존재: 이 시스템에서 KVM 가상 머신을 실행할 수 있음"; any=1
    881   fi
    882   # /proc/modules 는 한 번만 읽는다
    883   if [ -r /proc/modules ]; then
    884     while read -r m _; do mods="$mods $m"; done < /proc/modules
    885   fi
    886   case "$mods " in *" vhost_net "*)
    887     line "- vhost_net 모듈 로드됨: virtio-net 백엔드 사용 중"; any=1; HV_HOST=1 ;;
    888   esac
    889   case "$mods " in *" tun "*|*" tap "*|*" macvtap "*)
    890     line "- tun/tap 모듈 로드됨"; any=1 ;;
    891   esac
    892   set -- /sys/class/net/*/tun_flags
    893   if [ -e "$1" ]; then n=$#; else n=0; fi
    894   if [ "$n" -gt 0 ]; then
    895     line "- TUN/TAP 인터페이스 ${n}개 존재"; any=1
    896   fi
    897   # 프로세스 이름은 grep 하나가 /proc/*/comm 을 직접 읽고, awk 가 중복 제거·연결
    898   #   kvm = Proxmox 의 VM 프로세스, vhost-<pid> = virtio-net vhost 작업 스레드
    899   procs=$(grep -hE '^(qemu-system|qemu-kvm|kvm$|vhost-[0-9]|firecracker|cloud-hyperviso|crosvm|libvirtd|virtqemud|VBoxHeadless|VirtualBoxVM|vmware-vmx)' \
    900             /proc/[0-9]*/comm 2>/dev/null | awk '{ sub(/^vhost-[0-9]+$/, "vhost") } !s[$0]++ { printf "%s ", $0 }')
    901   if [ -n "$procs" ]; then
    902     line "- 가상화 프로세스 실행 중: $procs"; any=1; HV_HOST=1
    903   fi
    904   if [ "$FAMILY" = proxmox ] && [ "$HV_HOST" = 0 ]; then
    905     line "- Proxmox VE 노드"; any=1; HV_HOST=1
    906   fi
    907   [ "$any" = 1 ] || line "- 가상화 호스트 징후 없음 (KVM · TUN/TAP · VM 프로세스 미발견)"
    908   if [ "$HV_HOST" = 1 ]; then
    909     line "${C_RED}→ 가상화 호스트로 보입니다. 게스트→호스트 탈출의 직접 대상이므로 최우선 패치 대상입니다.${C_OFF}"
    910   fi
    911   [ -n "$VIRT" ] && line "- 이 시스템은 VM 게스트입니다 ($VIRT). 호스트 커널 패치는 호스트 관리자/사업자 몫입니다."
    912 
    913   if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then
    914     read_sysctl /proc/sys/kernel/unprivileged_userns_clone; v=$SYSCTL_VAL
    915     if [ "$v" = 0 ]; then line "- 비특권 사용자 네임스페이스: 차단 (kernel.unprivileged_userns_clone=0)"
    916     else line "- 비특권 사용자 네임스페이스: 허용 (kernel.unprivileged_userns_clone=$v)"; fi
    917   elif [ -r /proc/sys/user/max_user_namespaces ]; then
    918     read_sysctl /proc/sys/user/max_user_namespaces; v=$SYSCTL_VAL
    919     if [ "$v" = 0 ]; then line "- 사용자 네임스페이스: 차단 (user.max_user_namespaces=0)"
    920     else line "- 사용자 네임스페이스: 허용 (user.max_user_namespaces=$v)"; fi
    921   fi
    922   read_sysctl /proc/sys/kernel/apparmor_restrict_unprivileged_userns
    923   if [ "$SYSCTL_VAL" = 1 ]; then
    924     line "  (AppArmor 가 비특권 네임스페이스 권한을 제한 중)"
    925   fi
    926   note "네임스페이스 항목은 참고용입니다. 비특권 사용자가 네임스페이스 안에서 CAP_NET_ADMIN/CAP_NET_RAW 를 얻으면 TUN/TAP·패킷 소켓 경로에 닿을 수 있어 로컬 공격 가능성에 영향을 줄 수 있으나(추정), 벤더가 발표한 완화책은 아닙니다."
    927 }
    928 
    929 # -----------------------------------------------------------------------------
    930 # 조치 방법
    931 # -----------------------------------------------------------------------------
    932 remedy_eol() {
    933   line "이 릴리스/커널 브랜치용 수정 커널은 나오지 않습니다. $1"
    934 }
    935 
    936 remedy_custom() {
    937   line "배포판 기본 커널이 아닌 커널(ELRepo·직접 빌드 등)로 판정했습니다."
    938   line "커널 공급처의 업데이트를 적용하거나 업스트림 stable 수정 버전 이상으로 올리세요:"
    939   line "  $UPSTREAM_TEXT"
    940   line "  수정 커밋: $FIX_COMMIT ($FIX_URL)"
    941   cmt "커널 CNA 는 개별 커밋 cherry-pick 보다 최신 stable 커널 사용을 권장"
    942   cmt "ELRepo 예: sudo dnf --enablerepo=elrepo-kernel update 'kernel-ml*' 'kernel-lt*'"
    943   cmt "또는 배포판 기본 커널로 부팅을 전환한 뒤 배포판 패치를 적용"
    944 }
    945 
    946 remedy_rhel_like() {  # $1=status
    947   local pm=dnf core=kernel-core
    948   case $OS_MAJ_NUM in [1-7]) pm=yum; core=kernel ;; esac
    949   if [ "$1" = eol ]; then
    950     remedy_eol "지원되는 릴리스(RHEL·Rocky·AlmaLinux 8/9/10, CentOS Stream 9/10)로 이전하세요."
    951     return
    952   fi
    953   line "$DATA_DATE 기준 수정 커널이 출시되지 않았습니다. (Red Hat: 완화책 없음)"
    954   cmt "1) 패치 출시 여부 확인"
    955   cmd "sudo $pm updateinfo list --cve $CVE"
    956   cmd "rpm -q --changelog $core | grep -iE '$CVE|checksum_start'"
    957   cmt "2) 출시되면 적용 후 재부팅"
    958   cmd "sudo $pm update --cve $CVE        # 또는: sudo $pm update 'kernel*'"
    959   cmd "sudo reboot"
    960   if [ "$FAMILY" = rhel ]; then
    961     cmt "Red Hat 이 kpatch 라이브패치를 제공하면 재부팅 없이 적용 가능"
    962     cmd "sudo $pm install \"kpatch-patch = \$(uname -r)\""
    963   fi
    964 }
    965 
    966 remedy_family() {  # $1=status
    967   local st=$1 pm
    968   case $FAMILY in
    969     alma)
    970       if [ "$st" = eol ]; then remedy_eol "지원되는 AlmaLinux(8.10 / 9 / 10)로 업그레이드하세요."; return; fi
    971       line "수정 커널: ${R_FIXED:-AlmaLinux 공지 참고} 이상"
    972       cmt "1) 정식 저장소에 이미 반영됐는지 먼저 시도"
    973       cmd "sudo dnf clean metadata && sudo dnf update 'kernel*'"
    974       cmt "2) 아직 없으면 testing 저장소에서 설치 (AlmaLinux 2026-10-16 공지 방법)"
    975       cmd "sudo dnf install -y almalinux-release-testing"
    976       cmd "sudo dnf update 'kernel*' --enablerepo=almalinux-testing"
    977       cmd "sudo dnf remove -y almalinux-release-testing     # testing 저장소 정리"
    978       cmt "3) 재부팅 후 'uname -r' 로 수정 버전인지 확인"
    979       cmd "sudo reboot"
    980       ;;
    981     kitten)
    982       line "AlmaLinux Kitten 은 다음 정기 커널 업데이트에 수정이 포함될 예정입니다."
    983       cmd "sudo dnf update 'kernel*'"
    984       cmd "sudo reboot"
    985       ;;
    986     rhel|centos|rocky|rhelclone)
    987       remedy_rhel_like "$st"
    988       ;;
    989     oracle)
    990       pm=dnf; case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
    991       case $KREL in
    992         *uek*)
    993           if [ "$st" = vulnerable ]; then
    994             line "수정 커널: $R_FIXED 이상 ($R_ADV)"
    995             cmd "sudo $pm update kernel-uek"
    996             cmd "sudo reboot"
    997             cmt "Ksplice 구독 시 재부팅 없이 적용: sudo uptrack-upgrade -y"
    998           else
    999             line "사용 중인 UEK 릴리스용 수정 공지가 없습니다. 수정된 UEK 로 전환하세요."
   1000             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)"
   1001             cmd "sudo $pm install kernel-uek"
   1002             cmd "sudo reboot"
   1003           fi ;;
   1004         *)
   1005           line "RHCK(Red Hat 호환 커널)용 수정은 아직 없습니다. UEK 는 이미 수정됐으므로 UEK 로 전환하는 것이 가장 빠릅니다."
   1006           cmd "sudo $pm install kernel-uek"
   1007           cmd "sudo grubby --info=ALL | grep -E '^kernel'        # 설치된 UEK 경로 확인"
   1008           cmd "sudo grubby --set-default /boot/vmlinuz-<UEK 버전>"
   1009           cmd "sudo reboot"
   1010           cmt "RHCK 를 유지해야 하면 출시 여부 확인: sudo $pm updateinfo list --cve $CVE"
   1011           ;;
   1012       esac ;;
   1013     amazon)
   1014       case $OS_VER in
   1015         2)
   1016           if [ -n "$R_ADV" ]; then
   1017             line "수정 커널: $R_FIXED 이상 ($R_ADV)"
   1018             cmd "sudo yum update --advisory $R_ADV        # 또는: sudo yum update kernel"
   1019           else
   1020             cmd "sudo yum update kernel"
   1021           fi
   1022           cmd "sudo reboot" ;;
   1023         2023)
   1024           if [ -n "$R_ADV" ]; then
   1025             line "수정 커널: $R_FIXED 이상 ($R_ADV)"
   1026             cmd "sudo dnf update $R_PKG --releasever 2023.12.20261012"
   1027             cmt "또는: sudo dnf update --advisory $R_ADV --releasever 2023.12.20261012"
   1028           else
   1029             cmd "sudo dnf update 'kernel*' --releasever latest"
   1030           fi
   1031           cmd "sudo reboot" ;;
   1032         *)
   1033           remedy_eol "Amazon Linux 2023 으로 이전하세요." ;;
   1034       esac
   1035       note "AWS 발표: EC2·Lambda·Fargate 호스트 인프라는 영향 없음. 위 조치는 이 인스턴스 커널 자체에 대한 것입니다."
   1036       ;;
   1037     debian)
   1038       if [ "$st" = eol ]; then
   1039         remedy_eol "bookworm(12) 이상으로 업그레이드하거나 Freexian ELTS 같은 연장 지원을 확인하세요."
   1040         return
   1041       fi
   1042       line "수정 커널: $R_FIXED 이상 ($R_ADV)"
   1043       cmt "1) 보안 저장소(<코드네임>-security)가 켜져 있는지 확인"
   1044       cmd "grep -rhE '^[^#].*security' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null"
   1045       cmt "2) 업데이트 후 재부팅"
   1046       cmd "sudo apt update"
   1047       cmd "sudo apt full-upgrade"
   1048       cmt "커널 메타패키지가 없으면: sudo apt install linux-image-\$(dpkg --print-architecture)"
   1049       case $RUN_DVER in *bpo*)
   1050         cmt "backports 커널 사용 중: sudo apt install -t ${OS_CODENAME:-<코드네임>}-backports linux-image-\$(dpkg --print-architecture)" ;;
   1051       esac
   1052       cmd "sudo reboot"
   1053       cmt "3) 재부팅 후 'uname -v' 의 'Debian <버전>' 이 수정 버전 이상인지 확인"
   1054       ;;
   1055     debianlike)
   1056       line "배포판 커널을 최신으로 업데이트하세요. (수정 기준: 업스트림 ${R_FIXED:-stable} 이상)"
   1057       cmd "sudo apt update && sudo apt full-upgrade"
   1058       cmd "sudo reboot"
   1059       ;;
   1060     ubuntu)
   1061       line "$DATA_DATE 기준 Ubuntu 수정 커널이 출시되지 않았습니다. (모든 커널 'Needs evaluation')"
   1062       cmt "1) 패치 출시 여부 확인 (Ubuntu Pro 클라이언트)"
   1063       cmd "pro fix $CVE --dry-run"
   1064       cmt "또는 https://ubuntu.com/security/$CVE"
   1065       cmt "2) Livepatch 사용 중이면 적용 여부 확인"
   1066       cmd "sudo canonical-livepatch status --verbose | grep -A2 $CVE"
   1067       cmt "3) 출시되면 적용 후 재부팅"
   1068       cmd "sudo apt update && sudo apt full-upgrade"
   1069       cmd "sudo reboot"
   1070       ;;
   1071     proxmox)
   1072       line "Proxmox VE 커널(Ubuntu 커널 기반)의 수정 여부는 Proxmox 공지를 확인해야 합니다."
   1073       cmd "pveversion -v | grep -E 'proxmox-kernel|pve-manager'"
   1074       cmd "apt changelog proxmox-kernel-\$(uname -r)-signed 2>/dev/null | grep -iE '$CVE|checksum_start'"
   1075       cmt "수정 커널이 나오면 (pve-enterprise 또는 pve-no-subscription 저장소)"
   1076       cmd "sudo apt update && sudo apt full-upgrade"
   1077       cmt "게스트를 다른 노드로 마이그레이션하거나 종료한 뒤 재부팅"
   1078       cmd "sudo reboot"
   1079       ;;
   1080     sles|slemicro|leap)
   1081       if [ "$st" = eol ]; then
   1082         if [ "$FAMILY" = leap ]; then remedy_eol "openSUSE Leap 16 으로 업그레이드하세요."
   1083         else remedy_eol "지원(LTSS 포함)되는 서비스팩으로 업그레이드하세요."; fi
   1084         return
   1085       fi
   1086       line "$DATA_DATE 기준 SUSE 수정 커널이 출시되지 않았습니다. (상태 'Pending')"
   1087       cmt "1) 패치 출시 여부 확인"
   1088       cmd "sudo zypper refresh && sudo zypper list-patches --cve=$CVE"
   1089       cmt "2) 출시되면 적용 후 재부팅"
   1090       if [ "$FAMILY" = slemicro ]; then cmd "sudo transactional-update patch"
   1091       else cmd "sudo zypper patch --cve=$CVE"; fi
   1092       cmd "sudo reboot"
   1093       cmt "SLE Live Patching 구독 시 kernel-livepatch 패치로 재부팅 없이 적용 가능"
   1094       ;;
   1095     fedora)
   1096       if [ "$st" = eol ]; then
   1097         line "이 커널 브랜치는 업스트림 지원이 끝났습니다. 커널을 업데이트하고, Fedora 릴리스가 EOL 이면 지원 릴리스로 올리세요."
   1098         cmd "sudo dnf upgrade --refresh"
   1099         cmd "sudo dnf system-upgrade download --releasever=<새 릴리스>"
   1100         return
   1101       fi
   1102       line "수정 기준: 업스트림 $R_FIXED 이상 커널"
   1103       cmd "sudo dnf upgrade --refresh 'kernel*'"
   1104       cmt "stable 저장소에 아직 없으면 updates-testing 에서 설치"
   1105       cmd "sudo dnf upgrade --refresh --enablerepo=updates-testing 'kernel*'"
   1106       cmd "sudo reboot"
   1107       ;;
   1108     arch)
   1109       line "수정 기준: linux ${R_FIXED:-7.2.11} 이상 / linux-lts 6.12.113 · 6.18.57 이상"
   1110       cmd "sudo pacman -Syu"
   1111       cmd "sudo reboot"
   1112       ;;
   1113     tumbleweed)
   1114       line "수정 기준: 업스트림 ${R_FIXED:-stable} 이상"
   1115       cmd "sudo zypper refresh && sudo zypper dup"
   1116       cmd "sudo reboot"
   1117       ;;
   1118     microos)
   1119       cmd "sudo transactional-update dup"
   1120       cmd "sudo reboot"
   1121       ;;
   1122     alpine)
   1123       line "수정 기준: 업스트림 ${R_FIXED:-stable} 이상 (linux-lts / linux-virt)"
   1124       cmd "sudo apk update && sudo apk upgrade"
   1125       cmd "sudo reboot"
   1126       [ "$st" = eol ] && cmt "커널 브랜치가 EOL 이면 Alpine 릴리스 자체를 지원 버전으로 올려야 함"
   1127       ;;
   1128     gentoo)
   1129       line "수정 기준: 업스트림 ${R_FIXED:-stable} 이상"
   1130       cmd "sudo emerge --sync"
   1131       cmd "sudo emerge --ask --update --oneshot sys-kernel/gentoo-kernel-bin   # 또는 gentoo-kernel"
   1132       cmt "gentoo-sources 로 직접 빌드한다면 수정 버전 이상 소스로 다시 빌드"
   1133       cmd "sudo reboot"
   1134       ;;
   1135     wsl)
   1136       line "WSL2 커널은 Microsoft 가 배포합니다. Windows PowerShell 에서 실행하세요:"
   1137       cmd "wsl --update"
   1138       cmd "wsl --shutdown"
   1139       note "WSL2 의 호스트는 Windows(Hyper-V)라서 'VM → 리눅스 호스트 탈출' 시나리오는 해당하지 않습니다. WSL 커널 자체만 업스트림 기준으로 판정했습니다."
   1140       ;;
   1141     *)
   1142       line "배포판 공급사의 보안 공지를 확인하고 커널을 업데이트하세요."
   1143       line "직접 빌드한 커널이라면 업스트림 stable 수정 버전 이상으로 올리세요:"
   1144       line "  $UPSTREAM_TEXT"
   1145       line "  수정 커밋: $FIX_COMMIT ($FIX_URL)"
   1146       ;;
   1147   esac
   1148 }
   1149 
   1150 print_remedy() {  # $1=최종 상태
   1151   local st=$1
   1152   case $st in
   1153     fixed|unaffected)
   1154       line "${C_GRN}추가 조치가 필요하지 않습니다.${C_OFF}"
   1155       [ -n "$VIRT" ] && note "이 시스템은 VM 게스트($VIRT)입니다. 호스트(클라우드/호스팅 사업자) 커널의 패치 여부는 따로 확인하세요."
   1156       return ;;
   1157     livepatched)
   1158       line "라이브패치($LP_NAME)로 실행 중인 커널에 수정이 적용되어 있습니다."
   1159       line "다음 정기 점검 때 수정된 커널 패키지를 설치하고 재부팅하세요."
   1160       return ;;
   1161     reboot)
   1162       line "수정된 커널이 이미 설치되어 있습니다: $REBOOT_TARGET"
   1163       line "재부팅해야 적용됩니다."
   1164       [ "$HV_HOST" = 1 ] && cmt "가상화 호스트입니다. 재부팅 전에 게스트 VM 을 다른 호스트로 옮기거나 종료하세요."
   1165       cmd "sudo reboot"
   1166       cmt "재부팅 후 이 스크립트를 다시 실행해 확인"
   1167       return ;;
   1168   esac
   1169 
   1170   if [ "$R_MODE" = upstream ]; then
   1171     case $FAMILY in
   1172       fedora|arch|tumbleweed|microos|alpine|gentoo|wsl|generic|debianlike) remedy_family "$st" ;;
   1173       debian) FAMILY=debianlike remedy_family "$st" ;;
   1174       *)      remedy_custom ;;
   1175     esac
   1176   else
   1177     remedy_family "$st"
   1178   fi
   1179 
   1180   printf '\n'
   1181   note "공식 우회책(sysctl·모듈 차단 등)은 없습니다. 수정된 커널로 재부팅해야 해결됩니다."
   1182   if [ "$HV_HOST" = 1 ]; then
   1183     note "게스트 OS 커널만 패치해서는 호스트가 보호되지 않습니다. 재부팅 전 게스트를 옮기거나 종료하세요."
   1184   fi
   1185   case $st in pending|eol|unknown)
   1186     note "패치 전까지는 신뢰할 수 없는 워크로드(VM·microVM·샌드박스) 생성을 제한하고 민감한 시스템과 분리하세요. (위험을 줄일 뿐 해결책은 아님)" ;;
   1187   esac
   1188   [ -n "$VIRT" ] && note "이 시스템은 VM 게스트($VIRT)입니다. 호스트(클라우드/호스팅 사업자)의 패치 여부도 확인하세요."
   1189 }
   1190 
   1191 print_refs() {
   1192   line "https://www.cve.org/CVERecord?id=$CVE"
   1193   line "https://nvd.nist.gov/vuln/detail/$CVE"
   1194   line "https://pwn.ai/blog/pwnescape"
   1195   line "https://almalinux.org/blog/2026-10-16-pwnescape-cve-2026-99999/"
   1196   line "https://www.penligent.ai/hackinglabs/cve-2026-99999/"
   1197   line "https://news.hada.io/topic?id=35568"
   1198   case $FAMILY in
   1199     debian|debianlike) line "https://security-tracker.debian.org/tracker/$CVE" ;;
   1200     ubuntu|proxmox)    line "https://ubuntu.com/security/$CVE" ;;
   1201     alma|kitten|rhel|centos|rocky|rhelclone)
   1202                        line "https://access.redhat.com/security/cve/cve-2026-99999" ;;
   1203     oracle)            line "https://linux.oracle.com/cve/$CVE.html" ;;
   1204     amazon)            line "https://explore.alas.aws.amazon.com/$CVE.html" ;;
   1205     sles|slemicro|leap|tumbleweed|microos)
   1206                        line "https://www.suse.com/security/cve/$CVE.html" ;;
   1207     *)                 line "$FIX_URL" ;;
   1208   esac
   1209 }
   1210 
   1211 print_refs_section() {
   1212   section "참고 자료"
   1213   print_refs
   1214   note "판정 데이터 기준일 $DATA_DATE. 벤더 공지는 계속 갱신되므로 최신 공지도 함께 확인하세요."
   1215 }
   1216 
   1217 show_manual() {
   1218   section "조치 방법 (수동)"
   1219   print_remedy "$FINAL"
   1220   print_refs_section
   1221 }
   1222 
   1223 # -----------------------------------------------------------------------------
   1224 # 자동 패치 (메뉴 1번)
   1225 # -----------------------------------------------------------------------------
   1226 run() {
   1227   printf '  %s$ %s%s\n' "$C_DIM" "$*" "$C_OFF"
   1228   "$@"
   1229 }
   1230 
   1231 setup_sudo() {
   1232   SUDO=""
   1233   [ "$(id -u)" = 0 ] && return 0
   1234   if command -v sudo >/dev/null 2>&1; then SUDO=sudo; return 0; fi
   1235   line "${C_RED}root 권한이 필요하지만 sudo 가 없습니다. root 로 다시 실행하세요.${C_OFF}"
   1236   return 1
   1237 }
   1238 
   1239 can_autopatch() {
   1240   case $FAMILY in wsl|wsl1) return 1 ;; esac   # WSL 안에서 reboot 해도 배포판 커널로 부팅되지 않음
   1241   [ "$FINAL" = reboot ] && return 0
   1242   [ "$FINAL" = eol ] && return 1
   1243   case $FAMILY in
   1244     debian|debianlike|fedora|arch|tumbleweed|microos|alpine) return 0 ;;
   1245     alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon|ubuntu|proxmox|sles|slemicro|leap)
   1246       [ "$R_MODE" != upstream ] ;;   # ELRepo·직접 빌드 커널은 자동 패치 대상 아님
   1247     *) return 1 ;;
   1248   esac
   1249 }
   1250 
   1251 # zypper 종료 코드 100~103 은 정보성(업데이트 있음·재부팅 필요 등)이라 성공으로 본다
   1252 zypper_ok() { [ "$1" -eq 0 ] || { [ "$1" -ge 100 ] && [ "$1" -le 103 ]; }; }
   1253 zypper_run() {      # 커널 패키지만 다루는 작업: 확인 없이 진행
   1254   local rc
   1255   run $SUDO zypper --non-interactive "$@"; rc=$?
   1256   zypper_ok "$rc"
   1257 }
   1258 zypper_ask() {      # 시스템 전체 업그레이드(dup): 패키지 제거가 있을 수 있어 사용자가 직접 확인
   1259   local rc
   1260   run $SUDO zypper "$@"; rc=$?
   1261   zypper_ok "$rc"
   1262 }
   1263 
   1264 # 설치된 linux-* 패키지만 올린다 (메타패키지가 새 ABI 커널을 끌어옴).
   1265 #  - --no-remove: 다른 패키지를 지워야 하면 멈춤
   1266 #  - 'apt-get install' 은 지정한 패키지를 수동 설치로 바꾸므로, 원래 자동 설치였던 것은 되돌려
   1267 #    오래된 커널이 'apt autoremove' 로 계속 정리되게 한다
   1268 #  - Proxmox 는 공식 권장대로 dist-upgrade 하되 제거 목록을 사용자가 확인하도록 -y 없이 실행
   1269 patch_apt() {
   1270   local pkgs="" autos="" st pkg a meta=0
   1271   local opts="-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold"
   1272   run $SUDO apt-get update || return 1
   1273   if [ "$FAMILY" = proxmox ]; then
   1274     run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get $opts dist-upgrade
   1275     return
   1276   fi
   1277   while read -r st pkg; do
   1278     case $st in ii*) ;; *) continue ;; esac
   1279     pkgs="$pkgs $pkg"
   1280     case $pkg in linux-image-[a-z]*) meta=1 ;; esac
   1281   done <<EOF
   1282 $(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'linux-*' 2>/dev/null)
   1283 EOF
   1284   if [ -z "$pkgs" ]; then
   1285     line "설치된 linux-* 커널 패키지를 찾지 못했습니다."; return 1
   1286   fi
   1287   while read -r a; do
   1288     case "$pkgs " in *" $a "*) autos="$autos $a" ;; esac
   1289   done <<EOF
   1290 $(apt-mark showauto 2>/dev/null)
   1291 EOF
   1292   run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install --only-upgrade $pkgs || return 1
   1293   [ -n "$autos" ] && run $SUDO apt-mark auto $autos >/dev/null
   1294   # Debian 에서 커널 메타패키지가 없으면 새 ABI 커널이 들어오지 않으므로 설치
   1295   if [ "$FAMILY" = debian ] && [ "$meta" = 0 ]; then
   1296     run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install "linux-image-$(dpkg --print-architecture)"
   1297   fi
   1298 }
   1299 
   1300 # AlmaLinux 공지 방법. testing 저장소는 이번 한 번만 켜고, 끝나면 저장소 패키지를 지워
   1301 # 이후 일반 업데이트에 testing 패키지가 섞이지 않게 한다.
   1302 patch_alma() {
   1303   local rc
   1304   run $SUDO dnf clean metadata && run $SUDO dnf -y update 'kernel*' || return 1
   1305   installed_fix_ready && return 0
   1306   line "정식 저장소에 수정 커널이 아직 없어 almalinux-testing 저장소에서 설치합니다. (AlmaLinux 공지 방법)"
   1307   run $SUDO dnf -y install almalinux-release-testing || return 1
   1308   run $SUDO dnf -y update 'kernel*' --enablerepo=almalinux-testing; rc=$?
   1309   run $SUDO dnf -y remove almalinux-release-testing ||
   1310     note "almalinux-release-testing 을 지우지 못했습니다. testing 저장소가 켜져 있지 않은지 /etc/yum.repos.d/ 를 확인하세요."
   1311   return $rc
   1312 }
   1313 
   1314 # 시스템 전체 업그레이드(pacman -Syu, zypper dup, apk upgrade, transactional-update dup)는
   1315 # 제거·교체되는 패키지가 있을 수 있어 확인 질문을 남겨 둔다. 커널 패키지만 올리는 작업은 -y.
   1316 patch_now() {
   1317   local pm=dnf
   1318   case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
   1319   case $FAMILY in
   1320     alma)       patch_alma ;;
   1321     kitten|rhel|centos|rocky|rhelclone|oracle)
   1322                 run $SUDO $pm -y update 'kernel*' ;;
   1323     amazon)
   1324       if [ "$OS_VER" = 2 ]; then run $SUDO yum -y update kernel
   1325       else run $SUDO dnf -y update "${R_PKG:-kernel}" --releasever=latest; fi ;;
   1326     fedora)     run $SUDO dnf -y upgrade --refresh 'kernel*' ;;
   1327     debian|debianlike|ubuntu|proxmox) patch_apt ;;
   1328     sles|leap)  zypper_run refresh && zypper_run update "kernel-${KREL##*-}" ;;
   1329     slemicro)   run $SUDO transactional-update --non-interactive patch ;;
   1330     microos)    run $SUDO transactional-update --interactive dup ;;
   1331     tumbleweed) zypper_run refresh && zypper_ask dup ;;
   1332     arch)       run $SUDO pacman -Syu ;;
   1333     alpine)     run $SUDO apk -U --interactive upgrade ;;
   1334     *)          return 1 ;;
   1335   esac
   1336 }
   1337 
   1338 # /boot 여유 공간이 부족하면 새 커널·initramfs 생성이 반쯤 실패한 채 기본 부팅 항목만 바뀔 수 있다
   1339 boot_space_ok() {
   1340   local avail
   1341   have df || return 0
   1342   avail=$(df -Pk /boot 2>/dev/null | awk 'NR == 2 { print $4 }')
   1343   case $avail in ''|*[!0-9]*) return 0 ;; esac
   1344   [ "$avail" -ge 102400 ] && return 0
   1345   line "${C_RED}/boot 여유 공간이 $((avail / 1024))MB 뿐이라 새 커널을 넣다 실패할 수 있어 패치를 멈춥니다.${C_OFF}"
   1346   cmt "오래된 커널 정리 예: sudo dnf remove --oldinstallonly kernel   /   sudo apt autoremove --purge"
   1347   return 1
   1348 }
   1349 
   1350 # transactional-update 뒤: 다음 부팅 스냅샷(btrfs 기본 서브볼륨)에 수정 커널이 들어 있는지
   1351 snapshot_fix_ready() {
   1352   local out n root k list="" tab pick
   1353   REBOOT_TARGET=""; REBOOT_KREL=""
   1354   have btrfs || return 1
   1355   out=$($SUDO btrfs subvolume get-default / 2>/dev/null) || return 1
   1356   case $out in */.snapshots/*/snapshot*) ;; *) return 1 ;; esac
   1357   n=${out##*/.snapshots/}; n=${n%%/*}
   1358   case $n in ''|*[!0-9]*) return 1 ;; esac
   1359   root=/.snapshots/$n/snapshot
   1360   if [ "$FAMILY" = slemicro ]; then
   1361     $SUDO rpm --root "$root" -q --changelog "kernel-${KREL##*-}" 2>/dev/null | has_fix_text || return 1
   1362   else
   1363     tab=$(printf '\t')
   1364     for k in $($SUDO ls "$root/usr/lib/modules" 2>/dev/null); do list="$list$k$tab$k
   1365 "; done
   1366     pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode=rpm -v ref="" "$AWK_VERCMP")
   1367     k=${pick%%"$tab"*}
   1368     [ -n "$k" ] || return 1
   1369     upstream_eval "$k"; [ "$E_STATUS" = fixed ] || return 1
   1370   fi
   1371   REBOOT_TARGET="스냅샷 $n"
   1372 }
   1373 
   1374 # 패치 뒤 재부팅하면 수정 커널로 부팅되는지 (제자리 교체 → 다음 부팅 커널 순서로 확인)
   1375 reboot_fix_ready() {
   1376   case $FAMILY in slemicro|microos) snapshot_fix_ready; return ;; esac
   1377   evaluate_kernel "$KREL" "$RUN_DVER"
   1378   if [ "$E_INPLACE" = 1 ]; then
   1379     REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (부팅 뒤 같은 이름으로 교체된 패키지)"
   1380     return 0
   1381   fi
   1382   installed_fix_ready
   1383 }
   1384 
   1385 show_default_kernel() {
   1386   local d
   1387   have grubby || return 0
   1388   d=$($SUDO grubby --default-kernel 2>/dev/null) || return 0
   1389   [ -n "$d" ] && kv "다음 부팅 기본 커널" "$d"
   1390 }
   1391 
   1392 # grubby 가 없고 GRUB_DEFAULT 가 0(첫 항목=최신 커널)이 아니면 다음 부팅 커널을 확신할 수 없다
   1393 grub_default_custom() {
   1394   local l v=""
   1395   have grubby && return 1
   1396   [ -r /etc/default/grub ] || return 1
   1397   while IFS= read -r l; do case $l in GRUB_DEFAULT=*) v=${l#GRUB_DEFAULT=} ;; esac; done < /etc/default/grub
   1398   v=${v#\"}; v=${v%\"}; v=${v#\'}; v=${v%\'}
   1399   case $v in ''|0) return 1 ;; esac
   1400   GRUB_DEFAULT_VAL=$v
   1401 }
   1402 
   1403 do_reboot() {
   1404   local ans grub_custom=0
   1405   grub_default_custom && grub_custom=1
   1406   # 가상화 호스트, 다음 부팅 커널이 불확실한 GRUB 설정, 시간 제한 입력(read -t)이 없는 셸이면
   1407   # 카운트다운 대신 'yes' 를 직접 입력받는다
   1408   if [ "$HV_HOST" = 1 ] || [ "$grub_custom" = 1 ] || [ -z "${BASH_VERSION:-}" ]; then
   1409     if [ "$HV_HOST" = 1 ]; then
   1410       line "${C_YEL}가상화 호스트입니다. 재부팅하면 실행 중인 VM 이 모두 중단됩니다.${C_OFF}"
   1411       line "가능하면 게스트를 다른 호스트로 옮기거나 먼저 종료하세요."
   1412     fi
   1413     if [ "$grub_custom" = 1 ]; then
   1414       line "${C_YEL}GRUB_DEFAULT=$GRUB_DEFAULT_VAL 이라 다음 부팅 커널이 최신 커널이 아닐 수 있습니다. 부팅 메뉴 설정을 확인하세요.${C_OFF}"
   1415     fi
   1416     printf '  지금 재부팅하려면 yes 를 입력하세요: '
   1417     read -r ans || ans=""
   1418     if [ "$ans" != yes ]; then
   1419       line "재부팅을 취소했습니다. 준비되면 'sudo reboot' 를 실행하세요."
   1420       return 1
   1421     fi
   1422   else
   1423     printf '  10초 후 재부팅합니다. 취소하려면 Enter 를 누르세요... '
   1424     # 시간이 다 됐을 때(종료 코드 > 128)만 재부팅. Enter·Ctrl-D(EOF)·오류는 모두 취소.
   1425     read -r -t 10 ans
   1426     if [ $? -le 128 ]; then
   1427       printf '\n'
   1428       line "재부팅을 취소했습니다. 준비되면 'sudo reboot' 를 실행하세요."
   1429       return 1
   1430     fi
   1431     printf '\n'
   1432   fi
   1433   line "재부팅합니다. 부팅 후 이 스크립트를 다시 실행해 결과를 확인하세요."
   1434   run $SUDO reboot
   1435 }
   1436 
   1437 auto_patch() {
   1438   setup_sudo || { show_manual; return; }
   1439   if [ "$FINAL" != reboot ]; then
   1440     section "패치 진행"
   1441     note "커널 패키지를 업데이트합니다. sudo 비밀번호를 물을 수 있습니다."
   1442     boot_space_ok || { show_manual; return; }
   1443     if ! patch_now; then
   1444       line "${C_RED}패키지 업데이트 중 오류가 났습니다. 위 출력을 확인하고 아래 수동 절차로 진행하세요.${C_OFF}"
   1445       show_manual
   1446       return
   1447     fi
   1448     if reboot_fix_ready; then
   1449       FINAL=reboot
   1450       line "${C_GRN}다음 부팅에 수정 커널 확인: $REBOOT_TARGET${C_OFF}"
   1451     else
   1452       line "업데이트는 끝났지만 다음 부팅 커널에서 $CVE 수정을 확인하지 못했습니다. 재부팅하지 않습니다."
   1453       if [ -n "${NB_ALT:-}" ]; then
   1454         line "수정 커널 $NB_ALT 은(는) 설치돼 있지만 기본 부팅 커널이 아닙니다:"
   1455         cmd "sudo grubby --set-default /boot/vmlinuz-$NB_ALT && sudo reboot"
   1456       else
   1457         line "배포판 수정 커널이 아직 나오지 않았을 수 있습니다."
   1458       fi
   1459       show_manual
   1460       return
   1461     fi
   1462   fi
   1463   # 부팅에 필요한 파일이 빠진 커널로 재부팅하지 않는다 (dracut·initramfs 생성 실패 등)
   1464   if [ -n "${REBOOT_KREL:-}" ] && ! boot_files_ok "$REBOOT_KREL"; then
   1465     line "${C_RED}$REBOOT_KREL 의 커널 이미지나 initramfs 가 없어 자동 재부팅을 멈춥니다.${C_OFF}"
   1466     cmt "initramfs 다시 만들기: sudo dracut -f --kver $REBOOT_KREL   /   sudo update-initramfs -u -k $REBOOT_KREL"
   1467     return
   1468   fi
   1469   section "재부팅"
   1470   show_default_kernel
   1471   do_reboot
   1472 }
   1473 
   1474 choose_action() {
   1475   local choice label
   1476   case $FINAL in
   1477     reboot)          label="지금 재부팅 (설치된 수정 커널 적용)" ;;
   1478     pending|unknown) label="지금 업데이트 시도 (수정 커널이 나왔으면 설치 후 재부팅)" ;;
   1479     *)               label="지금 패치 (수정 커널 설치 후 재부팅)" ;;
   1480   esac
   1481   can_autopatch || label="지금 패치 — 이 시스템은 자동 패치를 지원하지 않음"
   1482 
   1483   section "다음 작업 선택"
   1484   if [ -n "$VIRT" ]; then
   1485     line "${C_YEL}이 시스템은 VM 게스트($VIRT)입니다. 게스트를 패치해도 호스트는 보호되지 않으니${C_OFF}"
   1486     line "${C_YEL}호스트 OS 에서도 이 스크립트를 실행해 패치하세요.${C_OFF}"
   1487     printf '\n'
   1488   fi
   1489   line "1. $label"
   1490   line "2. 나중에 수동 패치 (명령어 보기)"
   1491   line "3. 종료"
   1492   while :; do
   1493     printf '\n  선택 [1-3]: '
   1494     read -r choice || { printf '\n'; return; }
   1495     case $choice in
   1496       1)
   1497         if can_autopatch; then
   1498           auto_patch
   1499         else
   1500           line "이 배포판/커널은 자동으로 패치할 수 없습니다. 아래 수동 절차를 따르세요."
   1501           show_manual
   1502         fi
   1503         return ;;
   1504       2) show_manual; return ;;
   1505       3) line "종료합니다."; return ;;
   1506       *) line "1, 2, 3 중 하나를 입력하세요." ;;
   1507     esac
   1508   done
   1509 }
   1510 
   1511 print_header() {
   1512   printf '%s' "$C_BLU"
   1513   printf '==================================================================\n'
   1514   printf ' %s (PwnEscape) 리눅스 커널 취약점 점검\n' "$CVE"
   1515   printf ' IPv4 checksum_start 검증 누락 → 게스트 VM 에서 호스트 탈출 가능\n'
   1516   printf ' CVSS 9.3 Critical · 2.6.27 이후 모든 커널 · 수정 커밋 %s\n' "$FIX_COMMIT"
   1517   printf '==================================================================\n'
   1518   printf '%s' "$C_OFF"
   1519 }
   1520 
   1521 print_table_rows() {  # $1=테이블 이름 $2=행 앞에 붙일 말
   1522   local br cmp disp adv pkg
   1523   while IFS='|' read -r br cmp disp adv pkg; do
   1524     [ -n "$br" ] && line "$2$disp 이상 ($adv)"
   1525   done <<EOF
   1526 $(vendor_table "$1")
   1527 EOF
   1528 }
   1529 
   1530 print_matrix() {
   1531   printf '%s%s 배포판별 영향·수정 현황 (기준일 %s)%s\n' "$C_BLU" "$CVE" "$DATA_DATE" "$C_OFF"
   1532   section "업스트림 커널 (kernel.org)"
   1533   line "영향: 2.6.27 이후 모든 커널 / 수정 커밋 $FIX_COMMIT"
   1534   line "수정: $UPSTREAM_TEXT"
   1535   line "그 밖의 브랜치(5.4, 6.7~6.11, 6.13~6.17, 7.0, 7.1 등)는 EOL — 지원 브랜치로 올려야 함"
   1536   section "AlmaLinux — 수정됨 (2026-10-16, almalinux-testing 저장소)"
   1537   print_table_rows alma8 "8:  "
   1538   print_table_rows alma9 "9:  "
   1539   print_table_rows alma10 "10: "
   1540   line "Kitten 10: 별도 빌드 없이 다음 정기 커널 업데이트에 포함 예정"
   1541   section "Red Hat Enterprise Linux 7 / 8 / 9 / 10 — 영향 받음 · 패치 미출시"
   1542   line "Red Hat CVSS 8.8 (Important), 완화책 없음, kernel / kernel-rt 모두 대상"
   1543   section "Rocky Linux · CentOS Stream · 기타 RHEL 호환 — 공개된 수정 커널 없음"
   1544   line "RHEL 수정 커널 출시 후 반영 예상 (Oracle Linux RHCK 포함)"
   1545   section "Oracle Linux UEK — 수정됨 (2026-10-16)"
   1546   print_table_rows uek ""
   1547   section "Amazon Linux 2 — 수정됨"
   1548   print_table_rows al2 ""
   1549   section "Amazon Linux 2023 — 수정됨"
   1550   print_table_rows al2023 ""
   1551   line "AWS 발표: EC2 · Lambda · Fargate 호스트 인프라는 영향 없음"
   1552   section "Debian"
   1553   line "bookworm (12):  6.1.190-1 이상 (DLA-4841-1, bookworm-security)"
   1554   line "trixie (13):    6.12.113-1 이상 (DSA-6568-1, trixie-security)"
   1555   line "forky / sid:    7.2.11-1 이상"
   1556   line "bullseye (11) 이하: 보안 추적 대상 아님 (EOL)"
   1557   section "Ubuntu — 패치 미출시"
   1558   line "14.04 ~ 26.04 모든 커널 패키지(linux, -aws, -azure, -gcp, -oracle, -hwe 등) 'Needs evaluation'"
   1559   section "SUSE / openSUSE"
   1560   line "SLES 12 SP5 · 15 SP4~SP7 · 16.x, SLE Micro 5.3~5.5 · 6.x, Leap 16: 영향 받음 · 패치 미출시(Pending)"
   1561   line "openSUSE Leap 15.x, SLES 15 SP1~SP3, SLE Micro 5.0~5.2: 지원 종료"
   1562   section "Fedora · Arch · openSUSE Tumbleweed · Alpine · Gentoo 등"
   1563   line "배포판 커널이 업스트림 stable 을 따르므로 위 업스트림 수정 버전 기준"
   1564   printf '\n'
   1565   note "벤더 공지는 계속 갱신됩니다. 최신 정보는 각 배포판 보안 트래커를 확인하세요."
   1566 }
   1567 
   1568 # -----------------------------------------------------------------------------
   1569 main() {
   1570   parse_args "$@"
   1571   setup_colors
   1572   if [ "$LIST_ONLY" = 1 ]; then print_matrix; exit 0; fi
   1573 
   1574   if [ "$OFFLINE" = 0 ]; then
   1575     # uname -r / -v 와 같은 값을 /proc 에서 바로 읽는다 (프로세스 생성 없음)
   1576     if [ -r /proc/sys/kernel/osrelease ] && [ -r /proc/sys/kernel/version ]; then
   1577       read -r KREL < /proc/sys/kernel/osrelease
   1578       read -r KVER < /proc/sys/kernel/version
   1579     else
   1580       [ "$(uname -s)" = Linux ] ||
   1581         die "리눅스에서 실행하세요. (다른 서버 값으로 판정하려면 --uname-r / --uname-v / --os-release)"
   1582       KREL=$(uname -r); KVER=$(uname -v)
   1583     fi
   1584   fi
   1585   RUN_DVER=""
   1586   case $KVER in *" Debian "*) RUN_DVER=${KVER#* Debian }; RUN_DVER=${RUN_DVER%% *} ;; esac
   1587 
   1588   detect_os
   1589   VIRT=""; CTR=""; HV_HOST=0
   1590   [ "$OFFLINE" = 0 ] && detect_virt_container
   1591   case $FAMILY in wsl|wsl1) VIRT="" ;; esac   # WSL 의 호스트는 Windows 라 '리눅스 호스트 패치' 안내 대상 아님
   1592   # Debian 실행 커널 버전은 uname -v 에서만 얻는다. 디스크의 패키지 버전은 부팅 뒤 바뀌었을 수 있어 쓰지 않음.
   1593 
   1594   print_header
   1595   section "시스템 정보"
   1596   kv "배포판" "${OS_NAME:-알 수 없음} [${OS_ID:-?} ${OS_VER:-?}]"
   1597   kv "실행 커널" "$KREL"
   1598   [ -n "$RUN_DVER" ] && kv "Debian 커널 패키지" "$RUN_DVER"
   1599   if [ "$OFFLINE" = 1 ]; then
   1600     kv "점검 모드" "지정값으로 판정 (로컬 검사 생략)"
   1601   elif [ "$FAMILY" = wsl ]; then
   1602     kv "가상화" "WSL2 (Windows Hyper-V 위에서 실행)"
   1603   elif [ -n "$VIRT" ]; then
   1604     kv "가상화" "VM 게스트 ($VIRT)"
   1605   else
   1606     kv "가상화" "물리 서버이거나 확인 불가"
   1607   fi
   1608   kv "판정 방식" "$(family_label)"
   1609 
   1610   if [ -n "$CTR" ]; then
   1611     section "판정 결과"
   1612     kv "상태" "$(status_label unknown)"
   1613     line "컨테이너($CTR) 안에서 실행 중입니다. 컨테이너는 호스트 커널을 공유하므로"
   1614     line "컨테이너 이미지의 배포판 정보로는 판정할 수 없습니다. 호스트에서 다시 실행하세요."
   1615     line "호스트에 직접 접근할 수 없다면 호스트의 값을 지정해 판정할 수 있습니다:"
   1616     cmd "bash $0 --uname-r '<호스트 uname -r>' --uname-v '<호스트 uname -v>' --os-release <호스트 os-release 사본>"
   1617     exit 4
   1618   fi
   1619 
   1620   evaluate_kernel "$KREL" "$RUN_DVER"
   1621   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
   1622   FINAL=$R_STATUS; LP_NAME=""; REBOOT_TARGET=""; REBOOT_KREL=""; NEXT_WARN=""; NB_ALT=""
   1623   # WSL 은 배포판 커널로 부팅하지 않으므로 설치된 커널·라이브패치 검사가 의미 없다
   1624   if [ "$OFFLINE" = 0 ] && [ "$FAMILY" != wsl ] && [ "$FAMILY" != wsl1 ]; then
   1625     case $R_STATUS in
   1626       vulnerable|pending|eol|unknown)
   1627         if livepatch_has_fix; then
   1628           FINAL=livepatched
   1629         elif [ "$R_INPLACE" = 1 ]; then
   1630           FINAL=reboot; REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (부팅 뒤 같은 이름으로 교체된 패키지)"
   1631         elif installed_fix_ready; then
   1632           FINAL=reboot
   1633         fi ;;
   1634       fixed)
   1635         # 지금은 안전해도, 다음 부팅 커널이 수정되지 않았다면 재부팅하는 순간 다시 취약해진다
   1636         next_boot_eval
   1637         if [ -n "$NB_KREL" ]; then
   1638           case $NB_STATUS in fixed|unaffected) ;; *) NEXT_WARN=$NB_KREL ;; esac
   1639         fi ;;
   1640     esac
   1641   fi
   1642 
   1643   section "판정 결과"
   1644   kv "상태" "$(status_label "$FINAL")"
   1645   kv "근거" "$R_BASIS"
   1646   [ -n "$R_FIXED" ] && kv "수정 버전" "$R_FIXED${R_ADV:+ ($R_ADV)}"
   1647   [ "$FINAL" = livepatched ] && kv "라이브패치" "$LP_NAME"
   1648   [ "$FINAL" = reboot ] && kv "다음 부팅 수정 커널" "$REBOOT_TARGET"
   1649   [ -n "$NEXT_WARN" ] &&
   1650     kv "주의" "${C_YEL}다음 부팅 커널 $NEXT_WARN 은(는) 수정되지 않았습니다. 재부팅하면 다시 취약해질 수 있습니다.${C_OFF}"
   1651   if [ "$FINAL" != reboot ] && [ -n "$NB_ALT" ]; then
   1652     kv "참고" "수정 커널 $NB_ALT 이(가) 설치돼 있지만 기본 부팅 커널이 아닙니다 → sudo grubby --set-default /boot/vmlinuz-$NB_ALT"
   1653   fi
   1654 
   1655   if [ "$OFFLINE" = 0 ]; then
   1656     section "노출 위험 요소"
   1657     check_exposure
   1658   fi
   1659 
   1660   case $FINAL in
   1661     fixed|unaffected|livepatched)
   1662       section "조치 방법"
   1663       print_remedy "$FINAL"
   1664       print_refs_section ;;
   1665     *)
   1666       # 입력과 출력이 모두 터미널일 때만 메뉴 (출력을 파일로 돌리면 보이지 않는 질문에서 멈추므로)
   1667       if [ "$OFFLINE" = 0 ] && [ -t 0 ] && [ -t 1 ]; then choose_action; else show_manual; fi ;;
   1668   esac
   1669 
   1670   case $FINAL in
   1671     fixed|unaffected|livepatched) exit 0 ;;
   1672     vulnerable)                   exit 1 ;;
   1673     reboot)                       exit 2 ;;
   1674     pending|eol)                  exit 3 ;;
   1675     *)                            exit 4 ;;
   1676   esac
   1677 }
   1678 
   1679 main "$@"