# DF-4749 — VERDICT

**Status: REPRODUCED (logic/isolation defect, both directions demonstrated live;
fix validated by patched-module A/B).** Impact: broken jail affinity for nmdm
tty pairs (host↔jail tty-half sharing) + same-jail reopen regression. No
memory-safety impact — severity Low.

## Root cause

`struct nm_softc::pt_prison` (sys/dev/misc/nmdm/nmdm.c:88) is **never
assigned anywhere** — the only reference is the guard in `nmdmopen()`:

- nmdm.c:211-216:
  `} else if (pti->pt_prison != ap->a_cred->cr_prison) { ... return (EBUSY); }`
- `nmdminit()` kmallocs the softc with `M_ZERO` (nmdm.c:126) → `pt_prison`
  stays NULL for the lifetime of the pair.

DragonFly keeps `cr_prison == NULL` for unjailed credentials
(sys/kern/kern_prot.c:1223: `newcr->cr_prison = NULL;`) and only jails set it
(sys/kern/kern_jail.c:154). Consequences:

1. **Unjailed → unjailed**: `NULL != NULL` is false → the guard is inert;
   reopen always allowed (correct by luck).
2. **Jailed opener, half already open** (by *anyone*, incl. the same jail):
   `NULL != &jail` is true → **unconditional EBUSY**. A jail can never share
   an nmdm half between two of its processes — a functional regression vs.
   the FreeBSD pty/nmdm heritage this file descends from ($FreeBSD tag,
   nmdm.c:31), where the first open executes
   `pti->pt_prison = ap->a_cred->cr_prison`.
3. **Unjailed opener, half held open by a jail**: `NULL != &jail`… wait —
   the *opener's* cred is unjailed (`cr_prison == NULL`) so `NULL != NULL`
   is false → **open succeeds**. The intended prison affinity (pair pinned
   to the opening prison, others get EBUSY) never happens in any direction.
   An unprivileged host process can attach to — and share the tty input
   queue of — an nmdm half held by a jailed process, and vice versa when a
   jail's devfs exposes nmdm nodes (the `jail /` permissive-devfs case is
   exactly what the PoC uses).

## Reproduction (all on the live guest, DragonFly 6.5-DEVELOPMENT x86_64)

Baseline module (`kldload nmdm` → `/dev/nmdm0A,B` at 0666, nmdm.c:127-130):

- unpriv unjailed `open#2(/dev/nmdm0A)` → fd 4 (guard inert) — run.log Part A.
- jailed holder holds A; same-jail `open#1` → **EBUSY "Device busy"**
  (run.log Part B / jailtest_baseline.log) — direction 1.
- jailed holder still holding (pid verified); unpriv host user `maxx`
  `open(/dev/nmdm0A)` → **succeeds, fd 3** — direction 2.

## Fix validation (single patched module, both nmdm findings)

Patched `nmdm.c` (fix.diff: pin `pt_prison` + `prison_hold()` on first open,
matching the FreeBSD heritage semantics) built as a module on the guest
(fix_build.log), installed, `kldunload nmdm && kldload nmdm`, re-run:

- DF-4750 post-fix: `TIOCMGET after TIOCCDTR = 0x0005 → DTR=clear [correct]
  RTS=SET [correct]` (pre-fix 0x0002, DTR stuck).
- same-jail reopen post-fix: jailed probe `open#1 = 3 [ok]`, `open#2 = 4`
  (pre-fix: open#1 EBUSY) — direction 1 resolved.
- unpriv unjailed attach post-fix: `open(/dev/nmdm0A) FAILED: Device busy`
  (pre-fix: succeeded) — direction 2 resolved: prison affinity now enforced.

## Threat / impact ceiling

The isolation break requires a jail whose devfs exposes nmdm nodes (not the
default ruleset) plus a host user — hence Low. The functional regression
(jailed processes cannot share an nmdm half at all) is availability-only.
No corruption, no leak: the guard is a pure pointer comparison.

## Honest notes

- The PoC's `jail /` uses `path=/`, i.e. the jail shares the host devfs —
  this is the *permissive* devfs configuration; real jail rulesets must
  explicitly expose nmdm for direction 2 to matter.
- `prison_hold()` in the fix is bounded: nmdm pairs are capped at 128
  (`nmdminit()`: `if (n & ~0x7f) return;`, nmdm.c:123) and never destroyed,
  so at most 128 pin references can exist.

## 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.
