ale_rxeof trusts the controller-DMA'd 14-bit RRS frame length with no bound vs page/max_frame_size: m_devget heap OOB read up to ~14.8KB past the DMA Rx page into delivered mbufs (kernel heap info leak) β DF-3604/3759/3933 family, ale instance
Summary
length = ALE_RX_BYTES(rs->length) is a device-fed 14-bit field used at m_devget(rs+1, length-ETHER_CRC_LEN) with no bound against ale_max_frame_size or remaining page space; the error-bit filter is itself device-fed. The last in-page rs copies up to 14819/8195 bytes past the bus_dmamem_alloc'd page into adjacent kernel heap, passed to if_input -> disclosure to bpf/raw/TCP sockets. Legit frames max 8132 so any length above is a device lie. First finding ever on this file (pass 1 clean). Length<4 underflow benign here (m_devget tot<=0 => drop).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3989 Β· 9 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger | hardware-precondition model / protocol replica | 3.6 KB | view raw |
| README.md | readme | build/run/expected | 932 B | β raw |
| VERDICT.md | verdict | full path:line trace | 3.9 KB | β raw |
| build.sh | script | exact build commands | 65 B | view raw |
| run.sh | script | exact run commands | 89 B | view raw |
| build.log | log | full untrimmed build output | 298 B | view raw |
| run.log | log | full untrimmed runtime output | 1.1 KB | view raw |
| env.txt | env | guest hardware/src-md5 proof | 1.1 KB | view raw |
| fix.diff | fix | git-apply-able unified diff | 794 B | view raw |
DF-3989 β ale_rxeof trusts the controller-DMA'd 14-bit RRS frame length with no bound vs the Rx page / ale_max_frame_size: m_devget copies up to 16379 bytes from a DMA page of 198144 (non-jumbo) / 204768 (jumbo) bytes β heap OOB read of up to ~14.8KB of adjacent kernel memory, delivered to the network stack (kernleak). DF-3604 (bce) / DF-3759 (alc) / DF-3933 (age) family, ale(4) instance. NOTE: unlike bce/alc, the length<4 underflow is SAFE here (m_devget(int tot<=0) returns NULL β drop).
BUILD cc -O2 -Wall -Wextra -o trigger trigger.c
RUN ./trigger (page-geometry model; prints OOB extents)
On the audit guest: builds and runs clean (run.log). Hardware-gated: guest has virtio-net only; if_ale is compiled into the kernel but no 1969:1026 device exists (env.txt).
EXPECTED "non-jumbo ... OOB READ past DMA page: 14819 bytes" "jumbo ... OOB READ past DMA page: 8195 bytes" TRIGGER_RC=0.
DF-3989 VERDICT β source-level certain, runtime hardware-gated (inconclusive)
Root cause (path:line)
ale_rxeof β sys/dev/netif/ale/if_ale.c:2153-2254:
2204: length = ALE_RX_BYTES(le32toh(rs->length)); /* 14-bit, if_alereg.h:675:
ALE_RD_LEN_MASK 0x3FFF0000 */
2216: if ((status & (ALE_RD_CRC | ALE_RD_CODE | ... | ALE_RD_TRUNC)) != 0) {
2219: ale_rx_update_page(sc, &rx_page, length, &prod);
2220: continue; /* only error-bit filter */
2221: }
2232: m = m_devget(rs + 1, length - ETHER_CRC_LEN, 0, ifp);
length is a device-fed 14-bit field (0..16383) used with NO bound against
sc->ale_max_frame_size or the remaining space in the DMA'd Rx page. The
error-bit filter at :2216 is itself device-fed, so it bounds nothing against
a lying controller. m_devget (sys/kern/uipc_mbuf.c:2235) bcopy's
length - 4 bytes from rx_page->page_addr + rx_page->cons + 8 into a
fresh mbuf chain passed to ifp->if_input (:2249) β i.e. into BPF/sockets.
Page geometry (if_alevar.h:110-116, if_ale.c:936-941)
- ALE_RX_PAGE_SZ = roundup(ALE_MAX_FRAMELEN=1522,32) Γ 128 = 196608.
- allocated page = roundup(guard + 196608, 32): non-jumbo (AR8113/8114) guard=1522 β 198144; jumbo (AR8121) guard=8132 β 204768.
rx_page->consis only reset when it reaches ALE_RX_PAGE_SZ (ale_rx_update_page :2068-2075), so the last in-page rs sits at cons = 196576; the copy then reads up to 196576+8+16379 = 212963:- non-jumbo: 14819 bytes past the DMA allocation,
- jumbo: 8195 bytes past.
Adjacent kernel heap is copied into the delivered mbuf β kernel info leak to any local BPF/raw/TCP listener (or the wire for unicast flows). Legit frames max out at 8132 bytes (jumbo MTU 8110 + 18 + 4), so any length in (8132..16383] is a device lie that this driver would faithfully copy.
Why this is NOT a re-report
First pass over if_ale.c found nothing (verified: no findings rows for this file). Sibling family findings exist for OTHER drivers (DF-3604 bce, DF-3759 alc, DF-3933 age); this is the ale(4) instance of the same vendor family defect, independently derived.
Distinct from bce/alc siblings
The length < ETHER_CRC_LEN underflow is benign in ale because the
arithmetic stays uint32 and is passed to int tot in m_devget:
tot <= 0 β the copy loop never runs β returns NULL β iqdrops drop
(if_ale.c:2233-2237). So ale has ONLY the over-read half of the family bug
(no negative-m_len panic variant).
Verification on the guest (honest)
- trigger.c models the page geometry and copy extents; builds and runs on the guest (run.log, RC=0).
- Hardware-gated: guest ifconfig shows vtnet0/lo0 only;
pciconf -lvhas zero 0x1969 (Atheros) devices;if_aleis compiled into the kernel (kldload refuses: already in kernel) but no hw.ale sysctl exists β ale_attach/ale_rxeof unreachable. Status inconclusive per family convention (DF-3759/DF-3604). - Guest src md5 ae824d7a4429b82f0ad92a72b7c91da8 == audited tree (env.txt).
Threat model
Faulty or malicious AR8121/AR8113/AR8114 controller (PCIe DMA attacks, broken firmware, QEMU pcie-passthrough of hostile hardware). Impact: kernel heap disclosure (read-only). No write primitive from this bug alone.
Fix
fix.diff drops the frame (error counter + page advance + continue) when
length > sc->ale_max_frame_size || length < ETHER_HDR_LEN + ETHER_CRC_LEN,
using the file's own error-frame idiom (:2216-2221). git-apply clean;
not runtime-validable (no Atheros NIC on the guest).
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.diff adds the max_frame_size / minimum bound using the file's own error-frame early-return idiom (cf. if_ale.c:2216-2221). Cannot be runtime-validated: guest has no Atheros device. Patch touches only existing symbols on an already-compiled path; git-apply clean.
fix.diff; hardware absence proof in env.txt (NO_ATHEROS_PCI / no hw.ale)
Confirmed kernel references
Detail
Exploit chain
On real hardware: faulty/malicious AR8121/AR8113/AR8114 DMAs an RRS with bytes=16383 and no error bits near the end of an Rx page -> m_devget copies 16379 bytes including up to ~14.8KB past the DMA page -> unprivileged user with a bpf/raw/TCP socket on ale0 reads adjacent kernel heap (info leak). No write primitive from this bug alone (copy is inbound-only).
Evidence (decisive lines)
run.log (page model: 14819/8195-byte OOB extents, RC=0), env.txt (NO_ATHEROS_PCI, no hw.ale, if_ale in kernel unattached, guest-src md5 match), VERDICT.md (path:line trace), trigger.c (annotated hardware precondition), fix.diff (drop-frame bound, git-apply clean)
PoC changes
No seed PoC (pass-1 clean file). Authored trigger.c as an annotated hardware-precondition + page-geometry model; honest hardware-absent verification per family convention (DF-3759 pattern).
Verified recommended fix
In ale_rxeof, drop the frame (advance the page, count ierrors) when length > sc->ale_max_frame_size or length < ETHER_HDR_LEN + ETHER_CRC_LEN (see fix.diff).
Verdict
Source-level certain, runtime hardware-gated (inconclusive per family convention). ale_rxeof (sys/dev/netif/ale/if_ale.c:2153) takes the received frame length from the controller-DMA'd RRS 14-bit field (ALE_RX_BYTES on rs->length, mask 0x3FFF0000, if_alereg.h:673-683) and uses it at :2232 m_devget(rs+1, length-ETHER_CRC_LEN, 0, ifp) with NO bound against sc->ale_max_frame_size or the remaining space in the DMA'd Rx page; the error-bit filter :2216 is itself device-fed and bounds nothing. m_devget bcopy's length-4 bytes from page_addr+cons+8 (sys/kern/uipc_mbuf.c:2235) into a fresh mbuf passed to if_input (:2249). Page geometry: allocated page = roundup(guard + 196608, 32) = 198144 (non-jumbo, guard 1522) / 204768 (jumbo AR8121, guard 8132); cons is only reset at >= ALE_RX_PAGE_SZ (ale_rx_update_page :2068-2075) so the last in-page rs sits at cons=196576 and the copy reads up to 212963 -> 14819 / 8195 bytes PAST the bus_dmamem_alloc'd page into adjacent kernel heap, delivered to BPF/sockets (kernel heap info leak). Legit frames max out at 8132 bytes, so any length in (8132..16383] is a device lie faithfully copied. Distinct from the bce/alc siblings: the length<4 underflow is SAFE in ale (uint32 arithmetic passed to m_devget's int tot; tot<=0 -> loop never runs -> NULL -> iqdrops drop at :2233-2237). Runtime: guest has virtio-net only, zero 0x1969 devices, no hw.ale sysctl (if_ale in kernel but never attached) -> ale_rxeof unreachable; trigger.c (page-geometry model) builds and runs on the guest (run.log RC=0, both OOB extents). Guest src md5 ae824d7a4429b82f0ad92a72b7c91da8 == audited tree. First finding ever filed against if_ale.c (pass 1 clean) - family instance of DF-3604(bce)/DF-3759(alc)/DF-3933(age), independently derived.
No comments yet.