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

cpuctl microcode update paths ignore rdmsr_safe() failures: uninitialized stack rev0/rev1/res printed to dmesg and used in the success decision

Summary

rdmsr_safe leaves *val untouched on fault, so on a hypervisor that #GPs MSR_BIOS_SIGN/VIA 0x1205 the kprintf discloses up to 64 bits of kernel stack to dmesg and the rev comparison drives the success return on garbage. Root-only triple-gated (priv_check + 0640 root:kmem + securelevel); QEMU implements 0x8B so not triggerable there (probe shows initialized rev=0x1). Zero-init fix. NOTE: the MSR ioctls themselves are safe (rdmsr_safe/wrmsr_safe + msr_onfault verified live: EFAULT no panic) - the DF-4735/4825 #GP family does NOT apply here.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4865 Β· 9 files
FileTypeDescriptionSize
cpuctl_probe.c β€” 4.6 KB view raw
build.sh β€” 101 B view raw
run.sh β€” 176 B view raw
build.log β€” 96 B view raw
run.log β€” 1.4 KB view raw
run.2.log β€” 164 B view raw
env.txt β€” 336 B view raw
fix.diff β€” 579 B view raw
VERDICT.md β€” 3.9 KB ↓ raw
VERDICT.md
↓ download raw

DF-4865 VERDICT β€” not_reproduced (trigger precondition absent on this guest); code-level defect certain

What was tested

Live verification on the QEMU guest (DragonFly 6.5-DEVELOPMENT, X86_64_GENERIC, QEMU Virtual CPU version 2.5+, 6 CPUs, securelevel -1, gcc 8.3):

  1. Device perms (task item g): cpuctl is an optional module (sys/conf/files:408), not present in X86_64_GENERIC; after kldload cpuctl the nodes /dev/cpuctl0..5 appear as crw-r----- root kmem β€” mode 0640 (cpuctl.c:480-481). An unprivileged user (maxx, uid 1001, no kmem group) cannot open the device at all (Permission denied). The MSR porthole is root:kmem-gated exactly as coded; identical to upstream FreeBSD.

  2. The #GP family question (DF-4735/DF-4825 context): all four MSR ioctls (RDMSR/WRMSR/MSRSBIT/MSRCBIT) issued against invalid MSR 0x11111111 returned -1 / EFAULT and the guest stayed alive. cpuctl.c uses rdmsr_safe/wrmsr_safe (cpuctl.c:197,199,202,204,208,210) whose #GP is fielded by msr_onfault (sys/platform/pc64/x86_64/support.s:861-866). No bare rdmsr/wrmsr exists in this file β€” the coretemp/clockmod panic family does not reach here.

  3. The finding itself (uninit stack print): CPUCTL_UPDATE with a garbage 64-byte blob returned EEXIST (rev1 == rev0, cpuctl.c:307-310) and the kernel printed [cpu 0]: updated microcode from rev=0x1 to rev=0x1 β€” initialized values. Root cause analysis is code-certain: - cpuctl.c:289 rdmsr_safe(MSR_BIOS_SIGN, &rev0); β€” return ignored; - cpuctl.c:302 rdmsr_safe(MSR_BIOS_SIGN, &rev1); β€” return ignored; - cpuctl.c:400/422/425 same pattern for rev0/res/rev1 in update_via(); - rdmsr_safe writes *val only on success (support.s:820-836: the store at movq %rax,(%rsi) is skipped when rdmsr faults into msr_onfault). On fault, rev0/rev1/res remain uninitialized stack; cpuctl.c:304 kprintf's (unsigned)(rev0>>32)/(unsigned)(rev1>>32) (≀64 bits of kernel stack to dmesg) and cpuctl.c:307/431 make the success decision on garbage. The branch requires MSR 0x8B (architectural since P6) or 0x1205 to #GP β€” cannot be made to fault on this QEMU CPU, so not_reproduced / impact none here; classified Info (root-only, exotic-fault precondition).

  4. Negative results (pass-2 classes killed on this file) β€” see the JSON negative_notes for the full list: MSR RMW race serialized (global cpuctl_lock cpuctl.c:103 + crit_enter cpuctl.c:201/207); CPUID copyout framing exact and pre-zeroed (cpuctl.c:153; sys_generic.c:668-724); UPDATE bounds 0 < size ≀ 4MB with no size_t overflow and single clean free (cpuctl.c:265-313); negative dev2unit unreachable (units only 0..ncpus-1, cpuctl.c:480).

Exploit chain

None β€” no memory corruption; worst case is ≀64 bits of kernel stack to the console/dmesg, readable only by root-equivalent users (the ioctl requires SYSCAP_NOCPUCTL_UPDATE and an FWRITE open of a 0640 root:kmem node).

Fix

fix.diff: zero-initialize rev0/rev1 (update_intel) and rev0/rev1/res (update_via). Fix validation (kernel rebuild) not performed: the finding did not reproduce on this guest (no observable misbehavior to diff against) β€” fix_status: not_testable.

Artifacts

  • cpuctl_probe.c β€” probe source (sha256 2a7d932d6d99d028fa8e59a1c9323385e9c628f1f7700a1557ef1b7a421f39a3)
  • build.log / run.log (root run incl. dmesg kprintf) / run.2.log (unpriv user)
  • env.txt β€” guest uname, securelevel, module state, node perms

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 validation moot: the uninit branch could not be triggered on this guest (MSR_BIOS_SIGN never faults under QEMU), so there is no observable baseline misbehavior to confirm fixed. fix.diff is a one-line-per-function zero-init, trivially correct by inspection.

fix.diff; VERDICT.md
↓ fix.diffper-fix-DF-4865

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (root probe: sections 2/3/5 - EFAULT family, UPDATE validation, kprintf rev=0x1 initialized); run.2.log (unprivileged Permission denied); env.txt (perms 0640 root:kmem, securelevel -1, optional module); VERDICT.md (full trace); cpuctl_probe.c sha256 2a7d932d6d99d028fa8e59a1c9323385e9c628f1f7700a1557ef1b7a421f39a3

PoC changes

Seed was a fresh probe authored for this run (no prior seed existed): covers perms gate, all 4 MSR ioctls on an invalid MSR, CPUID framing with poisoned buffers, and UPDATE argument validation incl. the kprintf observation.

Verified recommended fix

Zero-initialize rev0/rev1 (update_intel) and rev0/rev1/res (update_via) - see fix.diff.

Verdict

Code-certain CWE-457: update_intel (cpuctl.c:289,302) and update_via (cpuctl.c:400,422,425) ignore rdmsr_safe() returns; rdmsr_safe leaves val untouched on #GP (support.s:820-836), so rev0/rev1/res would be uninitialized stack, printed by kprintf (cpuctl.c:304, <=64 bits of kernel stack to dmesg) and fed into the rev1>rev0 / res!=0x01 decisions. On the audit guest the branch cannot fire: MSR_BIOS_SIGN (0x8B) reads fine under QEMU (probe shows 'updated microcode from rev=0x1 to rev=0x1' with initialized values), so not_reproduced/impact none here. Root-only path (SYSCAP_NOCPUCTL_UPDATE + FWRITE on 0640 root:kmem node) -> Info severity. The same guest run positively confirmed: /dev/cpuctl = 0640 root:kmem (cpuctl.c:481), unprivileged user cannot open at all, and all four MSR ioctls on an invalid MSR return EFAULT without panic (rdmsr_safe/wrmsr_safe + msr_onfault, support.s:861) - the DF-4735/DF-4825 #GP-panic family does not apply to the cpuctl ioctl path.