Skip to content

Commit e5ba17e

Browse files
authored
fix(abi): dynamic_cast error on Predicate across shared libraries (alibaba#450)
1 parent aa9fbf5 commit e5ba17e

5 files changed

Lines changed: 263 additions & 1 deletion

File tree

include/paimon/predicate/predicate.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ class Function;
3131
/// @see PredicateBuilder
3232
class PAIMON_EXPORT Predicate {
3333
public:
34-
virtual ~Predicate() = default;
34+
virtual ~Predicate();
3535
virtual bool operator==(const Predicate& other) const = 0;
3636

3737
virtual const Function& GetFunction() const = 0;

src/paimon/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ set(PAIMON_COMMON_SRCS
121121
common/predicate/not_equal.cpp
122122
common/predicate/not_in.cpp
123123
common/predicate/or.cpp
124+
common/predicate/predicate.cpp
124125
common/predicate/predicate_builder.cpp
125126
common/predicate/predicate_utils.cpp
126127
common/predicate/starts_with.cpp
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
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+
#include "paimon/predicate/predicate.h"
18+
19+
namespace paimon {
20+
/// key function technique for Predicate
21+
Predicate::~Predicate() = default;
22+
} // namespace paimon

test/inte/CMakeLists.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,4 +111,11 @@ if(PAIMON_BUILD_TESTS)
111111
test_utils_static
112112
${GTEST_LINK_TOOLCHAIN})
113113

114+
add_paimon_test(predicate_abi_inte_test
115+
STATIC_LINK_LIBS
116+
paimon_shared
117+
${TEST_STATIC_LINK_LIBS}
118+
test_utils_static
119+
${GTEST_LINK_TOOLCHAIN})
120+
114121
endif()
Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,232 @@
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

Comments
 (0)