Unserialized ISR/netgraph concurrency over per-channel descriptor rings: UAF writes, hook write-after-free, descriptor double-free, and device DMA into freed mbufs (ignored channel-zap status)
Summary
mn_intr is registered MPSAFE with a NULL serializer and the netgraph core invokes rcvdata/disconnect synchronously with no lock - three unsynchronized contexts mutate the same schan rings. The guards are only TOCTOU state reads against an unordered state=DOWN followed by full ring teardown: a raced ISR writes through freed-and-reused trxd->m, executes hook->refs++ on a core-freed hook, and both contexts push the same descriptors onto the free list (double free / alias). Independently of any race, disconnect only WARNS when the channel-zap handshake failed and frees the RX mbufs anyway - a still-armed MUNICH32X keeps DMA-ing E1 frames into freed clusters. Root + mn hardware; deterministic zap-failure path needs no timing. Serializer + poll-zap fix.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-4341 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 7.8 KB | β raw | |
| VERDICT.md | β | 4.8 KB | β raw | |
| trigger_race.sh | β | 1.5 KB | view raw | |
| run.sh | β | 244 B | view raw | |
| run.log | β | 296 B | view raw | |
| env_guest.txt | β | 569 B | view raw | |
| fix.diff | β | 4.0 KB | view raw | |
| verdict.json | β | 4.3 KB | view raw | |
| manifest.json | β | 1.1 KB | view raw |
DF-4341 β Unserialized ISR / netgraph concurrency over per-channel descriptor rings in if_mn.c β UAF, double-free, and device write-after-free
- File:
sys/dev/netif/mn/if_mn.c - Lines: 1329-1330 (INTR_MPSAFE, NULL serializer), 1074-1091 (mn_tx_intr walk), 1116-1176 (mn_rx_intr walk), 744-797 (ngmn_disconnect teardown), 599-631 (ngmn_rcvdata append), 1125 (ng_queue_data on sch->hook)
- Severity: Medium (hardware-gated: Siemens/Infineon "Easy321-R1" Munich32X+FALC54 PCI card 0x110a:2101; actor needs netgraph access, effectively root)
- CVSS:
CVSS:3.1/AV:L/AC:H/PR:H/UI:N/S:U/C:H/I:H/A:H - CWE: CWE-416 (UAF) / CWE-362 (race) / CWE-415 (double-free)
- Confidence: certain (statically provable absence of any synchronization; trigger needs hardware + timing)
Summary
All mutable per-channel state in the mn driver β the schan.state flag, the
software descriptor rings r1/rl (RX) and x1/xl (TX), the mbufs hanging off
trxd.m, and sch->hook β is touched concurrently by three unsynchronized
contexts:
- the hardware interrupt
mn_intrβmn_rx_intr/mn_tx_intr(if_mn.c:1329registersmn_intrwithINTR_MPSAFEand a NULL serializer β no crit_enter, no lock, no token); - arbitrary user threads sending data into the node:
ng_send_data()(sys/netgraph/netgraph/ng_base.c:1678-1697) invokes the peer node'srcvdatamethod synchronously in the sender's thread with no lock βngmn_rcvdata(if_mn.c:573-645) mutatesxl,dp->vnext,dp->flags,dp->m; - control threads tearing a hook down:
ng_disconnect_hook()(ng_base.c:839-858) invokesngmn_disconnect(if_mn.c:743-797) with no lock; it frees every descriptor on both rings to the globalmn_desc_freefreelist and everydp->mmbuf viam_freem.
The only "protection" is a TOCTOU if (sch->state != UP) return; check at the
top of mn_rx_intr/mn_tx_intr (if_mn.c:1109-1112, 1070-1073) β a plain
memory read with no fence, no lock, and no ordering against
ngmn_disconnect's sch->state = DOWN (if_mn.c:758), which executes
before the frees.
Root cause / race windows (path:line)
Window A β ISR vs ngmn_disconnect:
mn_rx_intr passes the state check at if_mn.c:1109 while state is still UP.
A netgraph thread runs ngmn_disconnect: sets state = DOWN (:758), then
frees the whole RX ring sc->ch[chan]->r1 β¦ (:779-784, mn_free_desc to the
global freelist :498-503, m_freem(dp->m)). The ISR resumes:
- dp = sch->r1 (:1117) β freed descriptor, typically already reused by
another channel's mn_alloc_desc (:486-496) since RX replenish runs in the
same ISR;
- m->m_pkthdr.len = m->m_len = (dp->status >> 16) & 0x1fff; (:1122) β
write through dp->m read out of freed/reused descriptor memory
(cross-channel mbuf corruption or wild pointer write β panic);
- ng_queue_data(sch->hook, m, NULL) (:1125) β sch->hook was freed by the
netgraph core right after ngmn_disconnect returned (ng_base.c:854-858)
β ng_queue_data executes hook->refs++ (ng_base.c:1946), a
write-after-free increment on freed M_NETGRAPH memory, and the raw
pointer is later dequeued and dereferenced by the netgraph softintr.
Identical exposure on TX: mn_tx_intr (:1074-1091) walks/frees x1 while
ngmn_disconnect frees the same chain (:787-794) β same descriptor freed
twice onto mn_desc_free β freelist cycle β two channels alias one descriptor.
Window B β ngmn_rcvdata vs mn_tx_intr / ngmn_disconnect:
ngmn_rcvdata (sender thread, :599-631) links new descriptors at xl,
clears dp2->flags &= ~0x40000000 on the old tail (:626), while the ISR's
mn_tx_intr concurrently frees from the x1 head (:1089-1090). A
descriptor freed by the ISR between dp2 = sc->ch[chan]->xl (:597) and the
link writes (:620-622) is written after free and later freed a second time
by ngmn_disconnect (:787-794) β double free / ring corruption. Also
tx_pending -= m->m_pkthdr.len (:1085) double-decrements on raced
processing β u_long underflow, permanently wedging the
tx_pending > tx_limit * mn_maxlatency gate (:590).
Window C β device write-after-free (no concurrency needed, failure-ignored
handshake): ngmn_disconnect zaps the channel by issuing
sc->m32x->cmd = 0x1 and waiting DELAY(30) (:770-772); if the device did
NOT complete (!(u & 1)) it only prints a warning and frees the RX ring
anyway (:773-784). A MUNICH32X still armed on that channel keeps DMA-ing
received E1 frames (1600-byte bursts) into the freed mbuf clusters and writing
completion status into freed descriptors β hardware write-after-free.
Threat
Preconditions: the Easy321-R1 card present and driver attached (node mn0),
live E1 traffic, and an actor that can create/destroy hooks on mn0
(PF_NETGRAPH control socket β DF-1539 established the privilege bar is
SYSCAP_RESTRICTEDROOT). Under continuous RX/TX interrupts (E1 line carrying
frames), repeated ngctl connect β¦ ; ngctl rmhook β¦ cycles hit Window A/B
with realistic probability; Window C fires whenever the 30 Β΅s zap deadline is
missed. Impact: kernel heap corruption (mbuf zone, M_MN descriptor pool,
netgraph hook/node structs) β panic or, with the freelist-alias primitive,
potential arbitrary write; plus E1-payload writes into freed kernel pages.
Exploit / PoC
trigger_race.sh β the exact userland sequence (run as root on real
hardware with an active E1 line):
# start line traffic (any HDLC peer) then:
while :; do
ngctl mkpeer mn0: echo ts2 downstream # ngmn_newhook + connect
dd if=/dev/zero bs=2048 count=64 | ngctl sendfn ... # pump TX
ngctl rmhook mn0: ts2 # races mn_intr / ngmn_rcvdata
done
Success criterion: Fatal trap 12/10 in mn_rx_intr/mn_tx_intr
(panic.txt), or freelist-alias corruption (duplicate descriptors observed
via NGM_TEXT_STATUS channel counters running together).
This could not be executed on the audit guest β no Munich32X PCI device
(pciconf -l | grep 110a empty) and if_mn is optional mn netgraph, absent
from GENERIC and not built as a module (kldload if_mn β ENOENT). See
VERDICT.md.
Recommended fix
Serialize all driver contexts against the interrupt: take a serializer at
bus_setup_intr time and enter it in every netgraph method, or convert to a
lock + explicitly quiesce the channel (with a real timeout + error return)
before freeing rings. Minimum defensible patch (see fix.diff): pass a
serializer to bus_setup_intr and lwkt_serialize_enter/exit it in
ngmn_connect, ngmn_disconnect, ngmn_rcvdata, ngmn_rcvmsg, plus bail
out of the zap path when the device refuses.
--- a/sys/dev/netif/mn/if_mn.c
+++ b/sys/dev/netif/mn/if_mn.c
@@ static int
ngmn_disconnect(hook_p hook)
{
int chan, i;
struct softc *sc;
struct schan *sch;
struct trxd *dp, *dp2;
u_int32_t u;
sch = hook->private;
chan = sch->chan;
sc = sch->sc;
+ lwkt_serialize_enter(sc->slicer); /* block mn_intr */
if (sch->state == DOWN)
- return (0);
+ goto out;
sch->state = DOWN;
...
sc->m32x->cmd = 0x1;
- DELAY(30);
- u = sc->m32x->stat;
- if (!(u & 1))
- kprintf("%s: zap chan %d stat %08x\n", sc->name, chan, u);
+ for (i = 0; i < 1000; i++) { /* poll, don't guess */
+ DELAY(30);
+ u = sc->m32x->stat;
+ if (u & 1)
+ break;
+ }
+ if (!(u & 1)) {
+ kprintf("%s: zap chan %d FAILED stat %08x\n", sc->name, chan, u);
+ sch->state = UP; /* keep DMA targets alive */
+ goto out; /* DO NOT free rings */
+ }
sc->m32x->stat = 1;
... free rings ...
+out:
+ lwkt_serialize_exit(sc->slicer);
sc->nhooks--;
return(0);
}
(with error = bus_setup_intr(self, sc->irq, INTR_MPSAFE, mn_intr, sc,
&sc->intrhand, sc->slicer) and matching enter/exit in ngmn_rcvdata,
ngmn_connect, ngmn_newhook, ngmn_rcvmsg.)
DF-4341 VERDICT
Status: inconclusive on this guest (finding stands on static proof) reproduced: 0 Β· impact on this guest: none Β· confidence in the finding: certain
What the finding claims
Three unsynchronized execution contexts mutate the same per-channel state in
sys/dev/netif/mn/if_mn.c:
| Context | Entry | Evidence of no synchronization |
|---|---|---|
| HW interrupt | bus_setup_intr(self, sc->irq, INTR_MPSAFE, mn_intr, sc, &sc->intrhand, NULL) β if_mn.c:1329-1330 |
7th arg (serializer) is NULL; mn_rx_intr/mn_tx_intr take no lock, no crit section (if_mn.c:1061-1092, 1097-1177) |
| Data sender thread | ng_send_data() calls rcvdata synchronously in the caller's thread β sys/netgraph/netgraph/ng_base.c:1678-1697 |
ngmn_rcvdata (if_mn.c:573-645) mutates xl/dp->vnext/dp->m lock-free |
| Hook-teardown thread | ng_disconnect_hook() β type->disconnect(hook) lock-free β ng_base.c:839-858 |
ngmn_disconnect (if_mn.c:743-797) frees both rings + mbufs, then the core frees the hook itself (ng_base.c:854-858) |
The only guard is the TOCTOU sch->state != UP read (if_mn.c:1070,
:1109) against sch->state = DOWN at if_mn.c:758 β unordered, unlocked.
Consequences (each traced to a sink):
- UAF write: m->m_pkthdr.len = m->m_len = (dp->status >> 16) & 0x1fff
(if_mn.c:1122) writes through dp->m read from a freed-and-reused
descriptor (the global freelist mn_desc_free, if_mn.c:483-503, recycles
them across channels inside the same ISR).
- UAF write on freed hook: ng_queue_data(sch->hook, m, NULL)
(if_mn.c:1125) after the core freed the hook.
- Double free / freelist alias: mn_tx_intr (if_mn.c:1089-1090) and
ngmn_disconnect (if_mn.c:787-794) both push the same trxd onto
mn_desc_free; ngmn_rcvdata (if_mn.c:620-626) links/writes descriptors
the ISR may already have freed.
- HW write-after-free without any race: ngmn_disconnect only warns
when the 30 Β΅s channel-zap did not complete (if_mn.c:773-776) and then
frees the RX mbufs anyway (:779-784) β an still-armed MUNICH32X keeps
DMA-ing E1 frames into freed clusters.
- NULL-deref amplification: once the ring pointers are corrupted,
dp = sc->ch[chan]->x1 can be NULL in the drain loop and
vtophys(dp)==ctxd fails, so m = dp->m (if_mn.c:1078) faults on NULL.
Verification attempt on the guest
Guest: DragonFly 6.5-DEVELOPMENT #0 ... X86_64_GENERIC (see env_guest.txt).
pciconf -l | grep 110aβ no Munich32X device present./boot/kernel/if_mn.koβ not built;kldload if_mnβ ENOENT.grep dev/netif/mn /usr/src/sys/conf/filesβdev/netif/mn/if_mn.c optional mn netgraphβ the driver is not in GENERIC (device mnabsent), so the audit kernel cannot instantiate it.- Even with a hand-built module,
mn_probe(if_mn.c:1252-1275) matches only PCI ID0x2101110aand returns ENXIO otherwise, so no softc, nong_newtype(if_mn.c:1287-1291), and nomn0node can ever exist on this guest.
Therefore the trigger (trigger_race.sh) cannot be executed: the target
device/feature is absent. The honest classification for this run is
inconclusive (not not_reproduced β nothing was refuted; the code-level
absence of synchronization is a static fact, and every sink above is cited).
Why the finding is still real
- The netgraph core provably calls driver methods with no lock
(
ng_base.c:1678-1697,:839-858). - The driver provably registers its ISR with no serializer (
if_mn.c:1329). - The driver provably frees the exact objects the ISR walks
(
if_mn.c:779-794vs:1075-1090,:1117-1150). - Sibling findings DF-1539/1540/1541 already established this node type's
surface is reachable by a
SYSCAP_RESTRICTEDROOTactor when the hardware exists.
Fix validation
Not performable on this guest (no hardware, driver not in the build). The
fix.diff in this pack is the reviewed, line-accurate patch: serializer
wiring + polled zap with refusal path. It applies cleanly to the read-only
sys/ tree (git apply --check verified on the host clone).
Files
README.mdβ full finding write-uptrigger_race.shβ the userland trigger (needs real hardware + E1 traffic)run.shβ what would be run, and what was actually run on the guestenv_guest.txt/run.logβ guest environment + verification-session logfix.diffβ git-apply-able fix (not applied tosys/)verdict.json/manifest.json
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_testableNo baseline can be established on a guest without the Munich32X hardware; fix.diff is line-accurate against sys/dev/netif/mn/if_mn.c (git apply --check clean) and covers every cited sink: serializer shared by ISR and all ngmn_* methods; polled zap with leak-on-failure instead of free-under-armed-device.
findings/poc/DF-4341/fix.diff (git apply --check OK on the read-only tree)
Confirmed kernel references
- sys/dev/netif/mn/if_mn.c:1329
- sys/dev/netif/mn/if_mn.c:1074
- sys/dev/netif/mn/if_mn.c:1109
- sys/dev/netif/mn/if_mn.c:1117
- sys/dev/netif/mn/if_mn.c:1122
- sys/dev/netif/mn/if_mn.c:1125
- sys/dev/netif/mn/if_mn.c:743
- sys/dev/netif/mn/if_mn.c:758
- sys/dev/netif/mn/if_mn.c:773
- sys/dev/netif/mn/if_mn.c:779
- sys/dev/netif/mn/if_mn.c:787
- sys/dev/netif/mn/if_mn.c:599
- sys/dev/netif/mn/if_mn.c:620
- sys/dev/netif/mn/if_mn.c:483
- sys/netgraph/netgraph/ng_base.c:1678
- sys/netgraph/netgraph/ng_base.c:839
- sys/netgraph/netgraph/ng_base.c:1946
Detail
Exploit chain
on real hardware: root ngctl loop { mkpeer mn0 ksocket ts2 ...; rmhook mn0 ts2 } while E1 traffic drives RX/TX interrupts -> mn_rx_intr/tx_intr race ngmn_disconnect ring teardown -> freed trxd reused via mn_desc_free freelist -> cross-channel write through stale dp->m (if_mn.c:1122) / double-free -> M_MN pool corruption -> panic or freelist-alias arbitrary-write primitive. Not executable on this guest (no device).
Evidence (decisive lines)
VERDICT.md (race-window table + sinks); env_guest.txt + run.log (guest: no 110a PCI device, no if_mn.ko, optional-not-GENERIC); fix.diff (serializer wiring + polled zap, git apply --check OK); README.md full write-up
PoC changes
trigger is an ngctl connect/rmhook loop written fresh for this pack (no seed); guest-mode runner documents surface absence
Verified recommended fix
Register a lwkt serializer with bus_setup_intr and hold it across all ngmn_* methods; poll the channel-zap handshake in ngmn_disconnect and refuse to free rings on failure (fix.diff in this pack)
Verdict
Could not be exercised on the audit guest: the mn(4) driver requires the Siemens Easy321-R1 Munich32X+FALC54 PCI card (0x110a:2101), which QEMU does not emulate; if_mn is 'optional mn netgraph', absent from GENERIC, and if_mn.ko is not built (kldload -> ENOENT), so the mn netgraph node type can never register (ng_newtype runs only in mn_attach). The finding itself is statically certain: mn_intr is registered INTR_MPSAFE with a NULL serializer (if_mn.c:1329), the netgraph core invokes ngmn_rcvdata synchronously in sender threads with no lock (ng_base.c:1678-1697) and ngmn_disconnect in teardown threads with no lock (ng_base.c:839-858), and ngmn_disconnect (if_mn.c:779-794) frees the exact descriptor rings and mbufs that mn_rx_intr/mn_tx_intr walk (if_mn.c:1075-1090, 1117-1150) behind a bare TOCTOU state check -- plus it frees RX rings even when the 30us channel-zap handshake failed (if_mn.c:773-784), leaving the device DMA-ing into freed memory with no race required. Net effect on real hardware: UAF writes (if_mn.c:1122 via reused trxd->m), UAF on the core-freed hook (if_mn.c:1125), descriptor double-free onto mn_desc_free, and E1-payload hardware writes into freed mbuf clusters. Classification 'inconclusive' (not 'not_reproduced') because nothing was refuted -- the surface simply cannot exist on this guest.
No comments yet.