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
inswcrosses 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_extfunction 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
- walks devclass
snto find eachsn_softc, - waits for
IFF_UPso the RX path is armed, - 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 is0x0000withstatus 0x0000(SMC91C92 allows host-written packet memory viaPTR+DATA_REG_WwithPTR_RCVclear), then triggerssnreadvia 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.
Recommended fix
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.
Related findings
- 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)
PoC verification
Evidence pack
findings/poc/DF-1551 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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 β 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 infix_apply.log). - Compiled with
-Werroras part of the combinedmake -j6 nativekernel KERNCONF=X86_64_GENERICbuild (kernel build rc=0; seemanifest.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
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- s
- n
- /
- i
- f
- _
- s
- n
- .
- c
- :
- 9
- 8
- 3
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- s
- n
- /
- i
- f
- _
- s
- n
- .
- c
- :
- 9
- 8
- 9
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- s
- n
- /
- i
- f
- _
- s
- n
- .
- c
- :
- 1
- 0
- 1
- 6
- s
- y
- s
- /
- d
- e
- v
- /
- n
- e
- t
- i
- f
- /
- s
- n
- /
- i
- f
- _
- s
- n
- .
- c
- :
- 1
- 0
- 3
- 7
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.
No comments yet.