β¬’ DragonFlyBSD Kernel Audit
← triage Β· dashboard
DF-5796

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-5796 Β· 11 files
FileTypeDescriptionSize
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).

VERDICT.md
↓ download raw

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_getcl pkthdr+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-loop bcopy(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_testable

Fix-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.
↓ fix.diffper-fix-DF-5796

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 for M_WAITOK; used m_getcl + m_copym(M_COPYALL) to build the canonical shared-cluster pair and 0x41/0x5A markers to prove the cross-write read back through the original mbuf.

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).