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

sockaddr address fields read before sa_len validation in tcp_usr_bind/connect/preconnect and tcp6_usr_bind/connect (DF-3357 family): heap read of recycled/uninitialized bytes past the user-copied length (v6: up to 16 bytes past the allocation)

Summary

getsockaddr kmallocs exactly namlen (>=2) bytes and copies only that many; tcp_usr_bind (:316-321), tcp_usr_connect (:597-602) and tcp_usr_preconnect (:965-968) then read sin_addr (offsets 4..7) and the v6 twins read sin6_addr (offsets 8..23) plus pass the short buffer to prison_remote_ip - all BEFORE the sa_len gates in in_pcbbind/in_pcbladdr_find/in6_pcbbind/in6_pcbladdr. For namelen 2..4 (v4) the multicast decision consumes fully uncopied bytes; for namlen 8 (v6) sin6_addr is read 16 bytes past the 8-byte slab chunk into neighboring chunks. VERIFIED the read-before-validate exists live (5-byte sockaddr with byte4=224 returns EAFNOSUPPORT, proving the handler consumes the full sin_addr despite sa_len<16) but ceiling is a 1-bit error-code oracle - no data leaves the kernel; on the stock slab allocator the recycled-byte oracle could not be flipped (~600k groomed attempts: cross-CPU frees go to z_RChunks and the v6 neighbor byte is structurally sa_len=8) - honest not_reproduced/impact-none. Fix: validate sa_len >= sizeof(sockaddr_in/in6) before any field read at the five sites (diff in pack, inspection-correct).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3372 Β· 16 files
FileTypeDescriptionSize
oob_oracle.c β€” 3.3 KB view raw
probe.c β€” 2.2 KB view raw
probe_affinity.c β€” 1.0 KB view raw
probe_hammer.c β€” 945 B view raw
probe_fleet.c β€” 1017 B view raw
run.log β€” 1.2 KB view raw
probe.log β€” 2.4 KB view raw
probe2.log β€” 1.0 KB view raw
probe_matrix.log β€” 1.0 KB view raw
probe3.log β€” 73 B view raw
VERDICT.md β€” 3.4 KB ↓ raw
README.md β€” 3.0 KB ↓ raw
run.sh β€” 93 B view raw
fix.diff β€” 1.2 KB view raw
verdict.json β€” 3.5 KB view raw
manifest.json β€” 1.4 KB view raw

DF-3372 β€” sockaddr fields read before sa_len validation in TCP bind/connect

What

tcp_usr_bind (sys/netinet/tcp_usrreq.c:316-321), tcp_usr_connect (:597-602), tcp_usr_preconnect (:965-968), tcp6_usr_bind (:406-411) and tcp6_usr_connect (:640-656, incl. the prison_remote_ip call) cast the user-supplied sockaddr to sockaddr_in/sockaddr_in6 and read sin_addr (offsets 4..7) / sin6_addr (offsets 8..23) before any length validation. The deep validators (in_pcbbind in_pcb.c:648, in_pcbladdr_find in_pcb.c:954, in6_pcbbind in6_pcb.c:129, in6_pcbladdr in6_pcb.c:282) all check sa_len first β€” but they run after these pre-checks.

getsockaddr() (sys/kern/uipc_syscalls.c:1513-1532) kmallocs exactly namelen bytes (2..255) and copies only that many user bytes, so:

  • v4, namelen 2..4: sin_addr is read entirely beyond the copied data (recycled, uninitialized 8-byte slab chunk contents; zoneindex() maps sizes 2..8 to the 8-byte class).
  • v6, namelen 2..23: sin6_addr (offsets 8..23) is read up to 16 bytes past an 8-byte allocation (cross-object read into neighboring slab chunks); prison_remote_ip (kern_jail.c:469-472) reads the same bytes with no length check of its own.

Same defect family as DF-3357 (udp_usrreq.c), where the UDP code used the OOB address as a packet destination. In TCP the bytes only steer the multicast rejection (EAFNOSUPPORT vs the later EINVAL from the length check) β€” a 1-bit oracle.

PoC

  • oob_oracle.c β€” grooms the M_SONAME 8-byte slab class with a controlled sin_addr via UDP bind(2) (which allocates+frees the same chunk), then binds/connects a TCP socket with a 2-byte sockaddr. Success criterion: errno flips 22 (EINVAL) β†’ 47 (EAFNOSUPPORT) when the groom address is multicast.
  • Probes: probe.c-series (probe*.log) sweep groom vectors (bind/sendto, 8/16-byte, 2..256 reps, 48-process fleet).

Result (honest)

The read is certain from the code path and the multicast decision is live (a 5-byte sockaddr whose copied byte 4 = 224 deterministically returns EAFNOSUPPORT; = 8 returns EINVAL β€” proving the handler consumes sin_addr regardless of sa_len < 16). However the recycled-byte oracle could not be flipped on the stock kernel across ~600k groomed attempts: DF's slab allocator frees cross-CPU chunks to the owning CPU's z_RChunks (kern_slaballoc.c:1491-1547), which a userland-thread allocation never immediately reuses, and the v6 cross-object neighbor byte is structurally the next chunk's sa_len (=8, never 0xff). So: defect real, observable impact on this system: none demonstrated β†’ verdict not_reproduced (reproduced=0, impact=none).

Build / run

cc -O2 -include unistd.h -o oob_oracle oob_oracle.c
./oob_oracle

Fix sketch

Validate length (and family) before touching address fields in each of the five sites, e.g. in tcp_usr_bind/tcp_usr_connect/tcp_usr_preconnect:

if (nam->sa_len < sizeof(struct sockaddr_in))
        return EINVAL;          /* or goto out; as appropriate */
VERDICT.md
↓ download raw

DF-3372 VERDICT β€” sockaddr read-before-validate in TCP bind/connect paths

Reproduced?

No observable manifestation β€” not_reproduced (defect itself certain by code path).

What was verified

  1. The read is real (by construction). getsockaddr (sys/kern/uipc_syscalls.c:1523) kmallocs exactly namelen bytes (β‰₯2, ≀255) and copies only those; tcp_usr_bind (sys/netinet/tcp_usrreq.c:316-321) / tcp_usr_connect (:597-602) / tcp_usr_preconnect (:965-968) then read sin_family and sin_addr.s_addr (offsets 4..7) before in_pcbbind's sa_len == 16 gate (sys/netinet/in_pcb.c:648). The v6 twins (tcp6_usr_bind :406-411, tcp6_usr_connect :640-656) read sin6_addr (offsets 8..23) β€” for namelen 8 this is 16 bytes fully past the 8-byte slab chunk β€” and additionally pass the short buffer to prison_remote_ip (sys/kern/kern_jail.c:469-472), which reads address fields with no length check of its own.

  2. The multicast decision consumes sin_addr regardless of sa_len (guest-verified, deterministic): bind with a 5-byte sockaddr (sa_len=5, copied byte 4 = 224) β†’ EAFNOSUPPORT; byte 4 = 8 β†’ EINVAL (see probe2.log / probe_matrix.log). This proves the handler bases its decision on a sockaddr_in whose declared length (5) is smaller than the fields it reads (8 bytes of sin_addr); for namelen ≀ 4 the decision bytes are entirely uncopied.

  3. The recycled-byte oracle does not flip on the stock allocator (the honest negative): ~600k groomed attempts (8-byte and 16-byte M_SONAME grooms via UDP bind and sendto, 2..256 reps, single- and 48-process fleets, 200k-iteration hammers β€” probe.log, probe2.log, probe3.log) produced zero EAFNOSOURCE flips for namelen ≀ 4. Root cause of the resilience: DF's slab allocator puts cross-CPU frees on the owning CPU's z_RChunks remote list (sys/kern/kern_slaballoc.c:1491-1547) which user-thread allocations do not immediately reuse, and the v6 cross-object byte the multicast test samples is the neighbouring chunk's sa_len byte (structurally 8, never 0xff). Affinity pinning (usched_set) is privileged, so the owner-CPU LIFO reuse could not be forced unprivileged.

Impact ceiling

1-bit error-code oracle (EAFNOSUPPORT vs EINVAL) on kernel heap contents beyond the user-copied length, per bind/connect attempt β€” but not reliably groomable on the stock kernel (see 3). No data leaves the kernel; no write primitive. This is the DF-3357 family with a much lower ceiling because TCP never uses the OOB address (UDP did).

Classification

status=not_reproduced, reproduced=0, impact=none, confidence=certain (that the OOB read occurs), severity Low, CWE-125.

Fix

Validate sa_len (and family) before any field read at the five sites, e.g. tcp_usr_bind:

if (nam->sa_len < sizeof(struct sockaddr_in)) {
        error = EINVAL;
        goto out;
}

mirrored for connect/preconnect (sizeof(sockaddr_in)) and the v6 twins (sizeof(sockaddr_in6)), plus a length guard inside prison_remote_ip callers or in prison_remote_ip itself.

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 not kernel-validated: the defect has no observable manifestation on the stock system (oracle unflippable), so a patched kernel cannot be differentiated by this PoC; the length-check fix is trivially correct by inspection.

fix.diff (sa_len guards at the five sites)
↓ fix.diffper-fix-DF-3372

Confirmed kernel references

Detail

Evidence (decisive lines)

['probe_matrix.log: len=5 sa[4]=224 -> errno 47 vs sa[4]=8 -> errno 22 (decision consumes sin_addr with sa_len<16); len<=4 always 22 across 64x grooms', 'probe3.log: 200k-iteration hammer + 48-process fleet: zero EAFNOSUPPORT flips', 'VERDICT.md: full code-path proof and allocator-topology analysis']

PoC changes

PoC authored for this run: grooms M_SONAME size classes via UDP bind/sendto with controlled sockaddr content, then TCP bind/connect with 2-8 byte sockaddrs; probes swept groom size (8/16), vector (bind/sendto), repetitions (2..256), namelen (2..5), affinity (usched_set denied), fleet (48 procs) and 200k-iteration hammers.

Verified recommended fix

Validate nam->sa_len >= sizeof(struct sockaddr_in)/sin6 before reading address fields at the five tcp_usrreq.c sites (and length-check inside prison_remote_ip).

Verdict

The out-of-bounds/uninitialized read is certain from the code path (getsockaddr kmallocs exactly namelen bytes and tcp_usr_bind/tcp_usr_connect/tcp_usr_preconnect/tcp6_usr_bind/tcp6_usr_connect read sin_addr/sin6_addr before any sa_len validation), and the multicast decision is live on partially-copied addresses (5-byte sockaddr with byte4=224 deterministically returns EAFNOSUPPORT vs EINVAL, proving the handler consumes the full sin_addr regardless of sa_len). However the observable oracle (groomed recycled bytes flipping EAFNOSUPPORT for namelen<=4) never fired across ~600k attempts (8/16-byte M_SONAME grooms via UDP bind and sendto, single/fleet/hammer variants): DF's slab allocator routes cross-CPU frees to the owning CPU's z_RChunks list which user-thread allocations do not reuse, affinity pinning (usched_set) is privileged, and the v6 cross-object byte the multicast test samples is structurally the neighbor chunk's sa_len (=8, never 0xff). Defect real (CWE-125, same family as DF-3357), demonstrated impact on stock system: none.