Skip to content

Commit 916eeb4

Browse files
author
Emery Conrad
committed
Emit discardable-ODR variables via odr-use, not UsedAttr, in GetVariableOffset
ForceCodeGen forces deferred emission by planting a permanent __attribute__((used)) on the decl; every global emitted that way gets a WeakTrackingVH in codegen's llvm.used list. When such a global is deleted before emitUsed runs (ORC freeing a materialized PTU's IR), the handle nulls and release-built clang dereferences it unchecked — a process crash that accumulates with interpreter state rather than tracing to any one declaration (large reflection sweeps died; per-type bisection never converged, so the crash itself has no compact unit repro — root-caused via gdb). GetVariableOffset now emits GVA_DiscardableODR variables (the inline/constexpr class the crash traced to) by declaring an external-linkage odr-use of the qualified name instead: the definition flows through the regular deferred-decl path and leaves no used-list residue, no permanent attribute. Everything else keeps the UsedAttr path, deliberately: - Internal-linkage variables cannot be odr-used from a later PTU (the module-local symbol duplicates or goes missing); getting this wrong broke VariableReflection_GetVariableOffset (static int S) and cppyy's Lifeline::count lookup. - Available-externally definitions would not be emitted by a mere reference. - Template-specialization and anonymous/lambda spellings do not reliably round-trip as source (a printed LLONG_MIN non-type argument re-parses as an overflowing literal), and a parse-failing declaration poisons the incremental interpreter. An interpreter-level alternative — Undo(1) on parse failure to drop the failed PTU — was evaluated and rejected: cppyy depends on failed parses leaving side-effect declarations (an explicit-instantiation probe of a declared-but-undefined template is expected to fail and leave the specialization decl behind; test_templates test32). The dummy-name counter is deliberately plain, matching gWrapperSerial: the interpreter API is caller-serialized. ForceCodeGen's function path (GetFunctionAddress) still uses UsedAttr — same theoretical hazard, never observed. The test pins the mechanism: after an offset query on a discardable-ODR static, the decl carries no UsedAttr and later PTU modules carry no llvm.used, with a sweep-shaped regression net over interleaved absorbed parse failures. Co-developed-with-the-help-of: Claude Code (Fable 5, human in the loop)
1 parent 3103be9 commit 916eeb4

2 files changed

Lines changed: 109 additions & 1 deletion

File tree

lib/CppInterOp/CppInterOp.cpp

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -402,6 +402,45 @@ static void ForceCodeGen(Decl* D, compat::Interpreter& I) {
402402
#endif
403403
}
404404

405+
int Declare(compat::Interpreter& I, const char* code, bool silent);
406+
407+
// Force emission of a variable's definition by declaring an odr-use of it,
408+
// instead of ForceCodeGen's UsedAttr route. The UsedAttr route records the
409+
// emitted global in codegen's llvm.used list as a weak handle; the handle
410+
// can go null (global deleted) before a later incremental PTU's emitUsed
411+
// runs, and release-built clang dereferences it without checking — a
412+
// process crash that accumulates with interpreter state rather than
413+
// tracing to any one declaration. The odr-use emits the (discardable)
414+
// definition through the regular deferred-decl path and leaves no
415+
// used-list residue. Returns false when the variable cannot be named from
416+
// a fresh chunk of source (anonymous scopes, or printed names that fail to
417+
// parse) — the caller falls back to ForceCodeGen.
418+
static bool EmitVariableViaOdrUse(compat::Interpreter& I, VarDecl* VD) {
419+
std::string name;
420+
{
421+
llvm::raw_string_ostream OS(name);
422+
VD->printQualifiedName(OS);
423+
}
424+
if (name.find("(anonymous ") != std::string::npos ||
425+
name.find("(unnamed ") != std::string::npos ||
426+
name.find("(lambda ") != std::string::npos)
427+
return false;
428+
// Template-specialization spellings do not reliably round-trip as source
429+
// (e.g. a printed LLONG_MIN non-type argument re-parses as an overflowing
430+
// literal), and a parse-failing declaration poisons the incremental
431+
// interpreter; keep those on the caller's UsedAttr path.
432+
if (name.find('<') != std::string::npos)
433+
return false;
434+
435+
// External linkage on the dummy keeps it — and therefore the referenced
436+
// definition — from being discarded as unused internal state.
437+
static unsigned Counter = 0;
438+
std::string code = "namespace __cppinterop_odr_use { const void* __v" +
439+
std::to_string(Counter++) +
440+
" = (const void*)__builtin_addressof(::" + name + "); }";
441+
return Declare(I, code.c_str(), /*silent=*/true) == 0;
442+
}
443+
405444
#define DEBUG_TYPE "jitcall"
406445
bool JitCall::AreArgumentsValid(void* result, ArgList args, void* self,
407446
size_t nary) const {
@@ -2794,7 +2833,14 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D,
27942833
}
27952834
if (!address) {
27962835
auto Linkage = C.GetGVALinkageForVariable(VD);
2797-
if (isDiscardableGVALinkage(Linkage))
2836+
// Odr-use emission only for discardable-ODR entities (inline/constexpr
2837+
// statics) — the class the used-list crash traced to. Internal-linkage
2838+
// variables cannot be odr-used from a later PTU (module-local symbol:
2839+
// the reference duplicates or misses the entity), and an
2840+
// available-externally definition would not be emitted by a mere
2841+
// reference; both stay on the stock UsedAttr path.
2842+
if (isDiscardableGVALinkage(Linkage) &&
2843+
(Linkage != GVA_DiscardableODR || !EmitVariableViaOdrUse(I, VD)))
27982844
ForceCodeGen(VD, I);
27992845
}
28002846
auto VDAorErr = compat::getSymbolAddress(I, StringRef(mangledName));

unittests/CppInterOp/VariableReflectionTest.cpp

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -340,6 +340,68 @@ TYPED_TEST(CPPINTEROP_TEST_MODE, VariableReflection_GetVariableOffset) {
340340
EXPECT_TRUE(Cpp::GetVariableOffset(var));
341341
}
342342

343+
TYPED_TEST(CPPINTEROP_TEST_MODE,
344+
VariableReflection_GetVariableOffset_NoStaleUsedHandle) {
345+
// Emitting a discardable-ODR variable through the UsedAttr route records
346+
// the global as a weak handle in codegen's llvm.used list. If the global
347+
// is later replaced/erased (weak-def discard when a later PTU re-emits
348+
// the same entity), the next PTU's emitUsed dereferences the nulled
349+
// handle. The offset query must not leave used-list residue.
350+
TestFixture::CreateInterpreter();
351+
Cpp::Declare(R"(
352+
struct UsedHandle {
353+
inline static int probe = 3;
354+
};
355+
)");
356+
Cpp::DeclRef klass = Cpp::GetNamed("UsedHandle");
357+
EXPECT_TRUE(klass);
358+
Cpp::DeclRef var = Cpp::GetNamed("probe", klass);
359+
EXPECT_TRUE(var);
360+
EXPECT_TRUE(Cpp::GetVariableOffset(var));
361+
// ForceCodeGen's UsedAttr is planted permanently on the AST decl; the
362+
// odr-use route must not.
363+
EXPECT_FALSE(Cpp::unwrap<Decl>(var)->hasAttr<clang::UsedAttr>());
364+
// The UsedAttr lives on the AST decl, so every later PTU that re-emits
365+
// the entity re-adds it to that module's llvm.used — the residue the
366+
// stale-handle crash grows from. Neither a module that re-emits the
367+
// variable nor an unrelated one may carry llvm.used.
368+
{
369+
auto PTUOrErr = Interp->Parse("int consume_probe = UsedHandle::probe;");
370+
ASSERT_TRUE(bool(PTUOrErr));
371+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
372+
if (auto Err = Interp->Execute(*PTUOrErr))
373+
llvm::consumeError(std::move(Err));
374+
}
375+
{
376+
auto PTUOrErr = Interp->Parse("int flush_ptu = 0;");
377+
ASSERT_TRUE(bool(PTUOrErr));
378+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
379+
if (auto Err = Interp->Execute(*PTUOrErr))
380+
llvm::consumeError(std::move(Err));
381+
}
382+
EXPECT_TRUE(Cpp::GetNamed("flush_ptu"));
383+
// The crash this guards against was cumulative — many force-emitted
384+
// statics plus JIT materialization cycles, interleaved with absorbed
385+
// parse failures. Mimic that sweep shape as a regression net.
386+
for (int i = 0; i < 8; ++i) {
387+
std::string n = std::to_string(i);
388+
Cpp::Declare(("struct Sweep" + n + " { inline static int v" + n + " = " +
389+
n + "; };")
390+
.c_str());
391+
Cpp::DeclRef k = Cpp::GetNamed(("Sweep" + n).c_str());
392+
ASSERT_TRUE(k);
393+
Cpp::DeclRef v = Cpp::GetNamed(("v" + n).c_str(), k);
394+
ASSERT_TRUE(v);
395+
EXPECT_TRUE(Cpp::GetVariableOffset(v));
396+
Cpp::Declare(("template <> struct Sweep" + n + "<int>;").c_str(),
397+
/*silent=*/true); // expected parse failure, absorbed
398+
Cpp::Declare(
399+
("int use" + n + " = Sweep" + n + "::v" + n + ";").c_str());
400+
}
401+
Cpp::Declare("int sweep_done = 1;");
402+
EXPECT_TRUE(Cpp::GetNamed("sweep_done"));
403+
}
404+
343405
#define CODE \
344406
class BaseA { \
345407
public: \

0 commit comments

Comments
 (0)