|
| 1 | +/* |
| 2 | + * Copyright 2026-present Alibaba Inc. |
| 3 | + * |
| 4 | + * Licensed under the Apache License, Version 2.0 (the "License"); |
| 5 | + * you may not use this file except in compliance with the License. |
| 6 | + * You may obtain a copy of the License at |
| 7 | + * |
| 8 | + * http://www.apache.org/licenses/LICENSE-2.0 |
| 9 | + * |
| 10 | + * Unless required by applicable law or agreed to in writing, software |
| 11 | + * distributed under the License is distributed on an "AS IS" BASIS, |
| 12 | + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 13 | + * See the License for the specific language governing permissions and |
| 14 | + * limitations under the License. |
| 15 | + */ |
| 16 | + |
| 17 | +// Regression test for the Predicate cross-DSO RTTI/ABI fix (the out-of-line |
| 18 | +// `Predicate::~Predicate` key function in common/predicate/predicate.cpp). |
| 19 | +// |
| 20 | +// `Predicate` is an abstract base with only pure virtuals. Without an |
| 21 | +// out-of-line virtual ("key function"), the compiler has no single home for its |
| 22 | +// vtable and typeinfo, so it emits them *weakly* in every translation unit that |
| 23 | +// uses them. Combined with `-fvisibility=hidden`, separately linked modules |
| 24 | +// (e.g. libpaimon.so vs libpaimon_parquet_file_format.so, or plugins loaded via |
| 25 | +// dlopen) can each end up with their own `Predicate` typeinfo. A cross-module |
| 26 | +// `dynamic_cast`/`dynamic_pointer_cast` then compares mismatched typeinfo and |
| 27 | +// fails. Giving `Predicate` an out-of-line destructor anchors a single, |
| 28 | +// exported definition of its RTTI that all other modules import. |
| 29 | +// |
| 30 | +// This file guards the fix on two levels: |
| 31 | +// 1. Behavior: `Predicate` objects are created inside libpaimon.so and cast to |
| 32 | +// derived types here in the test module. This catches the failure on |
| 33 | +// toolchains that compare std::type_info by pointer (e.g. libc++) and in |
| 34 | +// downstream builds that link plugins aggressively. |
| 35 | +// 2. Symbol layout (Linux/ELF only): assert that `Predicate`'s typeinfo has a |
| 36 | +// single home in libpaimon.so which the format plugins import, rather than |
| 37 | +// each plugin emitting its own duplicate copy. This is the deterministic |
| 38 | +// signature of the key function on glibc/libstdc++ toolchains, which merge |
| 39 | +// weak typeinfo by name and therefore would not fail the behavioral cast |
| 40 | +// even without the fix. |
| 41 | + |
| 42 | +#include <memory> |
| 43 | + |
| 44 | +#include "gtest/gtest.h" |
| 45 | +#include "paimon/defs.h" |
| 46 | +#include "paimon/predicate/compound_predicate.h" |
| 47 | +#include "paimon/predicate/leaf_predicate.h" |
| 48 | +#include "paimon/predicate/literal.h" |
| 49 | +#include "paimon/predicate/predicate.h" |
| 50 | +#include "paimon/predicate/predicate_builder.h" |
| 51 | +#include "paimon/testing/utils/testharness.h" |
| 52 | + |
| 53 | +namespace paimon::test { |
| 54 | + |
| 55 | +// A leaf predicate built in libpaimon.so must survive dynamic_pointer_cast to |
| 56 | +// LeafPredicate across the module boundary. |
| 57 | +TEST(PredicateAbiInteTest, LeafPredicateCastsAcrossModule) { |
| 58 | + std::shared_ptr<Predicate> predicate = |
| 59 | + PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, |
| 60 | + Literal(static_cast<int64_t>(5))); |
| 61 | + ASSERT_NE(predicate, nullptr); |
| 62 | + |
| 63 | + std::shared_ptr<LeafPredicate> leaf = std::dynamic_pointer_cast<LeafPredicate>(predicate); |
| 64 | + ASSERT_NE(leaf, nullptr) << "dynamic_pointer_cast<LeafPredicate> failed across the module " |
| 65 | + "boundary; Predicate RTTI is likely duplicated (missing key " |
| 66 | + "function)"; |
| 67 | + EXPECT_EQ(leaf->FieldName(), "f0"); |
| 68 | + EXPECT_EQ(leaf->FieldIndex(), 0); |
| 69 | + |
| 70 | + // A leaf predicate must not be mistaken for a compound predicate. |
| 71 | + EXPECT_EQ(std::dynamic_pointer_cast<CompoundPredicate>(predicate), nullptr); |
| 72 | +} |
| 73 | + |
| 74 | +// A compound predicate built in libpaimon.so must survive dynamic_pointer_cast |
| 75 | +// to CompoundPredicate, and its children must remain castable to LeafPredicate. |
| 76 | +TEST(PredicateAbiInteTest, CompoundPredicateCastsAcrossModule) { |
| 77 | + std::shared_ptr<Predicate> left = |
| 78 | + PredicateBuilder::Equal(/*field_index=*/0, /*field_name=*/"f0", FieldType::BIGINT, |
| 79 | + Literal(static_cast<int64_t>(5))); |
| 80 | + std::shared_ptr<Predicate> right = PredicateBuilder::Equal( |
| 81 | + /*field_index=*/1, /*field_name=*/"f1", FieldType::INT, Literal(10)); |
| 82 | + |
| 83 | + ASSERT_OK_AND_ASSIGN(std::shared_ptr<Predicate> predicate, |
| 84 | + PredicateBuilder::And({left, right})); |
| 85 | + |
| 86 | + std::shared_ptr<CompoundPredicate> compound = |
| 87 | + std::dynamic_pointer_cast<CompoundPredicate>(predicate); |
| 88 | + ASSERT_NE(compound, nullptr) |
| 89 | + << "dynamic_pointer_cast<CompoundPredicate> failed across the module boundary"; |
| 90 | + ASSERT_EQ(compound->Children().size(), 2u); |
| 91 | + |
| 92 | + EXPECT_EQ(std::dynamic_pointer_cast<LeafPredicate>(predicate), nullptr); |
| 93 | + for (const auto& child : compound->Children()) { |
| 94 | + EXPECT_NE(std::dynamic_pointer_cast<LeafPredicate>(child), nullptr); |
| 95 | + } |
| 96 | +} |
| 97 | + |
| 98 | +} // namespace paimon::test |
| 99 | + |
| 100 | +#if defined(__linux__) && defined(__ELF__) |
| 101 | +#include <elf.h> |
| 102 | +#include <fcntl.h> |
| 103 | +#include <link.h> |
| 104 | +#include <sys/mman.h> |
| 105 | +#include <sys/stat.h> |
| 106 | +#include <unistd.h> |
| 107 | + |
| 108 | +#include <cstdint> |
| 109 | +#include <cstring> |
| 110 | +#include <string> |
| 111 | + |
| 112 | +namespace paimon::test { |
| 113 | +namespace { |
| 114 | + |
| 115 | +// Mangled name of `typeinfo for paimon::Predicate`. |
| 116 | +constexpr const char* kPredicateTypeInfoSymbol = "_ZTIN6paimon9PredicateE"; |
| 117 | + |
| 118 | +// Result of looking up a symbol in an ELF dynamic symbol table. |
| 119 | +enum class DynSym { |
| 120 | + kError, // could not read the file |
| 121 | + kAbsent, // symbol not present in .dynsym |
| 122 | + kDefined, // symbol has a definition in this module (st_shndx != SHN_UNDEF) |
| 123 | + kImported, // symbol is undefined here and resolved from another module |
| 124 | +}; |
| 125 | + |
| 126 | +// On-disk path of the loaded shared object whose path contains `needle`. |
| 127 | +std::string FindLoadedModule(const char* needle) { |
| 128 | + struct Ctx { |
| 129 | + const char* needle; |
| 130 | + std::string path; |
| 131 | + } ctx{needle, {}}; |
| 132 | + dl_iterate_phdr( |
| 133 | + [](struct dl_phdr_info* info, size_t, void* data) -> int { |
| 134 | + auto* c = static_cast<Ctx*>(data); |
| 135 | + if (info->dlpi_name != nullptr && std::strstr(info->dlpi_name, c->needle) != nullptr) { |
| 136 | + c->path = info->dlpi_name; |
| 137 | + return 1; |
| 138 | + } |
| 139 | + return 0; |
| 140 | + }, |
| 141 | + &ctx); |
| 142 | + return ctx.path; |
| 143 | +} |
| 144 | + |
| 145 | +// Whether `sym_name` is defined in, imported by, or absent from the dynamic |
| 146 | +// symbol table of the ELF file at `path`. |
| 147 | +// |
| 148 | +// We look at whether the symbol is *defined* (has a section index) rather than |
| 149 | +// at its binding (GLOBAL vs WEAK): a class with a key function has WEAK typeinfo |
| 150 | +// on GCC but GLOBAL typeinfo on Clang, so the binding is not portable. What both |
| 151 | +// compilers guarantee is that the key function makes the typeinfo a single |
| 152 | +// definition that other modules import. |
| 153 | +DynSym LookupDynSym(const std::string& path, const char* sym_name) { |
| 154 | + int fd = ::open(path.c_str(), O_RDONLY); |
| 155 | + if (fd < 0) { |
| 156 | + return DynSym::kError; |
| 157 | + } |
| 158 | + struct stat st{}; |
| 159 | + if (::fstat(fd, &st) != 0) { |
| 160 | + ::close(fd); |
| 161 | + return DynSym::kError; |
| 162 | + } |
| 163 | + void* map = ::mmap(nullptr, static_cast<size_t>(st.st_size), PROT_READ, MAP_PRIVATE, fd, 0); |
| 164 | + ::close(fd); |
| 165 | + if (map == MAP_FAILED) { |
| 166 | + return DynSym::kError; |
| 167 | + } |
| 168 | + |
| 169 | + const auto* base = static_cast<const uint8_t*>(map); |
| 170 | + const auto* ehdr = reinterpret_cast<const Elf64_Ehdr*>(base); |
| 171 | + DynSym result = DynSym::kAbsent; |
| 172 | + if (std::memcmp(ehdr->e_ident, ELFMAG, SELFMAG) == 0 && ehdr->e_ident[EI_CLASS] == ELFCLASS64) { |
| 173 | + const auto* shdrs = reinterpret_cast<const Elf64_Shdr*>(base + ehdr->e_shoff); |
| 174 | + for (int i = 0; i < ehdr->e_shnum; ++i) { |
| 175 | + if (shdrs[i].sh_type != SHT_DYNSYM) { |
| 176 | + continue; |
| 177 | + } |
| 178 | + const auto* syms = reinterpret_cast<const Elf64_Sym*>(base + shdrs[i].sh_offset); |
| 179 | + const char* strtab = |
| 180 | + reinterpret_cast<const char*>(base + shdrs[shdrs[i].sh_link].sh_offset); |
| 181 | + size_t count = shdrs[i].sh_size / sizeof(Elf64_Sym); |
| 182 | + for (size_t s = 0; s < count; ++s) { |
| 183 | + if (std::strcmp(strtab + syms[s].st_name, sym_name) == 0) { |
| 184 | + result = syms[s].st_shndx == SHN_UNDEF ? DynSym::kImported : DynSym::kDefined; |
| 185 | + break; |
| 186 | + } |
| 187 | + } |
| 188 | + break; |
| 189 | + } |
| 190 | + } |
| 191 | + ::munmap(map, static_cast<size_t>(st.st_size)); |
| 192 | + return result; |
| 193 | +} |
| 194 | + |
| 195 | +} // namespace |
| 196 | + |
| 197 | +// Deterministic guard (compiler-agnostic across GCC and Clang): the out-of-line |
| 198 | +// `~Predicate()` key function must give `Predicate`'s typeinfo a single home in |
| 199 | +// libpaimon.so, which the format plugins then import instead of each emitting |
| 200 | +// their own copy. Without the key function the typeinfo is weak and every |
| 201 | +// module that casts a Predicate defines its own duplicate -- exactly the |
| 202 | +// condition that breaks cross-DSO dynamic_cast. Reverting the fix flips this |
| 203 | +// test from PASS to FAIL. |
| 204 | +TEST(PredicateAbiInteTest, PredicateTypeInfoHasSingleHome) { |
| 205 | + std::string core = FindLoadedModule("libpaimon.so"); |
| 206 | + ASSERT_FALSE(core.empty()) << "could not locate loaded libpaimon.so"; |
| 207 | + |
| 208 | + // The core library owns the one definition of the typeinfo. |
| 209 | + EXPECT_EQ(LookupDynSym(core, kPredicateTypeInfoSymbol), DynSym::kDefined) |
| 210 | + << "typeinfo for paimon::Predicate must be defined in " << core; |
| 211 | + |
| 212 | + // A format plugin that casts predicates must import that definition rather |
| 213 | + // than defining its own duplicate. The parquet plugin performs |
| 214 | + // dynamic_pointer_cast<LeafPredicate>/<CompoundPredicate> on Predicate, so it |
| 215 | + // references the typeinfo; with the key function that reference resolves to |
| 216 | + // libpaimon.so (imported), without it the plugin carries its own weak copy. |
| 217 | + std::string plugin = FindLoadedModule("libpaimon_parquet_file_format.so"); |
| 218 | + ASSERT_FALSE(plugin.empty()) << "could not locate loaded libpaimon_parquet_file_format.so"; |
| 219 | + |
| 220 | + DynSym in_plugin = LookupDynSym(plugin, kPredicateTypeInfoSymbol); |
| 221 | + ASSERT_NE(in_plugin, DynSym::kError) << "could not read " << plugin; |
| 222 | + ASSERT_NE(in_plugin, DynSym::kAbsent) |
| 223 | + << "typeinfo for paimon::Predicate is not referenced by " << plugin |
| 224 | + << "; the parquet plugin is expected to cast Predicate across the DSO boundary"; |
| 225 | + EXPECT_EQ(in_plugin, DynSym::kImported) |
| 226 | + << "typeinfo for paimon::Predicate is defined inside " << plugin |
| 227 | + << " instead of being imported from libpaimon.so; the Predicate RTTI is duplicated across " |
| 228 | + "DSOs (the ~Predicate() key function is missing), which breaks cross-DSO dynamic_cast"; |
| 229 | +} |
| 230 | + |
| 231 | +} // namespace paimon::test |
| 232 | +#endif // defined(__linux__) && defined(__ELF__) |
0 commit comments