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)
PoC verification
Evidence pack
findings/poc/DF-5160 Β· 19 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
DF-5160 β VERDICT
Status: reproduced (driver-level). Impact: panic (privileged/boot-time DoS, no privilege boundary crossed). Severity: Low. Confidence: likely.
Root cause
aperf_sensor_task()executesaperf = rdmsr(MSR_APERF); mperf = rdmsr(MSR_MPERF);(aperf.c:159-160) with the raw inline-asmrdmsr(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;inaperf_identify()(aperf.c:88-90). This is the DF-4735/DF-5155 lying-CPUID class: CPUID is not the MSR. The in-tree siblingperfbiasat least guardscpu_high < 6 || cpu_vendor_id != CPU_VENDOR_INTELbeforedo_cpuid(6)(perfbias.c:99); aperf has nocpu_highcheck 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()(declaredcpufunc.h:1004) exactly as coretemp does forIA32_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).
- Stock-driver gate observation (both CPU models). Stock
aperf.kobuilt -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), whilerdmsr_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. - Trigger-level (run.log/panic.txt, qemu64).
aptest.koprints the gate state, showsrdmsr_safe= 14 twice, then executes the verbatimaperf.c:157-161body: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'scrit_enter()context. - Driver-level (run.2.log/panic2.txt, -cpu host).
lie.ko(DF-4735-documented technique) force-addsaperfchildren 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):
- adds
#include <machine/md_var.h>and ancpu_high < 6guard beforedo_cpuid(6)in identify (echo-hardening, mirrors perfbias.c:99), and - 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-CPUthr->lockand clearsrunningunder it (kern_sensors.c:264-266); the task thread runsst->func()under the same lock (kern_sensors.c:307, 334) and only re-enters a task after therunningcheck at kern_sensors.c:328-331 β no window whereaperf_sensor_tasktouchesscafteraperf_detachreturns (aperf.c:141-145 stops the task before detaching the sensor). No UAF. - attach error path:
CPU_GET_SENSDEVNULL-checked (aperf.c:118-120);sensor_task_register2allocates with M_WAITOK (kern_sensors.c:280) so it cannot return a dangling/failed pointer worth a check. - sysctl read races:
sensor_setwrites 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
fixedbaseline 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
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
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'.
No comments yet.