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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3759 Β· 10 files
FileTypeDescriptionSize
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 to if_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 β†’ negative m_pkthdr.len (int underflow).
  • nsegs=15, rxlen=100 β†’ last-segment m_len = 100 - 14*1536 = -21404; the CRC-strip branch (if_alc.c:3375-3381) then drives the previous mbuf's m_len negative 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, no hw.alc sysctl, 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).

VERDICT.md
↓ download raw

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)

  1. 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).
  2. sys/dev/netif/alc/if_alc.c:3307 β€” sc->alc_cdata.alc_rxlen = RRD_BYTES(status); RRD_BYTES masks a 14-bit field (sys/dev/netif/alc/if_alcreg.h:1240-1241, RRD_LEN_MASK 0x00003FFF) β†’ 0..16383, fully device-controlled.
  3. The frame data itself was DMA'd into at most nsegs buffers of sc->alc_buf_size = RX_BUF_SIZE_MAX = 1536 bytes (sys/dev/netif/alc/if_alcreg.h:554, programmed at sys/dev/netif/alc/if_alc.c:3653-3654), each backed by an MCLBYTES (2048-byte) cluster allocated in alc_newbuf (sys/dev/netif/alc/if_alc.c:3187-3190).
  4. 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 at sys/dev/netif/alc/if_alc.c:3400 (ifp->if_input(ifp, m, NULL, -1)).
  5. 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 = 16379 on a 2048-byte cluster β†’ every consumer past if_input walks ~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 β†’ negative m_pkthdr.len; dropped by ether_input's minimum-length check in most paths, but with nsegs > 1 the CRC-strip branch (if_alc.c:3375-3381) computes rxtail->m_len -= (ETHER_CRC_LEN - mp->m_len) with mp->m_len a large negative int β†’ negative m_len inside the delivered chain β†’ mbuf invariant violation (m_pullup/m_adj/m_copydata walkers) β†’ panic. Modeled deterministically in trigger.c / run.log: nsegs=15, rxlen=100 gives last m_len = -21404 and previous m_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; no hw.alc sysctl; if_alc.ko exists only as an unloadable module file (env.txt). alc_attach/alc_rxeof unreachable at runtime.
  • Guest /usr/src/sys/dev/netif/alc/if_alc.c is byte-identical to the audited tree (md5 fb1a46ef2b2559de4aa5d508e03efcd5, env.txt).
  • trigger.c (userland arithmetic model of if_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_testable
baseline no→ patch + rebuild →patched clean

fix.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
↓ fix.diffper-fix-DF-3759

Confirmed kernel references

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).