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

Heap buffer overflow via unchecked slot->len in VALE bridge forwarding: pkt_copy up to 65536 bytes into 2048-byte buffer

Field Value
ID DF-0401
Status new
Severity High
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:C/C:H/I:H/A:H
CWE CWE-122 Heap-based Buffer Overflow
File sys/net/netmap/netmap_vale.c
Lines 988, 1330-1346
Area net (netmap VALE switch)
Confidence certain
Discovered 2026-07-01
Reported pending

Summary

The VALE software switch forwarding path copies packet data using a user-supplied length field (slot->len) without validating it against the netmap buffer size (~2048 bytes). An attacker who has mapped a VALE TX ring via /dev/netmap can set slot->len to any value up to 65535, causing pkt_copy/copyin to write up to 65536 bytes into a ~2048-byte kernel heap buffer β€” a controlled heap overflow with attacker-chosen data and length. The sibling code in netmap.c validates slot->len against NETMAP_BDG_BUF_SIZE in three separate places, but this check was omitted in the VALE forwarding fast path.

Root cause

nm_bdg_preflush() at sys/net/netmap/netmap_vale.c:988:

ft[ft_i].ft_len = slot->len;    /* no bounds check */

slot->len is a uint16_t from the user-mapped shared ring (writable via mmap of /dev/netmap). It can be any value 0–65535.

nm_bdg_flush() at sys/net/netmap/netmap_vale.c:1330-1346:

size_t len = (ft_p->ft_len + 63) & ~63;    /* round up; up to 65536 */
...
dst = BDG_NMB(&dst_na->up, slot);          /* ~2048-byte kernel buffer */
...
if (ft_p->ft_flags & NS_INDIRECT) {
    if (copyin(src, dst, len)) { ... }     /* line 1339: writes len bytes */
} else {
    pkt_copy(src, dst, (int)len);          /* line 1345: writes len bytes */
}

The destination dst is a netmap buffer of NETMAP_BDG_BUF_SIZE (default 2048 bytes). With slot->len = 65535, len rounds to 65536, writing 32Γ— the buffer capacity.

The validation that exists in sibling code: - netmap.c:748: if (slot->len < 14 || slot->len > NETMAP_BDG_BUF_SIZE(...)) - netmap.c:1124: same check - netmap.c:2017: if (kring->nr_hwavail >= lim) (different but related)

None of these checks exist in the VALE forwarding path.

Threat model & preconditions

  • Attacker position: local user with access to /dev/netmap (mode 0660 root:wheel, or root in a jail where netmap is exposed).
  • Privileges gained or impact: kernel heap corruption with full attacker-controlled data and length. Enables kernel code execution, KASLR bypass, and jail escape.
  • Required config: netmap loaded (kldload netmap), a VALE bridge created.
  • Reachability: mmap the VALE TX ring, set slot->len to a large value, and trigger forwarding by sending packets through the bridge.

Proof of concept

PoC source: findings/poc/DF-0401/poc.c

Build & run

cc -o poc poc.c -lnetmap
./poc vale1:0        # requires /dev/netmap access

Expected output

Fatal trap 12: page fault while in kernel mode
KDB: stack backtrace:
#1 pkt_copy at netmap.c:...
#2 nm_bdg_flush at netmap_vale.c:1345
#3 netmap_bwrap_intr_notify at netmap_vale.c:...

Impact

  • Kernel heap overflow with 100% attacker-controlled data and length.
  • Heap grooming of the netmap buffer slab enables controlled overwrite of adjacent kernel objects, leading to arbitrary kernel code execution.
  • In a jail with exposed /dev/netmap, this is a reliable jail escape to host root.
  • The overflow also over-reads the source buffer (src via BDG_NMB), leaking adjacent netmap buffer contents.

Validate slot->len in nm_bdg_preflush() before storing it:

--- a/sys/net/netmap/netmap_vale.c
+++ b/sys/net/netmap/netmap_vale.c
@@ -985,6 +985,9 @@
        struct netmap_slot *slot = &ring->slot[j];
        char *buf;

+       if (slot->len > NETMAP_BDG_BUF_SIZE(na->up.nm_mem)) {
+           D("dropping oversize slot len %d", slot->len);
+           continue;
+       }
        ft[ft_i].ft_len = slot->len;

This mirrors the existing check at netmap.c:748.

References

  • NETMAP_BDG_BUF_SIZE is defined in netmap_kern.h and defaults to 2048.
  • The sibling validation in netmap.c at lines 748, 1124, 2017 proves the intended contract: slot lengths must be bounded by the buffer size.

Timeline

  • 2026-07-01 Discovered during automated audit.
  • 2026-07-01 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0401 Β· 16 files
FileTypeDescriptionSize
poc.c trigger-source intended VALE-overflow PoC (needs live /dev/netmap; cannot run on this master) 2.1 KB view raw
reachability_probe.c trigger-source self-contained probe that opens /dev/netmap; proves device-node absence; RUNS and returns [UNREACHABLE] 2.1 KB view raw
net/netmap.h vendored-header netmap UAPI header (vendored so poc.c could build if subsystem existed) 14.0 KB view raw
net/netmap_user.h vendored-header netmap userspace helper header (vendored) 9.3 KB view raw
build.sh build-script builds reachability_probe (ok) and attempts poc (fails on missing headers) 1.2 KB view raw
run.sh run-script runs reachability_probe; confirms /dev/netmap absence 601 B view raw
build.log build-log build.sh output: probe builds, poc fails on IFNAMSIZ/net/netmap/netmap.h 853 B view raw
run.log run-log run.sh output: [UNREACHABLE] /dev/netmap ENOENT 777 B view raw
netmap_build_attempt.log kernel-build-log cd /usr/src/sys/net/netmap && make: 15 errors, 30 if_unused7 expansions, no .ko produced - proves subsystem dead 9.3 KB view raw
fix.diff suggested-fix clamp slot->len to NETMAP_BDG_BUF_SIZE at netmap_vale.c:988, mirroring netmap.c:748; applies cleanly to sys/ and /usr/src 768 B view raw
env.txt environment uname, cc version, kldstat 365 B view raw
VERDICT.md verdict full source-level proof + unreachability analysis 10.5 KB ↓ raw
README.md readme human-facing summary 3.0 KB ↓ 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
live_reachability_check.txt reachability-test Live netmap reachability evidence on guest 902 B view raw
README.md readme human-facing summary
↓ download raw

DF-0401 PoC β€” VALE bridge heap overflow via unchecked slot->len

Verdict

NOT REPRODUCED (latent bug). The source-level defect is real and severe (a missing bounds check on slot->len in nm_bdg_preflush that would allow a 63488-byte heap overflow), but the entire netmap subsystem is dead code on DragonFlyBSD master DEV 6cc80ee9: struct ifnet no longer has the if_unused7 member that netmap's WNA macro requires, so netmap.ko cannot be compiled, loaded, or instantiated, /dev/netmap does not exist, and the VALE forwarding path cannot be entered live. See VERDICT.md for the full source-level proof and the unreachability evidence.

Build

./build.sh

Builds the self-contained reachability_probe (succeeds) and attempts to build the intended poc (fails: netmap userland headers are not installed and netmap.ko cannot be built).

Run

./run.sh        # as the unprivileged user (maxx)

Expected output (this is the actual result on this guest):

DF-0401 reachability probe
--------------------------
[UNREACHABLE] /dev/netmap does not exist: No such file or directory
[UNREACHABLE] netmap subsystem is not loaded/available.
[UNREACHABLE] The VALE forwarding path (nm_bdg_preflush/
              nm_bdg_flush) cannot be entered live.
REACHABILITY_PROBE_RC=2
...
ls: /dev/netmap: No such file or directory
no netmap.ko shipped in /boot/kernel

How the bug would work (if netmap were loadable)

  1. open("/dev/netmap") and NIOCREGIF a VALE port (e.g. vale1:0).
  2. mmap the shared ring memory.
  3. Set txring->slot[idx].len = 65535 (the buffer is only 2048 bytes).
  4. Fill the buffer with a controlled pattern.
  5. ioctl(fd, NIOCTXSYNC) triggers nm_bdg_preflush β†’ nm_bdg_flush.
  6. nm_bdg_preflush (netmap_vale.c:988) stores the unchecked slot->len into ft[].ft_len β€” no bounds check (unlike netmap.c:748).
  7. nm_bdg_flush (netmap_vale.c:1330) computes len = (65535 + 63) & ~63 = 65536 and calls pkt_copy(src, dst, 65536) into the 2048-byte dst buffer (BDG_NMB) β†’ 63488-byte heap overflow with attacker-controlled content and length.

The sibling checks that DO exist (and that the VALE path omits): - netmap.c:748 β€” if (slot->len < 14 || slot->len > NETMAP_BDG_BUF_SIZE(...)) - netmap.c:1124 β€” else if (len > NETMAP_BDG_BUF_SIZE(...)) - netmap.c:2017 β€” if (len > NETMAP_BDG_BUF_SIZE(...))

Fix

fix.diff adds the missing check at netmap_vale.c:988, mirroring netmap.c:748. It applies cleanly to both the host sys/ tree and the in-guest /usr/src tree. A full boot-test is not possible because the netmap subsystem does not compile on this master (see netmap_build_attempt.log).

Reproduce from scratch

scp -r findings/poc/DF-0401 dfbsd-maxx:/tmp/
ssh dfbsd-maxx 'cd /tmp/DF-0401 && ./build.sh && ./run.sh'
# To inspect the netmap build failure (proof the subsystem is dead):
ssh dfbsd 'cd /usr/src/sys/net/netmap && make obj && make 2>&1 | grep if_unused7 | head'
VERDICT.md verdict full source-level proof + unreachability analysis
↓ download raw

DF-0401 β€” VERDICT

Status: NOT REPRODUCED (latent bug β€” vulnerable code path is real in source but the containing netmap subsystem is dead/unloadable on DragonFlyBSD master DEV 6cc80ee9).

Impact (live): none β€” the bug path cannot be entered on this kernel. Impact (source-level primitive, if netmap were loadable): heap overflow of up to 63488 bytes (65536 βˆ’ 2048) with 100% attacker-controlled content and length into a NETMAP_BDG_BUF_SIZE (default 2048) kernel buffer β€” a critical memory-corruption primitive.

Confidence: certain (the source-level proof is ironclad; the unreachability is also ironclad).


1. The bug is real in the source

The vulnerable code path and the missing check are confirmed by direct source inspection.

Trigger β€” sys/net/netmap/netmap_vale.c:988 (nm_bdg_preflush):

984:    for (; likely(j != end); j = nm_next(j, lim)) {
985:        struct netmap_slot *slot = &ring->slot[j];
986:        char *buf;
987:
988:        ft[ft_i].ft_len = slot->len;          /* <-- NO bounds check */
989:        ft[ft_i].ft_flags = slot->flags;

slot is &ring->slot[j], where ring is the user-mmap'd netmap TX ring (kring->ring). slot->len is a uint16_t (confirmed in sys/net/netmap/netmap.h:131: uint16_t len; /* packet length */), writable directly by userspace via the shared ring memory. It can be any value 0–65535. nm_bdg_preflush copies it verbatim into ft[ft_i].ft_len with no validation against the bridge buffer size.

Sink β€” sys/net/netmap/netmap_vale.c:1330-1347 (nm_bdg_flush):

1329:               void *dst, *src = ft_p->ft_buf;
1330:               size_t len = (ft_p->ft_len + 63) & ~63;   /* round up; 65535 -> 65536 */
...
1333:               dst = BDG_NMB(&dst_na->up, slot);          /* 2048-byte netmap buf */
...
1338:               if (ft_p->ft_flags & NS_INDIRECT) {
1339:               if (copyin(src, dst, len)) { ... }      /* writes `len` bytes */
1342:               } else {
1344:               //memcpy(dst, src, len);
1345:               pkt_copy(src, dst, (int)len);           /* writes `len` bytes */
1346:               }
  • len = (65535 + 63) & ~63 = 65536.
  • dst = BDG_NMB(&dst_na->up, slot) returns a pointer into the destination adapter's NETMAP_BUF_POOL. Per sys/net/netmap/netmap_mem2.h:210, NETMAP_BDG_BUF_SIZE(n) = (n)->pools[NETMAP_BUF_POOL]._objsize, which defaults to 2048 (confirmed by the comment in the finding and the netmap_obj_malloc(... NETMAP_BDG_BUF_SIZE(n) ...) site at netmap_mem2.c:367).
  • pkt_copy(src, dst, 65536) writes 65536 bytes into the 2048-byte dst β†’ 63488-byte heap overflow with attacker-controlled content (the source buffer is the attacker's TX buffer) and attacker-controlled length (the slot->len value).

The intended contract β€” proven by the sibling check. The same slot->len value IS validated in three other places in sys/net/netmap/netmap.c:

  • netmap.c:748 β€” if (slot->len < 14 || slot->len > NETMAP_BDG_BUF_SIZE(na->nm_mem)) { D("bad pkt at %d len %d", n, slot->len); continue; }
  • netmap.c:1124 β€” } else if (len > NETMAP_BDG_BUF_SIZE(kring->na->nm_mem)) {
  • netmap.c:2017 β€” if (len > NETMAP_BDG_BUF_SIZE(na->nm_mem)) { /* too long for us */

The VALE forwarding fast path in netmap_vale.c omits this check entirely. This is the canonical "check exists in three sibling code paths but was forgotten in the fourth" pattern β€” a genuine defect, not a false positive.

Primitive characterization (source-level, since the subsystem is dead on this guest): - Write size: up to 65536 bytes ((slot->len + 63) & ~63 with slot->len up to 65535). - Overflow size: up to 63488 bytes past the 2048-byte buffer. - Content control: 100% β€” the source buffer src is the attacker's TX buffer (BDG_NMB(&na->up, slot) from the attacker's own ring slot), or, with NS_INDIRECT set, an arbitrary user pointer (copyin(src, dst, len)). - Length control: 100% β€” slot->len is the attacker's uint16_t. - Allocation bucket: the netmap buffer pool (NETMAP_BUF_POOL), 2048-byte objects, slab-backed. On a kernel with INVARIANTS off this would be a high-confidence arbitrary-overwrite primitive into adjacent slab objects; on default GENERIC (INVARIANTS on) slab poisoning/magic checks would likely catch cross-type reuse and panic (DoS) before a clean uid=0.


2. Why it cannot be reproduced on this guest (Phase 4d: latent bug)

The entire netmap subsystem is dead code on DragonFlyBSD master DEV 6cc80ee9. It cannot be compiled, loaded, or instantiated. Evidence:

(a) struct ifnet no longer has if_unused7. Netmap attaches its per-adapter state to an ifnet via a spare field, accessed through the WNA macro:

sys/net/netmap/netmap_kern.h:747:
#define WNA(_ifp)   (_ifp)->if_unused7  /* XXX better name ;) */

But the current struct ifnet (sys/net/if_var.h) only has:

370:    int if_unused2;
412:    int if_unused4;

β€” if_unused7 was removed in an ifnet refactor and netmap was never updated. Every netmap .c file includes netmap_kern.h and fails to compile, with 15 hard errors in netmap.c alone (30 if_unused7 expansions across the build). See netmap_build_attempt.log:

/usr/src/sys/net/netmap/netmap_kern.h:747:27: error: 'struct ifnet' has no member named 'if_unused7'; did you mean 'if_unused2'?
 #define WNA(_ifp) (_ifp)->if_unused7 /* XXX better name ;) */

(b) Netmap is not in GENERIC. grep -ci netmap /usr/src/sys/config/X86_64_GENERIC β†’ 0. No options NETMAP.

(c) No netmap.ko is shipped. ls /boot/kernel/netmap.ko β†’ no such file. kldload netmap β†’ "can't load netmap: No such file or directory".

(d) No /dev/netmap device node. Confirmed by the reachability probe (reachability_probe.c):

[UNREACHABLE] /dev/netmap does not exist: No such file or directory
[UNREACHABLE] netmap subsystem is not loaded/available.
[UNREACHABLE] The VALE forwarding path (nm_bdg_preflush/nm_bdg_flush)
              cannot be entered live.

(e) Netmap userland headers are not installed. Neither /usr/include/net/netmap.h nor /usr/include/net/netmap/netmap.h exist, so even the userspace PoC cannot build against the standard install path.

Conclusion: the missing-check defect at netmap_vale.c:988 is a genuine latent bug in the source. It is not triggerable live on this guest because the containing netmap subsystem does not build, load, or expose any device node. This is Phase 4(d): "genuinely not reachable on this kernel … the sink is dead code". The trigger conditions that would make it live are: (1) a future commit restoring if_unused7 (or migrating netmap to a dedicated ifnet member / NA(ifp) softc) so the subsystem compiles again, and (2) an admin loading netmap.ko and a local user gaining access to /dev/netmap. At that point this defect becomes a critical heap-overflow primitive.

Because the subsystem cannot be loaded, no heap-grooming / escalation chain can be developed or tested on this guest. This is a valid hard blocker (Phase 6: "dead/unreachable at runtime on this guest AND no harness can exercise it" β€” the harness itself, the netmap module, cannot be built).


3. The fix

fix.diff adds the missing bounds check at netmap_vale.c:988, mirroring the existing check at netmap.c:748:

--- a/sys/net/netmap/netmap_vale.c
+++ b/sys/net/netmap/netmap_vale.c
@@ -985,6 +985,15 @@
        struct netmap_slot *slot = &ring->slot[j];
        char *buf;

+       /* Validate slot length against the bridge buffer size, mirroring
+        * the check in netmap.c:netmap_bwrap_flush()/nm_bdg_flush_new().
+        * Without this, a userspace-mapped VALE TX ring can set slot->len
+        * up to 65535 and nm_bdg_flush() will pkt_copy/copyin that many
+        * bytes into a NETMAP_BDG_BUF_SIZE (default 2048) buffer. */
+       if (slot->len > NETMAP_BDG_BUF_SIZE(na->up.nm_mem)) {
+           RD(5, "dropping oversize slot len %d", slot->len);
+           continue;
+       }
        ft[ft_i].ft_len = slot->len;
        ft[ft_i].ft_flags = slot->flags;
  • na is struct netmap_vp_adapter * (function signature at line 962); na->up is the embedded struct netmap_adapter (netmap_kern.h:374); na->up.nm_mem is struct netmap_mem_d * (netmap_kern.h:349).
  • NETMAP_BDG_BUF_SIZE(n) expects struct netmap_mem_d * and expands to (n)->pools[NETMAP_BUF_POOL]._objsize (netmap_mem2.h:210). The expression is type-correct by construction and identical in form to the sibling check at netmap.c:748.
  • The continue drops the oversize slot (matching netmap.c:748's continue semantics) without advancing ft_i, so it neither corrupts the ft[] work area nor enters nm_bdg_flush with the bad length.

This matches the finding markdown's ## Recommended fix proposal in substance (same check, same location) and improves it with a comment explaining the defect and the use of RD(5, ...) (rate-limited debug, matching netmap's logging conventions) instead of a louder D(...).

Fix validation: the diff applies cleanly to both the host sys/ tree (git apply --check β†’ OK) and the in-guest /usr/src tree (patch -p1 --dry-run β†’ "Hunk #1 succeeded at 985"). The fix inserts the correct check at the correct location (verified by sed of the patched region). However, a full Phase 8 boot-test cannot be performed because the netmap subsystem itself does not compile on this master (the if_unused7 breakage upstream of our hunk), so no kernel/module can be built that contains a loadable netmap, so the bug cannot be triggered before or after the fix. fix_status = "not_testable" per the Phase 8f rubric: we validated that the diff applies and traced that it closes the code path (the check rejects any slot->len > 2048 before it reaches ft[ft_i].ft_len and thence nm_bdg_flush), but it could not be exercised live.


4. PoC artifacts

File Purpose
poc.c The intended VALE-overflow PoC (as given by the finding). Needs live /dev/netmap; cannot run on this master.
reachability_probe.c Self-contained probe that opens /dev/netmap. Proves the device node is absent. Runs and returns [UNREACHABLE].
net/netmap.h, net/netmap_user.h Vendored netmap UAPI headers (so poc.c could build if the subsystem existed).
netmap_build_attempt.log Full make output of cd /usr/src/sys/net/netmap && make β€” 15 if_unused7 errors, no .ko produced. Proves the subsystem is dead.
build.log Output of ./build.sh (probe builds; intended PoC fails on vendored headers).
run.log Output of ./run.sh β€” reachability probe shows /dev/netmap ENOENT.
fix.diff The verified fix (applies cleanly; closes the path).
env.txt Guest environment.

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

not_testable: netmap.ko build fails (if_unused7). fix.diff git apply --check OK + patch --dry-run OK.

git apply --check OK. patch --dry-run 'Hunk #1 succeeded at 985'.
↓ fix.diffn/a -- netmap subsystem doesn't compile

Confirmed kernel references

Detail

Exploit chain

none β€” dead code. netmap subsystem is not compiled into the kernel or any loadable module on this guest.

Evidence (decisive lines)

ls: /dev/netmap: No such file or directory
netmap.ko in /boot/kernel: not found
netmap symbols in kernel: 0
netmap in GENERIC: 0
netmap in conf/files: 0
if_unused7 in struct ifnet: 0

PoC changes

Added live_reachability_check.txt with definitive guest-side evidence confirming netmap is dead code.

Verified recommended fix

No code change needed for this guest β€” netmap is dead code. If netmap were to be re-enabled, the fix.diff (bounds check on slot->len vs NETMAP_BDG_BUF_SIZE in nm_bdg_preflush) should be applied. Matches finding proposal.

Verdict

NOT REPRODUCED (dead code). netmap is completely absent from this kernel. Live verification: /dev/netmap does not exist, netmap.ko not shipped, 0 netmap symbols in /boot/kernel/kernel, 0 entries in X86_64_GENERIC config, 0 entries in conf/files. struct ifnet no longer has if_unused7 (0 matches in if_var.h), so netmap's WNA() macro is broken and netmap.ko cannot compile. The source exists at sys/net/netmap/ but is never built. The VALE forwarding path (nm_bdg_preflush/nm_bdg_flush) is unreachable.