Use-after-free of struct ngd_private in all cdevsw methods: disconnect frees priv immediately after DragonFly's asynchronous destroy_dev()
Summary
ng_device_disconnect() calls destroy_dev() which in DragonFly is purely asynchronous (kern_conf.c:345-354 -> devfs fire-and-forget; no draining, no ops revocation, si_drv1 never cleared), then immediately IF_DRAIN+kfree(priv). Every cdevsw method fetches priv from dev->si_drv1 and an open fd keeps dispatching through it after destroy: ngdopen/ngdclose do a UAF WRITE (priv->flags |= NGDF_OPEN), ngdread IF_DEQUEUEs the drained-then-freed readq yielding dangling mbufs whose m_free is a double free and msleeps on a destroyed freed mutex, ngdwrite dereferences freed priv->hook into netgraph dispatch, ngdpoll UAF-reads the queue. LATENT: guest-verified non-compilable unwired FreeBSD-port rot (64-error build log; absent from Makefile SUBDIR; no /boot/kernel/ng_device.ko) and both trigger sides require root - hence Medium not High. Root-gated heap UAF read/write in M_NETGRAPH chunks + mbuf double-free + freed-hook walk if the port is completed without the lifetime fix; fix.diff (refcounted priv, si_drv1 cleared under lock, last-reference free) authored but not compile-testable (same rot). v1 twin sets DF-0660..666/DF-2570..2574 target the unrelated 633-LOC v1 implementation and carry nothing into this import (no VLA, no connection list, no m_devget).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3547 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.1 KB | β raw | |
| VERDICT.md | β | 6.6 KB | β raw | |
| stalefd.c | β | 1.5 KB | view raw | |
| build.sh | β | 162 B | view raw | |
| run.sh | β | 192 B | view raw | |
| build.log | β | 12.6 KB | view raw | |
| env.txt | β | 1.4 KB | view raw | |
| fix.diff | β | 4.4 KB | view raw | |
| verdict.json | β | 3.9 KB | view raw | |
| manifest.json | β | 968 B | view raw |
DF-3547 β use-after-free of struct ngd_private in ng7 ng_device cdevsw methods vs ng_device_disconnect
One-line summary
ng_device_disconnect() calls DragonFly's asynchronous destroy_dev() and then
immediately frees priv; every cdevsw method (ngdopen/ngdclose/ngdread/ngdwrite/ngdpoll)
still reaches priv through dev->si_drv1, producing a kernel heap use-after-free
(read+write) once the node type is built.
IMPORTANT STATUS NOTE (read first)
sys/netgraph7/ng_device.c is dead code in this tree: it does not compile against
DragonFly headers (it is unmodified FreeBSD porting rot from the 2008 ng7 import β
struct cdevsw, d_open_t, msleep, IF_LOCK, _IF_ENQUEUE, IFQ_IS_EMPTY,
IFQ_SET_MAXLEN, m_uiotombuf, MTX_DEF, ifqueue.ifq_mtx, unit2minor do not
exist in DF), it is absent from sys/netgraph7/Makefile SUBDIR, and no kernel config
defines NETGRAPH7_DEVICE. ./build.sh reproduces the compile failure on the guest.
Therefore the UAF cannot be triggered on any buildable kernel today; the verdict is
build_failed with the defect proven by source-level analysis (see VERDICT.md).
Build (reproduces non-compilability)
sh build.sh # guest, root β out-of-tree kmod build of ng_device.c
# β 64 compile errors, full log in build.log
Run (would trigger the UAF if the module existed)
cc -O2 -o stalefd stalefd.c # on the guest
sh run.sh # documents the intended sequence:
# open /dev/ngd0 -> keep fd -> ngctl rmhook
# -> read()/write() on the stale fd => UAF
Success criterion (if the node type ever becomes loadable): kernel panic /
INVARIANTS double-free / objcache corruption from IF_DEQUEUE on the freed queue or
NG_SEND_DATA_ONLY(..., priv->hook /* freed */, ...).
Fix
fix.diff β reference-counted priv (acquire/release around every cdevsw method,
si_drv1 cleared under the lock before destroy_dev, free deferred to last release).
Cannot be compile-validated for the same reason (file rot) β fix_status not_testable.
DF-3547 VERDICT β UAF of struct ngd_private (ng7 ng_device) vs async destroy_dev()
Status: build_failed / not runtime-testable (the containing file cannot be compiled into any DragonFly kernel β proven on the guest, see build.log). The defect itself is certain by source-level analysis; every link in the chain is cited below.
The bug
ng_device_disconnect() (sys/netgraph7/ng_device.c:292-312) tears the node down when
its hook is removed:
299: destroy_dev(priv->ngddev); /* (1) ASYNCHRONOUS in DragonFly */
300: mtx_destroy(&priv->ngd_mtx);
302: IF_DRAIN(&priv->readq); /* frees every queued mbuf */
303: mtx_destroy(&(priv)->readq.ifq_mtx);
305: free_unr(ngd_unit, priv->unit);
307: kfree(priv, M_NETGRAPH); /* (2) priv freed immediately */
(1) In DragonFly, destroy_dev() (sys/kern/kern_conf.c:345-354) only calls
devfs_destroy_dev() (sys/vfs/devfs/devfs_core.c:759-763), which calls
devfs_msg_send() (devfs_core.c:1113-1127) β a fire-and-forget lwkt_sendmsg
to the devfs core thread. It returns immediately: it does not wait for in-flight
cdevsw method invocations to drain, does not wait for blocked readers, and does not
revector the device's ops. devfs_destroy_dev_worker() (devfs_core.c:1440-1459) merely
unlinks the cdev and drops sysrefs. This is unlike FreeBSD, whose destroy_dev()
drains cdevsw users β the ng7 file was imported from FreeBSD assuming those semantics.
(2) Meanwhile every device method fetches the private structure straight from the cdev:
ngdopenng_device.c:334priv = dev->si_drv1;β :338-340mtx_lock(&priv->ngd_mtx); priv->flags |= NGDF_OPEN;β UAF write into the freed chunkngdcloseng_device.c:351priv = dev->si_drv1;β :354-356 same writengdreadng_device.c:411priv = dev->si_drv1;β :419IF_DEQUEUE(&priv->readq, m)(queue drained+freed at :302 β dangling mbufs βm_free()of freed mbufs = double free), :423-427msleep(priv, &priv->ngd_mtx, ...)on a destroyed, freed mutexngdwriteng_device.c:454priv = dev->si_drv1;β :469NG_SEND_DATA_ONLY(error, priv->hook, m)β dereferencespriv->hook(freed memory read) and, if the stale value survives, hands the mbuf to a freed hook pointer, walkingNG_HOOK_PEER/NG_PEER_NODE(ng_address_hook, ng_base.c:3062-3065)ngdpollng_device.c:481priv = dev->si_drv1;β :485IFQ_IS_EMPTY(&priv->readq)β UAF read
Why a stale path exists at all: an open file descriptor keeps the vnode β v_rdev
alive (sys/vfs/devfs/devfs_vnops.c:1272-1295 devfs_spec_read β reference_dev(dev)
β dev_dread(dev, ...) β release_dev(dev)), and the destroy path never clears
si_drv1 nor revokes ops β so every later read(2)/write(2)/poll(2) on the
still-open fd dispatches ngdread/ngdwrite/ngdpoll with priv already freed.
Additionally, because destroy_dev() is async, even a fresh open("/dev/ngd0") that
wins the race against the devfs core thread's unlink reaches ngdopen with dangling
si_drv1.
Trigger (if the node type were buildable)
ngctl mkpeer . device dl dl # root: creates ng_device node + hook, /dev/ngd0 appears ./stalefd & # opens /dev/ngd0, parks on the fd ngctl rmhook <device-node> dl # -> ng_device_disconnect -> destroy_dev(async) + kfree(priv) # resume stalefd: read(fd)/write(fd) on the freed priv
read() first dequeues from the drained-then-freed queue (double free of the mbufs that
IF_DRAIN already freed) β an INVARIANTS/objcache panic is the expected signature;
write() drives the freed priv->hook into netgraph dispatch. With heap grooming
(spraying M_NETGRAPH-sized chunks β e.g. more ng node privates) this is a
controlled-ish UAF-write/arb-walk primitive. Both sides require root (device is
UID_ROOT/GID_WHEEL 0600, ng control sockets require caps_priv_check(SYSCAP_RESTRICTEDROOT)
β sys/netgraph7/socket/ng_socket.c:182-184), so the realistic ceiling is
rootβkernel corruption, not unprivβroot.
Why it could NOT be verified at runtime (hard blocker, demonstrated)
sys/netgraph7/MakefileSUBDIR has nodeviceentry β no module is ever built (/boot/kernel/ng_device.koabsent on the guest,lsin env.txt).sys/conf/files:1699gates it behindoptional netgraph7_device; no kernel config in the tree defines it (grep -rn NETGRAPH7 /usr/src/sys/config/*β 0 hits).- The file does not compile: out-of-tree kmod build on the guest produces 64 errors β
'struct cdevsw' has no member named 'd_open',unknown type name 'd_poll_t','D_VERSION' undeclared,'MTX_DEF' undeclared,'struct ifqueue' has no member named 'ifq_mtx',implicit declaration of 'IF_LOCK',implicit declaration of 'IFQ_IS_EMPTY',implicit declaration of 'msleep'β¦ (full log: build.log). These are FreeBSD-only primitives that never existed in DragonFly.
A faithful runtime demonstration would require first completing the FreeBSDβDF port
(rewriting the cdevsw as struct dev_ops, msleepβssleep/tsleep with the
mutex-releasing semantics, the ifq shimβ¦), i.e. executing the maintainer's porting work
and then testing code that is no longer byte-identical to the tree. That was judged to
add no evidentiary value beyond the cited source chain above, so the honest
classification is build_failed / not runtime-testable, defect certain by
code reading.
Kernel references
- sys/netgraph7/ng_device.c:292-312 (disconnect free path), :332-343 (ngdopen), :348-359 (ngdclose), :408-443 (ngdread), :451-472 (ngdwrite), :478-489 (ngdpoll)
- sys/kern/kern_conf.c:345-354 (destroy_dev β devfs_destroy_dev, async)
- sys/vfs/devfs/devfs_core.c:759-763, :1113-1127 (async lwkt_sendmsg), :1440-1459 (worker)
- sys/vfs/devfs/devfs_vnops.c:1272-1295 (open fd keeps dispatching via si_drv1)
- sys/netgraph7/socket/ng_socket.c:182-184 (root-only control plane β bounds impact)
Recommended fix
Reference-count priv: acquire under ngd_mtx in every cdevsw method (check a
dying flag), drop the node's base reference in disconnect after clearing
si_drv1 under the same lock, and free priv only when the last method reference
returns β see fix.diff. (The file also needs its port finished or removed before any
of this is reachable; tracked as DF-3549.)
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_testableFix validation impossible on this guest: the unmodified file itself does not compile (64 errors, build.log), so neither a baseline nor a single-fix kernel exercising this code can be built. fix.diff is a source-level reference fix (refcounted priv lifetime, si_drv1 detached under lock before async destroy_dev, free deferred to last cdevsw reference); it should be re-validated as part of completing the ng7 device port (DF-3549).
build.log (file rot); VERDICT.md 'Why it could NOT be verified'; no fix_build.log/fix_run.log possible
Confirmed kernel references
- sys/netgraph7/ng_device.c:292
- sys/netgraph7/ng_device.c:299
- sys/netgraph7/ng_device.c:307
- sys/netgraph7/ng_device.c:334
- sys/netgraph7/ng_device.c:351
- sys/netgraph7/ng_device.c:411
- sys/netgraph7/ng_device.c:419
- sys/netgraph7/ng_device.c:454
- sys/netgraph7/ng_device.c:469
- sys/netgraph7/ng_device.c:481
- sys/kern/kern_conf.c:345
- sys/vfs/devfs/devfs_core.c:759
- sys/vfs/devfs/devfs_core.c:1113
- sys/vfs/devfs/devfs_core.c:1440
- sys/vfs/devfs/devfs_vnops.c:1272
- sys/netgraph7/socket/ng_socket.c:182
Detail
Evidence (decisive lines)
build.log (64 compile errors, out-of-tree kmod build on guest); env.txt (no ng_device.ko, no NETGRAPH7 in any kernel config, ng_etf/ng_rfc1490 load fine); VERDICT.md full path:line chain; fix.diff reference fix
PoC changes
PoC authored fresh (stalefd.c + build.sh); no seed existed. Build harness had to replicate the in-tree include environment via symlinks because the file is unwired.
Verified recommended fix
Refcount struct ngd_private: acquire under ngd_mtx in every cdevsw method with a dying flag; clear si_drv1 under the lock before async destroy_dev; free priv on last release instead of in disconnect (fix.diff).
Verdict
Certain use-after-free by source analysis, not runtime-testable: ng_device_disconnect() (ng_device.c:299-307) calls DragonFly's asynchronous destroy_dev() (kern_conf.c:345-354 -> devfs_core.c:1113-1127 fire-and-forget lwkt message; no draining, no ops revocation, si_drv1 never cleared) and then immediately kfree()s priv, while ngdopen/ngdclose/ngdread/ngdwrite/ngdpoll (ng_device.c:334/351/411/454/481) still fetch priv via dev->si_drv1 β an open fd keeps dispatching through it (devfs_vnops.c:1272-1295). Runtime verification is impossible because sys/netgraph7/ng_device.c is FreeBSD-port rot that cannot compile (64 errors: no struct cdevsw/d_open_t/msleep/IF_LOCK/_IF_ENQUEUE/IFQ_IS_EMPTY/MTX_DEF/ifq_mtx in DF β build.log) and is wired into no build (no Makefile SUBDIR, no NETGRAPH7_DEVICE config). Root-only trigger bounds impact; latent until the port is completed. fix.diff (refcounted priv + si_drv1 cleared under lock) also not compile-testable for the same reason.
No comments yet.