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

hyperv_md_uninit() frees the reference-TSC DMA page without quiescing in-flight hyperv_tc64 readers β€” shutdown-time use-after-free read window

Summary

Readers cache the page pointer at entry then loop; md_uninit swaps the fn pointer and immediately frees the page with no cross-CPU quiesce - a CPU already inside the old tc64 (vmbus timer IRQ) can deref freed DMA memory during teardown. Shutdown-only window (builtin SYSUNINIT), no unpriv trigger, KVM-guest dormant. Leak-the-page fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5124 Β· 5 files
FileTypeDescriptionSize
README.md β€” 3.4 KB ↓ raw
VERDICT.md β€” 2.8 KB ↓ raw
fix.diff β€” 719 B view raw
env.txt β€” 416 B view raw
verdict.json β€” 3.0 KB view raw

DF-5124 β€” hyperv_md_uninit() frees the reference-TSC page while in-flight hyperv_tc64 readers may still be dereferencing it

Severity: Low Β· Confidence: speculative (window is real by construction; not demonstrable on the KVM audit guest) Β· Impact ceiling: UAF read of hypervisor-shared memory / panic during shutdown on Hyper-V guests

What this is

sys/dev/virtual/hyperv/x86_64/hyperv_machdep.c:208-225:

    if (hyperv_ref_tsc.tsc_ref != NULL) {
        cputimer_deregister(&hyperv_tsc_cputimer);   /* :215 */
        hyperv_tc64 = hyperv_tc64_saved;             /* :217 */
        val = rdmsr(MSR_HV_REFERENCE_TSC);
        wrmsr(MSR_HV_REFERENCE_TSC, ...);            /* :220 */
        hyperv_dmamem_free(&hyperv_ref_tsc.tsc_ref_dma,
            hyperv_ref_tsc.tsc_ref);                 /* :222 */
        hyperv_ref_tsc.tsc_ref = NULL;               /* :224 */
    }

The readers are the HYPERV_TSC() macros (:96): struct hyperv_reftsc *tsc_ref = hyperv_ref_tsc.tsc_ref; β€” the page pointer is loaded once, then the page is read in a seqlock loop (tsc_seq, tsc_scale, tsc_ofs, re-check tsc_seq), including a possible re-loop.

The race: a CPU that has executed :96 (pointer load) and is inside the seqlock loop is not stopped by anything hyperv_md_uninit() does. :217 swaps the hyperv_tc64 function pointer so new callers are safe, and :215 deregisters the cputimer β€” but per-CPU vmbus timer code that already captured/entered the old tc64 (e.g. vmbus_timer_oneshot(), vmbus/vmbus.c:366,916) keeps running on other CPUs until interrupt shutdown. Between the pointer swap (:217) and the hyperv_dmamem_free() (:222) there is no quiesce β€” an in-flight reader dereferences freed (and unmapped/remapped) DMA memory: use-after-free read of a hypervisor-shared page.

Scope constraints (why Low)

  • The code is compiled into the kernel (sys/platform/pc64/conf/files:203-205 β€” hyperv.c, hyperv_busdma.c, x86_64/hyperv_machdep.c are standard), so hyperv_md_uninit() runs only via SYSUNINIT (hyperv.c:331-344) at system shutdown β€” kldunload cannot reach it.
  • Requires a Hyper-V guest (vmm_guest == VMM_GUEST_HYPERV, hyperv.c:207) with TIME_REFCNT+REFERENCE_TSC features, Intel/AMD CPU with SSE2 (hyperv_machdep.c:162-166).
  • Window is narrow (reader must be mid-loop during teardown) and the consequence is a garbage-timestamp read or a fault on unmapped memory β†’ panic during reboot. No confidentiality/integrity impact: the page contains only the TSC reference record.

Reproduce

Not possible on the audit guest: it is KVM (Origin="KVMKVMKVM", env.txt), so hyperv_identify() fails and the entire file is dormant. Reproducing would require a Hyper-V VM (Gen1/Gen2 with enlightenments) triggering reboot while a vmbus timer interrupt lands on another CPU inside hyperv_tsc_lfence() β€” plausible but a probabilistic shutdown-time race. Classified untested with the reasoning above.

The teardown frees a page the system is about to lose anyway. Either (a) simply do not free (leak one page at shutdown), or (b) add a real quiesce: after restoring hyperv_tc64, wbinv+lwkt_send_ipq_mask( smp_active_mask, ipi_func) style cross-call (or reuse the cputimer framework's systimer_changed() synchronization) to guarantee no CPU is still executing the old tc64 before hyperv_dmamem_free(). fix.diff implements (a) as the minimal safe change.

VERDICT.md
↓ download raw

VERDICT β€” DF-5124

status: untested (guest is KVM; code path dormant β€” cannot be exercised) reproduced: false Β· impact: none (observed) Β· confidence: speculative

Narrative

Claim: hyperv_md_uninit() (hyperv_machdep.c:208-225) frees the reference-TSC DMA page (:222) without quiescing CPUs that may currently be inside the TSC-reading seqlock loop (:96-125) using a pointer loaded before the teardown began.

Lifetime trace (all path:line verified in-tree):

  1. hyperv_md_init() allocates hyperv_ref_tsc.tsc_ref (hyperv_machdep.c:186-188), enables the MSR, registers hyperv_tsc_cputimer and publishes hyperv_tc64 = hyperv_tsc_lfence/ mfence (:201-205).
  2. Runtime readers: vmbus per-CPU timer code calls hyperv_tc64() (vmbus.c:366 in the timer reload path, vmbus.c:916 in vmbus_timer_oneshot) β€” executed in interrupt context, on any CPU. The HYPERV_TSC macro caches the page pointer at entry (:96) and then reads tsc_seq/tsc_scale/tsc_ofs repeatedly (:99-118).
  3. Teardown (SYSUNINIT hyperv_uninit, hyperv.c:331-344; the file is builtin β€” sys/platform/pc64/conf/files:203-205 β€” so this is shutdown-only): cputimer_deregister() (:215) switches sys_cputimer away, hyperv_tc64 is restored to the MSR reader (:217), then the page is freed (:222). Nothing between :217 and :222 guarantees an in-flight reader on another CPU has left the loop. A reader that loaded tsc_ref at :96 before :217 and is spinning/reading between :217 and :222 dereferences freed DMA memory: UAF read.
  4. Consequence bounded to: garbage uint64_t time values (vmbus timer mis-fire) or a fault if the mapping is torn down β†’ panic during shutdown. No unpriv-user trigger, no info leak (page holds only the TSC record), shutdown-gated.

Why "speculative": the window is real by construction (no synchronization exists between the free and in-flight readers), but I cannot demonstrate it β€” the audit guest is KVM (env.txt), where hyperv_identify() fails (hyperv.c:207-208) and this code never runs; even on a Hyper-V guest it is a probabilistic SMP race at reboot.

Sibling asymmetry not re-reported: hyperv_tsc_cpucounter is registered in md_init (:203) but never deregistered β€” already noted in the pass-2 hyperv.c review (no cpucounter_deregister API exists in kern_cputimer.c).

Fix validation

Not applicable (Low-class, shutdown-only, dormant 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

not_testable
baseline no→ patch + rebuild →patched clean

Fix not built: the race cannot be triggered on the KVM audit guest and is shutdown-only on real Hyper-V systems; the proposed fix (skip the free) trivially removes the window since the page outlives any in-flight reader.

findings/poc/DF-5124/fix.diff
↓ fix.diffper-fix-DF-5124

Confirmed kernel references

Detail

Evidence (decisive lines)

findings/poc/DF-5124/VERDICT.md (lifetime trace with path:line); README.md; env.txt (guest is KVM, code dormant)

PoC changes

No PoC exists: requires a Hyper-V guest and a probabilistic SMP race during shutdown; the audit guest is KVM where the code never initializes.

Verified recommended fix

Do not free the reference-TSC page at shutdown (leak one page; md_uninit is shutdown-only builtin code), or add a real cross-CPU quiesce after restoring hyperv_tc64 and before hyperv_dmamem_free (see fix.diff).

Verdict

hyperv_md_uninit() (hyperv_machdep.c:208-225) frees the reference-TSC DMA page at :222 with no quiesce of CPUs that may be inside the HYPERV_TSC seqlock loop using the page pointer cached at :96 - a use-after-free read window between the tc64 pointer swap (:217) and hyperv_dmamem_free (:222). The window is real by construction (no synchronization exists), but its trigger is confined to system shutdown on SMP Hyper-V guests: the file is compiled into the kernel (sys/platform/pc64/conf/files:203-205), so md_uninit runs only via SYSUNINIT (hyperv.c:331-344) at reboot, never kldunload; runtime readers are vmbus per-CPU timer interrupt code (vmbus.c:366,916). Consequence ceiling: garbage timestamps or a fault on freed/unmapped DMA memory -> panic during reboot; no unpriv trigger, no info leak (page holds only the TSC record). Cannot be exercised on the audit guest (KVM - hyperv_identify() fails, hyperv.c:207-208, code dormant) - hence untested/speculative.