From 781083751a89750db2da9e96ceea91e8a4a31890 Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Tue, 11 Aug 2026 11:43:20 +0200 Subject: [PATCH 1/2] fix(pj_plugins): propagate provenance errors from family enumeration A dladdr or path-identity failure during exported-family enumeration now returns its own error instead of folding into an empty family set, so admission surfaces the real cause rather than a misleading "found 0 families" rejection. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0165qLfnJYSYb51NQMdPidZr --- .../pj_plugins/host/plugin_catalog.hpp | 5 ++- pj_plugins/src/plugin_catalog.cpp | 44 ++++++++++++++----- pj_plugins/tests/plugin_catalog_test.cpp | 10 ++++- 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp b/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp index 098a873..cd9fbbc 100644 --- a/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp +++ b/pj_plugins/include/pj_plugins/host/plugin_catalog.hpp @@ -98,8 +98,9 @@ struct PluginScanResult { namespace detail { /// Return the plugin families whose getter symbols are defined by @p dso_path -/// itself. Missing getters and getters supplied only by dependencies are omitted. -[[nodiscard]] std::vector exportedPluginFamilies( +/// itself. Missing getters are omitted; provenance and path-identity failures +/// are returned to the caller instead of being mistaken for absent families. +[[nodiscard]] Expected> exportedPluginFamilies( const std::shared_ptr& handle, const std::filesystem::path& dso_path); } // namespace detail diff --git a/pj_plugins/src/plugin_catalog.cpp b/pj_plugins/src/plugin_catalog.cpp index 6beb978..dbde92e 100644 --- a/pj_plugins/src/plugin_catalog.cpp +++ b/pj_plugins/src/plugin_catalog.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -13,6 +14,7 @@ #include #include #include +#include #include "detail/library_loader.hpp" #include "detail/vtable_validation.hpp" @@ -43,6 +45,16 @@ bool hasDsoSuffix(const std::filesystem::path& path) { return path.extension().string() == kDsoSuffix; } +bool nativeSymbolPresent(void* handle, const char* symbol) noexcept { +#if defined(_WIN32) + return GetProcAddress(reinterpret_cast(handle), symbol) != nullptr; +#else + dlerror(); + void* resolved = dlsym(handle, symbol); + return dlerror() == nullptr && resolved != nullptr; +#endif +} + // Direct-vtable families share the exact same probe sequence: resolve symbol, // call entry, check protocol_version, check struct_size, read manifest_json. // Only the family-specific types and constants vary. @@ -348,26 +360,38 @@ Expected inspectPluginDso( namespace detail { -std::vector exportedPluginFamilies( +Expected> exportedPluginFamilies( const std::shared_ptr& handle, const std::filesystem::path& dso_path) { std::vector families; if (handle == nullptr) { - return families; + return unexpected("library not loaded"); } auto recorded_path = recordLibraryPathIdentity(dso_path); if (!recorded_path) { - return families; + return unexpected(recorded_path.error()); } - auto append_if_owned = [&](const char* symbol, PluginFamily family) { - if (resolveSymbol(handle.get(), symbol, *recorded_path)) { - families.push_back(family); + auto append_if_owned = [&](const char* symbol, PluginFamily family) -> Expected { + if (!nativeSymbolPresent(handle.get(), symbol)) { + return {}; + } + auto resolved = resolveSymbol(handle.get(), symbol, *recorded_path); + if (!resolved) { + return unexpected(resolved.error()); } + families.push_back(family); + return {}; }; - append_if_owned("PJ_get_data_source_vtable", PluginFamily::kDataSource); - append_if_owned("PJ_get_message_parser_vtable", PluginFamily::kMessageParser); - append_if_owned("PJ_get_toolbox_vtable", PluginFamily::kToolbox); - append_if_owned("PJ_get_dialog_vtable", PluginFamily::kDialog); + for (const auto& [symbol, family] : std::array, 4>{ + std::pair{"PJ_get_data_source_vtable", PluginFamily::kDataSource}, + {"PJ_get_message_parser_vtable", PluginFamily::kMessageParser}, + {"PJ_get_toolbox_vtable", PluginFamily::kToolbox}, + {"PJ_get_dialog_vtable", PluginFamily::kDialog}, + }) { + if (auto status = append_if_owned(symbol, family); !status) { + return unexpected(status.error()); + } + } return families; } diff --git a/pj_plugins/tests/plugin_catalog_test.cpp b/pj_plugins/tests/plugin_catalog_test.cpp index a9e8091..17c218e 100644 --- a/pj_plugins/tests/plugin_catalog_test.cpp +++ b/pj_plugins/tests/plugin_catalog_test.cpp @@ -148,6 +148,13 @@ TEST_F(PluginCatalogTest, EntryPointSymbolResolvedFromDependencyIsRejected) { auto dependency_descriptor = inspectPluginDso(PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); ASSERT_FALSE(dependency_descriptor.has_value()); EXPECT_TRUE(is_provenance_error(dependency_descriptor.error())) << dependency_descriptor.error(); + + auto raw_handle = detail::loadLibraryHandle(PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); + ASSERT_TRUE(raw_handle.has_value()) << raw_handle.error(); + auto family_query = detail::exportedPluginFamilies( + detail::adoptLibraryHandle(*raw_handle), PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); + ASSERT_FALSE(family_query.has_value()); + EXPECT_TRUE(is_provenance_error(family_query.error())) << family_query.error(); #endif auto library = DataSourceLibrary::load(PJ_ENTRY_POINT_WITH_OWN_EXPORTS_PLUGIN_PATH); @@ -193,7 +200,8 @@ TEST_F(PluginCatalogTest, AlreadyOpenHandleSupportsInspectionLoadingAndFamilyQue auto shared_handle = detail::adoptLibraryHandleNonOwning(owner.get()); const auto families = detail::exportedPluginFamilies(shared_handle, plugin_path); - EXPECT_EQ(families, std::vector{PluginFamily::kDataSource}); + ASSERT_TRUE(families.has_value()) << families.error(); + EXPECT_EQ(*families, std::vector{PluginFamily::kDataSource}); auto descriptor = inspectPluginDso(shared_handle, plugin_path); ASSERT_TRUE(descriptor.has_value()) << descriptor.error(); From 288d6281aaf339887d74ee542a22c79ca4d62e5b Mon Sep 17 00:00:00 2001 From: Davide Faconti Date: Tue, 11 Aug 2026 12:03:32 +0200 Subject: [PATCH 2/2] fix(tests): platform-honest family-query assertion for the via-dependency fixture RTLD_FIRST hides dependency symbols from dlsym on macOS, so the getter is cleanly absent there; the provenance error is the Linux shape. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_0165qLfnJYSYb51NQMdPidZr --- pj_plugins/tests/plugin_catalog_test.cpp | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pj_plugins/tests/plugin_catalog_test.cpp b/pj_plugins/tests/plugin_catalog_test.cpp index 17c218e..89d4fb2 100644 --- a/pj_plugins/tests/plugin_catalog_test.cpp +++ b/pj_plugins/tests/plugin_catalog_test.cpp @@ -153,8 +153,15 @@ TEST_F(PluginCatalogTest, EntryPointSymbolResolvedFromDependencyIsRejected) { ASSERT_TRUE(raw_handle.has_value()) << raw_handle.error(); auto family_query = detail::exportedPluginFamilies( detail::adoptLibraryHandle(*raw_handle), PJ_ENTRY_POINT_VIA_DEPENDENCY_PLUGIN_PATH); +#if defined(__APPLE__) + // RTLD_FIRST hides dependency symbols from dlsym outright, so the getter is + // cleanly absent on macOS rather than a provenance violation. + ASSERT_TRUE(family_query.has_value()) << family_query.error(); + EXPECT_TRUE(family_query->empty()); +#else ASSERT_FALSE(family_query.has_value()); EXPECT_TRUE(is_provenance_error(family_query.error())) << family_query.error(); +#endif #endif auto library = DataSourceLibrary::load(PJ_ENTRY_POINT_WITH_OWN_EXPORTS_PLUGIN_PATH);