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)
PoC verification
Evidence pack
findings/poc/DF-3533 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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
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
- sys/netgraph/tty/ng_tty.c:225
- sys/netgraph/tty/ng_tty.c:226
- sys/netgraph/tty/ng_tty.c:228
- sys/netgraph/tty/ng_tty.c:230
Evidence
run.log (ttyprobe output: EADDRINUSE first set, discipline unchanged, second set ok, guest alive; syslog 'tty0: node name exists?'), ttyprobe.c, kshold.c
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).
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_testableConfirmed 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.
No comments yet.