/*
 * LD_PRELOAD shim that surfaces the over-unref in
 * media/audio/pulse/pulse_util.cc::MuteAllSinksExcept / UnmuteAllSinks as an
 * AddressSanitizer use-after-poison report.
 *
 * The bug: WaitForOperationCompletion() drops the caller's only reference
 * (pa_operation_unref) on every exit path, then MuteAllSinksExcept /
 * UnmuteAllSinks unref the same pointer AGAIN. libpulse normally caches the
 * freed pa_operation slot on an internal lock-free freelist (PA_STATIC_FLIST)
 * instead of returning it to the system allocator, so on stock Debian/Ubuntu
 * libpulse the second unref trips the
 *     pa_assert(PA_REFCNT_VALUE(o) >= 1)
 * check and aborts; on a -DNDEBUG libpulse it silently re-pushes the slot,
 * aliasing two future pa_operations onto the same storage.
 *
 * To get a clean ASan report at the actual fault site, this shim wraps
 * pa_operation_unref(): after the real unref drops the refcount to 0 (the
 * object is now logically freed and only sitting in libpulse's flist) we
 * ASan-poison the slot. The very next access -- the redundant
 * pa_operation_unref(op) at pulse_util.cc:682 / :721 -- then reads the
 * poisoned refcount word and fires __asan_report_load4 with the buggy call
 * stack.
 *
 * This DOES NOT change what pulse_util.cc does. It only marks
 * already-logically-freed libpulse slots so the sanitizer can see the second
 * unref. Set NOFLIST=0 in run-poc.sh to observe the stock-libpulse behaviour
 * (refcount-assert SIGABRT in the unsandboxed audio-service utility process).
 *
 * Build: cc -shared -fPIC -O2 -o noflist.so noflist.c -ldl
 */

#define _GNU_SOURCE
#include <dlfcn.h>
#include <stddef.h>
#include <stdint.h>

/* sizeof(struct pa_operation) on libpulse 16.x is 88 bytes; the field we care
 * about (the atomic refcount) is at offset 0, so any small region works. */
enum { kPoisonBytes = 64 };

/* Everything ASan is weak so this .so also loads into the non-ASan helpers
 * (timeout, the http server, …) that inherit LD_PRELOAD. */
extern void __asan_poison_memory_region(void const volatile *addr, size_t sz)
    __attribute__((weak));
extern void __asan_unpoison_memory_region(void const volatile *addr, size_t sz)
    __attribute__((weak));
extern int __asan_address_is_poisoned(void const volatile *addr)
    __attribute__((weak));
extern void __asan_report_load4(void *addr) __attribute__((weak));

typedef void (*unref_fn)(void *);

void pa_operation_unref(void *o) {
  static unref_fn real;
  if (!real) {
    real = (unref_fn)dlsym(RTLD_NEXT, "pa_operation_unref");
  }
  if (!o) {
    real(o);
    return;
  }
  /* If this slot was already logically freed by a previous unref we poisoned
   * it below; ask ASan to report the use-after-free *here*, with the buggy
   * caller's stack. (libpulse itself isn't ASan-instrumented, so without this
   * the second unref's load wouldn't be checked.) */
  if (__asan_address_is_poisoned && __asan_address_is_poisoned(o)) {
    if (__asan_report_load4) {
      __asan_report_load4(o);
    }
  }
  int rc = *(volatile int *)o;
  if (rc == 1) {
    /* This unref drops it to 0 -> operation_free() pushes to flist. The slot
     * is now logically dead; poison it so the *next* unref (the bug) is seen
     * by ASan. */
    real(o);
    if (__asan_poison_memory_region) {
      __asan_poison_memory_region(o, kPoisonBytes);
    }
    return;
  }
  real(o);
}

void *pa_flist_pop(void *l) {
  /* If libpulse later legitimately reuses a slot we poisoned, unpoison it
   * before handing it back. This keeps the rest of libpulse happy and limits
   * the poison window to the lifetime in which the bug actually fires. */
  static void *(*real)(void *);
  if (!real) {
    real = (void *(*)(void *))dlsym(RTLD_NEXT, "pa_flist_pop");
  }
  void *p = real(l);
  if (p && __asan_unpoison_memory_region) {
    __asan_unpoison_memory_region(p, kPoisonBytes);
  }
  return p;
}
