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

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)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0743 Β· 16 files
FileTypeDescriptionSize
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
README.md readme human reproduce doc
↓ download 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.

VERDICT.md verdict full narrative: mechanism, reachability, both proofs, fix, validation
↓ download raw

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.

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED 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.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same #0 static kernel; only the if_gre.ko module rebuilt -- patched sha256 a34b03557871eaa4565df978ad889398632a53b4f0ae23d6f2a2263194b698b8 vs baseline 62643caa00240d805f8737faa75189e761992d6020be05610678b34d746fd741)

Confirmed kernel references

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).