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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-2573 Β· 21 files
FileTypeDescriptionSize
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_REMOVE on new_connection before 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 validation
  • ngdpoc_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 runs
  • panic.A.console.txt, panic.B.console.txt β€” captured panics
  • trapsite.disasm.txt β€” DWARF line mapping of both trap PCs
  • run.panic.log, run.leak.log, run.fix-validation.log β€” session logs
  • build.log (full untrimmed), env.txt, dmesg.txt
  • fix.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
VERDICT.md
↓ download raw

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 (only netgraph7/ng_device.c optional netgraph7_device is registered β€” the netgraph7 rewrite is a different file and does not share these bugs),
  • absent from the module SUBDIR list in sys/netgraph/Makefile (there is no ng_device.ko anywhere; guest /boot/kernel contains only netgraph.ko),
  • and it no longer compiles against current headers at all: it programs the 2003-era device API (struct cdevsw, make_dev(cdevsw*), old-signature d_*_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.sh builds sys/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.c by 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 current ng_constructor_t ABI 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 REAL make_dev()/devfs through a trampoline dev_ops, so a REAL /dev/ngd0 appears (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 interposer ngdpoc_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 sets curelm = NULL β†’ SLIST_NEXT(curelm, links) reads address 0x0 β†’ fatal trap 12 (fault virtual address = 0x0 β€” exactly what both panics show).
  • non-empty list: curelm walks the real connections; elm is not in the list, so curelm runs past the last element to NULL and the next SLIST_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, FIXOK round-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_NGDPOC INVARIANTS 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.log is 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 line sha256=7850d8ae… plus #0: … 23:52 in 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_testable
↓ fix.diffper-fix-DF-2573

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