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: unlockedoutqrace). The legacy version does NOT have that outq race — it correctly acquirestp->t_tokenbefore queue operations. But it lacks netgraph7'sng_uncalloutcallout-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)atng_tty.c:194to installNETGRAPHDISC). - Trigger: closing the tty (or
TIOCSETDswitch away fromNETGRAPHDISC) 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.
Recommended fix
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
sys/netgraph/tty/ng_tty.c:454-457—callout_resetarm.sys/netgraph/tty/ng_tty.c:470—tp = sc->tpbefore token.sys/netgraph/tty/ng_tty.c:256-278—ngt_closecallout_stop+kfree.sys/kern/kern_timeout.c:1085-1094— callout_stop does not interlock.sys/netgraph7/tty/ng_tty.c:252-257,453-454,464-472— netgraph7'sng_uncalloutfix that legacy lacks.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0637 · 4 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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.
Recommended fix
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
- n
- e
- t
- g
- r
- a
- p
- h
- 7
- /
- t
- t
- y
- /
- n
- g
- _
- t
- t
- y
- .
- c
- :
- 2
- 5
- 3
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.
No comments yet.