DragonFlyBSD Kernel Audit
← triage · dashboard
DF-2572

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2572 · 10 files
FileTypeDescriptionSize
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
README.md readme summary + build/run + verdict
↓ download 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.

VERDICT.md verdict full reachability analysis: dead code, cannot compile, bug absent from maintained netgraph7, root-only device
↓ download raw

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 in ng_device_constructor, line 165). The device entry points find their private data via a direct pointer stored on the cdev at make_dev time (priv->ngddev->si_drv1 = priv, line 190) and dereferenced in ngdread (411), ngdwrite (454), ngdpoll (481), ngdopen (334), ngdclose (351) — priv_p priv = (priv_p)dev->si_drv1;. There is no SLIST_FOREACH traversal to race on (the only SLIST/ngd_softc/ngd_connection tokens in the maintained file are inside #if 0 dead 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_unlock in ngdopen 338/340, ngdclose 354/356, ngdread 423, ng_device_rcvdata 279/284; IF_LOCK/IF_UNLOCK around the queue at 269/278).
  • Correct lifetime ordering. ng_device_disconnect calls destroy_dev(priv->ngddev) (line 299) before kfree(priv,...) (307). destroy_dev blocks until all in-flight cdev operations have drained, so no reader can be inside ngdread holding a stale priv when 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_testable
baseline no→ patch + rebuild →patched clean

not_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'.
↓ fix.diffper-fix-DF-2572

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.