Skip to content

Commit 8aac73a

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 321657d commit 8aac73a

2 files changed

Lines changed: 159 additions & 3 deletions

File tree

lib/CppInterOp/CppInterOp.cpp

Lines changed: 48 additions & 2 deletions
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+
static 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 {
@@ -2806,7 +2845,14 @@ intptr_t GetVariableOffset(compat::Interpreter& I, Decl* D,
28062845
}
28072846
if (!address) {
28082847
auto Linkage = C.GetGVALinkageForVariable(VD);
2809-
if (isDiscardableGVALinkage(Linkage))
2848+
// Odr-use emission only for discardable-ODR entities (inline/constexpr
2849+
// statics) — the class the used-list crash traced to. Internal-linkage
2850+
// variables cannot be odr-used from a later PTU (module-local symbol:
2851+
// the reference duplicates or misses the entity), and an
2852+
// available-externally definition would not be emitted by a mere
2853+
// reference; both stay on the stock UsedAttr path.
2854+
if (isDiscardableGVALinkage(Linkage) &&
2855+
(Linkage != GVA_DiscardableODR || !EmitVariableViaOdrUse(I, VD)))
28102856
ForceCodeGen(VD, I);
28112857
}
28122858
auto VDAorErr = compat::getSymbolAddress(I, StringRef(mangledName));
@@ -4989,7 +5035,7 @@ class clangSilent {
49895035
};
49905036
} // namespace
49915037

4992-
int Declare(compat::Interpreter& I, const char* code, bool silent) {
5038+
static int Declare(compat::Interpreter& I, const char* code, bool silent) {
49935039
// Trap diagnostics on both paths: I.declare's rc is 0 even when
49945040
// Parse recovered from emitted errors, so callers need the trap to
49955041
// distinguish "parsed cleanly" from "parsed with errors".

unittests/CppInterOp/VariableReflectionTest.cpp

Lines changed: 111 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
#include "Utils.h"
22

3+
#include "../../lib/CppInterOp/Unwrap.h"
4+
35
#include "CppInterOp/CppInterOp.h"
46

57
#include "clang/AST/ASTContext.h"
8+
#include "clang/AST/Attr.h"
69
#include "clang/Basic/Version.h"
710
#include "clang/Frontend/CompilerInstance.h"
811
#include "clang/Sema/Sema.h"
912

10-
#include "gtest/gtest.h"
13+
#include "llvm/Support/Error.h"
14+
1115
#include <string>
16+
#include <utility>
17+
#include "gtest/gtest.h"
1218

1319
#include <cstddef>
1420

@@ -376,6 +382,110 @@ TYPED_TEST(CPPINTEROP_TEST_MODE,
376382
EXPECT_FALSE(Cpp::GetVariableOffset(var2));
377383
}
378384

385+
TYPED_TEST(CPPINTEROP_TEST_MODE,
386+
VariableReflection_GetVariableOffset_NoStaleUsedHandle) {
387+
// Emitting a discardable-ODR variable through the UsedAttr route records
388+
// the global as a weak handle in codegen's llvm.used list. If the global
389+
// is later replaced/erased (weak-def discard when a later PTU re-emits
390+
// the same entity), the next PTU's emitUsed dereferences the nulled
391+
// handle. The offset query must not leave used-list residue.
392+
TestFixture::CreateInterpreter();
393+
Cpp::Declare(R"(
394+
struct UsedHandle {
395+
inline static int probe = 3;
396+
};
397+
)");
398+
Cpp::DeclRef klass = Cpp::GetNamed("UsedHandle");
399+
EXPECT_TRUE(klass);
400+
Cpp::DeclRef var = Cpp::GetNamed("probe", klass);
401+
EXPECT_TRUE(var);
402+
EXPECT_TRUE(Cpp::GetVariableOffset(var));
403+
// ForceCodeGen's UsedAttr is planted permanently on the AST decl; the
404+
// odr-use route must not.
405+
EXPECT_FALSE(Cpp::unwrap<Decl>(var)->hasAttr<clang::UsedAttr>());
406+
#ifndef CPPINTEROP_USE_CLING
407+
// The UsedAttr lives on the AST decl, so every later PTU that re-emits
408+
// the entity re-adds it to that module's llvm.used — the residue the
409+
// stale-handle crash grows from. Neither a module that re-emits the
410+
// variable nor an unrelated one may carry llvm.used. (PTU/TheModule is
411+
// clang::Interpreter surface; cling covers this path via the UsedAttr
412+
// assert above.)
413+
{
414+
auto PTUOrErr = Interp->Parse("int consume_probe = UsedHandle::probe;");
415+
ASSERT_TRUE(bool(PTUOrErr));
416+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
417+
if (auto Err = Interp->Execute(*PTUOrErr))
418+
llvm::consumeError(std::move(Err));
419+
}
420+
{
421+
auto PTUOrErr = Interp->Parse("int flush_ptu = 0;");
422+
ASSERT_TRUE(bool(PTUOrErr));
423+
EXPECT_EQ(PTUOrErr->TheModule->getNamedGlobal("llvm.used"), nullptr);
424+
if (auto Err = Interp->Execute(*PTUOrErr))
425+
llvm::consumeError(std::move(Err));
426+
}
427+
EXPECT_TRUE(Cpp::GetNamed("flush_ptu"));
428+
#endif // !CPPINTEROP_USE_CLING
429+
// The crash this guards against was cumulative — many force-emitted
430+
// statics plus JIT materialization cycles, interleaved with absorbed
431+
// parse failures. Mimic that sweep shape as a regression net.
432+
for (int i = 0; i < 8; ++i) {
433+
std::string n = std::to_string(i);
434+
std::string decl = "struct Sweep";
435+
decl += n;
436+
decl += " { inline static int v";
437+
decl += n;
438+
decl += " = ";
439+
decl += n;
440+
decl += "; };";
441+
Cpp::Declare(decl.c_str());
442+
Cpp::DeclRef k = Cpp::GetNamed("Sweep" + n);
443+
ASSERT_TRUE(k);
444+
Cpp::DeclRef v = Cpp::GetNamed("v" + n, k);
445+
ASSERT_TRUE(v);
446+
EXPECT_TRUE(Cpp::GetVariableOffset(v));
447+
std::string probe = "template <> struct Sweep";
448+
probe += n;
449+
probe += "<int>;";
450+
Cpp::Declare(probe.c_str(), /*silent=*/true); // expected parse failure
451+
std::string use = "int use";
452+
use += n;
453+
use += " = Sweep";
454+
use += n;
455+
use += "::v";
456+
use += n;
457+
use += ";";
458+
Cpp::Declare(use.c_str());
459+
}
460+
Cpp::Declare("int sweep_done = 1;");
461+
EXPECT_TRUE(Cpp::GetNamed("sweep_done"));
462+
463+
// Template-specialization spellings bail out of the odr-use route
464+
// (printed non-type arguments do not reliably round-trip as source) and
465+
// keep the UsedAttr fallback.
466+
Cpp::Declare(R"(
467+
template <typename T> struct TmplStatic {
468+
inline static int member = 7;
469+
};
470+
)");
471+
Cpp::DeclRef tmpl = Cpp::GetNamed("TmplStatic");
472+
EXPECT_TRUE(tmpl);
473+
ASTContext& C = Interp->getCI()->getASTContext();
474+
std::vector<Cpp::TemplateArgInfo> template_args = {
475+
{C.IntTy.getAsOpaquePtr()}};
476+
Cpp::DeclRef inst = Cpp::InstantiateTemplate(tmpl, template_args);
477+
EXPECT_TRUE(inst);
478+
Cpp::DeclRef member = Cpp::GetNamed("member", inst);
479+
EXPECT_TRUE(member);
480+
EXPECT_TRUE(Cpp::GetVariableOffset(member));
481+
#ifndef CPPINTEROP_USE_CLING
482+
// cling's getAddressOfGlobal can emit on demand, resolving the address
483+
// before the forced-emission gate is reached; only clang-repl reliably
484+
// takes the UsedAttr fallback here.
485+
EXPECT_TRUE(Cpp::unwrap<Decl>(member)->hasAttr<clang::UsedAttr>());
486+
#endif
487+
}
488+
379489
#define CODE \
380490
class BaseA { \
381491
public: \

0 commit comments

Comments
 (0)