NULL deref / panic: SLIST_REMOVE on connection never inserted in error paths of ng_device_newhook
Summary
Both error paths in ng_device_newhook call SLIST_REMOVE on new_connection but SLIST_INSERT_HEAD does not happen until :309 well after these error branches. SLIST_REMOVE on not-inserted element walks off end of list dereferences NULL panicking kernel whenever make_dev or readq kmalloc fails under memory pressure. SLIST_REMOVE enters else branch curelm=SLIST_FIRST(head) if list empty curelm NULL SLIST_NEXT dereferences NULL. [VERIFIED pass2: reproduced both list shapes - Fatal trap 12 fault 0x0 at ng_device_newhook+0x1a3 and +0x1f3 DWARF-mapped exactly to lines 290 and 300 via fault-injected make_dev and readq kmalloc failures; MATERIAL reachability fact - ng_device.c is dead code in the stock build system, not in conf files, no module, does not compile against current headers, so stock systems unreachable; error-path fix-validated, deletion flagged as valid alternative disposition]
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-2573 Β· 21 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 2.5 KB | β raw | |
| VERDICT.md | β | 9.1 KB | β 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 | |
| run_df2573_panic.sh | β | 1.8 KB | view raw | |
| run_df2574_leak.sh | β | 2.0 KB | view raw | |
| run_fix_validation.sh | β | 1.8 KB | view raw | |
| build.log | β | 5.7 MB | β download | |
| run.panic.log | β | 2.5 KB | view raw | |
| run.leak.log | β | 1.9 KB | view raw | |
| run.fix-validation.log | β | 1.8 KB | view raw | |
| panic.A.console.txt | β | 2.1 KB | view raw | |
| panic.B.console.txt | β | 2.0 KB | view raw | |
| trapsite.disasm.txt | β | 891 B | view raw | |
| env.txt | β | 573 B | view raw | |
| dmesg.txt | β | 421 B | view raw | |
| fix.diff | β | 637 B | view raw |
DF-2573 β ng_device_newhook error paths SLIST_REMOVE a connection that was never inserted β guaranteed NULL-deref panic
- File:
sys/netgraph/ng_device.c(old netgraph) - Lines: 290 (make_dev failure path) and 300 (readq kmalloc failure path); insertion only at 309
- Claim: both error paths call
SLIST_REMOVEonnew_connectionbefore it is ever linked β empty-list walks NULL immediately, non-empty lists walk past the last element to NULL βSLIST_NEXT(NULL, links)β fatal trap.
VERIFIED REPRODUCED (impact: panic, confidence: certain). Both variants
panicked exactly at the cited lines (DWARF-mapped trap offsets +0x1a3 β
:290 and +0x1f3 β :300), with the verbatim make_dev failed /
readq malloc failed kprintfs immediately preceding. The fix (delete both
bogus SLIST_REMOVEs) was applied in-guest, rebuilt, and re-verified: same
injections now return -1 cleanly, no panic, normal operation unchanged.
Reachability caveat (material): the target file is dead code in the
stock build system (not in sys/conf/files, not in the module SUBDIR, and
it no longer compiles against current headers β see VERDICT.md). The
verification compiled it into a custom kernel via a shim; the pristine
source sha256 7850d8ae⦠is attested at boot, and the ONLY textual change
is the never-invoked constructor signature line (asserted exactly-one-line
by the generator).
Contents
VERDICT.mdβ full narrative, mechanics, A/B fix validationngdpoc_shim_prepend.h/ngdpoc_append.c/gen_ngdpoc.sh/df_ngdpoc.c/ngdpoc_slab.diffβ the verification rig (verbatim interposition)build.shβ BASELINE / FIX / REGEN build orchestration (nativekernel)run_df2573_panic.shβ panic driver (variants A and B)run_df2574_leak.shβ shared-rig leak runner (see DF-2574)run_fix_validation.shβ fixed-kernel validation runspanic.A.console.txt,panic.B.console.txtβ captured panicstrapsite.disasm.txtβ DWARF line mapping of both trap PCsrun.panic.log,run.leak.log,run.fix-validation.logβ session logsbuild.log(full untrimmed),env.txt,dmesg.txtfix.diffβ the validated fix
Quick reproduce
./build.sh BASELINE
./run_df2573_panic.sh # expect: two Fatal trap 12 panics at
# ng_device_newhook+0x1a3 / +0x1f3
./build.sh FIX # push fix.diff to /tmp/fix.d first (see build.sh)
./run_fix_validation.sh # expect: no panic, ng_mkpeer = -1, guest alive
DF-2573 β VERDICT
status: reproduced β impact: panic (kernel NULL dereference / local DoS from ng_device_newhook error paths). confidence: certain.
Crucial reachability fact discovered during verification: the audit
target sys/netgraph/ng_device.c is dead code in the stock build system:
- absent from
sys/conf/files(onlynetgraph7/ng_device.c optional netgraph7_deviceis registered β the netgraph7 rewrite is a different file and does not share these bugs), - absent from the module
SUBDIRlist insys/netgraph/Makefile(there is no ng_device.ko anywhere; guest/boot/kernelcontains onlynetgraph.ko), - and it no longer compiles against current headers at all: it programs the
2003-era device API (
struct cdevsw,make_dev(cdevsw*), old-signatured_*_t,cdevsw_add,ng_constructor_t(node_p)) that has since been removed from the tree.
So on any stock DragonFly system this code cannot be reached β not by root,
not under memory pressure, not via kldload (the module does not exist).
Verification therefore compiled the file INTO a custom kernel
(X86_64_NGDPOC, stock X86_64_GENERIC INVARIANTS build plus
options NETGRAPH, NETGRAPH_ECHO, NETGRAPH_SOCKET, device ngdpoc)
using a shim that provides only the missing infrastructure (see below). The
bugs are real in the source text and mechanically deterministic once the
code is compiled; the finding stands as filed (Medium), with the dead-code
caveat front and center for triage (see Recommended disposition).
The rig (verbatim interposition)
gen_ngdpoc.shbuildssys/netgraph/ng_device_poc.c= shim prologue +ng_device.c+ accessor epilogue. The verbatim region is verified byte-identical to/usr/src/sys/netgraph/ng_device.cby reconstruction and cmp; the pristine sha256 (7850d8aeβ¦, identical to the audit tree) is baked into the binary and printed at every boot (NGDPOC: verbatim ng_device.c sha256=β¦ conn_size=40).- EXACTLY ONE line differs from pristine, asserted by the generator
(
diff | grep -c '^< '== 1):ng_device_cons(node_p node)βng_device_cons(node_p *node)β aligning the (never-invoked, always-EINVAL) constructor with the currentng_constructor_tABI that the tree changed under the file. The functions under test βng_device_newhook(ng_device.c:263-312),ng_device_disconnect(385-414),get_free_unit(224-258) β are byte-identical. - The shim
make_dev()forwards to the REALmake_dev()/devfs through a trampolinedev_ops, so a REAL/dev/ngd0appears (verified: crw-------, data written to /dev/ngd0 traversed the echo netgraph node and was read back). Fault injection is scoped to this file only: - make_dev failure: armed via
kern.ngdpoc.fail_makedev(shim returns NULL on the Nth call from ng_device.c); - readq kmalloc failure: one-shot
kmalloc(10240, M_DEVBUF)failure, patched at the entry of_kmalloc/_kmalloc_debug(sys/kern/kern_slaballoc.c, guest-only interposerngdpoc_slab.diff), armed and consumed inside the same sysctl call. - The driver is driven through the REAL netgraph machinery β the harness
calls the exported
ng_mkpeer()/ng_destroy_hook()/ng_findname()(exactly what ngctl mkpeer/rmhook drive at ng_base.c:1270/1314), and the pure-userland path was also exercised with /usr/sbin/ngctl over the compiled-in ng_socket.
Baseline results (vulnerable kernel #0, built 23:52 UTC)
Variant B β non-empty list, injected readq kmalloc failure
(ng_device.c:300). One live connection first (conn=1 β¦ unit=0), then
sysctl -n "kern.ngdpoc.trigger=mkpeer_failreadq tB":
NGDPOC: armed one-shot kmalloc(10240, M_DEVBUF) failure ng_device_newhook() get_free_unit() NGDPOC: injected kmalloc(10240, M_DEVBUF) failure ng_device_newhook(): readq malloc failed Fatal user address access from kernel mode from sysctl at ffffffff80a87223 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 Stopped at ng_device_newhook+0x1f3: movq (%rdx),%rax db>
(panic.B.console.txt; guest wedged in DDB, ssh dead, vm status down.)
Variant A β empty list, injected make_dev failure (ng_device.c:290).
Fresh boot (list empty, M_DEVBUF baseline), kern.ngdpoc.fail_makedev=1,
then trigger:
ng_device_newhook() get_free_unit() NGDPOC: injected make_dev() failure ng_device_newhook(): make_dev failed Fatal user address access from kernel mode from sysctl at ffffffff80a871d3 Fatal trap 12: page fault while in kernel mode fault virtual address = 0x0 Stopped at ng_device_newhook+0x1a3: movq (%rdx),%rax db>
(panic.A.console.txt.)
Line-accurate confirmation: with -g DWARF from the built object
(trapsite.disasm.txt), both trap instructions resolve to the two
SLIST_REMOVE lines:
* ng_device_newhook+0x1a3 β ng_device_poc.c:480 β ng_device.c:290
(make_dev error path),
* ng_device_newhook+0x1f3 β ng_device_poc.c:490 β ng_device.c:300
(readq error path).
Mechanics (why it always panics)
SLIST_REMOVE(head, elm, β¦) (sys/sys/queue.h:208-220) on an element that
was never SLIST_INSERTed (insert happens only at ng_device.c:309, after
every failure point):
- empty list:
SLIST_FIRST(head)==NULL != elmβ else-branch setscurelm = NULLβSLIST_NEXT(curelm, links)reads address 0x0 β fatal trap 12 (fault virtual address = 0x0 β exactly what both panics show). - non-empty list:
curelmwalks the real connections;elmis not in the list, socurelmruns past the last element to NULL and the nextSLIST_NEXT(NULL)faults identically.
No KASSERT is involved; this is a plain NULL dereference on INVARIANTS and production kernels alike. Preconditions in a hypothetical live deployment: root (netgraph configuration) + a make_dev or 10 KB M_DEVBUF allocation failure under memory pressure β M_NOWAIT failures are routine under load on DragonFly (see malloc.h M_NOWAIT commentary).
Fix validation (fix.diff applied to guest /usr/src, incremental rebuild)
fix.diff deletes both SLIST_REMOVE calls (the connection is only linked
at :309, after all failure points; the error paths already kfree the
connection). The POC TU was regenerated from the FIXED source (boot attest
sha256 249587a7β¦) and the exact PoCs re-run (run.fix-validation.log):
- A: injected make_dev failure β
ng_device_newhook(): make_dev failed,ng_mkpeer(fixA, type echo) = -1, conn=0, guest alive, no panic. - B: injected readq failure (with a live connection on the list) β
readq malloc failed, destroy_dev cleanup,ng_mkpeer(fixB) = -1, list intact (conn=1), guest alive, no panic. - Normal operation unchanged: hook create,
FIXOKround-trip through /dev/ngd0 via the echo node, clean disconnect; M_DEVBUF returns to baseline (inuse 211).
Impact ceiling and disposition
- Primitive: attacker-timed NULL-page READ from kernel mode β guaranteed panic / local DoS. No write primitive, no info leak; not an escalation.
- Privilege needed to reach it on a system where the file were built: root (netgraph node configuration via ngctl; nodes cannot be created by unprivileged users, and stock kernels ship no netgraph at all).
- On stock systems: unreachable (dead code β see top).
- Per the team directive (panic + leak, root-gated netgraph β close or flag): close as panic, with the dead-code status recorded; the one-line-each fix (or outright removal of the bit-rotted file) is validated above.
Environment
- Guest: DragonFly 6.5-DEVELOPMENT x86_64, custom
X86_64_NGDPOCINVARIANTS kernel (gcc 8.3 / cc80), 6 vCPU, 4 GB RAM, QEMU/KVM (env.txt). make -j6 nativekernel(buildkernel is gated by bk_tools on this snapshot) +make installkernel DESTKERNNAME=kernel(build.sh;build.logis the full untrimmed log of the final rebuild β the two earlier baseline build logs were overwritten in /root/build_ngdpoc.log by subsequent rebuilds; the procedure is byte-identical, and the baseline kernel is attested in-run by the boot linesha256=7850d8aeβ¦plus#0: β¦ 23:52in run.leak.log).- Harness behavior notes (documented for reproducers): DF sysctl(8) string-writes retry the handler while it returns an error β the harness trigger therefore always returns 0 and reports results via kprintf (df_ngdpoc.c header comment).
Reproduce
./build.sh BASELINE # ~12 min full build, installs + reboots vm.sh run_root 'sysctl -n kern.ngdpoc.stats' # list empty, baseline vm.sh run_root 'sysctl kern.ngdpoc.fail_makedev=1; sysctl -n "kern.ngdpoc.trigger=mkpeer tA"' # -> panic "Fatal trap 12 ... Stopped at ng_device_newhook+0x1a3: movq (%rdx),%rax" # variant B: mkpeer keepB first, then trigger=mkpeer_failreadq tB -> panic at +0x1f3 ./build.sh FIX # applies fix.d/*.diff, rebuilds from fixed source # re-run: clean -1 returns, no panic (run_fix_validation.sh)
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)
panic.A.console.txt and panic.B.console.txt (Fatal trap 12, fault virtual address 0x0, Stopped at ng_device_newhook+0x1a3/+0x1f3 preceded by the verbatim error kprintfs); trapsite.disasm.txt (DWARF maps +0x1a3 to ng_device_poc.c:480 = ng_device.c:290 and +0x1f3 to ng_device_poc.c:490 = ng_device.c:300); run.fix-validation.log (fixed kernel: ng_mkpeer = -1, guest alive, normal /dev/ngd0 data round-trip unchanged); dmesg.txt (boot attest verbatim sha256 and ng_device_init).
PoC changes
PoC is a kernel-compiled verbatim-interposition rig, not the seeded userland program: gen_ngdpoc.sh concatenates shim + byte-verbatim ng_device.c + epilogue with cmp-verified verbatim region and an asserted exactly-one-line constructor-signature compatibility edit (ng_device_cons node_p to node_p star, never invoked); the shim make_dev forwards to the real devfs make_dev via a trampoline dev_ops so a real /dev/ngd0 appears; make_dev failure injection is scoped inside that shim, readq failure injection is a one-shot guard patched at kern_slaballoc.c _kmalloc entry; driving uses real ng_mkpeer and ng_destroy_hook on the real boot-created device node, plus pure-userland ngctl for the leak runs.
Verified recommended fix
Delete both SLIST_REMOVE calls in the ng_device_newhook error paths (see fix.diff); the connection is linked only at line 309 after all failure points.
Verdict
Both error paths of ng_device_newhook were driven on a custom INVARIANTS kernel compiling the byte-verbatim file (pristine sha256 7850d8ae attested at boot) through the real netgraph machinery, with make_dev and the 10240-byte readq kmalloc failing via file-scoped fault injection. Both variants panicked deterministically: Fatal trap 12, fault address 0x0, stopped at ng_device_newhook+0x1a3 and +0x1f3, DWARF-mapped to exactly ng_device.c:290 and ng_device.c:300, immediately after the verbatim make_dev failed / readq malloc failed kprintfs. Empty list walks curelm=NULL at once; non-empty list walks past the last element to NULL. Removing both SLIST_REMOVE calls and rebuilding from the fixed source makes both injections return -1 with no panic and no behavioral regression. Material caveat: the file is dead code in the stock build system (absent from sys/conf/files and the module SUBDIR, and it no longer compiles against current headers), so stock systems are not reachable; even if revived the trigger needs root plus an allocation failure. Panic-only primitive, no escalation; per team directive close as panic with the dead-code status recorded.
No comments yet.