Use-after-free / NULL-deref: connection list mutated and traversed with no synchronization
Summary
Single global ngd_softc.head SLIST mutated by newhook(insert) and disconnect(kfree(readq) destroy_dev SLIST_REMOVE) and traversed by rcvdata/ioctl/read/write/poll. No mutex no spl no netgraph serialization. Two threads racing disconnect against any I/O path observe connection whose readq freed or entry unlinked UAF. readq allocated kmalloc M_DEVBUF fixed 10240-byte size attacker can groom slab controlled UAF primitive info leak or corruption.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2572 · 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | concurrent racer: reader thread vs hook create/disconnect to exercise the global SLIST UAF (cannot run - module is dead code) | 4.2 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -pthread -o trigger trigger.c | 183 B | view raw |
| run.sh | run-script | checks for /dev/ngd0; runs trigger if present; documents dead-code status | 1.6 KB | view raw |
| fix.diff | suggested-fix | add struct spinlock ngd_lock to ngd_softc, wrap all SLIST mutation/traversal, fix disconnect drain-before-free ordering (defense-in-depth for orphaned file) | 3.9 KB | view raw |
| VERDICT.md | verdict | full reachability analysis: dead code, cannot compile, bug absent from maintained netgraph7, root-only device | 9.0 KB | ↓ raw |
| README.md | readme | summary + build/run + verdict | 2.4 KB | ↓ raw |
| module_build_failure.txt | build-log | proof the orphaned source fails to compile as a kld module (19+ errors, removed cdevsw API, MAKE_RC=1) | 12.9 KB | view raw |
| env.txt | environment | uname, cc version, nm count, kldload attempts, kldstat, /dev/ngd check, conf/files entry | 726 B | view raw |
| build.log | build-log | trigger build output (success, no warnings) | 78 B | view raw |
| run.log | run-log | trigger run output (/dev/ngd0 does not exist on default kernel) | 647 B | view raw |
DF-2572 — ng_device global SLIST race (UAF / NULL-deref) — PoC
Finding
sys/netgraph/ng_device.c keeps a single global ngd_softc.head SLIST of
ngd_connection that is mutated by ng_device_newhook
(SLIST_INSERT_HEAD, line 309) and ng_device_disconnect
(kfree(readq):407, destroy_dev:409, SLIST_REMOVE:411) and traversed
(unlocked SLIST_FOREACH) by ng_device_rcvdata (:345), ngdioctl (:464),
ngdread (:518), ngdwrite (:572), ngdpoll (:617). No mutex, no spl, no
serialization anywhere in the file. Two threads racing disconnect against a
reader → UAF on the freed readq (10 KiB M_DEVBUF) / NULL-deref / list
corruption.
Verdict: NOT REPRODUCED — unreachable dead code (false positive)
The unsynchronized-list bug is real in the source text, but the cited file
is orphaned dead code:
- not in sys/conf/files (only netgraph7/ng_device.c is, at :1699),
- not in sys/config/X86_64_GENERIC,
- cannot compile (the struct cdevsw/d_*_t/cdevsw_add/make_dev API
it uses was removed from DragonFly; 19+ compile errors),
- not in the running kernel (nm count = 0), no module on disk, kldload
fails, no /dev/ngd* nodes.
The maintained sys/netgraph7/ng_device.c does not have the bug: it has no
global SLIST at all (per-node ngd_private + direct dev->si_drv1 pointer),
uses proper mutexes (mtx_init 171-172, mtx_lock throughout), and gets the
disconnect ordering right (destroy_dev before kfree). Even if the dead file
were live, /dev/ngdN is mode 0600 (root-only) → root→kernel, not an
unprivileged escalation.
This is the same dead-code conclusion the sibling finding DF-2571 reached for the same file.
Reproduce
./build.sh && ./run.sh
On the default kernel: /dev/ngd0 does not exist, ng_device is not loaded,
no effect. (The trigger would race a reader against hook create/disconnect if
the module were built and loaded as root.)
Defense-in-depth fix
fix.diff adds a struct spinlock ngd_lock to ngd_softc, initializes it in
ng_device_init, and wraps every SLIST mutation/traversal; it also corrects
ng_device_disconnect to destroy_dev-before-kfree (matching the maintained
version). The real resolution is to delete the orphaned file
(netgraph7/ng_device.c fully supersedes it). fix_status: not_testable (no
kernel image to build the fix into; git apply --check rc=0).
See VERDICT.md for the full reachability analysis.
DF-2572 — ng_device global SLIST race (UAF / NULL-deref) — VERDICT
Verdict: NOT REPRODUCED (unreachable dead code; impact claim is a false positive)
The unsynchronized global SLIST the finding describes is genuinely present
in the cited source text of sys/netgraph/ng_device.c, but that file is
orphaned dead code that is not compiled into any shipping DragonFlyBSD
kernel or module and cannot even compile against the current kernel
headers. The finding's impact claim — "attacker can groom slab controlled UAF
primitive" — is therefore a false positive: the code is unreachable, and the
maintained equivalent (sys/netgraph7/ng_device.c) has already eliminated the
entire bug class. This is the same dead-code conclusion the sibling finding
DF-2571 reached for the same file.
What the finding claims
sys/netgraph/ng_device.c keeps a single global connection list on
ngd_softc.head (a SLIST_HEAD, line 96) that is mutated by the netgraph
callbacks and traversed by the device entry points with no synchronization:
| Site | Operation | Line |
|---|---|---|
ng_device_newhook |
SLIST_INSERT_HEAD(&sc->head, ...) (insert) |
309 |
ng_device_disconnect |
kfree(connection->readq) then destroy_dev then SLIST_REMOVE (remove + free) |
407, 409, 411 |
ng_device_rcvdata |
SLIST_FOREACH (traverse) |
345 |
ngdioctl |
SLIST_FOREACH (traverse) |
464 |
ngdread |
SLIST_FOREACH + memcpy(connection->readq,...) |
518, 531 |
ngdwrite |
SLIST_FOREACH |
572 |
ngdpoll |
SLIST_FOREACH |
617 |
get_free_unit |
SLIST_EMPTY / SLIST_FOREACH |
237, 247 |
A grep for mtx_/mutex/lock/spl in the file returns zero matches —
there is no lock of any kind. The race the finding describes (thread B in
ngdread mid-SLIST_FOREACH holding a connection pointer while thread A's
ng_device_disconnect kfrees readq at line 407 before destroy_dev
drains at 409) is a real unsynchronized-access bug in the source text, and
would be dangerous IF the code were compiled and reachable. The kfree(readq)
before destroy_dev ordering (line 407 before 409) is itself a classic UAF
window even ignoring the list race.
Why it does NOT reproduce — full trace
(1) The cited file is dead code — no build path
| Build path | Status for sys/netgraph/ng_device.c |
|---|---|
sys/conf/files |
No entry. The only ng_device entry, at conf/files:1699, is netgraph7/ng_device.c optional netgraph7_device — a different file. |
Kernel config sys/config/X86_64_GENERIC |
No netgraph7_device / ng_device option. |
| Module Makefile | There is no sys/netgraph/device/ directory. |
| Compiled into running kernel | No. nm /boot/kernel/kernel.debug \| grep -c ng_device = 0. |
| Loadable module on disk | No. kldload ng_device → "No such file or directory"; kldload netgraph7_device → "No such file or directory". /boot/kernel/ has only netgraph.ko (no ng_device modules at all). |
/dev/ngd* device nodes |
None (ls /dev/ngd* → No such file or directory). |
(2) The cited file cannot even compile
Building sys/netgraph/ng_device.c as a kld module (mirroring the
ng_echo/ng_socket pattern) fails hard — the entire struct cdevsw /
d_*_t / cdevsw_add / make_dev character-device API it targets was removed
from DragonFly years ago. Freshly reproduced for this finding (full output in
module_build_failure.txt, MAKE_RC=1):
ng_device.c:111: error: unknown type name 'd_close_t' ng_device.c:112: error: unknown type name 'd_open_t' ng_device.c:113: error: unknown type name 'd_read_t' ng_device.c:114: error: unknown type name 'd_write_t' ng_device.c:115: error: unknown type name 'd_ioctl_t' ng_device.c:116: error: unknown type name 'd_poll_t' ng_device.c:119: error: variable 'ngd_cdevsw' has initializer but incomplete type ng_device.c:132: error: 'nommap' undeclared here ng_device.c:133: error: 'nostrategy' undeclared here ng_device.c:152: error: implicit declaration of function 'cdevsw_add' ng_device.c:159: error: implicit declaration of function 'cdevsw_remove' ng_device.c:286: error: implicit declaration of function 'make_dev' ng_device.c:419: error: 'ngdopen' redeclared as different kind of symbol ng_device.c:506: error: 'ngdread' redeclared as different kind of symbol ... cc1: all warnings being treated as errors
The file is a relic of the pre-netgraph7 era (FreeBSD 1.1.2.1, 2002) and has not tracked the kernel API.
(3) The maintained version does NOT have the bug
sys/netgraph7/ng_device.c — the file actually referenced in conf/files (as
optional netgraph7_device) — has a completely different architecture that
eliminates the entire bug class:
- No global SLIST. Connections are not kept in a list at all. Each netgraph
node has its own
struct ngd_private(allocated per-node inng_device_constructor, line 165). The device entry points find their private data via a direct pointer stored on the cdev atmake_devtime (priv->ngddev->si_drv1 = priv, line 190) and dereferenced inngdread(411),ngdwrite(454),ngdpoll(481),ngdopen(334),ngdclose(351) —priv_p priv = (priv_p)dev->si_drv1;. There is noSLIST_FOREACHtraversal to race on (the onlySLIST/ngd_softc/ngd_connectiontokens in the maintained file are inside#if 0dead ioctl code at lines 374-376). - Proper locking. It initializes a node mutex and a queue mutex
(
mtx_init(&priv->ngd_mtx...)line 171;mtx_init(&priv->readq.ifq_mtx...)line 172) and takes them in every entry point (mtx_lock/mtx_unlockinngdopen338/340,ngdclose354/356,ngdread423,ng_device_rcvdata279/284;IF_LOCK/IF_UNLOCKaround the queue at 269/278). - Correct lifetime ordering.
ng_device_disconnectcallsdestroy_dev(priv->ngddev)(line 299) beforekfree(priv,...)(307).destroy_devblocks until all in-flight cdev operations have drained, so no reader can be insidengdreadholding a staleprivwhen it is freed. This is exactly the ordering the dead file gets backwards (kfree-readq at 407 before destroy_dev at 409).
So the DF-2572 bug pattern is absent from the maintained equivalent on every axis (no shared list, mutexes present, drain-before-free ordering correct).
(4) Even if reachable, the device is root-only
/dev/ngdN is created by make_dev(&ngd_cdevsw, unit, 0, 0, 0600, "ngd%d",
unit) (sys/netgraph/ng_device.c:286-287). Mode 0600 means only root
can open() it. So even on a hypothetical kernel where this code were live,
the bug is a root→kernel issue, not an unprivileged→kernel escalation.
Per the Phase-6 hard-blocker list, a write/UAF reachable only from an already-
root context is game-over by definition — there is no privilege boundary to
cross. (The maintained netgraph7/ng_device.c likewise creates the device
mode 0600, lines 179-180.)
Conclusion — which step-4 category
(d) Genuinely not reachable on this kernel (primary) + partial (a) false
positive (the "groomable slab UAF" impact claim is wrong on every axis):
- The vulnerable file is not in any build path (conf/files references only
netgraph7/ng_device.c at :1699; absent from X86_64_GENERIC).
- It cannot compile against the current kernel API (19+ errors, removed cdevsw).
- It is not in the running kernel (nm count = 0) and no module exists on disk.
- The maintained equivalent has no global SLIST, uses proper mutexes, and gets
the drain-before-free ordering right — the bug class is eliminated there.
- The character device is mode 0600 (root-only) in both the dead and maintained
versions, so even a live instance would be root→kernel, not unprivileged.
Because the cited code cannot run, there is no memory corruption to
reproduce and therefore no Phase-6 escalation chain to develop and no
Phase-8 patched-kernel build to validate (there is no live kernel image into
which fix.diff can be built to exercise the path). The unsynchronized-list
defect is a real code-quality / defense-in-depth issue in the orphaned source
text, so fix.diff is provided: it adds a struct spinlock to ngd_softc,
initializes it, and wraps every SLIST mutation/traversal (and also corrects
the disconnect ordering to destroy_dev-before-kfree, matching the
maintained version). The real resolution, however, is to delete the orphaned
file (it is fully superseded by netgraph7/ng_device.c).
Fix validation status
not_testable — the cited file (sys/netgraph/ng_device.c) is not compiled
into the default kernel and cannot be built as a module (the cdevsw/make_dev
API it uses was removed). There is therefore no kernel image into which
fix.diff can be built to exercise the path. fix.diff is verified to apply
cleanly (git apply --check rc=0) and is a correct minimal change: it adds a
spinlock protecting the global SLIST plus corrects the disconnect
drain-before-free ordering, matching the design already used in the fixed
netgraph7/ng_device.c.
Fix verification
not_testablenot_testable: cited file is orphaned dead code not in conf/files, not in X86_64_GENERIC, cannot compile (19+ errors MAKE_RC=1). No kernel image into which fix.diff can be built. fix.diff verified to apply cleanly (git apply --check rc=0) and is a correct minimal change matching already-correct netgraph7 design. No patched kernel built because bug is unreachable dead code.
n/a (not_reproduced dead-code; no baseline bad-behavior on any kernel because file never compiled). Compile proof: module_build_failure.txt MAKE_RC=1 'unknown type d_close_t'/'implicit make_dev'/'cc1 all warnings as errors'.
Confirmed kernel references
Detail
Exploit chain
none — cited code is unreachable dead code that cannot compile, no live primitive, no privilege boundary to cross (device root-only regardless). trigger.c documents the concurrent racer that WOULD exercise the SLIST UAF if the module were built/loaded as root, but /dev/ngd0 never exists.
Evidence (decisive lines)
run.log: /dev/ngd0 does NOT exist; orphaned dead code (not in conf/files; not in X86_64_GENERIC; cannot compile removed cdevsw/make_dev API; not in running kernel nm count=0). env: nm count=0; kldload ng_device -> No such file rc=1; kldload netgraph7_device -> rc=1; kldstat netgraph=none; no /dev/ngd*. module_build_failure.txt: ng_device.c:111 unknown type d_close_t / :286 implicit make_dev / cc1 all warnings as errors MAKE_RC=1.
PoC changes
PoC dir empty; authored from scratch: trigger.c (concurrent racer documenting SLIST UAF race), build.sh (cc -pthread), run.sh (checks /dev/ngd0, documents dead-code), VERDICT.md (reachability analysis), README.md, fix.diff (defense-in-depth: spinlock ngd_lock + wrap all 8 SLIST sites + reorder disconnect destroy_dev-before-kfree matching netgraph7 + fixes DF-2574 leak), manifest.json, env.txt, module_build_failure.txt.
Verified recommended fix
fix.diff adds struct spinlock ngd_lock to ngd_softc (:96), initializes it in ng_device_init, wraps every SLIST mutation (newhook :309, disconnect :411) and traversal (rcvdata/ioctl/read/write/poll/get_free_unit), and reorders ng_device_disconnect to SLIST_REMOVE-under-lock then destroy_dev BEFORE kfree(readq)+kfree(connection) (matching netgraph7 drain-before-free, fixing DF-2574 leak). BUT the real resolution is to DELETE the orphaned file entirely (netgraph7/ng_device.c fully supersedes it). git apply --check rc=0.
Verdict
NOT REPRODUCED (unreachable dead code; impact claim is a false positive). The unsynchronized global ngd_softc.head SLIST IS genuinely present in the source text of sys/netgraph/ng_device.c (mutated by newhook SLIST_INSERT_HEAD:309 + disconnect kfree(readq):407/destroy_dev:409/SLIST_REMOVE:411; traversed unlocked by rcvdata:345, ioctl:464, read:518, write:572, poll:617; zero mtx_/lock/spl). But the file is ORPHANED DEAD CODE: no entry in sys/conf/files (only netgraph7/ng_device.c is, at conf/files:1699), absent from X86_64_GENERIC, not in running kernel (nm count=0), no module on disk, kldload ng_device AND netgraph7_device both fail, no /dev/ngd nodes. Building it as kld FAILS HARD (19+ errors, MAKE_RC=1) because the entire struct cdevsw/d__t/cdevsw_add/make_dev API was removed. Maintained sys/netgraph7/ng_device.c does NOT have the bug (no global SLIST; per-node priv via dev->si_drv1; proper mutexes; correct destroy_dev-before-kfree ordering). Even if live, /dev/ngdN is make_dev 0600 root-only. Matches sibling DF-2571's dead-code conclusion.
No comments yet.