diff --git a/.gitattributes b/.gitattributes
@@ -0,0 +1,3 @@
+# Keep LF line endings so the scripts run on Linux even when checked out on Windows
+* text=auto eol=lf
+*.sh text eol=lf
diff --git a/README.md b/README.md
@@ -0,0 +1,190 @@
+# CVE-2026-99999 Scanner
+
+**English** | [한국어](README_kr.md)
+
+A single shell script that checks whether a Linux system is affected by **CVE-2026-99999 ("PwnEscape")**. It explains the verdict, then either prints the patch steps for that distribution or patches and reboots the system for you.
+
+> The vendor data embedded in the script is current as of **2026-10-20**. Advisories change quickly, so always confirm with your distribution's security tracker.
+
+## The vulnerability
+
+| | |
+|---|---|
+| Component | Linux kernel IPv4 stack (`skb_checksum_help` → `ip_do_fragment`) |
+| Cause | `checksum_start` / `csum_offset` supplied through the virtio-net header or packet sockets is not validated against the IPv4 header. Completing the checksum can then overwrite the already-validated header length (CWE-20). |
+| Impact | Guest VM → host: host memory disclosure, host crash, code execution as root on the host |
+| Severity | CVSS 3.1 **9.3 Critical** (kernel CNA) · 8.8 Important (Red Hat) |
+| Affected | Every kernel since 2.6.27 |
+| Fix | Upstream commit [`6785011f8b16`](https://git.kernel.org/stable/c/6785011f8b16abf40b1e9d8b2e332232b3e68822) "ipv4: validate checksum_start before completing checksum" |
+| Workaround | None published. The only fix is to boot a fixed kernel. |
+
+The main risk is to **virtualization hosts** that run untrusted guests (KVM/QEMU, Firecracker, Proxmox, cloud and VPS hosts). Patching a guest's kernel does not protect the host.
+
+## Files
+
+| File | Description |
+|---|---|
+| `scanner-cve-2026-99999.sh` | The scanner, English output |
+| `scanner-cve-2026-99999_kr.sh` | The same scanner, Korean output |
+
+Both files contain the same logic; only the messages and comments differ.
+
+## Checksums (v1.0.0)
+
+| File | SHA-256 | MD5 |
+|---|---|---|
+| `scanner-cve-2026-99999.sh` | `65c3c226487d5bd80d436c06cc5256304cfa17011be82d8404c54355998801a6` | `8ab38c22ed737986775f6f64f5a800aa` |
+| `scanner-cve-2026-99999_kr.sh` | `05d4af248ab2cab26566c81067ed35dd0a8c966f7fa8459867597c56ac43a805` | `6223f8db3c1a8dec361fe301a7193ff4` |
+
+Verify the file before running it:
+
+```bash
+echo "65c3c226487d5bd80d436c06cc5256304cfa17011be82d8404c54355998801a6 scanner-cve-2026-99999.sh" | sha256sum -c
+echo "8ab38c22ed737986775f6f64f5a800aa scanner-cve-2026-99999.sh" | md5sum -c
+```
+
+These values are for the files in the [v1.0.0 release](https://github.com/sqzer-x/cve-2026-99999-scanner/releases/tag/v1.0.0). Files on later commits may differ.
+
+## Quick start
+
+From the release (recommended):
+
+```bash
+curl -LO https://github.com/sqzer-x/cve-2026-99999-scanner/releases/download/v1.0.0/scanner-cve-2026-99999.sh
+echo "65c3c226487d5bd80d436c06cc5256304cfa17011be82d8404c54355998801a6 scanner-cve-2026-99999.sh" | sha256sum -c
+sudo bash scanner-cve-2026-99999.sh
+```
+
+Or from the repository:
+
+```bash
+git clone https://github.com/sqzer-x/cve-2026-99999-scanner.git
+cd cve-2026-99999-scanner
+sudo bash scanner-cve-2026-99999.sh
+```
+
+Run it on the **host**, not inside a container. Checking works as a regular user; root (through `sudo`) is only needed for menu option 1.
+
+## What it does
+
+1. Detects the distribution (`/etc/os-release`) and the running kernel (`/proc`).
+2. Judges the kernel:
+ - against the fixed versions published by the vendor (AlmaLinux, Oracle UEK, Amazon Linux, Debian), or
+ - against the upstream stable fixed versions (Fedora, Arch, openSUSE Tumbleweed, Alpine, Gentoo, WSL2, ...), or
+ - for distributions that have not released a fix yet (RHEL family, Ubuntu, SUSE), by searching the installed kernel package changelog for the CVE id or the fix commit — so a fix released after 2026-10-20 is still recognized.
+3. Checks for an applied livepatch (kpatch, Canonical Livepatch, SUSE kernel-livepatch, Ksplice), a fixed kernel that is installed but not booted yet, and whether the next boot kernel is fixed.
+4. Reports exposure indicators: `/dev/kvm`, `vhost_net`, TUN/TAP devices, VM processes (QEMU, Firecracker, Proxmox `kvm`, ...), user namespace settings.
+5. If the system is not safe, offers a menu:
+
+ ```
+ 1. Patch now (install the fixed kernel and reboot)
+ 2. Patch manually later (show the commands)
+ 3. Exit
+ ```
+
+ When not run from a terminal (cron, pipes, output redirected to a file), no menu is shown and it behaves like option 2.
+
+## Options
+
+| Option | Description |
+|---|---|
+| `-l`, `--list` | Print only the per-distribution status table |
+| `--no-color` | Disable colored output (`NO_COLOR` is honored too) |
+| `--os-release FILE` | Use this os-release file instead of `/etc/os-release` |
+| `--uname-r STRING` | Judge this kernel release instead of the running one (skips local checks) |
+| `--uname-v STRING` | Kernel build string (`uname -v`), needed for Debian kernels with `--uname-r` |
+| `-h`, `--help` | Show help |
+
+## Exit codes
+
+| Code | Meaning |
+|---|---|
+| 0 | Safe (fixed, not affected, or livepatched) |
+| 1 | Vulnerable — a fixed kernel is available |
+| 2 | Fixed kernel installed — reboot needed |
+| 3 | Vulnerable — no vendor fix yet, or end of life |
+| 4 | Undetermined (unknown kernel, running inside a container, ...) |
+
+The exit codes make it usable from configuration management or monitoring:
+
+```bash
+bash scanner-cve-2026-99999.sh --no-color > report.txt; echo "exit code: $?"
+```
+
+## Checking another server
+
+The verdict can be computed for values collected elsewhere, for example from a container or an inventory:
+
+```bash
+# On the target: uname -r ; uname -v ; cat /etc/os-release > os-release.txt
+bash scanner-cve-2026-99999.sh --uname-r 5.14.0-570.12.1.el9_6.x86_64 --os-release os-release.txt
+bash scanner-cve-2026-99999.sh --uname-r 6.1.0-38-amd64 \
+ --uname-v '#1 SMP PREEMPT_DYNAMIC Debian 6.1.176-1 (2026-08-02)' --os-release os-release.txt
+```
+
+## Status by distribution (as of 2026-10-20)
+
+`bash scanner-cve-2026-99999.sh --list` prints the same table.
+
+| Distribution | Status | Fixed version |
+|---|---|---|
+| Upstream (kernel.org) | Fixed | 5.10.272 · 5.15.223 · 6.1.190 · 6.6.159 · 6.12.113 · 6.18.57 · 7.2.11 · 7.3-rc7. Other branches are end of life. |
+| AlmaLinux 8 / 9 / 10 | Fixed (almalinux-testing repo, 2026-10-16) | `kernel-4.18.0-553.174.2.el8_10` · `kernel-5.14.0-687.57.2.el9_8` · `kernel-6.12.0-211.66.2.el10_2` |
+| AlmaLinux Kitten 10 | Next regular kernel update | — |
+| RHEL 7 / 8 / 9 / 10 | Affected, no fix yet | — |
+| Rocky Linux, CentOS Stream, other RHEL compatibles | No fix published | — |
+| Oracle Linux UEK | Fixed | UEK R6 `5.4.17-2136.360.3.1` (ELSA-2026-500397) · UEK R7 `5.15.0-325.221.6.4` (ELSA-2026-500398) · UEK R8 `6.12.0-207.111.5.4` (ELSA-2026-500399) |
+| Oracle Linux RHCK | No advisory | — |
+| Amazon Linux 2 | Fixed | 4.14 `4.14.355-287.753` (ALAS2-2026-3962) · 5.4 `5.4.302-229.498` (ALAS2KERNEL-5.4-2026-131) · 5.10 `5.10.269-270.1108` (ALAS2KERNEL-5.10-2026-133) · 5.15 `5.15.221-157.262` (ALAS2KERNEL-5.15-2026-117) |
+| Amazon Linux 2023 | Fixed | kernel `6.1.188-233.385` (ALAS2023-2026-3197) · kernel6.12 `6.12.110-135.201` (ALAS2023-2026-3196) · kernel6.18 `6.18.54-121.166` (ALAS2023-2026-3192) |
+| Debian | Fixed | bookworm `6.1.190-1` (DLA-4841-1) · trixie `6.12.113-1` (DSA-6568-1) · forky/sid `7.2.11-1`. bullseye and older are not tracked. |
+| Ubuntu 14.04 – 26.04 | Needs evaluation, no fix yet | — |
+| SLES 12 SP5 / 15 SP4–SP7 / 16.x, SLE Micro, openSUSE Leap 16 | Affected, pending | — |
+| openSUSE Leap 15.x, SLES 15 SP1–SP3 | End of life | — |
+| Fedora, Arch, openSUSE Tumbleweed, Alpine, Gentoo | Follow upstream stable | see the upstream row |
+
+AWS states that the EC2, Lambda and Fargate host infrastructure is not affected; Amazon Linux instances still need their own kernel updated.
+
+## Safety of "Patch now"
+
+- Only kernel packages are updated where possible: `dnf`/`yum update 'kernel*'`, and on Debian/Ubuntu only the installed `linux-*` packages with `--no-remove` (packages that were automatically installed keep that mark, so old kernels are still cleaned up by `apt autoremove`).
+- Full-system upgrade tools keep their own confirmation prompt: `pacman -Syu`, `zypper dup`, `apk upgrade`, `transactional-update dup`, Proxmox `apt-get dist-upgrade`.
+- Patching does not start when `/boot` has less than 100 MB free.
+- It reboots only after confirming that the **next boot kernel** is fixed (the `grubby` default, otherwise the newest installed kernel, or the next btrfs snapshot on SLE Micro / MicroOS) and that its kernel image and initramfs exist.
+- Before rebooting there is a 10-second countdown; Enter or Ctrl-D cancels. Typing `yes` is required instead when VMs are running, when `GRUB_DEFAULT` is not `0`, or when bash is not available.
+- On AlmaLinux the testing repository is used once and its repository package is removed afterwards.
+- It never reboots inside WSL.
+
+## Air-gapped environments
+
+- Detection needs no network access and gives the same result with the network cut off.
+- Option 1 needs a package repository that the server can reach, such as an internal mirror. Without one, the update fails safely: no reboot, and the manual steps are printed.
+
+## Requirements
+
+- Linux with `bash`, `awk`, `sed`, `grep` and coreutils. When started with `sh`, the script re-runs itself under `bash`.
+- Optional, used when present: `rpm` / `dpkg`, `systemd-detect-virt`, `grubby`, `gzip`, `stat`, `df`, `btrfs`.
+
+## Limitations
+
+- The fixed-version tables are a snapshot of vendor advisories as of 2026-10-20.
+- For distributions not listed above, the verdict is estimated from upstream version numbers; vendors may have backported the fix without changing the version.
+- Changelog-based detection relies on the vendor listing the CVE id or the fix commit title in the kernel changelog.
+- On GRUB systems without `grubby`, the newest installed kernel is assumed to boot next (`GRUB_DEFAULT=0`).
+- Test option 1 on a disposable machine of the same distribution before using it in production.
+- This tool is provided as is, without warranty. You are responsible for changes made to your systems.
+
+## References
+
+- CVE record: https://www.cve.org/CVERecord?id=CVE-2026-99999
+- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-99999
+- Researcher write-up: https://pwn.ai/blog/pwnescape
+- AlmaLinux: https://almalinux.org/blog/2026-10-16-pwnescape-cve-2026-99999/
+- Analysis: https://www.penligent.ai/hackinglabs/cve-2026-99999/
+- Summary (Korean): https://news.hada.io/topic?id=35568
+- Debian: https://security-tracker.debian.org/tracker/CVE-2026-99999
+- Ubuntu: https://ubuntu.com/security/CVE-2026-99999
+- Red Hat: https://access.redhat.com/security/cve/cve-2026-99999
+- SUSE: https://www.suse.com/security/cve/CVE-2026-99999.html
+- Amazon Linux: https://explore.alas.aws.amazon.com/CVE-2026-99999.html
+- Oracle Linux: https://linux.oracle.com/cve/CVE-2026-99999.html
diff --git a/README_kr.md b/README_kr.md
@@ -0,0 +1,190 @@
+# CVE-2026-99999 Scanner
+
+[English](README.md) | **한국어**
+
+리눅스 시스템이 **CVE-2026-99999 ("PwnEscape")** 에 영향을 받는지 점검하는 셸 스크립트입니다. 판정 근거를 보여 준 뒤, 그 배포판에 맞는 패치 방법을 출력하거나 직접 패치하고 재부팅합니다.
+
+> 스크립트에 들어 있는 벤더 데이터는 **2026-10-20** 기준입니다. 보안 공지는 빠르게 바뀌므로 각 배포판 보안 트래커도 꼭 확인하세요.
+
+## 취약점 요약
+
+| | |
+|---|---|
+| 구성요소 | 리눅스 커널 IPv4 스택 (`skb_checksum_help` → `ip_do_fragment`) |
+| 원인 | virtio-net 헤더·패킷 소켓이 넘긴 `checksum_start` / `csum_offset` 을 IPv4 헤더와 대조하지 않아, 체크섬을 완료할 때 이미 검증된 헤더 길이를 덮어쓸 수 있음 (CWE-20) |
+| 영향 | 게스트 VM → 호스트: 호스트 메모리 유출, 호스트 크래시, 호스트 root 권한 코드 실행 |
+| 심각도 | CVSS 3.1 **9.3 Critical** (커널 CNA) · 8.8 Important (Red Hat) |
+| 영향 범위 | 2.6.27 이후 모든 커널 |
+| 수정 | 업스트림 커밋 [`6785011f8b16`](https://git.kernel.org/stable/c/6785011f8b16abf40b1e9d8b2e332232b3e68822) "ipv4: validate checksum_start before completing checksum" |
+| 우회책 | 공개된 우회책 없음. 수정된 커널로 부팅하는 것이 유일한 해결책 |
+
+가장 위험한 곳은 신뢰할 수 없는 게스트를 실행하는 **가상화 호스트**(KVM/QEMU, Firecracker, Proxmox, 클라우드·VPS 호스트)입니다. 게스트 커널만 패치해서는 호스트가 보호되지 않습니다.
+
+## 파일
+
+| 파일 | 설명 |
+|---|---|
+| `scanner-cve-2026-99999.sh` | 스캐너, 영어 출력 |
+| `scanner-cve-2026-99999_kr.sh` | 같은 스캐너, 한국어 출력 |
+
+두 파일의 동작은 같고, 출력 문구와 주석만 다릅니다.
+
+## 해시값 (v1.0.0)
+
+| 파일 | SHA-256 | MD5 |
+|---|---|---|
+| `scanner-cve-2026-99999.sh` | `65c3c226487d5bd80d436c06cc5256304cfa17011be82d8404c54355998801a6` | `8ab38c22ed737986775f6f64f5a800aa` |
+| `scanner-cve-2026-99999_kr.sh` | `05d4af248ab2cab26566c81067ed35dd0a8c966f7fa8459867597c56ac43a805` | `6223f8db3c1a8dec361fe301a7193ff4` |
+
+실행 전에 파일이 변조되지 않았는지 확인하세요.
+
+```bash
+echo "05d4af248ab2cab26566c81067ed35dd0a8c966f7fa8459867597c56ac43a805 scanner-cve-2026-99999_kr.sh" | sha256sum -c
+echo "6223f8db3c1a8dec361fe301a7193ff4 scanner-cve-2026-99999_kr.sh" | md5sum -c
+```
+
+이 값은 [v1.0.0 릴리스](https://github.com/sqzer-x/cve-2026-99999-scanner/releases/tag/v1.0.0)의 파일 기준입니다. 이후 커밋의 파일은 값이 다를 수 있습니다.
+
+## 빠른 시작
+
+릴리스에서 받기 (권장):
+
+```bash
+curl -LO https://github.com/sqzer-x/cve-2026-99999-scanner/releases/download/v1.0.0/scanner-cve-2026-99999_kr.sh
+echo "05d4af248ab2cab26566c81067ed35dd0a8c966f7fa8459867597c56ac43a805 scanner-cve-2026-99999_kr.sh" | sha256sum -c
+sudo bash scanner-cve-2026-99999_kr.sh
+```
+
+저장소에서 받기:
+
+```bash
+git clone https://github.com/sqzer-x/cve-2026-99999-scanner.git
+cd cve-2026-99999-scanner
+sudo bash scanner-cve-2026-99999_kr.sh
+```
+
+컨테이너 안이 아니라 **호스트**에서 실행하세요. 점검은 일반 사용자로도 되며, root 권한(`sudo`)은 메뉴 1번에만 필요합니다.
+
+## 동작
+
+1. 배포판(`/etc/os-release`)과 실행 중인 커널(`/proc`)을 확인합니다.
+2. 커널을 판정합니다.
+ - 벤더가 공개한 수정 버전과 비교 (AlmaLinux, Oracle UEK, Amazon Linux, Debian)
+ - 업스트림 stable 수정 버전과 비교 (Fedora, Arch, openSUSE Tumbleweed, Alpine, Gentoo, WSL2 등)
+ - 아직 수정본을 내지 않은 배포판(RHEL 계열, Ubuntu, SUSE)은 설치된 커널 패키지 changelog 에서 CVE 번호나 수정 커밋을 찾습니다. 그래서 2026-10-20 이후 나온 수정본도 알아봅니다.
+3. 라이브패치 적용 여부(kpatch, Canonical Livepatch, SUSE kernel-livepatch, Ksplice), 설치만 되고 아직 부팅하지 않은 수정 커널, 다음 부팅 커널의 수정 여부를 확인합니다.
+4. 노출 위험 요소를 보여 줍니다: `/dev/kvm`, `vhost_net`, TUN/TAP 장치, VM 프로세스(QEMU, Firecracker, Proxmox `kvm` 등), 사용자 네임스페이스 설정.
+5. 안전하지 않으면 메뉴를 띄웁니다.
+
+ ```
+ 1. 지금 패치 (수정 커널 설치 후 재부팅)
+ 2. 나중에 수동 패치 (명령어 보기)
+ 3. 종료
+ ```
+
+ 터미널이 아닌 곳(cron, 파이프, 출력을 파일로 돌린 경우)에서는 메뉴 없이 2번처럼 명령어만 출력합니다.
+
+## 옵션
+
+| 옵션 | 설명 |
+|---|---|
+| `-l`, `--list` | 배포판별 현황표만 출력 |
+| `--no-color` | 색상 없이 출력 (`NO_COLOR` 환경 변수도 지원) |
+| `--os-release 파일` | `/etc/os-release` 대신 지정한 파일 사용 |
+| `--uname-r 문자열` | 실행 중인 커널 대신 지정한 커널 릴리스로 판정 (로컬 검사 생략) |
+| `--uname-v 문자열` | 커널 빌드 문자열(`uname -v`). Debian 커널을 `--uname-r` 로 판정할 때 필요 |
+| `-h`, `--help` | 도움말 |
+
+## 종료 코드
+
+| 코드 | 의미 |
+|---|---|
+| 0 | 안전 (수정됨, 영향 없음, 라이브패치 적용) |
+| 1 | 취약 — 수정 커널이 나와 있음 |
+| 2 | 수정 커널 설치됨 — 재부팅 필요 |
+| 3 | 취약 — 벤더 수정본 미출시 또는 지원 종료 |
+| 4 | 판단 불가 (알 수 없는 커널, 컨테이너 안에서 실행 등) |
+
+종료 코드를 이용해 구성 관리 도구나 모니터링에서 쓸 수 있습니다.
+
+```bash
+bash scanner-cve-2026-99999_kr.sh --no-color > report.txt; echo "종료 코드: $?"
+```
+
+## 다른 서버 판정
+
+다른 곳에서 모은 값으로도 판정할 수 있습니다. 예를 들어 컨테이너 안에 있거나 자산 목록만 가지고 있을 때입니다.
+
+```bash
+# 대상 서버에서: uname -r ; uname -v ; cat /etc/os-release > os-release.txt
+bash scanner-cve-2026-99999_kr.sh --uname-r 5.14.0-570.12.1.el9_6.x86_64 --os-release os-release.txt
+bash scanner-cve-2026-99999_kr.sh --uname-r 6.1.0-38-amd64 \
+ --uname-v '#1 SMP PREEMPT_DYNAMIC Debian 6.1.176-1 (2026-08-02)' --os-release os-release.txt
+```
+
+## 배포판별 현황 (2026-10-20 기준)
+
+`bash scanner-cve-2026-99999_kr.sh --list` 로도 같은 표를 볼 수 있습니다.
+
+| 배포판 | 상태 | 수정 버전 |
+|---|---|---|
+| 업스트림 (kernel.org) | 수정됨 | 5.10.272 · 5.15.223 · 6.1.190 · 6.6.159 · 6.12.113 · 6.18.57 · 7.2.11 · 7.3-rc7. 그 밖의 브랜치는 지원 종료 |
+| AlmaLinux 8 / 9 / 10 | 수정됨 (almalinux-testing 저장소, 2026-10-16) | `kernel-4.18.0-553.174.2.el8_10` · `kernel-5.14.0-687.57.2.el9_8` · `kernel-6.12.0-211.66.2.el10_2` |
+| AlmaLinux Kitten 10 | 다음 정기 커널 업데이트에 포함 예정 | — |
+| RHEL 7 / 8 / 9 / 10 | 영향 받음, 수정본 미출시 | — |
+| Rocky Linux, CentOS Stream, 기타 RHEL 호환 | 공개된 수정본 없음 | — |
+| Oracle Linux UEK | 수정됨 | UEK R6 `5.4.17-2136.360.3.1` (ELSA-2026-500397) · UEK R7 `5.15.0-325.221.6.4` (ELSA-2026-500398) · UEK R8 `6.12.0-207.111.5.4` (ELSA-2026-500399) |
+| Oracle Linux RHCK | 공지 없음 | — |
+| Amazon Linux 2 | 수정됨 | 4.14 `4.14.355-287.753` (ALAS2-2026-3962) · 5.4 `5.4.302-229.498` (ALAS2KERNEL-5.4-2026-131) · 5.10 `5.10.269-270.1108` (ALAS2KERNEL-5.10-2026-133) · 5.15 `5.15.221-157.262` (ALAS2KERNEL-5.15-2026-117) |
+| Amazon Linux 2023 | 수정됨 | kernel `6.1.188-233.385` (ALAS2023-2026-3197) · kernel6.12 `6.12.110-135.201` (ALAS2023-2026-3196) · kernel6.18 `6.18.54-121.166` (ALAS2023-2026-3192) |
+| Debian | 수정됨 | bookworm `6.1.190-1` (DLA-4841-1) · trixie `6.12.113-1` (DSA-6568-1) · forky/sid `7.2.11-1`. bullseye 이하는 추적 대상 아님 |
+| Ubuntu 14.04 – 26.04 | Needs evaluation, 수정본 미출시 | — |
+| SLES 12 SP5 / 15 SP4–SP7 / 16.x, SLE Micro, openSUSE Leap 16 | 영향 받음, Pending | — |
+| openSUSE Leap 15.x, SLES 15 SP1–SP3 | 지원 종료 | — |
+| Fedora, Arch, openSUSE Tumbleweed, Alpine, Gentoo | 업스트림 stable 을 따름 | 업스트림 행 참고 |
+
+AWS 는 EC2·Lambda·Fargate 호스트 인프라가 영향을 받지 않는다고 발표했습니다. 다만 Amazon Linux 인스턴스 자체의 커널은 따로 업데이트해야 합니다.
+
+## "지금 패치"의 안전장치
+
+- 가능하면 커널 패키지만 올립니다. `dnf`/`yum update 'kernel*'`, Debian/Ubuntu 에서는 설치된 `linux-*` 패키지만 `--no-remove` 로 올립니다. 원래 자동 설치였던 패키지는 그 표시를 유지하므로 오래된 커널은 계속 `apt autoremove` 로 정리됩니다.
+- 시스템 전체 업그레이드 도구는 자체 확인 질문을 그대로 둡니다: `pacman -Syu`, `zypper dup`, `apk upgrade`, `transactional-update dup`, Proxmox `apt-get dist-upgrade`.
+- `/boot` 여유 공간이 100MB 미만이면 패치를 시작하지 않습니다.
+- **다음 부팅 커널**이 수정본이고(`grubby` 기본값, 없으면 가장 최신 설치 커널, SLE Micro / MicroOS 는 다음 btrfs 스냅샷) 그 커널 이미지와 initramfs 가 있을 때만 재부팅합니다.
+- 재부팅 전 10초 카운트다운이 있고 Enter 나 Ctrl-D 로 취소됩니다. VM 이 실행 중이거나, `GRUB_DEFAULT` 가 `0` 이 아니거나, bash 가 없으면 카운트다운 대신 `yes` 를 입력해야 합니다.
+- AlmaLinux testing 저장소는 한 번만 쓰고 끝나면 저장소 패키지를 지웁니다.
+- WSL 안에서는 재부팅하지 않습니다.
+
+## 에어갭 환경
+
+- 점검(판정)에는 네트워크가 필요 없고, 네트워크를 끊어도 결과가 같습니다.
+- 1번(지금 패치)은 서버에서 접근할 수 있는 패키지 저장소(내부 미러 등)가 있어야 합니다. 없으면 업데이트가 안전하게 실패하고, 재부팅 없이 수동 절차를 보여 줍니다.
+
+## 요구 사항
+
+- `bash`, `awk`, `sed`, `grep`, coreutils 가 있는 리눅스. `sh` 로 실행해도 스스로 `bash` 로 다시 실행합니다.
+- 있으면 사용: `rpm` / `dpkg`, `systemd-detect-virt`, `grubby`, `gzip`, `stat`, `df`, `btrfs`.
+
+## 한계
+
+- 수정 버전 표는 2026-10-20 시점 벤더 공지를 옮긴 것입니다.
+- 위 표에 없는 배포판은 업스트림 버전 번호로 추정합니다. 벤더가 버전을 바꾸지 않고 수정을 백포트했을 수 있습니다.
+- changelog 기반 판정은 벤더가 커널 changelog 에 CVE 번호나 수정 커밋 제목을 적어 두는 것을 전제로 합니다.
+- `grubby` 가 없는 GRUB 시스템은 가장 최신 설치 커널이 다음에 부팅된다고 가정합니다 (`GRUB_DEFAULT=0`).
+- 운영 서버에서 1번을 쓰기 전에 같은 배포판의 테스트용 머신에서 먼저 실행해 보세요.
+- 이 도구는 어떠한 보증 없이 "있는 그대로" 제공됩니다. 시스템 변경에 대한 책임은 사용자에게 있습니다.
+
+## 참고 자료
+
+- CVE 레코드: https://www.cve.org/CVERecord?id=CVE-2026-99999
+- NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-99999
+- 연구자 글: https://pwn.ai/blog/pwnescape
+- AlmaLinux: https://almalinux.org/blog/2026-10-16-pwnescape-cve-2026-99999/
+- 분석: https://www.penligent.ai/hackinglabs/cve-2026-99999/
+- 요약(한국어): https://news.hada.io/topic?id=35568
+- Debian: https://security-tracker.debian.org/tracker/CVE-2026-99999
+- Ubuntu: https://ubuntu.com/security/CVE-2026-99999
+- Red Hat: https://access.redhat.com/security/cve/cve-2026-99999
+- SUSE: https://www.suse.com/security/cve/CVE-2026-99999.html
+- Amazon Linux: https://explore.alas.aws.amazon.com/CVE-2026-99999.html
+- Oracle Linux: https://linux.oracle.com/cve/CVE-2026-99999.html
diff --git a/scanner-cve-2026-99999.sh b/scanner-cve-2026-99999.sh
@@ -0,0 +1,1684 @@
+#!/usr/bin/env bash
+# =============================================================================
+# scanner-cve-2026-99999.sh (English output · Korean edition: scanner-cve-2026-99999_kr.sh)
+# CVE-2026-99999 "PwnEscape" — Linux kernel exposure check and patch guidance
+# -----------------------------------------------------------------------------
+# Component : kernel IPv4 stack (skb_checksum_help → ip_do_fragment)
+# Cause : checksum_start/csum_offset supplied through the virtio-net header or
+# packet sockets is not validated against the IPv4 header, so completing
+# the checksum can overwrite an already-validated header (IHL) (CWE-20)
+# Impact : guest VM → host memory disclosure · host crash · root code execution on host
+# Score : CVSS 3.1 9.3 Critical (kernel CNA) / 8.8 Important (Red Hat)
+# Introduced: Linux 2.6.27 (f43798c27684)
+# Fixed by : 6785011f8b16 "ipv4: validate checksum_start before completing checksum"
+# Workaround: none published — boot a fixed kernel
+#
+# Verdicts use vendor advisories as of 2026-10-20. Fixes released later are picked up
+# by searching the installed kernel package changelog for the CVE / fix commit.
+#
+# Usage : bash scanner-cve-2026-99999.sh [-l] [--no-color] [...] (see -h)
+# Menu after the verdict: 1) patch now (update kernel → reboot)
+# 2) patch manually later (print commands) 3) exit
+# When not run from a terminal (cron, pipes) it behaves like option 2
+# Exit codes: 0 safe / 1 vulnerable (fix available) / 2 fixed kernel installed, reboot needed
+# 3 vulnerable (no vendor fix yet or end of life) / 4 undetermined
+# =============================================================================
+
+# If started with sh/dash, re-run under bash when available (uses bash features such as read -t)
+if [ -z "${BASH_VERSION:-}" ] && [ -f "$0" ] && command -v bash >/dev/null 2>&1; then
+ exec bash "$0" "$@"
+fi
+
+set -u
+
+CVE="CVE-2026-99999"
+FIX_TITLE="ipv4: validate checksum_start before completing checksum"
+FIX_COMMIT="6785011f8b16"
+FIX_URL="https://git.kernel.org/stable/c/6785011f8b16abf40b1e9d8b2e332232b3e68822"
+DATA_DATE="2026-10-20"
+# Changelog entries written before this year cannot contain the fix (CVE reserved 2026-09,
+# fix committed 2026-10). Changelogs list the newest entry first, so reading stops there.
+CHANGELOG_CUTOFF_YEAR=2026
+
+# First fixed release per upstream stable branch (kernel CNA, 2026-10-15). Mainline: 7.3-rc7.
+UPSTREAM_FIXED="5.10.272 5.15.223 6.1.190 6.6.159 6.12.113 6.18.57 7.2.11"
+UPSTREAM_TEXT="5.10.272 · 5.15.223 · 6.1.190 · 6.6.159 · 6.12.113 · 6.18.57 · 7.2.11 · 7.3-rc7 or later"
+
+OS_RELEASE_FILE=/etc/os-release
+OSR_GIVEN=0
+KREL=""
+KVER=""
+OFFLINE=0
+USE_COLOR=1
+LIST_ONLY=0
+SUDO=""
+OS_MAJ_NUM=0
+# Define global state up front so every code path is safe under set -u
+RUN_DVER=""; VIRT=""; CTR=""; HV_HOST=0; FINAL=""; R_MODE=""; R_PKG=""; R_ADV=""; R_FIXED=""
+REBOOT_TARGET=""; REBOOT_KREL=""; NB_ALT=""; NEXT_WARN=""; LP_NAME=""; E_INPLACE=0
+
+# -----------------------------------------------------------------------------
+# Fixed kernels per vendor
+# Format: branch|version-release to compare (no dist tag)|fixed package|advisory|package name
+# -----------------------------------------------------------------------------
+vendor_table() {
+ case $1 in
+ alma8) echo '4.18|4.18.0-553.174.2|kernel-4.18.0-553.174.2.el8_10|AlmaLinux advisory 2026-10-16, testing repo|kernel' ;;
+ alma9) echo '5.14|5.14.0-687.57.2|kernel-5.14.0-687.57.2.el9_8|AlmaLinux advisory 2026-10-16, testing repo|kernel' ;;
+ alma10) echo '6.12|6.12.0-211.66.2|kernel-6.12.0-211.66.2.el10_2|AlmaLinux advisory 2026-10-16, testing repo|kernel' ;;
+ uek)
+ cat <<'EOF'
+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
+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
+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
+EOF
+ ;;
+ al2)
+ cat <<'EOF'
+4.14|4.14.355-287.753|kernel-4.14.355-287.753.amzn2|ALAS2-2026-3962|kernel
+5.4|5.4.302-229.498|kernel-5.4.302-229.498.amzn2|ALAS2KERNEL-5.4-2026-131|kernel
+5.10|5.10.269-270.1108|kernel-5.10.269-270.1108.amzn2|ALAS2KERNEL-5.10-2026-133|kernel
+5.15|5.15.221-157.262|kernel-5.15.221-157.262.amzn2|ALAS2KERNEL-5.15-2026-117|kernel
+EOF
+ ;;
+ al2023)
+ cat <<'EOF'
+6.1|6.1.188-233.385|kernel-6.1.188-233.385.amzn2023|ALAS2023-2026-3197|kernel
+6.12|6.12.110-135.201|kernel6.12-6.12.110-135.201.amzn2023|ALAS2023-2026-3196|kernel6.12
+6.18|6.18.54-121.166|kernel6.18-6.18.54-121.166.amzn2023|ALAS2023-2026-3192|kernel6.18
+EOF
+ ;;
+ esac
+}
+
+# -----------------------------------------------------------------------------
+# Output helpers
+# -----------------------------------------------------------------------------
+setup_colors() {
+ local e
+ if [ "$USE_COLOR" = 1 ] && [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ e=$(printf '\033')
+ C_RED="$e[1;31m"; C_GRN="$e[1;32m"; C_YEL="$e[1;33m"
+ C_BLU="$e[1;36m"; C_DIM="$e[2m"; C_OFF="$e[0m"
+ else
+ C_RED=; C_GRN=; C_YEL=; C_BLU=; C_DIM=; C_OFF=
+ fi
+}
+section() { printf '\n%s■ %s%s\n' "$C_BLU" "$1" "$C_OFF"; }
+kv() { printf ' %s: %s\n' "$1" "$2"; }
+line() { printf ' %s\n' "$*"; }
+note() { printf ' %s* %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
+cmd() { printf ' %s%s%s\n' "$C_GRN" "$*" "$C_OFF"; }
+cmt() { printf ' %s# %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
+die() { printf 'Error: %s\n' "$*" >&2; exit 4; }
+
+# Look a command up in PATH once and cache the result
+have() {
+ local v="HAVE_${1//[!A-Za-z0-9]/_}"
+ eval "case \${$v-} in 1) return 0 ;; 0) return 1 ;; esac"
+ if command -v "$1" >/dev/null 2>&1; then eval "$v=1"; return 0; fi
+ eval "$v=0"; return 1
+}
+
+# True if stdin (a newest-first changelog) records the fix.
+# - the CVE id (not adjacent to other letters/digits) or the "ipv4: validate checksum_start ..." title
+# - lines containing "revert" never count as the fix
+# - stops at the first entry date (rpm "* Mon Oct 13 2026 ..." / deb " -- Name Mon, 13 Oct 2026 ...")
+# older than CHANGELOG_CUTOFF_YEAR → only the recent part of a multi-MB changelog is read
+# It exits as soon as it matches or stops, so the upstream rpm/gzip also stops (SIGPIPE).
+has_fix_text() {
+ LC_ALL=C awk -v cve="$CVE" -v title="$FIX_TITLE" -v cutoff="$CHANGELOG_CUTOFF_YEAR" '
+ BEGIN { cve = tolower(cve); title = tolower(title); found = 0 }
+ {
+ l = tolower($0)
+ if ((substr(l, 1, 2) == "* " || substr(l, 1, 4) == " -- ") &&
+ match(l, / (19|20)[0-9][0-9]( |$)/)) {
+ if (substr(l, RSTART + 1, 4) + 0 < cutoff) exit
+ }
+ if (index(l, "revert")) next
+ p = index(l, cve)
+ if (p) {
+ a = (p > 1) ? substr(l, p - 1, 1) : ""
+ b = substr(l, p + length(cve), 1)
+ if (a !~ /[0-9a-z]/ && b !~ /[0-9a-z]/) { found = 1; exit }
+ }
+ if (index(l, title)) { found = 1; exit }
+ }
+ END { exit found ? 0 : 1 }'
+}
+
+usage() {
+ cat <<EOF
+Usage: $0 [options]
+
+Checks this system's distribution and running kernel for $CVE (PwnEscape).
+If it is vulnerable, choose one of:
+
+ 1. Patch now update the kernel with the distro package manager and reboot
+ 2. Patch manually later only print the commands to run
+ 3. Exit
+
+Checking works as a regular user. Option 1 needs root and uses sudo.
+When not run from a terminal (cron, pipes) no menu is shown; it prints the commands like option 2.
+
+Options:
+ -l, --list print only the per-distribution fixed-version table
+ --no-color disable colored output
+ --os-release FILE use this os-release file (default: /etc/os-release)
+ --uname-r STRING kernel release to judge (e.g. 'uname -r' from another server)
+ --uname-v STRING kernel build string ('uname -v', used for Debian)
+ -h, --help show this help
+
+ With --uname-r, local checks of this system (changelog, installed kernels,
+ livepatch, virtualization host) are skipped.
+
+Exit codes:
+ 0 safe 1 vulnerable (fix available) 2 fixed kernel installed, reboot needed
+ 3 vulnerable (no vendor fix yet or end of life) 4 undetermined
+EOF
+}
+
+parse_args() {
+ while [ $# -gt 0 ]; do
+ case $1 in
+ -l|--list) LIST_ONLY=1 ;;
+ --no-color) USE_COLOR=0 ;;
+ --os-release) [ $# -ge 2 ] || die "--os-release requires a file path"
+ OS_RELEASE_FILE=$2; OSR_GIVEN=1; shift
+ [ -r "$OS_RELEASE_FILE" ] || die "cannot read os-release file: $OS_RELEASE_FILE" ;;
+ --uname-r) [ $# -ge 2 ] || die "--uname-r requires a value"; KREL=$2; OFFLINE=1; shift ;;
+ --uname-v) [ $# -ge 2 ] || die "--uname-v requires a value"; KVER=$2; shift ;;
+ -h|--help) usage; exit 0 ;;
+ *) usage >&2; exit 4 ;;
+ esac
+ shift
+ done
+ # Mixing the real kernel release with a supplied build string would skew the verdict
+ if [ -n "$KVER" ] && [ "$OFFLINE" = 0 ]; then
+ die "--uname-v must be used together with --uname-r"
+ fi
+}
+
+# -----------------------------------------------------------------------------
+# Version comparison (rpm: rpmvercmp, deb: dpkg rules). deb uses dpkg when available.
+# -----------------------------------------------------------------------------
+AWK_VERCMP='
+function numcmp(x, y) {
+ sub(/^0+/, "", x); sub(/^0+/, "", y)
+ if (length(x) != length(y)) return (length(x) < length(y)) ? -1 : 1
+ if (x == y) return 0
+ return (x < y) ? -1 : 1
+}
+function splitvr(s, i) {
+ V = s; R = ""
+ for (i = length(s); i > 0; i--)
+ if (substr(s, i, 1) == "-") { V = substr(s, 1, i - 1); R = substr(s, i + 1); return }
+}
+function epoch(s) {
+ E = 0; S = s
+ if (match(s, /^[0-9]+:/)) { E = substr(s, 1, RLENGTH - 1); S = substr(s, RLENGTH + 1) }
+}
+function rpmvercmp(a, b, sa, sb, r) {
+ if (a == b) return 0
+ while (a != "" || b != "") {
+ sub(/^[^A-Za-z0-9~^]+/, "", a); sub(/^[^A-Za-z0-9~^]+/, "", b)
+ if (substr(a, 1, 1) == "~" || substr(b, 1, 1) == "~") {
+ if (substr(a, 1, 1) != "~") return 1
+ if (substr(b, 1, 1) != "~") return -1
+ a = substr(a, 2); b = substr(b, 2); continue
+ }
+ if (substr(a, 1, 1) == "^" || substr(b, 1, 1) == "^") {
+ if (a == "") return -1
+ if (b == "") return 1
+ if (substr(a, 1, 1) != "^") return 1
+ if (substr(b, 1, 1) != "^") return -1
+ a = substr(a, 2); b = substr(b, 2); continue
+ }
+ if (a == "" || b == "") break
+ if (substr(a, 1, 1) ~ /[0-9]/) {
+ match(a, /^[0-9]+/); sa = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1)
+ if (!match(b, /^[0-9]+/)) return 1
+ sb = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1)
+ r = numcmp(sa, sb)
+ } else {
+ match(a, /^[A-Za-z]+/); sa = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1)
+ if (!match(b, /^[A-Za-z]+/)) return -1
+ sb = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1)
+ r = (sa < sb) ? -1 : (sa > sb) ? 1 : 0
+ }
+ if (r) return r
+ }
+ if (a == "" && b == "") return 0
+ return (a == "") ? -1 : 1
+}
+function rpmevr(a, b, ea, eb, va, vb, ra, rb, r) {
+ epoch(a); ea = E; a = S
+ epoch(b); eb = E; b = S
+ r = numcmp(ea, eb); if (r) return r
+ splitvr(a); va = V; ra = R
+ splitvr(b); vb = V; rb = R
+ r = rpmvercmp(va, vb); if (r) return r
+ return rpmvercmp(ra, rb)
+}
+function dorder(c) {
+ if (c == "") return 0
+ if (c == "~") return -1
+ if (c ~ /[A-Za-z]/) return index(ASCII, c) + 31
+ return index(ASCII, c) + 31 + 256
+}
+function dvercmp(a, b, ca, cb, oa, ob, da, db, r) {
+ while (a != "" || b != "") {
+ while ((a != "" && substr(a, 1, 1) !~ /[0-9]/) || (b != "" && substr(b, 1, 1) !~ /[0-9]/)) {
+ ca = substr(a, 1, 1); if (ca ~ /[0-9]/) ca = ""
+ cb = substr(b, 1, 1); if (cb ~ /[0-9]/) cb = ""
+ oa = dorder(ca); ob = dorder(cb)
+ if (oa != ob) return (oa < ob) ? -1 : 1
+ a = substr(a, 2); b = substr(b, 2)
+ }
+ da = ""; db = ""
+ if (match(a, /^[0-9]+/)) { da = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1) }
+ if (match(b, /^[0-9]+/)) { db = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1) }
+ r = numcmp(da, db); if (r) return r
+ }
+ return 0
+}
+function debcmp(a, b, ea, eb, ua, ub, ra, rb, r) {
+ epoch(a); ea = E; a = S
+ epoch(b); eb = E; b = S
+ r = numcmp(ea, eb); if (r) return r
+ splitvr(a); ua = V; ra = R
+ splitvr(b); ub = V; rb = R
+ r = dvercmp(ua, ub); if (r) return r
+ return dvercmp(ra, rb)
+}
+function cmp(x, y) { return (mode == "deb") ? debcmp(x, y) : rpmevr(x, y) }
+BEGIN {
+ for (i = 32; i < 127; i++) ASCII = ASCII sprintf("%c", i)
+ if (op != "pick") { print cmp(a, b); exit }
+}
+# op=pick: of the "key<TAB>..." lines, print the one with the largest key above ref (one pass, O(N))
+{
+ k = $0; sub(/\t.*/, "", k)
+ if (cmp(k, ref) > 0 && (best == "" || cmp(k, best) > 0)) { best = k; bestline = $0 }
+}
+END { if (op == "pick" && bestline != "") print bestline }'
+
+ver_cmp() { # $1=rpm|deb $2 $3 → -1 / 0 / 1
+ if [ "$1" = deb ] && have dpkg; then
+ if dpkg --compare-versions "$2" lt "$3"; then echo -1
+ elif dpkg --compare-versions "$2" gt "$3"; then echo 1
+ else echo 0; fi
+ return
+ fi
+ LC_ALL=C awk -v mode="$1" -v a="$2" -v b="$3" "$AWK_VERCMP"
+}
+ver_lt() { [ "$(ver_cmp "$1" "$2" "$3")" = "-1" ]; }
+
+# "4.18.0-553.1.1" → BRANCH=4.18 (no subprocess)
+branch_of() {
+ local v=${1%%[!0-9.]*} maj rest
+ BRANCH=""
+ maj=${v%%.*}; rest=${v#*.}
+ [ -n "$maj" ] && [ "$rest" != "$v" ] || return 0
+ rest=${rest%%.*}
+ [ -n "$rest" ] && BRANCH="$maj.$rest"
+}
+
+# "4.18.0-553.1.1.el8_10.x86_64+debug" → EVR=4.18.0-553.1.1 (no subprocess)
+rpm_evr_of_krel() {
+ local k=${1%%+*}
+ case $k in
+ *.x86_64|*.aarch64|*.ppc64le|*.ppc64|*.s390x|*.i[3-6]86|*.armv7hl|*.armv7l|*.riscv64|*.noarch) k=${k%.*} ;;
+ esac
+ case ${k##*.} in el[0-9]*|amzn[0-9]*|fc[0-9]*) k=${k%.*} ;; esac
+ EVR=$k
+}
+
+# -----------------------------------------------------------------------------
+# System information
+# -----------------------------------------------------------------------------
+# Read os-release in one pass (handles quotes and CRLF, no subprocess)
+read_os_release() {
+ local key val cr
+ OS_ID=""; OS_LIKE=""; OS_VER=""; OS_NAME=""; OS_CODENAME=""
+ [ -r "$OS_RELEASE_FILE" ] || OS_RELEASE_FILE=/usr/lib/os-release
+ if [ ! -r "$OS_RELEASE_FILE" ]; then
+ # Old RHEL family (5/6) without os-release: "CentOS release 6.10 (Final)"
+ if [ "$OFFLINE" = 0 ] && [ -r /etc/redhat-release ]; then
+ read -r OS_NAME < /etc/redhat-release
+ OS_ID=rhel
+ case $OS_NAME in *"release "[0-9]*) val=${OS_NAME#*release }; OS_VER=${val%% *} ;; esac
+ fi
+ return 0
+ fi
+ cr=$(printf '\r')
+ local pretty="" name=""
+ while IFS='=' read -r key val || [ -n "$key" ]; do
+ val=${val%"$cr"}
+ case $val in
+ \"*\") val=${val#\"}; val=${val%\"} ;;
+ \'*\') val=${val#\'}; val=${val%\'} ;;
+ esac
+ case $key in
+ ID) OS_ID=$val ;;
+ ID_LIKE) OS_LIKE=$val ;;
+ VERSION_ID) OS_VER=$val ;;
+ PRETTY_NAME) pretty=$val ;;
+ NAME) name=$val ;;
+ VERSION_CODENAME) OS_CODENAME=$val ;;
+ esac
+ done < "$OS_RELEASE_FILE"
+ OS_NAME=${pretty:-$name}
+}
+
+detect_os() {
+ read_os_release
+ OS_MAJOR=${OS_VER%%.*}
+ case $OS_MAJOR in ''|*[!0-9]*) OS_MAJ_NUM=0 ;; *) OS_MAJ_NUM=$OS_MAJOR ;; esac
+
+ FAMILY=generic
+ case $KREL in
+ *-Microsoft) FAMILY=wsl1; return ;; # WSL1: a Windows compatibility layer, not a Linux kernel
+ *[Mm]icrosoft*) FAMILY=wsl; return ;;
+ *-pve) FAMILY=proxmox; return ;;
+ esac
+ case $OS_ID in
+ almalinux) case $OS_NAME in *Kitten*) FAMILY=kitten ;; *) FAMILY=alma ;; esac ;;
+ rhel) FAMILY=rhel ;;
+ centos) FAMILY=centos ;;
+ rocky) FAMILY=rocky ;;
+ ol) FAMILY=oracle ;;
+ amzn) FAMILY=amazon ;;
+ fedora) FAMILY=fedora ;;
+ debian) FAMILY=debian ;;
+ ubuntu) FAMILY=ubuntu ;;
+ sles|sled|sles_sap|sle_hpc) FAMILY=sles ;;
+ sle-micro|sl-micro) FAMILY=slemicro ;;
+ opensuse-leap) FAMILY=leap ;;
+ opensuse-tumbleweed|opensuse-slowroll) FAMILY=tumbleweed ;;
+ opensuse-microos|opensuse-aeon|opensuse-kalpa) FAMILY=microos ;;
+ arch|archarm|manjaro|manjaro-arm|endeavouros|cachyos|garuda|artix) FAMILY=arch ;;
+ alpine|postmarketos) FAMILY=alpine ;;
+ gentoo) FAMILY=gentoo ;;
+ *)
+ case " $OS_LIKE " in
+ *" ubuntu "*) FAMILY=ubuntu ;;
+ *" rhel "*|*" centos "*|*" fedora "*)
+ case $KREL in *.el[0-9]*) FAMILY=rhelclone ;; esac ;;
+ *" arch "*) FAMILY=arch ;;
+ *" debian "*) FAMILY=debianlike ;;
+ esac ;;
+ esac
+}
+
+family_label() {
+ case $FAMILY in
+ alma) echo "compared with fixed kernels in the AlmaLinux advisory (2026-10-16)" ;;
+ kitten) echo "AlmaLinux Kitten — fix due in the next regular kernel update" ;;
+ rhel) echo "Red Hat advisory (no fix yet) + kernel changelog check" ;;
+ centos|rocky|rhelclone) echo "RHEL-family advisory + kernel changelog check" ;;
+ oracle) echo "compared with fixed kernels in Oracle Linux ELSA advisories (UEK)" ;;
+ amazon) echo "compared with fixed kernels in Amazon Linux ALAS advisories" ;;
+ debian) echo "compared with fixed package versions in the Debian security tracker" ;;
+ debianlike) echo "Debian derivative — Debian rules for Debian kernels, upstream rules otherwise" ;;
+ ubuntu) echo "Ubuntu CVE tracker (no fix yet) + kernel changelog check" ;;
+ proxmox) echo "Proxmox VE kernel — kernel changelog check" ;;
+ sles|slemicro|leap) echo "SUSE security advisory (no fix yet) + kernel changelog check" ;;
+ fedora|arch|tumbleweed|microos|alpine|gentoo) echo "distribution tracks upstream stable — compared with upstream fixed versions" ;;
+ wsl) echo "WSL2 (Microsoft kernel) — compared with upstream fixed versions" ;;
+ wsl1) echo "WSL1 — no Linux kernel involved" ;;
+ *) echo "unknown distribution — compared with upstream fixed versions (indicative only)" ;;
+ esac
+}
+
+# Detect virtualization (VIRT) and containers (CTR) with a single systemd-detect-virt call
+detect_virt_container() {
+ local v=""
+ VIRT=""; CTR=""
+ if [ -f /.dockerenv ]; then CTR=docker
+ elif [ -f /run/.containerenv ]; then CTR=podman
+ elif [ -s /run/systemd/container ]; then read -r CTR < /run/systemd/container
+ elif [ -n "${KUBERNETES_SERVICE_HOST:-}" ]; then CTR=kubernetes
+ elif [ -r /proc/1/environ ] && tr '\0' '\n' < /proc/1/environ 2>/dev/null | grep -q '^container='; then
+ CTR=container
+ fi
+ if have systemd-detect-virt; then
+ v=$(systemd-detect-virt 2>/dev/null) # inside a container the container type is printed first
+ case $v in
+ ''|none) ;;
+ docker|podman|lxc|lxc-libvirt|systemd-nspawn|openvz|rkt|proot|pouch|wsl) [ -n "$CTR" ] || CTR=$v ;;
+ *) VIRT=$v ;;
+ esac
+ else
+ if [ -z "$CTR" ] && grep -qE '/(docker|kubepods|lxc|libpod)' /proc/1/cgroup 2>/dev/null; then
+ CTR=container
+ fi
+ grep -qw hypervisor /proc/cpuinfo 2>/dev/null && VIRT=hypervisor
+ fi
+ [ "$CTR" = wsl ] && CTR=""
+}
+
+# Does the kernel package changelog record the fix? (RHEL, SUSE, Oracle, Debian, Ubuntu families)
+# For rpm, a single 'rpm -qf --changelog' both finds the owning package and prints its changelog.
+# CL_SRC records where the evidence came from (rpm|deb).
+changelog_has_fix() {
+ local krel=$1 f
+ CL_SRC=""
+ if have rpm; then
+ for f in "/lib/modules/$krel/vmlinuz" "/boot/vmlinuz-$krel"; do
+ [ -e "$f" ] || continue
+ rpm -qf --changelog "$f" 2>/dev/null | has_fix_text && { CL_SRC=rpm; return 0; }
+ break
+ done
+ fi
+ have gzip || return 1
+ CL_SRC=deb
+ for f in /usr/share/doc/linux-image-"$krel"/changelog.Debian.gz \
+ /usr/share/doc/linux-modules-"$krel"/changelog.Debian.gz \
+ /usr/share/doc/linux-image-unsigned-"$krel"/changelog.Debian.gz \
+ /usr/share/doc/proxmox-kernel-"$krel"*/changelog.Debian.gz; do
+ [ -r "$f" ] || continue
+ gzip -dc "$f" 2>/dev/null | has_fix_text && return 0
+ done
+ return 1
+}
+
+# Only kernels that still have a bootable image (skips empty module dirs left by DKMS etc.).
+# Alpine and Gentoo may not put the release in the image name, so modules.dep is used there.
+list_installed_krels() {
+ local d k
+ for d in /lib/modules/*/; do
+ k=${d%/}; k=${k##*/}
+ [ "$k" = "*" ] && continue
+ if [ -e "/lib/modules/$k/vmlinuz" ] || [ -e "/boot/vmlinuz-$k" ] ||
+ { case $FAMILY in alpine|gentoo) true ;; *) false ;; esac && [ -e "/lib/modules/$k/modules.dep" ]; }; then
+ printf '%s\n' "$k"
+ fi
+ done
+}
+
+# Was the running kernel's image replaced under the same name after boot (same-ABI in-place
+# update on Debian-like systems)? dpkg keeps the build time as mtime, so compare the install
+# time (ctime) with the boot time (btime).
+image_replaced_after_boot() {
+ local f c k v bt=""
+ for f in "/boot/vmlinuz-$1" "/lib/modules/$1/vmlinuz" ""; do [ -n "$f" ] && [ -e "$f" ] && break; done
+ [ -n "$f" ] || return 1
+ c=$(stat -c %Z "$f" 2>/dev/null) || return 1
+ while read -r k v; do [ "$k" = btime ] && { bt=$v; break; }; done < /proc/stat
+ [ -n "$bt" ] && [ "$c" -gt "$bt" ]
+}
+
+# If grubby can tell the default boot kernel, put it in DEFAULT_KREL (RHEL, Fedora, Amazon families)
+default_boot_krel() {
+ local d
+ DEFAULT_KREL=""
+ have grubby || return 1
+ d=$($SUDO grubby --default-kernel 2>/dev/null) || return 1
+ case $d in /boot/vmlinuz-?*) DEFAULT_KREL=${d#/boot/vmlinuz-} ;; *) return 1 ;; esac
+}
+
+# Do the image and initramfs of the reboot target actually exist? (otherwise the boot may fail)
+boot_files_ok() {
+ local k=$1 f img=0 ird=0
+ case $FAMILY in arch|alpine|gentoo|microos|slemicro) return 0 ;; esac # image names carry no release
+ for f in "/boot/vmlinuz-$k" "/lib/modules/$k/vmlinuz" "/boot/Image-$k"; do [ -s "$f" ] && img=1; done
+ for f in "/boot/initramfs-$k.img" "/boot/initrd.img-$k" "/boot/initrd-$k"; do [ -s "$f" ] && ird=1; done
+ [ "$img" = 1 ] && [ "$ird" = 1 ]
+}
+
+# The newest kernel installed after the running one (usually the next default boot kernel)
+# Result: NEW_KREL, NEW_DVER (Debian package version). Candidates are collected with shell
+# builtins and compared in a single awk run.
+newest_installed() {
+ local mode=rpm ref k list="" st pkg ver pick tab
+ NEW_KREL=""; NEW_DVER=""
+ tab=$(printf '\t')
+ if [ "$FAMILY" = debian ]; then
+ have dpkg-query || return 1
+ mode=deb; ref=$RUN_DVER
+ while IFS='|' read -r st pkg ver; do
+ case $st in ii*) ;; *) continue ;; esac
+ k=${pkg#linux-image-}
+ [ -e "/lib/modules/$k/modules.dep" ] || continue
+ list="$list$ver$tab$k$tab$ver
+"
+ done <<EOF
+$(dpkg-query -W -f='${db:Status-Abbrev}|${Package}|${Version}\n' 'linux-image-*' 2>/dev/null)
+EOF
+ else
+ rpm_evr_of_krel "$KREL"; ref=$EVR
+ for k in $(list_installed_krels); do
+ [ "$k" = "$KREL" ] && continue
+ [ "$FAMILY" = proxmox ] && case $k in *-pve) ;; *) continue ;; esac # Proxmox kernels only
+ rpm_evr_of_krel "$k"
+ list="$list$EVR$tab$k$tab
+"
+ done
+ fi
+ [ -n "$list" ] || return 1
+ pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode="$mode" -v ref="$ref" "$AWK_VERCMP")
+ [ -n "$pick" ] || return 1
+ pick=${pick#*"$tab"}
+ NEW_KREL=${pick%%"$tab"*}
+ NEW_DVER=${pick#*"$tab"}
+}
+
+# Determine and judge the next boot kernel.
+# The grubby default when known, otherwise the newest kernel installed after the running one.
+# Result: NB_KREL / NB_DVER / NB_STATUS (NB_KREL is empty when the next boot is the running kernel)
+# NB_ALT = a fixed kernel that is installed but is not the default boot kernel
+next_boot_eval() {
+ NB_KREL=""; NB_DVER=""; NB_STATUS=""; NB_ALT=""
+ newest_installed || { NEW_KREL=""; NEW_DVER=""; }
+ if default_boot_krel; then
+ [ "$DEFAULT_KREL" != "$KREL" ] && NB_KREL=$DEFAULT_KREL
+ else
+ NB_KREL=$NEW_KREL; NB_DVER=$NEW_DVER
+ fi
+ if [ -n "$NB_KREL" ]; then
+ evaluate_kernel "$NB_KREL" "$NB_DVER"; NB_STATUS=$E_STATUS
+ fi
+ if [ "$NB_STATUS" != fixed ] && [ -n "$NEW_KREL" ] && [ "$NEW_KREL" != "$NB_KREL" ]; then
+ evaluate_kernel "$NEW_KREL" "$NEW_DVER"
+ [ "$E_STATUS" = fixed ] && NB_ALT=$NEW_KREL
+ fi
+ return 0
+}
+
+# Will a reboot come up on a fixed kernel? → sets REBOOT_TARGET / REBOOT_KREL
+installed_fix_ready() {
+ REBOOT_TARGET=""; REBOOT_KREL=""
+ next_boot_eval
+ [ "$NB_STATUS" = fixed ] || return 1
+ REBOOT_KREL=$NB_KREL
+ REBOOT_TARGET="$NB_KREL${NB_DVER:+ (package $NB_DVER)}"
+}
+
+# Is any kernel livepatch active? (reads sysfs only)
+livepatch_active() {
+ local f v
+ for f in /sys/kernel/livepatch/*/enabled; do
+ [ -r "$f" ] || continue
+ read -r v < "$f" 2>/dev/null && [ "$v" = 1 ] && return 0
+ done
+ return 1
+}
+
+# Has a livepatch applied the fix to the running kernel?
+# Without an active livepatch the full rpm database query (rpm -qa) is skipped entirely.
+livepatch_has_fix() {
+ local p rel
+ if livepatch_active; then
+ if have canonical-livepatch && canonical-livepatch status --verbose 2>/dev/null | has_fix_text; then
+ LP_NAME="Canonical Livepatch"; return 0
+ fi
+ if have rpm; then
+ rpm_evr_of_krel "$KREL"; rel=$EVR
+ case $rel in *-default|*-azure|*-rt|*-64kb) rel=${rel%-*} ;; esac
+ # Only the package built for the running kernel (RHEL/SUSE use dots→underscores, Amazon keeps dots)
+ for p in $(rpm -qa 'kpatch-patch*' 'kernel-livepatch-*' 2>/dev/null); do
+ case $p in *-"${rel//./_}"-*|*-"$rel"-*) ;; *) continue ;; esac
+ if rpm -q --changelog "$p" 2>/dev/null | has_fix_text; then LP_NAME=$p; return 0; fi
+ done
+ fi
+ fi
+ if have uptrack-show && uptrack-show 2>/dev/null | has_fix_text; then
+ LP_NAME="Oracle Ksplice"; return 0
+ fi
+ return 1
+}
+
+# -----------------------------------------------------------------------------
+# Verdict — results go into E_STATUS / E_FIXED / E_BASIS / E_ADV / E_PKG / E_MODE
+# E_STATUS: fixed | vulnerable | pending | eol | unaffected | unknown
+# -----------------------------------------------------------------------------
+upstream_eval() { # pure shell pattern parsing, no subprocess
+ local rel=$1 base maj min pat rest rc="" br v fpat
+ E_MODE=upstream
+ base=${rel%%[!0-9.]*}
+ maj=${base%%.*}; rest=${base#*.}
+ min=${rest%%.*}; pat=${rest#*.}; pat=${pat%%.*}
+ [ "$pat" = "$rest" ] && pat=0 # x.y form
+ case $maj:$min:$pat in
+ *[!0-9:]*|:*|*::*|*:) E_STATUS=unknown; E_BASIS="cannot parse kernel version ($rel)"; return ;;
+ esac
+ [ "$rest" = "$base" ] && { E_STATUS=unknown; E_BASIS="cannot parse kernel version ($rel)"; return; }
+ base="$maj.$min.$pat"
+ # rc number: rcN after a non-letter, e.g. "-rc6", ".rc6", Ubuntu mainline "070300rc5"
+ case $rel in
+ *[!A-Za-z]rc[0-9]*) rc=${rel#*[!A-Za-z]rc}; rc=${rc%%[!0-9]*} ;;
+ esac
+ br="$maj.$min"
+
+ if [ "$maj" -lt 2 ] || { [ "$maj" -eq 2 ] && [ "$min" -lt 6 ]; } || { [ "$br" = 2.6 ] && [ "$pat" -lt 27 ]; }; then
+ case $rel in
+ *.el[0-9]*|*.amzn*) # vendor kernels may carry backported networking code under old version numbers
+ E_STATUS=unknown; E_BASIS="$base vendor kernel — version predates 2.6.27 but backports must be checked" ;;
+ *)
+ E_STATUS=unaffected; E_BASIS="$base — older than 2.6.27, where the vulnerable code was introduced" ;;
+ esac
+ return
+ fi
+ if [ "$maj" -gt 7 ] || { [ "$maj" -eq 7 ] && [ "$min" -ge 3 ]; }; then
+ E_FIXED="7.3-rc7"
+ if [ "$br" = 7.3 ] && [ -n "$rc" ] && [ "$rc" -lt 7 ]; then
+ E_STATUS=vulnerable; E_BASIS="7.3-rc$rc < mainline fix point 7.3-rc7"
+ else
+ E_STATUS=fixed; E_BASIS="$base — mainline 7.3-rc7 or later (includes the fix)"
+ fi
+ return
+ fi
+ for v in $UPSTREAM_FIXED; do
+ [ "${v%.*}" = "$br" ] || continue
+ fpat=${v##*.}; E_FIXED=$v
+ if [ "$pat" -ge "$fpat" ]; then
+ E_STATUS=fixed; E_BASIS="$base ≥ upstream fixed version $v"
+ else
+ E_STATUS=vulnerable; E_BASIS="$base < upstream fixed version $v"
+ fi
+ return
+ done
+ E_STATUS=eol; E_BASIS="upstream $br branch is end of life — no fixed release for this branch"
+}
+
+nonvendor_eval() { # $1=kernel release $2=reason
+ upstream_eval "$1"
+ E_BASIS="$2 → upstream rules: $E_BASIS"
+}
+
+rpm_table_eval() { # $1=kernel release $2=table name
+ local evr br t_br t_cmp t_disp t_adv t_pkg
+ rpm_evr_of_krel "$1"; evr=$EVR
+ branch_of "$evr"; br=$BRANCH
+ while IFS='|' read -r t_br t_cmp t_disp t_adv t_pkg; do
+ [ -n "$t_br" ] && [ "$t_br" = "$br" ] || continue
+ E_FIXED=$t_disp; E_ADV=$t_adv; E_PKG=$t_pkg
+ if ver_lt rpm "$evr" "$t_cmp"; then
+ E_STATUS=vulnerable; E_BASIS="kernel $evr < fixed version $t_cmp"
+ else
+ E_STATUS=fixed; E_BASIS="kernel $evr ≥ fixed version $t_cmp"
+ fi
+ return 0
+ done <<EOF
+$(vendor_table "$2")
+EOF
+ return 1
+}
+
+deb_eval() { # $1=kernel release $2=Debian package version
+ local v=$2 up br t
+ if [ -z "$v" ]; then
+ if [ "$OFFLINE" = 1 ]; then
+ E_STATUS=unknown
+ E_BASIS="Debian kernels need the package version — pass the target server's 'uname -v' with --uname-v"
+ return
+ fi
+ nonvendor_eval "$1" "not a Debian-packaged kernel"; return
+ fi
+ case $v in *+rpt*|*rpi*) nonvendor_eval "$1" "Raspberry Pi kernel"; return ;; esac
+ up=${v#*:}; branch_of "$up"; br=$BRANCH
+ case $br in
+ 6.1) t="6.1.190-1~"; E_FIXED="6.1.190-1"; E_ADV="bookworm-security, DLA-4841-1" ;; # ~ : also covers bullseye-LTS ~deb11u
+ 6.12) t="6.12.113-1~"; E_FIXED="6.12.113-1"; E_ADV="trixie-security, DSA-6568-1" ;;
+ 7.2) t="7.2.11-1~"; E_FIXED="7.2.11-1"; E_ADV="forky/sid" ;;
+ *)
+ upstream_eval "${up%%-*}"
+ if [ "$E_STATUS" = fixed ] || [ "$E_STATUS" = unaffected ]; then
+ E_BASIS="Debian kernel $v → upstream rules: $E_BASIS"
+ else
+ E_STATUS=eol
+ E_BASIS="kernel branch $br is not tracked by Debian security — end-of-life release (bullseye or older) or non-standard kernel"
+ fi
+ E_MODE=vendor
+ return ;;
+ esac
+ if ver_lt deb "$v" "$t"; then
+ E_STATUS=vulnerable; E_BASIS="Debian kernel $v < fixed version $E_FIXED"
+ else
+ E_STATUS=fixed; E_BASIS="Debian kernel $v ≥ fixed version $E_FIXED"
+ fi
+}
+
+# Verdict: decide from vendor tables / upstream versions first; only when that is inconclusive
+# (no fix yet, end of life, unknown) is the kernel changelog (several MB) checked. Distributions
+# decided by a table never read a changelog.
+# E_INPLACE=1: a package with the running kernel's name was replaced by a fixed one after boot → reboot
+evaluate_kernel() { # $1=kernel release $2=Debian package version (optional)
+ E_INPLACE=0
+ evaluate_by_version "$@"
+ case $E_STATUS in pending|unknown|eol) ;; *) return ;; esac
+ [ "$OFFLINE" = 0 ] || return
+ if changelog_has_fix "$1"; then
+ # A deb changelog belongs to the package on disk — if the running kernel was replaced
+ # after boot, the fix is not in the kernel that is in memory yet
+ if [ "$1" = "$KREL" ] && [ "$CL_SRC" = deb ] && image_replaced_after_boot "$1"; then
+ E_INPLACE=1
+ E_BASIS="$E_BASIS · the package that replaced this kernel after boot contains the fix (reboot needed)"
+ return
+ fi
+ E_STATUS=fixed; E_MODE=changelog
+ E_BASIS="the kernel package changelog records $CVE or the fix commit"
+ else
+ case $FAMILY in
+ rhel|centos|rocky|rhelclone|kitten|oracle|ubuntu|proxmox|sles|slemicro|leap|debian|debianlike)
+ E_BASIS="$E_BASIS · no fix recorded in the kernel changelog either" ;;
+ esac
+ fi
+}
+
+evaluate_by_version() {
+ local krel=$1 dver=${2:-}
+ E_STATUS=unknown; E_FIXED=""; E_BASIS=""; E_ADV=""; E_PKG=""; E_MODE=vendor
+
+ # On RPM distributions, a kernel that is not the distro kernel (ELRepo, self-built) uses upstream rules
+ case $FAMILY in
+ alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon)
+ case $krel in
+ *elrepo*) nonvendor_eval "$krel" "ELRepo kernel"; return ;;
+ *.el[0-9]*|*.amzn[0-9]*) ;;
+ *) nonvendor_eval "$krel" "not the distribution kernel"; return ;;
+ esac ;;
+ esac
+
+ case $FAMILY in
+ alma)
+ case $OS_MAJ_NUM in
+ 8|9|10) rpm_table_eval "$krel" "alma$OS_MAJ_NUM" ||
+ nonvendor_eval "$krel" "not the default kernel branch of AlmaLinux $OS_MAJ_NUM" ;;
+ *) E_STATUS=eol; E_BASIS="end-of-life AlmaLinux release ($OS_VER)" ;;
+ esac ;;
+ kitten)
+ E_STATUS=pending
+ E_BASIS="AlmaLinux Kitten gets the fix in the next regular kernel update (no separate build)" ;;
+ rhel|centos|rocky|rhelclone)
+ if { [ "$FAMILY" = rhel ] && [ "$OS_MAJ_NUM" -lt 7 ]; } ||
+ { [ "$FAMILY" = centos ] && [ "$OS_MAJ_NUM" -le 8 ]; }; then
+ E_STATUS=eol; E_BASIS="end-of-life release (${OS_NAME:-$OS_ID $OS_VER}) — no fixed kernel will ship"
+ elif [ "$FAMILY" = rhel ]; then
+ E_STATUS=pending; E_BASIS="Red Hat: RHEL 7/8/9/10 affected, no fixed kernel released (as of $DATA_DATE)"
+ else
+ E_STATUS=pending; E_BASIS="no fixed RHEL kernel yet — none published for this distribution either (as of $DATA_DATE)"
+ fi ;;
+ oracle)
+ case $krel in
+ *uek*)
+ rpm_table_eval "$krel" uek || {
+ rpm_evr_of_krel "$krel"; branch_of "$EVR"
+ E_STATUS=pending
+ E_BASIS="no advisory for this UEK release (${BRANCH:-?}) — move to UEK R6 or later"
+ } ;;
+ *)
+ E_STATUS=pending
+ E_BASIS="no advisory for RHCK (Red Hat compatible kernel) (as of $DATA_DATE) — UEK is fixed" ;;
+ esac ;;
+ amazon)
+ case $OS_VER in
+ 2|2023)
+ rpm_table_eval "$krel" "al$OS_VER" ||
+ { E_STATUS=unknown; E_BASIS="kernel branch not covered by ALAS advisories ($krel)"; } ;;
+ *)
+ E_STATUS=eol; E_BASIS="Amazon Linux 1 is end of life — no advisory" ;;
+ esac ;;
+ debian)
+ deb_eval "$krel" "$dver" ;;
+ debianlike)
+ if [ -n "$dver" ]; then deb_eval "$krel" "$dver"
+ else nonvendor_eval "$krel" "distribution's own kernel"; fi ;;
+ ubuntu)
+ E_STATUS=pending
+ E_BASIS="Ubuntu CVE tracker: every kernel package 'Needs evaluation' — no fix released (as of $DATA_DATE)" ;;
+ proxmox)
+ E_STATUS=unknown
+ E_BASIS="no published fix information for the Proxmox VE kernel (based on the Ubuntu kernel)" ;;
+ sles|slemicro)
+ case $FAMILY:$OS_VER in
+ sles:11*|sles:12|sles:12.[0-4]|sles:15|sles:15.[1-3]|slemicro:5.[0-2])
+ E_STATUS=eol; E_BASIS="end-of-life SUSE release ($OS_VER) — not proactively patched" ;;
+ *)
+ E_STATUS=pending; E_BASIS="SUSE advisory: kernel-default etc. 'Affected', state 'Pending' — no fix released (as of $DATA_DATE)" ;;
+ esac ;;
+ leap)
+ case $OS_MAJ_NUM in
+ 15|42) E_STATUS=eol; E_BASIS="openSUSE Leap $OS_VER is end of life" ;;
+ *) E_STATUS=pending; E_BASIS="SUSE advisory: Leap 16 'Affected', state 'Pending' — no fix released (as of $DATA_DATE)" ;;
+ esac ;;
+ fedora|arch|tumbleweed|microos|alpine|gentoo|wsl)
+ upstream_eval "$krel" ;;
+ wsl1)
+ E_STATUS=unaffected; E_BASIS="WSL1 has no Linux kernel; Windows handles the system calls" ;;
+ *)
+ upstream_eval "$krel"
+ [ "$E_STATUS" = eol ] && E_STATUS=unknown
+ E_BASIS="estimated from upstream versions (check distribution backports): $E_BASIS" ;;
+ esac
+}
+
+status_label() {
+ case $1 in
+ fixed) printf '%sSAFE — running a fixed kernel%s' "$C_GRN" "$C_OFF" ;;
+ unaffected) printf '%sNOT AFFECTED — this kernel does not contain the vulnerable code%s' "$C_GRN" "$C_OFF" ;;
+ livepatched) printf '%sSAFE (temporary) — fix applied by livepatch%s' "$C_GRN" "$C_OFF" ;;
+ reboot) printf '%sREBOOT NEEDED — fixed kernel installed, still running the old one%s' "$C_YEL" "$C_OFF" ;;
+ vulnerable) printf '%sVULNERABLE — a fixed kernel is available, update needed%s' "$C_RED" "$C_OFF" ;;
+ pending) printf '%sVULNERABLE — no fixed kernel from the distribution yet (as of %s)%s' "$C_RED" "$DATA_DATE" "$C_OFF" ;;
+ eol) printf '%sVULNERABLE — end-of-life release/branch, no fixed kernel%s' "$C_RED" "$C_OFF" ;;
+ *) printf '%sUNDETERMINED — check manually%s' "$C_YEL" "$C_OFF" ;;
+ esac
+}
+
+# -----------------------------------------------------------------------------
+# Exposure — the main target is a virtualization host running untrusted guests
+# -----------------------------------------------------------------------------
+read_sysctl() { # $1=file → SYSCTL_VAL (empty if missing, no subprocess)
+ SYSCTL_VAL=""
+ [ -r "$1" ] && read -r SYSCTL_VAL < "$1" 2>/dev/null
+ return 0
+}
+
+check_exposure() {
+ local any=0 n procs v mods="" m _
+ HV_HOST=0
+ if [ -e /dev/kvm ]; then
+ line "- /dev/kvm present: this system can run KVM virtual machines"; any=1
+ fi
+ # Read /proc/modules only once
+ if [ -r /proc/modules ]; then
+ while read -r m _; do mods="$mods $m"; done < /proc/modules
+ fi
+ case "$mods " in *" vhost_net "*)
+ line "- vhost_net module loaded: virtio-net backend in use"; any=1; HV_HOST=1 ;;
+ esac
+ case "$mods " in *" tun "*|*" tap "*|*" macvtap "*)
+ line "- tun/tap module loaded"; any=1 ;;
+ esac
+ set -- /sys/class/net/*/tun_flags
+ if [ -e "$1" ]; then n=$#; else n=0; fi
+ if [ "$n" -gt 0 ]; then
+ line "- $n TUN/TAP interface(s) present"; any=1
+ fi
+ # A single grep reads /proc/*/comm directly; awk removes duplicates and joins
+ # kvm = Proxmox VM process, vhost-<pid> = virtio-net vhost worker thread
+ procs=$(grep -hE '^(qemu-system|qemu-kvm|kvm$|vhost-[0-9]|firecracker|cloud-hyperviso|crosvm|libvirtd|virtqemud|VBoxHeadless|VirtualBoxVM|vmware-vmx)' \
+ /proc/[0-9]*/comm 2>/dev/null | awk '{ sub(/^vhost-[0-9]+$/, "vhost") } !s[$0]++ { printf "%s ", $0 }')
+ if [ -n "$procs" ]; then
+ line "- virtualization processes running: $procs"; any=1; HV_HOST=1
+ fi
+ if [ "$FAMILY" = proxmox ] && [ "$HV_HOST" = 0 ]; then
+ line "- Proxmox VE node"; any=1; HV_HOST=1
+ fi
+ [ "$any" = 1 ] || line "- no sign of a virtualization host (no KVM, TUN/TAP or VM processes)"
+ if [ "$HV_HOST" = 1 ]; then
+ line "${C_RED}→ Looks like a virtualization host. It is the direct target of guest→host escape: patch it first.${C_OFF}"
+ fi
+ [ -n "$VIRT" ] && line "- This system is a VM guest ($VIRT). Patching the host kernel is up to the host operator/provider."
+
+ if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then
+ read_sysctl /proc/sys/kernel/unprivileged_userns_clone; v=$SYSCTL_VAL
+ if [ "$v" = 0 ]; then line "- unprivileged user namespaces: blocked (kernel.unprivileged_userns_clone=0)"
+ else line "- unprivileged user namespaces: allowed (kernel.unprivileged_userns_clone=$v)"; fi
+ elif [ -r /proc/sys/user/max_user_namespaces ]; then
+ read_sysctl /proc/sys/user/max_user_namespaces; v=$SYSCTL_VAL
+ if [ "$v" = 0 ]; then line "- user namespaces: blocked (user.max_user_namespaces=0)"
+ else line "- user namespaces: allowed (user.max_user_namespaces=$v)"; fi
+ fi
+ read_sysctl /proc/sys/kernel/apparmor_restrict_unprivileged_userns
+ if [ "$SYSCTL_VAL" = 1 ]; then
+ line " (AppArmor restricts unprivileged namespace privileges)"
+ fi
+ note "The namespace lines are informational. An unprivileged user who gains CAP_NET_ADMIN/CAP_NET_RAW inside a namespace may reach the TUN/TAP and packet-socket paths, which could matter for local attacks (an assumption), but no vendor has published this as a mitigation."
+}
+
+# -----------------------------------------------------------------------------
+# Remediation
+# -----------------------------------------------------------------------------
+remedy_eol() {
+ line "No fixed kernel will be released for this release/kernel branch. $1"
+}
+
+remedy_custom() {
+ line "Judged as a kernel that is not the distribution kernel (ELRepo, self-built, ...)."
+ line "Apply the kernel provider's update or move to an upstream stable fixed version or later:"
+ line " $UPSTREAM_TEXT"
+ line " Fix commit: $FIX_COMMIT ($FIX_URL)"
+ cmt "The kernel CNA recommends the latest stable kernel over cherry-picking single commits"
+ cmt "ELRepo example: sudo dnf --enablerepo=elrepo-kernel update 'kernel-ml*' 'kernel-lt*'"
+ cmt "Or boot the distribution kernel again and apply the distribution patch"
+}
+
+remedy_rhel_like() { # $1=status
+ local pm=dnf core=kernel-core
+ case $OS_MAJ_NUM in [1-7]) pm=yum; core=kernel ;; esac
+ if [ "$1" = eol ]; then
+ remedy_eol "Move to a supported release (RHEL, Rocky, AlmaLinux 8/9/10, CentOS Stream 9/10)."
+ return
+ fi
+ line "No fixed kernel has been released as of $DATA_DATE. (Red Hat: no mitigation available)"
+ cmt "1) Check whether a fix has been released"
+ cmd "sudo $pm updateinfo list --cve $CVE"
+ cmd "rpm -q --changelog $core | grep -iE '$CVE|checksum_start'"
+ cmt "2) Once released, apply it and reboot"
+ cmd "sudo $pm update --cve $CVE # or: sudo $pm update 'kernel*'"
+ cmd "sudo reboot"
+ if [ "$FAMILY" = rhel ]; then
+ cmt "If Red Hat ships a kpatch livepatch, it can be applied without a reboot"
+ cmd "sudo $pm install \"kpatch-patch = \$(uname -r)\""
+ fi
+}
+
+remedy_family() { # $1=status
+ local st=$1 pm
+ case $FAMILY in
+ alma)
+ if [ "$st" = eol ]; then remedy_eol "Upgrade to a supported AlmaLinux (8.10 / 9 / 10)."; return; fi
+ line "Fixed kernel: ${R_FIXED:-see the AlmaLinux advisory} or later"
+ cmt "1) First try the regular repositories"
+ cmd "sudo dnf clean metadata && sudo dnf update 'kernel*'"
+ cmt "2) If it is not there yet, install from the testing repo (AlmaLinux advisory of 2026-10-16)"
+ cmd "sudo dnf install -y almalinux-release-testing"
+ cmd "sudo dnf update 'kernel*' --enablerepo=almalinux-testing"
+ cmd "sudo dnf remove -y almalinux-release-testing # clean up the testing repo"
+ cmt "3) Reboot and confirm the fixed version with 'uname -r'"
+ cmd "sudo reboot"
+ ;;
+ kitten)
+ line "AlmaLinux Kitten will include the fix in its next regular kernel update."
+ cmd "sudo dnf update 'kernel*'"
+ cmd "sudo reboot"
+ ;;
+ rhel|centos|rocky|rhelclone)
+ remedy_rhel_like "$st"
+ ;;
+ oracle)
+ pm=dnf; case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
+ case $KREL in
+ *uek*)
+ if [ "$st" = vulnerable ]; then
+ line "Fixed kernel: $R_FIXED or later ($R_ADV)"
+ cmd "sudo $pm update kernel-uek"
+ cmd "sudo reboot"
+ cmt "With a Ksplice subscription, apply without a reboot: sudo uptrack-upgrade -y"
+ else
+ line "There is no advisory for the UEK release in use. Switch to a fixed UEK."
+ 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)"
+ cmd "sudo $pm install kernel-uek"
+ cmd "sudo reboot"
+ fi ;;
+ *)
+ line "There is no fix for RHCK (Red Hat compatible kernel) yet. UEK is already fixed, so switching to UEK is the fastest route."
+ cmd "sudo $pm install kernel-uek"
+ cmd "sudo grubby --info=ALL | grep -E '^kernel' # find the installed UEK path"
+ cmd "sudo grubby --set-default /boot/vmlinuz-<UEK version>"
+ cmd "sudo reboot"
+ cmt "If you must stay on RHCK, check for a release: sudo $pm updateinfo list --cve $CVE"
+ ;;
+ esac ;;
+ amazon)
+ case $OS_VER in
+ 2)
+ if [ -n "$R_ADV" ]; then
+ line "Fixed kernel: $R_FIXED or later ($R_ADV)"
+ cmd "sudo yum update --advisory $R_ADV # or: sudo yum update kernel"
+ else
+ cmd "sudo yum update kernel"
+ fi
+ cmd "sudo reboot" ;;
+ 2023)
+ if [ -n "$R_ADV" ]; then
+ line "Fixed kernel: $R_FIXED or later ($R_ADV)"
+ cmd "sudo dnf update $R_PKG --releasever 2023.12.20261012"
+ cmt "or: sudo dnf update --advisory $R_ADV --releasever 2023.12.20261012"
+ else
+ cmd "sudo dnf update 'kernel*' --releasever latest"
+ fi
+ cmd "sudo reboot" ;;
+ *)
+ remedy_eol "Move to Amazon Linux 2023." ;;
+ esac
+ note "AWS states that the EC2, Lambda and Fargate host infrastructure is not affected. The steps above are for this instance's own kernel."
+ ;;
+ debian)
+ if [ "$st" = eol ]; then
+ remedy_eol "Upgrade to bookworm (12) or later, or look at extended support such as Freexian ELTS."
+ return
+ fi
+ line "Fixed kernel: $R_FIXED or later ($R_ADV)"
+ cmt "1) Make sure the security repository (<codename>-security) is enabled"
+ cmd "grep -rhE '^[^#].*security' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null"
+ cmt "2) Update and reboot"
+ cmd "sudo apt update"
+ cmd "sudo apt full-upgrade"
+ cmt "If the kernel metapackage is missing: sudo apt install linux-image-\$(dpkg --print-architecture)"
+ case $RUN_DVER in *bpo*)
+ cmt "Using a backports kernel: sudo apt install -t ${OS_CODENAME:-<codename>}-backports linux-image-\$(dpkg --print-architecture)" ;;
+ esac
+ cmd "sudo reboot"
+ cmt "3) After the reboot, check that 'Debian <version>' in 'uname -v' is the fixed version or later"
+ ;;
+ debianlike)
+ line "Update the distribution kernel to the latest version. (fixed in upstream ${R_FIXED:-stable} or later)"
+ cmd "sudo apt update && sudo apt full-upgrade"
+ cmd "sudo reboot"
+ ;;
+ ubuntu)
+ line "No fixed Ubuntu kernel has been released as of $DATA_DATE. (every kernel 'Needs evaluation')"
+ cmt "1) Check whether a fix has been released (Ubuntu Pro client)"
+ cmd "pro fix $CVE --dry-run"
+ cmt "or https://ubuntu.com/security/$CVE"
+ cmt "2) If you use Livepatch, check whether it has been applied"
+ cmd "sudo canonical-livepatch status --verbose | grep -A2 $CVE"
+ cmt "3) Once released, apply it and reboot"
+ cmd "sudo apt update && sudo apt full-upgrade"
+ cmd "sudo reboot"
+ ;;
+ proxmox)
+ line "Whether the Proxmox VE kernel (based on the Ubuntu kernel) is fixed must be checked in Proxmox announcements."
+ cmd "pveversion -v | grep -E 'proxmox-kernel|pve-manager'"
+ cmd "apt changelog proxmox-kernel-\$(uname -r)-signed 2>/dev/null | grep -iE '$CVE|checksum_start'"
+ cmt "When a fixed kernel is out (pve-enterprise or pve-no-subscription repository)"
+ cmd "sudo apt update && sudo apt full-upgrade"
+ cmt "Migrate or shut down the guests, then reboot"
+ cmd "sudo reboot"
+ ;;
+ sles|slemicro|leap)
+ if [ "$st" = eol ]; then
+ if [ "$FAMILY" = leap ]; then remedy_eol "Upgrade to openSUSE Leap 16."
+ else remedy_eol "Upgrade to a supported service pack (including LTSS)."; fi
+ return
+ fi
+ line "No fixed SUSE kernel has been released as of $DATA_DATE. (state 'Pending')"
+ cmt "1) Check whether a fix has been released"
+ cmd "sudo zypper refresh && sudo zypper list-patches --cve=$CVE"
+ cmt "2) Once released, apply it and reboot"
+ if [ "$FAMILY" = slemicro ]; then cmd "sudo transactional-update patch"
+ else cmd "sudo zypper patch --cve=$CVE"; fi
+ cmd "sudo reboot"
+ cmt "With SLE Live Patching, a kernel-livepatch patch can be applied without a reboot"
+ ;;
+ fedora)
+ if [ "$st" = eol ]; then
+ line "This kernel branch is no longer supported upstream. Update the kernel, and if the Fedora release is end of life, move to a supported release."
+ cmd "sudo dnf upgrade --refresh"
+ cmd "sudo dnf system-upgrade download --releasever=<new release>"
+ return
+ fi
+ line "Fixed in: upstream $R_FIXED or later"
+ cmd "sudo dnf upgrade --refresh 'kernel*'"
+ cmt "If it is not in the stable repository yet, install from updates-testing"
+ cmd "sudo dnf upgrade --refresh --enablerepo=updates-testing 'kernel*'"
+ cmd "sudo reboot"
+ ;;
+ arch)
+ line "Fixed in: linux ${R_FIXED:-7.2.11} or later / linux-lts 6.12.113 · 6.18.57 or later"
+ cmd "sudo pacman -Syu"
+ cmd "sudo reboot"
+ ;;
+ tumbleweed)
+ line "Fixed in: upstream ${R_FIXED:-stable} or later"
+ cmd "sudo zypper refresh && sudo zypper dup"
+ cmd "sudo reboot"
+ ;;
+ microos)
+ cmd "sudo transactional-update dup"
+ cmd "sudo reboot"
+ ;;
+ alpine)
+ line "Fixed in: upstream ${R_FIXED:-stable} or later (linux-lts / linux-virt)"
+ cmd "sudo apk update && sudo apk upgrade"
+ cmd "sudo reboot"
+ [ "$st" = eol ] && cmt "If the kernel branch is end of life, the Alpine release itself must be upgraded"
+ ;;
+ gentoo)
+ line "Fixed in: upstream ${R_FIXED:-stable} or later"
+ cmd "sudo emerge --sync"
+ cmd "sudo emerge --ask --update --oneshot sys-kernel/gentoo-kernel-bin # or gentoo-kernel"
+ cmt "If you build from gentoo-sources, rebuild from a fixed version or later"
+ cmd "sudo reboot"
+ ;;
+ wsl)
+ line "The WSL2 kernel is shipped by Microsoft. Run this in Windows PowerShell:"
+ cmd "wsl --update"
+ cmd "wsl --shutdown"
+ note "The WSL2 host is Windows (Hyper-V), so the 'VM → Linux host escape' scenario does not apply. Only the WSL kernel itself was judged, using upstream versions."
+ ;;
+ *)
+ line "Check your distribution vendor's security advisory and update the kernel."
+ line "For a self-built kernel, move to an upstream stable fixed version or later:"
+ line " $UPSTREAM_TEXT"
+ line " Fix commit: $FIX_COMMIT ($FIX_URL)"
+ ;;
+ esac
+}
+
+print_remedy() { # $1=final status
+ local st=$1
+ case $st in
+ fixed|unaffected)
+ line "${C_GRN}No further action needed.${C_OFF}"
+ [ -n "$VIRT" ] && note "This system is a VM guest ($VIRT). Check separately whether the host (cloud/hosting provider) kernel is patched."
+ return ;;
+ livepatched)
+ line "A livepatch ($LP_NAME) has applied the fix to the running kernel."
+ line "At the next maintenance window, install the fixed kernel package and reboot."
+ return ;;
+ reboot)
+ line "A fixed kernel is already installed: $REBOOT_TARGET"
+ line "It takes effect after a reboot."
+ [ "$HV_HOST" = 1 ] && cmt "This is a virtualization host. Migrate or shut down the guest VMs before rebooting."
+ cmd "sudo reboot"
+ cmt "Run this script again after the reboot to confirm"
+ return ;;
+ esac
+
+ if [ "$R_MODE" = upstream ]; then
+ case $FAMILY in
+ fedora|arch|tumbleweed|microos|alpine|gentoo|wsl|generic|debianlike) remedy_family "$st" ;;
+ debian) FAMILY=debianlike remedy_family "$st" ;;
+ *) remedy_custom ;;
+ esac
+ else
+ remedy_family "$st"
+ fi
+
+ printf '\n'
+ note "There is no official workaround (sysctl, module blacklisting, ...). Booting a fixed kernel is the only fix."
+ if [ "$HV_HOST" = 1 ]; then
+ note "Patching only the guest OS kernels does not protect the host. Migrate or shut down the guests before rebooting."
+ fi
+ case $st in pending|eol|unknown)
+ note "Until patched, limit the creation of untrusted workloads (VMs, microVMs, sandboxes) and keep them away from sensitive systems. (This lowers risk; it is not a fix)" ;;
+ esac
+ [ -n "$VIRT" ] && note "This system is a VM guest ($VIRT). Also check whether the host (cloud/hosting provider) is patched."
+}
+
+print_refs() {
+ line "https://www.cve.org/CVERecord?id=$CVE"
+ line "https://nvd.nist.gov/vuln/detail/$CVE"
+ line "https://pwn.ai/blog/pwnescape"
+ line "https://almalinux.org/blog/2026-10-16-pwnescape-cve-2026-99999/"
+ line "https://www.penligent.ai/hackinglabs/cve-2026-99999/"
+ line "https://news.hada.io/topic?id=35568"
+ case $FAMILY in
+ debian|debianlike) line "https://security-tracker.debian.org/tracker/$CVE" ;;
+ ubuntu|proxmox) line "https://ubuntu.com/security/$CVE" ;;
+ alma|kitten|rhel|centos|rocky|rhelclone)
+ line "https://access.redhat.com/security/cve/cve-2026-99999" ;;
+ oracle) line "https://linux.oracle.com/cve/$CVE.html" ;;
+ amazon) line "https://explore.alas.aws.amazon.com/$CVE.html" ;;
+ sles|slemicro|leap|tumbleweed|microos)
+ line "https://www.suse.com/security/cve/$CVE.html" ;;
+ *) line "$FIX_URL" ;;
+ esac
+}
+
+print_refs_section() {
+ section "References"
+ print_refs
+ note "Verdict data as of $DATA_DATE. Vendor advisories keep changing, so check the latest ones too."
+}
+
+show_manual() {
+ section "Remediation (manual)"
+ print_remedy "$FINAL"
+ print_refs_section
+}
+
+# -----------------------------------------------------------------------------
+# Automatic patching (menu option 1)
+# -----------------------------------------------------------------------------
+run() {
+ printf ' %s$ %s%s\n' "$C_DIM" "$*" "$C_OFF"
+ "$@"
+}
+
+setup_sudo() {
+ SUDO=""
+ [ "$(id -u)" = 0 ] && return 0
+ if command -v sudo >/dev/null 2>&1; then SUDO=sudo; return 0; fi
+ line "${C_RED}Root privileges are required but sudo is not available. Run this again as root.${C_OFF}"
+ return 1
+}
+
+can_autopatch() {
+ case $FAMILY in wsl|wsl1) return 1 ;; esac # rebooting inside WSL never boots a distribution kernel
+ [ "$FINAL" = reboot ] && return 0
+ [ "$FINAL" = eol ] && return 1
+ case $FAMILY in
+ debian|debianlike|fedora|arch|tumbleweed|microos|alpine) return 0 ;;
+ alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon|ubuntu|proxmox|sles|slemicro|leap)
+ [ "$R_MODE" != upstream ] ;; # ELRepo / self-built kernels are not patched automatically
+ *) return 1 ;;
+ esac
+}
+
+# zypper exit codes 100-103 are informational (updates available, reboot needed, ...), treat as success
+zypper_ok() { [ "$1" -eq 0 ] || { [ "$1" -ge 100 ] && [ "$1" -le 103 ]; }; }
+zypper_run() { # kernel packages only: proceed without asking
+ local rc
+ run $SUDO zypper --non-interactive "$@"; rc=$?
+ zypper_ok "$rc"
+}
+zypper_ask() { # full system upgrade (dup): may remove packages, so the user confirms
+ local rc
+ run $SUDO zypper "$@"; rc=$?
+ zypper_ok "$rc"
+}
+
+# Upgrade only the installed linux-* packages (metapackages pull in the new ABI kernel).
+# - --no-remove: stop if other packages would have to be removed
+# - 'apt-get install' marks the named packages as manually installed, so restore the ones that
+# were automatic, letting 'apt autoremove' keep cleaning up old kernels
+# - Proxmox: dist-upgrade as Proxmox recommends, without -y so the user reviews any removals
+patch_apt() {
+ local pkgs="" autos="" st pkg a meta=0
+ local opts="-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold"
+ run $SUDO apt-get update || return 1
+ if [ "$FAMILY" = proxmox ]; then
+ run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get $opts dist-upgrade
+ return
+ fi
+ while read -r st pkg; do
+ case $st in ii*) ;; *) continue ;; esac
+ pkgs="$pkgs $pkg"
+ case $pkg in linux-image-[a-z]*) meta=1 ;; esac
+ done <<EOF
+$(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'linux-*' 2>/dev/null)
+EOF
+ if [ -z "$pkgs" ]; then
+ line "No installed linux-* kernel packages found."; return 1
+ fi
+ while read -r a; do
+ case "$pkgs " in *" $a "*) autos="$autos $a" ;; esac
+ done <<EOF
+$(apt-mark showauto 2>/dev/null)
+EOF
+ run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install --only-upgrade $pkgs || return 1
+ [ -n "$autos" ] && run $SUDO apt-mark auto $autos >/dev/null
+ # On Debian without a kernel metapackage the new ABI kernel never arrives, so install it
+ if [ "$FAMILY" = debian ] && [ "$meta" = 0 ]; then
+ run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install "linux-image-$(dpkg --print-architecture)"
+ fi
+}
+
+# The AlmaLinux advisory method. The testing repo is enabled only for this run, and the repo
+# package is removed afterwards so regular updates never mix in testing packages.
+patch_alma() {
+ local rc
+ run $SUDO dnf clean metadata && run $SUDO dnf -y update 'kernel*' || return 1
+ installed_fix_ready && return 0
+ line "The fixed kernel is not in the regular repositories yet; installing from almalinux-testing. (AlmaLinux advisory method)"
+ run $SUDO dnf -y install almalinux-release-testing || return 1
+ run $SUDO dnf -y update 'kernel*' --enablerepo=almalinux-testing; rc=$?
+ run $SUDO dnf -y remove almalinux-release-testing ||
+ note "Could not remove almalinux-release-testing. Check /etc/yum.repos.d/ to make sure the testing repo is not enabled."
+ return $rc
+}
+
+# Full system upgrades (pacman -Syu, zypper dup, apk upgrade, transactional-update dup) may remove
+# or replace packages, so their confirmation prompt is kept. Kernel-only upgrades use -y.
+patch_now() {
+ local pm=dnf
+ case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
+ case $FAMILY in
+ alma) patch_alma ;;
+ kitten|rhel|centos|rocky|rhelclone|oracle)
+ run $SUDO $pm -y update 'kernel*' ;;
+ amazon)
+ if [ "$OS_VER" = 2 ]; then run $SUDO yum -y update kernel
+ else run $SUDO dnf -y update "${R_PKG:-kernel}" --releasever=latest; fi ;;
+ fedora) run $SUDO dnf -y upgrade --refresh 'kernel*' ;;
+ debian|debianlike|ubuntu|proxmox) patch_apt ;;
+ sles|leap) zypper_run refresh && zypper_run update "kernel-${KREL##*-}" ;;
+ slemicro) run $SUDO transactional-update --non-interactive patch ;;
+ microos) run $SUDO transactional-update --interactive dup ;;
+ tumbleweed) zypper_run refresh && zypper_ask dup ;;
+ arch) run $SUDO pacman -Syu ;;
+ alpine) run $SUDO apk -U --interactive upgrade ;;
+ *) return 1 ;;
+ esac
+}
+
+# With too little free space in /boot, creating the new kernel/initramfs can half-fail while the
+# default boot entry still changes
+boot_space_ok() {
+ local avail
+ have df || return 0
+ avail=$(df -Pk /boot 2>/dev/null | awk 'NR == 2 { print $4 }')
+ case $avail in ''|*[!0-9]*) return 0 ;; esac
+ [ "$avail" -ge 102400 ] && return 0
+ line "${C_RED}Only $((avail / 1024))MB free in /boot; installing a new kernel may fail, so patching stops here.${C_OFF}"
+ cmt "Remove old kernels, e.g.: sudo dnf remove --oldinstallonly kernel / sudo apt autoremove --purge"
+ return 1
+}
+
+# After transactional-update: does the next boot snapshot (btrfs default subvolume) contain a fixed kernel?
+snapshot_fix_ready() {
+ local out n root k list="" tab pick
+ REBOOT_TARGET=""; REBOOT_KREL=""
+ have btrfs || return 1
+ out=$($SUDO btrfs subvolume get-default / 2>/dev/null) || return 1
+ case $out in */.snapshots/*/snapshot*) ;; *) return 1 ;; esac
+ n=${out##*/.snapshots/}; n=${n%%/*}
+ case $n in ''|*[!0-9]*) return 1 ;; esac
+ root=/.snapshots/$n/snapshot
+ if [ "$FAMILY" = slemicro ]; then
+ $SUDO rpm --root "$root" -q --changelog "kernel-${KREL##*-}" 2>/dev/null | has_fix_text || return 1
+ else
+ tab=$(printf '\t')
+ for k in $($SUDO ls "$root/usr/lib/modules" 2>/dev/null); do list="$list$k$tab$k
+"; done
+ pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode=rpm -v ref="" "$AWK_VERCMP")
+ k=${pick%%"$tab"*}
+ [ -n "$k" ] || return 1
+ upstream_eval "$k"; [ "$E_STATUS" = fixed ] || return 1
+ fi
+ REBOOT_TARGET="snapshot $n"
+}
+
+# After patching, will a reboot come up on a fixed kernel? (in-place replacement first, then next boot kernel)
+reboot_fix_ready() {
+ case $FAMILY in slemicro|microos) snapshot_fix_ready; return ;; esac
+ evaluate_kernel "$KREL" "$RUN_DVER"
+ if [ "$E_INPLACE" = 1 ]; then
+ REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (package replaced under the same name after boot)"
+ return 0
+ fi
+ installed_fix_ready
+}
+
+show_default_kernel() {
+ local d
+ have grubby || return 0
+ d=$($SUDO grubby --default-kernel 2>/dev/null) || return 0
+ [ -n "$d" ] && kv "Default boot kernel" "$d"
+}
+
+# Without grubby, when GRUB_DEFAULT is not 0 (first entry = newest kernel) the next boot kernel is uncertain
+grub_default_custom() {
+ local l v=""
+ have grubby && return 1
+ [ -r /etc/default/grub ] || return 1
+ while IFS= read -r l; do case $l in GRUB_DEFAULT=*) v=${l#GRUB_DEFAULT=} ;; esac; done < /etc/default/grub
+ v=${v#\"}; v=${v%\"}; v=${v#\'}; v=${v%\'}
+ case $v in ''|0) return 1 ;; esac
+ GRUB_DEFAULT_VAL=$v
+}
+
+do_reboot() {
+ local ans grub_custom=0
+ grub_default_custom && grub_custom=1
+ # On a virtualization host, with a GRUB setup that makes the next boot kernel uncertain, or in a
+ # shell without timed input (read -t), ask for a typed 'yes' instead of a countdown
+ if [ "$HV_HOST" = 1 ] || [ "$grub_custom" = 1 ] || [ -z "${BASH_VERSION:-}" ]; then
+ if [ "$HV_HOST" = 1 ]; then
+ line "${C_YEL}This is a virtualization host. Rebooting stops every running VM.${C_OFF}"
+ line "If possible, migrate the guests to another host or shut them down first."
+ fi
+ if [ "$grub_custom" = 1 ]; then
+ line "${C_YEL}GRUB_DEFAULT=$GRUB_DEFAULT_VAL, so the next boot may not use the newest kernel. Check the boot menu settings.${C_OFF}"
+ fi
+ printf ' Type yes to reboot now: '
+ read -r ans || ans=""
+ if [ "$ans" != yes ]; then
+ line "Reboot cancelled. Run 'sudo reboot' when you are ready."
+ return 1
+ fi
+ else
+ printf ' Rebooting in 10 seconds. Press Enter to cancel... '
+ # Reboot only when the timer runs out (exit code > 128). Enter, Ctrl-D (EOF) or errors cancel.
+ read -r -t 10 ans
+ if [ $? -le 128 ]; then
+ printf '\n'
+ line "Reboot cancelled. Run 'sudo reboot' when you are ready."
+ return 1
+ fi
+ printf '\n'
+ fi
+ line "Rebooting. Run this script again after boot to confirm the result."
+ run $SUDO reboot
+}
+
+auto_patch() {
+ setup_sudo || { show_manual; return; }
+ if [ "$FINAL" != reboot ]; then
+ section "Patching"
+ note "Updating kernel packages. sudo may ask for your password."
+ boot_space_ok || { show_manual; return; }
+ if ! patch_now; then
+ line "${C_RED}The package update failed. Check the output above and continue with the manual steps below.${C_OFF}"
+ show_manual
+ return
+ fi
+ if reboot_fix_ready; then
+ FINAL=reboot
+ line "${C_GRN}Fixed kernel confirmed for the next boot: $REBOOT_TARGET${C_OFF}"
+ else
+ line "The update finished, but the fix for $CVE could not be confirmed in the next boot kernel. Not rebooting."
+ if [ -n "${NB_ALT:-}" ]; then
+ line "The fixed kernel $NB_ALT is installed but is not the default boot kernel:"
+ cmd "sudo grubby --set-default /boot/vmlinuz-$NB_ALT && sudo reboot"
+ else
+ line "The distribution may not have released a fixed kernel yet."
+ fi
+ show_manual
+ return
+ fi
+ fi
+ # Never reboot into a kernel whose boot files are missing (dracut/initramfs generation failure etc.)
+ if [ -n "${REBOOT_KREL:-}" ] && ! boot_files_ok "$REBOOT_KREL"; then
+ line "${C_RED}The kernel image or initramfs for $REBOOT_KREL is missing, so the automatic reboot stops here.${C_OFF}"
+ cmt "Rebuild the initramfs: sudo dracut -f --kver $REBOOT_KREL / sudo update-initramfs -u -k $REBOOT_KREL"
+ return
+ fi
+ section "Reboot"
+ show_default_kernel
+ do_reboot
+}
+
+choose_action() {
+ local choice label
+ case $FINAL in
+ reboot) label="Reboot now (apply the installed fixed kernel)" ;;
+ pending|unknown) label="Try updating now (install and reboot if a fixed kernel is out)" ;;
+ *) label="Patch now (install the fixed kernel and reboot)" ;;
+ esac
+ can_autopatch || label="Patch now — automatic patching is not supported on this system"
+
+ section "Choose the next step"
+ if [ -n "$VIRT" ]; then
+ line "${C_YEL}This system is a VM guest ($VIRT). Patching the guest does not protect the host,${C_OFF}"
+ line "${C_YEL}so run this script on the host OS as well and patch it there.${C_OFF}"
+ printf '\n'
+ fi
+ line "1. $label"
+ line "2. Patch manually later (show the commands)"
+ line "3. Exit"
+ while :; do
+ printf '\n Choice [1-3]: '
+ read -r choice || { printf '\n'; return; }
+ case $choice in
+ 1)
+ if can_autopatch; then
+ auto_patch
+ else
+ line "This distribution/kernel cannot be patched automatically. Follow the manual steps below."
+ show_manual
+ fi
+ return ;;
+ 2) show_manual; return ;;
+ 3) line "Exiting."; return ;;
+ *) line "Enter 1, 2 or 3." ;;
+ esac
+ done
+}
+
+print_header() {
+ printf '%s' "$C_BLU"
+ printf '==================================================================\n'
+ printf ' %s (PwnEscape) Linux kernel vulnerability scanner\n' "$CVE"
+ printf ' Missing IPv4 checksum_start validation → guest VM can escape to the host\n'
+ printf ' CVSS 9.3 Critical · every kernel since 2.6.27 · fix commit %s\n' "$FIX_COMMIT"
+ printf '==================================================================\n'
+ printf '%s' "$C_OFF"
+}
+
+print_table_rows() { # $1=table name $2=row prefix
+ local br cmp disp adv pkg
+ while IFS='|' read -r br cmp disp adv pkg; do
+ [ -n "$br" ] && line "$2$disp or later ($adv)"
+ done <<EOF
+$(vendor_table "$1")
+EOF
+}
+
+print_matrix() {
+ printf '%s%s status and fixed versions by distribution (as of %s)%s\n' "$C_BLU" "$CVE" "$DATA_DATE" "$C_OFF"
+ section "Upstream kernel (kernel.org)"
+ line "Affected: every kernel since 2.6.27 / fix commit $FIX_COMMIT"
+ line "Fixed: $UPSTREAM_TEXT"
+ line "Other branches (5.4, 6.7-6.11, 6.13-6.17, 7.0, 7.1, ...) are end of life — move to a supported branch"
+ section "AlmaLinux — fixed (2026-10-16, almalinux-testing repo)"
+ print_table_rows alma8 "8: "
+ print_table_rows alma9 "9: "
+ print_table_rows alma10 "10: "
+ line "Kitten 10: included in the next regular kernel update (no separate build)"
+ section "Red Hat Enterprise Linux 7 / 8 / 9 / 10 — affected · no fix yet"
+ line "Red Hat CVSS 8.8 (Important), no mitigation, both kernel and kernel-rt affected"
+ section "Rocky Linux · CentOS Stream · other RHEL compatibles — no fixed kernel published"
+ line "Expected to follow the RHEL fix (including Oracle Linux RHCK)"
+ section "Oracle Linux UEK — fixed (2026-10-16)"
+ print_table_rows uek ""
+ section "Amazon Linux 2 — fixed"
+ print_table_rows al2 ""
+ section "Amazon Linux 2023 — fixed"
+ print_table_rows al2023 ""
+ line "AWS states that the EC2 · Lambda · Fargate host infrastructure is not affected"
+ section "Debian"
+ line "bookworm (12): 6.1.190-1 or later (DLA-4841-1, bookworm-security)"
+ line "trixie (13): 6.12.113-1 or later (DSA-6568-1, trixie-security)"
+ line "forky / sid: 7.2.11-1 or later"
+ line "bullseye (11) and older: not tracked by Debian security (end of life)"
+ section "Ubuntu — no fix yet"
+ line "14.04 - 26.04: every kernel package (linux, -aws, -azure, -gcp, -oracle, -hwe, ...) 'Needs evaluation'"
+ section "SUSE / openSUSE"
+ line "SLES 12 SP5 · 15 SP4-SP7 · 16.x, SLE Micro 5.3-5.5 · 6.x, Leap 16: affected · no fix yet (Pending)"
+ line "openSUSE Leap 15.x, SLES 15 SP1-SP3, SLE Micro 5.0-5.2: end of life"
+ section "Fedora · Arch · openSUSE Tumbleweed · Alpine · Gentoo, ..."
+ line "Distribution kernels track upstream stable, so the upstream fixed versions above apply"
+ printf '\n'
+ note "Vendor advisories keep changing. Check each distribution's security tracker for the latest status."
+}
+
+# -----------------------------------------------------------------------------
+main() {
+ parse_args "$@"
+ setup_colors
+ if [ "$LIST_ONLY" = 1 ]; then print_matrix; exit 0; fi
+
+ if [ "$OFFLINE" = 0 ]; then
+ # Read the same values as uname -r / -v straight from /proc (no subprocess)
+ if [ -r /proc/sys/kernel/osrelease ] && [ -r /proc/sys/kernel/version ]; then
+ read -r KREL < /proc/sys/kernel/osrelease
+ read -r KVER < /proc/sys/kernel/version
+ else
+ [ "$(uname -s)" = Linux ] ||
+ die "Run this on Linux. (To judge another server's values use --uname-r / --uname-v / --os-release)"
+ KREL=$(uname -r); KVER=$(uname -v)
+ fi
+ fi
+ RUN_DVER=""
+ case $KVER in *" Debian "*) RUN_DVER=${KVER#* Debian }; RUN_DVER=${RUN_DVER%% *} ;; esac
+
+ detect_os
+ VIRT=""; CTR=""; HV_HOST=0
+ [ "$OFFLINE" = 0 ] && detect_virt_container
+ case $FAMILY in wsl|wsl1) VIRT="" ;; esac # the WSL host is Windows, so the 'patch the Linux host' advice does not apply
+ # The running Debian kernel version comes only from uname -v. The package version on disk may have changed since boot.
+
+ print_header
+ section "System information"
+ kv "Distribution" "${OS_NAME:-unknown} [${OS_ID:-?} ${OS_VER:-?}]"
+ kv "Running kernel" "$KREL"
+ [ -n "$RUN_DVER" ] && kv "Debian kernel package" "$RUN_DVER"
+ if [ "$OFFLINE" = 1 ]; then
+ kv "Check mode" "judging supplied values (local checks skipped)"
+ elif [ "$FAMILY" = wsl ]; then
+ kv "Virtualization" "WSL2 (running on Windows Hyper-V)"
+ elif [ -n "$VIRT" ]; then
+ kv "Virtualization" "VM guest ($VIRT)"
+ else
+ kv "Virtualization" "physical server or not detected"
+ fi
+ kv "Method" "$(family_label)"
+
+ if [ -n "$CTR" ]; then
+ section "Verdict"
+ kv "Status" "$(status_label unknown)"
+ line "Running inside a container ($CTR). Containers share the host kernel, so the"
+ line "container image's distribution cannot be used for the verdict. Run this on the host."
+ line "If you cannot reach the host directly, judge the host's values instead:"
+ cmd "bash $0 --uname-r '<host uname -r>' --uname-v '<host uname -v>' --os-release <copy of the host os-release>"
+ exit 4
+ fi
+
+ evaluate_kernel "$KREL" "$RUN_DVER"
+ 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
+ FINAL=$R_STATUS; LP_NAME=""; REBOOT_TARGET=""; REBOOT_KREL=""; NEXT_WARN=""; NB_ALT=""
+ # WSL never boots a distribution kernel, so installed-kernel and livepatch checks are meaningless there
+ if [ "$OFFLINE" = 0 ] && [ "$FAMILY" != wsl ] && [ "$FAMILY" != wsl1 ]; then
+ case $R_STATUS in
+ vulnerable|pending|eol|unknown)
+ if livepatch_has_fix; then
+ FINAL=livepatched
+ elif [ "$R_INPLACE" = 1 ]; then
+ FINAL=reboot; REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (package replaced under the same name after boot)"
+ elif installed_fix_ready; then
+ FINAL=reboot
+ fi ;;
+ fixed)
+ # Safe now, but if the next boot kernel is not fixed, the system becomes vulnerable again on reboot
+ next_boot_eval
+ if [ -n "$NB_KREL" ]; then
+ case $NB_STATUS in fixed|unaffected) ;; *) NEXT_WARN=$NB_KREL ;; esac
+ fi ;;
+ esac
+ fi
+
+ section "Verdict"
+ kv "Status" "$(status_label "$FINAL")"
+ kv "Basis" "$R_BASIS"
+ [ -n "$R_FIXED" ] && kv "Fixed version" "$R_FIXED${R_ADV:+ ($R_ADV)}"
+ [ "$FINAL" = livepatched ] && kv "Livepatch" "$LP_NAME"
+ [ "$FINAL" = reboot ] && kv "Fixed kernel for next boot" "$REBOOT_TARGET"
+ [ -n "$NEXT_WARN" ] &&
+ kv "Warning" "${C_YEL}The next boot kernel $NEXT_WARN is not fixed. Rebooting may make the system vulnerable again.${C_OFF}"
+ if [ "$FINAL" != reboot ] && [ -n "$NB_ALT" ]; then
+ kv "Note" "the fixed kernel $NB_ALT is installed but is not the default boot kernel → sudo grubby --set-default /boot/vmlinuz-$NB_ALT"
+ fi
+
+ if [ "$OFFLINE" = 0 ]; then
+ section "Exposure"
+ check_exposure
+ fi
+
+ case $FINAL in
+ fixed|unaffected|livepatched)
+ section "Remediation"
+ print_remedy "$FINAL"
+ print_refs_section ;;
+ *)
+ # Show the menu only when both input and output are a terminal (with output redirected to a
+ # file the prompt would be invisible and the script would seem to hang)
+ if [ "$OFFLINE" = 0 ] && [ -t 0 ] && [ -t 1 ]; then choose_action; else show_manual; fi ;;
+ esac
+
+ case $FINAL in
+ fixed|unaffected|livepatched) exit 0 ;;
+ vulnerable) exit 1 ;;
+ reboot) exit 2 ;;
+ pending|eol) exit 3 ;;
+ *) exit 4 ;;
+ esac
+}
+
+main "$@"
diff --git a/scanner-cve-2026-99999_kr.sh b/scanner-cve-2026-99999_kr.sh
@@ -0,0 +1,1679 @@
+#!/usr/bin/env bash
+# =============================================================================
+# scanner-cve-2026-99999_kr.sh (한국어 출력판 · 영어판: scanner-cve-2026-99999.sh)
+# CVE-2026-99999 "PwnEscape" — 리눅스 커널 취약점 영향 점검 및 패치 안내
+# -----------------------------------------------------------------------------
+# 구성요소 : 커널 IPv4 스택 (skb_checksum_help → ip_do_fragment)
+# 원인 : virtio-net 헤더·패킷 소켓이 넘긴 checksum_start/csum_offset 을
+# IPv4 헤더와 대조하지 않아, 체크섬 완료 시 이미 검증된 헤더(IHL)를
+# 덮어씀 (CWE-20)
+# 영향 : 게스트 VM → 호스트 메모리 유출 · 호스트 크래시 · 호스트 root 코드 실행
+# 점수 : CVSS 3.1 9.3 Critical (커널 CNA) / 8.8 Important (Red Hat)
+# 도입 : Linux 2.6.27 (f43798c27684)
+# 수정 : 6785011f8b16 "ipv4: validate checksum_start before completing checksum"
+# 우회책 : 공식 우회책 없음 — 수정된 커널로 교체하고 재부팅해야 함
+#
+# 판정 데이터는 2026-10-20 시점의 각 벤더 공지 기준이다. 그 이후 출시된 패치는
+# 설치된 커널 패키지 changelog 에서 CVE/수정 커밋을 찾아 보완 판정한다.
+#
+# 사용법 : bash scanner-cve-2026-99999_kr.sh [-l] [--no-color] [...] (-h 참고)
+# 판정 후 메뉴: 1) 지금 패치(커널 업데이트 → 재부팅)
+# 2) 나중에 수동 패치(명령어 출력) 3) 종료
+# 터미널이 아닌 곳(cron·파이프)에서 실행하면 메뉴 없이 2번처럼 동작
+# 종료 코드: 0 안전 / 1 취약(패치 있음) / 2 수정 커널 설치됨·재부팅 필요
+# 3 취약(벤더 패치 미출시 또는 지원 종료) / 4 판단 불가
+# =============================================================================
+
+# sh/dash 로 실행했어도 bash 가 있으면 bash 로 다시 실행 (read -t 등 bash 기능 사용)
+if [ -z "${BASH_VERSION:-}" ] && [ -f "$0" ] && command -v bash >/dev/null 2>&1; then
+ exec bash "$0" "$@"
+fi
+
+set -u
+
+CVE="CVE-2026-99999"
+FIX_TITLE="ipv4: validate checksum_start before completing checksum"
+FIX_COMMIT="6785011f8b16"
+FIX_URL="https://git.kernel.org/stable/c/6785011f8b16abf40b1e9d8b2e332232b3e68822"
+DATA_DATE="2026-10-20"
+# 이 해 이전에 작성된 changelog 항목에는 수정이 있을 수 없다 (CVE 예약 2026-09, 수정 커밋 2026-10).
+# changelog 는 최신 항목부터 나오므로 이보다 오래된 항목에 닿으면 읽기를 멈춘다.
+CHANGELOG_CUTOFF_YEAR=2026
+
+# 업스트림 stable 브랜치별 최초 수정 버전 (커널 CNA, 2026-10-15). 메인라인은 7.3-rc7.
+UPSTREAM_FIXED="5.10.272 5.15.223 6.1.190 6.6.159 6.12.113 6.18.57 7.2.11"
+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 이상"
+
+OS_RELEASE_FILE=/etc/os-release
+OSR_GIVEN=0
+KREL=""
+KVER=""
+OFFLINE=0
+USE_COLOR=1
+LIST_ONLY=0
+SUDO=""
+OS_MAJ_NUM=0
+# set -u 에서 어느 경로로 호출돼도 안전하도록 전역 상태를 미리 정의
+RUN_DVER=""; VIRT=""; CTR=""; HV_HOST=0; FINAL=""; R_MODE=""; R_PKG=""; R_ADV=""; R_FIXED=""
+REBOOT_TARGET=""; REBOOT_KREL=""; NB_ALT=""; NEXT_WARN=""; LP_NAME=""; E_INPLACE=0
+
+# -----------------------------------------------------------------------------
+# 벤더별 수정 커널
+# 형식: 브랜치|비교용 버전-릴리스(dist 태그 제외)|수정 패키지|공지|패키지 이름
+# -----------------------------------------------------------------------------
+vendor_table() {
+ case $1 in
+ 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' ;;
+ 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' ;;
+ 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' ;;
+ uek)
+ cat <<'EOF'
+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
+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
+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
+EOF
+ ;;
+ al2)
+ cat <<'EOF'
+4.14|4.14.355-287.753|kernel-4.14.355-287.753.amzn2|ALAS2-2026-3962|kernel
+5.4|5.4.302-229.498|kernel-5.4.302-229.498.amzn2|ALAS2KERNEL-5.4-2026-131|kernel
+5.10|5.10.269-270.1108|kernel-5.10.269-270.1108.amzn2|ALAS2KERNEL-5.10-2026-133|kernel
+5.15|5.15.221-157.262|kernel-5.15.221-157.262.amzn2|ALAS2KERNEL-5.15-2026-117|kernel
+EOF
+ ;;
+ al2023)
+ cat <<'EOF'
+6.1|6.1.188-233.385|kernel-6.1.188-233.385.amzn2023|ALAS2023-2026-3197|kernel
+6.12|6.12.110-135.201|kernel6.12-6.12.110-135.201.amzn2023|ALAS2023-2026-3196|kernel6.12
+6.18|6.18.54-121.166|kernel6.18-6.18.54-121.166.amzn2023|ALAS2023-2026-3192|kernel6.18
+EOF
+ ;;
+ esac
+}
+
+# -----------------------------------------------------------------------------
+# 출력 도우미
+# -----------------------------------------------------------------------------
+setup_colors() {
+ local e
+ if [ "$USE_COLOR" = 1 ] && [ -t 1 ] && [ -z "${NO_COLOR:-}" ]; then
+ e=$(printf '\033')
+ C_RED="$e[1;31m"; C_GRN="$e[1;32m"; C_YEL="$e[1;33m"
+ C_BLU="$e[1;36m"; C_DIM="$e[2m"; C_OFF="$e[0m"
+ else
+ C_RED=; C_GRN=; C_YEL=; C_BLU=; C_DIM=; C_OFF=
+ fi
+}
+section() { printf '\n%s■ %s%s\n' "$C_BLU" "$1" "$C_OFF"; }
+kv() { printf ' %s: %s\n' "$1" "$2"; }
+line() { printf ' %s\n' "$*"; }
+note() { printf ' %s※ %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
+cmd() { printf ' %s%s%s\n' "$C_GRN" "$*" "$C_OFF"; }
+cmt() { printf ' %s# %s%s\n' "$C_DIM" "$*" "$C_OFF"; }
+die() { printf '오류: %s\n' "$*" >&2; exit 4; }
+
+# 명령 존재 여부를 한 번만 PATH 에서 찾고 결과를 캐시
+have() {
+ local v="HAVE_${1//[!A-Za-z0-9]/_}"
+ eval "case \${$v-} in 1) return 0 ;; 0) return 1 ;; esac"
+ if command -v "$1" >/dev/null 2>&1; then eval "$v=1"; return 0; fi
+ eval "$v=0"; return 1
+}
+
+# 표준 입력(최신 항목이 먼저 나오는 changelog)에 수정 기록이 있으면 참.
+# - CVE 번호(앞뒤가 영숫자가 아닌 경우만) 또는 "ipv4: validate checksum_start ..." 커밋 제목
+# - Revert 가 들어간 줄은 수정으로 치지 않음
+# - 항목 날짜(rpm "* Mon Oct 13 2026 ..." / deb " -- 이름 Mon, 13 Oct 2026 ...")가
+# CHANGELOG_CUTOFF_YEAR 보다 오래되면 거기서 멈춘다 → 수 MB changelog 도 최근 부분만 읽음
+# 일치하거나 멈추면 바로 끝나므로 앞단 rpm/gzip 도 SIGPIPE 로 함께 멈춘다.
+has_fix_text() {
+ LC_ALL=C awk -v cve="$CVE" -v title="$FIX_TITLE" -v cutoff="$CHANGELOG_CUTOFF_YEAR" '
+ BEGIN { cve = tolower(cve); title = tolower(title); found = 0 }
+ {
+ l = tolower($0)
+ if ((substr(l, 1, 2) == "* " || substr(l, 1, 4) == " -- ") &&
+ match(l, / (19|20)[0-9][0-9]( |$)/)) {
+ if (substr(l, RSTART + 1, 4) + 0 < cutoff) exit
+ }
+ if (index(l, "revert")) next
+ p = index(l, cve)
+ if (p) {
+ a = (p > 1) ? substr(l, p - 1, 1) : ""
+ b = substr(l, p + length(cve), 1)
+ if (a !~ /[0-9a-z]/ && b !~ /[0-9a-z]/) { found = 1; exit }
+ }
+ if (index(l, title)) { found = 1; exit }
+ }
+ END { exit found ? 0 : 1 }'
+}
+
+usage() {
+ cat <<EOF
+사용법: $0 [옵션]
+
+이 시스템의 배포판과 실행 중인 커널을 확인해 $CVE (PwnEscape) 영향 여부를
+판정합니다. 취약하면 다음 중 하나를 고릅니다:
+
+ 1. 지금 패치 배포판 패키지 관리자로 커널을 업데이트하고 재부팅
+ 2. 나중에 수동 패치 실행할 명령어만 출력
+ 3. 종료
+
+점검은 일반 사용자로도 됩니다. 1번은 root 권한이 필요하며 sudo 로 실행합니다.
+터미널이 아닌 곳(cron·파이프)에서 실행하면 메뉴 없이 2번처럼 명령어만 출력합니다.
+
+옵션:
+ -l, --list 배포판별 수정 버전 현황표만 출력
+ --no-color 색상 없이 출력
+ --os-release 파일 os-release 파일 지정 (기본: /etc/os-release)
+ --uname-r 문자열 커널 릴리스 지정 (다른 서버의 'uname -r' 값으로 판정)
+ --uname-v 문자열 커널 빌드 문자열 지정 ('uname -v', Debian 판정에 사용)
+ -h, --help 도움말
+
+ --uname-r 를 지정하면 이 시스템에 대한 로컬 검사(changelog, 설치된 커널,
+ 라이브패치, 가상화 호스트 여부)는 건너뜁니다.
+
+종료 코드:
+ 0 안전 1 취약(패치 있음) 2 수정 커널 설치됨·재부팅 필요
+ 3 취약(벤더 패치 미출시 또는 지원 종료) 4 판단 불가
+EOF
+}
+
+parse_args() {
+ while [ $# -gt 0 ]; do
+ case $1 in
+ -l|--list) LIST_ONLY=1 ;;
+ --no-color) USE_COLOR=0 ;;
+ --os-release) [ $# -ge 2 ] || die "--os-release 에 파일 경로가 필요합니다"
+ OS_RELEASE_FILE=$2; OSR_GIVEN=1; shift
+ [ -r "$OS_RELEASE_FILE" ] || die "os-release 파일을 읽을 수 없습니다: $OS_RELEASE_FILE" ;;
+ --uname-r) [ $# -ge 2 ] || die "--uname-r 에 값이 필요합니다"; KREL=$2; OFFLINE=1; shift ;;
+ --uname-v) [ $# -ge 2 ] || die "--uname-v 에 값이 필요합니다"; KVER=$2; shift ;;
+ -h|--help) usage; exit 0 ;;
+ *) usage >&2; exit 4 ;;
+ esac
+ shift
+ done
+ # 실제 커널 릴리스와 지정한 빌드 문자열이 섞이면 판정이 틀어진다
+ if [ -n "$KVER" ] && [ "$OFFLINE" = 0 ]; then
+ die "--uname-v 는 --uname-r 와 함께 지정해야 합니다"
+ fi
+}
+
+# -----------------------------------------------------------------------------
+# 버전 비교 (rpm: rpmvercmp, deb: dpkg 규칙). dpkg 가 있으면 deb 는 dpkg 사용.
+# -----------------------------------------------------------------------------
+AWK_VERCMP='
+function numcmp(x, y) {
+ sub(/^0+/, "", x); sub(/^0+/, "", y)
+ if (length(x) != length(y)) return (length(x) < length(y)) ? -1 : 1
+ if (x == y) return 0
+ return (x < y) ? -1 : 1
+}
+function splitvr(s, i) {
+ V = s; R = ""
+ for (i = length(s); i > 0; i--)
+ if (substr(s, i, 1) == "-") { V = substr(s, 1, i - 1); R = substr(s, i + 1); return }
+}
+function epoch(s) {
+ E = 0; S = s
+ if (match(s, /^[0-9]+:/)) { E = substr(s, 1, RLENGTH - 1); S = substr(s, RLENGTH + 1) }
+}
+function rpmvercmp(a, b, sa, sb, r) {
+ if (a == b) return 0
+ while (a != "" || b != "") {
+ sub(/^[^A-Za-z0-9~^]+/, "", a); sub(/^[^A-Za-z0-9~^]+/, "", b)
+ if (substr(a, 1, 1) == "~" || substr(b, 1, 1) == "~") {
+ if (substr(a, 1, 1) != "~") return 1
+ if (substr(b, 1, 1) != "~") return -1
+ a = substr(a, 2); b = substr(b, 2); continue
+ }
+ if (substr(a, 1, 1) == "^" || substr(b, 1, 1) == "^") {
+ if (a == "") return -1
+ if (b == "") return 1
+ if (substr(a, 1, 1) != "^") return 1
+ if (substr(b, 1, 1) != "^") return -1
+ a = substr(a, 2); b = substr(b, 2); continue
+ }
+ if (a == "" || b == "") break
+ if (substr(a, 1, 1) ~ /[0-9]/) {
+ match(a, /^[0-9]+/); sa = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1)
+ if (!match(b, /^[0-9]+/)) return 1
+ sb = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1)
+ r = numcmp(sa, sb)
+ } else {
+ match(a, /^[A-Za-z]+/); sa = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1)
+ if (!match(b, /^[A-Za-z]+/)) return -1
+ sb = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1)
+ r = (sa < sb) ? -1 : (sa > sb) ? 1 : 0
+ }
+ if (r) return r
+ }
+ if (a == "" && b == "") return 0
+ return (a == "") ? -1 : 1
+}
+function rpmevr(a, b, ea, eb, va, vb, ra, rb, r) {
+ epoch(a); ea = E; a = S
+ epoch(b); eb = E; b = S
+ r = numcmp(ea, eb); if (r) return r
+ splitvr(a); va = V; ra = R
+ splitvr(b); vb = V; rb = R
+ r = rpmvercmp(va, vb); if (r) return r
+ return rpmvercmp(ra, rb)
+}
+function dorder(c) {
+ if (c == "") return 0
+ if (c == "~") return -1
+ if (c ~ /[A-Za-z]/) return index(ASCII, c) + 31
+ return index(ASCII, c) + 31 + 256
+}
+function dvercmp(a, b, ca, cb, oa, ob, da, db, r) {
+ while (a != "" || b != "") {
+ while ((a != "" && substr(a, 1, 1) !~ /[0-9]/) || (b != "" && substr(b, 1, 1) !~ /[0-9]/)) {
+ ca = substr(a, 1, 1); if (ca ~ /[0-9]/) ca = ""
+ cb = substr(b, 1, 1); if (cb ~ /[0-9]/) cb = ""
+ oa = dorder(ca); ob = dorder(cb)
+ if (oa != ob) return (oa < ob) ? -1 : 1
+ a = substr(a, 2); b = substr(b, 2)
+ }
+ da = ""; db = ""
+ if (match(a, /^[0-9]+/)) { da = substr(a, 1, RLENGTH); a = substr(a, RLENGTH + 1) }
+ if (match(b, /^[0-9]+/)) { db = substr(b, 1, RLENGTH); b = substr(b, RLENGTH + 1) }
+ r = numcmp(da, db); if (r) return r
+ }
+ return 0
+}
+function debcmp(a, b, ea, eb, ua, ub, ra, rb, r) {
+ epoch(a); ea = E; a = S
+ epoch(b); eb = E; b = S
+ r = numcmp(ea, eb); if (r) return r
+ splitvr(a); ua = V; ra = R
+ splitvr(b); ub = V; rb = R
+ r = dvercmp(ua, ub); if (r) return r
+ return dvercmp(ra, rb)
+}
+function cmp(x, y) { return (mode == "deb") ? debcmp(x, y) : rpmevr(x, y) }
+BEGIN {
+ for (i = 32; i < 127; i++) ASCII = ASCII sprintf("%c", i)
+ if (op != "pick") { print cmp(a, b); exit }
+}
+# op=pick: "비교키<TAB>..." 줄 가운데 ref 보다 크고 가장 큰 줄 하나를 출력 (한 번의 실행으로 O(N))
+{
+ k = $0; sub(/\t.*/, "", k)
+ if (cmp(k, ref) > 0 && (best == "" || cmp(k, best) > 0)) { best = k; bestline = $0 }
+}
+END { if (op == "pick" && bestline != "") print bestline }'
+
+ver_cmp() { # $1=rpm|deb $2 $3 → -1 / 0 / 1
+ if [ "$1" = deb ] && have dpkg; then
+ if dpkg --compare-versions "$2" lt "$3"; then echo -1
+ elif dpkg --compare-versions "$2" gt "$3"; then echo 1
+ else echo 0; fi
+ return
+ fi
+ LC_ALL=C awk -v mode="$1" -v a="$2" -v b="$3" "$AWK_VERCMP"
+}
+ver_lt() { [ "$(ver_cmp "$1" "$2" "$3")" = "-1" ]; }
+
+# "4.18.0-553.1.1" → BRANCH=4.18 (프로세스 생성 없음)
+branch_of() {
+ local v=${1%%[!0-9.]*} maj rest
+ BRANCH=""
+ maj=${v%%.*}; rest=${v#*.}
+ [ -n "$maj" ] && [ "$rest" != "$v" ] || return 0
+ rest=${rest%%.*}
+ [ -n "$rest" ] && BRANCH="$maj.$rest"
+}
+
+# "4.18.0-553.1.1.el8_10.x86_64+debug" → EVR=4.18.0-553.1.1 (프로세스 생성 없음)
+rpm_evr_of_krel() {
+ local k=${1%%+*}
+ case $k in
+ *.x86_64|*.aarch64|*.ppc64le|*.ppc64|*.s390x|*.i[3-6]86|*.armv7hl|*.armv7l|*.riscv64|*.noarch) k=${k%.*} ;;
+ esac
+ case ${k##*.} in el[0-9]*|amzn[0-9]*|fc[0-9]*) k=${k%.*} ;; esac
+ EVR=$k
+}
+
+# -----------------------------------------------------------------------------
+# 시스템 정보 수집
+# -----------------------------------------------------------------------------
+# os-release 를 한 번에 읽는다 (따옴표·CRLF 처리, 프로세스 생성 없음)
+read_os_release() {
+ local key val cr
+ OS_ID=""; OS_LIKE=""; OS_VER=""; OS_NAME=""; OS_CODENAME=""
+ [ -r "$OS_RELEASE_FILE" ] || OS_RELEASE_FILE=/usr/lib/os-release
+ if [ ! -r "$OS_RELEASE_FILE" ]; then
+ # os-release 가 없는 옛 RHEL 계열(5/6): "CentOS release 6.10 (Final)"
+ if [ "$OFFLINE" = 0 ] && [ -r /etc/redhat-release ]; then
+ read -r OS_NAME < /etc/redhat-release
+ OS_ID=rhel
+ case $OS_NAME in *"release "[0-9]*) val=${OS_NAME#*release }; OS_VER=${val%% *} ;; esac
+ fi
+ return 0
+ fi
+ cr=$(printf '\r')
+ local pretty="" name=""
+ while IFS='=' read -r key val || [ -n "$key" ]; do
+ val=${val%"$cr"}
+ case $val in
+ \"*\") val=${val#\"}; val=${val%\"} ;;
+ \'*\') val=${val#\'}; val=${val%\'} ;;
+ esac
+ case $key in
+ ID) OS_ID=$val ;;
+ ID_LIKE) OS_LIKE=$val ;;
+ VERSION_ID) OS_VER=$val ;;
+ PRETTY_NAME) pretty=$val ;;
+ NAME) name=$val ;;
+ VERSION_CODENAME) OS_CODENAME=$val ;;
+ esac
+ done < "$OS_RELEASE_FILE"
+ OS_NAME=${pretty:-$name}
+}
+
+detect_os() {
+ read_os_release
+ OS_MAJOR=${OS_VER%%.*}
+ case $OS_MAJOR in ''|*[!0-9]*) OS_MAJ_NUM=0 ;; *) OS_MAJ_NUM=$OS_MAJOR ;; esac
+
+ FAMILY=generic
+ case $KREL in
+ *-Microsoft) FAMILY=wsl1; return ;; # WSL1: 리눅스 커널이 아니라 Windows 의 호환 계층
+ *[Mm]icrosoft*) FAMILY=wsl; return ;;
+ *-pve) FAMILY=proxmox; return ;;
+ esac
+ case $OS_ID in
+ almalinux) case $OS_NAME in *Kitten*) FAMILY=kitten ;; *) FAMILY=alma ;; esac ;;
+ rhel) FAMILY=rhel ;;
+ centos) FAMILY=centos ;;
+ rocky) FAMILY=rocky ;;
+ ol) FAMILY=oracle ;;
+ amzn) FAMILY=amazon ;;
+ fedora) FAMILY=fedora ;;
+ debian) FAMILY=debian ;;
+ ubuntu) FAMILY=ubuntu ;;
+ sles|sled|sles_sap|sle_hpc) FAMILY=sles ;;
+ sle-micro|sl-micro) FAMILY=slemicro ;;
+ opensuse-leap) FAMILY=leap ;;
+ opensuse-tumbleweed|opensuse-slowroll) FAMILY=tumbleweed ;;
+ opensuse-microos|opensuse-aeon|opensuse-kalpa) FAMILY=microos ;;
+ arch|archarm|manjaro|manjaro-arm|endeavouros|cachyos|garuda|artix) FAMILY=arch ;;
+ alpine|postmarketos) FAMILY=alpine ;;
+ gentoo) FAMILY=gentoo ;;
+ *)
+ case " $OS_LIKE " in
+ *" ubuntu "*) FAMILY=ubuntu ;;
+ *" rhel "*|*" centos "*|*" fedora "*)
+ case $KREL in *.el[0-9]*) FAMILY=rhelclone ;; esac ;;
+ *" arch "*) FAMILY=arch ;;
+ *" debian "*) FAMILY=debianlike ;;
+ esac ;;
+ esac
+}
+
+family_label() {
+ case $FAMILY in
+ alma) echo "AlmaLinux 공지(2026-10-16)의 수정 커널 버전과 비교" ;;
+ kitten) echo "AlmaLinux Kitten — 정기 커널 업데이트로 수정 예정" ;;
+ rhel) echo "Red Hat 공지(패치 미출시) + 커널 changelog 검사" ;;
+ centos|rocky|rhelclone) echo "RHEL 계열 공지 + 커널 changelog 검사" ;;
+ oracle) echo "Oracle Linux ELSA 공지(UEK)의 수정 커널 버전과 비교" ;;
+ amazon) echo "Amazon Linux ALAS 공지의 수정 커널 버전과 비교" ;;
+ debian) echo "Debian 보안 트래커의 수정 패키지 버전과 비교" ;;
+ debianlike) echo "Debian 파생 — Debian 커널이면 Debian 기준, 아니면 업스트림 기준" ;;
+ ubuntu) echo "Ubuntu CVE 트래커(패치 미출시) + 커널 changelog 검사" ;;
+ proxmox) echo "Proxmox VE 커널 — 커널 changelog 검사" ;;
+ sles|slemicro|leap) echo "SUSE 보안 공지(패치 미출시) + 커널 changelog 검사" ;;
+ fedora|arch|tumbleweed|microos|alpine|gentoo) echo "업스트림 stable 기반 배포판 — 업스트림 수정 버전과 비교" ;;
+ wsl) echo "WSL2 (Microsoft 커널) — 업스트림 수정 버전과 비교" ;;
+ wsl1) echo "WSL1 — 리눅스 커널을 쓰지 않음" ;;
+ *) echo "알 수 없는 배포판 — 업스트림 수정 버전과 비교(참고용)" ;;
+ esac
+}
+
+# 가상화(VIRT)와 컨테이너(CTR)를 systemd-detect-virt 한 번으로 판별
+detect_virt_container() {
+ local v=""
+ VIRT=""; CTR=""
+ if [ -f /.dockerenv ]; then CTR=docker
+ elif [ -f /run/.containerenv ]; then CTR=podman
+ elif [ -s /run/systemd/container ]; then read -r CTR < /run/systemd/container
+ elif [ -n "${KUBERNETES_SERVICE_HOST:-}" ]; then CTR=kubernetes
+ elif [ -r /proc/1/environ ] && tr '\0' '\n' < /proc/1/environ 2>/dev/null | grep -q '^container='; then
+ CTR=container
+ fi
+ if have systemd-detect-virt; then
+ v=$(systemd-detect-virt 2>/dev/null) # 컨테이너 안이면 컨테이너 종류가 우선 출력됨
+ case $v in
+ ''|none) ;;
+ docker|podman|lxc|lxc-libvirt|systemd-nspawn|openvz|rkt|proot|pouch|wsl) [ -n "$CTR" ] || CTR=$v ;;
+ *) VIRT=$v ;;
+ esac
+ else
+ if [ -z "$CTR" ] && grep -qE '/(docker|kubepods|lxc|libpod)' /proc/1/cgroup 2>/dev/null; then
+ CTR=container
+ fi
+ grep -qw hypervisor /proc/cpuinfo 2>/dev/null && VIRT=hypervisor
+ fi
+ [ "$CTR" = wsl ] && CTR=""
+}
+
+# 커널 패키지 changelog 에 수정 기록이 있는지 (RHEL·SUSE·Oracle·Debian·Ubuntu 계열)
+# rpm 은 'rpm -qf --changelog' 한 번으로 소유 패키지 조회와 changelog 출력을 함께 한다.
+# CL_SRC 에 근거 종류(rpm|deb)를 남긴다.
+changelog_has_fix() {
+ local krel=$1 f
+ CL_SRC=""
+ if have rpm; then
+ for f in "/lib/modules/$krel/vmlinuz" "/boot/vmlinuz-$krel"; do
+ [ -e "$f" ] || continue
+ rpm -qf --changelog "$f" 2>/dev/null | has_fix_text && { CL_SRC=rpm; return 0; }
+ break
+ done
+ fi
+ have gzip || return 1
+ CL_SRC=deb
+ for f in /usr/share/doc/linux-image-"$krel"/changelog.Debian.gz \
+ /usr/share/doc/linux-modules-"$krel"/changelog.Debian.gz \
+ /usr/share/doc/linux-image-unsigned-"$krel"/changelog.Debian.gz \
+ /usr/share/doc/proxmox-kernel-"$krel"*/changelog.Debian.gz; do
+ [ -r "$f" ] || continue
+ gzip -dc "$f" 2>/dev/null | has_fix_text && return 0
+ done
+ return 1
+}
+
+# 부팅 가능한 커널 이미지가 남아 있는 커널만 (DKMS 등이 남긴 빈 모듈 디렉터리 제외).
+# Alpine·Gentoo 는 이미지 이름에 커널 릴리스가 없을 수 있어 modules.dep 로 판단.
+list_installed_krels() {
+ local d k
+ for d in /lib/modules/*/; do
+ k=${d%/}; k=${k##*/}
+ [ "$k" = "*" ] && continue
+ if [ -e "/lib/modules/$k/vmlinuz" ] || [ -e "/boot/vmlinuz-$k" ] ||
+ { case $FAMILY in alpine|gentoo) true ;; *) false ;; esac && [ -e "/lib/modules/$k/modules.dep" ]; }; then
+ printf '%s\n' "$k"
+ fi
+ done
+}
+
+# 실행 중인 커널 이미지가 부팅 뒤 같은 이름으로 교체됐는지 (Debian 계열 같은 ABI 제자리 업데이트).
+# dpkg 는 파일 mtime 을 빌드 시각으로 두므로 설치 시각인 ctime 을 부팅 시각(btime)과 비교한다.
+image_replaced_after_boot() {
+ local f c k v bt=""
+ for f in "/boot/vmlinuz-$1" "/lib/modules/$1/vmlinuz" ""; do [ -n "$f" ] && [ -e "$f" ] && break; done
+ [ -n "$f" ] || return 1
+ c=$(stat -c %Z "$f" 2>/dev/null) || return 1
+ while read -r k v; do [ "$k" = btime ] && { bt=$v; break; }; done < /proc/stat
+ [ -n "$bt" ] && [ "$c" -gt "$bt" ]
+}
+
+# grubby 로 다음 부팅 기본 커널을 알 수 있으면 DEFAULT_KREL 에 담는다 (RHEL·Fedora·Amazon 계열)
+default_boot_krel() {
+ local d
+ DEFAULT_KREL=""
+ have grubby || return 1
+ d=$($SUDO grubby --default-kernel 2>/dev/null) || return 1
+ case $d in /boot/vmlinuz-?*) DEFAULT_KREL=${d#/boot/vmlinuz-} ;; *) return 1 ;; esac
+}
+
+# 재부팅 대상 커널의 이미지와 initramfs 가 실제로 있는지 (없으면 부팅 실패 위험)
+boot_files_ok() {
+ local k=$1 f img=0 ird=0
+ case $FAMILY in arch|alpine|gentoo|microos|slemicro) return 0 ;; esac # 이미지 이름에 릴리스가 없음
+ for f in "/boot/vmlinuz-$k" "/lib/modules/$k/vmlinuz" "/boot/Image-$k"; do [ -s "$f" ] && img=1; done
+ for f in "/boot/initramfs-$k.img" "/boot/initrd.img-$k" "/boot/initrd-$k"; do [ -s "$f" ] && ird=1; done
+ [ "$img" = 1 ] && [ "$ird" = 1 ]
+}
+
+# 실행 중인 커널보다 새로 설치된 커널 가운데 가장 최신 것(보통 다음 부팅 기본 커널)
+# 결과: NEW_KREL, NEW_DVER(Debian 패키지 버전). 후보 수집은 셸 내장으로, 비교는 awk 한 번으로.
+newest_installed() {
+ local mode=rpm ref k list="" st pkg ver pick tab
+ NEW_KREL=""; NEW_DVER=""
+ tab=$(printf '\t')
+ if [ "$FAMILY" = debian ]; then
+ have dpkg-query || return 1
+ mode=deb; ref=$RUN_DVER
+ while IFS='|' read -r st pkg ver; do
+ case $st in ii*) ;; *) continue ;; esac
+ k=${pkg#linux-image-}
+ [ -e "/lib/modules/$k/modules.dep" ] || continue
+ list="$list$ver$tab$k$tab$ver
+"
+ done <<EOF
+$(dpkg-query -W -f='${db:Status-Abbrev}|${Package}|${Version}\n' 'linux-image-*' 2>/dev/null)
+EOF
+ else
+ rpm_evr_of_krel "$KREL"; ref=$EVR
+ for k in $(list_installed_krels); do
+ [ "$k" = "$KREL" ] && continue
+ [ "$FAMILY" = proxmox ] && case $k in *-pve) ;; *) continue ;; esac # Proxmox 커널만
+ rpm_evr_of_krel "$k"
+ list="$list$EVR$tab$k$tab
+"
+ done
+ fi
+ [ -n "$list" ] || return 1
+ pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode="$mode" -v ref="$ref" "$AWK_VERCMP")
+ [ -n "$pick" ] || return 1
+ pick=${pick#*"$tab"}
+ NEW_KREL=${pick%%"$tab"*}
+ NEW_DVER=${pick#*"$tab"}
+}
+
+# 다음 부팅 커널을 정하고 판정한다.
+# grubby 로 기본 커널을 알 수 있으면 그것, 아니면 실행 중인 커널보다 새로 설치된 커널 중 최신 것.
+# 결과: NB_KREL / NB_DVER / NB_STATUS (다음 부팅이 지금 커널과 같으면 NB_KREL 비움)
+# NB_ALT = 수정된 커널이 설치돼 있지만 기본 부팅 커널이 아닌 경우 그 커널
+next_boot_eval() {
+ NB_KREL=""; NB_DVER=""; NB_STATUS=""; NB_ALT=""
+ newest_installed || { NEW_KREL=""; NEW_DVER=""; }
+ if default_boot_krel; then
+ [ "$DEFAULT_KREL" != "$KREL" ] && NB_KREL=$DEFAULT_KREL
+ else
+ NB_KREL=$NEW_KREL; NB_DVER=$NEW_DVER
+ fi
+ if [ -n "$NB_KREL" ]; then
+ evaluate_kernel "$NB_KREL" "$NB_DVER"; NB_STATUS=$E_STATUS
+ fi
+ if [ "$NB_STATUS" != fixed ] && [ -n "$NEW_KREL" ] && [ "$NEW_KREL" != "$NB_KREL" ]; then
+ evaluate_kernel "$NEW_KREL" "$NEW_DVER"
+ [ "$E_STATUS" = fixed ] && NB_ALT=$NEW_KREL
+ fi
+ return 0
+}
+
+# 재부팅하면 수정된 커널로 부팅되는지 → REBOOT_TARGET / REBOOT_KREL 설정
+installed_fix_ready() {
+ REBOOT_TARGET=""; REBOOT_KREL=""
+ next_boot_eval
+ [ "$NB_STATUS" = fixed ] || return 1
+ REBOOT_KREL=$NB_KREL
+ REBOOT_TARGET="$NB_KREL${NB_DVER:+ (패키지 $NB_DVER)}"
+}
+
+# 활성화된 커널 라이브패치가 있는지 (sysfs 만 읽음)
+livepatch_active() {
+ local f v
+ for f in /sys/kernel/livepatch/*/enabled; do
+ [ -r "$f" ] || continue
+ read -r v < "$f" 2>/dev/null && [ "$v" = 1 ] && return 0
+ done
+ return 1
+}
+
+# 실행 중인 커널에 라이브패치로 수정이 적용됐는지.
+# 활성 라이브패치가 없으면 rpm 데이터베이스 전체 조회(rpm -qa)를 아예 하지 않는다.
+livepatch_has_fix() {
+ local p rel
+ if livepatch_active; then
+ if have canonical-livepatch && canonical-livepatch status --verbose 2>/dev/null | has_fix_text; then
+ LP_NAME="Canonical Livepatch"; return 0
+ fi
+ if have rpm; then
+ rpm_evr_of_krel "$KREL"; rel=$EVR
+ case $rel in *-default|*-azure|*-rt|*-64kb) rel=${rel%-*} ;; esac
+ # 실행 커널용 패키지만 정확히 (RHEL/SUSE 는 점→밑줄, Amazon 은 점 그대로)
+ for p in $(rpm -qa 'kpatch-patch*' 'kernel-livepatch-*' 2>/dev/null); do
+ case $p in *-"${rel//./_}"-*|*-"$rel"-*) ;; *) continue ;; esac
+ if rpm -q --changelog "$p" 2>/dev/null | has_fix_text; then LP_NAME=$p; return 0; fi
+ done
+ fi
+ fi
+ if have uptrack-show && uptrack-show 2>/dev/null | has_fix_text; then
+ LP_NAME="Oracle Ksplice"; return 0
+ fi
+ return 1
+}
+
+# -----------------------------------------------------------------------------
+# 판정 — 결과는 E_STATUS / E_FIXED / E_BASIS / E_ADV / E_PKG / E_MODE 에 담긴다
+# E_STATUS: fixed | vulnerable | pending | eol | unaffected | unknown
+# -----------------------------------------------------------------------------
+upstream_eval() { # 프로세스 생성 없이 셸 패턴으로만 해석
+ local rel=$1 base maj min pat rest rc="" br v fpat
+ E_MODE=upstream
+ base=${rel%%[!0-9.]*}
+ maj=${base%%.*}; rest=${base#*.}
+ min=${rest%%.*}; pat=${rest#*.}; pat=${pat%%.*}
+ [ "$pat" = "$rest" ] && pat=0 # x.y 형식
+ case $maj:$min:$pat in
+ *[!0-9:]*|:*|*::*|*:) E_STATUS=unknown; E_BASIS="커널 버전 형식을 해석할 수 없음 ($rel)"; return ;;
+ esac
+ [ "$rest" = "$base" ] && { E_STATUS=unknown; E_BASIS="커널 버전 형식을 해석할 수 없음 ($rel)"; return; }
+ base="$maj.$min.$pat"
+ # rc 번호: "-rc6", ".rc6", Ubuntu mainline "070300rc5" 처럼 영문자가 아닌 문자 뒤의 rcN
+ case $rel in
+ *[!A-Za-z]rc[0-9]*) rc=${rel#*[!A-Za-z]rc}; rc=${rc%%[!0-9]*} ;;
+ esac
+ br="$maj.$min"
+
+ if [ "$maj" -lt 2 ] || { [ "$maj" -eq 2 ] && [ "$min" -lt 6 ]; } || { [ "$br" = 2.6 ] && [ "$pat" -lt 27 ]; }; then
+ case $rel in
+ *.el[0-9]*|*.amzn*) # 벤더 커널은 옛 버전 번호에 새 네트워크 코드를 백포트했을 수 있음
+ E_STATUS=unknown; E_BASIS="$base 벤더 커널 — 버전 번호는 2.6.27 이전이지만 백포트 여부 확인 필요" ;;
+ *)
+ E_STATUS=unaffected; E_BASIS="$base — 취약 코드가 들어온 2.6.27 이전 커널" ;;
+ esac
+ return
+ fi
+ if [ "$maj" -gt 7 ] || { [ "$maj" -eq 7 ] && [ "$min" -ge 3 ]; }; then
+ E_FIXED="7.3-rc7"
+ if [ "$br" = 7.3 ] && [ -n "$rc" ] && [ "$rc" -lt 7 ]; then
+ E_STATUS=vulnerable; E_BASIS="7.3-rc$rc < 메인라인 수정 시점 7.3-rc7"
+ else
+ E_STATUS=fixed; E_BASIS="$base — 메인라인 7.3-rc7 이후 커널(수정 포함)"
+ fi
+ return
+ fi
+ for v in $UPSTREAM_FIXED; do
+ [ "${v%.*}" = "$br" ] || continue
+ fpat=${v##*.}; E_FIXED=$v
+ if [ "$pat" -ge "$fpat" ]; then
+ E_STATUS=fixed; E_BASIS="$base ≥ 업스트림 수정 버전 $v"
+ else
+ E_STATUS=vulnerable; E_BASIS="$base < 업스트림 수정 버전 $v"
+ fi
+ return
+ done
+ E_STATUS=eol; E_BASIS="업스트림 $br 브랜치는 지원 종료(EOL) — 이 브랜치용 수정 버전이 나오지 않음"
+}
+
+nonvendor_eval() { # $1=커널 릴리스 $2=사유
+ upstream_eval "$1"
+ E_BASIS="$2 → 업스트림 기준: $E_BASIS"
+}
+
+rpm_table_eval() { # $1=커널 릴리스 $2=테이블 이름
+ local evr br t_br t_cmp t_disp t_adv t_pkg
+ rpm_evr_of_krel "$1"; evr=$EVR
+ branch_of "$evr"; br=$BRANCH
+ while IFS='|' read -r t_br t_cmp t_disp t_adv t_pkg; do
+ [ -n "$t_br" ] && [ "$t_br" = "$br" ] || continue
+ E_FIXED=$t_disp; E_ADV=$t_adv; E_PKG=$t_pkg
+ if ver_lt rpm "$evr" "$t_cmp"; then
+ E_STATUS=vulnerable; E_BASIS="커널 $evr < 수정 버전 $t_cmp"
+ else
+ E_STATUS=fixed; E_BASIS="커널 $evr ≥ 수정 버전 $t_cmp"
+ fi
+ return 0
+ done <<EOF
+$(vendor_table "$2")
+EOF
+ return 1
+}
+
+deb_eval() { # $1=커널 릴리스 $2=Debian 패키지 버전
+ local v=$2 up br t
+ if [ -z "$v" ]; then
+ if [ "$OFFLINE" = 1 ]; then
+ E_STATUS=unknown
+ E_BASIS="Debian 커널은 패키지 버전이 필요합니다 — 대상 서버의 'uname -v' 값을 --uname-v 로 지정하세요"
+ return
+ fi
+ nonvendor_eval "$1" "Debian 패키지 커널이 아님"; return
+ fi
+ case $v in *+rpt*|*rpi*) nonvendor_eval "$1" "Raspberry Pi 커널"; return ;; esac
+ up=${v#*:}; branch_of "$up"; br=$BRANCH
+ case $br in
+ 6.1) t="6.1.190-1~"; E_FIXED="6.1.190-1"; E_ADV="bookworm-security, DLA-4841-1" ;; # ~ : bullseye-LTS 의 ~deb11u 포함
+ 6.12) t="6.12.113-1~"; E_FIXED="6.12.113-1"; E_ADV="trixie-security, DSA-6568-1" ;;
+ 7.2) t="7.2.11-1~"; E_FIXED="7.2.11-1"; E_ADV="forky/sid" ;;
+ *)
+ upstream_eval "${up%%-*}"
+ if [ "$E_STATUS" = fixed ] || [ "$E_STATUS" = unaffected ]; then
+ E_BASIS="Debian 커널 $v → 업스트림 기준: $E_BASIS"
+ else
+ E_STATUS=eol
+ E_BASIS="Debian 보안 추적 대상이 아닌 커널 브랜치($br) — 지원 종료 릴리스(bullseye 이하) 또는 비표준 커널"
+ fi
+ E_MODE=vendor
+ return ;;
+ esac
+ if ver_lt deb "$v" "$t"; then
+ E_STATUS=vulnerable; E_BASIS="Debian 커널 $v < 수정 버전 $E_FIXED"
+ else
+ E_STATUS=fixed; E_BASIS="Debian 커널 $v ≥ 수정 버전 $E_FIXED"
+ fi
+}
+
+# 판정: 먼저 벤더 표·업스트림 버전으로 판정하고, 그것으로 결론이 안 날 때(미출시·지원 종료·불명)만
+# 커널 changelog(수 MB)를 검사한다. 표로 결론이 나는 배포판은 changelog 를 전혀 읽지 않는다.
+# E_INPLACE=1: 실행 중인 커널과 같은 이름의 패키지가 부팅 뒤 수정본으로 교체됨 → 재부팅 필요
+evaluate_kernel() { # $1=커널 릴리스 $2=Debian 패키지 버전(선택)
+ E_INPLACE=0
+ evaluate_by_version "$@"
+ case $E_STATUS in pending|unknown|eol) ;; *) return ;; esac
+ [ "$OFFLINE" = 0 ] || return
+ if changelog_has_fix "$1"; then
+ # deb 계열 changelog 는 디스크에 있는 패키지의 것 — 실행 중 커널이 부팅 뒤 교체됐다면
+ # 그 수정은 아직 메모리의 커널에 들어 있지 않다
+ if [ "$1" = "$KREL" ] && [ "$CL_SRC" = deb ] && image_replaced_after_boot "$1"; then
+ E_INPLACE=1
+ E_BASIS="$E_BASIS · 부팅 뒤 같은 이름으로 교체된 커널 패키지에는 수정이 있음(재부팅 필요)"
+ return
+ fi
+ E_STATUS=fixed; E_MODE=changelog
+ E_BASIS="커널 패키지 changelog 에 $CVE 또는 수정 커밋이 기록되어 있음"
+ else
+ case $FAMILY in
+ rhel|centos|rocky|rhelclone|kitten|oracle|ubuntu|proxmox|sles|slemicro|leap|debian|debianlike)
+ E_BASIS="$E_BASIS · 커널 changelog 에도 수정 기록 없음" ;;
+ esac
+ fi
+}
+
+evaluate_by_version() {
+ local krel=$1 dver=${2:-}
+ E_STATUS=unknown; E_FIXED=""; E_BASIS=""; E_ADV=""; E_PKG=""; E_MODE=vendor
+
+ # RPM 계열에서 배포판 기본 커널이 아닌 경우(ELRepo·직접 빌드)는 업스트림 기준
+ case $FAMILY in
+ alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon)
+ case $krel in
+ *elrepo*) nonvendor_eval "$krel" "ELRepo 커널"; return ;;
+ *.el[0-9]*|*.amzn[0-9]*) ;;
+ *) nonvendor_eval "$krel" "배포판 기본 커널이 아님"; return ;;
+ esac ;;
+ esac
+
+ case $FAMILY in
+ alma)
+ case $OS_MAJ_NUM in
+ 8|9|10) rpm_table_eval "$krel" "alma$OS_MAJ_NUM" ||
+ nonvendor_eval "$krel" "AlmaLinux $OS_MAJ_NUM 기본 커널 브랜치가 아님" ;;
+ *) E_STATUS=eol; E_BASIS="지원 종료된 AlmaLinux 릴리스 ($OS_VER)" ;;
+ esac ;;
+ kitten)
+ E_STATUS=pending
+ E_BASIS="AlmaLinux Kitten 은 별도 빌드 없이 다음 정기 커널 업데이트에 수정이 포함될 예정" ;;
+ rhel|centos|rocky|rhelclone)
+ if { [ "$FAMILY" = rhel ] && [ "$OS_MAJ_NUM" -lt 7 ]; } ||
+ { [ "$FAMILY" = centos ] && [ "$OS_MAJ_NUM" -le 8 ]; }; then
+ E_STATUS=eol; E_BASIS="지원 종료된 릴리스 (${OS_NAME:-$OS_ID $OS_VER}) — 수정 커널이 나오지 않음"
+ elif [ "$FAMILY" = rhel ]; then
+ E_STATUS=pending; E_BASIS="Red Hat: RHEL 7/8/9/10 영향 받음, 수정 커널 미출시 ($DATA_DATE 기준)"
+ else
+ E_STATUS=pending; E_BASIS="RHEL 수정 커널 미출시 — 이 배포판도 공개된 수정 커널 없음 ($DATA_DATE 기준)"
+ fi ;;
+ oracle)
+ case $krel in
+ *uek*)
+ rpm_table_eval "$krel" uek || {
+ rpm_evr_of_krel "$krel"; branch_of "$EVR"
+ E_STATUS=pending
+ E_BASIS="이 UEK 릴리스(${BRANCH:-?})용 수정 공지 없음 — UEK R6 이상으로 전환 필요"
+ } ;;
+ *)
+ E_STATUS=pending
+ E_BASIS="RHCK(Red Hat 호환 커널)용 수정 공지 없음 ($DATA_DATE 기준) — UEK 는 수정됨" ;;
+ esac ;;
+ amazon)
+ case $OS_VER in
+ 2|2023)
+ rpm_table_eval "$krel" "al$OS_VER" ||
+ { E_STATUS=unknown; E_BASIS="ALAS 공지에 없는 커널 브랜치 ($krel)"; } ;;
+ *)
+ E_STATUS=eol; E_BASIS="Amazon Linux 1 은 지원 종료 — 수정 공지 없음" ;;
+ esac ;;
+ debian)
+ deb_eval "$krel" "$dver" ;;
+ debianlike)
+ if [ -n "$dver" ]; then deb_eval "$krel" "$dver"
+ else nonvendor_eval "$krel" "배포판 자체 커널"; fi ;;
+ ubuntu)
+ E_STATUS=pending
+ E_BASIS="Ubuntu CVE 트래커: 모든 커널 패키지 'Needs evaluation' — 패치 미출시 ($DATA_DATE 기준)" ;;
+ proxmox)
+ E_STATUS=unknown
+ E_BASIS="Proxmox VE 커널(Ubuntu 커널 기반)의 공개된 수정 정보 없음" ;;
+ sles|slemicro)
+ case $FAMILY:$OS_VER in
+ sles:11*|sles:12|sles:12.[0-4]|sles:15|sles:15.[1-3]|slemicro:5.[0-2])
+ E_STATUS=eol; E_BASIS="지원 종료된 SUSE 릴리스 ($OS_VER) — 사전 패치 대상 아님" ;;
+ *)
+ E_STATUS=pending; E_BASIS="SUSE 공지: kernel-default 등 'Affected', 상태 'Pending' — 패치 미출시 ($DATA_DATE 기준)" ;;
+ esac ;;
+ leap)
+ case $OS_MAJ_NUM in
+ 15|42) E_STATUS=eol; E_BASIS="openSUSE Leap $OS_VER 은 지원 종료" ;;
+ *) E_STATUS=pending; E_BASIS="SUSE 공지: Leap 16 'Affected', 상태 'Pending' — 패치 미출시 ($DATA_DATE 기준)" ;;
+ esac ;;
+ fedora|arch|tumbleweed|microos|alpine|gentoo|wsl)
+ upstream_eval "$krel" ;;
+ wsl1)
+ E_STATUS=unaffected; E_BASIS="WSL1 은 리눅스 커널 없이 Windows 가 시스템 호출을 처리함" ;;
+ *)
+ upstream_eval "$krel"
+ [ "$E_STATUS" = eol ] && E_STATUS=unknown
+ E_BASIS="업스트림 기준 추정(배포판 백포트 여부 확인 필요): $E_BASIS" ;;
+ esac
+}
+
+status_label() {
+ case $1 in
+ fixed) printf '%s안전 — 수정된 커널로 실행 중%s' "$C_GRN" "$C_OFF" ;;
+ unaffected) printf '%s영향 없음 — 이 커널에는 취약한 코드가 없음%s' "$C_GRN" "$C_OFF" ;;
+ livepatched) printf '%s안전(임시) — 라이브패치로 수정 적용됨%s' "$C_GRN" "$C_OFF" ;;
+ reboot) printf '%s재부팅 필요 — 수정 커널은 설치됐지만 이전 커널로 실행 중%s' "$C_YEL" "$C_OFF" ;;
+ vulnerable) printf '%s취약 — 수정 커널이 나와 있음, 업데이트 필요%s' "$C_RED" "$C_OFF" ;;
+ pending) printf '%s취약 — 배포판 수정 커널 미출시 (%s 기준)%s' "$C_RED" "$DATA_DATE" "$C_OFF" ;;
+ eol) printf '%s취약 — 지원 종료(EOL) 릴리스/브랜치, 수정 커널 없음%s' "$C_RED" "$C_OFF" ;;
+ *) printf '%s판단 불가 — 수동 확인 필요%s' "$C_YEL" "$C_OFF" ;;
+ esac
+}
+
+# -----------------------------------------------------------------------------
+# 노출 위험 요소 — 주 공격 대상은 신뢰할 수 없는 게스트를 돌리는 가상화 호스트
+# -----------------------------------------------------------------------------
+read_sysctl() { # $1=파일 → SYSCTL_VAL (없으면 빈 값, 프로세스 생성 없음)
+ SYSCTL_VAL=""
+ [ -r "$1" ] && read -r SYSCTL_VAL < "$1" 2>/dev/null
+ return 0
+}
+
+check_exposure() {
+ local any=0 n procs v mods="" m _
+ HV_HOST=0
+ if [ -e /dev/kvm ]; then
+ line "- /dev/kvm 존재: 이 시스템에서 KVM 가상 머신을 실행할 수 있음"; any=1
+ fi
+ # /proc/modules 는 한 번만 읽는다
+ if [ -r /proc/modules ]; then
+ while read -r m _; do mods="$mods $m"; done < /proc/modules
+ fi
+ case "$mods " in *" vhost_net "*)
+ line "- vhost_net 모듈 로드됨: virtio-net 백엔드 사용 중"; any=1; HV_HOST=1 ;;
+ esac
+ case "$mods " in *" tun "*|*" tap "*|*" macvtap "*)
+ line "- tun/tap 모듈 로드됨"; any=1 ;;
+ esac
+ set -- /sys/class/net/*/tun_flags
+ if [ -e "$1" ]; then n=$#; else n=0; fi
+ if [ "$n" -gt 0 ]; then
+ line "- TUN/TAP 인터페이스 ${n}개 존재"; any=1
+ fi
+ # 프로세스 이름은 grep 하나가 /proc/*/comm 을 직접 읽고, awk 가 중복 제거·연결
+ # kvm = Proxmox 의 VM 프로세스, vhost-<pid> = virtio-net vhost 작업 스레드
+ procs=$(grep -hE '^(qemu-system|qemu-kvm|kvm$|vhost-[0-9]|firecracker|cloud-hyperviso|crosvm|libvirtd|virtqemud|VBoxHeadless|VirtualBoxVM|vmware-vmx)' \
+ /proc/[0-9]*/comm 2>/dev/null | awk '{ sub(/^vhost-[0-9]+$/, "vhost") } !s[$0]++ { printf "%s ", $0 }')
+ if [ -n "$procs" ]; then
+ line "- 가상화 프로세스 실행 중: $procs"; any=1; HV_HOST=1
+ fi
+ if [ "$FAMILY" = proxmox ] && [ "$HV_HOST" = 0 ]; then
+ line "- Proxmox VE 노드"; any=1; HV_HOST=1
+ fi
+ [ "$any" = 1 ] || line "- 가상화 호스트 징후 없음 (KVM · TUN/TAP · VM 프로세스 미발견)"
+ if [ "$HV_HOST" = 1 ]; then
+ line "${C_RED}→ 가상화 호스트로 보입니다. 게스트→호스트 탈출의 직접 대상이므로 최우선 패치 대상입니다.${C_OFF}"
+ fi
+ [ -n "$VIRT" ] && line "- 이 시스템은 VM 게스트입니다 ($VIRT). 호스트 커널 패치는 호스트 관리자/사업자 몫입니다."
+
+ if [ -r /proc/sys/kernel/unprivileged_userns_clone ]; then
+ read_sysctl /proc/sys/kernel/unprivileged_userns_clone; v=$SYSCTL_VAL
+ if [ "$v" = 0 ]; then line "- 비특권 사용자 네임스페이스: 차단 (kernel.unprivileged_userns_clone=0)"
+ else line "- 비특권 사용자 네임스페이스: 허용 (kernel.unprivileged_userns_clone=$v)"; fi
+ elif [ -r /proc/sys/user/max_user_namespaces ]; then
+ read_sysctl /proc/sys/user/max_user_namespaces; v=$SYSCTL_VAL
+ if [ "$v" = 0 ]; then line "- 사용자 네임스페이스: 차단 (user.max_user_namespaces=0)"
+ else line "- 사용자 네임스페이스: 허용 (user.max_user_namespaces=$v)"; fi
+ fi
+ read_sysctl /proc/sys/kernel/apparmor_restrict_unprivileged_userns
+ if [ "$SYSCTL_VAL" = 1 ]; then
+ line " (AppArmor 가 비특권 네임스페이스 권한을 제한 중)"
+ fi
+ note "네임스페이스 항목은 참고용입니다. 비특권 사용자가 네임스페이스 안에서 CAP_NET_ADMIN/CAP_NET_RAW 를 얻으면 TUN/TAP·패킷 소켓 경로에 닿을 수 있어 로컬 공격 가능성에 영향을 줄 수 있으나(추정), 벤더가 발표한 완화책은 아닙니다."
+}
+
+# -----------------------------------------------------------------------------
+# 조치 방법
+# -----------------------------------------------------------------------------
+remedy_eol() {
+ line "이 릴리스/커널 브랜치용 수정 커널은 나오지 않습니다. $1"
+}
+
+remedy_custom() {
+ line "배포판 기본 커널이 아닌 커널(ELRepo·직접 빌드 등)로 판정했습니다."
+ line "커널 공급처의 업데이트를 적용하거나 업스트림 stable 수정 버전 이상으로 올리세요:"
+ line " $UPSTREAM_TEXT"
+ line " 수정 커밋: $FIX_COMMIT ($FIX_URL)"
+ cmt "커널 CNA 는 개별 커밋 cherry-pick 보다 최신 stable 커널 사용을 권장"
+ cmt "ELRepo 예: sudo dnf --enablerepo=elrepo-kernel update 'kernel-ml*' 'kernel-lt*'"
+ cmt "또는 배포판 기본 커널로 부팅을 전환한 뒤 배포판 패치를 적용"
+}
+
+remedy_rhel_like() { # $1=status
+ local pm=dnf core=kernel-core
+ case $OS_MAJ_NUM in [1-7]) pm=yum; core=kernel ;; esac
+ if [ "$1" = eol ]; then
+ remedy_eol "지원되는 릴리스(RHEL·Rocky·AlmaLinux 8/9/10, CentOS Stream 9/10)로 이전하세요."
+ return
+ fi
+ line "$DATA_DATE 기준 수정 커널이 출시되지 않았습니다. (Red Hat: 완화책 없음)"
+ cmt "1) 패치 출시 여부 확인"
+ cmd "sudo $pm updateinfo list --cve $CVE"
+ cmd "rpm -q --changelog $core | grep -iE '$CVE|checksum_start'"
+ cmt "2) 출시되면 적용 후 재부팅"
+ cmd "sudo $pm update --cve $CVE # 또는: sudo $pm update 'kernel*'"
+ cmd "sudo reboot"
+ if [ "$FAMILY" = rhel ]; then
+ cmt "Red Hat 이 kpatch 라이브패치를 제공하면 재부팅 없이 적용 가능"
+ cmd "sudo $pm install \"kpatch-patch = \$(uname -r)\""
+ fi
+}
+
+remedy_family() { # $1=status
+ local st=$1 pm
+ case $FAMILY in
+ alma)
+ if [ "$st" = eol ]; then remedy_eol "지원되는 AlmaLinux(8.10 / 9 / 10)로 업그레이드하세요."; return; fi
+ line "수정 커널: ${R_FIXED:-AlmaLinux 공지 참고} 이상"
+ cmt "1) 정식 저장소에 이미 반영됐는지 먼저 시도"
+ cmd "sudo dnf clean metadata && sudo dnf update 'kernel*'"
+ cmt "2) 아직 없으면 testing 저장소에서 설치 (AlmaLinux 2026-10-16 공지 방법)"
+ cmd "sudo dnf install -y almalinux-release-testing"
+ cmd "sudo dnf update 'kernel*' --enablerepo=almalinux-testing"
+ cmd "sudo dnf remove -y almalinux-release-testing # testing 저장소 정리"
+ cmt "3) 재부팅 후 'uname -r' 로 수정 버전인지 확인"
+ cmd "sudo reboot"
+ ;;
+ kitten)
+ line "AlmaLinux Kitten 은 다음 정기 커널 업데이트에 수정이 포함될 예정입니다."
+ cmd "sudo dnf update 'kernel*'"
+ cmd "sudo reboot"
+ ;;
+ rhel|centos|rocky|rhelclone)
+ remedy_rhel_like "$st"
+ ;;
+ oracle)
+ pm=dnf; case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
+ case $KREL in
+ *uek*)
+ if [ "$st" = vulnerable ]; then
+ line "수정 커널: $R_FIXED 이상 ($R_ADV)"
+ cmd "sudo $pm update kernel-uek"
+ cmd "sudo reboot"
+ cmt "Ksplice 구독 시 재부팅 없이 적용: sudo uptrack-upgrade -y"
+ else
+ line "사용 중인 UEK 릴리스용 수정 공지가 없습니다. 수정된 UEK 로 전환하세요."
+ 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)"
+ cmd "sudo $pm install kernel-uek"
+ cmd "sudo reboot"
+ fi ;;
+ *)
+ line "RHCK(Red Hat 호환 커널)용 수정은 아직 없습니다. UEK 는 이미 수정됐으므로 UEK 로 전환하는 것이 가장 빠릅니다."
+ cmd "sudo $pm install kernel-uek"
+ cmd "sudo grubby --info=ALL | grep -E '^kernel' # 설치된 UEK 경로 확인"
+ cmd "sudo grubby --set-default /boot/vmlinuz-<UEK 버전>"
+ cmd "sudo reboot"
+ cmt "RHCK 를 유지해야 하면 출시 여부 확인: sudo $pm updateinfo list --cve $CVE"
+ ;;
+ esac ;;
+ amazon)
+ case $OS_VER in
+ 2)
+ if [ -n "$R_ADV" ]; then
+ line "수정 커널: $R_FIXED 이상 ($R_ADV)"
+ cmd "sudo yum update --advisory $R_ADV # 또는: sudo yum update kernel"
+ else
+ cmd "sudo yum update kernel"
+ fi
+ cmd "sudo reboot" ;;
+ 2023)
+ if [ -n "$R_ADV" ]; then
+ line "수정 커널: $R_FIXED 이상 ($R_ADV)"
+ cmd "sudo dnf update $R_PKG --releasever 2023.12.20261012"
+ cmt "또는: sudo dnf update --advisory $R_ADV --releasever 2023.12.20261012"
+ else
+ cmd "sudo dnf update 'kernel*' --releasever latest"
+ fi
+ cmd "sudo reboot" ;;
+ *)
+ remedy_eol "Amazon Linux 2023 으로 이전하세요." ;;
+ esac
+ note "AWS 발표: EC2·Lambda·Fargate 호스트 인프라는 영향 없음. 위 조치는 이 인스턴스 커널 자체에 대한 것입니다."
+ ;;
+ debian)
+ if [ "$st" = eol ]; then
+ remedy_eol "bookworm(12) 이상으로 업그레이드하거나 Freexian ELTS 같은 연장 지원을 확인하세요."
+ return
+ fi
+ line "수정 커널: $R_FIXED 이상 ($R_ADV)"
+ cmt "1) 보안 저장소(<코드네임>-security)가 켜져 있는지 확인"
+ cmd "grep -rhE '^[^#].*security' /etc/apt/sources.list /etc/apt/sources.list.d/ 2>/dev/null"
+ cmt "2) 업데이트 후 재부팅"
+ cmd "sudo apt update"
+ cmd "sudo apt full-upgrade"
+ cmt "커널 메타패키지가 없으면: sudo apt install linux-image-\$(dpkg --print-architecture)"
+ case $RUN_DVER in *bpo*)
+ cmt "backports 커널 사용 중: sudo apt install -t ${OS_CODENAME:-<코드네임>}-backports linux-image-\$(dpkg --print-architecture)" ;;
+ esac
+ cmd "sudo reboot"
+ cmt "3) 재부팅 후 'uname -v' 의 'Debian <버전>' 이 수정 버전 이상인지 확인"
+ ;;
+ debianlike)
+ line "배포판 커널을 최신으로 업데이트하세요. (수정 기준: 업스트림 ${R_FIXED:-stable} 이상)"
+ cmd "sudo apt update && sudo apt full-upgrade"
+ cmd "sudo reboot"
+ ;;
+ ubuntu)
+ line "$DATA_DATE 기준 Ubuntu 수정 커널이 출시되지 않았습니다. (모든 커널 'Needs evaluation')"
+ cmt "1) 패치 출시 여부 확인 (Ubuntu Pro 클라이언트)"
+ cmd "pro fix $CVE --dry-run"
+ cmt "또는 https://ubuntu.com/security/$CVE"
+ cmt "2) Livepatch 사용 중이면 적용 여부 확인"
+ cmd "sudo canonical-livepatch status --verbose | grep -A2 $CVE"
+ cmt "3) 출시되면 적용 후 재부팅"
+ cmd "sudo apt update && sudo apt full-upgrade"
+ cmd "sudo reboot"
+ ;;
+ proxmox)
+ line "Proxmox VE 커널(Ubuntu 커널 기반)의 수정 여부는 Proxmox 공지를 확인해야 합니다."
+ cmd "pveversion -v | grep -E 'proxmox-kernel|pve-manager'"
+ cmd "apt changelog proxmox-kernel-\$(uname -r)-signed 2>/dev/null | grep -iE '$CVE|checksum_start'"
+ cmt "수정 커널이 나오면 (pve-enterprise 또는 pve-no-subscription 저장소)"
+ cmd "sudo apt update && sudo apt full-upgrade"
+ cmt "게스트를 다른 노드로 마이그레이션하거나 종료한 뒤 재부팅"
+ cmd "sudo reboot"
+ ;;
+ sles|slemicro|leap)
+ if [ "$st" = eol ]; then
+ if [ "$FAMILY" = leap ]; then remedy_eol "openSUSE Leap 16 으로 업그레이드하세요."
+ else remedy_eol "지원(LTSS 포함)되는 서비스팩으로 업그레이드하세요."; fi
+ return
+ fi
+ line "$DATA_DATE 기준 SUSE 수정 커널이 출시되지 않았습니다. (상태 'Pending')"
+ cmt "1) 패치 출시 여부 확인"
+ cmd "sudo zypper refresh && sudo zypper list-patches --cve=$CVE"
+ cmt "2) 출시되면 적용 후 재부팅"
+ if [ "$FAMILY" = slemicro ]; then cmd "sudo transactional-update patch"
+ else cmd "sudo zypper patch --cve=$CVE"; fi
+ cmd "sudo reboot"
+ cmt "SLE Live Patching 구독 시 kernel-livepatch 패치로 재부팅 없이 적용 가능"
+ ;;
+ fedora)
+ if [ "$st" = eol ]; then
+ line "이 커널 브랜치는 업스트림 지원이 끝났습니다. 커널을 업데이트하고, Fedora 릴리스가 EOL 이면 지원 릴리스로 올리세요."
+ cmd "sudo dnf upgrade --refresh"
+ cmd "sudo dnf system-upgrade download --releasever=<새 릴리스>"
+ return
+ fi
+ line "수정 기준: 업스트림 $R_FIXED 이상 커널"
+ cmd "sudo dnf upgrade --refresh 'kernel*'"
+ cmt "stable 저장소에 아직 없으면 updates-testing 에서 설치"
+ cmd "sudo dnf upgrade --refresh --enablerepo=updates-testing 'kernel*'"
+ cmd "sudo reboot"
+ ;;
+ arch)
+ line "수정 기준: linux ${R_FIXED:-7.2.11} 이상 / linux-lts 6.12.113 · 6.18.57 이상"
+ cmd "sudo pacman -Syu"
+ cmd "sudo reboot"
+ ;;
+ tumbleweed)
+ line "수정 기준: 업스트림 ${R_FIXED:-stable} 이상"
+ cmd "sudo zypper refresh && sudo zypper dup"
+ cmd "sudo reboot"
+ ;;
+ microos)
+ cmd "sudo transactional-update dup"
+ cmd "sudo reboot"
+ ;;
+ alpine)
+ line "수정 기준: 업스트림 ${R_FIXED:-stable} 이상 (linux-lts / linux-virt)"
+ cmd "sudo apk update && sudo apk upgrade"
+ cmd "sudo reboot"
+ [ "$st" = eol ] && cmt "커널 브랜치가 EOL 이면 Alpine 릴리스 자체를 지원 버전으로 올려야 함"
+ ;;
+ gentoo)
+ line "수정 기준: 업스트림 ${R_FIXED:-stable} 이상"
+ cmd "sudo emerge --sync"
+ cmd "sudo emerge --ask --update --oneshot sys-kernel/gentoo-kernel-bin # 또는 gentoo-kernel"
+ cmt "gentoo-sources 로 직접 빌드한다면 수정 버전 이상 소스로 다시 빌드"
+ cmd "sudo reboot"
+ ;;
+ wsl)
+ line "WSL2 커널은 Microsoft 가 배포합니다. Windows PowerShell 에서 실행하세요:"
+ cmd "wsl --update"
+ cmd "wsl --shutdown"
+ note "WSL2 의 호스트는 Windows(Hyper-V)라서 'VM → 리눅스 호스트 탈출' 시나리오는 해당하지 않습니다. WSL 커널 자체만 업스트림 기준으로 판정했습니다."
+ ;;
+ *)
+ line "배포판 공급사의 보안 공지를 확인하고 커널을 업데이트하세요."
+ line "직접 빌드한 커널이라면 업스트림 stable 수정 버전 이상으로 올리세요:"
+ line " $UPSTREAM_TEXT"
+ line " 수정 커밋: $FIX_COMMIT ($FIX_URL)"
+ ;;
+ esac
+}
+
+print_remedy() { # $1=최종 상태
+ local st=$1
+ case $st in
+ fixed|unaffected)
+ line "${C_GRN}추가 조치가 필요하지 않습니다.${C_OFF}"
+ [ -n "$VIRT" ] && note "이 시스템은 VM 게스트($VIRT)입니다. 호스트(클라우드/호스팅 사업자) 커널의 패치 여부는 따로 확인하세요."
+ return ;;
+ livepatched)
+ line "라이브패치($LP_NAME)로 실행 중인 커널에 수정이 적용되어 있습니다."
+ line "다음 정기 점검 때 수정된 커널 패키지를 설치하고 재부팅하세요."
+ return ;;
+ reboot)
+ line "수정된 커널이 이미 설치되어 있습니다: $REBOOT_TARGET"
+ line "재부팅해야 적용됩니다."
+ [ "$HV_HOST" = 1 ] && cmt "가상화 호스트입니다. 재부팅 전에 게스트 VM 을 다른 호스트로 옮기거나 종료하세요."
+ cmd "sudo reboot"
+ cmt "재부팅 후 이 스크립트를 다시 실행해 확인"
+ return ;;
+ esac
+
+ if [ "$R_MODE" = upstream ]; then
+ case $FAMILY in
+ fedora|arch|tumbleweed|microos|alpine|gentoo|wsl|generic|debianlike) remedy_family "$st" ;;
+ debian) FAMILY=debianlike remedy_family "$st" ;;
+ *) remedy_custom ;;
+ esac
+ else
+ remedy_family "$st"
+ fi
+
+ printf '\n'
+ note "공식 우회책(sysctl·모듈 차단 등)은 없습니다. 수정된 커널로 재부팅해야 해결됩니다."
+ if [ "$HV_HOST" = 1 ]; then
+ note "게스트 OS 커널만 패치해서는 호스트가 보호되지 않습니다. 재부팅 전 게스트를 옮기거나 종료하세요."
+ fi
+ case $st in pending|eol|unknown)
+ note "패치 전까지는 신뢰할 수 없는 워크로드(VM·microVM·샌드박스) 생성을 제한하고 민감한 시스템과 분리하세요. (위험을 줄일 뿐 해결책은 아님)" ;;
+ esac
+ [ -n "$VIRT" ] && note "이 시스템은 VM 게스트($VIRT)입니다. 호스트(클라우드/호스팅 사업자)의 패치 여부도 확인하세요."
+}
+
+print_refs() {
+ line "https://www.cve.org/CVERecord?id=$CVE"
+ line "https://nvd.nist.gov/vuln/detail/$CVE"
+ line "https://pwn.ai/blog/pwnescape"
+ line "https://almalinux.org/blog/2026-10-16-pwnescape-cve-2026-99999/"
+ line "https://www.penligent.ai/hackinglabs/cve-2026-99999/"
+ line "https://news.hada.io/topic?id=35568"
+ case $FAMILY in
+ debian|debianlike) line "https://security-tracker.debian.org/tracker/$CVE" ;;
+ ubuntu|proxmox) line "https://ubuntu.com/security/$CVE" ;;
+ alma|kitten|rhel|centos|rocky|rhelclone)
+ line "https://access.redhat.com/security/cve/cve-2026-99999" ;;
+ oracle) line "https://linux.oracle.com/cve/$CVE.html" ;;
+ amazon) line "https://explore.alas.aws.amazon.com/$CVE.html" ;;
+ sles|slemicro|leap|tumbleweed|microos)
+ line "https://www.suse.com/security/cve/$CVE.html" ;;
+ *) line "$FIX_URL" ;;
+ esac
+}
+
+print_refs_section() {
+ section "참고 자료"
+ print_refs
+ note "판정 데이터 기준일 $DATA_DATE. 벤더 공지는 계속 갱신되므로 최신 공지도 함께 확인하세요."
+}
+
+show_manual() {
+ section "조치 방법 (수동)"
+ print_remedy "$FINAL"
+ print_refs_section
+}
+
+# -----------------------------------------------------------------------------
+# 자동 패치 (메뉴 1번)
+# -----------------------------------------------------------------------------
+run() {
+ printf ' %s$ %s%s\n' "$C_DIM" "$*" "$C_OFF"
+ "$@"
+}
+
+setup_sudo() {
+ SUDO=""
+ [ "$(id -u)" = 0 ] && return 0
+ if command -v sudo >/dev/null 2>&1; then SUDO=sudo; return 0; fi
+ line "${C_RED}root 권한이 필요하지만 sudo 가 없습니다. root 로 다시 실행하세요.${C_OFF}"
+ return 1
+}
+
+can_autopatch() {
+ case $FAMILY in wsl|wsl1) return 1 ;; esac # WSL 안에서 reboot 해도 배포판 커널로 부팅되지 않음
+ [ "$FINAL" = reboot ] && return 0
+ [ "$FINAL" = eol ] && return 1
+ case $FAMILY in
+ debian|debianlike|fedora|arch|tumbleweed|microos|alpine) return 0 ;;
+ alma|kitten|rhel|centos|rocky|rhelclone|oracle|amazon|ubuntu|proxmox|sles|slemicro|leap)
+ [ "$R_MODE" != upstream ] ;; # ELRepo·직접 빌드 커널은 자동 패치 대상 아님
+ *) return 1 ;;
+ esac
+}
+
+# zypper 종료 코드 100~103 은 정보성(업데이트 있음·재부팅 필요 등)이라 성공으로 본다
+zypper_ok() { [ "$1" -eq 0 ] || { [ "$1" -ge 100 ] && [ "$1" -le 103 ]; }; }
+zypper_run() { # 커널 패키지만 다루는 작업: 확인 없이 진행
+ local rc
+ run $SUDO zypper --non-interactive "$@"; rc=$?
+ zypper_ok "$rc"
+}
+zypper_ask() { # 시스템 전체 업그레이드(dup): 패키지 제거가 있을 수 있어 사용자가 직접 확인
+ local rc
+ run $SUDO zypper "$@"; rc=$?
+ zypper_ok "$rc"
+}
+
+# 설치된 linux-* 패키지만 올린다 (메타패키지가 새 ABI 커널을 끌어옴).
+# - --no-remove: 다른 패키지를 지워야 하면 멈춤
+# - 'apt-get install' 은 지정한 패키지를 수동 설치로 바꾸므로, 원래 자동 설치였던 것은 되돌려
+# 오래된 커널이 'apt autoremove' 로 계속 정리되게 한다
+# - Proxmox 는 공식 권장대로 dist-upgrade 하되 제거 목록을 사용자가 확인하도록 -y 없이 실행
+patch_apt() {
+ local pkgs="" autos="" st pkg a meta=0
+ local opts="-o Dpkg::Options::=--force-confdef -o Dpkg::Options::=--force-confold"
+ run $SUDO apt-get update || return 1
+ if [ "$FAMILY" = proxmox ]; then
+ run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get $opts dist-upgrade
+ return
+ fi
+ while read -r st pkg; do
+ case $st in ii*) ;; *) continue ;; esac
+ pkgs="$pkgs $pkg"
+ case $pkg in linux-image-[a-z]*) meta=1 ;; esac
+ done <<EOF
+$(dpkg-query -W -f='${db:Status-Abbrev} ${Package}\n' 'linux-*' 2>/dev/null)
+EOF
+ if [ -z "$pkgs" ]; then
+ line "설치된 linux-* 커널 패키지를 찾지 못했습니다."; return 1
+ fi
+ while read -r a; do
+ case "$pkgs " in *" $a "*) autos="$autos $a" ;; esac
+ done <<EOF
+$(apt-mark showauto 2>/dev/null)
+EOF
+ run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install --only-upgrade $pkgs || return 1
+ [ -n "$autos" ] && run $SUDO apt-mark auto $autos >/dev/null
+ # Debian 에서 커널 메타패키지가 없으면 새 ABI 커널이 들어오지 않으므로 설치
+ if [ "$FAMILY" = debian ] && [ "$meta" = 0 ]; then
+ run $SUDO env DEBIAN_FRONTEND=noninteractive apt-get -y $opts --no-remove install "linux-image-$(dpkg --print-architecture)"
+ fi
+}
+
+# AlmaLinux 공지 방법. testing 저장소는 이번 한 번만 켜고, 끝나면 저장소 패키지를 지워
+# 이후 일반 업데이트에 testing 패키지가 섞이지 않게 한다.
+patch_alma() {
+ local rc
+ run $SUDO dnf clean metadata && run $SUDO dnf -y update 'kernel*' || return 1
+ installed_fix_ready && return 0
+ line "정식 저장소에 수정 커널이 아직 없어 almalinux-testing 저장소에서 설치합니다. (AlmaLinux 공지 방법)"
+ run $SUDO dnf -y install almalinux-release-testing || return 1
+ run $SUDO dnf -y update 'kernel*' --enablerepo=almalinux-testing; rc=$?
+ run $SUDO dnf -y remove almalinux-release-testing ||
+ note "almalinux-release-testing 을 지우지 못했습니다. testing 저장소가 켜져 있지 않은지 /etc/yum.repos.d/ 를 확인하세요."
+ return $rc
+}
+
+# 시스템 전체 업그레이드(pacman -Syu, zypper dup, apk upgrade, transactional-update dup)는
+# 제거·교체되는 패키지가 있을 수 있어 확인 질문을 남겨 둔다. 커널 패키지만 올리는 작업은 -y.
+patch_now() {
+ local pm=dnf
+ case $OS_MAJ_NUM in [1-7]) pm=yum ;; esac
+ case $FAMILY in
+ alma) patch_alma ;;
+ kitten|rhel|centos|rocky|rhelclone|oracle)
+ run $SUDO $pm -y update 'kernel*' ;;
+ amazon)
+ if [ "$OS_VER" = 2 ]; then run $SUDO yum -y update kernel
+ else run $SUDO dnf -y update "${R_PKG:-kernel}" --releasever=latest; fi ;;
+ fedora) run $SUDO dnf -y upgrade --refresh 'kernel*' ;;
+ debian|debianlike|ubuntu|proxmox) patch_apt ;;
+ sles|leap) zypper_run refresh && zypper_run update "kernel-${KREL##*-}" ;;
+ slemicro) run $SUDO transactional-update --non-interactive patch ;;
+ microos) run $SUDO transactional-update --interactive dup ;;
+ tumbleweed) zypper_run refresh && zypper_ask dup ;;
+ arch) run $SUDO pacman -Syu ;;
+ alpine) run $SUDO apk -U --interactive upgrade ;;
+ *) return 1 ;;
+ esac
+}
+
+# /boot 여유 공간이 부족하면 새 커널·initramfs 생성이 반쯤 실패한 채 기본 부팅 항목만 바뀔 수 있다
+boot_space_ok() {
+ local avail
+ have df || return 0
+ avail=$(df -Pk /boot 2>/dev/null | awk 'NR == 2 { print $4 }')
+ case $avail in ''|*[!0-9]*) return 0 ;; esac
+ [ "$avail" -ge 102400 ] && return 0
+ line "${C_RED}/boot 여유 공간이 $((avail / 1024))MB 뿐이라 새 커널을 넣다 실패할 수 있어 패치를 멈춥니다.${C_OFF}"
+ cmt "오래된 커널 정리 예: sudo dnf remove --oldinstallonly kernel / sudo apt autoremove --purge"
+ return 1
+}
+
+# transactional-update 뒤: 다음 부팅 스냅샷(btrfs 기본 서브볼륨)에 수정 커널이 들어 있는지
+snapshot_fix_ready() {
+ local out n root k list="" tab pick
+ REBOOT_TARGET=""; REBOOT_KREL=""
+ have btrfs || return 1
+ out=$($SUDO btrfs subvolume get-default / 2>/dev/null) || return 1
+ case $out in */.snapshots/*/snapshot*) ;; *) return 1 ;; esac
+ n=${out##*/.snapshots/}; n=${n%%/*}
+ case $n in ''|*[!0-9]*) return 1 ;; esac
+ root=/.snapshots/$n/snapshot
+ if [ "$FAMILY" = slemicro ]; then
+ $SUDO rpm --root "$root" -q --changelog "kernel-${KREL##*-}" 2>/dev/null | has_fix_text || return 1
+ else
+ tab=$(printf '\t')
+ for k in $($SUDO ls "$root/usr/lib/modules" 2>/dev/null); do list="$list$k$tab$k
+"; done
+ pick=$(printf '%s' "$list" | LC_ALL=C awk -v op=pick -v mode=rpm -v ref="" "$AWK_VERCMP")
+ k=${pick%%"$tab"*}
+ [ -n "$k" ] || return 1
+ upstream_eval "$k"; [ "$E_STATUS" = fixed ] || return 1
+ fi
+ REBOOT_TARGET="스냅샷 $n"
+}
+
+# 패치 뒤 재부팅하면 수정 커널로 부팅되는지 (제자리 교체 → 다음 부팅 커널 순서로 확인)
+reboot_fix_ready() {
+ case $FAMILY in slemicro|microos) snapshot_fix_ready; return ;; esac
+ evaluate_kernel "$KREL" "$RUN_DVER"
+ if [ "$E_INPLACE" = 1 ]; then
+ REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (부팅 뒤 같은 이름으로 교체된 패키지)"
+ return 0
+ fi
+ installed_fix_ready
+}
+
+show_default_kernel() {
+ local d
+ have grubby || return 0
+ d=$($SUDO grubby --default-kernel 2>/dev/null) || return 0
+ [ -n "$d" ] && kv "다음 부팅 기본 커널" "$d"
+}
+
+# grubby 가 없고 GRUB_DEFAULT 가 0(첫 항목=최신 커널)이 아니면 다음 부팅 커널을 확신할 수 없다
+grub_default_custom() {
+ local l v=""
+ have grubby && return 1
+ [ -r /etc/default/grub ] || return 1
+ while IFS= read -r l; do case $l in GRUB_DEFAULT=*) v=${l#GRUB_DEFAULT=} ;; esac; done < /etc/default/grub
+ v=${v#\"}; v=${v%\"}; v=${v#\'}; v=${v%\'}
+ case $v in ''|0) return 1 ;; esac
+ GRUB_DEFAULT_VAL=$v
+}
+
+do_reboot() {
+ local ans grub_custom=0
+ grub_default_custom && grub_custom=1
+ # 가상화 호스트, 다음 부팅 커널이 불확실한 GRUB 설정, 시간 제한 입력(read -t)이 없는 셸이면
+ # 카운트다운 대신 'yes' 를 직접 입력받는다
+ if [ "$HV_HOST" = 1 ] || [ "$grub_custom" = 1 ] || [ -z "${BASH_VERSION:-}" ]; then
+ if [ "$HV_HOST" = 1 ]; then
+ line "${C_YEL}가상화 호스트입니다. 재부팅하면 실행 중인 VM 이 모두 중단됩니다.${C_OFF}"
+ line "가능하면 게스트를 다른 호스트로 옮기거나 먼저 종료하세요."
+ fi
+ if [ "$grub_custom" = 1 ]; then
+ line "${C_YEL}GRUB_DEFAULT=$GRUB_DEFAULT_VAL 이라 다음 부팅 커널이 최신 커널이 아닐 수 있습니다. 부팅 메뉴 설정을 확인하세요.${C_OFF}"
+ fi
+ printf ' 지금 재부팅하려면 yes 를 입력하세요: '
+ read -r ans || ans=""
+ if [ "$ans" != yes ]; then
+ line "재부팅을 취소했습니다. 준비되면 'sudo reboot' 를 실행하세요."
+ return 1
+ fi
+ else
+ printf ' 10초 후 재부팅합니다. 취소하려면 Enter 를 누르세요... '
+ # 시간이 다 됐을 때(종료 코드 > 128)만 재부팅. Enter·Ctrl-D(EOF)·오류는 모두 취소.
+ read -r -t 10 ans
+ if [ $? -le 128 ]; then
+ printf '\n'
+ line "재부팅을 취소했습니다. 준비되면 'sudo reboot' 를 실행하세요."
+ return 1
+ fi
+ printf '\n'
+ fi
+ line "재부팅합니다. 부팅 후 이 스크립트를 다시 실행해 결과를 확인하세요."
+ run $SUDO reboot
+}
+
+auto_patch() {
+ setup_sudo || { show_manual; return; }
+ if [ "$FINAL" != reboot ]; then
+ section "패치 진행"
+ note "커널 패키지를 업데이트합니다. sudo 비밀번호를 물을 수 있습니다."
+ boot_space_ok || { show_manual; return; }
+ if ! patch_now; then
+ line "${C_RED}패키지 업데이트 중 오류가 났습니다. 위 출력을 확인하고 아래 수동 절차로 진행하세요.${C_OFF}"
+ show_manual
+ return
+ fi
+ if reboot_fix_ready; then
+ FINAL=reboot
+ line "${C_GRN}다음 부팅에 수정 커널 확인: $REBOOT_TARGET${C_OFF}"
+ else
+ line "업데이트는 끝났지만 다음 부팅 커널에서 $CVE 수정을 확인하지 못했습니다. 재부팅하지 않습니다."
+ if [ -n "${NB_ALT:-}" ]; then
+ line "수정 커널 $NB_ALT 은(는) 설치돼 있지만 기본 부팅 커널이 아닙니다:"
+ cmd "sudo grubby --set-default /boot/vmlinuz-$NB_ALT && sudo reboot"
+ else
+ line "배포판 수정 커널이 아직 나오지 않았을 수 있습니다."
+ fi
+ show_manual
+ return
+ fi
+ fi
+ # 부팅에 필요한 파일이 빠진 커널로 재부팅하지 않는다 (dracut·initramfs 생성 실패 등)
+ if [ -n "${REBOOT_KREL:-}" ] && ! boot_files_ok "$REBOOT_KREL"; then
+ line "${C_RED}$REBOOT_KREL 의 커널 이미지나 initramfs 가 없어 자동 재부팅을 멈춥니다.${C_OFF}"
+ cmt "initramfs 다시 만들기: sudo dracut -f --kver $REBOOT_KREL / sudo update-initramfs -u -k $REBOOT_KREL"
+ return
+ fi
+ section "재부팅"
+ show_default_kernel
+ do_reboot
+}
+
+choose_action() {
+ local choice label
+ case $FINAL in
+ reboot) label="지금 재부팅 (설치된 수정 커널 적용)" ;;
+ pending|unknown) label="지금 업데이트 시도 (수정 커널이 나왔으면 설치 후 재부팅)" ;;
+ *) label="지금 패치 (수정 커널 설치 후 재부팅)" ;;
+ esac
+ can_autopatch || label="지금 패치 — 이 시스템은 자동 패치를 지원하지 않음"
+
+ section "다음 작업 선택"
+ if [ -n "$VIRT" ]; then
+ line "${C_YEL}이 시스템은 VM 게스트($VIRT)입니다. 게스트를 패치해도 호스트는 보호되지 않으니${C_OFF}"
+ line "${C_YEL}호스트 OS 에서도 이 스크립트를 실행해 패치하세요.${C_OFF}"
+ printf '\n'
+ fi
+ line "1. $label"
+ line "2. 나중에 수동 패치 (명령어 보기)"
+ line "3. 종료"
+ while :; do
+ printf '\n 선택 [1-3]: '
+ read -r choice || { printf '\n'; return; }
+ case $choice in
+ 1)
+ if can_autopatch; then
+ auto_patch
+ else
+ line "이 배포판/커널은 자동으로 패치할 수 없습니다. 아래 수동 절차를 따르세요."
+ show_manual
+ fi
+ return ;;
+ 2) show_manual; return ;;
+ 3) line "종료합니다."; return ;;
+ *) line "1, 2, 3 중 하나를 입력하세요." ;;
+ esac
+ done
+}
+
+print_header() {
+ printf '%s' "$C_BLU"
+ printf '==================================================================\n'
+ printf ' %s (PwnEscape) 리눅스 커널 취약점 점검\n' "$CVE"
+ printf ' IPv4 checksum_start 검증 누락 → 게스트 VM 에서 호스트 탈출 가능\n'
+ printf ' CVSS 9.3 Critical · 2.6.27 이후 모든 커널 · 수정 커밋 %s\n' "$FIX_COMMIT"
+ printf '==================================================================\n'
+ printf '%s' "$C_OFF"
+}
+
+print_table_rows() { # $1=테이블 이름 $2=행 앞에 붙일 말
+ local br cmp disp adv pkg
+ while IFS='|' read -r br cmp disp adv pkg; do
+ [ -n "$br" ] && line "$2$disp 이상 ($adv)"
+ done <<EOF
+$(vendor_table "$1")
+EOF
+}
+
+print_matrix() {
+ printf '%s%s 배포판별 영향·수정 현황 (기준일 %s)%s\n' "$C_BLU" "$CVE" "$DATA_DATE" "$C_OFF"
+ section "업스트림 커널 (kernel.org)"
+ line "영향: 2.6.27 이후 모든 커널 / 수정 커밋 $FIX_COMMIT"
+ line "수정: $UPSTREAM_TEXT"
+ line "그 밖의 브랜치(5.4, 6.7~6.11, 6.13~6.17, 7.0, 7.1 등)는 EOL — 지원 브랜치로 올려야 함"
+ section "AlmaLinux — 수정됨 (2026-10-16, almalinux-testing 저장소)"
+ print_table_rows alma8 "8: "
+ print_table_rows alma9 "9: "
+ print_table_rows alma10 "10: "
+ line "Kitten 10: 별도 빌드 없이 다음 정기 커널 업데이트에 포함 예정"
+ section "Red Hat Enterprise Linux 7 / 8 / 9 / 10 — 영향 받음 · 패치 미출시"
+ line "Red Hat CVSS 8.8 (Important), 완화책 없음, kernel / kernel-rt 모두 대상"
+ section "Rocky Linux · CentOS Stream · 기타 RHEL 호환 — 공개된 수정 커널 없음"
+ line "RHEL 수정 커널 출시 후 반영 예상 (Oracle Linux RHCK 포함)"
+ section "Oracle Linux UEK — 수정됨 (2026-10-16)"
+ print_table_rows uek ""
+ section "Amazon Linux 2 — 수정됨"
+ print_table_rows al2 ""
+ section "Amazon Linux 2023 — 수정됨"
+ print_table_rows al2023 ""
+ line "AWS 발표: EC2 · Lambda · Fargate 호스트 인프라는 영향 없음"
+ section "Debian"
+ line "bookworm (12): 6.1.190-1 이상 (DLA-4841-1, bookworm-security)"
+ line "trixie (13): 6.12.113-1 이상 (DSA-6568-1, trixie-security)"
+ line "forky / sid: 7.2.11-1 이상"
+ line "bullseye (11) 이하: 보안 추적 대상 아님 (EOL)"
+ section "Ubuntu — 패치 미출시"
+ line "14.04 ~ 26.04 모든 커널 패키지(linux, -aws, -azure, -gcp, -oracle, -hwe 등) 'Needs evaluation'"
+ section "SUSE / openSUSE"
+ line "SLES 12 SP5 · 15 SP4~SP7 · 16.x, SLE Micro 5.3~5.5 · 6.x, Leap 16: 영향 받음 · 패치 미출시(Pending)"
+ line "openSUSE Leap 15.x, SLES 15 SP1~SP3, SLE Micro 5.0~5.2: 지원 종료"
+ section "Fedora · Arch · openSUSE Tumbleweed · Alpine · Gentoo 등"
+ line "배포판 커널이 업스트림 stable 을 따르므로 위 업스트림 수정 버전 기준"
+ printf '\n'
+ note "벤더 공지는 계속 갱신됩니다. 최신 정보는 각 배포판 보안 트래커를 확인하세요."
+}
+
+# -----------------------------------------------------------------------------
+main() {
+ parse_args "$@"
+ setup_colors
+ if [ "$LIST_ONLY" = 1 ]; then print_matrix; exit 0; fi
+
+ if [ "$OFFLINE" = 0 ]; then
+ # uname -r / -v 와 같은 값을 /proc 에서 바로 읽는다 (프로세스 생성 없음)
+ if [ -r /proc/sys/kernel/osrelease ] && [ -r /proc/sys/kernel/version ]; then
+ read -r KREL < /proc/sys/kernel/osrelease
+ read -r KVER < /proc/sys/kernel/version
+ else
+ [ "$(uname -s)" = Linux ] ||
+ die "리눅스에서 실행하세요. (다른 서버 값으로 판정하려면 --uname-r / --uname-v / --os-release)"
+ KREL=$(uname -r); KVER=$(uname -v)
+ fi
+ fi
+ RUN_DVER=""
+ case $KVER in *" Debian "*) RUN_DVER=${KVER#* Debian }; RUN_DVER=${RUN_DVER%% *} ;; esac
+
+ detect_os
+ VIRT=""; CTR=""; HV_HOST=0
+ [ "$OFFLINE" = 0 ] && detect_virt_container
+ case $FAMILY in wsl|wsl1) VIRT="" ;; esac # WSL 의 호스트는 Windows 라 '리눅스 호스트 패치' 안내 대상 아님
+ # Debian 실행 커널 버전은 uname -v 에서만 얻는다. 디스크의 패키지 버전은 부팅 뒤 바뀌었을 수 있어 쓰지 않음.
+
+ print_header
+ section "시스템 정보"
+ kv "배포판" "${OS_NAME:-알 수 없음} [${OS_ID:-?} ${OS_VER:-?}]"
+ kv "실행 커널" "$KREL"
+ [ -n "$RUN_DVER" ] && kv "Debian 커널 패키지" "$RUN_DVER"
+ if [ "$OFFLINE" = 1 ]; then
+ kv "점검 모드" "지정값으로 판정 (로컬 검사 생략)"
+ elif [ "$FAMILY" = wsl ]; then
+ kv "가상화" "WSL2 (Windows Hyper-V 위에서 실행)"
+ elif [ -n "$VIRT" ]; then
+ kv "가상화" "VM 게스트 ($VIRT)"
+ else
+ kv "가상화" "물리 서버이거나 확인 불가"
+ fi
+ kv "판정 방식" "$(family_label)"
+
+ if [ -n "$CTR" ]; then
+ section "판정 결과"
+ kv "상태" "$(status_label unknown)"
+ line "컨테이너($CTR) 안에서 실행 중입니다. 컨테이너는 호스트 커널을 공유하므로"
+ line "컨테이너 이미지의 배포판 정보로는 판정할 수 없습니다. 호스트에서 다시 실행하세요."
+ line "호스트에 직접 접근할 수 없다면 호스트의 값을 지정해 판정할 수 있습니다:"
+ cmd "bash $0 --uname-r '<호스트 uname -r>' --uname-v '<호스트 uname -v>' --os-release <호스트 os-release 사본>"
+ exit 4
+ fi
+
+ evaluate_kernel "$KREL" "$RUN_DVER"
+ 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
+ FINAL=$R_STATUS; LP_NAME=""; REBOOT_TARGET=""; REBOOT_KREL=""; NEXT_WARN=""; NB_ALT=""
+ # WSL 은 배포판 커널로 부팅하지 않으므로 설치된 커널·라이브패치 검사가 의미 없다
+ if [ "$OFFLINE" = 0 ] && [ "$FAMILY" != wsl ] && [ "$FAMILY" != wsl1 ]; then
+ case $R_STATUS in
+ vulnerable|pending|eol|unknown)
+ if livepatch_has_fix; then
+ FINAL=livepatched
+ elif [ "$R_INPLACE" = 1 ]; then
+ FINAL=reboot; REBOOT_KREL=$KREL; REBOOT_TARGET="$KREL (부팅 뒤 같은 이름으로 교체된 패키지)"
+ elif installed_fix_ready; then
+ FINAL=reboot
+ fi ;;
+ fixed)
+ # 지금은 안전해도, 다음 부팅 커널이 수정되지 않았다면 재부팅하는 순간 다시 취약해진다
+ next_boot_eval
+ if [ -n "$NB_KREL" ]; then
+ case $NB_STATUS in fixed|unaffected) ;; *) NEXT_WARN=$NB_KREL ;; esac
+ fi ;;
+ esac
+ fi
+
+ section "판정 결과"
+ kv "상태" "$(status_label "$FINAL")"
+ kv "근거" "$R_BASIS"
+ [ -n "$R_FIXED" ] && kv "수정 버전" "$R_FIXED${R_ADV:+ ($R_ADV)}"
+ [ "$FINAL" = livepatched ] && kv "라이브패치" "$LP_NAME"
+ [ "$FINAL" = reboot ] && kv "다음 부팅 수정 커널" "$REBOOT_TARGET"
+ [ -n "$NEXT_WARN" ] &&
+ kv "주의" "${C_YEL}다음 부팅 커널 $NEXT_WARN 은(는) 수정되지 않았습니다. 재부팅하면 다시 취약해질 수 있습니다.${C_OFF}"
+ if [ "$FINAL" != reboot ] && [ -n "$NB_ALT" ]; then
+ kv "참고" "수정 커널 $NB_ALT 이(가) 설치돼 있지만 기본 부팅 커널이 아닙니다 → sudo grubby --set-default /boot/vmlinuz-$NB_ALT"
+ fi
+
+ if [ "$OFFLINE" = 0 ]; then
+ section "노출 위험 요소"
+ check_exposure
+ fi
+
+ case $FINAL in
+ fixed|unaffected|livepatched)
+ section "조치 방법"
+ print_remedy "$FINAL"
+ print_refs_section ;;
+ *)
+ # 입력과 출력이 모두 터미널일 때만 메뉴 (출력을 파일로 돌리면 보이지 않는 질문에서 멈추므로)
+ if [ "$OFFLINE" = 0 ] && [ -t 0 ] && [ -t 1 ]; then choose_action; else show_manual; fi ;;
+ esac
+
+ case $FINAL in
+ fixed|unaffected|livepatched) exit 0 ;;
+ vulnerable) exit 1 ;;
+ reboot) exit 2 ;;
+ pending|eol) exit 3 ;;
+ *) exit 4 ;;
+ esac
+}
+
+main "$@"
|