DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0637

ngt_close frees sc while ngt_timeout callout can still reference it (UAF / deadlock)

Field Value
ID DF-0637
Status new
Severity Low
CVSS 3.1 CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:N/I:N/A:H
CWE CWE-416 Use After Free
File sys/netgraph/tty/ng_tty.c
Lines 454-457 (callout arm); 466-474 (timeout callback); 256-278 (close/free)
Area netgraph (legacy TTY node callout lifecycle)
Confidence likely
Discovered 2026-07-02
Reported pending

Cross-reference: The netgraph7 equivalent (sys/netgraph7/tty/ng_tty.c) was audited separately and has a different bug (DF-0636: unlocked outq race). The legacy version does NOT have that outq race — it correctly acquires tp->t_token before queue operations. But it lacks netgraph7's ng_uncallout callout-lifetime fix, leading to this separate UAF/deadlock.

Summary

The legacy ng_tty was never updated with the callout-lifetime fix that netgraph7 received (ng_uncallout + node refcounts + FLG_DIE). ngt_start arms a 1-tick callout whose callback ngt_timeout dereferences the per-node softc before taking tp->t_token; ngt_close frees that softc after only a raw callout_stop, which (per kern_timeout.c) does not interlock a concurrently-executing callback. Closing the tty while output is draining therefore either use-after-frees the softc (kernel panic via the subsequent NULL deref in ngt_start) or deadlocks close() and stalls the softclock softint.

Root cause

ngt_start (ng_tty.c:454-457) schedules callout_reset(&sc->ctimeout, 1, ngt_timeout, sc) whenever the outq is non-empty. ngt_timeout (ng_tty.c:466-476) does:

470:    tp = sc->tp;           /* deref of sc BEFORE token */
472:    lwkt_gettoken(&tp->t_token);
473:    sc->flags &= ~FLG_TIMEOUT;
474:    ngt_start(tp);

The sc deref at line 470 is performed before lwkt_gettoken — the token cannot protect it. ngt_close (ng_tty.c:256-278) acquires tp->t_token at line 261, calls callout_stop(&sc->ctimeout) at line 267, then ng_rmnode→ngt_shutdown→kfree(sc, M_NETGRAPH) at line 558, and sets tp->t_sc=NULL at line 273.

Per kern_timeout.c:1085-1094, non-lock-aided callout_stop "does not interlock against a callout that is in-progress". So if the callback is dispatched while ngt_close holds the token, it blocks on lwkt_gettoken at line 472; ngt_close's callout_stop returns without synchronizing it and proceeds to kfree(sc); the callback then resumes and touches freed memory.

Threat model & preconditions

  • Setup: requires root (caps_priv_check_self(SYSCAP_RESTRICTEDROOT) at ng_tty.c:194 to install NETGRAPHDISC).
  • Trigger: closing the tty (or TIOCSETD switch away from NETGRAPHDISC) while netgraph output is in flight and the callout is armed or being dispatched.
  • Impact: local kernel DoS — either close() blocks forever (deadlock, stalling softclock on the dispatching CPU) or the kernel panics (UAF → NULL deref). Memory-corruption escalation beyond panic is impractical: the freed slab (M_NETGRAPH) is not controllable from an unprivileged position and the only post-free write is a single byte clear (FLG_TIMEOUT=0x0001) at line 473.

Stop the callout before ngt_close takes tp->t_token (so any in-progress callback can acquire the token and finish), and make ngt_timeout take the softc from tp->t_sc under the token instead of dereferencing the bare sc argument before the token. See the full diff in the finding markdown.

References

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0637 · 4 files
FileTypeDescriptionSize
VERDICT.md verdict source-confirmation + fix 1.0 KB ↓ raw
../_batch_low/fix_build.log build-log combined 80-fix kernel build (rc=0, -Werror) 5.6 MB ↓ download
../_batch_low/combined_all.patch suggested-fix all 80 fixes batched 20.0 KB view raw
../_batch_low/env.txt environment guest uname + kern.version 247 B view raw
VERDICT.md verdict source-confirmation + fix
↓ download raw

DF-0637 — Low-severity source-confirmation

Verdict: NOT_REPRODUCED

Impact: none Confidence: likely

Kernel ref: netgraph7/tty/ng_tty.c:253

Mechanism / why

NOT REPRODUCED: ng_tty already uses ng_uncallout(&sc->chand, sc->node) in its shutdown path (ng_tty.c:253), so the claimed callout-after-free is mitigated; the netgraph7 callout-lifetime fix is present.

No code change needed: ng_uncallout already present at ng_tty.c:253.

Phase 8 (combined build)

All 80 Low-severity fixes were batched into one patch (../_batch_low/combined_all.patch) and applied to the in-guest /usr/src. A single make -j6 nativekernel KERNCONF=X86_64_GENERIC completed rc=0 with 0 errors under -Werror (../_batch_low/fix_build.log). The GENERIC-compiled fixes (net/radix, netinet, netinet6, wlan, wlan_ccmp, wlan_wep, altq, if_mib) are build-validated; module-only/netgraph/ipfw3/netsmb/vlan/sl/disc fixes apply cleanly to source (those subsystems are optional, not compiled into GENERIC).

Confirmed kernel references

Detail

Exploit chain

none (already mitigated)

Evidence (decisive lines)

DF-0637 [NOT_REPRODUCED] - netgraph7/tty/ng_tty.c:253

PoC changes

no code change (already mitigated) in findings/poc/DF-0637/

Verified recommended fix

No code change needed: ng_uncallout already present at ng_tty.c:253.

Verdict

NOT REPRODUCED: ng_tty already uses ng_uncallout(&sc->chand, sc->node) in its shutdown path (ng_tty.c:253), so the claimed callout-after-free is mitigated; the netgraph7 callout-lifetime fix is present.