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)
PoC verification
Evidence pack
findings/poc/DF-2574 Β· 17 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 validationrun.sh(=run_df2574_leak.sh) β the leak measurement driverrun.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.txtfix.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
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-createddevicenode; - pure userland:
/usr/sbin/ngctl mkpeer device: echo aN bN+ngctl rmhookover 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 20000on 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,
FIXOKround-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.cis dead code in the stock build system (absent fromsys/conf/filesand 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_testableConfirmed 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.
No comments yet.