# DF-1061 — pmap_inval LOOPRECOVER watchdog silent TLB-invalidation abandonment

## Verdict

**NOT REPRODUCED (runtime) — STATIC VERIFICATION CONFIRMED.**

The cited code path and bug exist verbatim in the running default GENERIC
kernel (`X86_64_GENERIC`, `6.5-DEVELOPMENT #0`). The runtime trigger,
however, requires a target vCPU to be descheduled / unresponsive to the
`Xinvltlb` IPI for **> 2 seconds** during a pending TLB shootdown. This
precondition is **not inducible by an unprivileged local user inside the
guest**:

- `Xinvltlb` fires at **interrupt level** and ignores critical sections
  (`sys/platform/pc64/x86_64/mp_machdep.c`, `Xinvltlb` IPI handler).
  Even a `SCHED_FIFO` busy-loop pinned to vCPU N cannot prevent the
  kernel on that vCPU from servicing the IPI the instant the vCPU is
  scheduled.
- The only realistic producers of the > 2 s stall are **outside the guest**:
  a hostile or oversubscribed **host** starving a vCPU of host pCPU time
  (the exact scenario the watchdog exists for — see the author's comment
  at `pmap_inval.c:73-77` *"VMs could be very slow at handling IPIs"*), or
  an SMI storm / wedged pCPU on bare metal.

So this is a **latent / host-gated** defect: real in source, present in
the default kernel, but unreachable from the unprivileged-guest threat
model that this audit exercises. Classified as Medium with CVSS
`AV:L/AC:H/PR:L` — the AC:High reflects exactly this host-induced
precondition.

## Mechanism (confirmed by source trace)

Originator-cpu `pmap_inval_smp()` ("A" loop) and `pmap_inval_smp_cmpset()`
("B" loop) each wait for the **prior** command's `info->done` mask to drain
before reusing the per-cpu command slot:

- `sys/platform/pc64/x86_64/pmap_inval.c:342` — `while (CPUMASK_TESTNZERO(info->done))`
- `sys/platform/pc64/x86_64/pmap_inval.c:491` — same in cmpset variant

On `LOOPRECOVER_TIMEOUT1 = 2 s` (`:78`) watchdog expiry both loops
**force-zero** `info->done` and fall through, allowing the originator to
overwrite the command slot at `:363-372` (`info->va / npgs / ptep / npte / mode`)
and return success (`opte` / `success = 1`):

- `sys/platform/pc64/x86_64/pmap_inval.c:344-348` — A-path force-clear
  ```c
  if (loopwdog(info)) {
      info->failed = 1;
      loopdebug("A", info);
      /* XXX recover from possible bug */
      CPUMASK_ASSZERO(info->done);   /* <-- silent abandonment */
  }
  ```
- `sys/platform/pc64/x86_64/pmap_inval.c:491-497` — B-path force-clear (identical)

A still-set `done` bit means the target cpu has **NOT yet** flushed that VA:
targets clear `done` only **AFTER** `cpu_invlpg()` at
`sys/platform/pc64/x86_64/pmap_inval.c:760 -> :764`. When the lagging cpu
resumes it tests `CPUMASK_TESTBIT(info->done, cpu)` at `:696` against the
**NEW** command's mask; if it is not in the new mask it `continue`s and the
**OLD** command's `cpu_invlpg()` is never executed. The prior invalidation
is lost on that cpu.

`info->failed` is **dead** — the audit grepped the file and `pmap.c`:

```
sys/platform/pc64/x86_64/pmap_inval.c:345: info->failed = 1;     (writer, A)
sys/platform/pc64/x86_64/pmap_inval.c:370: info->failed = 0;     (reset)
sys/platform/pc64/x86_64/pmap_inval.c:494: info->failed = 1;     (writer, B)
sys/platform/pc64/x86_64/pmap_inval.c:519: info->failed = 0;     (reset)
sys/platform/pc64/x86_64/pmap_inval.c:786: info->failed = 1;     (writer, C)
```

Five writers, **zero readers**. Callers cannot detect the lost invalidation.

Contrast the sibling originator-quiesce wait **"C"** at `:782-792` which on
timeout does **NOT** abandon the command — it **re-broadcasts** the IPI
(`ATOMIC_CPUMASK_NANDMASK(smp_smurf_mask, info->mask); smp_invlpg(&smp_active_mask);`)
and keeps looping. The "A"/"B" paths cannot do this because their command
slot is about to be reused; the only correct options are (1) panic, or (2)
a per-cpu "flush-all-on-resume" flag. The current code does neither — it
silently loses the invalidation.

`LOOPRECOVER` is **unconditionally `#define`-d in the .c file** (`:67-68`,
`#if 1 /* DEBUGGING */ #define LOOPRECOVER`), so the watchdog is **always
compiled in** to the default kernel regardless of any kernel option.
`pmap_inval_smp` is at `0xffffffff80c18320` and `pmap_inval_smp_cmpset`
at `0xffffffff80c187e0` in the running kernel — confirmed via
`nm /boot/kernel/kernel`.

## Why it cannot be triggered from the guest as an unprivileged user

The 6-vCPU guest has the multi-cpu path enabled, so the bug *applies*. But
to make `loopwdog()` return true, an `Xinvltlb` IPI must remain unacked for
**> 2 s**. Inside the guest:

- `Xinvltlb` is serviced at IPI interrupt level by `pmap_inval_intr`
  (`pmap_inval.c:672`), which is reached via the IPI handler
  (`mp_machdep.c`) at hardware interrupt priority. A userland process
  spinning on a pinned cpu cannot defer this — the instant the vCPU gets
  any host time, the IPI is taken.
- There is no sysctl / ioctl that lets an unprivileged user stall another
  cpu's interrupt servicing for > 2 s.
- `cpuctl(4)`, `cpuset(1)`, and `/dev/cpuctl` let you pin user threads,
  not disable IPI delivery.

The only realistic producers are host-side (oversubscribed / hostile host
starving the vCPU) or an SMI storm on bare metal — neither inside the
guest's unprivileged threat model. This is why the finding's CVSS is
`AV:L/AC:H` and severity Medium.

## Exploit chain

None developed — the primitive is not derivable from inside the guest. The
finding itself acknowledges this (markdown lines 71-83): the attacker
position required is *"an adversary who can cause a target CPU/vCPU to be
descheduled or stalled for >= 2 seconds while a pmap invalidation is
pending against it"*, and explicitly notes *"In a virtualised DragonFly
guest the precondition is realistic and out of the guest's control"*.
This audit's guest is single-tenant and host-controlled; the orchestrator
does not provide a host-side CPU-starvation primitive, so the
demonstration is confined to the static-verification fallback in the
finding markdown (lines 124-132), which `verify.sh` reproduces.

## PoC

`verify.sh` — static-verification script that walks the cited path
end-to-end with `grep`/`sed` against `sys/`, confirming all four static
claims (force-clear in A and B; `info->failed` is dead; target clears done
only after `cpu_invlpg`; sibling C path re-broadcasts). Run from the repo
root: `sh findings/poc/DF-1061/verify.sh`.

## Fix

`fix.diff` — converts the silent abandonment into a diagnostic `panic`.
The finding's recommended alternative (a per-cpu "flush-all-on-next-Xinvltlb"
flag) is a larger change best left to the maintainer. **Supersedes** the
finding markdown's proposal only in line-number accuracy; the substantive
fix matches.

## Reproduce

```
sh findings/poc/DF-1061/verify.sh     # static source verification
```
