From d418b4c1ae262fbb082fd8e5b88bad6c964466de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20I=C3=B1igo=20Blasco?= Date: Fri, 24 Apr 2026 12:31:45 +0200 Subject: [PATCH 1/3] fix(pj_media): remove stale Qt-fallback demo from pj_media/demos The elseif(PJ_BUILD_DIALOG_ENGINE_QT) branch built a basic Qt::Widgets mcap_image_viewer as a fallback when pj_media_qt was not available. Now that pj_media_qt exists and is the correct path for media demos, this fallback is dead code: it never executes when pj_media_qt is present, and building a raw-widget demo without QRhi support provides no value when pj_media_qt is absent. Remove it to keep the demo CMakeLists clean. --- pj_media/demos/CMakeLists.txt | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/pj_media/demos/CMakeLists.txt b/pj_media/demos/CMakeLists.txt index 2464ea4..2f8d225 100644 --- a/pj_media/demos/CMakeLists.txt +++ b/pj_media/demos/CMakeLists.txt @@ -60,14 +60,4 @@ if(TARGET pj_media_qt) target_compile_options(mp4_video_viewer PRIVATE ${PJ_WARNING_FLAGS}) target_link_libraries(mp4_video_viewer PRIVATE pj_media_qt pj_media_core) endif() -elseif(PJ_BUILD_DIALOG_ENGINE_QT) - find_package(Qt6 REQUIRED COMPONENTS Widgets) - set(CMAKE_AUTOMOC ON) - add_executable(mcap_image_viewer mcap_image_viewer.cpp image_widget.hpp) - target_compile_features(mcap_image_viewer PRIVATE cxx_std_20) - target_compile_options(mcap_image_viewer PRIVATE ${PJ_WARNING_FLAGS}) - target_link_libraries(mcap_image_viewer PRIVATE - pj_media_core pj_datastore mcap::mcap libjpeg-turbo::libjpeg-turbo - Qt6::Widgets - ) endif() From 12c46f3fa360f187b5ad38385275ad0aee23b60c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20I=C3=B1igo=20Blasco?= Date: Fri, 24 Apr 2026 14:07:11 +0200 Subject: [PATCH 2/3] fix(plugins): enforce -fvisibility=hidden on all plugin .so targets pj_emit_plugin_manifest now sets CXX/C_VISIBILITY_PRESET=hidden and VISIBILITY_INLINES_HIDDEN=ON on every plugin target. This completes the RTLD_DEEPBIND replacement strategy: statically bundled deps (e.g. OpenSSL inside paho-mqtt-c) cannot conflict with the host's shared libs because their symbols are hidden and RTLD_LOCAL keeps them out of the global namespace. Only pj_plugin_abi_version and PJ_get__vtable remain default-visible via the PJ_*_PLUGIN macros. Update the library_loader.hpp comment to reflect that the mechanism is now actually in place, not merely aspirational. --- cmake/PjPluginManifest.cmake | 12 ++++++++++++ pj_plugins/src/detail/library_loader.hpp | 10 ++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/cmake/PjPluginManifest.cmake b/cmake/PjPluginManifest.cmake index 1e59e3c..e3534e9 100644 --- a/cmake/PjPluginManifest.cmake +++ b/cmake/PjPluginManifest.cmake @@ -39,6 +39,18 @@ function(pj_emit_plugin_manifest TARGET) message(FATAL_ERROR "pj_emit_plugin_manifest(${TARGET}): FAMILY is required") endif() + # Hide all symbols by default. The only two that must remain visible are + # pj_plugin_abi_version and PJ_get__vtable — both are explicitly + # annotated with visibility("default") by the PJ_*_PLUGIN macros in pj_base. + # This is the replacement for RTLD_DEEPBIND: statically bundled deps (e.g. + # OpenSSL inside paho-mqtt) cannot conflict with the host's shared libs + # because their symbols are hidden and never added to the global namespace. + set_target_properties(${TARGET} PROPERTIES + CXX_VISIBILITY_PRESET hidden + C_VISIBILITY_PRESET hidden + VISIBILITY_INLINES_HIDDEN ON + ) + set(_valid_families data_source message_parser toolbox dialog) list(FIND _valid_families "${ARG_FAMILY}" _family_idx) if(_family_idx LESS 0) diff --git a/pj_plugins/src/detail/library_loader.hpp b/pj_plugins/src/detail/library_loader.hpp index cfdb9b6..392daed 100644 --- a/pj_plugins/src/detail/library_loader.hpp +++ b/pj_plugins/src/detail/library_loader.hpp @@ -40,10 +40,12 @@ inline Expected loadLibraryHandle(std::string_view path) { // breaks LD_PRELOAD'd malloc interposition, which makes every plugin // dlopen fail under AddressSanitizer (and similarly for jemalloc / // tcmalloc interposition in production). Plugin-local symbol isolation - // is instead achieved by building plugins with -fvisibility=hidden and - // explicitly marking only the boot-level exports - // (pj_plugin_abi_version + PJ_get__vtable) as default visible. - // See cmake/PjPluginManifest.cmake for the plugin build flags. + // is achieved by building plugins with -fvisibility=hidden (enforced by + // pj_emit_plugin_manifest in cmake/PjPluginManifest.cmake), marking only + // pj_plugin_abi_version + PJ_get__vtable as default visible via + // the PJ_*_PLUGIN macros. Bundled static deps (e.g. OpenSSL inside + // paho-mqtt) cannot conflict with the host because their symbols are + // hidden and RTLD_LOCAL keeps them out of the global namespace. int flags = RTLD_NOW | RTLD_LOCAL; void* handle = dlopen(std::string(path).c_str(), flags); if (handle == nullptr) { From 5032721afc1a3c7cd07712d248f00260c524d5fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pablo=20I=C3=B1igo=20Blasco?= Date: Mon, 27 Apr 2026 10:28:01 +0200 Subject: [PATCH 3/3] fix(plugins): add -Bsymbolic-functions to complete symbol isolation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit -fvisibility=hidden alone is insufficient when static deps (e.g. libssl.a from Conan) are compiled without that flag: their symbols enter the plugin .so with DEFAULT visibility and their calls go through PLT — which resolves to the host's namespace first (system libssl), causing version-mismatch crashes. -Wl,-Bsymbolic-functions resolves all function calls within the .so directly to the embedded definition, bypassing PLT entirely. This covers pre-compiled static libs like OpenSSL regardless of how they were built. Only applied on Linux/ELF (macOS uses two-level namespace by default — equivalent behavior). Empirically verified with objdump: with -Bsymbolic-functions, calls from plugin_func to SSL_new are 'call SSL_new' (direct, no @plt). Without it, they remain 'call SSL_new@plt' even with -fvisibility=hidden. malloc/pthread/system calls are not defined in the plugin so they still resolve to the host — ASAN malloc interposition is unaffected. --- cmake/PjPluginManifest.cmake | 31 +++++++++++++++++++----- pj_plugins/src/detail/library_loader.hpp | 15 +++++++----- 2 files changed, 34 insertions(+), 12 deletions(-) diff --git a/cmake/PjPluginManifest.cmake b/cmake/PjPluginManifest.cmake index e3534e9..5395dfc 100644 --- a/cmake/PjPluginManifest.cmake +++ b/cmake/PjPluginManifest.cmake @@ -39,17 +39,36 @@ function(pj_emit_plugin_manifest TARGET) message(FATAL_ERROR "pj_emit_plugin_manifest(${TARGET}): FAMILY is required") endif() - # Hide all symbols by default. The only two that must remain visible are - # pj_plugin_abi_version and PJ_get__vtable — both are explicitly - # annotated with visibility("default") by the PJ_*_PLUGIN macros in pj_base. - # This is the replacement for RTLD_DEEPBIND: statically bundled deps (e.g. - # OpenSSL inside paho-mqtt) cannot conflict with the host's shared libs - # because their symbols are hidden and never added to the global namespace. + # Symbol isolation — functional replacement for RTLD_DEEPBIND. + # + # Two complementary mechanisms are needed: + # + # 1. -fvisibility=hidden (compile-time): hides symbols DEFINED in the plugin's + # own source files. Prevents them from being interposable by the host. + # + # 2. -Wl,-Bsymbolic-functions (link-time): makes function calls WITHIN the .so + # resolve to the definitions inside it, bypassing the PLT entirely. + # This is critical for statically bundled deps (e.g. libssl.a from Conan) + # that were compiled WITHOUT -fvisibility=hidden: their symbols enter the .so + # with DEFAULT visibility, and without -Bsymbolic-functions their calls would + # still go through PLT → resolved to the host's namespace first → crash. + # + # Together: all function calls inside the plugin use the embedded static copies. + # The two boot-level exports (pj_plugin_abi_version + PJ_get__vtable) + # keep visibility("default") via the PJ_*_PLUGIN macros and are unaffected. + # malloc / pthread / system calls are NOT defined in the plugin, so they still + # resolve to the host — ASAN malloc interposition works correctly. + # + # -Bsymbolic-functions is Linux/ELF-specific. On macOS the linker uses + # two-level namespace by default (equivalent behavior), so the flag is omitted. set_target_properties(${TARGET} PROPERTIES CXX_VISIBILITY_PRESET hidden C_VISIBILITY_PRESET hidden VISIBILITY_INLINES_HIDDEN ON ) + target_link_options(${TARGET} PRIVATE + $<$:-Wl,-Bsymbolic-functions> + ) set(_valid_families data_source message_parser toolbox dialog) list(FIND _valid_families "${ARG_FAMILY}" _family_idx) diff --git a/pj_plugins/src/detail/library_loader.hpp b/pj_plugins/src/detail/library_loader.hpp index 392daed..91206cf 100644 --- a/pj_plugins/src/detail/library_loader.hpp +++ b/pj_plugins/src/detail/library_loader.hpp @@ -40,12 +40,15 @@ inline Expected loadLibraryHandle(std::string_view path) { // breaks LD_PRELOAD'd malloc interposition, which makes every plugin // dlopen fail under AddressSanitizer (and similarly for jemalloc / // tcmalloc interposition in production). Plugin-local symbol isolation - // is achieved by building plugins with -fvisibility=hidden (enforced by - // pj_emit_plugin_manifest in cmake/PjPluginManifest.cmake), marking only - // pj_plugin_abi_version + PJ_get__vtable as default visible via - // the PJ_*_PLUGIN macros. Bundled static deps (e.g. OpenSSL inside - // paho-mqtt) cannot conflict with the host because their symbols are - // hidden and RTLD_LOCAL keeps them out of the global namespace. + // uses two build-time mechanisms (cmake/PjPluginManifest.cmake): + // 1. -fvisibility=hidden: hides symbols defined in plugin source files. + // 2. -Wl,-Bsymbolic-functions (Linux): function calls within the .so + // resolve to the embedded static copies, bypassing PLT. This covers + // deps compiled without -fvisibility=hidden (e.g. libssl.a from Conan) + // whose symbols enter the .so with default visibility and whose calls + // would otherwise resolve to the host's namespace first via PLT. + // malloc/pthread/system calls are NOT defined in the plugin so they still + // reach the host — ASAN malloc interposition works correctly. int flags = RTLD_NOW | RTLD_LOCAL; void* handle = dlopen(std::string(path).c_str(), flags); if (handle == nullptr) {