nmdm TIOCCDTR uses inverted mask: keeps DTR set and wipes all other modem bits
Summary
TIOCCDTR executes modemsignals &= TIOCM_DTR (missing ~): clearing DTR keeps DTR and zeroes LE/RTS; via the crossover it keeps the peer carrier up so userland DTR hangup never propagates. REPRODUCED live unpriv (TIOCMGET 0x0002 bug vs 0x0005 fixed module A/B). One-char fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4750 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 888 B | β raw | |
| VERDICT.md | β | 2.1 KB | β raw | |
| nmdm_probe.c | β | 3.0 KB | view raw | |
| build.sh | β | 98 B | view raw | |
| run.sh | β | 89 B | view raw | |
| run.log | β | 589 B | view raw | |
| env.txt | β | 926 B | view raw | |
| fix.diff | β | 309 B | view raw | |
| fix_build.log | β | 6.9 KB | view raw | |
| fix_run.log | β | 2.5 KB | view raw | |
| manifest.json | β | 954 B | view raw | |
| verdict.json | β | 2.2 KB | view raw |
DF-4750 β nmdm TIOCCDTR uses inverted mask: keeps DTR, wipes all other modem bits
Build (guest, any user)
cc -O -Wall -o /tmp/nmdm_probe nmdm_probe.c
Run
Root once: kldload nmdm (creates /dev/nmdm0A,B at 0666).
Then as any unprivileged user: /tmp/nmdm_probe.
Expected output (buggy kernel)
TIOCMSET(LE|DTR|RTS) = 0 TIOCMGET after SET = 0x0007 (LE=1 DTR=1 RTS=1) TIOCCDTR = 0 TIOCMGET after TIOCCDTR = 0x0002 -> DTR=SET <-- BUG: must be cleared RTS=clear <-- BUG: must stay set
TIOCCDTR must clear DTR only; instead DTR stays set and every other bit
(LE, RTS, ...) is cleared, because nmdmioctl() line 565 does
ourpart->modemsignals &= TIOCM_DTR; (missing ~).
After fix
TIOCMGET after TIOCCDTR = 0x0005 -> DTR=clear [correct] RTS=SET [correct]
(see fix_run.log β same module A/B as DF-4749).
DF-4750 β VERDICT
Status: REPRODUCED (logic bug in the TIOCCDTR handler; fix validated by patched-module A/B). Impact: functional only (modem-line state machine lies to userland); no memory safety, no privilege boundary. Severity Info.
Root cause
sys/dev/misc/nmdm/nmdm.c:561-566 in nmdmioctl():
case TIOCSDTR:
ourpart->modemsignals |= TIOCM_DTR;
break;
case TIOCCDTR:
ourpart->modemsignals &= TIOCM_DTR; /* line 565: missing ~ */
break;
TIOCCDTR ("clear DTR") ANDs the signal word with the DTR bit instead of
with its complement. Net effect: DTR survives, and every other local modem
bit (TIOCM_LE, TIOCM_RTS, ...) is zeroed. Downstream,
nmdm_crossover() (nmdm.c:606-616) recomputes the peer's CTS/CAR from
RTS/DTR, so the stuck DTR also keeps the peer's carrier (TIOCM_CAR) up β
userland that drops DTR to hang up the link (classic nmdm/pty usage in
termios-compatible programs, e.g. cu/tip-style state machines) never
sees carrier drop on the peer.
Reproduction
Unprivileged user, live guest (run.log):
TIOCMSET(LE|DTR|RTS) -> TIOCMGET = 0x0007 (LE=1 DTR=1 RTS=1) TIOCCDTR -> TIOCMGET = 0x0002 DTR=SET (bug), RTS/LE wiped (bug)
The ioctl marshalling is safe (sys_ioctl pre-copies IOC_IN data to a kernel
buffer, sys/kern/sys_generic.c:679-697), so the direct *(int *)ap->a_data
writes in the handler operate on kernel scratch β the bug is purely the mask.
Fix validation
fix.diff (&= ~TIOCM_DTR) applied together with the DF-4749 fix in one
rebuilt nmdm.ko on the guest (fix_build.log), module swapped, re-run
(fix_run.log):
TIOCMGET after TIOCCDTR = 0x0005 -> DTR=clear [correct] RTS=SET [correct]
Bug gone; the two findings share the same A/B module cycle.
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
fixedOne-character fix applied to guest /usr/src copy, nmdm.ko rebuilt (fix_build.log), swapped via kldunload/kldload, PoC re-run (fix_run.log): TIOCMGET after TIOCCDTR = 0x0005 β DTR cleared, RTS preserved; buggy 0x0002 behavior gone.
fix_run.log: 'TIOCMGET after TIOCCDTR = 0x0005 -> DTR=clear [correct] RTS=SET [correct]'
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log decisive line: 'TIOCMGET after TIOCCDTR = 0x0002 -> DTR=SET <-- BUG ... RTS=clear <-- BUG'; fix_run.log decisive line: 'TIOCMGET after TIOCCDTR = 0x0005 -> DTR=clear [correct] RTS=SET [correct]'
PoC changes
Probe written fresh; shares the nmdm_probe binary with DF-4749 (TIOCM* section exercises this path).
Verified recommended fix
Change nmdm.c:565 to ourpart->modemsignals &= ~TIOCM_DTR;
Verdict
Reproduced live as an unprivileged user on the DragonFly 6.5-DEVELOPMENT guest: after TIOCMSET(LE|DTR|RTS), TIOCCDTR leaves TIOCMGET=0x0002 β DTR (the bit that must be cleared) stays SET while LE and RTS are wiped, because nmdmioctl() line 565 ANDs with TIOCM_DTR instead of its complement. Functional/modem-state bug only (no memory safety: sys_ioctl pre-copies the ioctl data, sys/kern/sys_generic.c:679-697). Fix (&= ~TIOCM_DTR) validated in the same patched-module A/B cycle as DF-4749: post-fix TIOCMGET=0x0005, DTR cleared and RTS preserved.
No comments yet.