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:
mmapthe VALE TX ring, setslot->lento 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 (
srcviaBDG_NMB), leaking adjacent netmap buffer contents.
Recommended fix
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_SIZEis defined innetmap_kern.hand defaults to 2048.- The sibling validation in
netmap.cat 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)
PoC verification
Evidence pack
findings/poc/DF-0401 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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)
open("/dev/netmap")andNIOCREGIFa VALE port (e.g.vale1:0).mmapthe shared ring memory.- Set
txring->slot[idx].len = 65535(the buffer is only 2048 bytes). - Fill the buffer with a controlled pattern.
ioctl(fd, NIOCTXSYNC)triggersnm_bdg_preflushβnm_bdg_flush.nm_bdg_preflush(netmap_vale.c:988) stores the uncheckedslot->lenintoft[].ft_lenβ no bounds check (unlikenetmap.c:748).nm_bdg_flush(netmap_vale.c:1330) computeslen = (65535 + 63) & ~63 = 65536and callspkt_copy(src, dst, 65536)into the 2048-bytedstbuffer (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'
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'sNETMAP_BUF_POOL. Persys/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 thenetmap_obj_malloc(... NETMAP_BDG_BUF_SIZE(n) ...)site atnetmap_mem2.c:367).pkt_copy(src, dst, 65536)writes 65536 bytes into the 2048-bytedstβ 63488-byte heap overflow with attacker-controlled content (the source buffer is the attacker's TX buffer) and attacker-controlled length (theslot->lenvalue).
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;
naisstruct netmap_vp_adapter *(function signature at line 962);na->upis the embeddedstruct netmap_adapter(netmap_kern.h:374);na->up.nm_memisstruct netmap_mem_d *(netmap_kern.h:349).NETMAP_BDG_BUF_SIZE(n)expectsstruct 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 atnetmap.c:748.- The
continuedrops the oversize slot (matchingnetmap.c:748'scontinuesemantics) without advancingft_i, so it neither corrupts theft[]work area nor entersnm_bdg_flushwith 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_testablenot_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'.
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.
No comments yet.