DF-0743 — struct greip/mobip_h overlay assumes ip_hl == 5 (IP options misparse) ============================================================================= Verdict ------- **REPRODUCED** (misparse / read-class). The overlay derefs read IP-OPTION bytes instead of the GRE/mobile header whenever the outer encapsulating IP header carries options (`ip_hl > 5`). Confirmed two independent ways: 1. **Deterministic harness** (`harness.c`) — replicates the kernel's exact struct-overlay derefs from `ip_gre.c` and shows EVERY field is misread. 2. **Live in-kernel discriminator** (`live_trigger.c` + `live_run.sh`) — a GRE packet with `ip_hl=7` and a valid real GRE header is silently DROPPED by the buggy kernel (ptype misread → switch default → `return(0)`) and correctly DECAPSULATED by the fixed kernel. Impact class: **misparse / DoS / potential info-leak** (read-class). No write primitive, no escalation. Finding ------- `sys/netinet/ip_gre.c:gre_input2()` overlays `struct greip` at `mtod(m)`: struct greip *gip = mtod(m, struct greip *); /* ip_gre.c:133 */ ... flags = ntohs(gip->gi_flags); /* ip_gre.c:151 */ ... switch (ntohs(gip->gi_ptype)) { /* ip_gre.c:164 */ `struct greip { struct ip gi_i; struct gre_h gi_g; }` (if_gre.h:90) places `gi_flags` at a FIXED byte offset 20 and `gi_ptype` at offset 22 — the layout of a 20-byte (`ip_hl==5`) IP header followed immediately by the GRE header. `sys/netinet/ip_gre.c:gre_mobile_input()` makes the identical assumption with `struct mobip_h { struct ip mi; struct mobile_h mh; }` (if_gre.h:141), reading `mh.proto` @20, `mh.odst` @24, `mh.osrc` @28 and checksumming the range @20..31 — all fixed offsets that correspond to `ip_hl==5`: struct mobip_h *mip = mtod(m, struct mobip_h *); /* ip_gre.c:210 */ if(ntohs(mip->mh.proto) & MOB_H_SBIT) { ... /* ip_gre.c:223 */ mip->mi.ip_src.s_addr = mip->mh.osrc; /* ip_gre.c:225 */ mip->mi.ip_dst.s_addr = mip->mh.odst; /* ip_gre.c:229 */ mip->mi.ip_p = (ntohs(mip->mh.proto) >> 8); /* ip_gre.c:230 */ if (gre_in_cksum((u_short*)&mip->mh,msiz) != 0) { /* ip_gre.c:232 */ When the OUTER encapsulating IP header carries IP options (`ip_hl > 5`), the real GRE/mobile header begins at byte offset `ip_hl*4 > 20`, but the overlay still dereferences offsets 20/22/24/28 — i.e. it reads the IP-OPTION bytes, not the GRE/mobile header. Reachability (confirmed live) ----------------------------- `ip_input` computes `hlen = IP_VHL_HL(ip->ip_vhl) << 2` (ip_input.c:525/536) and only guarantees `m->m_len >= hlen` (KASSERT ip_input.c:539). It passes `hlen` (== `ip_hl*4`, options included) as `*offp` to the protocol input (ip_input.c:415 `pr->pr_input(&m, &hlen, ip->ip_p)`). For packets with `ip_hl > 5`, ip_input.c:661 calls `ip_dooptions()`. For benign options (NOP / record-route / timestamp that do not require forwarding) `ip_dooptions` returns 0 and the packet CONTINUES to transport processing with the options still in the header and `hlen > 20`. The packet then reaches ip_input -> ip_protox[47|55] -> encap4_input (ip_encap.c:131) -> (*psw->pr_input)() = gre_input -> gre_input2 (IPPROTO_GRE) = gre_mobile_input (IPPROTO_MOBILE) `gre_input2` receives `hlen = ip_hl*4` (correct for the pointer-arithmetic strip: `hlen += sizeof(struct gre_h); m->m_data += hlen;`), but the overlay field reads at the FIXED offsets 20/22 ignore `ip_hl`. So a packet with IP options is deterministically misparsed. (The strip math is right; only the field reads are wrong.) Is this subsumed by the DF-0742 `m_pullup` fix? NO. `m_pullup(m, sizeof(struct greip))` guarantees the first 24 bytes are contiguous in the head mbuf, but the overlay STILL reads offsets 20/22 as fixed fields. With `ip_hl > 5` those offsets are IP-option bytes regardless of contiguity. DF-0743 is an orthogonal bug (offset assumption, not mbuf-chaining). Likewise DragonFly's gre path does NOT strip/advance past IP options before overlaying — the overlay is directly on `mtod(m)`. Proof #1 — deterministic harness (harness.c) -------------------------------------------- Pure userspace C, no privilege. Builds an mbuf-shaped buffer with an outer IP header of `ip_hl=7` (8 bytes of options @20..27) and the REAL GRE/mobile header at offset 28, then performs the kernel's overlay derefs byte-for-byte and compares with the correct `ip_hl*4`-aware reads: GRE path (gre_input2), ip_hl=7: gi_flags overlay=0xaaaa correct=0x0000 *** MISMATCH *** gi_ptype overlay=0xbbbb correct=0x0800 *** MISMATCH *** decision: overlay DROP (0xbbbb != ETHERTYPE_IP) vs correct ACCEPT *** DECISION DIVERGES *** MOBILE path (gre_mobile_input), ip_hl=7: mh.proto overlay=0xcccc correct=0x0080 *** MISMATCH *** mh.odst overlay=0xdddddddd correct=0x0200000a *** MISMATCH *** mh.osrc overlay=0x7aeb8000 correct=0x0300000a *** MISMATCH *** gre_in_cksum overlay=0x7c8b FAIL->DROP vs correct=0x0000 PASS *** CHECKSUM DECISION DIVERGES *** Every overlay field is wrong; the accept/drop and checksum decisions diverge. Proof #2 — live in-kernel discriminator (live_trigger.c + live_run.sh) ---------------------------------------------------------------------- A single gre0 (GRE mode) tunnel `127.0.0.1 -> 127.0.0.1` is configured; `tcpdump -i gre0` captures the decapsulated INNER packets (gre_input2 BPF-taps the inner packet at ip_gre.c:184-189 ONLY when decapsulation succeeds). Two packets are injected: Packet A: `ip_hl=5`, real GRE ptype @20 = ETHERTYPE_IP, inner id 0xAAAA. Packet B: `ip_hl=7` (8 bytes options), real GRE ptype @28 = ETHERTYPE_IP, option bytes @22-23 = 0x0000, inner id 0xBBBB. BUGGY kernel: gi_ptype reads @22 = 0x0000 -> switch default -> return(0) -> packet B NOT decapsulated. tcpdump captures ONLY packet A (id 0xAAAA). **1 packet captured.** FIXED kernel: reads ptype @28 = 0x0800 -> accepted -> decapsulated. tcpdump captures BOTH A and B (ids 0xAAAA and 0xBBBB). **2 packets captured.** BASELINE (unpatched #0, if_gre.ko sha 62643caa…): 1 packet captured (id 43690 = 0xAAAA only). PATCHED (#0 + fixed if_gre.ko sha a34b0355…): 2 packets captured (id 43690 = 0xAAAA AND id 48059 = 0xBBBB). The change from 1->2 captures, with packet B's inner (id 0xBBBB) appearing only after the fix, is the deterministic before/after discriminator. Impact ------ Read-class misparse. Realistic ceiling: - GRE mode: wrong ptype -> silent drop of legitimate GRE-over-IP-options tunneled packets, OR wrong inner-protocol classification if the option bytes happen to encode an accepted ptype (ETHERTYPE_IP / WCCP). Wrong flags -> wrong optional-field sizing -> `hlen` over/under-advances -> misaligned inner packet injected into netisr (garbage / DoS). - MOBILE mode: wrong S-bit -> wrong msiz; wrong osrc/odst -> inner packet rewritten with attacker-influenced (option-derived) src/dst; checksum computed over option bytes -> valid packets dropped. The rewritten ip_src/ip_dst of the re-injected inner packet is a (weak) info-leak / spoofing vector if it reaches a recipient. No memory-corruption write primitive -> no escalation (correctly). Exploit chain ------------- Not applicable (non-corruption / read-class misparse). There is no write primitive to convert; the realistic impact ceiling is misparse-driven DoS + possible inner-src/dst rewrite. No uid=0 escalation attempted (correct). Fix (fix.diff — supersedes any prior proposal; standalone, git apply-able) -------------------------------------------------------------------------- Compute the GRE / mobile header pointer from the actual IP header length (`ip_hl*4`), not the fixed-offset overlay: gre_input2: drop `struct greip *gip`; add `struct ip *ip = mtod(m, struct ip *);` `struct gre_h *gh = (struct gre_h *)((caddr_t)ip + hlen);` (hlen == ip_hl*4 on entry) read `gh->flags` / `gh->ptype` (was `gip->gi_flags` / `gip->gi_ptype`). gre_mobile_input: drop `struct mobip_h *mip`; add `struct mobile_h *mh = (struct mobile_h *)((caddr_t)ip + (ip->ip_hl << 2));` read `mh->proto` / `mh->osrc` / `mh->odst`; the writes back into the OUTER ip header use `ip->ip_src` / `ip->ip_dst` / `ip->ip_p` (== the former `mip->mi.*` which was the same outer header at offset 0). This is the minimal targeted change: only the mislocated field reads move; the `hlen`/`m->m_data` strip arithmetic and the mobile `bcopy` (which already used `ip->ip_hl << 2`) are unchanged. After the fix the overlay derefs agree with the correct `ip_hl*4`-aware reads (harness "correct" column). Fix build --------- `gre` is a loadable module (NOT compiled into X86_64_GENERIC). The single-fix build is the module: cd /usr/src/sys/net/gre make KERNBUILDDIR=/usr/obj/usr/src/sys/X86_64_GENERIC cp if_gre.ko /boot/kernel/if_gre.ko sync; reboot Built clean (`-Werror`, rc=0). Patched if_gre.ko sha256 a34b03557871eaa4565df978ad889398632a53b4f0ae23d6f2a2263194b698b8. Fix validation -------------- BASELINE (#0 kernel, original if_gre.ko sha 62643caa…): live discriminator -> 1 packet captured (id 0xAAAA only; packet B dropped). PATCHED (#0 kernel, fixed if_gre.ko sha a34b0355…): live discriminator -> 2 packets captured (id 0xAAAA AND id 0xBBBB). Deterministic over 3 runs (2+ captures every time; 1 every time on buggy). Before: packet B (ip_hl=7) dropped by the misparse (1 capture). After: packet B decapsulated correctly (2 captures, id 0xBBBB present). => fix closes the bug. Verdict: REPRODUCED on baseline #0 (misparse: ip_hl>5 packet dropped/ misdecapsulated), FIXED on single-fix if_gre.ko module (packet decapsulated correctly). Impact class: read-class misparse / DoS (+ possible inner-src/dst rewrite). No escalation.