# DF-0589 — VERDICT

## Verdict: RACE CONFIRMED (code-level); NOT TRIGGERABLE ON THIS GUEST (sio-console-gated)

The race condition described in the finding is **real at the code level** — all
the structural preconditions hold. However, triggering it live requires a real
serial port with the **sio** driver (NOT a pty), and the only sio port on this
guest is the kernel console (`sio0`), which cannot be freed for `BTUARTDISC`.
The race is also gated behind `SYSCAP_NONET_NETGRAPH` (not a default
unprivileged capability) and requires the netgraph7 module stack to be loaded
by root (netgraph7 is compiled OUT of the default `X86_64_GENERIC` kernel).

---

## What was confirmed

### 1. NG_H4_LOCK is per-CPU only (the root cause)

`sys/netgraph7/bluetooth/drivers/h4/ng_h4_var.h:88-89`:
```c
#define NG_H4_LOCK(sc)    crit_enter();
#define NG_H4_UNLOCK(sc)  crit_exit();
```
DragonFly `crit_enter(9)` blocks preemption and defers IPIs on the **current
CPU only** — it provides **zero cross-CPU exclusion**. Two CPUs can both be
inside `NG_H4_LOCK` simultaneously.

### 2. ng_h4_start does IF_DEQUEUE without NG_H4_LOCK

`sys/netgraph7/bluetooth/drivers/h4/ng_h4.c:572-648`:
- Line 579: `lwkt_gettoken(&tp->t_token);` — acquires the tty token only.
- Line 592: `IF_DEQUEUE(&sc->outq, m);` — **no NG_H4_LOCK held**.
- Line 615: `IF_PREPEND(&sc->outq, m);` — **no NG_H4_LOCK held**.
- Lines 601/620/638: NG_H4_LOCK is acquired only briefly for stat counter updates.

### 3. ng_h4_disconnect / rcvmsg / shutdown do IF_DRAIN with crit_enter only

- `ng_h4_disconnect` (line 729-743): `NG_H4_LOCK` (crit_enter) → `IF_DRAIN` (line 735) → unlock. Does NOT acquire `tp->t_token`.
- `ng_h4_rcvmsg` NGM_H4_NODE_RESET (line 886): `IF_DRAIN` inside `NG_H4_LOCK`.
- `ng_h4_shutdown` (line 773): `IF_DRAIN` **without even** `NG_H4_LOCK`.

### 4. The ONLY concurrent execution path: sio siopoll → l_start

The finding claims `ng_h4_start` runs from the tty `l_start` line-discipline
callback concurrently with netgraph methods. **This is correct — but with a
critical DragonFly-specific nuance:**

Exhaustive search of the entire `sys/` tree reveals that `linesw[].l_start` is
called from **exactly one place** in the kernel:

```
sys/dev/serial/sio/sio.c:2241    (*linesw[tp->t_line].l_start)(tp);
```

This is inside `siopoll()` (line 2176), which is registered as a **Software
Interrupt (SWI)** handler:
```
sys/dev/serial/sio/sio.c:1209    register_swi_mp(SWI_TTY, siopoll, ...);
```

The general tty layer's `ttstart()` (sys/kern/tty.c:1549) does NOT call
`l_start` — it only calls `tp->t_oproc`. **On DragonFly, pty-based triggering
does not work** because `l_start` is never invoked for pty devices.

So the race fires ONLY on systems with a **real sio serial port** where:
1. `siopoll` (SWI context, CPU A) calls `l_start` → `ng_h4_start` → `IF_DEQUEUE`
2. The netgraph writer thread (CPU B) processes `ng_h4_disconnect`/`RESET` → `IF_DRAIN`
3. Both operate on `sc->outq` with no common cross-CPU lock → double-free / UAF

### 5. FORCE_WRITER serializes netgraph-internal calls

`NG_NODE_FORCE_WRITER(sc->node)` at line 215 ensures all **netgraph methods**
(rcvdata, rcvmsg, disconnect, shutdown) are serialized. The callout path
(`ng_h4_process_timeout` → `ng_h4_start`) also goes through `ng_callout` which
sets `NGQF_WRITER` (sys/netgraph7/netgraph/ng_base.c:~3260), so it is also
serialized. **Only the sio siopoll → l_start path bypasses FORCE_WRITER.**

---

## Why it was NOT triggered live on this guest

1. **pty doesn't call l_start**: The DragonFly tty layer's `ttstart()` calls
   `tp->t_oproc`, not `linesw[].l_start`. A pty-based PoC cannot exercise the
   race. (Confirmed: 80 seconds × 4M RESET + 4M data packets on pty → no panic.)

2. **sio0 is the console**: The guest has one sio port (`sio0` at 0x3f8, IRQ4)
   which is the kernel console. Opening it for `BTUARTDISC` returns `EBUSY`
   because the console subsystem holds it. Getty was killed but the kernel
   console driver retains the reference.

3. **Netgraph7 compiled out of GENERIC**: `NNETGRAPH7_BLUETOOTH_H4 = 0` in the
   default kernel config. Requires root `kldload` of netgraph7 core + ng_socket
   + ng_h4 modules.

4. **Capability gate**: `ng_h4_open` (line 157) requires
   `SYSCAP_NONET_NETGRAPH` — not a default unprivileged capability. Verified:
   `maxx` (uid 1001) gets `EPERM` on `TIOCSETD BTUARTDISC`.

---

## Impact assessment

| Factor | Assessment |
|--------|-----------|
| Race real? | **YES** — confirmed by exhaustive source analysis |
| Triggerable via pty? | **NO** — DragonFly tty layer doesn't call l_start for ptys |
| Triggerable via sio? | **YES** — siopoll SWI calls l_start; requires real serial port |
| Reachable unprivileged? | **NO** — needs SYSCAP_NONET_NETGRAPH + netgraph7 loaded (root) |
| Default kernel? | **NO** — netgraph7 compiled out of X86_64_GENERIC |
| Exploitation chain? | DoS/panic via mbuf double-free (INVARIANTS catches it) |
| Privilege escalation? | **NOT assessed** — not reachable from unprivileged on default kernel |
| Severity | **Medium** is appropriate (HW + capability + module-loading gated) |

---

## Exploit chain (Phase 6 assessment)

This is a memory-corruption class (potential mbuf double-free / UAF), so Phase 6
escalation assessment is required. However:

**Valid hard blocker — root-only reachability**: The bug's write primitive
(IF_DEQUEUE / IF_DRAIN on a shared mbuf queue) is reachable ONLY when:
1. An admin has loaded the netgraph7 module stack (root `kldload`)
2. The user holds `SYSCAP_NONET_NETGRAPH` (delegated capability)
3. A real sio serial port is available and not the console

On the **default GENERIC kernel**, netgraph7 is not compiled in and the code
path is dead. There is **no unprivileged path** to the write without root module
loading. This meets the Phase 6 valid hard blocker criterion: *"The write is
reachable only from an already-root context (kldload / wheel-only ioctl)"*.

Even with root-loaded modules + delegated capability, the race is narrow
(2-3 instruction window in IF_DEQUEUE vs IF_DRAIN) and the realistic outcome is
a DoS panic (INVARIANTS mbuf double-free detection), not reliable code execution.
The speculative m_ext.ext_free hijack chain described in the finding is unverified.

**No uid0 escalation was attempted** because the bug is not reachable from an
unprivileged user on the default kernel. Documented as a root→kernel hardening
gap with DoS impact.

---

## PoC changes

Rewrote `race.c` from the non-compiling sketch into a working harness that:
- Constructs netgraph7-format `ng_mesg` manually (NG_VERSION=8, 32-bit arglen)
  since the system's libnetgraph/ngctl use the incompatible old-netgraph ABI
- Creates pty + BTUARTDISC + ng socket node + hook connection correctly
- Races data flooding + NGM_H4_NODE_RESET (IF_DRAIN) for 60-80 seconds
- Also created `race_sio.c` variant targeting real sio ports (not usable on this
  guest due to console conflict, but correct for systems with a free serial port)

The pty-based PoC confirmed 4M+ RESET + 4M+ data iterations with no panic,
which is **expected** — the pty path doesn't exercise l_start. The PoC is kept
as a functional harness; on a system with a free sio port and the right
capabilities, it would exercise the actual race via `race_sio`.

---

## Recommended fix

Upgrade `NG_H4_LOCK` from `crit_enter/crit_exit` to a real spinlock, and wrap
the `IF_DEQUEUE`/`IF_PREPEND` in `ng_h4_start` with it. This matches the
finding's recommendation. See `fix.diff`.

The same pattern (crit_enter as the only outq lock, unprotected IF_DEQUEUE in
the l_start method) also exists in `sys/netgraph7/tty/ng_tty.c` and should be
fixed there as well (flagged for the maintainer, out of scope for this finding).

---

## Fix validation

The fix (`fix.diff`) was applied to the in-guest source and the ng_h4.ko module
was rebuilt successfully. All three netgraph7 modules (netgraph core, ng_socket,
ng_h4) loaded correctly. The PoC ran for 30 seconds (1.37M data packets + 1.36M
RESET messages) on the fixed module with no panic and the guest stayed up.

**Note**: Since the pty-based PoC cannot exercise the actual race path (l_start
is only called by siopoll for real serial ports), this validates that the fix
**compiles, loads, and does not introduce regressions** — but cannot directly
demonstrate the fix preventing the race (which requires a free sio serial port).

`fix_status: not_testable` — the race cannot be triggered on this guest (no
free sio port, pty doesn't call l_start), so the fix's effectiveness against
the specific race cannot be verified live. The fix is compile-validated,
load-validated, and correct by code inspection.

---

## Unrelated panic observed

During heavy PoC operations (1M+ ng_socket control messages), an unrelated
panic occurred in the **socket receive path** (NOT in ng_h4):
```
panic: memory chunk 0xfffff8004f0a407f is already allocated!
chunk_mark_allocated → _kmalloc → dup_sockaddr → soreceive → kern_recvmsg
```
This is a separate issue (likely a double-allocation or UAF in the ng_socket/
netgraph7 message handling under heavy load), not the DF-0589 outq race.
Documented in `panic.txt` for reference.
