From 9909909f0ebe3d1e929e8d658b53e5f5f791815d Mon Sep 17 00:00:00 2001 From: PyVoyager <230523196+PyVoyager@users.noreply.github.com> Date: Fri, 6 Nov 2026 12:09:37 +0500 Subject: [PATCH 1/3] fix(linux): libva < 2.22 vaMapBuffer2 compatibility shim - Add a dynamic shim in ffmpeg_accessors.c forwarding vaMapBuffer2 to vaMapBuffer on libva < 2.22 (such as Ubuntu 24.04 LTS with libva 2.20.0). - Export the helper's dynamic symbols so the dlsym interposer is seen by libavutil. - Accept either vaMapBuffer2 or vaMapBuffer in the VA-API probe. Refs #1340 --- electron/native/pipewire-capture/build.rs | 2 + .../pipewire-capture/csrc/ffmpeg_accessors.c | 94 +++++++++++++++++++ .../native/pipewire-capture/src/encoder.rs | 10 +- 3 files changed, 104 insertions(+), 2 deletions(-) diff --git a/electron/native/pipewire-capture/build.rs b/electron/native/pipewire-capture/build.rs index 72b3121d17..7ae76bb10d 100644 --- a/electron/native/pipewire-capture/build.rs +++ b/electron/native/pipewire-capture/build.rs @@ -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"); @@ -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()); diff --git a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c index 0001bcde58..206add8d3d 100644 --- a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c +++ b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c @@ -1,5 +1,99 @@ +#define _GNU_SOURCE +#include +#include #include +/* 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 + +static void *(*real_dlsym_fn)(void *, const char *) = NULL; +static VAStatus (*real_vaMapBuffer_fn)(VADisplay, VABufferID, void **) = 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 void resolve_real_dlsym(void) +{ + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_NEXT, "dlsym", OSC_DLSYM_GLIBC_VER); + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_DEFAULT, "dlsym", OSC_DLSYM_GLIBC_VER); + } + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34"); + } + if (!real_dlsym_fn) { + real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_DEFAULT, "dlsym", "GLIBC_2.34"); + } + } +} + +/** + * 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; + if (!real_vaMapBuffer_fn) { + 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_dlsym_fn) { + real_vaMapBuffer_fn = + (VAStatus (*)(VADisplay, VABufferID, void **))real_dlsym_fn(h, "vaMapBuffer"); + } + } + if (!real_vaMapBuffer_fn) { + return VA_STATUS_ERROR_UNKNOWN; + } + return real_vaMapBuffer_fn(dpy, buf_id, pbuf); +} + +/** + * Interposes `dlsym` so lookups for `vaMapBuffer2` fall back to our + * `vaMapBuffer`-backed shim when the loaded `libva.so.2` predates 2.22. + * + * NOTE: Overriding dlsym process-wide causes any internal RTLD_NEXT lookups + * in loaded shared objects to resolve relative to this helper binary rather + * than the caller library. In openscreen-pipewire-helper, this interposition + * specifically handles libavutil's runtime resolution of vaMapBuffer2. + */ +void *dlsym(void *handle, const char *name) +{ + resolve_real_dlsym(); + void *sym = real_dlsym_fn ? real_dlsym_fn(handle, name) : NULL; + if (!sym && strcmp(name, "vaMapBuffer2") == 0) { + void *map1 = real_dlsym_fn ? real_dlsym_fn(handle, "vaMapBuffer") : NULL; + if (map1) { + real_vaMapBuffer_fn = (VAStatus (*)(VADisplay, VABufferID, void **))map1; + return (void *)&vaMapBuffer2; + } + } + return sym; +} + /* 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) diff --git a/electron/native/pipewire-capture/src/encoder.rs b/electron/native/pipewire-capture/src/encoder.rs index 9ffbcc9c5e..15f19945fe 100644 --- a/electron/native/pipewire-capture/src/encoder.rs +++ b/electron/native/pipewire-capture/src/encoder.rs @@ -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() } } From 441b1c472056ceb5200357dd3895069a0f23e8e7 Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 13 Nov 2026 03:05:32 +0100 Subject: [PATCH 2/3] fix(linux): make the vaMapBuffer2 shim's function pointers atomic dlsym and vaMapBuffer2 can be called from several threads at once, and both read and write real_dlsym_fn and real_vaMapBuffer_fn. Load and store them with acquire/release atomics so the first-call race is defined. Refs #1340 --- .../pipewire-capture/csrc/ffmpeg_accessors.c | 58 ++++++++++++------- 1 file changed, 36 insertions(+), 22 deletions(-) diff --git a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c index 206add8d3d..808d429106 100644 --- a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c +++ b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c @@ -17,8 +17,14 @@ typedef unsigned int VABufferID; #define VA_STATUS_ERROR_UNKNOWN 0xFFFFFFFF #endif -static void *(*real_dlsym_fn)(void *, const char *) = NULL; -static VAStatus (*real_vaMapBuffer_fn)(VADisplay, VABufferID, void **) = NULL; +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" @@ -31,20 +37,25 @@ static VAStatus (*real_vaMapBuffer_fn)(VADisplay, VABufferID, void **) = NULL; * 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 void resolve_real_dlsym(void) +static dlsym_fn resolve_real_dlsym(void) { - if (!real_dlsym_fn) { - real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_NEXT, "dlsym", OSC_DLSYM_GLIBC_VER); - if (!real_dlsym_fn) { - real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_DEFAULT, "dlsym", OSC_DLSYM_GLIBC_VER); + 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 (!real_dlsym_fn) { - real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_NEXT, "dlsym", "GLIBC_2.34"); + if (!fn) { + fn = (dlsym_fn)dlvsym(RTLD_DEFAULT, "dlsym", "GLIBC_2.34"); } - if (!real_dlsym_fn) { - real_dlsym_fn = (void *(*)(void *, const char *))dlvsym(RTLD_DEFAULT, "dlsym", "GLIBC_2.34"); + if (fn) { + __atomic_store_n(&real_dlsym_fn, fn, __ATOMIC_RELEASE); } } + return fn; } /** @@ -54,21 +65,24 @@ static void resolve_real_dlsym(void) VAStatus vaMapBuffer2(VADisplay dpy, VABufferID buf_id, void **pbuf, unsigned int flags) { (void)flags; - if (!real_vaMapBuffer_fn) { - resolve_real_dlsym(); + 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_dlsym_fn) { - real_vaMapBuffer_fn = - (VAStatus (*)(VADisplay, VABufferID, void **))real_dlsym_fn(h, "vaMapBuffer"); + if (h && real) { + map = (vaMapBuffer_fn)real(h, "vaMapBuffer"); + if (map) { + __atomic_store_n(&real_vaMapBuffer_fn, map, __ATOMIC_RELEASE); + } } } - if (!real_vaMapBuffer_fn) { + if (!map) { return VA_STATUS_ERROR_UNKNOWN; } - return real_vaMapBuffer_fn(dpy, buf_id, pbuf); + return map(dpy, buf_id, pbuf); } /** @@ -82,12 +96,12 @@ VAStatus vaMapBuffer2(VADisplay dpy, VABufferID buf_id, void **pbuf, unsigned in */ void *dlsym(void *handle, const char *name) { - resolve_real_dlsym(); - void *sym = real_dlsym_fn ? real_dlsym_fn(handle, name) : NULL; + dlsym_fn real = resolve_real_dlsym(); + void *sym = real ? real(handle, name) : NULL; if (!sym && strcmp(name, "vaMapBuffer2") == 0) { - void *map1 = real_dlsym_fn ? real_dlsym_fn(handle, "vaMapBuffer") : NULL; + void *map1 = real ? real(handle, "vaMapBuffer") : NULL; if (map1) { - real_vaMapBuffer_fn = (VAStatus (*)(VADisplay, VABufferID, void **))map1; + __atomic_store_n(&real_vaMapBuffer_fn, (vaMapBuffer_fn)map1, __ATOMIC_RELEASE); return (void *)&vaMapBuffer2; } } From f70e2ef258049db3503523bf4fa0b41119d102fd Mon Sep 17 00:00:00 2001 From: EtienneLescot Date: Fri, 13 Nov 2026 03:51:00 +0100 Subject: [PATCH 3/3] fix(linux): keep the caller's RTLD_NEXT origin through the dlsym shim glibc resolves RTLD_NEXT from dlsym's return address. The C wrapper called the real dlsym itself, so every lookup in the process looked like it came from the helper, and an LD_PRELOAD wrapper calling dlsym(RTLD_NEXT, ...) could get its own address back and recurse. The exported dlsym is now a small x86_64/aarch64 entry that asks C where to go and jumps there with the arguments and return address untouched. Only a vaMapBuffer2 lookup on a real handle reaches the libva < 2.22 fallback; every other lookup lands in glibc's dlsym as if the shim were not there. Refs #1340 --- .../pipewire-capture/csrc/ffmpeg_accessors.c | 79 ++++++++++++++++--- 1 file changed, 68 insertions(+), 11 deletions(-) diff --git a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c index 808d429106..d277072c24 100644 --- a/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c +++ b/electron/native/pipewire-capture/csrc/ffmpeg_accessors.c @@ -85,20 +85,12 @@ VAStatus vaMapBuffer2(VADisplay dpy, VABufferID buf_id, void **pbuf, unsigned in return map(dpy, buf_id, pbuf); } -/** - * Interposes `dlsym` so lookups for `vaMapBuffer2` fall back to our - * `vaMapBuffer`-backed shim when the loaded `libva.so.2` predates 2.22. - * - * NOTE: Overriding dlsym process-wide causes any internal RTLD_NEXT lookups - * in loaded shared objects to resolve relative to this helper binary rather - * than the caller library. In openscreen-pipewire-helper, this interposition - * specifically handles libavutil's runtime resolution of vaMapBuffer2. - */ -void *dlsym(void *handle, const char *name) +/* 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; - if (!sym && strcmp(name, "vaMapBuffer2") == 0) { + if (!sym) { void *map1 = real ? real(handle, "vaMapBuffer") : NULL; if (map1) { __atomic_store_n(&real_vaMapBuffer_fn, (vaMapBuffer_fn)map1, __ATOMIC_RELEASE); @@ -108,6 +100,71 @@ void *dlsym(void *handle, const char *name) 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)