β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-0662

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0662 Β· 7 files
FileTypeDescriptionSize
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
VERDICT.md verdict source trace + UAF race + dead-code reachability
↓ download 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 the SLIST_FOREACH (:518) holds a raw connection pointer (no refcount/lock taken on it) and proceeds to memcpy(buffer, connection->readq, amnt) (:531) and memcpy(connection->readq, ...) (:532).
  • If ng_device_disconnect runs concurrently (e.g. ngctl rmhook) and reaches line 407 while that ngdread thread is between :518 and :531, the read dereferences/memcpys freed readq (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

Fix verification

not_testable

not_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.