struct greip/mobip_h overlay assumes ip_hl==5 β IP options make all GRE/mobile header reads use wrong offset compounds DF-0740
Summary
struct greip (if_gre.h:90) = {struct ip gi_i; struct gre_h gi_g} __packed. gi_g always at byte offset 20 regardless of actual ip_hl. struct mobip_h identical. When ip_hl>5 (IP options): :151 gip->gi_flags reads IP option bytes not GRE flags. :164 gip->gi_ptype reads IP option bytes. :180 m_data+=hlen uses hlen computed from attacker-controlled option bytes treated as GRE flags = deterministic underflow. gre_mobile_input :232 gre_in_cksum(&mip->mh,msiz) computes checksum over IP option bytes not real mobile header = may pass on arbitrary short packet. Remote unauth same GRE tunnel precondition. Compounds DF-0740 (deterministic) and DF-0741 (checksum bypass). Fix: read GRE/mobile header at mtod+(ip->ip_hl<<2) not via struct overlay.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0743 Β· 16 files| File | Type | Description | Size | |
|---|---|---|---|---|
| harness.c | trigger-source | deterministic userspace harness replicating the kernel struct-overlay derefs; proves the ip_hl-vs-offset mismatch | 13.5 KB | view raw |
| live_trigger.c | trigger-source | live in-kernel discriminator: GRE packets with/without IP options | 4.2 KB | view raw |
| live_run.sh | run-script | sets up gre0, tcpdump on gre0, injects A+B, reports capture count (1=buggy, 2=fixed) | 1.3 KB | view raw |
| build.sh | build-script | builds harness + live_trigger | 594 B | view raw |
| run.sh | run-script | runs the deterministic harness | 737 B | view raw |
| run.log | run-log | harness output β MISPARSE PROVEN (every field mismatched) | 1.8 KB | view raw |
| live_run_buggy.log | run-log | live discriminator on buggy baseline β 1 packet captured | 1.3 KB | view raw |
| fix_baseline.txt | fix-evidence | baseline live discriminator before fix β 1 capture | 166 B | view raw |
| fix_patched.txt | fix-evidence | patched live discriminator after fix β 2 captures | 886 B | view raw |
| fix.diff | suggested-fix | git-apply-able fix: compute GRE/mobile ptr from ip_hl*4 | 2.1 KB | view raw |
| fix_build.log | build-log | single-fix if_gre module build (-Werror, rc=0) | 1.9 KB | view raw |
| VERDICT.md | verdict | full narrative: mechanism, reachability, both proofs, fix, validation | 9.9 KB | β raw |
| env.txt | environment | uname, kern.version, if_gre.ko sha, cc version | 477 B | view raw |
| README.md | readme | human reproduce doc | 2.4 KB | β 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-0743 β struct greip/mobip_h overlay assumes ip_hl == 5
The GRE input handlers in sys/netinet/ip_gre.c overlay struct greip /
struct mobip_h at mtod(m) and dereference the GRE/mobile fields at FIXED
byte offsets (20/22/24/28) that correspond to a 20-byte (ip_hl==5) IP
header. When the outer encapsulating IP header carries IP options
(ip_hl > 5), those fixed offsets land on the IP-OPTION bytes, not the GRE /
mobile header β deterministic misparse.
Sibling of DF-0740 / DF-0741 / DF-0742 (same if_gre module), but an
orthogonal bug: DF-0742's missing-m_pullup fix does NOT close it
(contiguity is irrelevant β the overlay reads the wrong offset regardless).
Files
| file | purpose |
|---|---|
harness.c |
PRIMARY proof β deterministic userspace harness replicating the kernel's exact struct-overlay derefs; shows every field is misread when ip_hl > 5. |
live_trigger.c |
live in-kernel discriminator: sends GRE packets with/without IP options to a configured gre0. |
live_run.sh |
sets up gre0, runs tcpdump on gre0, injects A+B, reports capture count (1=buggy, 2=fixed). |
build.sh / run.sh |
build & run the harness. |
fix.diff |
standalone git apply-able fix (compute overlay ptr from ip_hl*4). |
VERDICT.md |
full narrative. |
run.log |
harness output (the misparse proof). |
live_run_buggy.log |
live discriminator on the BUGGY baseline (1 capture). |
fix_baseline.txt / fix_patched.txt |
before/after live discriminator. |
fix_build.log |
single-fix if_gre module build (-Werror, rc=0). |
env.txt |
guest environment. |
manifest.json |
artifact catalog. |
Reproduce
Primary (deterministic harness β no privilege, no setup)
./build.sh && ./run.sh
Expected: VERDICT: MISPARSE PROVEN (vulnerable) β every overlay field
mismatches the correct ip_hl*4-aware read; accept/drop and checksum
decisions diverge.
Live in-kernel discriminator (needs root)
cc -O2 -Wall -o live_trigger live_trigger.c ./live_run.sh
Expected on the buggy kernel: 1 packet captured (packet B with
ip_hl=7 dropped by the misparse).
Expected on the fixed kernel: 2 packets captured (packet B
decapsulated; id 48059 / 0xBBBB appears in the tcpdump).
Impact
Read-class misparse / DoS (+ possible inner-src/dst rewrite in mobile mode). No memory-corruption write primitive β no escalation.
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:
- Deterministic harness (
harness.c) β replicates the kernel's exact struct-overlay derefs fromip_gre.cand shows EVERY field is misread. - Live in-kernel discriminator (
live_trigger.c+live_run.sh) β a GRE packet withip_hl=7and 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.
Fix verification
fixedVALIDATED the fix. Live tcpdump discriminator on gre0: on the unpatched baseline (#0 + if_gre.ko 62643caa) a valid GRE packet with ip_hl=7 (real ptype@28=ETHERTYPE_IP, option bytes@22=0x0000, inner id 0xBBBB) is silently DROPPED by the misparse -> '1 packet captured' (only packet A id 0xAAAA decapsulated). On the single-fix kernel (#0 + fixed if_gre.ko a34b0355) the SAME packet is decapsulated correctly (ptype read from offset 28) -> '2 packets captured' (id 0xAAAA AND id 0xBBBB visible via tcpdump -vv). Deterministic over 3 runs each (buggy always 1, patched always 2+). Fix closes the bug.
BEFORE (baseline 62643caa): './live_run.sh -> ... 1 packet captured' (tcpdump -vv shows id 43690=0xAAAA only; id 48059=0xBBBB absent = packet B dropped by misparse). AFTER (fixed a34b0355): './live_run.sh -> ... 2 packets captured' (tcpdump -vv shows id 43690=0xAAAA AND id 48059=0xBBBB = packet B decapsulated). HARNESS also independently proves the fix approach: the 'correct' (ip_hl*4-aware) column reads match the real GRE/mobile header bytes (gi_ptype=0x0800 ACCEPT, gre_in_cksum=0x0000 PASS) while the 'overlay' (vulnerable) column does not.
Confirmed kernel references
- sys/netinet/ip_gre.c:133
- sys/netinet/ip_gre.c:151
- sys/netinet/ip_gre.c:164
- sys/netinet/ip_gre.c:210
- sys/netinet/ip_gre.c:223
- sys/netinet/ip_gre.c:225
- sys/netinet/ip_gre.c:229
- sys/netinet/ip_gre.c:230
- sys/netinet/ip_gre.c:232
- sys/net/gre/if_gre.h:90
- sys/net/gre/if_gre.h:141
- sys/netinet/ip_input.c:536
- sys/netinet/ip_input.c:415
- sys/netinet/ip_input.c:661
Detail
Exploit chain
none (non-corruption / read-class misparse). The primitive is a misparse: when ip_hl>5 the overlay fields (ptype, flags, mobile proto/osrc/odst, checksum range) are read from IP-option bytes instead of the real GRE/mobile header. Realistic impact ceiling: GRE mode -> silent drop of legitimate GRE-over-IP-options tunneled packets, or wrong inner-protocol classification / wrong optional-field sizing -> misaligned inner packet injected into netisr (garbage/DoS); MOBILE mode -> wrong S-bit/msiz, inner packet rewritten with option-derived ip_src/ip_dst (weak spoof/info-leak vector), valid packets dropped by the option-byte checksum. No memory-corruption write primitive to convert to uid=0; no escalation chain developed (correctly).
Evidence (decisive lines)
HARNESS (run.log): GRE path ip_hl=7 -> gi_flags overlay=0xaaaa correct=0x0000 MISMATCH; gi_ptype overlay=0xbbbb correct=0x0800 MISMATCH; decision DIVERGES (overlay DROP vs correct ACCEPT). MOBILE path -> mh.proto overlay=0xcccc correct=0x0080 MISMATCH; mh.odst overlay=0xdddddddd correct=0x0200000a MISMATCH; gre_in_cksum overlay=0x7c8b FAIL->DROP vs correct=0x0000 PASS. LIVE discriminator: BASELINE (#0, if_gre.ko 62643caa) -> '1 packet captured' (id 43690=0xAAAA only; packet B id 0xBBBB dropped by misparse); PATCHED (#0 + fixed if_gre.ko a34b0355) -> '2 packets captured' (id 43690 AND id 48059). Deterministic over 3 runs each.
PoC changes
Created findings/poc/DF-0743/ from scratch (no prior PoC existed). harness.c = deterministic userspace harness replicating the kernel's exact struct-overlay derefs (the PRIMARY proof, no privilege). live_trigger.c + live_run.sh = live in-kernel tcpdump discriminator (1 capture=buggy, 2=fixed). fix.diff = compute GRE/mobile header pointer from ip_hl*4 instead of the fixed-offset-20 overlay. Removed a superseded early trigger draft. VERDICT.md/manifest.json/env.txt/build.sh/run.sh all written.
Verified recommended fix
In gre_input2, replace the struct greip overlay with a struct gre_h pointer computed from the actual IP header length: gh = (struct gre_h )((caddr_t)mtod(m,struct ip ) + hlen) (hlen == ip_hl4 on entry), and read gh->flags / gh->ptype (was gip->gi_flags / gip->gi_ptype). In gre_mobile_input, replace struct mobip_h overlay with mh = (struct mobile_h )((caddr_t)ip + (ip->ip_hl << 2)), read mh->proto/osrc/odst, and write the outer-header fields via ip->ip_src/ip_dst/ip_p (== the former mip->mi.*). Minimal targeted change; the hlen/m_data strip and the mobile bcopy (which already used ip_hl<<2) are unchanged. fix.diff is standalone git-apply-able (verified git apply --check); supersedes any prior proposal.
Verdict
REPRODUCED (read-class misparse). gre_input2 (ip_gre.c:133,151,164) overlays struct greip at mtod(m) and reads gi_flags@20 / gi_ptype@22 at FIXED offsets assuming a 20-byte (ip_hl==5) IP header; gre_mobile_input (ip_gre.c:210,223,225,229,230,232) does the same with struct mobip_h (mh.proto@20, mh.odst@24, mh.osrc@28, cksum range@20..31). When the outer IP header carries options (ip_hl>5), ip_input passes hlen=ip_hl4 and ip_dooptions returns 0 for benign options (ip_input.c:661) so the packet reaches gre_input2/gre_mobile_input with options still inline -- but the overlay derefs still read offsets 20/22/24/28, i.e. the IP-OPTION bytes, not the real GRE/mobile header (which lives at ip_hl4). The strip math (hlen += sizeof(gre_h); m->m_data += hlen) correctly uses ip_hl*4; ONLY the struct-overlay field reads are wrong. Proven two ways: (1) deterministic harness replicating the kernel's exact derefs shows EVERY field (gi_flags, gi_ptype, mh.proto, mh.odst, mh.osrc) misread and accept/drop + checksum decisions diverge; (2) live tcpdump discriminator on gre0 shows a valid GRE packet with ip_hl=7 (real ptype@28=ETHERTYPE_IP, option bytes@22=0x0000) is silently DROPPED on the buggy kernel (gi_ptype misread as 0x0000 -> switch default -> return(0)) and decapsulated on the fixed kernel. NOT subsumed by DF-0742's m_pullup fix (contiguity is irrelevant -- the overlay reads the wrong offset regardless) and DragonFly's gre path does NOT strip IP options before overlaying. No write primitive -> no escalation (correctly a read-class bug).
No comments yet.