# 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`:

```c
	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.

## Recommended fix

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.
