m_move_pkthdr() never clears M_PKTHDR on the source mbuf β live stale packet-header (stale len/rcvif/csum/pf) glued mid-chain on every M_MOVE_PKTHDR, contradicting its documented contract
Summary
The function moves the pkthdr to 'to' but leaves M_PKTHDR and the full stale m_pkthdr (len, rcvif, csum_*, fw_flags, pf) on 'from' β mbuf.h:406-410 documents 'from no longer has a pkthdr' and FreeBSD's macro clears the flag; in-tree callers (icmp6.c:556 saves n0len first) are written to that contract. Reproduced on the production guest: after M_MOVE_PKTHDR the source keeps flags=0x8002 (M_PKTHDR|M_PHCACHE) and m_adj then mutates the 'moved' pkthdr.len 30->22 β the exact icmp6.c:559/563 sequence any ICMPv6 echo request drives. Producers span remote (icmp6 echo reflect, ip6_input.c:255, ip6_splithdr) and local (m_prepend, m_unshare, if_stge.c, ieee80211) paths; all current pkthdr readers operate on chain heads so no in-tree memory-safety sink β kernel-state/contract corruption feeding stale metadata into remote-reachable paths. One-line flag-clear fix proposed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5795 Β· 11 files| File | Type | Description | Size | |
|---|---|---|---|---|
| mbuf_probe.c | β | 4.7 KB | view raw | |
| Makefile | β | 60 B | β download | |
| build.sh | β | 84 B | view raw | |
| run.sh | β | 108 B | view raw | |
| build.log | β | 1.0 KB | view raw | |
| run.log | β | 748 B | view raw | |
| env.txt | β | 158 B | view raw | |
| VERDICT.md | β | 2.9 KB | β raw | |
| README.md | β | 2.7 KB | β raw | |
| fix.diff | β | 1.2 KB | view raw | |
| verdict.json | β | 3.2 KB | view raw |
DF-5795 β m_move_pkthdr() does not clear M_PKTHDR on the source mbuf
What
m_move_pkthdr() (sys/kern/uipc_mbuf.c:2551-2558) moves the packet header
from from to to but never clears the M_PKTHDR flag on from β and
leaves from's m_pkthdr content (stale len, rcvif, csum_*, pf,
emptied tags) fully intact. Its documented contract in sys/sys/mbuf.h
(lines 405-410) states: "from should have M_PKTHDR set, and to must be
empty. from no longer has a pkthdr after this operation." FreeBSD's
M_MOVE_PKTHDR macro, from which this API is derived, does
(from)->m_flags &= ~M_PKTHDR.
The in-tree callers were written to the documented (FreeBSD) semantics β
e.g. icmp6.c:556 saves n0len = n0->m_pkthdr.len before the move
"for use below", precisely because the move is supposed to invalidate the
source's pkthdr.
Result
After every M_MOVE_PKTHDR in the tree, a mid-chain mbuf keeps a live,
wrong packet header:
- the moved-from mbuf claims
M_PKTHDRwith a stale total length (m_adj()on it mutates that stale header β reproduced: 30 β 22), - stale
rcvif,csum_*,fw_flags,pfmetadata rides along until the mbuf is freed, - remote-reachable producers: icmp6.c:559 (ICMPv6 echo reflect β any ping6), ip6_input.c:255, ip6_output.c:3072 (ip6_splithdr), if_stge.c:1535, ieee80211_output.c:1271, ieee80211_dragonfly.c:643.
No in-tree sink turns the stale header into memory corruption (all pkthdr readers use the chain head), so this is a kernel-state/contract bug, not an exploitable primitive on its own β but it silently breaks the invariant every consumer and every auditor (and FreeBSD) assumes.
Build / run / expected (guest, root)
cd /tmp/df5795 && sh build.sh # builds mbuf_probe.ko via bsd.kmod.mk sh run.sh # kldload, kldunload, dmesg tail
Expected decisive lines in dmesg:
DF5795: after move : from.flags=00008002 ... M_PKTHDR-on-from=1 DF5795: REPRODUCED: source mbuf still carries M_PKTHDR after M_MOVE_PKTHDR DF5795: m_adj(from, 8) mutated the 'moved' pkthdr.len: 30 -> 22 (MUTATED)
With the one-line fix the first two lines would report
M_PKTHDR-on-from=0 / NOT REPRODUCED, and m_adj would leave the
(no-longer-existent) header untouched.
Fix
fix.diff (single functional line): clear M_PKTHDR on from.
Deliberately does not touch from's M_EXT/data pointer: DF callers
(m_unshare uipc_mbuf.c:2037, m_prepend:1513) rely on from keeping its
cluster attachment β the comment in m_unshare claiming M_MOVE_PKTHDR
"will smash the data pointer and drop the M_EXT marker" describes the
old FreeBSD macro and is wrong for DF's function; the diff corrects that
comment too.
DF-5795 VERDICT
Status: reproduced (deterministic invariant break; impact: none β no memory corruption demonstrated or claimed)
What was run
mbuf_probe.ko (this directory) on the live DF 6.5-DEVELOPMENT guest
(X86_64_GENERIC, non-INVARIANTS production kernel, hammer2 root),
root-loaded KLD; production trigger needs no KLD (any ICMPv6 echo request
drives icmp6.c:559 M_MOVE_PKTHDR(n, n0) + m_adj(n0, β¦)).
What happened (run.log)
DF5795: before move : from.flags=00008002 from.pkthdr.len=30 DF5795: after move : from.flags=00008002 from.pkthdr.len=30 M_PKTHDR-on-from=1 DF5795: REPRODUCED: source mbuf still carries M_PKTHDR after M_MOVE_PKTHDR DF5795: m_adj(from, 8) mutated the 'moved' pkthdr.len: 30 -> 22 (MUTATED)
0x8002 = M_PKTHDR|M_PHCACHE. After M_MOVE_PKTHDR(to, from):
fromstill has M_PKTHDR set β sys/sys/mbuf.h:406-410 says it must not ("from no longer has a pkthdr after this operation"); FreeBSD's macro clears it.from->m_pkthdr.lenis stale (30) and live:m_adj(from, 8)(uipc_mbuf.c:1882-1883 readsmp->m_flags & M_PKTHDR) decremented it to 22 β the exact call sequence of the remote-reachable ICMPv6 echo-reflect path (icmp6.c:559,563).
Why it matters / ceiling
Every M_MOVE_PKTHDR call site (icmp6.c:559/1331, ip6_input.c:255,
ip6_output.c:3072, if_stge.c:1535, ieee80211_output.c:1271,
ieee80211_dragonfly.c:643, uipc_mbuf.c:1513 m_prepend, uipc_mbuf.c:2037
m_unshare) leaves a duplicate packet-header with stale len/rcvif/csum/pf
metadata glued mid-chain. Audited every in-file and grep-visible
consumer: all pkthdr readers operate on chain heads, m_free only
touches the (emptied) tag list, so no in-tree memory-safety sink was
found β hence severity Low (kernel-state/contract bug feeding stale
metadata into remote-reachable paths), not Medium+.
False-positive check
None of this is INVARIANTS-assert-guarded (KKASSERT is a no-op on this kernel, sys/sys/systm.h:94-118), so nothing masks or fires on the divergence; behavior is the shipped production behavior.
Fix validation
Not performed (no kernel rebuild): the finding is not a reproduced
memory-corruption, so the mandatory rebuild cycle does not apply.
fix.diff is a one-line flag clear (+ stale-comment correction in
m_unshare). M_EXT/m_data on from are deliberately untouched β
DF's m_free/m_unshare depend on from keeping its cluster attachment,
unlike the FreeBSD macro the stale comment describes.
Reproduce
guest# cd /tmp/df5795 && sh build.sh && sh run.sh
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
not_testableFix-validation kernel rebuild not performed: finding is not a reproduced memory-corruption (severity Low), so the mandatory rebuild cycle does not apply. fix.diff is a one-line flag clear audited against all seven in-tree M_MOVE_PKTHDR call sites; expected patched behavior: M_PKTHDR-on-from=0, m_adj leaves the source untouched.
fix.diff in this directory; call-site audit in VERDICT.md.
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log decisive lines: 'DF5795: after move : from.flags=00008002 ... M_PKTHDR-on-from=1', 'DF5795: REPRODUCED: source mbuf still carries M_PKTHDR after M_MOVE_PKTHDR', 'DF5795: m_adj(from, 8) mutated the moved pkthdr.len: 30 -> 22 (MUTATED)'. Source: mbuf_probe.c probe_5795(); root-cause at sys/kern/uipc_mbuf.c:2551-2558.
PoC changes
Seed concept converted to a KLD invariant probe; added missing #include
Verified recommended fix
Clear M_PKTHDR on the source in m_move_pkthdr(): from->m_flags &= ~M_PKTHDR; (do NOT touch M_EXT/m_data - DF callers rely on the cluster staying attached; see fix.diff which also corrects the stale FreeBSD-macro comment in m_unshare).
Verdict
Deterministically reproduced on the production (non-INVARIANTS) guest: m_move_pkthdr() (sys/kern/uipc_mbuf.c:2551) never clears M_PKTHDR on the source mbuf, violating its documented contract (sys/sys/mbuf.h:406-410, "from no longer has a pkthdr after this operation") and FreeBSD macro semantics. The source keeps a live stale pkthdr (demonstrated: pkthdr.len 30 mutated to 22 by m_adj() after the move, the exact icmp6.c:559/563 sequence reachable by any ICMPv6 echo request); every M_MOVE_PKTHDR call site in the tree glues this duplicate header mid-chain. No in-tree memory-safety sink was found after auditing all pkthdr readers (heads only), so impact is kernel-state/contract corruption with stale metadata on remote-reachable paths, not memory corruption.
No comments yet.