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)
PoC verification
Evidence pack
findings/poc/DF-3440 Β· 8 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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_portat offset 4..8 (OOB for namelen < 8) -> divinfo->skiptosin->sin_addrat 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.
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_outputwith zero validation betweengetsockaddr()(4-byte minimum allocation) and the field reads (ip_divert.c:344-350).sin_port(4..8) becomesdivinfo->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 ipdeltas).
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_testableConfirmed 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).
No comments yet.