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