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:
- connect a hook on
mn0(e.g.mkpeer mn0 echo ts2 downstream) sosc->ch[2]->hookpoints to a live hook; - disconnect it (
rmhook mn0 ts2/ng_shutdownof the peer) βngmn_disconnectruns, hook is freed by the core; - issue
NGM_TEXT_STATUSonmn0β the loop visitssc->ch[2](non-NULL), dereferences the freed hook, and treats the first qword of the freed/reused hook struct as achar *namepointer, 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.
Recommended fix
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;
Related findings
- DF-1539 (sibling): heap overflow in same file's
NGM_TEXT_STATUShandler. - DF-1541 (sibling): OOB read in
NGM_TEXT_CONFIG.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-1540 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.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
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 4
- 7
- 5
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 7
- 5
- 8
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- m
- n
- /
- i
- f
- _
- m
- n
- .
- c
- :
- 4
- 0
- 7
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.
No comments yet.