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

v1 ng_tty: ngt_open() error path leaves tp->t_sc dangling after synchronously freeing sc

Summary

v1 ngt_open installs the back-pointers before naming the node (tp->t_sc = sc at :225), then on ng_name_node() failure runs ng_rmnode which synchronously executes ngt_shutdown (bzero+kfree of sc) without clearing tp->t_sc - the tty structure keeps a pointer to freed memory. Downgraded to Low/latent: the TIOCSETD error path leaves t_line on the old discipline and re-opens it, so no linesw[NETGRAPHDISC] dispatch (ngt_close/ngt_input/ngt_tioctl are all t_line-gated) and the tty core never reads t_sc (verified) - the stale pointer is only overwritten by a later successful TIOCSETD or destroyed with the tty. A landmine for any future dispatch or t_sc-consuming change. Also: DF-0636 does NOT apply to v1 (v1 ngt_rcvdata takes tp->t_token before any queue op at :578); no ng_callout machinery so no DF-3523 leak twin. [VERIFIED pass2: reproduced latent on stock module - colliding node name forces ngt_open EADDRINUSE error path with sc synchronously freed while tp t_sc keeps it, no dispatch no crash exactly as filed]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3533 Β· 11 files
FileTypeDescriptionSize
README.md β€” 557 B ↓ raw
VERDICT.md β€” 2.1 KB ↓ raw
build.sh β€” 58 B view raw
env.txt β€” 309 B view raw
fix.diff β€” 329 B view raw
kshold.c β€” 2.7 KB view raw
manifest.json β€” 773 B view raw
run.log β€” 446 B view raw
run.sh β€” 84 B view raw
ttyprobe.c β€” 2.3 KB view raw
verdict.json β€” 2.4 KB view raw

DF-3533 - v1 ng_tty failed-open dangling t_sc (latent)

gcc -O -o ttyprobe ttyprobe.c; ./kshold e1 e0 echo tty0 & ./ttyprobe Observed: TIOCSETD #1 -> -1/errno 48 (EADDRINUSE), syslog 'tty0: node name exists?', discipline unchanged, guest alive; second TIOCSETD + restore clean. Error path runs ng_rmnode->ngt_shutdown which kfrees sc while tp->t_sc keeps it (code-certain, single-threaded); never dispatched (latent as filed).

Kernel: DragonFly dfbsd 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 root@dfbsd:/usr/obj/usr/src/sys/X86_64_GENERIC x86_64

VERDICT.md
↓ download raw

DF-3533 - VERDICT

status: reproduced | reproduced: 1 | impact: none | confidence: certain

The dangling-pointer condition is reproduced mechanically and confirmed LATENT exactly as filed (downgraded Low). ttyprobe.c pre-created a netgraph node named 'tty0' (echo node via generic NGM_MKPEER+NGM_NAME, held open), then issued TIOCSETD NETGRAPHDISC(6) on a pty slave: v1 ngt_open (ng_tty.c:200-231) installs tp->t_sc = sc at :225 BEFORE naming the node, and the pre-created 'tty0' forces ng_name_node to fail - ioctl returned -1 errno=48 (EADDRINUSE) and the kernel log printed 'tty0: node name exists?' (ng_tty.c:227), proving the error branch executed. That branch runs ng_rmnode(sc->node) -> ngt_shutdown, which bzero+kfree's sc synchronously WITHOUT clearing tp->t_sc (verified: no t_sc assignment on that path; the free is single-threaded in the caller, so the dangling store is certain). Guest stayed healthy; TIOCGETD showed the discipline unchanged (0) and a second TIOCSETD plus a restore completed cleanly - the tty core never dispatches through the stale pointer (all ngt_* entry points are t_line-gated), matching the finding's own latent-landmine assessment. No user-visible manifestation by construction; the stale pointer is only overwritten by a later successful TIOCSETD or destroyed with the tty.

Kernel references

Evidence

run.log (ttyprobe output: EADDRINUSE first set, discipline unchanged, second set ok, guest alive; syslog 'tty0: node name exists?'), ttyprobe.c, kshold.c

in the ng_name_node failure branch set tp->t_sc = NULL before/after ng_rmnode (order the rmnode after clearing the back-pointer).

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

not_testable
↓ fix.diffper-fix-DF-3533

Confirmed kernel references

Detail

Evidence (decisive lines)

run.log (ttyprobe output: EADDRINUSE first set, discipline unchanged, second set ok, guest alive; syslog 'tty0: node name exists?'), ttyprobe.c, kshold.c

PoC changes

written fresh (pty via /dev/ptmx + ptsname; NETGRAPHDISC=6 from sys/ttycom.h)

Verified recommended fix

in the ng_name_node failure branch set tp->t_sc = NULL before/after ng_rmnode (order the rmnode after clearing the back-pointer).

Verdict

The dangling-pointer condition is reproduced mechanically and confirmed LATENT exactly as filed (downgraded Low). ttyprobe.c pre-created a netgraph node named 'tty0' (echo node via generic NGM_MKPEER+NGM_NAME, held open), then issued TIOCSETD NETGRAPHDISC(6) on a pty slave: v1 ngt_open (ng_tty.c:200-231) installs tp->t_sc = sc at :225 BEFORE naming the node, and the pre-created 'tty0' forces ng_name_node to fail - ioctl returned -1 errno=48 (EADDRINUSE) and the kernel log printed 'tty0: node name exists?' (ng_tty.c:227), proving the error branch executed. That branch runs ng_rmnode(sc->node) -> ngt_shutdown, which bzero+kfree's sc synchronously WITHOUT clearing tp->t_sc (verified: no t_sc assignment on that path; the free is single-threaded in the caller, so the dangling store is certain). Guest stayed healthy; TIOCGETD showed the discipline unchanged (0) and a second TIOCSETD plus a restore completed cleanly - the tty core never dispatches through the stale pointer (all ngt_* entry points are t_line-gated), matching the finding's own latent-landmine assessment. No user-visible manifestation by construction; the stale pointer is only overwritten by a later successful TIOCSETD or destroyed with the tty.