nm_bdg_preflush: unbounded NS_MOREFRAG fragment chains overrun the nkr_ft forwarding table β linear heap OOB write with attacker-chosen pointer values, u_int-wrap wild write ~4GiB above the object, and uint8 ft_frags truncation livelock
Summary
The bridge_batch flush check is evaluated only when a packet completes (slot without NS_MOREFRAG); fragment slots 'continue' past it, so ft_i grows once per slot up to the full ring size (up to 4096) while nkr_ft is sized for 1088 entries (52104-byte kmalloc). slot->flags is user-writable shared memory never validated (netmap_ring_reinit checks cur/buf_idx/len only). VERIFIED in-kernel on stock INVARIANTS guest with a verbatim-function KLD: 13418 attacker-data bytes written past the ft[] contract starting at ft[1088]; with the verbatim allocation writes land past the 52104-byte object (up to 13416B at 4096 slots) silently; all-MOREFRAG batches make the post-loop ft[ft_i - frags] store evaluate index -1 in 32-bit unsigned - observed Fatal trap 12 writing at ft+0xFFFFFFF8 (the :1013 store); a completed 256-frag chain truncates into the uint8_t ft_frags as 0 so nm_bdg_flush's pass-1 walk never advances (livelock). Full linear heap overflow with ~10 attacker-chosen bytes plus an arbitrary 64-bit pointer per 16-byte entry at attacker-chosen indices. Any user able to open /dev/netmap (0660; same precondition class as DF-0401) registering a VALE port with large nr_tx_slots; in-tree code builds on FreeBSD-lineage systems. uid=0 not developed (subsystem cannot compile on this platform - documented hard blocker); fixed-variant A/B clean (cap chains at NM_MULTISEG).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3368 Β· 12 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.7 KB | β raw | |
| VERDICT.md | β | 5.6 KB | β raw | |
| df3368_kmod.c | β | 17.3 KB | view raw | |
| Makefile | β | 145 B | β download | |
| build.sh | β | 129 B | view raw | |
| run.sh | β | 191 B | view raw | |
| build.log | β | 913 B | view raw | |
| run.log | β | 3.9 KB | view raw | |
| run.crash.log | β | 2.8 KB | view raw | |
| env.txt | β | 221 B | view raw | |
| fix.diff | β | 606 B | view raw | |
| verdict.json | β | 4.8 KB | view raw |
DF-3368 β nm_bdg_preflush() unbounded NS_MOREFRAG chains: heap OOB write, +4 GiB wild write, uint8 livelock
Target: sys/net/netmap/netmap_vale.c:984-1016 (nm_bdg_preflush, reached from
NIOCTXSYNC β bdg_netmap_txsync β netmap_vp_txsync:1448 on every VALE port),
with sinks at netmap_vale.c:1012-1013 (wild/underflow write) and
netmap_vale.c:1161 (nm_bdg_flush pass-1 walk livelock).
Not a re-report of DF-0401 (that is the destination copy size check at :1330-1345); this is the forwarding-table index overrun in the fill loop.
The bug
The bridge_batch flush check is only evaluated when a packet completes
(a slot without NS_MOREFRAG); fragment slots continue past it
(netmap_vale.c:998-1001). slot->flags lives in user-writable shared memory
and is never validated (netmap_ring_reinit, netmap.c:1106, checks only
cur/buf_idx/len). Marking all slots NS_MOREFRAG lets ft_i
grow once per slot up to the full ring size β up to NM_BDG_MAXSLOTS = 4096
(bound set at port creation, netmap_vale.c:595-600) β while nkr_ft is sized
for NM_BDG_BATCH_MAX = 1088 entries (nm_alloc_bdgfwd, netmap_vale.c:401-403,
52104-byte kmalloc):
ft[ft_i]stores at indices β₯ 1088 β offsets β₯ 17408 (intodst_ents/dsts), and beyond index 3256 β offsets β₯ 52104, i.e. past the end of the heap object (up to 13416 bytes past at 4096 slots). Every 16-byte entry carries two attacker-chosen u16s (ft_len=slot->len,ft_flags=slot->flags) and, withNS_INDIRECT, an arbitrary attacker-chosen 64-bit pointer (ft_buf=slot->ptr, netmap_vale.c:994-995).- post-loop
ft[ft_i - frags].ft_frags = frags - 1;(netmap_vale.c:1013) withfrags == ft_i + 1evaluates index β1 in 32-bit unsigned β wild write at ft + 0xFFFFFFF8 (~4 GiB above the object), byte value(ring_size-1) & 0xff. - a completed 256-fragment chain stores
fragsinto the uint8_tft_fragsβ 256 & 0xff == 0 βnm_bdg_flushpass-1 walkfor (i=0; i<n; i+=ft[i].ft_frags)never advances β non-terminating loop in netisr context (fully in-bounds; needs only 257 slots). (NM_MULTISEG=64, "max size of a chain of bufs", netmap_vale.c:124, is documented but enforced nowhere.)
Platform note (established, netmap.c pass-2)
The netmap subsystem does not compile on this DragonFly tree
(netmap_kern.h:747 references ifnet.if_unused7, removed from if_var.h; no
sys/conf/files entry; no netmap.ko in /boot). The code ships in-tree and
builds on FreeBSD-lineage systems where netmap is enabled. Runtime proof here
is therefore done the established way: df3368_kmod.c copies
nm_bdg_preflush verbatim (stubbed lock/prefetch/BDG_NMB plumbing that
cannot influence ft_i) into a KLD and drives it with exactly the ring state
an mmap()ed user thread produces.
Build / run (guest, root)
cd /root/df3368 && make && make fixed # or ./build.sh pattern ./run.sh vuln 0 # control: 8 packets, canary intact, walk terminates ./run.sh vuln 1 # ft[0].ft_frags == 0 -> pass-1 walk STUCK at i=0 ./run.sh vuln 2 # 13418 canary bytes corrupted past ft[] contract @ft[1088].. ./run.sh vuln 3 # real kmalloc(52104): writes 680 B PAST the object (survives) # all-MOREFRAG variant: Fatal trap 12, fault VA = ft+0xFFFFFFF8, at the # netmap_vale.c:1013 store (see run.crash.log) ./run.sh fixed 1|2|3 # max index 1023 <= 1088, canary intact, walk terminates
Console output goes to the serial console; capture with dmesg | grep df3368
or dfbsd-qemu/vm.sh log.
Expected results
See run.log (verbatim captured lines) and run.crash.log (the wild-write
panic). fix.diff caps frags at NM_MULTISEG β validated A/B with the
fixed variant in the same module.
DF-3368 β VERDICT
Status: reproduced (memory-corruption primitive demonstrated in-kernel at every sink; impact observed: heap corruption + kernel panic; escalation to uid=0 blocked on this platform by the netmap build rot, documented below).
What was proven, on the guest, in a real kernel
All with the verbatim nm_bdg_preflush from sys/net/netmap/netmap_vale.c:961-1019
running as a KLD on the stock INVARIANTS kernel #0 (full console transcript in
run.log, wild-write crash in run.crash.log):
-
Forwarding-table overrun with attacker data (deterministic). 2047-slot all-fragment chain:
max table index written: 2046, OVER NM_BDG_BATCH_MAX=1088; canary scan: 13418 bytes corrupted past the ft[] contract, first @ft+17408 (ft[1088]). Each entry wroteft_len=slot->len(attacker u16),ft_flags=slot->flags(attacker u16),ft_next=NM_FT_NULL,ft_buf=slot->ptr(arbitrary attacker 64-bit pointer via NS_INDIRECT). -
Writes past the end of the real heap object. mode 3 with the verbatim
nm_alloc_bdgfwdsizing (kmalloc(52104), netmap_vale.c:401-403,412):max table index written: 3298 ... byte offset 52784 PAST alloc size 52104β 680 bytes past the object in the bounded test (ring cap 4096 β up to 13416 bytes past). The guest survived this run: silent corruption of whatever follows the object in the heap. -
Wild write ~4 GiB above the object (kernel panic). all-MOREFRAG batch β post-loop
ft[ft_i - frags].ft_frags = frags - 1;(netmap_vale.c:1013) withft_i - frags == -1in 32-bit unsigned:Fatal trap 12: page fault ... fault virtual address = 0xfffff81118640038= ft(0xfffff80118640040) + 0xFFFFFFF8,Stopped at df3368_sysctl+0x401: movb %dl,0x8(%r8,%rax,1)β the verbatim store instruction itself. Byte value =(ring_size-1)&0xff(attacker picks ring size 1..4096). -
uint8_t ft_frags truncation livelock (in-bounds, 257 slots only). 255ΓNS_MOREFRAG + 1 terminator = a completed 256-frag packet:
mode1 ft[0].ft_frags = 0 (256 truncated to uint8_t)βpass-1 walk STUCK at i=0 (ft[i].ft_frags=0, iters>100000)β the exactnm_bdg_flushpass-1 headfor (i=0; i<n; i+=ft[i].ft_frags)(netmap_vale.c:1161) never advances. Non-terminating loop in netisr context on the real subsystem.
Root cause, path:line
nm_bdg_preflush netmap_vale.c:998-1001: NS_MOREFRAG slots continue
past the only batch-limit check at :1006 ((int)ft_i >= bridge_batch),
and nothing enforces the documented NM_MULTISEG=64 chain cap
(netmap_vale.c:124). ft_i therefore runs to the full ring size
(β€ NM_BDG_MAXSLOTS=4096, netmap_vale.c:595-600) against a table sized
NM_BDG_BATCH_MAX=1088 (netmap_vale.c:401-403). The truncation at
:1004 (ft[ft_i-frags].ft_frags = frags, uint8_t) and the unterminated-chain
store at :1013 follow from the same missing cap.
Why not uid=0 (documented hard blocker, platform rule)
The netmap subsystem does not compile on this DragonFly tree
(netmap_kern.h:747 if_unused7 bit-rot; no conf/files entry; no netmap.ko;
no /dev/netmap) β established platform fact from the netmap.c pass-2
(task_log 2026-09-11 01:03). Exploitation on this guest is unreachable:
there is no device node to open, no module to load. The primitive itself is
nevertheless strong and fully characterized above (linear heap overflow,
~10 attacker-controlled bytes + arbitrary 64-bit pointer per 16-byte slot at
attacker-chosen indices; plus a semi-controlled +4 GiB single-byte wild
write): on any system where this in-tree code builds (FreeBSD lineage of the
same r257176 import), it is a textbook slab-groom β victim-object overwrite
β code-path pivot candidate. Verified here as far as the platform permits.
PoC changes vs the seeded sketch
None (finding authored in this pass; harness written fresh following the
DF-3332 verbatim-KLD pattern). Two harness-side (not verbatim-code) bugs
were fixed during iteration and are documented in build.log: a u_int k-64
underflow in the module's own canary scan, and a scratch kfree before the
mode-0/1 walkers. The captured wild-write panic (run.crash.log) occurred in
the v2 harness; the faulting instruction is the verbatim :1013 store.
Fix validation (A/B, same module)
-DDD3368_FIXED caps chains at NM_MULTISEG (see fix.diff for the tree
patch, authored against the read-only sys/ tree, never applied there):
| mode | vulnerable | fixed |
|---|---|---|
| 1 (256-frag) | ft[0].ft_frags=0, walk STUCK | ft[0].ft_frags=64, walk 4 steps |
| 2 (2047) | idx 2046 OVER, 13418 canary bytes | idx 1023 β€ 1088, 0 canary bytes |
| 3 (3299) | offset 52784 PAST 52104 | offset 16384 β€ 52104, flushΓ4 |
A full-subsystem patched-kernel build is impossible on this platform for the same build-rot reason; the verbatim A/B is the maximal validation available.
Kernel refs: sys/net/netmap/netmap_vale.c:984-1016 (loop), :994-995 (attacker ft_buf), :1004 (u8 truncation), :1006 (batch check), :1012-1013 (wild write), :1161 (flush walk), :119 (NM_BDG_MAXRINGS), :124 (NM_MULTISEG "max size of a chain of bufs"), :401-403/:412 (allocation), :595-600 (slot bound), :1448/:1471-1475 (reachability); sys/net/netmap/netmap.c:1106-1133 (netmap_ring_reinit validates cur/buf_idx/len β not flags).
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
fixedfix.diff caps fragment chains at NM_MULTISEG in nm_bdg_preflush. Validated A/B with the -DDD3368_FIXED variant of the same verbatim module: mode1 ft[0].ft_frags=64, walk terminates (4 steps); mode2/mode3 max index 1023 <= 1088, canary intact, flush invoked every batch - all three sinks (table overrun, past-object write, livelock) eliminated.
run.log fixed-variant section (last 8 lines); fix.diff
Confirmed kernel references
- sys/net/netmap/netmap_vale.c:984-1016
- sys/net/netmap/netmap_vale.c:994-995
- sys/net/netmap/netmap_vale.c:1004
- sys/net/netmap/netmap_vale.c:1006
- sys/net/netmap/netmap_vale.c:1012-1013
- sys/net/netmap/netmap_vale.c:1161
- sys/net/netmap/netmap_vale.c:124
- sys/net/netmap/netmap_vale.c:401-403
- sys/net/netmap/netmap_vale.c:595-600
- sys/net/netmap/netmap_vale.c:1448
- sys/net/netmap/netmap.c:1106-1133
Detail
Exploit chain
open /dev/netmap -> NIOCREGIF vale port with nr_tx_slots up to NM_BDG_MAXSLOTS=4096 -> mmap ring -> set slot[j].flags=NS_MOREFRAG|NS_INDIRECT, slot[j].ptr=arb, slot->len=arb, ring->cur=ring-1 -> ioctl(NIOCTXSYNC) -> bdg_netmap_txsync -> netmap_vp_txsync -> nm_bdg_preflush: linear heap overflow past nkr_ft (10 controlled bytes + arbitrary 64-bit pointer per 16B entry, index fully chosen), plus post-loop +4GiB wild byte-write, plus flush livelock. Escalation on this guest: blocked (subsystem dead code here).
Evidence (decisive lines)
run.log: 'max table index written: 2046, OVER NM_BDG_BATCH_MAX=1088' + 'CANARY bytes corrupted past ft[] contract: 13418, first @ft+17408 (ft[1088])'; mode 3: 'byte offset 52784 PAST alloc size 52104' on a real kmalloc(52104); run.crash.log: 'Fatal trap 12 ... fault virtual address = 0xfffff81118640038' (= ft+0xFFFFFFF8) 'Stopped at df3368_sysctl+0x401: movb %dl,0x8(%r8,%rax,1)' = netmap_vale.c:1013 store; 'mode1 ft[0].ft_frags = 0 (256 truncated to uint8_t)' + 'pass-1 walk STUCK at i=0'; fixed-variant A/B in run.log (idx<=1023, canary 0, walk terminates).
PoC changes
finding authored this pass; harness written fresh (DF-3332 verbatim-KLD pattern). Two harness-side bugs fixed during iteration (own canary-scan u_int underflow; scratch kfree before walkers) - documented in build.log; verbatim function untouched.
Verified recommended fix
Cap fragment chains at NM_MULTISEG in nm_bdg_preflush (see fix.diff): treat slot as chain-terminating once frags == NM_MULTISEG.
Verdict
nm_bdg_preflush (netmap_vale.c:984-1016) only batch-limits ft_i at packet completion, so NS_MOREFRAG chains (user-writable slot->flags, never validated) run ft_i to the full ring size (<=4096) against the 1088-entry nkr_ft table (52104-byte kmalloc). Proven in-kernel with the verbatim function in a KLD on the stock INVARIANTS kernel: (1) 13418 attacker-data bytes written past the ft[] contract at ft[1088..2046]; (2) with the verbatim nm_alloc_bdgfwd allocation, writes 680 bytes PAST the heap object at ring=3300 (up to 13416 past at 4096), guest surviving = silent corruption; (3) all-MOREFRAG batches make netmap_vale.c:1013 store ft[ft_i-frags].ft_frags with index -1 (u_int) -> wild write at ft+0xFFFFFFF8, observed Fatal trap 12 at exactly that store; (4) a 256-frag chain truncates into uint8_t ft_frags=0 so nm_bdg_flush's pass-1 walk (netmap_vale.c:1161) never advances - proven livelock. Each entry carries attacker-chosen u16 len/flags and, via NS_INDIRECT, an arbitrary 64-bit pointer (ft_buf=slot->ptr). uid=0 chain not developed: netmap does not compile on this platform (netmap_kern.h:747 if_unused7 rot; no module, no /dev/netmap) - exploitation unreachable on this guest, documented as the hard blocker; the primitive is fully characterized for platforms where the in-tree code builds.
No comments yet.