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 readssin_addr.s_addrat offset 4 (4 bytes). Both have the same root cause: missingsa_lenvalidation 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 holdCAP_NONET_RAWfor 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)whereshort_sais 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.
Recommended fix
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
sys/netinet/raw_ip.c:632— the unguarded read.sys/netinet/raw_ip.c:551,579—rip_bind/rip_connectwith the correctsa_lenguard.- DF-0619 — the IPv6 twin in
rip6_send.
Timeline
- 2026-07-02 Discovered during automated DragonFlyBSD kernel security audit.
- 2026-07-02 Reported to DragonFlyBSD security contact (pending).
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0650 · 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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
sendtocalls 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, allsendtocalls with the 4-byte sockaddr returnEINVALimmediately; 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.
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)
- Trigger (userspace).
sendto(rawsock, buf, 4, 0, sa, tolen)withtolen∈ {2,3,4,5,6,7} andsa->sa_family = AF_INET. sys/kern/uipc_syscalls.c:1513-1532getsockaddr(). Accepts anytolen >= offsetof(struct sockaddr, sa_data[0]) == 2. Allocateskmalloc(tolen, M_SONAME)and copiestolenbytes from userspace. Fortolen=4the buffer is exactly 4 bytes (sa_len,sa_family, 2 bytes ofsa_data).sys/netinet/raw_ip.c:632rip_send(). Readsdst = ((struct sockaddr_in *)nam)->sin_addr.s_addr.sin_addrlives at offset 4..7 of asockaddr_in. Withtolen<8the read runs past the allocation by(8 - tolen)bytes. Nosa_lencheck is performed — contrastrip_bind:551andrip_connect:579which both gate onnam->sa_len == sizeof(struct sockaddr_in).sys/netinet/raw_ip.c:250rip_output().ip->ip_dst.s_addr = dst;— the leaked 4 bytes become the IPv4 destination of the emitted raw packet.- Observable.
tcpdump -i vtnet0 -nnshows the emitted packets;ip_dstis 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 atsys/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=0from this bug alone. - Realistic impact ceiling: 4 bytes of kernel heap leak per call.
With slab grooming, an attacker controlling
SYSCAP_NONET_RAWcould leak adjacent slab-neighbor bytes (e.g. fromstruct 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.
Recommended fix
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
fixedvalidated
see evidence pack
Confirmed kernel references
—
Detail
Exploit chain
none
Evidence (decisive lines)
—
Verdict
REPRODUCED (live). rip_send sa_len
No comments yet.