alc_rxeof trusts the controller-DMA'd 14-bit RRS frame length with no bound vs nsegs*alc_buf_size/MCLBYTES: heap OOB read up to ~14.3KB past a 2KB RX cluster (kernel heap info leak) and negative m_len (panic)
Summary
alc_rxeof takes the received frame length from RRD_BYTES(status) (14-bit, 0..16383) and uses it directly for m->m_pkthdr.len and the last segment's m_len, while the frame data lives in at most nsegs buffers of alc_buf_size=1536 bytes. No check bounds rxlen against nsegs*alc_buf_size or ETHER_CRC_LEN. A faulty/malicious AR8131/8132/8151/8152/816x/817x (or descriptor corruption) hands the stack an mbuf describing 16379 bytes in a 2048-byte cluster (OOB read into bpf/socket receive paths = kernel heap disclosure to an unprivileged user), or lengths that underflow to negative m_len via the CRC-strip arithmetic (-21404/-19872 modeled deterministically) -> panic. Distinct from this file's DF-1207 (array-index half); same class as DF-3604; FreeBSD head has the identical missing bound (inherited).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-3759 Β· 10 files| File | Type | Description | Size | |
|---|---|---|---|---|
| README.md | β | 3.0 KB | β raw | |
| VERDICT.md | β | 4.9 KB | β raw | |
| trigger.c | β | 5.0 KB | view raw | |
| build.sh | β | 96 B | view raw | |
| run.sh | β | 194 B | view raw | |
| run.log | β | 1.5 KB | view raw | |
| env.txt | β | 459 B | view raw | |
| fix.diff | β | 1.0 KB | view raw | |
| manifest.json | β | 1.6 KB | view raw | |
| verdict.json | β | 4.7 KB | view raw |
DF-3759 β alc(4) RRS frame-length trust in alc_rxeof
Severity: Medium (hardware-gated kernel heap OOB read / info leak + panic
variant) β DragonFly sys/dev/netif/alc/if_alc.c, pass 2 (GLM 5.3).
What
alc_rxeof() (sys/dev/netif/alc/if_alc.c:3293-3405) takes the frame length
for a received frame straight from the Rx Return Structure the controller
DMA'd into the return ring:
sc->alc_cdata.alc_rxlen = RRD_BYTES(status); /* :3307 */
...
m->m_pkthdr.len = sc->alc_cdata.alc_rxlen - ETHER_CRC_LEN; /* :3368 */
if (nsegs > 1)
mp->m_len = sc->alc_cdata.alc_rxlen - (nsegs - 1) * sc->alc_buf_size; /* :3372 */
else
m->m_len = m->m_pkthdr.len; /* :3386 */
RRD_BYTES is a 14-bit field (if_alcreg.h:1240-1241, values 0..16383),
nsegs (RRD_RD_CNT) is 4 bits (0..15), and the mbufs backing a frame are
MCLBYTES (2048-byte) clusters (alc_newbuf, if_alc.c:3187-3190) of which
the controller may use at most alc_buf_size = RX_BUF_SIZE_MAX = 1536 bytes
each (if_alcreg.h:554, programmed at if_alc.c:3653-3654). No check
anywhere on the path bounds alc_rxlen against nsegs * alc_buf_size or
ETHER_CRC_LEN.
On a faulty or malicious AR8131/8132/8151/8152/816x/817x (or after Rx return-ring descriptor corruption):
rxlen=16383, nsegs=1β an mbuf describing 16379 bytes in a 2048-byte cluster is handed toif_input(if_alc.c:3400); everything downstream (bpf, ether_input, ip_input, TCP, socket copy-out) reads up to ~14.3KB past the cluster β kernel heap disclosure to unprivileged sockets.rxlen < ETHER_CRC_LENβ negativem_pkthdr.len(int underflow).nsegs=15, rxlen=100β last-segmentm_len = 100 - 14*1536 = -21404; the CRC-strip branch (if_alc.c:3375-3381) then drives the previous mbuf'sm_lennegative too β delivered chain violates mbuf invariants β panic in mbuf walkers.
Same class as DF-3604 (bce), DF-1478 (my), DF-1481 (vr), DF-1490 (tx),
DF-1514 (ste), DF-1519 (lge), DF-1562 (pcn), DF-1351 (nfe). Distinct from
the file's DF-1207, which covers the array index half
(rxdesc[rx_cons] with unvalidated rx_cons/nsegs, :3304/:3329) β this
finding is the length half (m_len/pkthdr.len vs cluster size), same
descriptor, different sink and different primitive.
Reproduce
- build:
cc -O2 -Wall -Wextra -o trigger trigger.c(on the guest or any box; pure userland arithmetic model of if_alc.c:3368-3386) - run:
./triggerβ prints the corrupted lengths the stack would receive for the three device-controlled shapes. - The real trigger needs the NIC itself (see VERDICT.md); the audit guest
has no Atheros PCI device (env.txt:
NO_ATHEROS_PCI,ifconfig -l=vtnet0 lo0, nohw.alcsysctl, driver only present as an unloadable module file/boot/kernel/if_alc.ko).
Fix
fix.diff β drop the frame when alc_rxlen > nsegs * alc_buf_size or
alc_rxlen < ETHER_CRC_LEN, using the file's own error-frame early-return
idiom (cf. if_alc.c:3322-3324; buffers are reused in place, hardware
re-fetches them).
DF-3759 β VERDICT
Status: source-level certain; runtime honestly inconclusive (hardware-gated β no Atheros NIC on the audit guest). reproduced=0, impact=none-on-this-guest; class ceiling when it fires on real hardware: kernel heap info leak (up to ~14.3KB OOB read per frame) or panic (negative m_len).
Root cause (path:line)
sys/dev/netif/alc/if_alc.c:3302-3305βalc_rxeof()parses the Rx Return Structure that the controller DMA'd:status,rdinfo,nsegs = RRD_RD_CNT(rdinfo)(4 bits, 0..15),rx_cons = RRD_RD_IDX(rdinfo)(12 bits).sys/dev/netif/alc/if_alc.c:3307βsc->alc_cdata.alc_rxlen = RRD_BYTES(status);RRD_BYTESmasks a 14-bit field (sys/dev/netif/alc/if_alcreg.h:1240-1241,RRD_LEN_MASK 0x00003FFF) β 0..16383, fully device-controlled.- The frame data itself was DMA'd into at most
nsegsbuffers ofsc->alc_buf_size = RX_BUF_SIZE_MAX = 1536bytes (sys/dev/netif/alc/if_alcreg.h:554, programmed atsys/dev/netif/alc/if_alc.c:3653-3654), each backed by an MCLBYTES (2048-byte) cluster allocated inalc_newbuf(sys/dev/netif/alc/if_alc.c:3187-3190). - No bound is ever enforced between (2) and (3):
*
sys/dev/netif/alc/if_alc.c:3368-3369βm->m_pkthdr.len = alc_rxlen - ETHER_CRC_LEN;*sys/dev/netif/alc/if_alc.c:3372-3373βmp->m_len = alc_rxlen - (nsegs - 1) * sc->alc_buf_size;*sys/dev/netif/alc/if_alc.c:3385-3386β nsegs==1:m->m_len = m->m_pkthdr.len;The mbuf is handed to the stack atsys/dev/netif/alc/if_alc.c:3400(ifp->if_input(ifp, m, NULL, -1)). - FreeBSD upstream has the identical defect (verified against FreeBSD head
sys/dev/alc/if_alc.c:3640,3701-3715β same expressions, no bound), i.e. this is inherited, not a DFly regression; the audit files it because the primitive is identical to the already-filed DF-3604 family.
Consequences (device-controlled)
rxlen=16383, nsegs=1βm_len = pkthdr.len = 16379on a 2048-byte cluster β every consumer pastif_inputwalks ~14.3KB past the cluster. bpf listeners and (more usefully to an attacker) the TCP/UDP receive path (m_copydataβuiomove) copy the "payload" to userland β kernel heap info disclosure to an unprivileged local user with a socket on the interface. No write primitive from this bug alone (the NIC's DMA into the posted buffer is bounded by the buffer-size register).rxlen < ETHER_CRC_LENβ negativem_pkthdr.len; dropped byether_input's minimum-length check in most paths, but withnsegs > 1the CRC-strip branch (if_alc.c:3375-3381) computesrxtail->m_len -= (ETHER_CRC_LEN - mp->m_len)withmp->m_lena large negative int β negativem_leninside the delivered chain β mbuf invariant violation (m_pullup/m_adj/m_copydata walkers) β panic. Modeled deterministically intrigger.c/run.log: nsegs=15, rxlen=100 gives lastm_len = -21404and previousm_len = -19872.
Why not DF-1207 (no re-report)
DF-1207 = the index half: rxdesc[rx_cons] OOB because rx_cons
(12-bit) / nsegs are unvalidated (if_alc.c:3304/:3329). DF-3759 is the
length half: even with perfectly valid indices, the mbuf metadata
(m_len / m_pkthdr.len) is derived from an unbounded device field. Same
descriptor, different sink (mbuf length metadata vs array index), different
primitive (OOB read via the stack vs direct rxdesc[] OOB). Both exist
independently.
Verification performed (guest, DragonFly 6.5-DEVELOPMENT x86_64)
- Guest has no Atheros PCI device:
pciconf -l | grep -i -E 'atheros|1969'βNO_ATHEROS_PCI;ifconfig -lβvtnet0 lo0; nohw.alcsysctl;if_alc.koexists only as an unloadable module file (env.txt).alc_attach/alc_rxeofunreachable at runtime. - Guest
/usr/src/sys/dev/netif/alc/if_alc.cis byte-identical to the audited tree (md5fb1a46ef2b2559de4aa5d508e03efcd5, env.txt). trigger.c(userland arithmetic model ofif_alc.c:3368-3386) builds and runs on the guest (run.log,TRIGGER_RC=0) and prints the exact corrupted lengths for the three device-controlled shapes β the OOB (>cluster) and negative (<0) cases are demonstrated as arithmetic, the DMA precondition is documented in the source header.
Fix
fix.diff (git-apply clean against the read-only tree): drop the frame
when alc_rxlen > nsegs * alc_buf_size || alc_rxlen < ETHER_CRC_LEN, using
the file's own error-frame early-return idiom (cf. if_alc.c:3322-3324;
on early return the buffers are simply reused in place β the same
mechanism the existing CRC/runt error path relies on).
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 nsegs*alc_buf_size / ETHER_CRC_LEN bound using the file's own error-frame early-return idiom (cf. if_alc.c:3322-3324; buffers reused in place). Cannot be runtime-validated: guest has no Atheros device, so neither baseline nor patched behavior is exercisable. The patch touches only existing symbols on an already-compiled path (git-apply clean).
fix.diff
Confirmed kernel references
- sys/dev/netif/alc/if_alc.c:3307
- sys/dev/netif/alc/if_alc.c:3368
- sys/dev/netif/alc/if_alc.c:3372
- sys/dev/netif/alc/if_alc.c:3375
- sys/dev/netif/alc/if_alc.c:3386
- sys/dev/netif/alc/if_alc.c:3400
- sys/dev/netif/alc/if_alc.c:3187
- sys/dev/netif/alc/if_alc.c:3653
- sys/dev/netif/alc/if_alcreg.h:1240
- sys/dev/netif/alc/if_alcreg.h:554
Detail
Exploit chain
On real hardware: (1) device DMAs an RRS with RRD_VALID set, rd_cnt=1, bytes=16383, no error bits; (2) alc_rxeof hands the stack an mbuf with m_len=m_pkthdr.len=16379 backed by a 2048-byte cluster; (3) an unprivileged user with a TCP/UDP socket on alc0 receives up to ~14.3KB of kernel heap adjacent to the cluster (info leak); variant bytes=100/rd_cnt=15 delivers negative m_len (-21404/-19872) -> mbuf walker panic (local DoS). No write primitive from this bug alone.
Evidence (decisive lines)
run.log (trigger model output: OOB and negative lengths computed deterministically + presence probe), env.txt (NO_ATHEROS_PCI / NO_ALC_KLD / no hw.alc / md5 of guest src == audited src), VERDICT.md (path:line trace), trigger.c (annotated hardware precondition), fix.diff (drop-frame bound, git-apply clean)
PoC changes
No seed PoC. Authored trigger.c as an annotated hardware-precondition + userland arithmetic model of if_alc.c:3368-3386 (a syscall-level PoC is meaningless for a DMA-device bug); honest hardware-absent verification per family convention (DF-3604 pattern).
Verified recommended fix
In alc_rxeof, drop the frame when alc_rxlen > nsegs * sc->alc_buf_size or alc_rxlen < ETHER_CRC_LEN (see fix.diff).
Verdict
Source-level certain: alc_rxeof (sys/dev/netif/alc/if_alc.c:3307) takes the received frame length from the controller-DMA'd RRS 14-bit field (RRD_BYTES, if_alcreg.h:1240) and uses it to set m->m_pkthdr.len (:3368) and the last mbuf's m_len (:3372, :3386) with NO bound against nsegs * alc_buf_size (1536) or ETHER_CRC_LEN, on MCLBYTES(2048) clusters from alc_newbuf (:3187). A faulty/malicious AR8131/8132/8151/8152/816x/817x can hand the stack an mbuf describing up to 16379 bytes in a 2048-byte cluster -> ~14.3KB heap OOB read past the cluster into bpf/socket receive paths (kernel heap info leak to an unprivileged local user), or lengths that underflow to negative m_len inside the delivered chain via the CRC-strip arithmetic at :3375-3381 (mbuf invariant violation -> panic). Runtime verification honestly inconclusive per family convention: the audit guest has virtio-net only, no Atheros PCI device (env.txt NO_ATHEROS_PCI, ifconfig=vtnet0 lo0, no hw.alc sysctl, if_alc.ko present but not loaded), so alc_attach/alc_rxeof are unreachable. Guest /usr/src copy is byte-identical to the audited tree (md5 fb1a46ef2b2559de4aa5d508e03efcd5). trigger.c (userland arithmetic model of :3368-3386) builds and runs on the guest (run.log TRIGGER_RC=0) and prints the exact corrupted lengths for the three device-controlled shapes. Distinct from this file's DF-1207 (index half: rxdesc[rx_cons] OOB) - this is the length half; same descriptor, different sink, different primitive. Same class as DF-3604 (bce), DF-1478 (my), DF-1481 (vr), DF-1490 (tx), DF-1514 (ste), DF-1519 (lge), DF-1562 (pcn), DF-1351 (nfe); FreeBSD head has the identical missing bound (inherited defect, verified against FreeBSD sys/dev/alc/if_alc.c:3640,3701-3715).
No comments yet.