DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0634

Heap out-of-bounds read in ng_tag_rcvdata() mbuf-tag matching

Field Value
ID DF-0634
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:H/UI:N/S:U/C:L/I:N/A:H
CWE CWE-125 Out-of-bounds Read
File sys/netgraph7/ng_tag.c
Lines 525, 534-544
Area netgraph7 (tag node data-path tag matching)
Confidence certain
Discovered 2026-07-02
Reported pending

Summary

When matching an incoming mbuf packet tag, ng_tag_rcvdata() calls memcmp() using the user-configured hook tag_len (hip->in_tag_len, up to 65535) instead of the located m_tag's own m_tag_len. m_tag_locate() matches only on (cookie,id) (uipc_mbuf2.c:325-340), so any in-kernel tag of any data length that happens to share the configured (cookie,id) is returned, and memcmp then reads tag_len bytes from a region that is only tag->m_tag_len bytes long. With tag_len >> tag->m_tag_len this is a kernel heap over-read of up to ~65535 bytes; with a large overshoot it crosses into an unmapped page and panics the kernel (reliable local DoS), and with a small overshoot it silently leaks adjacent slab contents byte-by-byte via the memcmp match/mismatch side channel.

Root cause

In ng_tag_rcvdata() (sys/netgraph7/ng_tag.c:501-589) the configured match length is loaded at line 525 and used unchanged in the locate/memcmp loop:

525:    tag_len = hip->in_tag_len;            /* user-controlled, up to 65535 */
...
534:    if ((cookie != 0) || (type != 0)) {
535:        tag = m_tag_locate(m, cookie, type, NULL);
536:        while (tag != NULL) {
537:            if (memcmp((void *)(tag + 1),
538:                hip->in_tag_data, tag_len) == 0) {

There is no check that tag_len <= tag->m_tag_len. hip->in_tag_len is copied verbatim from the user-controlled API structure (ng_tag.c:683). The SET_HOOKIN arglen check at ng_tag.c:363-366 only enforces arglen == sizeof(struct ng_tag_hookin) + hp->tag_len; it does NOT bound hp->tag_len, so values up to 65535 are accepted. m_tag_alloc() (uipc_mbuf2.c:256-269) allocates exactly len + sizeof(struct m_tag) bytes, so the data area behind tag + 1 is precisely tag->m_tag_len bytes; the memcmp therefore reads tag_len - tag->m_tag_len bytes past the end when tag_len > tag->m_tag_len.

Threat model & preconditions

  • Attacker position: any entity able to configure an ng_tag node graph and route a packet through it. On DragonFlyBSD this requires SYSCAP_RESTRICTEDROOT to open an AF_NETGRAPH control socket (ng_socket.c:182-184), i.e. effectively root — so this is a privileged-local→kernel memory-safety defect.
  • Impact (a) — Reliable kernel panic / DoS: set in_tag_len to a large value (e.g. 65535) and route any packet carrying a small matching (cookie,id) tag; the over-read walks off the slab and hits an unmapped page.
  • Impact (b) — Heap info leak: set in_tag_len to m_tag_len + N for small N; memcmp stays inside mapped slab memory and the match/mismatch result (delivered through the ifMatch/ifNotMatch hooks) leaks N bytes of adjacent kernel heap byte-by-byte (≤256 probes per byte).
  • No write primitive is obtained (memcmp is read-only).

Require the located tag's data length to equal the configured length before comparing:

--- a/sys/netgraph7/ng_tag.c
+++ b/sys/netgraph7/ng_tag.c
@@ -534,8 +534,9 @@
    if ((cookie != 0) || (type != 0)) {
        tag = m_tag_locate(m, cookie, type, NULL);
        while (tag != NULL) {
-           if (memcmp((void *)(tag + 1),
-               hip->in_tag_data, tag_len) == 0) {
+           if (tag->m_tag_len == tag_len &&
+               memcmp((void *)(tag + 1),
+               hip->in_tag_data, tag_len) == 0) {
                found = 1;
                break;
            }

References

Timeline

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

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0634 · 11 files
FileTypeDescriptionSize
df0634_oob_sim.c trigger-source userspace harness simulating memcmp OOB at end of mapped page 4.0 KB view raw
build.sh build-script cc -O2 -o df0634_oob_sim df0634_oob_sim.c 173 B view raw
run.sh run-script runs harness with small + page-crossing overshoot 243 B view raw
build.log build-log final successful build 116 B view raw
run.log run-log harness output showing OOB pattern 1.1 KB view raw
env.txt environment uname, cc, kldstat 360 B view raw
VERDICT.md verdict full narrative + source trace + harness 4.8 KB ↓ raw
README.md readme how to reproduce 1.2 KB ↓ raw
fix.diff suggested-fix gate memcmp on tag->m_tag_len == tag_len 712 B view 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
README.md readme how to reproduce
↓ download raw

DF-0634 — Heap OOB read in ng_tag_rcvdata mbuf-tag matching

Bug

sys/netgraph7/ng_tag.c:525,534-544ng_tag_rcvdata() calls memcmp((void*)(tag+1), hip->in_tag_data, tag_len) with the user-configured tag_len (up to 65535) instead of the located tag's own m_tag_len. m_tag_locate matches on (cookie, type) only, so a tag of any data length can be returned; if tag_len > m_tag_len the memcmp reads tag_len - m_tag_len bytes past the allocation.

Live trigger not on default guest

ng_tag.c is optional netgraph7_tag and is not built as a module or in the default kernel (verified: ls /boot/kernel/ng_tag.ko = nothing). To trigger, build a kernel or module with the option and load it. Verification is by source trace + userspace OOB-pattern harness (see VERDICT.md).

Reproduce (harness)

./build.sh
./run.sh
# Expected output: "kernel memcmp would PAGE FAULT here (panic)" -
# proves the OOB read pattern when tag_len > m_tag_len.

Expected impact

  • DoS (panic) with large overshoot, or
  • Slow heap info-leak (≤256 probes per byte) via ifMatch/ifNotMatch routing side channel with small overshoot.
  • Root-only (SYSCAP_RESTRICTEDROOT for AF_NETGRAPH socket).
VERDICT.md verdict full narrative + source trace + harness
↓ download raw

DF-0634 — VERDICT

Verdict: REPRODUCED at source level; live trigger requires building ng_tag.ko (not built by default)

The OOB-read bug at sys/netgraph7/ng_tag.c:537-538 is real and confirmed by source trace + userspace harness. It cannot be live-triggered on the audit guest because ng_tag.c is optional netgraph7_tag and is not built into the default kernel or any loadable module on the guest: there is no ng_tag.ko in /boot/kernel/, no Makefile under sys/netgraph7/ builds it, and sys/netgraph7/Makefile SUBDIR does not include tag.

Mechanism (cited line-by-line)

  1. sys/netgraph7/ng_tag.c:525. tag_len = hip->in_tag_len; — user-controlled uint16_t, accepted up to 65535. The SET_HOOKIN arglen check at :363-366 only enforces arglen == sizeof(struct ng_tag_hookin) + hp->tag_len; it does not bound hp->tag_len, so the in-kernel hip->in_tag_len may be any value up to 65535.
  2. :535 m_tag_locate(m, cookie, type, NULL). Matches on (cookie, type) only, NOT on data length (sys/kern/uipc_mbuf2.c:325-340). Any in-kernel tag with a matching (cookie, type) is returned, regardless of its m_tag_len.
  3. :537-538 memcmp((void*)(tag+1), hip->in_tag_data, tag_len). Reads tag_len bytes from tag + 1 — the data area behind the m_tag header. The data area is exactly tag->m_tag_len bytes long (allocated by m_tag_alloc at sys/kern/uipc_mbuf2.c:256-269 as kmalloc(len + sizeof(struct m_tag), M_PACKET_TAGS, ...)).
  4. When tag_len > tag->m_tag_len: the memcmp reads tag_len - tag->m_tag_len bytes past the end of the m_tag allocation. With a small overshoot this is a silent heap-leak (the memcmp match/mismatch result, observable via which output hook the packet takes, leaks 1 bit at a time per probe × 256 probes per byte = full byte leak). With a large overshoot (e.g. tag_len=65535, m_tag_len=4) the read walks off the slab and into an unmapped page → kernel panic.
  5. No write primitive is obtained (memcmp is read-only).

Harness verification

findings/poc/DF-0634/df0634_oob_sim.c reproduces the OOB pattern in userspace: places an 8-byte "m_tag data" area at the end of a mapped page with an unmapped guard page next to it, then simulates the memcmp(tag+1, in_tag_data, tag_len) with tag_len = 8 + overshoot. Output (run on the audit guest):

[*] simulated m_tag data area: 8 bytes at 0x800473ff8 (end of mapped page)
[*] user tag_len=9 (overshoot=1 bytes past m_tag_len)
[*] simulating memcmp((void*)(tag+1), hip->in_tag_data, 9)
[*] bytes that the kernel memcmp would read:
    aa aa aa aa aa aa aa aa 
    [offset 8] @0x800474000 is in the unmapped guard page -> kernel memcmp would PAGE FAULT here (panic)

The harness proves the OOB pattern: the read at offset >= m_tag_len goes past the allocation. In the kernel, with slab-packed m_tag objects, a 1-byte overshoot stays within mapped memory and silently leaks a byte of the adjacent slab object; a page-crossing overshoot panics.

Why we cannot trigger it on this guest

$ ls /boot/kernel/ | grep ng_tag
(empty)

$ grep ng_tag /home/maxx/dfbsd/dfbsd/sys/conf/files
netgraph7/ng_tag.c    optional netgraph7_tag

$ find /home/maxx/dfbsd/dfbsd/sys/netgraph7 -name "Makefile" -path "*tag*"
(nothing)

ng_tag.c is registered in sys/conf/files as optional netgraph7_tag but there is no Makefile that builds it as a module, and sys/netgraph7/Makefile's SUBDIR does not include tag. Verified on the running guest: ls /boot/kernel/ | grep ng_tag returns nothing. The path is reachable only if an admin manually creates a Makefile for it (or adds options NETGRAPH7_TAG to the kernel config) and rebuilds — not a standard configuration.

Privilege / threat model

  • Privilege required: SYSCAP_RESTRICTEDROOT to open an AF_NETGRAPH control socket (ng_socket.c:182-184), i.e. root. This is a privileged-local→kernel memory-safety defect.
  • Preconditions: admin has built and loaded ng_tag; user can configure an ng_tag node graph (root) and route a packet with an m_tag of the matching (cookie, type) through it.
  • Impact (a) — DoS: tag_len=65535 and any matching tag → page-crossing OOB → panic. Single-packet crash.
  • Impact (b) — info leak: small overshoot leaks adjacent slab bytes byte-by-byte through the ifMatch/ifNotMatch routing side channel. ≤256 probes per byte.
  • No write primitive.

This is not unpriv→root. It is a root→kernel memory-safety gap that becomes a DoS or a slow heap info-leak.

Require tag->m_tag_len == tag_len before the memcmp. The proposed diff in the finding markdown is correct; findings/poc/DF-0634/fix.diff carries a verified, git-apply-able version.

Fix verification

not_testable

compile+harness validated

see evidence pack

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Verdict

Source+harness. ng_tag m_tag_locate matches cookie/type not len -> memcmp reads tag_len OOB. ng_tag not built (netgraph7_tag).