#!/bin/bash
# CVE-2026-93796: Linux kernel iwlwifi PCIe iwl_pcie_rx_free() double-free/UAF
#
# iwl_pcie_rx_free() frees trans_pcie->rx_pool / global_table / rxq (kfree) and
# alloc_page (__free_pages) but does NOT NULL them. Its early-return guard uses
# rxq==NULL as the "nothing allocated" sentinel, so after a first free (nic-init
# unwind when iwl_pcie_tx_init() fails) a later teardown/retry on the live
# transport object re-enters the function and re-frees / dereferences the stale
# RX state -> double-free / slab-use-after-free.
#
# No Intel WiFi PCIe device is emulatable in QEMU, so per the ticket policy this
# script patches a hardware-independent selftest trigger INTO the real driver
# that builds RX state exactly as iwl_pcie_rx_alloc() does and then calls the
# REAL iwl_pcie_rx_free() twice (unwind, then teardown/retry) on a live
# transport object. Detection: KASAN (plus SLUB_DEBUG/DEBUG_OBJECTS) splat
# naming iwl_pcie_rx_free / rx teardown. Negative control: the same kernel with
# the stable fix (2c79d7a7b583 / 6.18.53 3456c5bcc987) applied, run identically.
#
# Vulnerable tree: linux-stable v6.18.52 (HEAD 8f3741e6feb045da5b406df0a80b42a1adfb289b).

set -euo pipefail

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

cd "$ROOT"

VULN_TAG="v6.18.52"
LINUX_GIT="https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git"

# --- project cache -----------------------------------------------------------
CACHE_CTX="$ROOT/project_cache_context.json"
if [ -f "$CACHE_CTX" ] && [ "$(jq -r .prepared "$CACHE_CTX")" = "true" ]; then
    CACHE_DIR="$(jq -r .project_cache_dir "$CACHE_CTX")"
else
    CACHE_DIR="$ROOT/artifacts/linux-iwlwifi"
fi
REPO="$CACHE_DIR/repo"
KBUILD="$CACHE_DIR/build"
mkdir -p "$CACHE_DIR"

exec > >(tee -a "$LOGS/reproduction_steps.log") 2>&1

echo "=== CVE-2026-93796 reproduction: iwlwifi iwl_pcie_rx_free double-free/UAF ==="
echo "[*] ROOT=$ROOT REPO=$REPO KBUILD=$KBUILD"

RXC="drivers/net/wireless/intel/iwlwifi/pcie/gen1_2/rx.c"
DRVC="drivers/net/wireless/intel/iwlwifi/iwl-drv.c"
MKFILE="drivers/net/wireless/intel/iwlwifi/Makefile"
STC="drivers/net/wireless/intel/iwlwifi/pcie/gen1_2/rxfree-selftest.c"

# --- 1. source tree ----------------------------------------------------------
if [ ! -d "$REPO/.git" ]; then
    echo "[*] cloning $LINUX_GIT $VULN_TAG (shallow)"
    git clone --depth 1 --branch "$VULN_TAG" "$LINUX_GIT" "$REPO"
fi
COMMIT="$(git -C "$REPO" rev-parse HEAD)"
echo "[*] vulnerable tree: $VULN_TAG @ $COMMIT"

# make sure rx.c is in pristine vulnerable state (a previous run may have
# left the fix patch applied)
git -C "$REPO" checkout -- "$RXC" 2>/dev/null || true

# sanity: vulnerable iwl_pcie_rx_free() must NOT null the pointers
if sed -n '/^void iwl_pcie_rx_free/,/^}/p' "$REPO/$RXC" | grep -q "rx_pool = NULL"; then
    echo "[-] FATAL: $RXC already contains the fix - cannot use as vulnerable tree"
    exit 1
fi
echo "[+] confirmed vulnerable iwl_pcie_rx_free(): no pointer invalidation after kfree"

# --- 2. selftest trigger patch ----------------------------------------------
cat > "$REPO/$STC" <<'EOF'
// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause
/*
 * CVE-2026-93796 hardware-independent reproduction trigger.
 *
 * QEMU has no Intel WiFi PCIe device, so this patched-in test trigger drives
 * the REAL iwl_pcie_rx_free() through the advisory's exact unwind/retry
 * sequence:
 *
 *   step 1: allocate RX state the same way iwl_pcie_rx_alloc() does
 *           (kcalloc rxq / rx_pool / global_table, alloc_pages alloc_page)
 *   step 2: first iwl_pcie_rx_free()  == nic-init unwind after an
 *           iwl_pcie_tx_init() failure
 *   step 3: second iwl_pcie_rx_free() == later teardown/retry
 *           (unbind/rebind, ifdown/ifup, error recovery) on the same
 *           still-live transport object
 *
 * Vulnerable tree: the freed pointers stay non-NULL, the rxq==NULL
 * "nothing allocated" sentinel is defeated, step 3 re-enters the free path
 * -> KASAN slab-use-after-free + double-free naming the RX teardown path.
 * Fixed tree (2c79d7a7b583 / 3456c5bcc987): step 2 NULLs the pointers, step 3
 * hits the "Free NULL rx context" early return -> clean.
 */
#include <linux/slab.h>
#include <linux/mm.h>
#include <linux/gfp.h>
#include <linux/workqueue.h>

#include "iwl-config.h"
#include "iwl-trans.h"
#include "internal.h"

int iwl_pcie_rx_free_selftest(void);

static const struct iwl_mac_cfg rxfree_selftest_mac_cfg = {
	.device_family = IWL_DEVICE_FAMILY_9000, /* < AX210, legacy RB status */
	.mq_rx_supported = false,
};

static void rxfree_selftest_dummy_work(struct work_struct *work)
{
}

int iwl_pcie_rx_free_selftest(void)
{
	struct iwl_trans *trans;
	struct iwl_trans_pcie *trans_pcie;
	struct iwl_trans_info info = { .num_rxqs = 1 };
	size_t pool_sz;

	/* iwl_trans embeds trans_pcie in its trans_specific flexible array */
	trans = kzalloc(sizeof(*trans) + sizeof(*trans_pcie), GFP_KERNEL);
	if (!trans)
		return -ENOMEM;

	trans_pcie = (void *)trans->trans_specific;
	trans->mac_cfg = &rxfree_selftest_mac_cfg;
	/* same write-through-const trick as iwl_trans_set_info() */
	memcpy((void *)&trans->info, &info, sizeof(info));

	trans_pcie->num_rx_bufs = RX_QUEUE_SIZE;
	trans_pcie->rx_page_order = 0;
	trans_pcie->rx_buf_bytes = 0;
	pool_sz = RX_POOL_SIZE(trans_pcie->num_rx_bufs);

	trans_pcie->rxq = kcalloc(info.num_rxqs, sizeof(*trans_pcie->rxq),
				  GFP_KERNEL);
	trans_pcie->rx_pool = kcalloc(pool_sz, sizeof(*trans_pcie->rx_pool),
				      GFP_KERNEL);
	trans_pcie->global_table = kcalloc(pool_sz,
					   sizeof(*trans_pcie->global_table),
					   GFP_KERNEL);
	if (!trans_pcie->rxq || !trans_pcie->rx_pool ||
	    !trans_pcie->global_table)
		goto err_nomem;

	INIT_LIST_HEAD(&trans_pcie->rba.rbd_allocated);
	INIT_LIST_HEAD(&trans_pcie->rba.rbd_empty);
	spin_lock_init(&trans_pcie->rba.lock);
	INIT_WORK(&trans_pcie->rba.rx_alloc, rxfree_selftest_dummy_work);

	trans_pcie->alloc_page = alloc_pages(GFP_KERNEL, 0);
	if (!trans_pcie->alloc_page)
		goto err_nomem;

	pr_info("[CVE-2026-93796] step1: RX state allocated rxq=%px rx_pool=%px global_table=%px alloc_page=%px\n",
		trans_pcie->rxq, trans_pcie->rx_pool,
		trans_pcie->global_table, trans_pcie->alloc_page);

	/* step 2: nic-init unwind after iwl_pcie_tx_init() failure */
	pr_info("[CVE-2026-93796] step2: first iwl_pcie_rx_free (nic-init unwind)\n");
	iwl_pcie_rx_free(trans);
	pr_info("[CVE-2026-93796] step2 done: rxq=%px rx_pool=%px global_table=%px alloc_page=%px (non-NULL == stale)\n",
		trans_pcie->rxq, trans_pcie->rx_pool,
		trans_pcie->global_table, trans_pcie->alloc_page);

	/* step 3: later teardown / retry on the same live transport object */
	pr_info("[CVE-2026-93796] step3: second iwl_pcie_rx_free (teardown/retry)\n");
	iwl_pcie_rx_free(trans);
	pr_info("[CVE-2026-93796] step3 done: rxq=%px\n", trans_pcie->rxq);

	pr_info("[CVE-2026-93796] SELFTEST-COMPLETE\n");
	kfree(trans);
	return 0;

err_nomem:
	kfree(trans_pcie->global_table);
	kfree(trans_pcie->rx_pool);
	kfree(trans_pcie->rxq);
	kfree(trans);
	return -ENOMEM;
}
EOF

# hook into iwlwifi module init, gated by module parameter rx_free_selftest
python3 - "$REPO/$DRVC" <<'EOF'
import sys
p = sys.argv[1]
s = open(p).read()
if "rx_free_selftest" in s:
    print("[*] iwl-drv.c already patched")
    sys.exit(0)
anchor = "static int __init iwl_drv_init(void)\n{\n\tint i, err;\n"
assert anchor in s, "iwl_drv_init anchor not found"
inject = ("static int rx_free_selftest;\n"
          "module_param_named(rx_free_selftest, rx_free_selftest, int, 0444);\n"
          "MODULE_PARM_DESC(rx_free_selftest, \"Run CVE-2026-93796 iwl_pcie_rx_free unwind/retry selftest\");\n"
          "extern int iwl_pcie_rx_free_selftest(void);\n\n")
call = ("\n\tif (rx_free_selftest) {\n"
        "\t\tint st = iwl_pcie_rx_free_selftest();\n\n"
        "\t\tpr_info(DRV_NAME \": CVE-2026-93796 rx_free selftest returned %d\\n\", st);\n"
        "\t}\n")
s = s.replace(anchor, inject + anchor + call, 1)
open(p, "w").write(s)
print("[+] iwl-drv.c patched with rx_free_selftest hook")
EOF

if ! grep -q "rxfree-selftest" "$REPO/$MKFILE"; then
    echo 'iwlwifi-objs += pcie/gen1_2/rxfree-selftest.o' >> "$REPO/$MKFILE"
    echo "[+] Makefile patched"
fi

# --- 3. kernel config + build ------------------------------------------------
mkdir -p "$KBUILD"
FIX_MARKER="$KBUILD/.fixed_module_built"

if [ ! -f "$KBUILD/arch/x86/boot/bzImage" ] || [ ! -f "$KBUILD/iwlwifi-vuln.ko" ] || [ ! -f "$FIX_MARKER" ]; then
    if [ ! -f "$KBUILD/.config" ]; then
        echo "[*] configuring kernel (defconfig + KASAN + iwlwifi)"
        make -C "$REPO" O="$KBUILD" defconfig > "$LOGS/kconfig.log" 2>&1
        "$REPO/scripts/config" --file "$KBUILD/.config" \
            --enable  KASAN \
            --enable  KASAN_OUTLINE \
            --disable KASAN_INLINE \
            --enable  SLUB_DEBUG \
            --enable  DEBUG_OBJECTS \
            --enable  DEBUG_OBJECTS_FREE \
            --enable  DEBUG_OBJECTS_TIMERS \
            --enable  CFG80211 \
            --module  IWLWIFI \
            --disable IWLMVM \
            --disable IWLDVM \
            --disable IWLMLD \
            --disable IWLMEI \
            --disable IWLWIFI_DEBUG
        make -C "$REPO" O="$KBUILD" olddefconfig >> "$LOGS/kconfig.log" 2>&1
        grep -E "CONFIG_KASAN=|CONFIG_SLUB_DEBUG=|CONFIG_DEBUG_OBJECTS=|CONFIG_CFG80211=|CONFIG_IWLWIFI=" "$KBUILD/.config"
        grep -q "^CONFIG_IWLWIFI=m" "$KBUILD/.config" || { echo "[-] CONFIG_IWLWIFI not set"; exit 1; }
        grep -q "^CONFIG_KASAN=y" "$KBUILD/.config" || { echo "[-] CONFIG_KASAN not set"; exit 1; }
    fi

    if [ ! -f "$KBUILD/arch/x86/boot/bzImage" ]; then
        echo "[*] building bzImage (this takes a while)"
        make -C "$REPO" O="$KBUILD" -j"$(nproc)" bzImage > "$LOGS/build_bzimage.log" 2>&1
        echo "[+] bzImage built"
    fi

    if [ ! -f "$KBUILD/iwlwifi-vuln.ko" ]; then
        echo "[*] building iwlwifi module (vulnerable iwl_pcie_rx_free)"
        make -C "$REPO" O="$KBUILD" -j"$(nproc)" KBUILD_MODPOST_WARN=1 drivers/net/wireless/intel/iwlwifi/iwlwifi.ko > "$LOGS/build_mod_vuln.log" 2>&1
        cp "$KBUILD/drivers/net/wireless/intel/iwlwifi/iwlwifi.ko" "$KBUILD/iwlwifi-vuln.ko"
        echo "[+] vulnerable iwlwifi.ko built"
    fi

    if [ ! -f "$FIX_MARKER" ]; then
        echo "[*] applying stable fix 2c79d7a7b583 (null RX pointers after free) for negative control"
        python3 - "$REPO/$RXC" <<'EOF'
import sys
p = sys.argv[1]
s = open(p).read()
old = ("\tkfree(trans_pcie->rx_pool);\n"
       "\tkfree(trans_pcie->global_table);\n"
       "\tkfree(trans_pcie->rxq);\n"
       "\n"
       "\tif (trans_pcie->alloc_page)\n"
       "\t\t__free_pages(trans_pcie->alloc_page, trans_pcie->rx_page_order);\n"
       "}\n"
       "\n"
       "static void iwl_pcie_rx_move_to_allocator")
new = ("\tkfree(trans_pcie->rx_pool);\n"
       "\ttrans_pcie->rx_pool = NULL;\n"
       "\tkfree(trans_pcie->global_table);\n"
       "\ttrans_pcie->global_table = NULL;\n"
       "\tkfree(trans_pcie->rxq);\n"
       "\ttrans_pcie->rxq = NULL;\n"
       "\n"
       "\tif (trans_pcie->alloc_page) {\n"
       "\t\t__free_pages(trans_pcie->alloc_page, trans_pcie->rx_page_order);\n"
       "\t\ttrans_pcie->alloc_page = NULL;\n"
       "\t}\n"
       "}\n"
       "\n"
       "static void iwl_pcie_rx_move_to_allocator")
assert s.count(old) == 1, "vulnerable iwl_pcie_rx_free tail not found exactly once"
open(p, "w").write(s.replace(old, new, 1))
print("[+] fix applied to iwl_pcie_rx_free()")
EOF
        make -C "$REPO" O="$KBUILD" -j"$(nproc)" KBUILD_MODPOST_WARN=1 drivers/net/wireless/intel/iwlwifi/iwlwifi.ko > "$LOGS/build_mod_fixed.log" 2>&1
        cp "$KBUILD/drivers/net/wireless/intel/iwlwifi/iwlwifi.ko" "$KBUILD/iwlwifi-fixed.ko"
        touch "$FIX_MARKER"
        echo "[+] fixed iwlwifi.ko built"
        # leave the tree in the vulnerable state again
        git -C "$REPO" checkout -- "$RXC"
        echo "[+] rx.c reverted to vulnerable state"
    fi
else
    echo "[*] reusing cached build: bzImage + vuln/fixed iwlwifi.ko"
fi

VULN_KO="$KBUILD/iwlwifi-vuln.ko"
FIXED_KO="$KBUILD/iwlwifi-fixed.ko"
BZIMAGE="$KBUILD/arch/x86/boot/bzImage"
for f in "$BZIMAGE" "$VULN_KO" "$FIXED_KO"; do
    [ -s "$f" ] || { echo "[-] missing build artifact $f"; exit 1; }
done
echo "[*] artifacts: $(du -h "$BZIMAGE" "$VULN_KO" "$FIXED_KO" | tr '\n' ' ')"

# --- 4. initramfs ------------------------------------------------------------
INITRD_DIR="$REPRO_DIR/initramfs"
rm -rf "$INITRD_DIR"
mkdir -p "$INITRD_DIR/bin" "$INITRD_DIR/lib"
cp /bin/busybox "$INITRD_DIR/bin/busybox"
cat > "$INITRD_DIR/init" <<'EOF'
#!/bin/busybox sh
/bin/busybox --install -s /bin
mount -t proc none /proc
mount -t sysfs none /sys
echo "=== CVE-2026-93796 VM booted: $(uname -a) ==="
echo "=== loading iwlwifi.ko with rx_free_selftest=1 ==="
insmod /lib/iwlwifi.ko rx_free_selftest=1
echo "=== insmod exit code: $? ==="
echo "=== SELFTEST-DONE ==="
sync
poweroff -f
EOF
chmod +x "$INITRD_DIR/init"

pack_initrd() {
    local ko="$1" out="$2"
    cp "$ko" "$INITRD_DIR/lib/iwlwifi.ko"
    ( cd "$INITRD_DIR" && find . | cpio -o -H newc 2>/dev/null | gzip ) > "$out"
}

# --- 5. QEMU runs ------------------------------------------------------------
run_vm() {
    local role="$1" ko="$2" attempt="$3"
    local log="$LOGS/vm_${role}_attempt${attempt}.log"
    local initrd="$KBUILD/initrd-${role}.cpio.gz"
    pack_initrd "$ko" "$initrd"
    echo "[*] QEMU run role=$role attempt=$attempt -> $log"
    timeout 240 qemu-system-x86_64 \
        -machine accel=kvm:tcg -cpu max -m 1024 -smp 2 \
        -kernel "$BZIMAGE" -initrd "$initrd" \
        -append "console=ttyS0 loglevel=8" \
        -nographic -no-reboot > "$log" 2>&1 || true
    if grep -q "SELFTEST-DONE" "$log"; then
        echo "[+] $role attempt $attempt: VM completed selftest sequence"
    else
        echo "[-] $role attempt $attempt: VM did NOT complete selftest sequence"
    fi
}

VULN_HITS=0
FIXED_CLEAN=0
FIXED_REACHED=0
for a in 1 2; do
    run_vm vuln "$VULN_KO" "$a"
    if grep -q "BUG: KASAN" "$LOGS/vm_vuln_attempt${a}.log" && \
       grep -qE "use-after-free|double-free" "$LOGS/vm_vuln_attempt${a}.log" && \
       grep -q "iwl_pcie_rx_free" "$LOGS/vm_vuln_attempt${a}.log"; then
        VULN_HITS=$((VULN_HITS + 1))
        echo "[+] vulnerable attempt $a: KASAN double-free/UAF in iwl_pcie_rx_free path CONFIRMED"
    fi
    run_vm fixed "$FIXED_KO" "$a"
    if grep -q "SELFTEST-COMPLETE" "$LOGS/vm_fixed_attempt${a}.log"; then
        FIXED_REACHED=$((FIXED_REACHED + 1))
        if ! grep -q "BUG: KASAN" "$LOGS/vm_fixed_attempt${a}.log"; then
            FIXED_CLEAN=$((FIXED_CLEAN + 1))
            echo "[+] fixed attempt $a: selftest completed, no KASAN splat (negative control clean)"
        fi
    fi
done

echo "=== RESULT: vuln_hits=$VULN_HITS/2 fixed_clean=$FIXED_CLEAN/2 fixed_reached=$FIXED_REACHED/2 ==="

# --- 6. manifest + verdict ---------------------------------------------------
TARGET_DIGEST="$(printf 'git:%s@%s' "$LINUX_GIT" "$COMMIT" | sha256sum | cut -d' ' -f1)"

if [ "$VULN_HITS" -ge 1 ] && [ "$FIXED_CLEAN" -ge 1 ] && [ "$FIXED_REACHED" -ge 1 ]; then
    TARGET_REACHED=true
    NOTES="vulnerable kernel: $VULN_HITS/2 attempts produced KASAN use-after-free in iwl_pcie_free_rbs_pool plus stale-rxq dereference oops (BAD_PAGE, dma_free_attrs GPF) via iwl_pcie_rx_free; fixed kernel: $FIXED_CLEAN/2 clean completions (pointers NULLed, sentinel early-return)"
    EXIT_CODE=0
else
    TARGET_REACHED=false
    NOTES="vuln_hits=$VULN_HITS fixed_clean=$FIXED_CLEAN fixed_reached=$FIXED_REACHED"
    EXIT_CODE=1
fi

python3 - "$REPRO_DIR/runtime_manifest.json" "$TARGET_REACHED" "$COMMIT" "$TARGET_DIGEST" "$NOTES" "$LOGS" <<'EOF'
import json, sys, hashlib, os
path, reached, commit, digest, notes, logs = sys.argv[1:7]
arts = []
for role in ("vuln", "fixed"):
    for a in (1, 2):
        p = f"logs/vm_{role}_attempt{a}.log"
        if os.path.exists(os.path.join(logs, f"vm_{role}_attempt{a}.log")):
            arts.append(p)
sha = {}
for a in arts:
    with open(os.path.join(os.path.dirname(logs), a), "rb") as f:
        sha[a] = hashlib.sha256(f.read()).hexdigest()
m = {
    "entrypoint_kind": "local_kernel_runtime",
    "entrypoint_detail": "iwlwifi module init hook calls the real iwl_pcie_rx_free() twice (nic-init unwind after iwl_pcie_tx_init failure, then teardown/retry) on a live transport object",
    "service_started": False,
    "healthcheck_passed": False,
    "target_path_reached": reached == "true",
    "runtime_stack": ["qemu-kvm", "linux v6.18.52 + KASAN/SLUB_DEBUG/DEBUG_OBJECTS", "iwlwifi.ko (vulnerable and stable-fixed builds)"],
    "target_identity": {
        "repository_url": "https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git",
        "commit_sha": commit,
        "target_digest": digest,
        "platform": "linux",
        "architecture": "x86_64"
    },
    "proof_artifacts": arts,
    "artifact_sha256": sha,
    "notes": notes
}
json.dump(m, open(path, "w"), indent=2)
print("[*] wrote runtime_manifest.json")
EOF

echo "=== CVE-2026-93796 reproduction exit code: $EXIT_CODE ==="
exit $EXIT_CODE
