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

Memory leak of struct ngd_connection in ng_device_disconnect (unbounded kernel heap exhaustion)

Summary

ng_device_disconnect frees readq and destroys ngddev removes connection from list but never kfrees connection struct itself. Each hook teardown leaks sizeof(struct ngd_connection). SLIST_REMOVE recycles unit number attacker can repeat connect/disconnect indefinitely growing kernel heap without bound. ~48 bytes per cycle slow kmem exhaustion. [VERIFIED pass2: reproduced with exact malloc-type accounting - 40500 cycles leak 1.62MB 40500 objects perfectly linear incl. 500 driven purely from userland ngctl, calls delta 2 vs inuse delta 1 proves 2 allocations vs 1 free; same dead-code reachability caveat as DF-2573; kfree fix-validated - 20000 cycles delta 0, data round-trip unchanged]

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2574 Β· 17 files
FileTypeDescriptionSize
README.md β€” 1.6 KB ↓ raw
VERDICT.md β€” 4.5 KB ↓ raw
run.sh β€” 89 B view raw
run_df2574_leak.sh β€” 89 B view raw
run_fix_validation.sh β€” 1.8 KB view raw
run.leak.log β€” 1.9 KB view raw
run.fix-validation.log β€” 1.8 KB view raw
ngdpoc_shim_prepend.h β€” 6.8 KB view raw
ngdpoc_append.c β€” 1.5 KB view raw
gen_ngdpoc.sh β€” 2.7 KB view raw
df_ngdpoc.c β€” 8.0 KB view raw
ngdpoc_slab.diff β€” 1015 B view raw
build.sh β€” 74 B view raw
build.log β€” 5.7 MB ↓ download
env.txt β€” 573 B view raw
dmesg.txt β€” 421 B view raw
fix.diff β€” 230 B view raw

DF-2574 β€” struct ngd_connection leaked in ng_device_disconnect (40 B of M_DEVBUF per cycle, unbounded)

  • File: sys/netgraph/ng_device.c
  • Line: 411 region β€” disconnect kfrees readq, destroys the cdev, unlinks the list entry, but never kfrees the connection allocated at :273
  • Claim: ~40 bytes leaked per connect/disconnect cycle, repeatable without bound.

VERIFIED REPRODUCED (impact: dos β€” heap exhaustion, confidence: certain). Measured with exact kernel M_DEVBUF accounting: +40 bytes and +1 in-use allocation per cycle, perfectly linear over 100 / 20000 / 20000 in-kernel cycles and 500 userland ngctl cycles (40500 total, +1.62 MB never reclaimed). The one-line fix (kfree(connection, M_DEVBUF) at the end of disconnect) drives the delta to exactly zero with normal operation unchanged.

Reachability caveat (material): dead code in the stock build system β€” see VERDICT.md and the DF-2573 pack.

Contents

  • VERDICT.md β€” full narrative + measurement tables + fix validation
  • run.sh (= run_df2574_leak.sh) β€” the leak measurement driver
  • run.leak.log, run.fix-validation.log β€” untrimmed run logs
  • rig sources shared with DF-2573 (ngdpoc_shim_prepend.h, ngdpoc_append.c, gen_ngdpoc.sh, df_ngdpoc.c, ngdpoc_slab.diff, build.sh, run_fix_validation.sh)
  • build.log, env.txt, dmesg.txt
  • fix.diff β€” the validated one-line fix

Quick reproduce

./build.sh BASELINE
./run.sh                  # expect +40 B / +1 inuse per cycle, linear
./build.sh FIX            # push fix.diff to /tmp/fix.d first
./run_fix_validation.sh   # expect cycle 20000 -> inuse delta 0
VERDICT.md
↓ download raw

DF-2574 β€” VERDICT

status: reproduced β€” impact: dos (unbounded kernel heap leak, 40 bytes of M_DEVBUF per connect/disconnect cycle, never reclaimed; root-gated, dead-code reachability β€” see below). confidence: certain.

What was verified and how

Same rig as DF-2573 (see that pack; shared sources copied here): the byte-verbatim sys/netgraph/ng_device.c compiled into the X86_64_NGDPOC INVARIANTS kernel (pristine sha256 7850d8ae… attested at boot, sizeof(struct ngd_connection) == 40 attested at boot), driven through the REAL netgraph paths:

  • in-kernel: exported ng_mkpeer() (what ngctl mkpeer drives at ng_base.c:1270) + ng_destroy_hook() (ng_base.c:1314) on the real boot-created device node;
  • pure userland: /usr/sbin/ngctl mkpeer device: echo aN bN + ngctl rmhook over the compiled-in ng_socket.

ng_device_disconnect (ng_device.c:385-414) frees readq (kfree at :407), destroys the cdev (:409) and unlinks the connection (SLIST_REMOVE at :411) β€” but never kfrees the struct ngd_connection itself, allocated in ng_device_newhook at :273 with kmalloc(sizeof(struct ngd_connection), M_DEVBUF, M_NOWAIT). get_free_unit() recycles unit numbers of removed connections, so the cycle can repeat without bound.

Measurements (exact in-kernel M_DEVBUF malloc-type accounting:

ks_use[cpu].{memuse,inuse,calls} summed over all CPUs; noise floor verified flat β€” three idle samples, zero drift)

stage cycles memuse delta inuse delta calls delta
control (idle, 18 s) 0 0 0 0
warmup +100 +4000 +100 +200
leak run 1 +20000 +800000 +20000 +40000
leak run 2 +20000 +800000 +20000 +40000
ngctl (userland path) +500 +20000 +500 +1000

Exactly 40 bytes and exactly 1 never-freed in-use allocation per cycle, perfectly linear, after which the connection list is empty (conn=0) and /dev/ngd0 is gone β€” the memory is leaked kernel heap that nothing reclaims (final state after 40500 cycles: inuse 212 β†’ 40812, memuse +1.62 MB; run.leak.log). The readq (10240 B) IS freed each cycle; only the 40-byte connection leaks (calls delta = 2 per cycle, inuse delta = 1). The leak is unbounded over time; ~26 M cycles would exhaust 1 GB of kernel heap with nothing but root netgraph syscalls.

Fix validation (fix.diff = add kfree(connection, M_DEVBUF); after the

SLIST_REMOVE in ng_device_disconnect; applied to guest /usr/src, POC TU regenerated from the fixed source, attest 249587a7…)

  • cycle 20000 on the fixed kernel: inuse delta 0, memuse delta 0 (calls +40000 β€” the allocations still happen and are now all freed; run.fix-validation.log).
  • Normal operation unchanged: hook create, FIXOK round-trip through /dev/ngd0 via the echo node, disconnect returns M_DEVBUF to baseline (inuse 211).

Impact ceiling and disposition

  • Resource-exhaustion DoS class (not an info leak; nothing is copied out β€” the leaked bytes never leave the kernel). Requires root to drive netgraph, and root can exhaust memory through many normal interfaces anyway, so the practical severity delta is low.
  • Material reachability caveat: sys/netgraph/ng_device.c is dead code in the stock build system (absent from sys/conf/files and the module SUBDIR list; it does not even compile against current headers β€” see the DF-2573 VERDICT). Stock systems cannot reach this code.
  • Per the team directive (panic + leak, root-gated netgraph β€” close or flag): close as a leak/doS with the dead-code status recorded; the one-line fix is validated above.

Environment

Same as DF-2573 (env.txt): DragonFly 6.5-DEVELOPMENT x86_64, X86_64_NGDPOC INVARIANTS kernel, gcc 8.3, 6 vCPU, QEMU/KVM; build via make -j6 nativekernel (build.sh, full log in build.log).

Reproduce

./build.sh BASELINE
./run.sh                  # run_df2574_leak.sh: control + 100 + 2x20000
                          # in-kernel cycles + 500 ngctl cycles
                          # expect: +40 bytes and +1 inuse per cycle
./build.sh FIX            # push fix.diff to /tmp/fix.d first
./run_fix_validation.sh   # expect: cycle 20000 -> inuse delta 0

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_testable
↓ fix.diffper-fix-DF-2574

Confirmed kernel references

Detail

Evidence (decisive lines)

run.leak.log (control noise floor flat; warmup +100 cycles = memuse +4000 / inuse +100; run1 +20000 = +800000 / +20000; run2 +20000 = +800000 / +20000; ngctl +500 = +20000 / +500; sanity block shows the live connection, the DF2574 round-trip through /dev/ngd0 and clean removal); run.fix-validation.log (cycle 20000 on fixed kernel: calls +40000 with inuse and memuse delta 0); dmesg.txt (boot attest sha256 and conn_size=40).

PoC changes

Measurement PoC instead of the seeded source-level claim: leak quantified via a kern.ngdpoc.stats sysctl that sums ks_use[cpu].memuse/inuse/calls for M_DEVBUF inside the kernel (no vmstat granularity involved); cycles driven through the real exported ng_mkpeer and ng_destroy_hook on the real device node, plus a pure-userland 500-cycle loop with /usr/sbin/ngctl; same verbatim-interposition rig as DF-2573 (see that verdict.json poc_changes).

Verified recommended fix

Add kfree(connection, M_DEVBUF) after the SLIST_REMOVE in ng_device_disconnect (see fix.diff).

Verdict

ng_device_disconnect (ng_device.c:385-414) frees the 10240-byte readq, destroys the cdev and SLIST_REMOVEs the list entry, but never kfrees the struct ngd_connection kmalloced in ng_device_newhook at ng_device.c:273. Measured with exact in-kernel M_DEVBUF malloc-type accounting on the verbatim-source harness kernel: exactly +40 bytes and +1 in-use allocation per real connect/disconnect cycle, perfectly linear over 100, 20000, 20000 in-kernel cycles and 500 userland ngctl cycles (40500 cycles total, 1.62 MB never reclaimed, inuse 212 to 40812), while the connection list returns to empty and the device node disappears, so unit recycling makes the leak unbounded. Adding kfree(connection, M_DEVBUF) after the SLIST_REMOVE and rebuilding from the fixed source drives the delta to exactly zero (inuse and memuse unchanged over 20000 cycles) with normal mkpeer, /dev/ngd0 data round-trip and rmhook unchanged. Impact ceiling is resource-exhaustion DoS only (no bytes leave the kernel, not an info leak), gated on root netgraph configuration. Material caveat: the file is dead code in the stock build system (absent from sys/conf/files and the module SUBDIR list, does not compile against current headers), so stock systems are not reachable; per team directive close as leak/doS with the dead-code status recorded.