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_RESTRICTEDROOTto open anAF_NETGRAPHcontrol 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_lento 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_lentom_tag_len + Nfor smallN;memcmpstays inside mapped slab memory and the match/mismatch result (delivered through theifMatch/ifNotMatchhooks) leaksNbytes of adjacent kernel heap byte-by-byte (≤256 probes per byte). - No write primitive is obtained (
memcmpis read-only).
Recommended fix
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
sys/netgraph7/ng_tag.c:525,534-544— the vulnerable match loop.sys/kern/uipc_mbuf2.c:256-269—m_tag_allocallocates exactlym_tag_lendata bytes.sys/kern/uipc_mbuf2.c:325-340—m_tag_locatematches only on(cookie,id), not length.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0634 · 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
DF-0634 — Heap OOB read in ng_tag_rcvdata mbuf-tag matching
Bug
sys/netgraph7/ng_tag.c:525,534-544 — ng_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).
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)
sys/netgraph7/ng_tag.c:525.tag_len = hip->in_tag_len;— user-controlleduint16_t, accepted up to 65535. TheSET_HOOKINarglen check at:363-366only enforcesarglen == sizeof(struct ng_tag_hookin) + hp->tag_len; it does not boundhp->tag_len, so the in-kernelhip->in_tag_lenmay be any value up to 65535.:535m_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 itsm_tag_len.:537-538memcmp((void*)(tag+1), hip->in_tag_data, tag_len). Readstag_lenbytes fromtag + 1— the data area behind them_tagheader. The data area is exactlytag->m_tag_lenbytes long (allocated bym_tag_allocatsys/kern/uipc_mbuf2.c:256-269askmalloc(len + sizeof(struct m_tag), M_PACKET_TAGS, ...)).- When
tag_len > tag->m_tag_len: thememcmpreadstag_len - tag->m_tag_lenbytes past the end of them_tagallocation. With a small overshoot this is a silent heap-leak (thememcmpmatch/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. - No write primitive is obtained (
memcmpis 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_RESTRICTEDROOTto open anAF_NETGRAPHcontrol 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=65535and 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.
Recommended fix
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_testablecompile+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).
No comments yet.