⬢ DragonFlyBSD Kernel Audit
DF-3531 / verdict.json
← back to finding ↓ download raw
{
  "finding_id": "DF-3531",
  "status": "reproduced",
  "reproduced": 1,
  "impact": "leak",
  "confidence": "certain",
  "verdict": "REPRODUCED on stock INVARIANS guest: every ng7 ng_tty close with a pending chand callout permanently leaks one ng_item + one node reference (ng_uncallouts rval>0 gate never passes on this kernel -- DF-3515 callout_stop contract -- and even past the gate NG_FREE_ITEM is flag-only). 12 -> 1.96K netgraph allocations (283K) per 2000 stall-close cycles; kldunload ng_tty stays Device busy; the leaked refs feed the DF-3514 rehash panic (observed within 20 cycles without DF-3514s module fix). Trigger is privileged (ngt_open needs SYSCAP_NONET_NETGRAPH). Fix layers 1+2 (kern_timeout PREVENTED contract + ng_free_item in ng_uncallout) measurably eliminate the leak (items flat: 9 in-use across 4000 stall cycles) BUT aggressive close-vs-fire racing on the patched state double-freed an in-flight timer item (immediate ng_snd_item NULL-el_node trap on an instrumented build; delayed memory chunk already free slotimer panic on clean modules) -- PREVENTED is being reported for a callout whose callback already dispatched. fix_status=inconclusive; the kern_timeout layer needs a dispatch-precise PREVENTED (flagged against DF-3515/DF-3523), and the ng_callout oitem-free hunk from DF-3514/3523s fix.diff must NOT be applied for self-rearming 1-tick timers like ng_tty (it reproduces the same double-free on every re-arm).",
  "exploit_chain": "unpriv-reachable? No: TIOCSETD->ngt_open requires SYSCAP_NONET_NETGRAPH (ng_tty.c:188). Chain for a capability-holding context: stall a ttys output (outq non-empty -> chand re-arms every tick), close the ldisc -> 1 item+node ref (~140B) leaked per cycle, unbounded; leaked refs pin the ng_tty module forever and push ng_nodes past the DF-3514 rehash panic threshold (panicked within 20 cycles pre-DF-3514-fix) -> local DoS / permanent kernel memory exhaustion.",
  "evidence": "run.log (baseline: 12 -> 1.96K/283K per 2000 stall cycles, control ~flat, kldunload busy); fix_run.log (kernel#1 + layer-2 module: items 9 in-use FLAT over 4000 stall cycles, nodes flat with DF-3534); panic.txt (delayed chunk_mark_free double-free on patched state under drain stress); VERDICT.md for the full separation of DF-3531 (items) vs DF-3534 (nodes) via the isolation runs in DF-3534s pack (tty_leak2 nodd/nodata/socksock/full).",
  "kernel_refs": [
    "sys/netgraph7/tty/ng_tty.c:453",
    "sys/netgraph7/tty/ng_tty.c:252",
    "sys/netgraph7/netgraph/ng_base.c:3248",
    "sys/netgraph7/netgraph/ng_base.c:3273",
    "sys/netgraph7/netgraph/ng_base.c:3284",
    "sys/netgraph7/netgraph/netgraph.h:787",
    "sys/kern/kern_timeout.c:888"
  ],
  "poc_changes": "Rewrote trigger vs the seed: close via TIOCSETD-away instead of fd-close (avoids the tty output-drain wait blocking close forever with a stalled master), O_NONBLOCK master for the drain control, NGIOCGINFO type check to prove the ng7 ldisc owns the slot, AF_NG=32 socket ABI harness mirroring DF-3523s working one. Pre-applied the known DF-3514 rehash module fix during baseline measurement so the known panic cannot mask the leak (documented in build.sh).",
  "attempts": 7,
  "guest_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #0 (baseline) / #1 Mon Sep 14 09:24:11 2026 (fix kernel) x86_64",
  "runtime_sec": 9600,
  "guest_dirty": 0,
  "build_cmd": "cd /usr/src/sys/netgraph7/netgraph && make && cd ../socket && make && cd ../tty && make && kldload .../netgraph.ko .../ng_socket.ko .../ng_tty.ko && cc -O2 -o ngtty_leak ngtty_leak.c",
  "run_cmd": "./ngtty_leak 2000        # stall-close leak run (vmstat -m before/after); ./ngtty_leak 2000 drain  # control",
  "code_hash": "c2d647f1bcec69221d61b0dc25fcac7d7278c79bb7a9e7b5e971739e3953bded",
  "fix_status": "inconclusive",
  "fix_baseline_reproduced": 1,
  "fix_patched_reproduced": 0,
  "fix_kernel_uname": "DragonFly dfbsd 6.5-DEVELOPMENT #1: Mon Sep 14 09:24:11 UTC 2026 (kern_timeout layer-1) + ng7 modules with layer-2",
  "fix_verdict": "Leak itself FIXED measurably: items flat (9 in-use) across 4000 stall-close cycles vs +1.95K/2000 on stock; module unpinned. NOT clean overall: under close-vs-fire race stress the patched state freed an in-flight timer item -> heap double-free (immediate ng_snd_item el_node==NULL trap; delayed memory chunk already free slotimer panic, panic.txt). Root: layer-1 reports PREVENTED for a callback that already dispatched (item in flight on the cpu0 netgraph port, refs==1). The kern_timeout fix must be dispatch-precise before this is shippable; ng_tty-class 1-tick self-rearming timers are the worst case. The ng_callout oitem hunk from DF-3514/3523 fix.diff was EXCLUDED here after it reproduced the same double-free signature on every re-arm.",
  "fix_evidence": "fix_run.log (=fix_run.final2.log): flat items/nodes over nodd500+stall2000x2 on kernel#1+layer2+DF-3534 module; panic.txt: panic: memory chunk 0xfffff8004f0a2740 is already free! chunk_mark_free<-slab_cleanup<-slotimer_callback after drain-mode stress; earlier instrumented run: Fatal trap 12 ng_snd_item lock addl $0x1,0x80(%rdi) with el_node==NULL.",
  "notes": "Node-count baseline delta includes DF-3534 (separate finding, separately fixed/validated). Guest reset to clean with-src snapshot after the run.",
  "recommended_fix": "kern_timeout.c: report PREVENTED only when the callback never dispatched (fix the ran-during-stop window); ng_base.c ng_uncallout: ng_free_item(item) after the flag-only NG_FREE_ITEM (keep the rval>0 + trampoline + node-match gate); do NOT free oitem in ng_callout for re-arm-from-callback timers."
}