m_copyback()/m_copyback2() write through shared (refcount>1) clusters with no M_WRITABLE gate β cross-packet data corruption; remote sink is bridge_output()'s in-place ether_shost rewrite on m_copypacket-shared flood copies
Summary
_m_copyback2's second loop bcopy()s into the target mbuf's existing data window with no M_WRITABLE/sharecount check β the file gates cluster growth (M_TRAILINGSPACE returns 0 when shared) but not writes into the existing window. Reproduced on the production guest: two mbufs sharing a cluster via m_copym (sharecount=2, M_WRITABLE=0), m_copyback through one rewrote the other's data (a[0] 0x41->0x5a). Concrete remote sink: bridge_output (if_bridge.c:4464-4471) rewrites ether_shost in place via m_copyback on bridge-forwarded frames while bridge flooding shares clusters through m_copypacket β flood copies taking different rewrite branches race inside one shared cluster, last write wins, bridged frames can leave with the wrong source MAC (switch MAC-table poisoning / L2 blackholing); trigger is any broadcast frame (ARP) on a bridge member. Same class: pf in-place header rewrites (pf.c:2120,3857,3997,4018,4085,4285,5060; pf_norm.c). Memory-safe (in-cluster bounds): integrity corruption, not memory corruption. INVARIANTS KASSERT fix proposed.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-5796 Β· 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.7 KB | β raw | |
| README.md | β | 2.7 KB | β raw | |
| fix.diff | β | 925 B | view raw | |
| verdict.json | β | 3.3 KB | view raw |
DF-5796 β m_copyback()/m_copyback2() write through shared clusters (no M_WRITABLE gate)
What
_m_copyback2() (sys/kern/uipc_mbuf.c:2326-2394) β the engine behind both
m_copyback() and m_copyback2() β bcopy()s caller data into the target
mbuf's existing data window with no M_WRITABLE() / m_sharecount()
check anywhere on the path. The file gates growth into shared
clusters indirectly (M_TRAILINGSPACE returns 0 when the cluster is
shared), so only the write into the existing window is ungated
(uipc_mbuf.c:2368-2369):
mlen = min(m->m_len - off, len);
bcopy(cp, mtod(m, caddr_t) + off, mlen); /* m may be a SHARED cluster */
When the target mbuf shares its cluster (m_sharecount() > 1, the normal
state after any m_copym/m_copypacket/m_split), the write lands in
memory also owned by every other sharer β silently rewriting another
live packet's bytes. Memory-safe (stays inside the cluster), but a
cross-packet data-integrity corruption.
Concrete remote sink
bridge_output() (sys/net/bridge/if_bridge.c:4464-4471) rewrites the
ethernet source MAC in place via m_copyback() on frames forwarded by
a bridge β and bridge flooding duplicates frames with m_copypacket()
(if_bridge.c:2317, 3056, 3208, 3274), whose copies share the original's
clusters. Copies that take different rewrite branches (copy re-emitted
on the receiving interface writes the bridge MAC; copies to other member
ports write the saved original shost or nothing) race inside one shared
cluster: whichever write lands last wins for every copy not yet
transmitted, so bridged frames can leave the box with the wrong source
MAC β switch MAC-learning corruption / L2 traffic blackholing. Trigger:
any broadcast frame (e.g. ARP request) arriving on a bridge member port;
no authentication, no local access.
Other same-class callers: pf scrubbing (pf/pf.c:2120,3857,3997,4018,4085, 4285,5060; pf_norm.c:1395,1620,1954) rewrites TCP/ICMP headers in place and operates on whatever chain it is handed.
Reproduced (guest, production non-INVARIANTS kernel)
DF5796: cluster shared, m_sharecount(a)=2 (M_WRITABLE=0)
DF5796: REPRODUCED: m_copyback through shared cluster rewrote the other
mbuf's data: a[0]=0x5a (expected 0x41)
a and b share one cluster (m_copym). m_copyback(b, 0, 16, "ZZ..")
mutated a's data β the exact ungated write, proven.
Build / run / expected
Same rig as DF-5795 (mbuf_probe.c, probe_5796):
cd /tmp/df5796 && sh build.sh && sh run.sh
Fix
fix.diff: INVARIANTS KASSERT on the ungated bcopy (catches every
misbehaving caller on debug kernels) β full remediation belongs at the
callers (unshare first via m_unshare()/m_dup(), or make
bridge_output rewrite on a private head mbuf).
DF-5796 VERDICT
Status: reproduced (the ungated shared-cluster write primitive); impact ceiling: cross-packet data-integrity corruption on bridged networks (remote-triggerable), memory-safe (writes stay in cluster bounds) β recorded impact "none" in the enum, integrity notes here.
What was run
Same rig (mbuf_probe.ko, probe_5796) on the live DF 6.5-DEVELOPMENT
guest (production non-INVARIANTS kernel).
What happened (run.log)
DF5796: cluster shared, m_sharecount(a)=2 (M_WRITABLE=0)
DF5796: REPRODUCED: m_copyback through shared cluster rewrote the other
mbuf's data: a[0]=0x5a (expected 0x41)
a=m_getclpkthdr+cluster filled with 0x41;b=m_copym(a, 0, M_COPYALL)β both share the cluster (m_sharecount=2,M_WRITABLE(a)==0).m_copyback(b, 0, 16, 0x5Aβ¦)executed_m_copyback2's second-loopbcopy(cp, mtod(m)+off, mlen)(uipc_mbuf.c:2368-2369) β no M_WRITABLE/m_sharecount gate exists anywhere on the path.- Read back through
a: first byte 0x5a. The write crossed the mbuf boundary into the other owner's packet. Primitive proven.
Production trigger (code-cited, not driven end-to-end on the guest)
bridge_output() if_bridge.c:4464-4471 rewrites ether_shost in place
with m_copyback on bridge-forwarded frames; bridge flooding first
duplicates frames via m_copypacket (if_bridge.c:2317/3056/3208/3274),
and m_copypacket shares clusters (uipc_mbuf.c:1647-1652
ext_ref+ext-copy). Flood copies dispatched to different member ports
take different rewrite branches inside one shared cluster; last write
wins for untransmitted copies β wrong source MAC emitted on the wire β
switch MAC-table poisoning / L2 blackholing. Any ARP/broadcast frame
into a bridge member triggers it; no auth. (End-to-end L2 impact not
exercised on the single-NIC audit guest β the primitive plus the call
graph is the evidence.)
Why not higher severity
Writes stay inside cluster bounds (mlen β€ m->m_len - off), so this is integrity, not memory corruption. No uaf/overflow primitive follows from it.
Fix validation
Not performed (no kernel rebuild): not a reproduced memory-corruption finding. fix.diff adds an INVARIANTS KASSERT at the ungated bcopy; durable remediation is caller-side (unshare before rewrite; make bridge_output's shost rewrite operate on a private head mbuf).
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: not a reproduced memory-corruption finding (in-bounds write). fix.diff adds an INVARIANTS-only KASSERT that would panic on the demonstrated rig and on the bridge path when INVARIANTS is enabled.
fix.diff in this directory; sink analysis in VERDICT.md.
Confirmed kernel references
Detail
Evidence (decisive lines)
run.log decisive lines: 'DF5796: cluster shared, m_sharecount(a)=2 (M_WRITABLE=0)', 'DF5796: REPRODUCED: m_copyback through shared cluster rewrote the other mbuf's data: a[0]=0x5a (expected 0x41)'. Source: mbuf_probe.c probe_5796(); ungated write at sys/kern/uipc_mbuf.c:2369.
PoC changes
Added missing #include
Verified recommended fix
Gate the ungated bcopy in _m_copyback2 with an INVARIANTS KASSERT on M_WRITABLE(m) (see fix.diff); durable remediation at callers: unshare (m_unshare/m_dup) before in-place rewrites, and make bridge_output's shost rewrite operate on a private head mbuf.
Verdict
Reproduced the primitive on the production guest: _m_copyback2()/m_copyback() (sys/kern/uipc_mbuf.c:2368-2369) bcopy()s caller data into a target mbuf's existing window with no M_WRITABLE()/m_sharecount() gate; with two mbufs sharing one cluster via m_copym() (sharecount=2, M_WRITABLE=0), m_copyback() through one mbuf rewrote the other mbuf's data (a[0]=0x41 -> 0x5a). Memory-safe (writes stay in cluster bounds) but a cross-packet data-integrity corruption. Production sink (code-cited): bridge_output() if_bridge.c:4464-4471 rewrites ether_shost in place via m_copyback on bridge-forwarded frames whose clusters are shared by m_copypacket() flood copies (if_bridge.c:2317/3056/3208/3274) - remote-triggerable by any broadcast frame on a bridged member; wrong source MAC can poison switch MAC tables / blackhole L2 traffic. Same class: pf in-place header rewrites (pf.c:2120,3857,3997,4018,4085,4285,5060; pf_norm.c:1395,1620,1954).
No comments yet.