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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3368 Β· 12 files
FileTypeDescriptionSize
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 (into dst_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, with NS_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) with frags == ft_i + 1 evaluates 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 frags into the uint8_t ft_frags β†’ 256 & 0xff == 0 β†’ nm_bdg_flush pass-1 walk for (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.

VERDICT.md
↓ download raw

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):

  1. 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 wrote ft_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).

  2. Writes past the end of the real heap object. mode 3 with the verbatim nm_alloc_bdgfwd sizing (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.

  3. 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) with ft_i - frags == -1 in 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).

  4. 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 exact nm_bdg_flush pass-1 head for (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

fixed
baseline reproduced→ patch + rebuild →patched clean

fix.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
↓ fix.diffn/a (verbatim-function KLD A/B on stock kernel #0; netmap subsystem does not compile on this platform so a full patched-kernel build is impossible)

Confirmed kernel references

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.