Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions electron/native/pipewire-capture/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,7 @@ fn ffmpeg_dir(root: &Path) -> PathBuf {
root.join("../../../crates/thirdparty/ffmpeg-linux64-lgpl-shared")
}

/// Configures Cargo link directives and bindgen for the vendored FFmpeg libraries.
fn link_ffmpeg(dir: &Path) {
let include = dir.join("include");
let lib = dir.join("lib");
Expand Down Expand Up @@ -171,6 +172,7 @@ fn link_ffmpeg(dir: &Path) {
// of the repo. `--disable-new-dtags` is what makes these RUNPATH entries
// apply to the transitive ffmpeg libs too; with the default DT_RUNPATH they
// would not.
println!("cargo:rustc-link-arg=-Wl,--export-dynamic");
println!("cargo:rustc-link-arg=-Wl,--disable-new-dtags");
println!("cargo:rustc-link-arg=-Wl,-rpath,$ORIGIN/helper-ffmpeg");
println!("cargo:rustc-link-arg=-Wl,-rpath,{}", lib.display());
Expand Down
165 changes: 165 additions & 0 deletions electron/native/pipewire-capture/csrc/ffmpeg_accessors.c
Original file line number Diff line number Diff line change
@@ -1,5 +1,170 @@
#define _GNU_SOURCE
#include <dlfcn.h>
#include <string.h>
#include <libavformat/avformat.h>

/* Compatibility shim for libva < 2.22 (e.g. Ubuntu 24.04 LTS with libva 2.20).
* The vendored FFmpeg 8.1 build links libva via implib-gen and resolves
* vaMapBuffer2 at runtime via dlsym(). On libva < 2.22, vaMapBuffer2 is absent
* and the implib trampoline aborts the process. When dlsym(handle, "vaMapBuffer2")
* returns NULL on a handle that exports vaMapBuffer, forward vaMapBuffer2 to
* vaMapBuffer(dpy, buf_id, pbuf). */
typedef int VAStatus;
typedef void *VADisplay;
typedef unsigned int VABufferID;

#ifndef VA_STATUS_ERROR_UNKNOWN
#define VA_STATUS_ERROR_UNKNOWN 0xFFFFFFFF
#endif

typedef void *(*dlsym_fn)(void *, const char *);
typedef VAStatus (*vaMapBuffer_fn)(VADisplay, VABufferID, void **);

/* Both pointers are read and written from any thread that calls dlsym or
* vaMapBuffer2, so every access is atomic. Two threads racing on the first
* call resolve the same value; the atomics only make that race defined. */
static dlsym_fn real_dlsym_fn = NULL;
static vaMapBuffer_fn real_vaMapBuffer_fn = NULL;

#if defined(__aarch64__)
#define OSC_DLSYM_GLIBC_VER "GLIBC_2.17"
#else
#define OSC_DLSYM_GLIBC_VER "GLIBC_2.2.5"
#endif

/**
* Resolves the underlying glibc `dlsym` implementation using the architecture's
* base symbol version (`GLIBC_2.17` on aarch64, `GLIBC_2.2.5` on x86_64) with a
* `GLIBC_2.34` fallback so `real_dlsym_fn` is never left NULL when available.
*/
static dlsym_fn resolve_real_dlsym(void)
{
dlsym_fn fn = __atomic_load_n(&real_dlsym_fn, __ATOMIC_ACQUIRE);
if (!fn) {
fn = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", OSC_DLSYM_GLIBC_VER);
if (!fn) {
fn = (dlsym_fn)dlvsym(RTLD_DEFAULT, "dlsym", OSC_DLSYM_GLIBC_VER);
}
if (!fn) {
fn = (dlsym_fn)dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34");
}
if (!fn) {
fn = (dlsym_fn)dlvsym(RTLD_DEFAULT, "dlsym", "GLIBC_2.34");
}
if (fn) {
__atomic_store_n(&real_dlsym_fn, fn, __ATOMIC_RELEASE);
}
}
return fn;
}

/**
* Compatibility entry point for `vaMapBuffer2` (added in libva 2.22) that
* forwards buffer mapping calls to `vaMapBuffer` on older libva runtimes.
*/
VAStatus vaMapBuffer2(VADisplay dpy, VABufferID buf_id, void **pbuf, unsigned int flags)
{
(void)flags;
vaMapBuffer_fn map = __atomic_load_n(&real_vaMapBuffer_fn, __ATOMIC_ACQUIRE);
if (!map) {
dlsym_fn real = resolve_real_dlsym();
void *h = dlopen("libva.so.2", RTLD_LAZY | RTLD_NOLOAD);
if (!h) {
h = dlopen("libva.so.2", RTLD_LAZY);
}
if (h && real) {
map = (vaMapBuffer_fn)real(h, "vaMapBuffer");
if (map) {
__atomic_store_n(&real_vaMapBuffer_fn, map, __ATOMIC_RELEASE);
}
}
}
if (!map) {
return VA_STATUS_ERROR_UNKNOWN;
}
return map(dpy, buf_id, pbuf);
}

/* The vaMapBuffer2 fallback itself: a real handle asked for vaMapBuffer2. */
static void *osc_dlsym_va(void *handle, const char *name)
{
dlsym_fn real = resolve_real_dlsym();
void *sym = real ? real(handle, name) : NULL;
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
if (!sym) {
void *map1 = real ? real(handle, "vaMapBuffer") : NULL;
if (map1) {
__atomic_store_n(&real_vaMapBuffer_fn, (vaMapBuffer_fn)map1, __ATOMIC_RELEASE);
return (void *)&vaMapBuffer2;
}
}
return sym;
}

static void *osc_dlsym_null(void *handle, const char *name)
{
(void)handle;
(void)name;
return NULL;
}

/**
* Picks where the exported `dlsym` below jumps: the fallback for a vaMapBuffer2
* lookup on a real handle (libavutil's implib trampoline), glibc's dlsym for
* everything else.
*/
__attribute__((visibility("hidden"), used)) void *osc_dlsym_target(void *handle, const char *name)
{
if (handle != RTLD_NEXT && handle != RTLD_DEFAULT && name != NULL &&
strcmp(name, "vaMapBuffer2") == 0) {
return (void *)&osc_dlsym_va;
}
dlsym_fn real = resolve_real_dlsym();
return real ? (void *)real : (void *)&osc_dlsym_null;
}

/* Interposes `dlsym` process-wide, so libavutil's vaMapBuffer2 lookup reaches
* the fallback when the loaded libva.so.2 predates 2.22.
*
* WHY ASSEMBLY. glibc resolves RTLD_NEXT (and the caller's scope for
* RTLD_DEFAULT) from dlsym's return address. A C wrapper that calls the real
* dlsym makes every lookup look like it comes from this helper, so an
* LD_PRELOAD wrapper calling dlsym(RTLD_NEXT, ...) could get its own address
* back and recurse. This entry asks osc_dlsym_target where to go, restores the
* arguments, and JUMPS there: the real dlsym sees the original caller's return
* address, so every other lookup behaves exactly as without the shim. */
#if defined(__x86_64__)
__asm__(".text\n"
".globl dlsym\n"
".type dlsym, @function\n"
"dlsym:\n"
#if defined(__CET__)
" endbr64\n"
#endif
" push %rdi\n"
" push %rsi\n"
" sub $8, %rsp\n"
" call osc_dlsym_target\n"
" add $8, %rsp\n"
" pop %rsi\n"
" pop %rdi\n"
" jmp *%rax\n"
".size dlsym, .-dlsym\n");
#elif defined(__aarch64__)
__asm__(".text\n"
".globl dlsym\n"
".type dlsym, %function\n"
"dlsym:\n"
" stp x29, x30, [sp, #-32]!\n"
" mov x29, sp\n"
" stp x0, x1, [sp, #16]\n"
" bl osc_dlsym_target\n"
" mov x16, x0\n"
" ldp x0, x1, [sp, #16]\n"
" ldp x29, x30, [sp], #32\n"
" br x16\n"
".size dlsym, .-dlsym\n");
#endif

/* FFmpeg 8 exposes AVFormatContext as opaque to bindgen on some clang builds.
* Keep these field reads in C, compiled against the exact headers being linked. */
AVIOContext **osc_avformat_pb(AVFormatContext *context)
Expand Down
10 changes: 8 additions & 2 deletions electron/native/pipewire-capture/src/encoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,15 +165,21 @@ pub fn prepare_environment() {
/// against, and what matters is what is loadable here and now.
fn vaapi_is_safe_to_probe() -> bool {
// SAFETY: dlopen/dlsym with a literal name; every pointer is checked before
// use and the handle is closed on both paths.
// use and the handle is closed on both paths. csrc/ffmpeg_accessors.c
// provides a vaMapBuffer2 -> vaMapBuffer compatibility bridge for libva < 2.22.
unsafe {
let handle = dlopen(c"libva.so.2".as_ptr(), RTLD_LAZY | RTLD_LOCAL);
if handle.is_null() {
return false;
}
let symbol = dlsym(handle, c"vaMapBuffer2".as_ptr());
let fallback = if symbol.is_null() {
dlsym(handle, c"vaMapBuffer".as_ptr())
} else {
symbol
};
dlclose(handle);
!symbol.is_null()
!fallback.is_null()
}
}

Expand Down
Loading