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

aperf: raw rdmsr(MSR_IA32_APERF/MSR_IA32_MPERF) in the 1s-periodic per-CPU sensor task turns a CPUID-lying platform (VMM advertising the leaf-6 hooks bit without the MSR) into a fatal kernel #GP ~1s after attach β€” DF-4735/DF-5155 class, driver-level guest-reproduced

Summary

Raw fault-unsafe rdmsr gated only by the lie-able CPUID.06H.ECX bit0 (no cpu_high guard either). DRIVER-LEVEL REPRODUCED on guest: lie.ko force-adds children (the DF-4735 CPUID-substitution only) -> the stock unmodified driver attaches and its REAL task traps 'Stopped at aperf_sensor_task+0x48: rdmsr', guest dead; KVM's own ECX-bit stripping on both cpu models demonstrates the lying class. Not in GENERIC (LINT64 only). rdmsr_safe + cpu_high-guard fix validated end-to-end (patched driver attaches on the same faulting platform, tasks fire, no trap).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5160 Β· 19 files
FileTypeDescriptionSize
aptest.c β€” 2.8 KB view raw
approbe.c β€” 1.4 KB view raw
lie.c β€” 2.0 KB view raw
apfix.c β€” 1.8 KB view raw
aperf_fixed.c β€” 4.6 KB view raw
build.sh β€” 323 B view raw
run.sh β€” 185 B view raw
build.log β€” 12.1 KB view raw
stock_driver_gate.log β€” 100 B view raw
env.txt β€” 535 B view raw
env.2.txt β€” 63 B view raw
run.log β€” 13.7 KB view raw
panic.txt β€” 772 B view raw
run.2.log β€” 14.6 KB view raw
panic2.txt β€” 665 B view raw
run.3.log β€” 14.9 KB view raw
fix.diff β€” 841 B view raw
VERDICT.md β€” 6.4 KB ↓ raw
verdict.json β€” 7.0 KB view raw
VERDICT.md
↓ download raw

DF-5160 β€” VERDICT

Status: reproduced (driver-level). Impact: panic (privileged/boot-time DoS, no privilege boundary crossed). Severity: Low. Confidence: likely.

Root cause

sys/dev/misc/aperf/aperf.c:

  • aperf_sensor_task() executes aperf = rdmsr(MSR_APERF); mperf = rdmsr(MSR_MPERF); (aperf.c:159-160) with the raw inline-asm rdmsr (sys/cpu/x86_64/include/cpufunc.h:528-535) that has no #GP handling β€” a faulting MSR in kernel mode is a fatal trap.
  • The task is registered with period 1s per CPU (aperf.c:131), and the FIRST invocation already performs the raw reads (aperf.c:157-161), so the panic fires ~1s after attach β€” at boot for a compiled-in driver.
  • The only gate is do_cpuid(0x00000006, regs); if ((regs[2] & 1) == 0) return; in aperf_identify() (aperf.c:88-90). This is the DF-4735/DF-5155 lying-CPUID class: CPUID is not the MSR. The in-tree sibling perfbias at least guards cpu_high < 6 || cpu_vendor_id != CPU_VENDOR_INTEL before do_cpuid(6) (perfbias.c:99); aperf has no cpu_high check at all, so on CPUs whose max basic leaf < 6 the documented Intel "echo the highest implemented leaf" behavior can feed foreign ECX data into the bit test (secondary, platform-dependent).
  • The driver already imports the sensors framework and could use rdmsr_safe() (declared cpufunc.h:1004) exactly as coretemp does for IA32_TEMPERATURE_TARGET.

Reachability: aperf is not in X86_64_GENERIC (LINT64:1670 only) β€” a kernel with device aperf on a lying platform panics ~1s after boot with zero user interaction; otherwise root can kldload it. No unprivileged trigger exists.

How it was reproduced (three runs, one guest)

Guest: DragonFly 6.5-DEVELOPMENT #0 (Jul 2 2026) X86_64_GENERIC, KVM, 6 vCPUs (env.txt: qemu64 "Intel" family 0xf model 0x6b; env.2.txt: -cpu host = Xeon E3-1275 v5 / Skylake).

  1. Stock-driver gate observation (both CPU models). Stock aperf.ko built -Werror from the unmodified guest source loads but never attaches: KVM presents CPUID.06H ECX=0 on qemu64 and on -cpu host (approbe/run.2.log: max basic leaf = 0x00000016, 06H EAX=4 ECX=0), while rdmsr_safe(0xE7/0xE8) returns 14 (EFAULT) on both. That is, the bare Skylake has the APERF/MPERF CPUID bit; KVM strips it precisely because it does not virtualize the MSR β€” the exact policy DF-5155 proved for leaf-6 MSRs. The CPUID gate is therefore the only protection, and it is the thing that lies on the vulnerable-platform class.
  2. Trigger-level (run.log/panic.txt, qemu64). aptest.ko prints the gate state, shows rdmsr_safe = 14 twice, then executes the verbatim aperf.c:157-161 body: Fatal trap 9: general protection fault while in kernel mode ... current thread = pri 6 (CRIT) ... Stopped at aptest_modevent+0xd2: rdmsr β†’ DDB, guest down. The CRIT priority matches the sensor task's crit_enter() context.
  3. Driver-level (run.2.log/panic2.txt, -cpu host). lie.ko (DF-4735-documented technique) force-adds aperf children to the cpu devices β€” substituting ONLY the missing CPUID advertisement. The stock, unmodified driver attaches (aperf0..aperf5: <CPU Frequency Sensor> on cpuN) and its real task traps: Stopped at aperf_sensor_task+0x48: rdmsr β†’ DDB, guest down. (Six per-CPU tasks fault within the same second; the concurrent DDB entry garbled the trap banner itself, but the stock function name in the stop line is decisive.)

Fix validation (run.3.log)

fix.diff (git-apply-able, git apply --check clean; also patch(1)-applied inside the guest against /usr/src/sys/dev/misc/aperf/aperf.c whose md5 efc02589720a8f59311a7d396ed325b2 matches the audit tree):

  1. adds #include <machine/md_var.h> and an cpu_high < 6 guard before do_cpuid(6) in identify (echo-hardening, mirrors perfbias.c:99), and
  2. replaces the raw pair with rdmsr_safe(), skipping the sample on fault.

The patched driver built clean under -Werror, was loaded and force-attached via the same lie.ko on the same faulting platform: all six CPUs attached, tasks fired every second for the whole run, no trap, guest alive, hw.sensors.cpuN.freq0: unknown (cpuN freq), UNKNOWN β€” the intended degraded behavior when the MSR faults. apfix.ko (patched body, verbatim shape) prints rdmsr_safe(MSR_APERF)=14 -> sample skipped, NO TRAP. Baseline vs patched on the identical setup: trap-in-stock-driver vs alive-with-patched- driver.

Negative results (classes hunted and killed)

  • div-by-zero (mperf==0): explicitly guarded at aperf.c:183-184; the diffs are unsigned wraparound-safe (aperf.c:171-172).
  • detach UAF / task lifetime: sensor_task_unregister2() takes the per-CPU thr->lock and clears running under it (kern_sensors.c:264-266); the task thread runs st->func() under the same lock (kern_sensors.c:307, 334) and only re-enters a task after the running check at kern_sensors.c:328-331 β€” no window where aperf_sensor_task touches sc after aperf_detach returns (aperf.c:141-145 stops the task before detaching the sensor). No UAF.
  • attach error path: CPU_GET_SENSDEV NULL-checked (aperf.c:118-120); sensor_task_register2 allocates with M_WAITOK (kern_sensors.c:280) so it cannot return a dangling/failed pointer worth a check.
  • sysctl read races: sensor_set writes aligned 64-bit value/status; torn reads are not a memory-safety issue on x86_64 and the sensor value is world-readable by design (envstat).
  • integer overflow: aperf_diff * (tsc_diff/1000) (aperf.c:187) can at worst mis-report frequency; no memory effect.
  • SMP per-CPU pinning: task is registered per-CPU via sensor_task_register2(..., cpu) (aperf.c:131) and runs on that CPU's sensor thread; MSRs are read on the owning CPU. Correct.

Bottom line

Real, driver-level-reproduced fatal trap in shipped source, same lying-CPUID class the project has already accepted twice (DF-4735, DF-5155), with strictly weaker reachability (LINT64-only device, root-gated load) β€” Low severity, fix authored and build/boot-validated end-to-end on the guest.

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

fixed
baseline reproduced→ patch + rebuild →patched clean

baseline reproduced twice (trigger-level Fatal trap 9 at the verbatim instruction body, run.log; driver-level 'Stopped at aperf_sensor_task+0x48: rdmsr' in the stock unmodified driver force-attached via the CPUID-substitution kld, run.2.log); fix.diff (git apply --check clean) applied to the guest /usr/src copy, patched aperf.ko built clean under -Werror, and the identical force-attach on the identical faulting platform yields: all six CPUs attached, 1s-period sensor tasks running continuously, NO trap, guest stays up, sensors correctly held at 'unknown' (hw.sensors.cpuN.freq0) because the patched task skips the sample when rdmsr_safe returns EFAULT (run.3.log; apfix.ko prints the same semantics standalone). fix_patched_reproduced=false means exactly: the patched driver does NOT reproduce the panic - the bad behavior is gone.

findings/poc/DF-5160/fix.diff; run.3.log (patched driver + lie.ko force-attach on -cpu host: aperf0..5 attached rc=0, apfix 'sample skipped, NO TRAP', guest alive); baseline run.2.log/panic2.txt ('Stopped at aperf_sensor_task+0x48: rdmsr'); build.log for the -Werror builds
↓ fix.diffper-fix-DF-5160

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5160/run.log + panic.txt (qemu64: safe reads EFAULT=14 then Fatal trap 9 at the verbatim aperf.c:157-161 body, thread pri 6 CRIT); run.2.log + panic2.txt (-cpu host Skylake: stock driver force-attached via lie.ko, 'Stopped at aperf_sensor_task+0x48: rdmsr'); stock_driver_gate.log + run.2.log approbe lines (KVM strips ECX bit0 on both CPU models while rdmsr_safe returns 14 - the masking policy that demonstrates the lying-CPUID class); run.3.log (patched driver, same force-attach, no trap, guest alive, sensors 'unknown'); aptest.c/approbe.c/lie.c/apfix.c/aperf_fixed.c + build.sh/run.sh + build.log (-Werror); fix.diff (git apply --check clean); env.txt/env.2.txt

PoC changes

no seed pack existed for this file; all trigger sources authored fresh: aptest.c (verbatim task body incl. crit_enter/crit_exit), approbe.c (non-fatal gate/safe-read probe), lie.c (newbus force-attach via devclass_find("cpu") + device_add_child + device_probe_and_attach, substituting only the missing CPUID advertisement per the DF-4735 technique), apfix.c (patched-body replica). lie.ko needs SRCS+= bus_if.h device_if.h under bsd.kmod.mk (first build failed without them, fixed).

Verified recommended fix

aperf_sensor_task: replace the raw pair with rdmsr_safe(MSR_APERF/MSR_MPERF), returning (skipping the sample) on fault; add cpu_high >= 6 guard (+#include ) before do_cpuid(6) in aperf_identify, mirroring perfbias.c:99.

Verdict

reproduced (driver-level): aperf_sensor_task() reads MSR_IA32_APERF(0xE8)/MSR_IA32_MPERF(0xE7) with the raw fault-unsafe rdmsr (aperf.c:159-160) every second (task period 1, aperf.c:131; first invocation already performs the reads), gated only by the live CPUID.06H.ECX bit0 check in aperf_identify (aperf.c:88-90) with no cpu_high guard (contrast perfbias.c:99) and no fault-safe accessor - the DF-4735/DF-5155 lying-CPUID class. Guest proof chain on KVM: (1) stock aperf.ko built -Werror from the unmodified guest source (md5 efc02589720a8f59311a7d396ed325b2 == audit tree) loads but never attaches on either CPU model because KVM presents CPUID.06H ECX=0 even under -cpu host (approbe: max basic leaf 0x16, 06H EAX=4 ECX=0) exactly because it does not virtualize the MSR - while rdmsr_safe(0xE7/0xE8) returns EFAULT(14) on both models; (2) trigger module aptest.ko executing the verbatim aperf.c:157-161 body incl. crit_enter(): 'Fatal trap 9: general protection fault while in kernel mode ... current thread = pri 6 (CRIT) ... Stopped at aptest_modevent+0xd2: rdmsr', guest dead in DDB (run.log/panic.txt); (3) DRIVER-LEVEL: lie.ko force-adds aperf children to the cpu devices (the DF-4735-documented CPUID-advertisement substitution, nothing else) - the stock unmodified driver attaches (aperf0..aperf5 'CPU Frequency Sensor' on cpuN) and its REAL task traps: 'Stopped at aperf_sensor_task+0x48: rdmsr', guest dead (run.2.log/panic2.txt). On any VMM/CPU that advertises ECX bit0 without implementing the MSR, a kernel with device aperf therefore panics ~1s after boot with zero user interaction. Reachability is weaker than DF-4735/DF-5155: aperf is NOT in X86_64_GENERIC (LINT64:1670 only), so it needs a custom/LINT kernel or a root kldload - Low severity, privileged/boot-time DoS, no privilege boundary crossed. Fix build- and boot-validated: fix.diff (cpu_high guard + rdmsr_safe pair, skip sample on fault) applied to guest /usr/src, patched driver built -Werror, force-attached via the same lie.ko on the same faulting platform: all 6 CPUs attach, tasks fire every second, NO trap, guest stays up, hw.sensors.cpuN.freq0 correctly held 'unknown' (run.3.log); apfix.ko replicating the patched body prints 'rdmsr_safe(MSR_APERF)=14 -> sample skipped, NO TRAP'.