llvm / llvm/llvm-project

[clang-repl] Weak globals defined in headers are duplicated between the JIT and the process

Open
#211,786 3 comments 0 reactions 0 assignees View on GitHub
clang-repl orcjit
Dominant language
LLVM
Stars
40.5k
Forks
18.7k
PR merge metrics
PR metrics pending

Description

## Summary

When interpreter code includes a header that *defines* a singleton -- a function-local static in an inline function (Meyers), or a C++17 inline static data member -- and a library already loaded in the process defines the same weak symbols, clang-repl materializes its own copy instead of binding the process's. The program ends up with two instances of what it means to be one singleton: state diverges silently across the JIT/process boundary, and teardown destroys the object twice.

A dynamic linker given the same two definitions would have unified them; the in-process JIT does not.

## Reproducer

`singleton.h`:
```cpp
#pragma once
struct Singleton {
static Singleton &get() {
static Singleton instance;
return instance;
}
static inline int inline_member = 0;
int value = 0;
};
```

`libsingleton.cpp`:
```cpp
#include "singleton.h"
extern "C" void *lib_singleton_addr() { return &Singleton::get(); }
extern "C" int lib_singleton_value() { return Singleton::get().value; }
```

```console
$ clang -xc++ -shared -fPIC libsingleton.cpp -o libsingleton.so
$ nm -CD libsingleton.so | grep -i singleton
0000000000004014 V Singleton::inline_member
0000000000001140 W Singleton::get()
0000000000004018 V Singleton::get()::instance
```

`session.txt`:
```cpp
extern "C" int printf(const char *, ...);
extern "C" void *lib_singleton_addr();
extern "C" int lib_singleton_value();
%lib libsingleton.so
#include "singleton.h"
auto *jit_copy = (void *)&Singleton::get();
auto *lib_copy = lib_singleton_addr();
printf("jit copy = %p\nlib copy = %p\nsame instance: %s\n", jit_copy, lib_copy, jit_copy == lib_copy ? "yes" : "NO");
Singleton::get().value = 42;
printf("library sees value = %d (expected 42)\n", lib_singleton_value());
%quit
```

```console
$ LD_LIBRARY_PATH=$PWD clang-repl -Xcc -I$PWD < session.txt
jit copy = 0x7f5e9f6c8008
lib copy = 0x7f5e9f6cf018
same instance: NO
library sees value = 0 (expected 42)
```

Reproduces on current main (82488f0e7119) and on release/21.x. Same behavior for the inline static data member (`&Singleton::inline_member` differs across the boundary).

## Analysis

The weak/linkonce_odr globals (`_ZZN9Singleton3getEvE8instance`, its `_ZGV*` guard, `_ZN9Singleton13inline_memberE`) are *defined* in the interpreter's module, so the JITDylib materializes them. The process-symbol generator (`getProcessSymbolsJITDylib()` / `EPCDynamicLibrarySearchGenerator`) is only consulted for symbols the JITDylib lacks, so the already-exported process definitions never win, for any interpreter input that repeats a header-level definition.

This bites embedders hard: cppyy/CppInterOp-based interpreters that JIT large existing codebases hit duplicated `ConfigCache`/logger-style singletons. compiler-research/CppInterOp#1073 is a downstream IR-level workaround, and the review there concluded this should be fixed and tracked in clang's JIT (cc @vgvassilev).

## Candidate fix

I have a patch that demotes, in `OrcIncrementalExecutor::addModule`, every *mutable* weak global-variable definition whose symbol the executor process already provides to an external declaration, so the JIT links against the process copy:

- probes through the LLJIT `DylibManager` (`loadDylib(nullptr)` + weakly-referenced `lookupSymbols`), so it works for in-process and out-of-process executors alike;
- pairs dynamically initialized statics with their `_ZGV` guards (both move or neither, keeping init-once semantics intact across the boundary);
- leaves constants (ODR-harmless to duplicate, keeps folding), functions, thread_locals, and anything on `llvm.used` untouched;
- ELF-gated for now.

With it, the reproducer prints `same instance: yes` / `library sees value = 42`, and a matching unit test (`InterpreterTest.ProcessWeakGlobalsAreBound`) goes red-to-green with the existing suites unaffected. Happy to open the PR if this direction sounds right to the JIT folks -- or to hear what the preferred fix location is (e.g. ORC-level weak-symbol policy vs. the Interpreter-level demotion).

Contributor guide

Open the contributing guide

Research direction

Reproduce the issue with singleton.h, libsingleton.cpp, and session.txt, then inspect OrcIncrementalExecutor::addModule and the LLJIT DylibManager/process-symbol lookup path. Review InterpreterTest.ProcessWeakGlobalsAreBound and the existing suites. Done means the reproducer shares one singleton and the matching test passes without regressions.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp
Domain
compilers
Issue type
Bug
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Mostly clear
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.