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

Use-after-free read of dangling sch->hook in NGM_TEXT_STATUS after hook disconnect

  • File: sys/dev/netif/mn/if_mn.c
  • Lines: 474, 758, 407
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U:C:L/I:N/A:H
  • CWE: CWE-416 Use After Free
  • Confidence: certain

Summary

ngmn_newhook stores the freshly created hook pointer in sc->ch[chan]->hook (if_mn.c:474-475), but ngmn_disconnect never clears that pointer and never frees the schan (the channel struct lives for the whole softc).

The netgraph core frees the hook β€” and its name β€” immediately after the disconnect callback returns (ng_base.c:855-858).

A later NGM_TEXT_STATUS iterates sc->ch[chan] (still non-NULL) and dereferences sch->hook->name (if_mn.c:407), reading freed memory: a deterministic UAF that leaks a kernel pointer or panics.

Root cause

ngmn_newhook: sc->ch[chan]->hook = hook; (if_mn.c:475).

ngmn_disconnect sets sch->state = DOWN; (758) and frees only the trxd rings and mbufs (779-794); it never executes sch->hook = NULL; and never frees sc->ch[chan].

The netgraph core then runs ng_disconnect_hook β†’ kfree(hook->name) β†’ ng_unref_hook β†’ kfree(hook) (ng_base.c:855-858, 692), so the hook pointer now stored in sc->ch[chan]->hook is dangling and hook->name is already freed.

The status loop unconditionally does sch = sc->ch[i]; ... ksprintf(r+pos, " Chan %d <%s> ", i, sch->hook->name); (404-407) with no validity check, dereferencing the dangling pointer and then the freed name string.

Threat

Attacker position: root with netgraph access (same as DF-1539).

Sequence:

  1. connect a hook on mn0 (e.g. mkpeer mn0 echo ts2 downstream) so sc->ch[2]->hook points to a live hook;
  2. disconnect it (rmhook mn0 ts2 / ng_shutdown of the peer) β€” ngmn_disconnect runs, hook is freed by the core;
  3. issue NGM_TEXT_STATUS on mn0 β€” the loop visits sc->ch[2] (non-NULL), dereferences the freed hook, and treats the first qword of the freed/reused hook struct as a char *name pointer, then dereferences THAT.

Impact: kernel pointer leak (the M_NETGRAPH freelist link at the start of the freed hook struct is printed back as part of the channel name, defeating KASLR) or, more likely, a non-canonical/invalid pointer dereference causing an immediate kernel panic (local DoS).

Reliability of the panic is high because the freed hook slot is typically reused quickly under any netgraph activity.

Hardware precondition: mn0 node must exist.

Exploit / PoC

Repro (root, on hardware with mn0):

#!/bin/sh
# poc_uaf_status.sh
ngctl mkpeer mn0 echo ts2 downstream    # sc->ch[2]->hook = live hook
ngctl rmhook mn0 ts2                    # ngmn_disconnect; hook then freed by core
ngctl show mn0                          # status loop dereferences dangling sch->hook->name

show mn0 after the rmhook typically panics in the ksprintf %s walk of the freed hook->name pointer, or prints garbage/pointer bytes as the channel name (info leak).

Capture boot.log for the fatal trap / page-fault address in ksprintf→PCHAR.

C variant: same NGM_MKPEER then NGM_RMHOOK (NGM_GENERIC_COOKIE/NGM_RMHOOK) then NGM_TEXT_STATUS sequence over a PF_NETGRAPH control socket.

Clear the back-pointer at disconnect and skip channels with no live hook in the status loop. The status-loop sch->hook == NULL check in the DF-1539 fix above also covers this; the matching writer-side fix is:

--- a/sys/dev/netif/mn/if_mn.c
+++ b/sys/dev/netif/mn/if_mn.c
@@ -756,6 +756,7 @@ ngmn_disconnect(hook_p hook)
    if (sch->state == DOWN)
        return (0);
    sch->state = DOWN;
+   sch->hook = NULL;   /* prevent UAF in NGM_TEXT_STATUS after the core frees hook */

    /* Set receiver & transmitter off */
    sc->m32_mem.cs[chan].flags = 0x80920006;
  • DF-1539 (sibling): heap overflow in same file's NGM_TEXT_STATUS handler.
  • DF-1541 (sibling): OOB read in NGM_TEXT_CONFIG.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1540 Β· 8 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.7 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.7 KB ↓ raw
fix.diff suggested-fix git-apply-able unified diff fixing the cited bug 640 B view raw
fix_apply.log apply-log patch --dry-run --forward output proving fix.diff applies cleanly on with-src 547 B view raw
env.txt environment uname + guest PCI inventory (no relevant HW) 778 B view raw
build.sh build-script echo pointer to kernel rebuild path 362 B view raw
run.sh run-script echo pointer to VERDICT.md 302 B view raw
fix_build.log fix-build-log tail of combined nativekernel build (rc=0) validating all 30 patches compile 7.2 KB view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1540: if_mn ngmn_disconnect UAF on sch->hook

Class: Use-after-free (dangling pointer) Cited site: sys/dev/netif/mn/if_mn.c:475,758,407

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so the cited code path is not reachable at runtime on this guest.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/netif/mn/if_mn.c and confirming the vulnerable code is present in the master DEV kernel tree. The fix.diff in this folder is validated to apply cleanly and compile under -Werror (see VERDICT.md).

Mechanism

ngmn_newhook sc->ch[chan]->hook=hook at 475. ngmn_disconnect at 758 sets sch->state=DOWN but NEVER clears sch->hook. Netgraph core (ng_base.c:855-858) kfree(hook->name)+kfree(hook) immediately after disconnect returns. Later NGM_TEXT_STATUS iterates sc->ch[chan] (still non-NULL) and derefs sch->hook->name at 407 -> dangling pointer.

Realistic impact ceiling (on suitable HW)

kernel pointer leak (M_NETGRAPH freelist ptr printed as name); potential UAF corrupt on re-alloc

Fix

Clear sch->hook = NULL in ngmn_disconnect after setting state=DOWN.

See fix.diff for the git-apply-able patch.

How to validate the fix

scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1540.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1540.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1540: if_mn ngmn_disconnect UAF on sch->hook

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/netif/mn/if_mn.c:475,758,407, but the affected driver attaches only to hardware not present in the audit QEMU guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered here. The fix.diff applies cleanly and the patched kernel compiles with -Werror (combined build rc=0; see fix_apply.log).

Mechanism (cited path β†’ primitive β†’ effect)

ngmn_newhook sc->ch[chan]->hook=hook at 475. ngmn_disconnect at 758 sets sch->state=DOWN but NEVER clears sch->hook. Netgraph core (ng_base.c:855-858) kfree(hook->name)+kfree(hook) immediately after disconnect returns. Later NGM_TEXT_STATUS iterates sc->ch[chan] (still non-NULL) and derefs sch->hook->name at 407 -> dangling pointer.

Reachability on this guest

No β€” sys/dev/netif/mn/if_mn.c:475 is in a driver/module that only attaches to hardware absent from the audit guest. The trigger requires the relevant PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS loaded by root or via VFIO passthrough).

Phase 6 β€” escalation potential

This is a Use-after-free primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).

Realistic impact ceiling on suitable HW: kernel pointer leak (M_NETGRAPH freelist ptr printed as name); potential UAF corrupt on re-alloc.

Phase 8 β€” fix validation

fix.diff is a minimal, targeted fix at the root cause confirmed above.

  • Applied cleanly with patch -p1 --forward (verified in fix_apply.log).
  • Compiled with -Werror as part of the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.json).
  • For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.

Fix approach: Clear sch->hook = NULL in ngmn_disconnect after setting state=DOWN.

PoC changes

Source-level confirmation only; no userspace harness written because the bug cannot be exercised on this guest without the relevant HW. The placeholder build.sh/run.sh echo pointers to VERDICT.md and the module/kernel rebuild path.

Confirmed kernel references

Detail

Exploit chain

none β€” HW-gated. Primitive is a kernel pointer leak (M_NETGRAPH freelist ptr printed as name) and a UAF corrupt opportunity on re-alloc.

Evidence (decisive lines)

Source: sys/dev/netif/mn/if_mn.c:475 β€” sc->ch[chan]->hook = hook; :758 β€” sch->state = DOWN (hook never cleared); :407 β€” sch->hook->name (dangling). Guest has no SigmaTel M32 card. fix.diff adds `sch->hook = NULL;` after the state=DOWN assignment in ngmn_disconnect.

PoC changes

Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.

Verified recommended fix

Clear sch->hook = NULL in ngmn_disconnect after setting state=DOWN. Full diff in findings/poc/DF-1540/fix.diff.

Verdict

INCONCLUSIVE (HW-gated). Bug confirmed at source level: if_mn.c:475 ngmn_newhook sets sc->ch[chan]->hook=hook. ngmn_disconnect :758 sets sch->state=DOWN but NEVER clears sch->hook and never frees sc->ch[chan]. Netgraph core (ng_base.c:855-858) kfree(hook->name)+kfree(hook) immediately after disconnect returns. Later NGM_TEXT_STATUS iterates sc->ch[chan] (still non-NULL) and derefs sch->hook->name at :407 -> dangling pointer. mn(4) attaches to SigmaTel M32 E1/T1 PCI cards not on the audit guest.