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)
PoC verification
Evidence pack
findings/poc/DF-0742 Β· 14 files| File | Type | Description | Size | |
|---|---|---|---|---|
| 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 |
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. Theodst@24-27,osrc@28-31, and the 12-bytegre_in_cksumrange all extend pastm_len=24β OOB read (DF-0742). The subsequentbcopysize24-12-20=-8underflows βmemmovepage-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-21andgi_ptype@22-23are entirely pastm_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 |
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
fixedVALIDATED 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.
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.
No comments yet.