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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4750 Β· 12 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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

fixed
baseline reproduced→ patch + rebuild →patched clean

One-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]'
↓ fix.diffper-fix-DF-4750

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.