From 954eb13468d1eb23b3fd19368906981c43d57340 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Lesimple?= Date: Mon, 6 Apr 2026 18:58:36 +0200 Subject: [PATCH] enh: when reading CPUID is unavailable (VM?), fallback to cpuinfo where applicable cap_* variable <= cpuinfo flag cap_ibrs <= ibrs cap_ibpb <= ibpb cap_stibp <= stibp cap_ssbd <= ssbd / virt_ssbd cap_l1df <= flush_l1d cap_md_clear <= md_clear cap_arch_capabilities <= arch_capabilities Should fix #288 --- DEVELOPMENT.md | 47 ++++++++++++++++++++++++++++++++++++++-- src/libs/400_hw_check.sh | 42 +++++++++++++++++++++++++++++++++++ 2 files changed, 87 insertions(+), 2 deletions(-) diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md index 56ba3bc..b128eaf 100644 --- a/DEVELOPMENT.md +++ b/DEVELOPMENT.md @@ -129,11 +129,54 @@ Never look at the microcode version to determine whether it has the proper mitig **Exception**: When a vulnerability is fixed purely by a microcode update and the fix exposes **no** detectable CPUID bit, MSR bit, or ARCH\_CAP flag, then we must hardcode the known-fixing microcode versions for each affected CPU stepping. In this case, build a `_ucode_list` table of `FF-MM-SS/platformid_mask,fixed_ucode_version` tuples (sourced from the Intel affected processor list and the Intel-Linux-Processor-Microcode-Data-Files release notes), match against `cpu_cpuid` + `cpu_platformid` in `is_cpu_affected()`, and store the required version in a `g__fixed_ucode_version` global. The CVE check then compares `cpu_ucode` against this threshold. Because Intel never lists EOL CPUs, the microcode list may be incomplete: keep a model blacklist as a fallback so that affected CPUs without a known fix are still flagged as affected (the CVE check should handle the empty `g__fixed_ucode_version` case by reporting VULN with "no microcode update available"). See Reptar (`g_reptar_fixed_ucode_version`) and BPI (`g_bpi_fixed_ucode_version`) for reference implementations. -### 4. Assume affected unless proven otherwise (whitelist approach) +### 4. `/proc/cpuinfo` fallback for CPUID reads + +The primary way to read CPU capability bits is via `read_cpuid` (which uses `/dev/cpu/N/cpuid`). However, this device may be unavailable — most commonly inside virtual machines where the `cpuid` kernel module cannot be loaded. When `read_cpuid` returns `READ_CPUID_RET_ERR` (could not read at all), we can fall back to checking `/proc/cpuinfo` flags as a secondary source, **in live mode only**. + +This works because the kernel always has direct access to CPUID (it doesn't need `/dev/cpu`), and exposes the results as flags in `/proc/cpuinfo`. When a hypervisor virtualizes a CPUID bit for the guest, the guest kernel sees it and reports it in `/proc/cpuinfo`. This is the same information `read_cpuid` would return if the device were available. + +**Rules:** +- This is strictly a fallback: `read_cpuid` via `/dev/cpu/N/cpuid` remains the primary method. +- Only use it when `read_cpuid` returned `READ_CPUID_RET_ERR` (device unavailable), **never** when it returned `READ_CPUID_RET_KO` (device available but bit is 0 — meaning the CPU/hypervisor explicitly reports the feature as absent). +- Only in live mode (`$opt_live = 1`), since `/proc/cpuinfo` is not available in offline mode. +- Only for CPUID bits that the kernel exposes as `/proc/cpuinfo` flags. Not all bits have a corresponding flag — only those listed in the kernel's `capflags.c`. If a bit has no `/proc/cpuinfo` flag, no fallback is possible. +- The fallback depends on the running kernel being recent enough to know about the CPUID bit in question. An older kernel won't expose a flag it doesn't know about, so the fallback will silently not trigger — which is fine (we just stay at UNKNOWN, same as the ERR case without fallback). + +**Known mappings** (CPUID bit → `/proc/cpuinfo` flag → script `cap_*` variable): + +| CPUID source | `/proc/cpuinfo` flag | `cap_*` variable | +|---|---|---| +| Intel 0x7.0.EDX[26] / AMD 0x80000008.EBX[14] | `ibrs` | `cap_ibrs` | +| AMD 0x80000008.EBX[12] | `ibpb` | `cap_ibpb` | +| Intel 0x7.0.EDX[27] / AMD 0x80000008.EBX[15] | `stibp` | `cap_stibp` | +| Intel 0x7.0.EDX[31] / AMD 0x80000008.EBX[24,25] | `ssbd` / `virt_ssbd` | `cap_ssbd` | +| Intel 0x7.0.EDX[28] | `flush_l1d` | `cap_l1df` | +| Intel 0x7.0.EDX[10] | `md_clear` | `cap_md_clear` | +| Intel 0x7.0.EDX[29] | `arch_capabilities` | `cap_arch_capabilities` | + +**Implementation pattern** in `check_cpu()`: + +```sh +read_cpuid 0x7 0x0 $EDX 31 1 1 +ret=$? +if [ $ret = $READ_CPUID_RET_OK ]; then + cap_ssbd='Intel SSBD' +elif [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ]; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + if grep ^flags "$g_procfs/cpuinfo" | grep -qw ssbd; then + cap_ssbd='Intel SSBD (cpuinfo)' + ret=$READ_CPUID_RET_OK + fi +fi +``` + +When the fallback sets a `cap_*` variable, append ` (cpuinfo)` to the value string so the output makes it clear the information was derived from `/proc/cpuinfo` rather than read directly from hardware. Update `ret` to `READ_CPUID_RET_OK` so downstream status display logic (`pstatus`) reports YES rather than UNKNOWN. + +### 5. Assume affected unless proven otherwise (whitelist approach) When a CPU is not explicitly known to be unaffected by a vulnerability, assume that it is affected. This conservative default has been the right call since the early Spectre/Meltdown days and remains sound. -### 5. Offline mode +### 6. Offline mode The script can analyze a non-running kernel via `--kernel`, `--config`, `--map` flags, allowing verification before deployment. diff --git a/src/libs/400_hw_check.sh b/src/libs/400_hw_check.sh index 04ca0fa..bc5f52d 100644 --- a/src/libs/400_hw_check.sh +++ b/src/libs/400_hw_check.sh @@ -446,6 +446,15 @@ check_cpu() { ret=invalid pstatus yellow NO "unknown CPU" fi + if [ -z "$cap_ibrs" ] && [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ]; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + if grep ^flags "$g_procfs/cpuinfo" | grep -qw ibrs; then + cap_ibrs='IBRS (cpuinfo)' + cap_spec_ctrl=1 + pstatus green YES "ibrs flag in $g_procfs/cpuinfo" + ret=$READ_CPUID_RET_OK + fi + fi if [ $ret = $READ_CPUID_RET_KO ]; then pstatus yellow NO elif [ $ret = $READ_CPUID_RET_ERR ]; then @@ -514,6 +523,10 @@ check_cpu() { if [ $ret = $READ_CPUID_RET_OK ]; then cap_ibpb='IBPB_SUPPORT' pstatus green YES "IBPB_SUPPORT feature bit" + elif [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ] && grep ^flags "$g_procfs/cpuinfo" | grep -qw ibpb; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + cap_ibpb='IBPB (cpuinfo)' + pstatus green YES "ibpb flag in $g_procfs/cpuinfo" elif [ $ret = $READ_CPUID_RET_KO ]; then pstatus yellow NO else @@ -581,6 +594,14 @@ check_cpu() { ret=invalid pstatus yellow UNKNOWN "unknown CPU" fi + if [ -z "$cap_stibp" ] && [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ]; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + if grep ^flags "$g_procfs/cpuinfo" | grep -qw stibp; then + cap_stibp='STIBP (cpuinfo)' + pstatus green YES "stibp flag in $g_procfs/cpuinfo" + ret=$READ_CPUID_RET_OK + fi + fi if [ $ret = $READ_CPUID_RET_KO ]; then pstatus yellow NO elif [ $ret = $READ_CPUID_RET_ERR ]; then @@ -645,6 +666,15 @@ check_cpu() { fi fi + if [ -z "$cap_ssbd" ] && [ "$ret24" = $READ_CPUID_RET_ERR ] && [ "$ret25" = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ]; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + if grep ^flags "$g_procfs/cpuinfo" | grep -qw ssbd; then + cap_ssbd='SSBD (cpuinfo)' + elif grep ^flags "$g_procfs/cpuinfo" | grep -qw virt_ssbd; then + cap_ssbd='SSBD in VIRT_SPEC_CTRL (cpuinfo)' + fi + fi + if [ -n "${cap_ssbd:=}" ]; then pstatus green YES "$cap_ssbd" elif [ "$ret24" = $READ_CPUID_RET_ERR ] && [ "$ret25" = $READ_CPUID_RET_ERR ]; then @@ -700,6 +730,10 @@ check_cpu() { if [ $ret = $READ_CPUID_RET_OK ]; then pstatus green YES "L1D flush feature bit" cap_l1df=1 + elif [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ] && grep ^flags "$g_procfs/cpuinfo" | grep -qw flush_l1d; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + pstatus green YES "flush_l1d flag in $g_procfs/cpuinfo" + cap_l1df=1 elif [ $ret = $READ_CPUID_RET_KO ]; then pstatus yellow NO cap_l1df=0 @@ -716,6 +750,10 @@ check_cpu() { if [ $ret = $READ_CPUID_RET_OK ]; then cap_md_clear=1 pstatus green YES "MD_CLEAR feature bit" + elif [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ] && grep ^flags "$g_procfs/cpuinfo" | grep -qw md_clear; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + cap_md_clear=1 + pstatus green YES "md_clear flag in $g_procfs/cpuinfo" elif [ $ret = $READ_CPUID_RET_KO ]; then cap_md_clear=0 pstatus yellow NO @@ -782,6 +820,10 @@ check_cpu() { if [ $ret = $READ_CPUID_RET_OK ]; then pstatus green YES cap_arch_capabilities=1 + elif [ $ret = $READ_CPUID_RET_ERR ] && [ "$opt_live" = 1 ] && grep ^flags "$g_procfs/cpuinfo" | grep -qw arch_capabilities; then + # CPUID device unavailable (e.g. in a VM): fall back to /proc/cpuinfo + pstatus green YES "arch_capabilities flag in $g_procfs/cpuinfo" + cap_arch_capabilities=1 elif [ $ret = $READ_CPUID_RET_KO ]; then pstatus yellow NO cap_arch_capabilities=0