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

nmdm prison guard never initialized: pt_prison never assigned β€” jail affinity unenforced, jailed reopen degenerates to unconditional EBUSY

Summary

pt_prison is M_ZERO'd and never assigned; its only use is the open guard != cr_prison -> EBUSY. Unjailed reopen always allowed (guard inert), jailed openers get unconditional EBUSY (NULL != jail - cannot reopen their own half), and no pair-to-prison pinning: unpriv host user opened a half held by a jail (reproduced live). Devices 0666. REPRODUCED live as unpriv user + fix validated on rebuilt nmdm.ko (same-jail reopen allowed, cross-prison EBUSY). Pin-on-first-open fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4749 Β· 14 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 4.1 KB ↓ raw
nmdm_probe.c β€” 3.0 KB view raw
holder.c β€” 388 B view raw
build.sh β€” 131 B view raw
run.sh β€” 89 B view raw
run.log β€” 1.4 KB view raw
jailtest_baseline.log β€” 447 B view raw
env.txt β€” 926 B view raw
fix.diff β€” 638 B view raw
fix_build.log β€” 6.9 KB view raw
fix_run.log β€” 2.5 KB view raw
manifest.json β€” 1.2 KB view raw
verdict.json β€” 3.1 KB view raw

DF-4749 β€” nmdm prison guard never initialized (pt_prison never assigned)

Build (on the guest, any user)

cc -O -Wall -o /tmp/nmdm_probe nmdm_probe.c     # probe: open-matrix + TIOCM* checks
cc -O -o /tmp/holder holder.c                   # fd holder used for the jail demo

Run

Root prerequisites (once): kldload nmdm (creates /dev/nmdm0A,B at 0666).

  1. Unprivileged baseline: as any non-root user run /tmp/nmdm_probe. Expected (buggy kernel): open#2 SUCCEEDS β€” for unjailed users the prison check at sys/dev/misc/nmdm/nmdm.c:211 can never fire because pti->pt_prison is NULL and unjailed cr_prison is NULL too (sys/kern/kern_prot.c:1223).
  2. Jail demo (as root): sh jail / nmdmj 127.0.0.1 /bin/sh -c \ '/tmp/holder /dev/nmdm0A >/tmp/h.log 2>&1 & sleep 1; /tmp/nmdm_probe' Expected (buggy kernel): the jailed open#1 FAILS with Device busy although the holder runs in the same jail β€” NULL != &jail is always true for jailed creds.
  3. While the jailed holder still sleeps, as an unprivileged host user: /tmp/holder /dev/nmdm0A β€” Expected (buggy kernel): open SUCCEEDS; the host user now shares a tty half pinned (by design intent) to the jail.

Success criterion

The bug is proven when: (a) same-jail reopen returns EBUSY, and (b) unjailed attach to a jail-held half succeeds. After the fix both invert (same-jail reopen allowed; cross-prison attach EBUSY) β€” see fix_run.log.

Full logs

run.log (pre-fix), jailtest_baseline.log (raw jail run), fix_build.log + fix_run.log (patched-module A/B), env.txt.

VERDICT.md
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.diff applied to guest /usr/src copy, nmdm.ko rebuilt (fix_build.log), module swapped via kldunload/kldload, PoC re-run (fix_run.log): pre-fix buggy behaviors (same-jail EBUSY on open#1; unpriv host attach succeeding) are gone β€” post-fix same-jail open#1/#2 succeed and cross-prison attach returns 'Device busy'.

fix_run.log lines: 'open#1(/dev/nmdm0A) = 3 [ok, TS_ISOPEN set]' (inside jail, holder holding) and 'holder: open(/dev/nmdm0A) FAILED: Device busy' (as maxx, jail-held half)
↓ fix.diffper-fix-DF-4749

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (pre-fix both directions: Part A inert guard, Part B same-jail EBUSY + unpriv host attach ok); jailtest_baseline.log (raw run, holder pid verified); fix_run.log (post-fix inversion, decisive lines 'open#1... = 3 [ok]' inside jail and 'holder: open(/dev/nmdm0A) FAILED: Device busy' for maxx); fix_build.log (patched module build)

PoC changes

Probe written fresh (open-matrix + holder); jailed side uses legacy jail(8) syntax jail / name 127.0.0.1 cmd since the guest jail(8) has no -c support; devfs note: jail path=/ shares host devfs.

Verified recommended fix

Assign pt_prison (with prison_hold) on first open of a half in nmdmopen(), restoring the FreeBSD pty-heritage prison pinning.

Verdict

Reproduced live on the DragonFly 6.5-DEVELOPMENT guest: pt_prison is never assigned (kmalloc M_ZERO, no writer anywhere; sole reference is the guard at nmdm.c:211), so the nmdm prison check is inert for unjailed users, degenerates to unconditional EBUSY-on-reopen for jailed users (same-jail reopen of /dev/nmdm0A failed 'Device busy' while the jail itself held it), and never enforces pair<->prison affinity (unprivileged host user maxx successfully opened the jail-held half). Not memory-safety: pure logic/isolation defect; impact capped Low because direction 2 needs a jail devfs that exposes nmdm. Fix (pin pt_prison + prison_hold on first open) validated by rebuilding nmdm.ko on the guest and A/B-ing: post-fix same-jail reopen succeeds and cross-prison attach returns EBUSY.