gre_mobile_input bcopy size underflow (m_len-msiz-ip_hl*4) β huge overwrite when mobile header larger than mbuf
Summary
gre_mobile_input :237-238 bcopy((ip)+(ip_hl<<2)+msiz,(ip)+(ip_hl<<2),m->m_len-msiz-(ip_hl<<2)). Third arg signed int: m_len<msiz+ip_hl*4 -> negative -> implicit conversion to size_t ~2^64 -> bcopy faults. gre_in_cksum :232 check bypassable via chained mbuf (m_len<m_pkthdr.len mobile header straddles mbufs) or IP options (DF-0743 checksum reads wrong offset). Trigger: outer IP fragmented -> ip_reass chained mbuf short head mbuf. Remote unauth requires configured IPPROTO_MOBILE tunnel. Fix: if(m->m_len<iphlen+msiz){m_freem;return IPPROTO_DONE}.
Discussion (0)
PoC verification
Evidence pack
findings/poc/DF-0741 Β· 15 files| File | Type | Description | Size | |
|---|---|---|---|---|
| trigger.c | trigger-source | userspace raw-socket injector: 3 short IPPROTO_MOBILE packets | 8.9 KB | view raw |
| build.sh | build-script | cc -O2 -Wall -o trigger trigger.c | 409 B | view raw |
| run.sh | run-script | sets up gre-mobile tunnel, runs trigger | 1.1 KB | view raw |
| README.md | readme | summary + how to reproduce | 4.1 KB | β raw |
| VERDICT.md | verdict | full narrative: trigger -> primitive -> effect -> fix | 5.8 KB | β raw |
| build.log | build-log | baseline trigger build output | 8 B | view raw |
| panic.txt | panic-signature | baseline panic: vm_fault stack-guard from memmove/encap4_input | 715 B | view raw |
| run.log.baseline_panic.txt | run-log | full interleaved baseline panic from boot.log | 1.0 KB | view raw |
| fix.diff | suggested-fix | git-apply-able: add m_len >= ip_hl*4+msiz guard before bcopy | 991 B | view raw |
| fix_build.log | build-log | excerpt of nativekernel log showing ip_gre.c + if_gre.ko rebuild | 7.7 KB | view raw |
| fix_nativekernel.log | build-log | full 5.8MB nativekernel build log | 5.6 MB | β download |
| fix_run.log | run-log | patched-kernel trigger: exits 0, no panic | 740 B | view raw |
| env.txt | environment | uname, cc, sysctl vm.randomize_mmap | 190 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-0741 β gre_mobile_input bcopy size underflow
Severity: Medium (CVSS 3.1: AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H)
Class: CWE-787 OOB write + CWE-191 integer underflow
Location: sys/netinet/ip_gre.c:237-238 (gre_mobile_input)
Bug
gre_mobile_input (the IP-in-IP mobile-IP decapsulation handler,
registered for IPPROTO_MOBILE = 55 in sys/net/gre/if_gre.c:140-154)
computes a bcopy length without validating that the head mbuf is
large enough:
bcopy((caddr_t)(ip) + (ip->ip_hl << 2) + msiz,
(caddr_t)(ip) + (ip->ip_hl << 2),
m->m_len - msiz - (ip->ip_hl << 2)); /* signed int -> size_t */
All three operands are int. When a short / chained-mbuf packet has
m->m_len < ip_hl*4 + msiz, the third argument goes negative and
is then implicitly widened to size_t for bcopy() β near-2^64
byte copy β immediate page fault β kernel panic.
Reachability
gre is a loadable module (if_gre.ko). When an admin creates a gre
interface in mobile mode (ifconfig greN -link0 β g_proto =
IPPROTO_MOBILE, sys/net/gre/if_gre.c:461-470) and brings it up,
encap_attach registers in_mobile_protosw whose .pr_input =
gre_mobile_input for IPPROTO_MOBILE. Any packet routed to the host
with ip_p = 55 and matching outer src/dst then takes the path:
ip_input -> ip_protox[55] -> encap4_input -> mask_match -> (*psw->pr_input)() = gre_mobile_input
gre_mobile_input does no m_pullup() and no length check.
PoC
trigger.c (compile: cc -O2 -Wall -o trigger trigger.c,
run: ./run.sh) opens a raw socket SOCK_RAW/IPPROTO_MOBILE with
IP_HDRINCL and sends three short crafted packets to 127.0.0.1 after
setting up gre0 in mobile mode. Each variant is shorter than the
mobile header requires, so each independently triggers the underflow.
Reproduction
Baseline (6.5-DEVELOPMENT #0, shipped if_gre.ko):
- panic: vm_fault: fault on stack guard
- memmove() at memmove+0x24f
- encap4_input() at encap4_input+0x20b
- ssh dies, vm.sh status => down
Patched (rebuilt if_gre.ko with fix.diff):
- trigger exits 0 cleanly, x3 consecutive runs
- guest stays up
- no panic in boot.log
Impact
DoS / kernel panic. No uid=0 escalation: the over-write that
would land in the mbuf cluster faults on the next unmapped page (the
kernel stack guard) before any attacker-controlled write reaches a
victim object, so there is no corruption primitive to convert.
Realistic threat: any remote attacker who can deliver a packet with the matching outer IP src/dst to a host that has a gre-mobile tunnel configured can panic the kernel with a single short datagram.
Fix
fix.diff adds a one-line guard before the bcopy:
if (m->m_len < (ip->ip_hl << 2) + msiz) {
m_freem(m);
return(IPPROTO_DONE);
}
This matches the finding markdown's recommended fix.
Build single-fix module
gre is NOT in the static X86_64_GENERIC kernel, so the correct
single-fix build is the module, not a full nativekernel:
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
A naive make nativekernel does NOT rebuild if_gre.ko (it reuses
the warm obj), so the first fix-validation pass appears to fail until
the module is rebuilt directly.
Files
| file | purpose |
|---|---|
trigger.c |
userspace raw-socket injector (3 short IPPROTO_MOBILE packets) |
build.sh |
cc -O2 -Wall -o trigger trigger.c |
run.sh |
setup gre-mobile tunnel + run trigger |
build.log |
baseline trigger build output |
panic.txt |
baseline panic signature from boot.log |
run.log.baseline_panic.txt |
full interleaved baseline panic |
fix.diff |
standalone git-apply-able fix (one-line guard) |
fix_build.log |
single-fix module build output (and full nativekernel log) |
fix_run.log |
patched-kernel trigger output (exit 0, no panic) |
env.txt |
guest uname / cc / sysctls |
VERDICT.md |
full narrative |
manifest.json |
machine-readable catalog |
DF-0741 β gre_mobile_input bcopy size underflow
Finding
sys/netinet/ip_gre.c:gre_mobile_input() (the IP-in-IP mobile-IP
decapsulation handler, registered via in_mobile_protosw for
IPPROTO_MOBILE = 55 in sys/net/gre/if_gre.c:140-154) computes a
bcopy length as
bcopy((caddr_t)(ip) + (ip->ip_hl << 2) + msiz,
(caddr_t)(ip) + (ip->ip_hl << 2),
m->m_len - msiz - (ip->ip_hl << 2)); /* ip_gre.c:237-238 */
without validating that m->m_len >= msiz + ip->ip_hl*4. All three
operands (m->m_len, msiz, (ip->ip_hl << 2)) are int, so the
third argument is evaluated as a signed int. When a short /
truncated / chained-mbuf packet makes the value negative, the implicit
widening to size_t for bcopy() produces a near-2^64 byte length
(memmove() underneath). The copy faults on the next unmapped page
immediately -> kernel panic.
Reachability path (confirmed live)
A gre interface configured for mobile encapsulation
(ifconfig greN -link0 -> g_proto = IPPROTO_MOBILE, see
sys/net/gre/if_gre.c:461-470) and brought up with a matching
src/dst pair causes encap_attach() (sys/net/gre/if_gre.c:563-566)
to register in_mobile_protosw (whose .pr_input = gre_mobile_input)
for IPPROTO_MOBILE.
When a crafted IP packet with ip_p = 55 arrives whose outer
src/dst matches the tunnel:
ip_input
-> ip_protox[IPPROTO_MOBILE] /* in_proto.c:201-215 */
-> encap4_input /* ip_encap.c:131 */
-> mask_match finds the gre-mobile entry
-> (*psw->pr_input)(mp,offp,proto)
= gre_mobile_input /* ip_gre.c:205 */
gre_mobile_input does NOT call m_pullup() and does NOT check the
mbuf length before computing the bcopy size β so any packet with
m_len < ip_hl*4 + msiz (i.e. shorter than 28 bytes with no S-bit,
or 32 bytes with S-bit) trips the underflow.
This matches the panic signature observed on the baseline kernel:
panic: vm_fault: fault on stack guard from
memmove+0x24f called via encap4_input+0x20b (i.e. the
(*psw->pr_input)() call site at ip_encap.c:223).
PoC (live path, not a harness)
trigger.c opens a raw socket socket(AF_INET, SOCK_RAW,
IPPROTO_MOBILE) with IP_HDRINCL and sends three short crafted
packets to the loopback address (127.0.0.1), each shorter than the
8-byte (no S-bit) / 12-byte (S-bit) mobile header requires:
Variant A: 24-byte packet, S-bit clear, msiz=8 -> bcopy len = -4 Variant B: 24-byte packet, S-bit set, msiz=12 -> bcopy len = -8 Variant C: 26-byte packet, S-bit clear, msiz=8 -> bcopy len = -2
Each one independently triggers the underflow.
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
Realistic threat model: gre-mobile tunnels are normally configured by an administrator (root). The reachable attacker is remote: anyone who can route a packet with the matching outer IP src/dst to the host. Once the tunnel exists, no further privilege is required to trip the panic β sending a single short IP packet is enough.
Reproduction
BASELINE (unpatched, with-src #0, if_gre.ko as shipped):
- run trigger -> guest dead within ~1s
- boot.log: panic: vm_fault: fault on stack guard,
memmove() at memmove+0x24f,
encap4_input() at encap4_input+0x20b
- ssh no longer answers; vm.sh status => down
Impact: kernel panic / reliable DoS. No write-primitive conversion attempted because the underflow is a bcopy with an absurd size that faults on the next unmapped page before any controlled write lands β the over-write is into the mbuf cluster and trips a stack guard before reaching an attacker-useful victim object. This is a DoS-class bug, not a corruption primitive.
Fix
One-line guard added at ip_gre.c (just after the existing
gre_in_cksum drop, before the bcopy):
if (m->m_len < (ip->ip_hl << 2) + msiz) {
m_freem(m);
return(IPPROTO_DONE);
}
fix.diff is a standalone git apply-able unified diff.
Fix build
Because gre is a loadable module (NOT compiled into the X86_64_GENERIC
static kernel β kldstat shows if_gre.ko is loaded on ifconfig gre
create), the correct single-fix build is the module, not a full
nativekernel:
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
A naive make nativekernel did NOT rebuild if_gre.ko (the warm obj
was reused), so the first fix-validation pass appeared to fail until
the module was rebuilt directly. See fix_build.log.
Fix validation
PATCHED (same #0 static kernel; freshly built if_gre.ko,
sha256 9f0e7e3f0b919b9c269987ef4d153f9d6a18b536b4dfbd21c1bdebe80f2c20a0):
- kldload if_gre (load OK)
- ifconfig gre create / tunnel / -link0 / up (gre0 up)
- run trigger x3 -> all three runs exit 0 cleanly,
guest still answers ssh,
boot.log has NO panic / memmove / encap4_input
entry after the run.
Before: panic, guest dead. After: trigger exits 0, guest up, no panic, no log noise. => fix closes the bug.
Disassembly of the patched gre_mobile_input confirms the new guard:
cmp %ecx,%edx (m_len vs (ip_hl<<2)+msiz) followed by jl drop
just before the bcopy/memmove call site (offsets 0x1732-0x1734 in
the rebuilt module).
Verdict: REPRODUCED on baseline #0 (panic), FIXED on single-fix
module (no panic). Impact class: DoS / kernel panic from a short
crafted packet once a gre-mobile tunnel is configured. No escalation
(write-primitive would-be over-write faults before any controlled
landing).
Fix verification
fixedVALIDATED. Baseline #0 kernel with the shipped if_gre.ko panics ('vm_fault: fault on stack guard' from memmove/encap4_input) on the same trigger. After installing the if_gre.ko built with fix.diff (rebuilt directly via cd /usr/src/sys/net/gre && make KERNBUILDDIR=...; nativekernel alone rebuilds the module but does not install it), three consecutive trigger runs all exited 0 cleanly with no panic and the guest stayed up. The fix closes the bug. (Caveat documented in notes: the static X86_64_GENERIC kernel does not contain ip_gre.c -- gre is a module -- so the proper single-fix artifact is if_gre.ko, not kernel.stripped.)
BEFORE (baseline #0 + shipped if_gre.ko): trigger -> guest dead in ~1s. boot.log shows 'panic: vm_fault: fault on stack guard, addr: 0xfffff80117618000' / 'memmove() at memmove+0x24f' / 'encap4_input() at encap4_input+0x20b' / 'db>'. AFTER (same #0 kernel + patched if_gre.ko): trigger stdout '[DF-0741] variant A sent 24 bytes' / '[DF-0741] variant B sent 24 bytes' / '[DF-0741] variant C sent 26 bytes' / 'TRIG_RC=0'; vm.sh status => up; grep panic|memmove|encap4 boot.log => empty (no panic). x3 consecutive runs identical.
Confirmed kernel references
Detail
Exploit chain
none -- over-read/over-write only, no escalation. The bcopy over-write would land in the mbuf cluster but the size is so large (~2^64) that memmove faults on the kernel stack guard page immediately, before any attacker-controlled write reaches a victim object. There is no corruption primitive to convert into a uid0 chain. Impact ceiling is reliable kernel panic / DoS (CVSS AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:N/A:H).
Evidence (decisive lines)
BASELINE (unpatched #0, shipped if_gre.ko sha256 unknown, 323544 bytes): run trigger -> panic within 1s. boot.log: 'panic: vm_fault: fault on stack guard, addr: 0xfffff80117618000' / 'memmove() at memmove+0x24f 0xffffffff80bcab4f' / 'encap4_input() at encap4_input+0x20b 0xffffffff807aa07b' / 'Debugger("panic")' / 'Stopped at Debugger+0x7c: movb $0,0xbdaf09(%rip)' / 'db>'. vm.sh status => down. Trigger sent three short variants: 24-byte (msiz=8, len would be -4), 24-byte S-bit (msiz=12, len -8), 26-byte (msiz=8, len -2); any one trips the underflow.
PoC changes
Wrote findings/poc/DF-0741/ from scratch (no prior PoC existed): trigger.c (raw-socket injector, 3 short IPPROTO_MOBILE packets), build.sh, run.sh, README.md, VERDICT.md, manifest.json, fix.diff, env.txt, full untrimmed build/run/panic logs. The finding markdown was never created by the orchestrator; I derived everything from the DB summary row.
Verified recommended fix
Add a one-line length guard immediately before the bcopy in gre_mobile_input (sys/netinet/ip_gre.c, just after the existing gre_in_cksum drop at line 235): if (m->m_len < (ip->ip_hl << 2) + msiz) { m_freem(m); return(IPPROTO_DONE); }. This validates that the head mbuf carries the full IP header plus the variable-length mobile header before the bcopy, eliminating the signed-int underflow that widens to ~2^64. Matches the finding proposal in the DB summary (m_freem + IPPROTO_DONE return). Full git-apply-able diff in findings/poc/DF-0741/fix.diff.
Verdict
REPRODUCED on baseline 6.5-DEVELOPMENT #0 (with-src). gre_mobile_input at sys/netinet/ip_gre.c:237-238 computes the bcopy length as the signed-int expression m->m_len - msiz - (ip->ip_hl << 2) and then implicitly widens it to size_t for bcopy(). For a short packet with m_len < ip_hl4 + msiz the value goes negative and becomes ~2^64, so memmove (which bcopy lowers to) faults on the next unmapped page. Live path confirmed: created gre0 in mobile mode (ifconfig gre0 -link0 sets g_proto=IPPROTO_MOBILE via sys/net/gre/if_gre.c:466-469), brought it up with tunnel 127.0.0.1 -> 127.0.0.1, and injected a 24-byte IPPROTO_MOBILE packet via raw socket with IP_HDRINCL. Guest panicked within ~1s with 'panic: vm_fault: fault on stack guard, addr 0xfffff80117618000' from memmove+0x24f called via encap4_input+0x20b (i.e. (psw->pr_input)() at ip_encap.c:223 = gre_mobile_input). ssh died; vm.sh status => down. Reachable remotely by any attacker who can deliver a packet matching the tunnel's outer src/dst to a host with a gre-mobile tunnel configured; no further privilege required. The original DB summary cited sys/net/gre/if_gre.c but the vulnerable code is in sys/netinet/ip_gre.c (the mobile-input handler); both files compile into the if_gre.ko module via sys/net/gre/Makefile.
No comments yet.