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)
PoC verification
Evidence pack
findings/poc/DF-4749 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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).
- Unprivileged baseline: as any non-root user run
/tmp/nmdm_probe. Expected (buggy kernel):open#2SUCCEEDS β for unjailed users the prison check at sys/dev/misc/nmdm/nmdm.c:211 can never fire becausepti->pt_prisonis NULL and unjailedcr_prisonis NULL too (sys/kern/kern_prot.c:1223). - 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 jailedopen#1FAILS withDevice busyalthough the holder runs in the same jail βNULL != &jailis always true for jailed creds. - 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.
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 withM_ZERO(nmdm.c:126) βpt_prisonstays 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:
- Unjailed β unjailed:
NULL != NULLis false β the guard is inert; reopen always allowed (correct by luck). - Jailed opener, half already open (by anyone, incl. the same jail):
NULL != &jailis 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 executespti->pt_prison = ap->a_cred->cr_prison. - Unjailed opener, half held open by a jail:
NULL != &jailβ¦ wait β the opener's cred is unjailed (cr_prison == NULL) soNULL != NULLis 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 (thejail /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
maxxopen(/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 /usespath=/, 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
fixedfix.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)
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.
No comments yet.