diff --git a/include/paimon/predicate/predicate.h b/include/paimon/predicate/predicate.h index 8248b9576..907da5758 100644 --- a/include/paimon/predicate/predicate.h +++ b/include/paimon/predicate/predicate.h @@ -31,7 +31,7 @@ class Function; /// @see PredicateBuilder class PAIMON_EXPORT Predicate { public: - virtual ~Predicate() = default; + virtual ~Predicate(); virtual bool operator==(const Predicate& other) const = 0; virtual const Function& GetFunction() const = 0; diff --git a/src/paimon/CMakeLists.txt b/src/paimon/CMakeLists.txt index ca8f7635b..5cd69337b 100644 --- a/src/paimon/CMakeLists.txt +++ b/src/paimon/CMakeLists.txt @@ -121,6 +121,7 @@ set(PAIMON_COMMON_SRCS common/predicate/not_equal.cpp common/predicate/not_in.cpp common/predicate/or.cpp + common/predicate/predicate.cpp common/predicate/predicate_builder.cpp common/predicate/predicate_utils.cpp common/predicate/starts_with.cpp diff --git a/src/paimon/common/predicate/predicate.cpp b/src/paimon/common/predicate/predicate.cpp new file mode 100644 index 000000000..64f045ccc --- /dev/null +++ b/src/paimon/common/predicate/predicate.cpp @@ -0,0 +1,22 @@ +/* + * Copyright 2026-present Alibaba Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "paimon/predicate/predicate.h" + +namespace paimon { +/// key function technique for Predicate +Predicate::~Predicate() = default; +} // namespace paimon diff --git a/test/inte/CMakeLists.txt b/test/inte/CMakeLists.txt index cb1d824c8..58b002764 100644 --- a/test/inte/CMakeLists.txt +++ b/test/inte/CMakeLists.txt @@ -111,4 +111,11 @@ if(PAIMON_BUILD_TESTS) test_utils_static ${GTEST_LINK_TOOLCHAIN}) + add_paimon_test(predicate_abi_inte_test + STATIC_LINK_LIBS + paimon_shared + ${TEST_STATIC_LINK_LIBS} + test_utils_static + ${GTEST_LINK_TOOLCHAIN}) + endif() diff --git a/test/inte/predicate_abi_inte_test.cpp b/test/inte/predicate_abi_inte_test.cpp new file mode 100644 index 000000000..74b9c61e8 --- /dev/null +++ b/test/inte/predicate_abi_inte_test.cpp @@ -0,0 +1,232 @@ +/* + * Copyright 2026-present Alibaba Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +// Regression test for the Predicate cross-DSO RTTI/ABI fix (the out-of-line +// `Predicate::~Predicate` key function in common/predicate/predicate.cpp). +// +// `Predicate` is an abstract base with only pure virtuals. Without an +// out-of-line virtual ("key function"), the compiler has no single home for its +// vtable and typeinfo, so it emits them *weakly* in every translation unit that +// uses them. Combined with `-fvisibility=hidden`, separately linked modules +// (e.g. libpaimon.so vs libpaimon_parquet_file_format.so, or plugins loaded via +// dlopen) can each end up with their own `Predicate` typeinfo. A cross-module +// `dynamic_cast`/`dynamic_pointer_cast` then compares mismatched typeinfo and +// fails. Giving `Predicate` an out-of-line destructor anchors a single, +// exported definition of its RTTI that all other modules import. +// +// This file guards the fix on two levels: +// 1. Behavior: `Predicate` objects are created inside libpaimon.so and cast to +// derived types here in the test module. This catches the failure on +// toolchains that compare std::type_info by pointer (e.g. libc++) and in +// downstream builds that link plugins aggressively. +// 2. Symbol layout (Linux/ELF only): assert that `Predicate`'s typeinfo has a +// single home in libpaimon.so which the format plugins import, rather than +// each plugin emitting its own duplicate copy. This is the deterministic +// signature of the key function on glibc/libstdc++ toolchains, which merge +// weak typeinfo by name and therefore would not fail the behavioral cast +// even without the fix. + +#include + +#include "gtest/gtest.h" +#include "paimon/defs.h" +#include "paimon/predicate/compound_predicate.h" +#include "paimon/predicate/leaf_predicate.h" +#include "paimon/predicate/literal.h" +#include "paimon/predicate/predicate.h" +#include "paimon/predicate/predicate_builder.h" +#include "paimon/testing/utils/testharness.h" + +namespace paimon::test { + +// A leaf predicate built in libpaimon.so must survive dynamic_pointer_cast to +// LeafPredicate across the module boundary. +TEST(PredicateAbiInteTest, LeafPredicateCastsAcrossModule) { + std::shared_ptr predicate = + PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, + Literal(static_cast(5))); + ASSERT_NE(predicate, nullptr); + + std::shared_ptr leaf = std::dynamic_pointer_cast(predicate); + ASSERT_NE(leaf, nullptr) << "dynamic_pointer_cast failed across the module " + "boundary; Predicate RTTI is likely duplicated (missing key " + "function)"; + EXPECT_EQ(leaf->FieldName(), "f0"); + EXPECT_EQ(leaf->FieldIndex(), 0); + + // A leaf predicate must not be mistaken for a compound predicate. + EXPECT_EQ(std::dynamic_pointer_cast(predicate), nullptr); +} + +// A compound predicate built in libpaimon.so must survive dynamic_pointer_cast +// to CompoundPredicate, and its children must remain castable to LeafPredicate. +TEST(PredicateAbiInteTest, CompoundPredicateCastsAcrossModule) { + std::shared_ptr left = + PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, + Literal(static_cast(5))); + std::shared_ptr right = PredicateBuilder::Equal( + /*field_index=*/1, /*field_name=*/"f1", FieldType::INT, Literal(10)); + + ASSERT_OK_AND_ASSIGN(std::shared_ptr predicate, + PredicateBuilder::And({left, right})); + + std::shared_ptr compound = + std::dynamic_pointer_cast(predicate); + ASSERT_NE(compound, nullptr) + << "dynamic_pointer_cast failed across the module boundary"; + ASSERT_EQ(compound->Children().size(), 2u); + + EXPECT_EQ(std::dynamic_pointer_cast(predicate), nullptr); + for (const auto& child : compound->Children()) { + EXPECT_NE(std::dynamic_pointer_cast(child), nullptr); + } +} + +} // namespace paimon::test + +#if defined(__linux__) && defined(__ELF__) +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace paimon::test { +namespace { + +// Mangled name of `typeinfo for paimon::Predicate`. +constexpr const char* kPredicateTypeInfoSymbol = "_ZTIN6paimon9PredicateE"; + +// Result of looking up a symbol in an ELF dynamic symbol table. +enum class DynSym { + kError, // could not read the file + kAbsent, // symbol not present in .dynsym + kDefined, // symbol has a definition in this module (st_shndx != SHN_UNDEF) + kImported, // symbol is undefined here and resolved from another module +}; + +// On-disk path of the loaded shared object whose path contains `needle`. +std::string FindLoadedModule(const char* needle) { + struct Ctx { + const char* needle; + std::string path; + } ctx{needle, {}}; + dl_iterate_phdr( + [](struct dl_phdr_info* info, size_t, void* data) -> int { + auto* c = static_cast(data); + if (info->dlpi_name != nullptr && std::strstr(info->dlpi_name, c->needle) != nullptr) { + c->path = info->dlpi_name; + return 1; + } + return 0; + }, + &ctx); + return ctx.path; +} + +// Whether `sym_name` is defined in, imported by, or absent from the dynamic +// symbol table of the ELF file at `path`. +// +// We look at whether the symbol is *defined* (has a section index) rather than +// at its binding (GLOBAL vs WEAK): a class with a key function has WEAK typeinfo +// on GCC but GLOBAL typeinfo on Clang, so the binding is not portable. What both +// compilers guarantee is that the key function makes the typeinfo a single +// definition that other modules import. +DynSym LookupDynSym(const std::string& path, const char* sym_name) { + int fd = ::open(path.c_str(), O_RDONLY); + if (fd < 0) { + return DynSym::kError; + } + struct stat st{}; + if (::fstat(fd, &st) != 0) { + ::close(fd); + return DynSym::kError; + } + void* map = ::mmap(nullptr, static_cast(st.st_size), PROT_READ, MAP_PRIVATE, fd, 0); + ::close(fd); + if (map == MAP_FAILED) { + return DynSym::kError; + } + + const auto* base = static_cast(map); + const auto* ehdr = reinterpret_cast(base); + DynSym result = DynSym::kAbsent; + if (std::memcmp(ehdr->e_ident, ELFMAG, SELFMAG) == 0 && ehdr->e_ident[EI_CLASS] == ELFCLASS64) { + const auto* shdrs = reinterpret_cast(base + ehdr->e_shoff); + for (int i = 0; i < ehdr->e_shnum; ++i) { + if (shdrs[i].sh_type != SHT_DYNSYM) { + continue; + } + const auto* syms = reinterpret_cast(base + shdrs[i].sh_offset); + const char* strtab = + reinterpret_cast(base + shdrs[shdrs[i].sh_link].sh_offset); + size_t count = shdrs[i].sh_size / sizeof(Elf64_Sym); + for (size_t s = 0; s < count; ++s) { + if (std::strcmp(strtab + syms[s].st_name, sym_name) == 0) { + result = syms[s].st_shndx == SHN_UNDEF ? DynSym::kImported : DynSym::kDefined; + break; + } + } + break; + } + } + ::munmap(map, static_cast(st.st_size)); + return result; +} + +} // namespace + +// Deterministic guard (compiler-agnostic across GCC and Clang): the out-of-line +// `~Predicate()` key function must give `Predicate`'s typeinfo a single home in +// libpaimon.so, which the format plugins then import instead of each emitting +// their own copy. Without the key function the typeinfo is weak and every +// module that casts a Predicate defines its own duplicate -- exactly the +// condition that breaks cross-DSO dynamic_cast. Reverting the fix flips this +// test from PASS to FAIL. +TEST(PredicateAbiInteTest, PredicateTypeInfoHasSingleHome) { + std::string core = FindLoadedModule("libpaimon.so"); + ASSERT_FALSE(core.empty()) << "could not locate loaded libpaimon.so"; + + // The core library owns the one definition of the typeinfo. + EXPECT_EQ(LookupDynSym(core, kPredicateTypeInfoSymbol), DynSym::kDefined) + << "typeinfo for paimon::Predicate must be defined in " << core; + + // A format plugin that casts predicates must import that definition rather + // than defining its own duplicate. The parquet plugin performs + // dynamic_pointer_cast/ on Predicate, so it + // references the typeinfo; with the key function that reference resolves to + // libpaimon.so (imported), without it the plugin carries its own weak copy. + std::string plugin = FindLoadedModule("libpaimon_parquet_file_format.so"); + ASSERT_FALSE(plugin.empty()) << "could not locate loaded libpaimon_parquet_file_format.so"; + + DynSym in_plugin = LookupDynSym(plugin, kPredicateTypeInfoSymbol); + ASSERT_NE(in_plugin, DynSym::kError) << "could not read " << plugin; + ASSERT_NE(in_plugin, DynSym::kAbsent) + << "typeinfo for paimon::Predicate is not referenced by " << plugin + << "; the parquet plugin is expected to cast Predicate across the DSO boundary"; + EXPECT_EQ(in_plugin, DynSym::kImported) + << "typeinfo for paimon::Predicate is defined inside " << plugin + << " instead of being imported from libpaimon.so; the Predicate RTTI is duplicated across " + "DSOs (the ~Predicate() key function is missing), which breaks cross-DSO dynamic_cast"; +} + +} // namespace paimon::test +#endif // defined(__linux__) && defined(__ELF__)