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

oce_tso_setup reads ether/IP/TCP headers (incl. th->th_off at offset up to ~74) before its m_pullup β€” out-of-contract mbuf reads on split-header TSO frames (DF-1373 class, different function)

Summary

TSO is enabled by default so oce_tso_setup runs for large TCP sends; it dereferences eh/ip/ip6 and th->th_off with no m_len check, and only then m_pullup(total_len). If the TCP header sits in a later mbuf, th->th_off reads stale bytes past m_len (inside the cluster) -> bogus total_len -> m_pullup fails -> silent ENXIO drop. Same class as DF-1373; distinct function. Pullup-first fix.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-4081 Β· 4 files
FileTypeDescriptionSize
README.md β€” 2.0 KB ↓ raw
VERDICT.md β€” 1.7 KB ↓ raw
verdict.json β€” 1.9 KB view raw
manifest.json β€” 609 B view raw

DF-4081 β€” oce_tso_setup parses IP/TCP headers before m_pullup (sibling of

DF-1373, different function): reads past m_len of the first mbuf

File: sys/dev/netif/oce/oce_if.c (pass 2) Class: parsing of attacker-influenced data (out-of-contract mbuf reads)

Root cause

oce_tso_setup() (oce_if.c:1021-1087) β€” reached from oce_tx() at oce_if.c:845 whenever m->m_pkthdr.csum_flags & CSUM_TSO β€” dereferences protocol headers before the m_pullup at line 1080:

  • 1046: eh = mtod(m, struct ether_vlan_header *); (18-byte read, no length check against m->m_len)
  • 1047-1052: eh->evl_encap_proto / eh->evl_proto reads
  • 1058/1068: ip / ip6 cast at m->m_data + ehdrlen β€” field reads
  • 1061/1071: th = (struct tcphdr *)((caddr_t)ip + (ip->ip_hl << 2));
  • 1063/1073: th->th_off dereferenced β€” offset ehdrlen+ihl*4 (up to 74) with no guarantee that many bytes are in the first mbuf (m_len).
  • only THEN: 1080 m = m_pullup(m, total_len);

CSUM_TSO frames are produced by every TCP connection over oce (TSO is in if_capabilities and if_capenable is set to capabilities at attach, oce_if.c:1697-1707), and the IP/TCP header split across mbufs is legal for locally-generated chains (e.g. unusual socket-buffer write patterns). When the TCP header is not (fully) in the first mbuf, th->th_off is read past m_len β€” stale cluster bytes within the MCLBYTES allocation (not an allocation-boundary overread; the same class DF-1373 established for oce_check_ipv6_ext_hdr). Consequence: garbage total_len β†’ m_pullup fails β†’ packet silently dropped (ENXIO at oce_if.c:849-852), or pullup to a wrong size. Correctness/robustness bug more than disclosure.

Fix

Pull up sizeof(*eh) + max(sizeof(struct ip6_hdr), sizeof(struct ip)) + sizeof(struct tcphdr) (or simply ETHER_HDR_LEN+sizeof(struct ip6_hdr)+64) before reading any header fields, and re-derive pointers afterwards β€” the same fix pattern as DF-1373.

Verification status

UNTESTED β€” hardware-gated (no oce NIC in QEMU, TSO path cannot be exercised).

VERDICT.md
↓ download raw

DF-4081 VERDICT β€” oce_tso_setup pre-pullup header reads

Status: UNTESTED (hardware-gated; no oce device in QEMU)

Source trace:

  • oce_if.c:842-853: CSUM_TSO frames take oce_tso_setup() on every TSO send.
  • TSO enabled by default: oce_if.c:1697-1699 adds IFCAP_TSO to capabilities, oce_if.c:1707 sets if_capenable = if_capabilities.
  • oce_if.c:1046-1073 dereferences eh / ip / ip6 / th (th->th_off at m_data+14+ip_hl*4, up to byte ~74 of the first mbuf) with no m_len check; the m_pullup that would guarantee contiguity is at oce_if.c:1080, after the reads.
  • If the TCP header lives in a later mbuf (legal chain shape from the socket layer), th->th_off reads stale bytes past m_len but inside the cluster β†’ bogus total_len β†’ m_pullup failure β†’ silent ENXIO drop (oce_if.c:849-852), or a wrong-size pullup consumed by the WQE builder.

Distinct from DF-1373 only in function (oce_check_ipv6_ext_hdr vs oce_tso_setup) β€” same defect class, both live on the TX path.

Fix

    m = *mpp;
+
+   m = m_pullup(m, ETHER_HDR_LEN + sizeof(struct ip6_hdr) +
+           sizeof(struct tcphdr));
+   if (m == NULL)
+       return NULL;
+   *mpp = m;

    if (M_WRITABLE(m) == 0) {

(re-derive eh/ip6/th after the pullup; existing pointer math then stays valid because m_pullup preserves data offsets.)

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
baseline no→ patch + rebuild →patched clean

Fix validation not possible: no OneConnect hardware/emulation in QEMU.

Guest pciconf -l: no 0x19a2/0x10df devices.
per-fix-DF-4081

Confirmed kernel references

Detail

Evidence (decisive lines)

README.md, VERDICT.md. Key lines: oce_if.c:1046-1073 (pre-pullup derefs), 1080 (pullup after), 842-852 (call site + drop), 1697-1707 (TSO on by default).

PoC changes

No runnable PoC: hardware-gated.

Verified recommended fix

m_pullup the ether+IP+TCP header region at the top of oce_tso_setup before any header dereference.

Verdict

oce_tso_setup (oce_if.c:1021-1087) reads ether/IP/TCP header fields β€” including th->th_off at byte offset up to ~74 β€” from the first mbuf before the m_pullup at line 1080, with no m_len validation. On TSO frames whose TCP header is not in the first mbuf, th->th_off reads past m_len (stale bytes within the cluster, in-allocation), producing a bogus total_len: m_pullup then fails and the packet is silently dropped. Sibling of pass-1 DF-1373 (oce_check_ipv6_ext_hdr), different function. Untested: no oce NIC in the QEMU guest.