DragonFlyBSD Kernel Audit
← triage · dashboard
DF-0650

Heap OOB read in rip_send via unvalidated sockaddr length (IPv4 twin of DF-0619)

Field Value
ID DF-0650
Status new
Severity Medium
CVSS 3.1 CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:U/C:L/I:N/A:N
CWE CWE-125 Out-of-bounds Read
File sys/netinet/raw_ip.c
Lines 632
Area netinet (IPv4 raw socket send path)
Confidence certain
Discovered 2026-07-02
Reported pending

Cross-reference: This is the IPv4 twin of DF-0619 (rip6_send). The IPv6 version reads the full 28-byte sockaddr_in6; this IPv4 version reads sin_addr.s_addr at offset 4 (4 bytes). Both have the same root cause: missing sa_len validation before the cast-and-dereference.

Summary

rip_send reads ((struct sockaddr_in *)nam)->sin_addr.s_addr (offset 4, 4 bytes) from a kernel heap buffer whose size is exactly the user-supplied sendto(2) tolen. getsockaddr() (uipc_syscalls.c:1513) accepts any tolen in [2,255] and forces sa_len=tolen, so tolen in [2,7] yields a buffer smaller than 8 bytes and the read at offset 4 runs off the end. The leaked bytes become the destination IPv4 address of the outgoing raw packet.

Root cause

raw_ip.c:632 in rip_send():

631:    } else {
632:        dst = ((struct sockaddr_in *)nam)->sin_addr.s_addr;  /* no sa_len check */
633:        error = rip_output(m, so, dst);

The cast-and-dereference happens unconditionally. Contrast with rip_bind (raw_ip.c:551: if (nam->sa_len == sizeof(*addr))) and rip_connect (raw_ip.c:579: if (nam->sa_len != sizeof(*addr))), which both gate on sa_len. Only rip_send skips the check.

Threat model & preconditions

  • Privilege: SYSCAP_NONET_RAW (root on stock installs). In a capability-sandboxed deployment, a process may legitimately hold CAP_NONET_RAW for raw packet injection while being denied direct kernel-memory read access — this OOB read becomes a sandbox-escape information channel leaking kernel heap bytes.
  • Trigger: sendto(rawsock, buf, 1, 0, &short_sa, 4) where short_sa is a 4-byte sockaddr. The leaked 4 bytes appear as the IPv4 destination of the emitted packet, recoverable by the same caller via tcpdump/pcap.
  • Impact: 4-byte kernel heap info leak per call. No crash.

Gate the cast on nam->sa_len exactly as rip_bind/rip_connect already do:

--- a/sys/netinet/raw_ip.c
+++ b/sys/netinet/raw_ip.c
@@ -628,6 +628,10 @@
        if (nam == NULL) {
            m_freem(m);
            error = ENOTCONN;
+       } else if (nam->sa_len != sizeof(struct sockaddr_in) ||
+              ((struct sockaddr_in *)nam)->sin_family != AF_INET) {
+           m_freem(m);
+           error = EINVAL;
        } else {
            dst = ((struct sockaddr_in *)nam)->sin_addr.s_addr;

References

Timeline

  • 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
  • 2026-07-02 Reported to DragonFlyBSD security contact (pending).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0650 · 14 files
FileTypeDescriptionSize
df0650_poc.c trigger-source raw socket OOB-read demonstrator varying sa_len in {2..7} 3.6 KB view raw
build.sh build-script cc -O -o df0650_poc df0650_poc.c 183 B view raw
run.sh run-script ipfw bucket counts + tcpdump on vtnet0 1.3 KB view raw
build.log build-log final successful build 103 B view raw
run.log run-log decisive run with leaked-dst captures 3.0 KB view raw
env.txt environment uname, cc, sysctl 234 B view raw
VERDICT.md verdict full narrative + evidence 4.3 KB ↓ raw
README.md readme how to reproduce 2.1 KB ↓ raw
fix.diff suggested-fix sa_len+sin_family guard, matches rip_bind/rip_connect 679 B view raw
fix_baseline.log fix-baseline-log PoC on unpatched #0 kernel: 96 emitted, 0 failed (leak observed) 3.0 KB view raw
fix_build.log fix-build-log full nativekernel build of single-fix kernel (NK_DONE rc=0) 5.6 MB ↓ download
fix_patched.log fix-patched-log PoC on patched #1 kernel: 0 emitted, 96 failed (no leak) 734 B view raw
../fix_build_combined.log build-log Combined 41-finding kernel build (rc=0, -Werror clean) 5.6 MB ↓ download
../fix_build_summary.txt build-summary Summary of the combined 41-finding kernel build 826 B view raw
README.md readme how to reproduce
↓ download raw

DF-0650 — Heap OOB read in rip_send via short sockaddr (raw IP)

Bug

sys/netinet/raw_ip.c:632 in rip_send():

} else {
    dst = ((struct sockaddr_in *)nam)->sin_addr.s_addr;   /* no sa_len check */
    error = rip_output(m, so, dst);
}

The cast-and-dereference happens unconditionally — there is no nam->sa_len == sizeof(struct sockaddr_in) check. Contrast rip_bind (line 551) and rip_connect (line 579), which both gate on sa_len. Only rip_send skips the check.

getsockaddr() (sys/kern/uipc_syscalls.c:1513) accepts any tolen in [2, SOCK_MAXADDRLEN] and forces sa_len = tolen, kmalloc(tolen, M_SONAME). With tolen=4 the buffer is only 4 bytes (sa_len, sa_family, 2 bytes of sa_data), but the sin_addr.s_addr read at offset 4 reads 4 bytes past the end. The leaked 4 bytes become the IPv4 destination of the emitted raw packet (ip->ip_dst.s_addr = dst at raw_ip.c:250).

Build / Run

./build.sh
# tcpdump on lo0 + run PoC in parallel:
./run.sh

The PoC requires root (SYSCAP_NONET_RAW enforced at raw_ip.c:473). On each sendto with a 4-byte sockaddr, the kernel reads 4 OOB bytes from the M_SONAME slab and emits a raw IP packet with those bytes as ip_dst. tcpdump on lo0 catches the packets whose leaked dst happens to land in 127.0.0.0/8; the rest fail with ENETUNREACH (silent drop) but the OOB read still occurred.

Expected (bug present)

  • A fraction of sendto calls succeed (seen_nonzero > 0).
  • tcpdump on lo0 captures packets whose IPv4 dst varies across runs and is not under our control = leaked heap residue.

Expected (fixed)

  • With the proposed sa_len == sizeof(struct sockaddr_in) guard, all sendto calls with the 4-byte sockaddr return EINVAL immediately; no packet emitted, no OOB read.

Threat model

  • Privilege: SYSCAP_NONET_RAW (root on stock installs). This is a privileged-local→kernel info leak — root→kernel hardening gap, not an unpriv→root escalation.
  • Impact: 4-byte heap info leak per call. Recoverable via tcpdump when leaked dst is routeable.
VERDICT.md verdict full narrative + evidence
↓ download raw

DF-0650 — VERDICT

Verdict: REPRODUCED (4-byte heap info leak, root-only)

The bug at sys/netinet/raw_ip.c:632 is real, live, and demonstrated.

Mechanism (every hop cited)

  1. Trigger (userspace). sendto(rawsock, buf, 4, 0, sa, tolen) with tolen ∈ {2,3,4,5,6,7} and sa->sa_family = AF_INET.
  2. sys/kern/uipc_syscalls.c:1513-1532 getsockaddr(). Accepts any tolen >= offsetof(struct sockaddr, sa_data[0]) == 2. Allocates kmalloc(tolen, M_SONAME) and copies tolen bytes from userspace. For tolen=4 the buffer is exactly 4 bytes (sa_len, sa_family, 2 bytes of sa_data).
  3. sys/netinet/raw_ip.c:632 rip_send(). Reads dst = ((struct sockaddr_in *)nam)->sin_addr.s_addr. sin_addr lives at offset 4..7 of a sockaddr_in. With tolen<8 the read runs past the allocation by (8 - tolen) bytes. No sa_len check is performed — contrast rip_bind:551 and rip_connect:579 which both gate on nam->sa_len == sizeof(struct sockaddr_in).
  4. sys/netinet/raw_ip.c:250 rip_output(). ip->ip_dst.s_addr = dst; — the leaked 4 bytes become the IPv4 destination of the emitted raw packet.
  5. Observable. tcpdump -i vtnet0 -nn shows the emitted packets; ip_dst is the leaked heap residue.

Evidence (decisive, from findings/poc/DF-0650/run.log)

The PoC sets in-buffer sin_addr bytes (offset 4..L-1) to 10.0.2.X so the leaked bytes appear in ip_dst on vtnet0. With each smaller sa_len, one more byte becomes OOB:

sa_len in-buffer sin_addr bytes leaked bytes observed ip_dst
7 10.0.2 1 byte 10.0.2.255
6 10.0 2 bytes 10.0.255.255
5 10 3 bytes 10.255.255.255
2/3/4 (none) 4 bytes 255.255.255.255

The leaked bytes (0xFF in this layout) are slab residue from the allocation immediately adjacent to our short M_SONAME chunk — bytes we never wrote. They cannot come from our sendto buffer; they are read out-of-bounds by rip_send. The fact that they are deterministic (0xFF on this kernel) and scale with 8 - sa_len is the OOB signature.

Across 16 reps × 6 sa_len values = 96 sendto calls, all 96 emitted raw IP packets with the predicted dst pattern. ipfw counts:

00100  40 ... ip from 10.0.2.0/24 to 10.0.2.0/24    # sa_len=7 (1 byte OOB)
00300  48 ... ip from 10.0.2.0/24 to 255.255.255.255 # sa_len=2/3/4 + broadcasts
00400 120 ... ip from 10.0.2.0/24 to any

tcpdump -i vtnet0 captures the actual leaked dsts (above table).

Privilege / threat model

  • Privilege required: SYSCAP_NONET_RAW, enforced at sys/netinet/raw_ip.c:473 (caps_priv_check(ai->p_ucred, SYSCAP_NONET_RAW)). On a stock install that means root.
  • This is a privileged-local→kernel info leak — a root→kernel hardening gap, not an unpriv→root escalation. There is no path to uid=0 from this bug alone.
  • Realistic impact ceiling: 4 bytes of kernel heap leak per call. With slab grooming, an attacker controlling SYSCAP_NONET_RAW could leak adjacent slab-neighbor bytes (e.g. from struct inpcb, struct socket, mbuf tags) one read at a time. No write primitive.

Why this is "leak" not "panic"

The OOB read stays within mapped slab memory for small overshoots because slab allocations are bucket-packed; large overshoots could cross a page boundary and panic, but the natural exploit regime is small (1..4 byte) leak via ip_dst.

PoC changes

The PoC dir shipped empty from the orchestrator; I authored df0650_poc.c from scratch. Key design choices: - Bind the raw socket to 10.0.2.15 (vtnet0 IP) so ip_output does not fail with EADDRNOTAVAIL on source selection. - Set in-buffer sin_addr bytes to 10.0.2.X so leaked packets land in 10.0.2.0/24 and tcpdump on vtnet0 can see them. - Iterate sa_len in {2..7} to show the leak scales with 8 - sa_len.

Apply the same sa_len guard that rip_bind:551 and rip_connect:579 already use. The proposed diff in the finding markdown is correct; findings/poc/DF-0650/fix.diff carries a verified, git-apply-able version that adds the same sa_len == sizeof(struct sockaddr_in) and sin_family == AF_INET check.

Fix verification

fixed

validated

see evidence pack
↓ fix.diffDragonFly 6.5-DEVELOPMENT #1: Sun Jul 19 04:42:22 UTC 2026

Confirmed kernel references

Detail

Exploit chain

none

Evidence (decisive lines)

Verdict

REPRODUCED (live). rip_send sa_len 4B heap leak into ip_dst. Root-only. Kernel rebuild fix validated.