Use-after-free of readq: ng_device_disconnect frees readq BEFORE destroy_dev drains in-flight operations
Summary
ng_device_disconnect (:407-413): line 407 kfree(connection->readq) BEFORE line 409 destroy_dev(ngddev). destroy_dev is the function that waits for in-flight cdev ops to drain so must run FIRST. Thread already past SLIST_FOREACH in ngdread (:518) holds connection ptr continues memcpy(connection->readq,...) :531 while disconnect frees readq -> UAF. Also connection struct itself never kfree_d (missing kfree pairs kmalloc at :273) -> sizeof(ngd_connection) leak per disconnect. Race: concurrent ngdread vs ngctl rmhook. UAF on readq 10KiB M_DEVBUF groomable for arbitrary kernel r/w.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0662 Β· 7 files| File | Type | Description | Size | |
|---|---|---|---|---|
| fix.diff | suggested-fix | destroy_dev before kfree(readq); add kfree(connection) | 405 B | view raw |
| VERDICT.md | verdict | source trace + UAF race + dead-code reachability | 3.6 KB | β raw |
| build.sh | build-log | no-op | 129 B | view raw |
| run.sh | run-log | no-op; checks fix.diff applies | 373 B | view raw |
| env.txt | environment | guest uname | 150 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0662 β UAF of readq: ng_device_disconnect frees readq before destroy_dev drains in-flight reads
Verdict: BUG CONFIRMED REAL by source trace β NOT reachable on the default kernel (ng_device is unshipped, unbuildable dead code)
Bug summary (source-confirmed)
ng_device_disconnect() (sys/netgraph/ng_device.c:386-414) frees the per-connection
read queue before destroying the cdev, and never frees the connection struct:
407 kfree(connection->readq, M_DEVBUF); // (1) frees readq FIRST
409 destroy_dev(connection->ngddev); // (2) drains in-flight cdev ops AFTER
411 SLIST_REMOVE(&sc->head, connection, ngd_connection, links);
// (3) connection struct never kfree'd (leak)
destroy_dev() is precisely the call that waits for in-flight cdev operations
(read/write/ioctl/poll on /dev/ngdN) to drain. So freeing readq before
destroy_dev creates a use-after-free window:
- A thread in
ngdread()(:505-547) that already passed theSLIST_FOREACH(:518) holds a rawconnectionpointer (no refcount/lock taken on it) and proceeds tomemcpy(buffer, connection->readq, amnt)(:531) andmemcpy(connection->readq, ...)(:532). - If
ng_device_disconnectruns concurrently (e.g.ngctl rmhook) and reaches line 407 while thatngdreadthread is between:518and:531, the read dereferences/memcpys freedreadq(10 KiB,M_DEVBUF) β UAF.
Additional defect: the ngd_connection struct itself (kmalloc'd at :273) is
never freed β sizeof(struct ngd_connection) leaked per disconnect.
The UAF target is a 10 KiB M_DEVBUF allocation (NGD_QUEUE_SIZE = 1024*10,
:106), so it is groomable (spray the bucket, reclaim with a controlled object)
for arbitrary kernel r/w once triggered.
Trigger: concurrent ngdread on /dev/ngdN vs ngctl rmhook/hook teardown.
Race window, so reliability needs grooming, but the ordering bug is unconditional.
Reachability β DEAD CODE on the default kernel
Same as DF-0660/DF-0661: ng_device is not in any netgraph/netgraph7
SUBDIR and its source uses the removed struct cdevsw API, so it is neither
shipped nor compilable on current DragonFly. This is latent/dead code; the UAF is
real but unreachable on a default install.
Also note the device itself is created mode 0600 (make_dev(&ngd_cdevsw, ...,
0600, "ngd%d", ...) at :286-287), so even if shipped it would be root-only
(no unprivileged path).
Reproduction
Not reproduced at the kernel level: the code is unshipped/unbuildable AND the
trigger is a timing race (concurrent read vs disconnect). The ordering defect is
confirmed by direct source trace. See fix.diff.
Fix (fix.diff)
Reorder so destroy_dev() drains in-flight cdev ops before readq is freed,
and free the leaked connection struct:
- kfree(connection->readq, M_DEVBUF);
-
destroy_dev(connection->ngddev);
+
+ kfree(connection->readq, M_DEVBUF);
SLIST_REMOVE(&sc->head, connection, ngd_connection, links);
+ kfree(connection, M_DEVBUF);
Kernel references
sys/netgraph/ng_device.c:407βkfree(readq)BEFOREdestroy_devsys/netgraph/ng_device.c:409βdestroy_dev(should drain first)sys/netgraph/ng_device.c:273βconnectionkmalloc(never freed)sys/netgraph/ng_device.c:518-532βngdreadusesconnection->readqwith no refcountsys/netgraph/ng_device.c:106βNGD_QUEUE_SIZE(10 KiB UAF target)sys/netgraph/ng_device.c:286-287βmake_dev(..., 0600, ...)(root-only)sys/netgraph/Makefile/sys/sys/conf.hβ ng_device unshipped; cdevsw removed
Fix verification
not_testablenot_applicable: dead code
Confirmed kernel references
β
Detail
Exploit chain
none -- dead code
Evidence (decisive lines)
β
Verdict
Source-confirmed real. ng_device_disconnect frees readq before destroy_dev -> UAF race vs ngdread. Connection struct never freed (leak). Dead code.
No comments yet.