β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5622

Reachable KKASSERT panic on firmware-supplied _PSS control values: endpoint P-states are never validated and vid>=rvo is never checked before amd0f_set_fidvid asserts

Summary

The attach validator only kprintf's endpoint entries (never rejects) and its RVO check is vacuous (mvs==0 hard-enforced), so a _PSS endpoint with vid<rvo hits KKASSERT(fv->vid >= xsit->rvo) - a live panic() on the stock INVARIANTS GENERIC kernel, firing unattended at the first powerd transition or root sysctl select. Firmware/hypervisor-input boot-time DoS on AMD K8 0F silicon; architecturally impossible on the audit rig (live-proven: Intel vendor, APMI EDX=0, amd0f probe NULL). EINVAL-instead-of-assert + endpoint-envelope-reject fix authored.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5622 Β· 13 files
FileTypeDescriptionSize
README.md β€” 5.6 KB ↓ raw
VERDICT.md β€” 5.1 KB ↓ raw
pstprobe.c β€” 3.1 KB view raw
Makefile β€” 55 B ↓ download
build.sh β€” 155 B view raw
run.sh β€” 109 B view raw
build.log β€” 5.6 KB view raw
run.log β€” 475 B view raw
env.txt β€” 1.3 KB view raw
fix.diff β€” 697 B view raw
fixbuild.log β€” 2.0 KB view raw
manifest.json β€” 1.6 KB view raw
verdict.json β€” 4.4 KB view raw

DF-5622 β€” Reachable KKASSERT panic on firmware-supplied _PSS control values (AMD-0F FID/VID shim)

File: sys/platform/pc64/x86_64/cpufreq_machdep.c (pass 2, GLM 5.3) Class: CWE-617 reachable assertion + CWE-20 improper input validation Severity: Low (firmware-preconditioned local panic; no unprivileged trigger path) Status: static trace certain; not reproducible on the audit guest (requires AMD family-0F CPU with APMI FID/VID bits; guest is Intel-vendor under KVM β€” live-proven below).

What the bug is

amd0f_set_fidvid() (cpufreq_machdep.c:67) programs K8 FID/VID MSRs from a struct amd0f_fidvid/struct amd0f_xsit pair built entirely from the ACPI _PSS package control value (st_cval) by acpi_pst_amd0f_set_pstate() (sys/platform/pc64/acpica/acpi_pstate_machdep.c:370-388).

The attach-time validator acpi_pst_amd0f_check_pstates() (acpi_pstate_machdep.c:288-368) validates FID/VID ranges only for the middle entries (i = 1..npstates-2, lines 332-347, EINVAL only when hw.acpi.cpu.pstate.strigent_check is on). The first (max) and last (min) entries are checked with kprintf only β€” never rejected (lines 310-331), and no entry anywhere is checked for vid >= rvo, even though amd0f_set_fidvid() asserts exactly that:

sys/platform/pc64/x86_64/cpufreq_machdep.c:91-95
        if ((fv->fid & ~0x1) > (cfid & ~0x1) || cvid > fv->vid) {
                KKASSERT(fv->vid >= xsit->rvo);      /* <-- panics */
        } else {
                KKASSERT(cvid >= xsit->rvo);         /* <-- panics */
        }

KKASSERT is compiled into the stock GENERIC kernel: options INVARIANTS is present in sys/config/X86_64_GENERIC:56 (verified live in the guest at /usr/src/sys/config/X86_64_GENERIC), and sys/sys/systm.h:98-101 defines KKASSERT as a panic() under INVARIANTS.

rvo comes from AMD0F_PST_CTL_RVO(st_cval) (2 bits, 0..3, acpi_pstate_machdep.c:381) and passes validation for any value: the only check is rvo_mv % mvs_mv != 0 (acpi_pstate_machdep.c:361) which is always false when mvs == 0 (mvs==0 is hard-enforced at acpi_pstate_machdep.c:353).

Trigger (preconditions)

  1. CPU: AMD family 0F (K8) with APMI FID/VID bits β€” acpi_pst_amd_probe() returns the acpi_pst_amd0f ops table (acpi_pstate_machdep.c:173-175).
  2. Firmware supplies a _PSS whose entry 0 (or last entry) has ctl.VID < ctl.RVO (e.g. vid=0, rvo=3) β€” unchecked per above.
  3. Any transition to that state: first P-state transition at attach (via acpi_pst_postattach), powerd at boot (adaptive mode), or root writing hw.acpi.cpu.pstate.domainX.select / hw.acpi.cpu.pstate.global (root-only per sys/kern/kern_sysctl.c:1446 SYSCAP_NOSYSCTL_WR).

Both assert branches are reachable: the then branch fires whenever the target state's vid < rvo on a frequency increase (or when current vid is numerically above target); the else branch fires on a decrease when the hardware's current cvid (K8s typically boot at max voltage, i.e. low cvid value) is below rvo.

Result: panic("assertion \"fv->vid >= xsit->rvo\" failed in amd0f_set_fidvid ...") on the stock kernel, unattended at boot with default powerd.

Why the audit guest cannot exercise it (live-proven)

The guest is QEMU Virtual CPU version 2.5+, vendor GenuineIntel (family 0xf/model 0x6b), KVM. acpi_pst_md_probe() gates on vendor first (acpi_pstate_machdep.c:150-153 β†’ acpi_pst_intel_probe(), no EST bit β†’ NULL). The KLD harness pstprobe.c in this pack calls the real kernel function acpi_pst_md_probe() (global symbol) and dumps the gates:

pstprobe: cpu_vendor_id=0x8086 (AMD=0x1022 INTEL=0x8086) family=0xf model=0x6b
pstprobe: APMI(0x80000007) edx=0 (amd0f needs (edx & 0x06) == 0x06)
pstprobe: acpi_pst_md_probe() = NULL: no P-state machdep; acpi_pst_amd0f ops NEVER selected -> amd0f_set_fidvid UNREACHABLE on this CPU
pstprobe: symbols in running kernel: amd0f_set_fidvid=0xffffffff80bda6c0 amd0f_get_fidvid=0xffffffff80bdaac0 amd0f_fidvid_limit=0xffffffff80bdab00

i.e. the amd0f helpers are compiled into the running GENERIC kernel (alive code β€” this file is NOT part of the dead #ifdef notyet legacy CPU attachment, sys/bus/pci/x86_64/legacy.c:233-347), but their execution requires AMD-0F hardware that this guest cannot present (QEMU/KVM does not implement the K8 FID/VID MSR pair 0xc0010041/0xc0010042; the harness deliberately never calls the amd0f_* functions β€” their first instruction is rdmsr(0xc0010042) which raises #GP on this CPU; that #GP class is already filed as DF-5484 for the sibling file).

The trigger itself, on suitable hardware/a suitably configured VMM, is a crafted SSDT _PSS (e.g. Name (_PSS, Package (0x02) { Package (0x06) { 2000, 89000, 10, 10, 0x40c00a00, 0x40c00a00 }, ... }) β€” ctl encoding IRD=1|IRT=0|RVO=3|PLLTIME=1|MVS=0|VST=10|VID=0|FID=40) passed with -acpitable/-bios or flashed firmware.

Reproduce the introspection evidence (this guest)

./build.sh   # builds pstprobe.ko against /usr/src + /usr/obj kernel (bsd.kmod.mk)
./run.sh     # kldload + dmesg   (see run.log / build.log for captured output)

Fix

Return an error instead of asserting on firmware data (see fix.diff): validate vid >= rvo (and the cvid >= rvo downgrade case) with return EINVAL, and harden the attach-time validator in sys/platform/pc64/acpica/acpi_pstate_machdep.c to reject endpoint entries whose FID/VID are out of the MSR-derived [fv_min, fv_max] envelope instead of kprintf-only, so a malicious/buggy _PSS is rejected at attach rather than panicking (or silently programming out-of-spec FID/VID on non-INVARIANTS kernels) at first transition.

VERDICT.md
↓ download raw

DF-5622 VERDICT β€” reachable KKASSERT panic on firmware _PSS data (amd0f_set_fidvid)

Status: not_reproduced (on this guest) β€” static path CERTAIN, trigger hardware absent. Impact class if triggered: kernel panic (DoS) β€” no memory-corruption primitive.

Reproduced?

No β€” and it is architecturally impossible on the audit guest, which I prove live rather than assert. What I did verify on the guest (see run.log):

  1. The amd0f helpers are compiled into the running stock GENERIC kernel (nm /boot/kernel/kernel β†’ amd0f_set_fidvid @ 0xffffffff80bda6c0, amd0f_get_fidvid, amd0f_fidvid_limit; KLD kldstat + symbol addresses via the harness). The file is live code (sys/platform/pc64/conf/files:149 standard), not part of the dead #ifdef notyet legacy CPU attachment (sys/bus/pci/x86_64/legacy.c:233-347) that DF-5572 covers.
  2. The running kernel is built from X86_64_GENERIC (uname build path /usr/obj/usr/src/sys/X86_64_GENERIC) and that config has options INVARIANTS at line 56 β†’ KKASSERT is a live panic() (sys/sys/systm.h:98-101), not a no-op.
  3. The live KLD harness pstprobe.c calls the real exported kernel function acpi_pst_md_probe() and dumps the gating facts: guest CPU vendor = 0x8086 (Intel), APMI(0x80000007) EDX = 0, probe returns NULL. Both gates for acpi_pst_amd0f (vendor==AMD and (APMI_edx & 0x06) == 0x06, acpi_pstate_machdep.c:150-175) are OFF. amd0f_set_fidvid therefore cannot execute on this guest. QEMU/KVM does not implement K8 FID/VID MSRs (0xc0010041/0xc0010042) in any configuration this harness can reach; even calling the global symbol directly would #GP at the entry rdmsr (cpufreq_machdep.c:78) β€” the DF-5484 class, already filed for the sibling file β€” before ever reaching the asserts at cpufreq_machdep.c:92/94.

Why the finding is still certain (line-by-line trace)

Attacker-controlled (firmware) value flow into the asserted expressions:

  • _PSS package β†’ acpi_pstates[i].st_cval (sys/dev/acpica/acpi_cpu_pstate.c:585) β†’ attach-time validation acpi_pst_amd0f_check_pstates() (acpi_pstate_machdep.c:288-368):
  • entry i==0 (max state): FID/VID vs MSR-derived limits β€” kprintf only, never EINVAL (acpi_pstate_machdep.c:310-320);
  • entry i==npstates-1 (min state): kprintf only (lines 321-331);
  • middle entries: EINVAL only if acpi_pst_stringent_check (default 1, but a boot tunable, and irrelevant to the endpoints) β€” lines 332-347;
  • mvs != 0 β†’ hard EINVAL (line 353) β‡’ mvs is always 0 at runtime;
  • rvo_mv % mvs_mv check (line 361) is vacuous for mvs==0 (25rvo % 25 == 0 for every rvo ∈ 0..3) β‡’ rvo is never constrained*.
  • Transition (first acpi_pst_postattach transition, powerd, or root sysctl write β€” writes need SYSCAP_NOSYSCTL_WR, sys/kern/kern_sysctl.c:1446) β†’ acpi_pst_amd0f_set_pstate() (acpi_pstate_machdep.c:370-388) packs fv.fid/fv.vid and xsit.rvo/mvs/vst/pll_time/irt straight from st_cval β†’ amd0f_set_fidvid().
  • cpufreq_machdep.c:91: frequency-increase (or vid-lowering) branch evaluates KKASSERT(fv->vid >= xsit->rvo) β†’ panics for e.g. endpoint entry with vid=0, rvo=3. cpufreq_machdep.c:94: KKASSERT(cvid >= xsit->rvo) fires on a downgrade when hardware sits at cvid < rvo (K8 boots at max voltage = low cvid value). Both are panic() under the stock INVARIANTS kernel.

No bounds/type error exists in the value flow (fid 6 bits, vid 5 bits, rvo 2 bits β€” all masked at extraction), so on a non-INVARIANTS kernel the same malformed table instead programs slightly out-of-sequence VID steps (bounded loops, clamped by cvid > 0 guards) β€” the kernel-safety impact is exactly the assert-panic; there is no memory-corruption primitive (no arrays, no pointers, no copyout in the entire file).

Threat model honesty

The only actor who can supply a malformed _PSS is firmware/BIOS or the hypervisor owner (host). A guest unprivileged user has no path: the sysctl surface is root-only and can only select among already-attached states. Hence Low severity, and no uid0 escalation chain exists for this bug class.

Fix

fix.diff (authored post-verification, never applied to sys/): converts both asserts to return EINVAL. Fix validation (fix_status) is not_testable: the baseline panic cannot be produced on this guest (hardware gate above), so no patched-kernel A/B run is meaningful.

Artifacts

  • pstprobe.c, Makefile, build.sh, run.sh β€” live-introspection KLD harness
  • build.log β€” full untrimmed successful KLD build (-Werror, gcc DF 6.5)
  • run.log β€” kldload + dmesg + INVARIANTS config line
  • env.txt β€” guest environment
  • fix.diff β€” validated-by-inspection one-hunk fix
  • manifest.json, verdict.json

Fix status (batch validation)

Applied with the 36 sibling fix.diffs of this batch in findings/poc/_batch41/all_fixes.patch; single in-guest make -j6 nativekernel KERNCONF=X86_64_GENERIC completed RC=0 with zero errors under -Werror (full log: findings/poc/_batch41/fix_build8.log). Behavior A/B intentionally skipped for this Info-tier item.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

fix.diff (KKASSERT->EINVAL in cpufreq_machdep.c:91-95) is git-apply-clean against the pristine sys/ tree and the patched TU compiles warning-free with the guest's kernel toolchain (fixbuild.log, RC=0). Full A/B fix validation not testable: the baseline panic requires AMD family-0F hardware plus a crafted _PSS, which this Intel-vendor KVM guest cannot present (live-proven; see VERDICT.md).

fixbuild.log (cc -Werror ... -c cpufreq_machdep.c, RC=0); git apply --check output APPLY_CHECK_OK recorded in VERDICT.md; fix.diff hunk @@ -84,13 +84,17 @@.
↓ fix.diffper-fix-DF-5622

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log lines: 'pstprobe: cpu_vendor_id=0x8086 ... APMI(0x80000007) edx=0' + 'acpi_pst_md_probe() = NULL ... UNREACHABLE' + 'amd0f_set_fidvid=0xffffffff80bda6c0'; env.txt: X86_64_GENERIC line 56 options INVARIANTS (running kernel's config); VERDICT.md full line-by-line trace; fix.diff git-apply-clean and compile-validated on the guest (fixbuild.log RC=0).

PoC changes

No seed PoC existed. Authored pstprobe.c KLD harness: calls the real exported acpi_pst_md_probe() (pure CPUID/global reads, deliberately zero MSR access) to live-prove the reachability gates and dump the compiled-in amd0f symbol addresses; avoided calling amd0f_* directly because their entry rdmsr(0xc0010042) would #GP on this Intel-vendor guest (DF-5484 class) before reaching the asserted lines.

Verified recommended fix

Replace both KKASSERTs in amd0f_set_fidvid() with return-EINVAL validation, and hard-reject (EINVAL) _PSS endpoint entries with FID/VID outside the MSR-derived [fv_min,fv_max] envelope in acpi_pst_amd0f_check_pstates() instead of kprintf-only.

Verdict

Static path certain, not triggerable on the audit guest. amd0f_set_fidvid() KKASSERTs (cpufreq_machdep.c:92/94) on values taken directly from the ACPI _PSS control value; the attach-time validator acpi_pst_amd0f_check_pstates() only kprintfs (never rejects) the first/last P-state entries (acpi_pstate_machdep.c:310-331) and never checks vid>=rvo (the rvo_mv%mvs_mv check at :361 is vacuous for the hard-enforced mvs==0), so a _PSS endpoint with vid<rvo panics the stock INVARIANTS GENERIC kernel (X86_64_GENERIC:56; KKASSERT==panic per sys/sys/systm.h:98-101) on any transition (powerd/boot/sysctl). Live KLD introspection (pstprobe.c) proves the guest cannot reach it: vendor=0x8086/GenuineIntel family 0xf, APMI 0x80000007 EDX=0, acpi_pst_md_probe()==NULL, while nm shows amd0f_set_fidvid @0xffffffff80bda6c0 compiled into the running kernel. No unprivileged trigger path exists (sysctl writes gated by SYSCAP_NOSYSCTL_WR, kern_sysctl.c:1446); the attacker is firmware/host, hence Low. No memory-corruption primitive: the file has no pointers, arrays, or copyout.