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

gre_input2 and gre_mobile_input dereference header fields at offset 20+ without m_pullup β€” OOB read on chained mbufs

Summary

gre_input2 :133 gip=mtod(m,struct greip*) then :151 gip->gi_flags (offset 20-21) :164 gip->gi_ptype (offset 22-23) without m_pullup. gre_mobile_input :210 mip=mtod :223 mip->mh.proto :225 osrc :229 odst :232 checksum range without m_pullup. ip_input only guarantees m->m_len>=ip_hl*4 (KASSERT ip_input.c:539). On chained mbufs (ip_reass m_cat gif/stf/divert/IPSec) bytes at offset>=ip_hl*4 not in head mbuf -> reads recycled cluster contents as protocol header = OOB read + potential info leak. Mobile path writes leaked bytes into ip_src/ip_dst of re-injected packet observable by recipient. Fix: m_pullup(m,hlen+sizeof(gre_h)) / m_pullup(m,iphlen+MOB_H_SIZ_L).

Discussion (0)

No comments yet.

PoC verification

Evidence pack

findings/poc/DF-0742 Β· 14 files
FileTypeDescriptionSize
trigger.c trigger-source raw-socket injector: short IPPROTO_MOBILE (24B, S-bit) + IPPROTO_GRE (20B) packets 8.0 KB view raw
build.sh build-script cc -O2 -Wall -o trigger trigger.c 110 B view raw
run.sh run-script setup gre0 (mobile) + gre1 (GRE) tunnels, run trigger 1.2 KB view raw
README.md readme human-facing summary: bug, PoC, reproduction, fix 5.5 KB ↓ raw
VERDICT.md verdict full narrative: mechanism, reachability, reproduction, fix validation 7.5 KB ↓ raw
build.log build-log trigger compilation output 150 B view raw
run.log run-log baseline run output + panic signature + analysis 2.0 KB view raw
panic.txt panic-signature Fatal trap 12 page fault in memmove+0x24f from boot.log 675 B view raw
fix.diff suggested-fix git-apply-able: m_pullup(sizeof greip) in gre_input2 + m_pullup(sizeof mobip_h) in gre_mobile_input 1.5 KB view raw
fix_build.log build-log single-fix if_gre.ko module build output 1.9 KB view raw
fix_run.log run-log patched-module trigger output, 3 runs all clean 2.0 KB view raw
env.txt environment uname, cc version, sysctls, kldstat, module sha256 673 B view 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-facing summary: bug, PoC, reproduction, fix
↓ download raw

DF-0742 β€” gre_input2 / gre_mobile_input missing m_pullup

Severity: Medium (CVSS 3.1: AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:N/A:H) Class: CWE-125 OOB read + CWE-787 (downstream write from stale data) Location: sys/netinet/ip_gre.c:133-164 (gre_input2), sys/netinet/ip_gre.c:210-232 (gre_mobile_input)

Bug

Both GRE input handlers overlay a struct starting at mtod(m) and dereference fields at byte offset >= 20 (right after the 20-byte outer IP header) without first calling m_pullup():

/* gre_input2 β€” ip_gre.c:133,151,164 */
struct greip *gip = mtod(m, struct greip *);     /* overlay 0..23 */
flags = ntohs(gip->gi_flags);                    /* offset 20-21 */
switch (ntohs(gip->gi_ptype)) {                  /* offset 22-23 */

/* gre_mobile_input β€” ip_gre.c:210,223,225,229,232 */
struct mobip_h *mip = mtod(m, struct mobip_h *); /* overlay 0..31 */
ntohs(mip->mh.proto);                            /* offset 20-21 */
mip->mh.osrc;                                    /* offset 28-31 */
mip->mh.odst;                                    /* offset 24-27 */
gre_in_cksum((u_short*)&mip->mh, msiz);          /* offset 20..31  */

ip_input only guarantees m->m_len >= ip_hl*4 (KASSERT at ip_input.c:539). For any packet whose head mbuf carries only the IP header β€” a short single-mbuf datagram, or a chained mbuf after ip_reass m_cat β€” the bytes at offset >= 20 are NOT in the head mbuf. The overlay derefs then read stale / recycled mbuf-cluster residue instead of the actual GRE/mobile header fields.

Reachability

Sibling to DF-0740 / DF-0741 / DF-0743 (same if_gre module). A gre interface configured by an admin (ifconfig greN tunnel ... [up]) causes encap_attach() to register in_gre_protosw (gre_input β†’ gre_input2, IPPROTO_GRE=47) and/or in_mobile_protosw (gre_mobile_input, IPPROTO_MOBILE=55). Any packet routed to the host with the matching outer src/dst and protocol then takes the path:

ip_input β†’ ip_protox[47|55] β†’ encap4_input β†’ mask_match
  β†’ gre_input2 / gre_mobile_input   (NO m_pullup before offset-20+ derefs)

The reachable attacker is remote: anyone who can deliver a packet with the matching outer IP src/dst. Loopback raw-socket send here is just the test harness.

PoC

trigger.c opens socket(AF_INET, SOCK_RAW, IPPROTO_RAW) with IP_HDRINCL and sends two short packets to 127.0.0.1 after root sets up gre0 (mobile mode) and gre1 (GRE mode):

  • Variant M (24-byte IPPROTO_MOBILE, S-bit set): m_len=24. The odst@24-27, osrc@28-31, and the 12-byte gre_in_cksum range all extend past m_len=24 β†’ OOB read (DF-0742). The subsequent bcopy size 24-12-20=-8 underflows β†’ memmove page-fault β†’ panic (DF-0741 sibling). Reliably panics the unpatched kernel.

  • Variant G (20-byte IPPROTO_GRE, IP header only): m_len=20. gi_flags@20-21 and gi_ptype@22-23 are entirely past m_len=20 β†’ OOB read of stale cluster data (DF-0742). Outcome is residue-dependent (may silently mis-parse or trigger the DF-0740 hlen underflow).

Reproduction

Baseline (6.5-DEVELOPMENT #0, shipped if_gre.ko): - Variant M β†’ panic: Fatal trap 12: page fault while in kernel mode - memmove+0x24f β†’ Stopped at memmove+0x24f: repe movsq - ssh dies, vm.sh status => down

Patched (rebuilt if_gre.ko with fix.diff): - Both variants sent, trigger exits 0, x3 consecutive runs - Guest stays up, NO panic in boot.log - m_pullup(m, sizeof(struct mobip_h)) drops the 24-byte packet (too short to hold the 32-byte overlay) before any offset-20+ deref

Impact

OOB read + downstream panic (DoS). The stale bytes read by the overlay derefs are mbuf-cluster residue β€” not directly attacker-controlled, but they DO flow into protocol decisions (wrong GRE ptype β†’ mis-parse; wrong mobile checksum β†’ silent drop; wrong osrc/odst β†’ written into the re-injected packet's ip_src/ip_dst, a potential info leak if the packet reaches a recipient). No escalation (read-class primitive, no attacker-controlled write).

Fix

fix.diff adds m_pullup(m, sizeof(struct greip)) (24 bytes) to gre_input2 and m_pullup(m, sizeof(struct mobip_h)) (32 bytes) to gre_mobile_input, right after the gre_lookup check and before the first offset-20+ deref. On failure (too-short packet) the mbuf is freed and the function returns early. m_pullup also coalesces chained-mbuf headers into the head mbuf so the overlay derefs are safe.

Build single-fix module

gre is NOT in the static X86_64_GENERIC kernel, so the correct single-fix build is the module (sibling to DF-0741's approach):

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

Files

file purpose
trigger.c userspace raw-socket injector (IPPROTO_MOBILE + IPPROTO_GRE short packets)
build.sh cc -O2 -Wall -o trigger trigger.c
run.sh setup gre-mobile + gre tunnels, run trigger
build.log trigger build output
run.log baseline run output + panic signature + analysis
panic.txt baseline panic signature from boot.log
fix.diff standalone git-apply-able fix (m_pullup in both handlers)
fix_build.log single-fix module build output
fix_run.log patched-module trigger output (3 runs, all clean)
env.txt guest uname / cc / sysctls / module sha
VERDICT.md full narrative
manifest.json machine-readable catalog
VERDICT.md verdict full narrative: mechanism, reachability, reproduction, fix validation
↓ download raw

DF-0742 β€” gre_input2 / gre_mobile_input missing m_pullup

Finding

sys/netinet/ip_gre.c:gre_input2() (the GRE decapsulation handler, registered via in_gre_protosw for IPPROTO_GRE = 47) overlays struct greip (IP header + 4-byte GRE header = 24 bytes) starting at mtod(m) and dereferences gi_flags (offset 20-21) and gi_ptype (offset 22-23) at ip_gre.c:151 and :164 without calling m_pullup().

sys/netinet/ip_gre.c:gre_mobile_input() (the mobile-IP decapsulation handler, registered via in_mobile_protosw for IPPROTO_MOBILE = 55) overlays struct mobip_h (IP header + 12-byte mobile header = 32 bytes) starting at mtod(m) and dereferences mh.proto (offset 20-21), mh.osrc (offset 28-31), mh.odst (offset 24-27), and the gre_in_cksum range (offset 20..31) at ip_gre.c:223,225,229,232 without calling m_pullup().

ip_input only guarantees m->m_len >= ip_hl*4 (KASSERT at ip_input.c:539 β€” "complete IP header not in one mbuf"). On chained mbufs (after ip_reass m_cat, gif/stf/divert re-injection, IPSec, or hardware header-split), the bytes at offset

= 20 may NOT be in the head mbuf. The overlay derefs then read stale / recycled mbuf-cluster residue instead of the actual GRE/mobile header fields β€” an OOB read.

No m_pullup, no length check anywhere in either function before these derefs. Confirmed by grep m_pullup sys/netinet/ip_gre.c => zero hits on the unpatched tree.

Reachability path (confirmed live)

An admin creates a gre interface (ifconfig greN tunnel SRC DST [up]). gre_attach β†’ encap_attach registers in_gre_protosw (gre_input β†’ gre_input2, IPPROTO_GRE) and/or in_mobile_protosw (gre_mobile_input, IPPROTO_MOBILE) for the matching outer src/dst. Any packet routed to the host with the matching src/dst/proto:

ip_input
  β†’ ip_protox[47|55]
     β†’ encap4_input                 /* ip_encap.c:131 */
        β†’ mask_match finds the gre entry
           β†’ (*psw->pr_input)(mp,offp,proto)
              = gre_input β†’ gre_input2     /* IPPROTO_GRE */
              = gre_mobile_input           /* IPPROTO_MOBILE */

Neither gre_input2 nor gre_mobile_input calls m_pullup() before the offset-20+ overlay derefs.

PoC (live path)

trigger.c opens socket(AF_INET, SOCK_RAW, IPPROTO_RAW) with IP_HDRINCL and sends two short packets to 127.0.0.1 after root sets up gre0 (mobile mode) and gre1 (GRE mode):

Variant M: 24-byte IPPROTO_MOBILE packet, S-bit set. m_len=24. proto@20-21 OK, but odst@24-27, osrc@28-31, and the 12-byte gre_in_cksum range all extend past m_len=24 β†’ OOB read (DF-0742). The bcopy at line 237 then underflows (size 24-12-20 = -8 β†’ ~2^64) β†’ memmove page-fault β†’ panic (DF-0741 sibling).

Variant G: 20-byte IPPROTO_GRE packet (IP header only). m_len=20. gi_flags@20-21 and gi_ptype@22-23 are entirely past m_len=20 β†’ OOB read of stale cluster data (DF-0742). Outcome is residue-dependent.

Setup (root, see run.sh): ifconfig gre create ifconfig gre0 tunnel 127.0.0.1 127.0.0.1 ifconfig gre0 -link0 # g_proto = IPPROTO_MOBILE ifconfig gre0 up ifconfig gre create ifconfig gre1 tunnel 127.0.0.1 127.0.0.1 ifconfig gre1 up # g_proto = IPPROTO_GRE (default)

Realistic threat model: the gre tunnel is configured by an admin. The reachable attacker is remote: anyone who can route a matching packet to the host. The raw-socket loopback send here is just the harness.

Reproduction

BASELINE (unpatched, with-src #0, if_gre.ko as shipped, sha256 62643caa00240d805f8737faa75189e761992d6020be05610678b34d746fd741): - Variant M β†’ guest dead within ~1s - boot.log: Fatal trap 12: page fault while in kernel mode fault virtual address = 0xfffff801175dfffc fault code = supervisor write data, page not present instruction pointer = 0x8:0xffffffff80bcab4f current process = Idle Stopped at memmove+0x24f: repe movsq (%rsi),%es:(%rdi) - Variant G never sent (guest already dead from Variant M) - ssh no longer answers; vm.sh status => down

The memmove+0x24f fault is the bcopy at ip_gre.c:237 executing with a near-2^64 size after the DF-0742 OOB reads at lines 223/225/229/232 already ran without m_pullup. The panic proves the code reached gre_mobile_input and exercised the offset-20+ overlay derefs β€” confirming the missing m_pullup.

Impact: OOB read (stale mbuf-cluster residue) + downstream kernel panic / DoS. The stale bytes flow into protocol decisions: - gre_input2: wrong gi_flags β†’ wrong hlen; wrong gi_ptype β†’ wrong protocol classification - gre_mobile_input: wrong osrc/odst β†’ written into ip_src/ip_dst of the re-injected inner packet (potential info leak if it reaches a recipient); wrong gre_in_cksum β†’ silent drop No escalation (read-class primitive; the would-be bcopy over-write from DF-0741 faults on the next unmapped page before any controlled landing).

Exploit chain

Not applicable (non-corruption / read-class finding). The primitive is an OOB read of stale mbuf-cluster residue into protocol-decision fields. There is no write primitive to convert. The realistic impact ceiling is: info leak via stale bytes reaching a packet recipient + reliable DoS from the downstream bcopy/hlen underflow panics. No uid=0 escalation attempted (correctly β€” read-class bug).

Fix

fix.diff adds m_pullup(m, sizeof(struct greip)) (24 bytes) to gre_input2 (after the gre_lookup check, before reading gi_flags) and m_pullup(m, sizeof(struct mobip_h)) (32 bytes) to gre_mobile_input (after gre_lookup, before reading mh.proto).

Both calls are placed after gre_lookup (which only reads ip_src/ip_dst at offset 12-19, already guaranteed by ip_input) and before the first offset-20+ overlay deref. On failure (too-short packet) m_pullup frees m and returns NULL; the function returns early (consuming the mbuf). On success, the header is coalesced into the head mbuf so the overlay derefs are safe.

This fix supersedes the finding markdown's suggested m_pullup(m, hlen+sizeof(gre_h)) / m_pullup(m, iphlen+MOB_H_SIZ_L) by using sizeof(struct greip) / sizeof(struct mobip_h) β€” the exact struct-overlay sizes the code dereferences β€” which is simpler and provably sufficient.

Fix build

Because gre is a loadable module (NOT compiled into the X86_64_GENERIC static kernel), 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

Fix validation

PATCHED (same #0 static kernel; freshly built if_gre.ko, sha256 bf9c40021a6918470d9477f30023c2e5554eb79f5178436cf801cfd9d4571069): - nm if_gre.ko | grep m_pullup => U m_pullup (external call confirmed) - gre0 (mobile) + gre1 (GRE) tunnels set up - run trigger x3 β†’ all three runs: * Variant M sent 24 bytes β†’ no panic * Variant G sent 20 bytes β†’ no panic * trigger exits 0 * guest still answers ssh * boot.log has NO panic / memmove / encap4_input / trap entry

Before: panic (memmove+0x24f), guest dead. After: trigger exits 0 x3, guest up, no panic. => fix closes the bug.

Verdict: REPRODUCED on baseline #0 (panic from the OOB-read code path), FIXED on single-fix module (no panic). Impact class: OOB read + downstream DoS/panic from a short/chained packet once a gre or gre-mobile tunnel is configured. No escalation (read-class primitive).

Fix verification

fixed
baseline reproduced→ patch + rebuild →patched clean

VALIDATED the fix: on the unpatched baseline (if_gre.ko sha 62643caa), the 24-byte IPPROTO_MOBILE S-bit trigger panics with 'Fatal trap 12 page fault, Stopped at memmove+0x24f' (guest dead). On the single-fix module (if_gre.ko sha bf9c4002 with m_pullup calls), the SAME trigger exits 0 cleanly x3 consecutive runs, guest stays up, NO panic/trap in boot.log. The m_pullup(m, sizeof(struct mobip_h)) call drops the 24-byte packet (too short to hold the 32-byte overlay) before any offset-20+ deref => fix closes the bug.

BASELINE (unpatched, sha 62643caa): Fatal trap 12 page fault while in kernel mode | fault virtual address = 0xfffff801175dfffc | Stopped at memmove+0x24f: repe movsq | db> (guest DOWN). PATCHED (sha bf9c4002): [DF-0742-M] sent 24 bytes + [DF-0742-G] sent 20 bytes | TRIGGER_RC=0 | guest=up | (no panic/trap signatures) -- x3 runs identical.
↓ fix.diffDragonFly 6.5-DEVELOPMENT #0: Thu Jul 2 06:02:54 UTC 2026 (same #0 static kernel; freshly rebuilt if_gre.ko sha256 bf9c40021a6918470d9477f30023c2e5554eb79f5178436cf801cfd9d4571069 with m_pullup fix.diff applied)

Confirmed kernel references

Detail

Exploit chain

none -- non-corruption (read-class) finding. The primitive is an OOB read of stale mbuf-cluster residue into protocol-decision fields (gi_flags, gi_ptype, mh.osrc/odst, cksum range). The stale bytes flow into GRE/mobile protocol classification and may be written into the re-injected inner packet's ip_src/ip_dst (potential info leak if the packet reaches a recipient), but there is no write primitive to convert to uid=0. The realistic impact ceiling is: info leak via stale bytes + reliable DoS from the downstream bcopy/hlen underflow panics (DF-0740/DF-0741 siblings). No escalation attempted (correctly -- read-class bug).

Evidence (decisive lines)

BASELINE (unpatched if_gre.ko sha 62643caa): Variant M 24-byte IPPROTO_MOBILE S-bit packet sent -> Fatal trap 12: page fault while in kernel mode | fault virtual address = 0xfffff801175dfffc | fault code = supervisor write data, page not present | instruction pointer = 0x8:0xffffffff80bcab4f | current process = Idle | Stopped at memmove+0x24f: repe movsq (%rsi),%es:(%rdi) | db> (ssh dead, vm.sh status=>down). PATCHED (if_gre.ko sha bf9c4002 with m_pullup): nm shows U m_pullup | Variant M sent 24 bytes + Variant G sent 20 bytes | trigger exits 0 | guest stays up | NO panic/trap in boot.log | x3 consecutive clean runs.

PoC changes

Created the full PoC evidence pack from scratch (no prior poc directory existed). trigger.c: raw-socket injector with two variants -- Variant M (24-byte IPPROTO_MOBILE S-bit packet, reliably panics via the OOB-read code path) and Variant G (20-byte IPPROTO_GRE packet, residue-dependent). build.sh/run.sh scripts for exact reproduction. fix.diff: adds m_pullup(m, sizeof(struct greip)) to gre_input2 and m_pullup(m, sizeof(struct mobip_h)) to gre_mobile_input, right after gre_lookup and before the first offset-20+ overlay deref. VERDICT.md/README.md/manifest.json with full narrative.

Verified recommended fix

Add m_pullup(m, sizeof(struct greip)) after gre_lookup in gre_input2 (ip_gre.c:~142) and m_pullup(m, sizeof(struct mobip_h)) after gre_lookup in gre_mobile_input (ip_gre.c:~231), both before the first offset-20+ overlay deref. On failure (too-short packet), free m and return early. This ensures the GRE/mobile header is contiguous in the head mbuf before dereferencing fields at offset 20+. Supersedes finding markdown's suggested m_pullup(m, hlen+sizeof(gre_h)) / m_pullup(m, iphlen+MOB_H_SIZ_L) by using the exact struct-overlay sizes -- simpler and provably sufficient. Full git-apply-able diff in findings/poc/DF-0742/fix.diff.

Verdict

REPRODUCED. Both gre_input2 (ip_gre.c:133,151,164) and gre_mobile_input (ip_gre.c:210,223-232) overlay struct greip/mobip_h at mtod(m) and dereference fields at byte offset >= 20 (gi_flags, gi_ptype, mh.proto, mh.osrc, mh.odst, gre_in_cksum range) WITHOUT calling m_pullup. ip_input only guarantees m_len >= ip_hl*4 (KASSERT ip_input.c:539). On the unpatched #0 kernel, a 24-byte IPPROTO_MOBILE S-bit packet with m_len=24 reaches gre_mobile_input and exercises the offset-20+ overlay derefs past m_len (odst@24-27, osrc@28-31, cksum[20..31]) -- the stale-byte OOB reads (DF-0742) execute before the bcopy size 24-12-20=-8 underflows to ~2^64 and memmove faults: 'Fatal trap 12 page fault while in kernel mode, Stopped at memmove+0x24f: repe movsq'. ssh dies, vm.sh status=>down. grep m_pullup sys/netinet/ip_gre.c => 0 hits on the unpatched tree confirms the missing guard. The panic proves the code reached gre_mobile_input and dereferenced the overlay without m_pullup.