ifnet locking-contract violation and stored-pointer UAF on member ifnets
Summary
if_var.h:894-896 mandates ifunit() requires ifnet_lock held + returned ifp only accessed under lock. ng_fec_addport/delport call ifunit(:370,:481) NO ifnet_lock, store ifp in fec_portlist->fec_if(:450). Deref indefinitely from ng_fec_tick(callout :687-691 1Hz), ng_fec_input(netisr RX :885-888), ng_fec_start(tx :1066-1095). If member iface detached/destroyed -> ifnet freed+reused -> UAF via stored fec_if. 1Hz callout makes race practical. Fix: ifunit_ref or ifnet_departure eventhandler + ifnet_lock around derefs.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0503 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| df0503.c | trigger-source | scaffolding for ng_fec bundle setup + member destroy (needs working constructor) | 3.5 KB | view raw |
| build.sh | build-script | cc -O2 -o df0503 df0503.c | 98 B | view raw |
| run.sh | run-script | prints reachability summary; see VERDICT.md | 980 B | view raw |
| VERDICT.md | verdict | code-path analysis, constructor-bug blocker, fix rationale | 4.3 KB | β raw |
| fix.diff | suggested-fix | ifnet_lock around ifunit + ifnet_detach_event handler + NULL guards at deref sites | 3.5 KB | view raw |
| panic.txt | panic-signature | separate ng_fec_constructor NULL-kfree panic (blocks the DF-0503 live trigger) | 1.1 KB | view raw |
| env.txt | environment | uname + cc version | 188 B | view raw |
| ../fix_build_combined.log | build-log | Combined 41-finding kernel build (rc=0, -Werror clean) | 5.6 MB | β download |
| ../fix_build_summary.txt | build-summary | Summary of the combined 41-finding kernel build | 826 B | view raw |
DF-0503 β ng_fec stored-pointer UAF on member ifnets
Verdict: CODE-PATH CONFIRMED; runtime UAF trigger blocked by a separate constructor bug
Mechanism (confirmed at code level)
ng_fec_addport in sys/netgraph7/ng_fec.c:349-457 looks up a
member interface via ifunit(iface) at line 370 with no
ifnet_lock held, then stores the returned struct ifnet * in
new->fec_if at line 450. ng_fec_delport (line 460-518) repeats
the pattern at line 481.
sys/net/if_var.h:894-896 explicitly mandates:
ifunit() must only be called in non-netisr threads and ifnet lock must be held before calling this function and for the accessing of the ifp returned by this function.
The stored fec_if pointer is dereferenced indefinitely from three
runtime paths:
ng_fec_tick(1 Hz callout, lines 687-691):ifp = p->fec_if; (*ifp->if_ioctl)(ifp, SIOCGIFMEDIA, ...)β derefsifp->if_ioctl.ng_fec_input(netisr RX path, lines 885-888): TAILQ_FOREACH comparesp->fec_if == m0->m_pkthdr.rcvif.ng_fec_start/ng_fec_choose_port(TX path, lines 1066-1095): TAILQ_FOREACH +*ifp = p->fec_if(writes the chosen port's ifp).
If a member interface is later detached/destroyed (ifconfig destroy,
hot-unplug), the kernel frees the struct ifnet and may reuse the
slab. The next 1 Hz ng_fec_tick dereferences the dangling pointer,
producing a UAF read (and indirect function call through if_ioctl).
Runtime trigger attempt
A root-driven attempt to set up a fec bundle via ngctl mkpeer on
the test guest instead hit a separate latent bug in
ng_fec_constructor β a panic: trying to free NULL pointer in
_kfree() called from ng_fec_constructor+0x3ae. This blocks the
DF-0503 trigger path entirely: you can't construct a fec node to add
member interfaces to, because the constructor itself panics on the
malformed hook-name parsing.
panic: trying to free NULL pointer cpuid = 0 _kfree() at _kfree+0x558 _kfree() at _kfree+0x558 ng_fec_constructor() at ng_fec_constructor+0x3ae ng_mkpeer() at ng_mkpeer+0x4c ng_generic_msg() at ng_generic_msg+0x415 ng_send_msg() at ng_send_msg+0xf1
(Saved in panic.txt. This is a different bug worth a separate
finding; it does at least confirm that the ng_fec module is exercised
and the DF-0503 code paths are reachable in principle β just not
constructible on this kernel without first fixing the constructor
bug.)
Reachability β root-only throughout
ng_fecmodule load:kldload(root)- bundle configuration:
ngctl mkpeer/msg(root β netgraph control socket issocket(AF_NETGRAPH), root-only) - member interface detach:
ifconfig destroy(root) - UAF then fires automatically from
ng_fec_tick(1 Hz callout)
So this is a root-configured, root-triggered UAF β a kernel hardening gap, not an unprivβroot vector. The realistic scenario: an admin has set up a fec bundle of N member interfaces and later detaches one (USB NIC hot-unplug, virtual NIC removal). The bundle continues to deref the freed ifnet.
The v1 twin in sys/netgraph/fec/ng_fec.c (the actually-loadable
ng_fec.ko) has the same defect at lines 345, 408, 433 (ifunit), 490,
525, 551, 581, 764, 974 (deref sites).
Fix
fix.diff:
- Wraps the
ifunit()calls inifnet_lock()/ifnet_unlock()in bothng_fec_addport(line 370) andng_fec_delport(line 481), satisfying theif_var.hcontract. - Adds a global
ifnet_detach_eventhandler (ng_fec_ifnet_detach_event) that walks every existing fec bundle on interface departure and NULLs out any matchingp->fec_ifpointer. - Adds
if (p->fec_if == NULL) continue;/ skip guards in the deref sites (ng_fec_tickat :687 andng_fec_choose_portat :1066) so they tolerate the now-possible NULL. - Registers/deregisters the event handler in
ng_fec_mod_event.
The v1 module would need the same fix applied to
sys/netgraph/fec/ng_fec.c.
Build / run
ssh dfbsd 'kldload ng_fec ng_socket if_tap' # root setup ssh dfbsd 'ifconfig tap0 create && ifconfig tap1 create && ifconfig tap0 up && ifconfig tap1 up' ssh dfbsd 'ngctl mkpeer fec fec0 inet/inet/0' # may hit the constructor bug # Then configure the bundle, add tap0/tap1 as members, bring it up, # and `ifconfig tap0 destroy` to trigger the UAF via ng_fec_tick.
Fix verification
not_testablecompile validated
see evidence pack
Confirmed kernel references
β
Detail
Exploit chain
none
Evidence (decisive lines)
β
Verdict
Source-confirmed. ng_fec ifunit no ifnet_lock + stored ifp UAF. Separate constructor panic blocks live trigger. ng7 not built by default.
No comments yet.