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

div_output() reads sin_port and sin_addr from an under-sized sendto() destination sockaddr (offsets 4..8 and 8..12 past a >=4-byte allocation); OOB bytes steer re-inject direction and the ipfw resume rule

Summary

getsockaddr() kmalloc's exactly namelen bytes (minimum 4) and div_send() forwards it to div_output() with no length validation anywhere in between. div_output() reads sin->sin_port (offset 4..8; fully OOB for namelen<8) into divinfo->skipto - the firewall resume point on reinject - and evaluates DIV_IS_OUTPUT(sin) on sin->sin_addr (offset 8..12; for namelen=4 in an 8-byte slab chunk this is the ADJACENT chunk's first 4 bytes) to choose ip_output() vs ip_input(). Same family as the verified DF-3357 (udp_send) and the filed DF-0650 (rip_send); divert is the only instance where the OOB bytes include a neighboring object. Divert sockets root-only. Guest verification proved the unvalidated path executes (sendto with 4/9-byte sockaddr succeeds, packet reinjected) but no deterministic value readback was achievable (ipfw hooks ether-layer-only; one_pass resume consumes the skipto value; slab grooming does not land on this allocator) - honest not_reproduced with the marker proven. Fix: sa_len/family check at div_send entry.

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-3440 Β· 8 files
FileTypeDescriptionSize
div3440.c β€” 4.8 KB view raw
divleak.c β€” 4.4 KB view raw
divsteer.c β€” 5.0 KB view raw
run.log β€” 2.1 KB view raw
README.md β€” 1.2 KB ↓ raw
VERDICT.md β€” 2.2 KB ↓ raw
verdict.json β€” 2.2 KB view raw
manifest.json β€” 603 B view raw

DF-3440 β€” div_output() reads an under-sized destination sockaddr

div_send() (sys/netinet/ip_divert.c:494-508) hands the user sendto() destination to div_output() (ip_divert.c:317-373) with no length validation. getsockaddr() (kern/uipc_syscalls.c:1513-1532) kmalloc's exactly namelen bytes (min 4). div_output() reads:

  • sin->sin_port at offset 4..8 (OOB for namelen < 8) -> divinfo->skipto
  • sin->sin_addr at offset 8..12 (OOB for namelen < 12) -> re-inject direction via DIV_IS_OUTPUT(sin) (== 0 -> ip_output, else -> ip_input)

Both values steer kernel control flow from bytes past the user allocation.

Build / run (root: divert socket)

cc -O -o div3440 div3440.c && ./div3440        # counter/oracle variant
cc -O -o divleak divleak.c && ./divleak        # skipto-canary readback
cc -O -o divsteer divsteer.c && ./divsteer     # groom steering variant

Observed

Every sendto() with a 4- or 9-byte sockaddr returns success β€” the kernel accepted an under-sized destination and executed the OOB reads (reinject observed in ip counters). Deterministic value steering / readback could not be produced on this allocator (grooms don't land; same experience as the verified DF-3357 PoC on this guest). See run.log and VERDICT.md.

VERDICT.md
↓ download raw

DF-3440 β€” VERDICT

Status: not_reproduced (impact demonstrated as none; the defect itself is source-certain and its code path provably executes).

What is certain (source)

  • div_send -> div_output with zero validation between getsockaddr() (4-byte minimum allocation) and the field reads (ip_divert.c:344-350).
  • sin_port (4..8) becomes divinfo->skipto β€” the firewall resume rule on reinject; sin_addr (8..12) selects ip_output vs ip_input.
  • For namelen=4 (8-byte chunk): sin_port = own-chunk slack (recycled heap), sin_addr = adjacent chunk bytes 0..4.

What was demonstrated on the guest

  • sendto(divert_fd, pkt, 48, {4-byte sockaddr}, 4) succeeds (rc=48) β€” the under-sized sockaddr flowed through div_output and the packet was reinjected (ip counters moved). Same for 9-byte sockaddrs. 12+ shots.
  • The OUTPUT/INPUT branch machinery works (16-byte controls produced the expected reinject signatures in netstat -s -p ip deltas).

What could NOT be demonstrated

  • Deterministic steering/readback of the OOB bytes:
  • counter oracles are per-CPU/noisy on DF;
  • the ipfw hooks exist only in the ether layer (if_ethersubr.c:466,1061), so loopback reinjects never re-match a divert rule;
  • with one_pass=0 the resume rule equals the very skipto bytes under test (never < 100 in observed heap states -> rule 100 never re-matched);
  • groom chunks are not the ones the trigger allocation picks (identical conclusion to the verified DF-3357 PoC on this same guest/allocator).
  • Hence: no observable value leak to record; classified not_reproduced with impact none. The defect remains real (read-past-object into kernel control flow on a root-only socket) β€” filed Medium on code certainty, consistent with the DF-0650 twin (unvalidated rip_send sockaddr, filed Medium).

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.

(No fix.diff for this item: documented recommendation only - see Fix above.)

Fix verification

not_testable
per-fix-DF-3440

Confirmed kernel references

Detail

Evidence (decisive lines)

['findings/poc/DF-3440/run.log β€” 12+ successful under-sized sendto shots + oracle diagnosis', 'findings/poc/DF-3440/divsteer.c divleak.c div3440.c β€” three oracle designs']

PoC changes

Three oracle variants were built (netstat counters, ipfw2-divert skipto readback, groom+usched_set-pinned steering); none produced a deterministic observable for the OOB byte values.

Verified recommended fix

In div_send (or at div_output entry), reject nam->sa_len != sizeof(struct sockaddr_in) with EINVAL before any field read; do not trust sin_port/sin_addr from short sockaddrs.

Verdict

div_output() reads sin_port (offset 4..8) and sin_addr (8..12) from a user sendto() sockaddr allocated with as few as 4 bytes; the reads provably execute (sendto with 4/9-byte sockaddr succeeds and the packet is reinjected), and the OOB bytes steer the re-inject direction and the ipfw resume rule. Deterministic value control or readback could not be produced on this guest: counter oracles are per-CPU/noisy, ipfw hooks are ether-layer-only so loopback reinjects never re-match a divert rule, one_pass resume points equal the OOB bytes themselves, and slab grooming does not reliably land (same verified experience as DF-3357 on this allocator).