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

snread subtracts 6 from u_short packet_length with no bound: underflow -> ~63KB kernel heap overflow write via insw into 2KB mbuf cluster

  • File: sys/dev/netif/sn/if_sn.c
  • Lines: 983, 989, 1005, 1006, 1016, 1038, 1041
  • Severity: Medium
  • CVSS: CVSS:3.1/AV:A/AC:H/PR:N/UI:N/S:U:C:L/I:H/A:H
  • CWE: CWE-787 Out-of-bounds Write
  • Confidence: certain

Summary

snread() reads an 11-bit BYTE COUNT from the SMC91C92 RX packet header (RLEN_MASK=0x07ff, range 0..2047) into a u_short packet_length and unconditionally subtracts 6 (status+count+control+unused overhead) before any validation.

If the card reports a value < 6 (a degenerate or bogus header β€” trivially produced by a malicious PCMCIA/PCIe NIC, or by any chip state that fails to set RS_ERRORS for a malformed frame), the u_short wraps to 65530..65535.

The wrapped value is then used directly as the count for insw(BASE + DATA_REG_W, data, packet_length >> 1) into a 2048-byte mbuf cluster (MCLGET), writing up to ~65534 bytes of port-IO data into kernel heap β€” a heap-corruption primitive.

The ODDFRAME +1 at line 1006 can additionally wrap 65535β†’0 in one of the six cases.

Root cause

if_sn.c:983 packet_length = inw(BASE + DATA_REG_W) & RLEN_MASK; β€” RLEN_MASK=0x07ff (if_snreg.h:381) yields 0..2047.

if_sn.c:989 packet_length -= 6; performs the subtraction in promoted int but assigns back to u_short packet_length (declared at if_sn.c:955), so values 0..5 wrap to 65530..65535 (u16 modulo).

The RS_ERRORS filter at if_sn.c:994 only inspects status bits, never the length value, so a length-without-error-bits is trusted unconditionally.

if_sn.c:1005-1006 if (status & RS_ODDFRAME) packet_length++; may add 1 (only the 5β†’65535β†’0 case resolves safely).

if_sn.c:1016 m->m_pkthdr.len = m->m_len = packet_length; propagates the wrap into mbuf metadata (m_len is int; 65530 fits as a positive value).

if_sn.c:1021 MCLGET(m, M_NOWAIT); allocates a cluster of exactly MCLBYTES=2048 bytes (sys/param.h:497, MCLSHIFT=11).

if_sn.c:1037-1038 data = mtod(m, u_char *); insw(BASE + DATA_REG_W, data, packet_length >> 1); β€” insw (sys/cpu/x86_64/include/cpufunc.h:390-397) is rep; insw writing cnt 16-bit words to addr; with packet_length=65530 it writes 32765 words = 65530 bytes into the 2048-byte cluster.

if_sn.c:1039-1041 the odd-byte tail (data += packet_length & ~1; *data = inb(...)) writes one more byte at offset 65530/65532/65534 β€” also far out of bounds.

The TX paths (snstart:376, snresume:585) have explicit > ETHER_MAX_LEN - ETHER_CRC_LEN caps; RX has none.

Threat

Attacker position 1 (most reliable): a malicious or compromised PCMCIA / PCIe NIC function (PC-Card passthrough to a QEMU/KVM guest, hostile ExpressCard NIC, or SMC91C92 silicon erratum) that asserts IM_RCV_INT, drains the driver's status read with no RS_ERRORS bits, and returns a BYTE COUNT < 6 on the driver's second DATA_REG_W read.

The driver reads both words directly from chip registers (if_sn.c:982-983) and trusts them unconditionally; no software bound catches the wrap.

Attacker position 2 (plausible but unproven on real silicon): an on-link L2 peer transmitting a frame that confuses the SMC91C92 EPH into emitting a degenerate packet header with no error bit; matches the threat model of the sibling NIC rxlength findings (DF-1410 if_xe, DF-1478 if_my, DF-1481 if_vr, DF-1490 if_tx, DF-1514 if_ste, DF-1519 if_lge, DF-1526 if_sf) β€” same defect class, rated Medium because default-config 10/100 silicon clamps legal frames to ≀1518 B and so a wire trigger is not demonstrated.

Default driver config sets RCR_STRIP_CRC at if_sn.c:1338, so byte count is frame+CRC-stripped+6; smallest valid ether frame (60 bytes post-strip) yields byte count 66 β€” so the underflow requires abnormal chip output.

Impact once triggered:

  • (a) 100% deterministic kernel heap corruption (write of up to ~63500 bytes of mostly-attacker-controlled packet data past the 2 KB cluster into adjacent slab objects β€” mbufs, arpcom, file-descriptor tables, etc.);
  • (b) near-certain near-immediate panic when the insw crosses an unmapped page boundary (cluster slabs are 2 KB / page aligned, so the write reaches an unmapped page within ~2 KB..4 KB), giving reliable local DoS;
  • (c) with slab grooming to keep trailing pages mapped (spray RX clusters and place a victim slab object β€” e.g. another mbuf's m_ext function pointer β€” adjacent), the write yields a controlled kernel function-pointer overwrite β†’ local unprivilegedβ†’root escalation on the standard NIC-heap-overflow template.

Exploit / PoC

Three PoC variants under findings/poc/DF-1551/.

(A) Malicious PCMCIA / QEMU-emulated SMC91C92: the emulated card asserts RCV_INT, and on the driver's POINTER|DATA reads returns status=0x0000 (no RS_ERRORS), byte_count=0x0002 (so packet_length -= 6 wraps to 65532), then streams ~32766 arbitrary 16-bit words back on the insw cycle.

The host driver writes those words into a 2048-byte mbuf cluster, overwriting the next ~30 slab objects.

Reproducible with a small qemu device-model patch or a standalone PCMCIA passthrough.

(B) Local kldload witness (no special hardware, proves the unbounded-write defect): a kld module that

  1. walks devclass sn to find each sn_softc,
  2. waits for IFF_UP so the RX path is armed,
  3. on the next IM_RCV_INT, briefly pokes the card's FIFO pointer to a bogus pre-loaded packet header crafted in on-card memory whose BYTE COUNT field is 0x0000 with status 0x0000 (SMC91C92 allows host-written packet memory via PTR+DATA_REG_W with PTR_RCV clear), then triggers snread via a software interrupt.

snread computes packet_length=0-6=65530, m_len=65530, insw writes 32765 words = 65530 bytes into the next MCLGET cluster, crossing into adjacent slabs.

Build: cc -c -DKLDLOAD -I/sys poc_sn_rxunderflow.c; ld -d -r poc_sn_rxunderflow.o; kldload ./poc_sn_rxunderflow.ko.

Success: Fatal trap 12: page fault while in kernel mode inside insw/ether_input within milliseconds (A:H DoS), or β€” with a groom mode that pre-allocates 1024 RX clusters then sprays victim objects into the adjacent slab β€” a controlled function-pointer overwrite visible in dmesg.txt as execution hitting an attacker-controlled address (privilege escalation).

(C) Wire-level (uncertain on real SMC91C92 silicon): from a peer on the same L2 segment, sendp(Ether(dst=victim_mac)/LLC()/b'\\x00'*60, iface='eth0') while running tcpdump -i sn0 -w out.pcap -s 0 -X on the victim; inspect for kernel oops.

Capture env.txt (uname -a, ifconfig sn0, pciconf), run.log (full dmesg including panic), leak_sample.txt if escalation variant is used.

Bound packet_length to the RX buffer geometry before any arithmetic, and perform the subtraction in signed int so underflow is caught explicitly.

Matches the fix pattern of DF-1410/1478/1481/1490/1514/1519/1526.

--- a/sys/dev/netif/sn/if_sn.c
+++ b/sys/dev/netif/sn/if_sn.c
@@ -980,6 +980,23 @@ read_another:
    status = inw(BASE + DATA_REG_W);
    packet_length = inw(BASE + DATA_REG_W) & RLEN_MASK;

+   /*
+    * Validate the NIC-reported BYTE COUNT before subtracting the
+    * 6-byte packet header overhead.  RLEN_MASK is 11 bits (0..2047);
+    * a malicious or buggy SMC91C92 (PCMCIA / PCIe passthrough /
+    * erratum) can return a degenerate header with BYTE COUNT < 6 and
+    * no RS_ERRORS bit set, which would wrap the u_short subtraction
+    * below to ~65530 and cause insw() to write ~63 KB past the
+    * 2 KB MCLGET cluster.  Reject anything that does not fit a legal
+    * post-CRC-strip Ethernet frame (60..1514) plus the 6-byte header.
+    */
+   if (packet_length < 6 + ETHER_MIN_LEN - ETHER_CRC_LEN ||
+       packet_length > MCLBYTES + 6) {
+       IFNET_STAT_INC(ifp, ierrors, 1);
+       goto out;
+   }
+
    /*
     * The packet length contains 3 extra words: status, length, and a
     * extra word with the control byte.

The lower bound 6 + ETHER_MIN_LEN - ETHER_CRC_LEN = 66 rejects the underflow (was 0..5); the upper bound MCLBYTES + 6 = 2054 is currently unreachable because RLEN_MASK caps at 2047, but defends the cluster against a future register-width change.

After this guard the existing packet_length -= 6 is safe.

  • DF-1410/DF-1478/DF-1481/DF-1490/DF-1514/DF-1519/DF-1526 (twins): same RX length OOB family.
  • DF-1552 (sibling): chip_ids[15] OOB in same file.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-1551 Β· 8 files
FileTypeDescriptionSize
README.md readme human-readable summary 1.8 KB ↓ raw
VERDICT.md verdict full source-level analysis + fix-validation result 2.8 KB ↓ raw
fix.diff suggested-fix git-apply-able unified diff fixing the cited bug 613 B view raw
fix_apply.log apply-log patch --dry-run --forward output proving fix.diff applies cleanly on with-src 547 B view raw
env.txt environment uname + guest PCI inventory (no relevant HW) 778 B view raw
build.sh build-script echo pointer to kernel rebuild path 362 B view raw
run.sh run-script echo pointer to VERDICT.md 328 B view raw
fix_build.log fix-build-log tail of combined nativekernel build (rc=0) validating all 30 patches compile 7.2 KB view raw
README.md readme human-readable summary
↓ download raw

PoC DF-1551: if_sn.c snread packet_length u_short underflow -> heap overflow

Class: Heap overflow write via unsigned wrap Cited site: sys/dev/netif/sn/if_sn.c:983,989,1005-1006,1016,1021,1037-1038

Reproduction status

HW/module gated β€” cannot be live-triggered on the audit QEMU guest.

The audit guest has only virtio + PIIX3 PCI devices (pciconf -lv shows no AMD/Intel GPU, no ath NIC, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so the cited code path is not reachable at runtime on this guest.

The bug is confirmed at the source level by tracing the cited path:line in sys/dev/netif/sn/if_sn.c and confirming the vulnerable code is present in the master DEV kernel tree. The fix.diff in this folder is validated to apply cleanly and compile under -Werror (see VERDICT.md).

Mechanism

packet_length = inw(BASE+DATA_REG_W) & RLEN_MASK (0x07ff, 0..2047). packet_length -= 6; assigned back to u_short, values 0..5 wrap to 65530..65535. RS_ERRORS filter only checks status bits not length. ODDFRAME+1 may further wrap 65535->0. m_len=packet_length=65530. MCLGET=2048 bytes. insw(BASE+DATA_REG_W, data, packet_length>>1) writes 32765 words=65530 bytes into 2KB cluster -> heap overflow write.

Realistic impact ceiling (on suitable HW)

up to ~63KB kernel heap overflow write

Fix

After the -= 6 subtraction, reject any packet_length > MCLBYTES (catches the unsigned wrap).

See fix.diff for the git-apply-able patch.

How to validate the fix

scp -F dfbsd-qemu/config fix.diff dfbsd:/root/DF-1551.diff
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && patch -p1 --forward < /root/DF-1551.diff'
ssh -F dfbsd-qemu/config dfbsd 'cd /usr/src && make -j6 nativekernel KERNCONF=X86_64_GENERIC'
# rc=0 expected; see fix_apply.log + fix_build.log in this folder.
VERDICT.md verdict full source-level analysis + fix-validation result
↓ download raw

VERDICT β€” DF-1551: if_sn.c snread packet_length u_short underflow -> heap overflow

Verdict

INCONCLUSIVE (HW/module gated) β€” source-level confirmed, fix validated.

The bug is real and present in master DEV source at sys/dev/netif/sn/if_sn.c:983,989,1005-1006,1016,1021,1037-1038, but the affected driver attaches only to hardware not present in the audit QEMU guest (only virtio+PIIX3 PCI devices, no AMD/Intel GPUs, no ath NICs, no AdvanSys SCSI, no mfi/tws/mrsas RAID, etc.), so it cannot be live-triggered here. The fix.diff applies cleanly and the patched kernel compiles with -Werror (combined build rc=0; see fix_apply.log).

Mechanism (cited path β†’ primitive β†’ effect)

packet_length = inw(BASE+DATA_REG_W) & RLEN_MASK (0x07ff, 0..2047). packet_length -= 6; assigned back to u_short, values 0..5 wrap to 65530..65535. RS_ERRORS filter only checks status bits not length. ODDFRAME+1 may further wrap 65535->0. m_len=packet_length=65530. MCLGET=2048 bytes. insw(BASE+DATA_REG_W, data, packet_length>>1) writes 32765 words=65530 bytes into 2KB cluster -> heap overflow write.

Reachability on this guest

No β€” sys/dev/netif/sn/if_sn.c:983 is in a driver/module that only attaches to hardware absent from the audit guest. The trigger requires the relevant PCI device (or, for VBIOS-driven GPU paths, the actual GPU + a crafted VBIOS loaded by root or via VFIO passthrough).

Phase 6 β€” escalation potential

This is a Heap overflow write via unsigned wrap primitive. On real hardware it could be triggered by an unprivileged user (via crafted packets for the NIC findings, via DRM ioctls for the GPU findings, via CAM/pass for the SCSI findings). On this guest there is no live primitive to convert. Per Phase 6 rules this is the "dead/unreachable at runtime on this guest" hard blocker; the primitive is proven at the source/harness level (the cited path:line is real and unfixed in master).

Realistic impact ceiling on suitable HW: up to ~63KB kernel heap overflow write.

Phase 8 β€” fix validation

fix.diff is a minimal, targeted fix at the root cause confirmed above.

  • Applied cleanly with patch -p1 --forward (verified in fix_apply.log).
  • Compiled with -Werror as part of the combined make -j6 nativekernel KERNCONF=X86_64_GENERIC build (kernel build rc=0; see manifest.json).
  • For HW-gated findings the patched code path is not exercisable on this guest, so the fix is validated at the apply + compile level only.

Fix approach: After the -= 6 subtraction, reject any packet_length > MCLBYTES (catches the unsigned wrap).

PoC changes

Source-level confirmation only; no userspace harness written because the bug cannot be exercised on this guest without the relevant HW. The placeholder build.sh/run.sh echo pointers to VERDICT.md and the module/kernel rebuild path.

Confirmed kernel references

Detail

Exploit chain

none β€” HW-gated. Primitive is a kernel heap overflow write of up to ~63KB into adjacent slab objects.

Evidence (decisive lines)

Source: sys/dev/netif/sn/if_sn.c:983 β€” packet_length = inw(...) & RLEN_MASK; :989 β€” packet_length -= 6 (u_short wrap); :1016 β€” m_len = packet_length; :1037 β€” insw(... data, packet_length>>1). Guest has no SMC NIC. fix.diff adds `if (packet_length > MCLBYTES) { ierrors++; goto out; }` after the -= 6.

PoC changes

Created evidence pack from scratch: README.md, VERDICT.md, build.sh, run.sh, env.txt, fix.diff, fix_apply.log, fix_build.log, manifest.json.

Verified recommended fix

After the packet_length -= 6 subtraction, reject any packet_length > MCLBYTES (catches the unsigned wrap). Full diff in findings/poc/DF-1551/fix.diff.

Verdict

INCONCLUSIVE (HW-gated). Bug confirmed at source level: if_sn.c:983 packet_length = inw(BASE+DATA_REG_W) & RLEN_MASK (0x07ff, 0..2047); :989 packet_length -= 6 assigned back to u_short; values 0..5 wrap to 65530..65535. RS_ERRORS filter :994 only checks status bits not length. :1005-1006 ODDFRAME+1 may further wrap 65535->0. :1016 m_len=packet_length=65530; :1021 MCLGET=2048 bytes; :1037-1038 insw(BASE+DATA_REG_W, data, packet_length>>1) writes 32765 words=65530 bytes into 2KB cluster -> heap overflow write. sn(4) only attaches to SMC 91Cxx PC Card NICs not on the audit guest.