From af4fa6ecdac7b925cf92b8f55391f74ae3786107 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Sun, 4 Oct 2026 10:28:46 +0000 Subject: [PATCH 1/6] Test that libCling keeps LLVM private libCling links LLVM and Clang statically and hides them with -Bsymbolic and libCling.script, so that another LLVM in the process, such as Triton's (PyTorch's compiler), neither binds to libCling's nor has libCling bind to its own. Nothing tests this. When the hiding was incomplete, ALICE saw sporadic crashes with a second libLLVM.so in the process (#19889), and linking libCling against a shared libLLVM, which cannot be hidden, would bring them back unnoticed. Run root.exe and rootcling with a preloaded canary that defines LLVM functions libCling calls, and fail if one of them is reached; a self-test checks that the canary catches a library that relies on the LLVM of its host. With glibc, also fail if the dynamic linker's own log, LD_DEBUG=bindings, shows a binding of an LLVM or Clang symbol across libCling's boundary. Check that the canary's names exist in the LLVM ROOT was built with, so that the test cannot go stale. The tests are ELF only, and skipped for AddressSanitizer builds, which control LD_PRELOAD themselves. Related: the 'Option registered more than once' error with an external LLVM (#12156), and the discussions in #20903 and #23546 about rootcling's llvm::cl options and linking against a shared libLLVM. --- core/metacling/test/CMakeLists.txt | 65 ++++++++++++++++ core/metacling/test/CheckLLVMBindings.cmake | 68 +++++++++++++++++ core/metacling/test/LLVMIsolation.C | 74 +++++++++++++++++++ core/metacling/test/LLVMIsolation.h | 3 + core/metacling/test/LLVMIsolationCanary.cxx | 35 +++++++++ core/metacling/test/LLVMIsolationCanary.h | 23 ++++++ core/metacling/test/LLVMIsolationLinkDef.h | 3 + .../test/LLVMIsolationTestLibrary.cxx | 21 ++++++ 8 files changed, 292 insertions(+) create mode 100644 core/metacling/test/CheckLLVMBindings.cmake create mode 100644 core/metacling/test/LLVMIsolation.C create mode 100644 core/metacling/test/LLVMIsolation.h create mode 100644 core/metacling/test/LLVMIsolationCanary.cxx create mode 100644 core/metacling/test/LLVMIsolationCanary.h create mode 100644 core/metacling/test/LLVMIsolationLinkDef.h create mode 100644 core/metacling/test/LLVMIsolationTestLibrary.cxx diff --git a/core/metacling/test/CMakeLists.txt b/core/metacling/test/CMakeLists.txt index eaec1bfd31863..92eb9f7bce46b 100644 --- a/core/metacling/test/CMakeLists.txt +++ b/core/metacling/test/CMakeLists.txt @@ -35,3 +35,68 @@ ROOT_ADD_GTEST(TClingLoadUnloadFile TClingLoadUnloadFileTests.cxx LIBRARIES Core if(pyroot) ROOT_ADD_PYUNITTEST(MetaClingTests MetaClingTests.py) endif() + +# libCling must keep its LLVM private: another LLVM in the process, for example +# from a graphics driver or a Python package, must neither bind to it nor have +# libCling bind to its own (https://github.com/root-project/root/issues/19889). +set(macro ${CMAKE_CURRENT_SOURCE_DIR}/LLVMIsolation.C) +if(CMAKE_EXECUTABLE_FORMAT STREQUAL "ELF" AND NOT asan) + + # The canary's symbols must exist in the LLVM and Clang ROOT was built with. + set(llvm_archives) + foreach(lib LLVMSupport LLVMCore clangAST) + if(TARGET ${lib}) + list(APPEND llvm_archives $) + elseif(EXISTS ${LLVM_LIBRARY_DIR}/lib${lib}.a) + list(APPEND llvm_archives ${LLVM_LIBRARY_DIR}/lib${lib}.a) + endif() + endforeach() + list(LENGTH llvm_archives n_llvm_archives) + if(n_llvm_archives EQUAL 3) + string(REPLACE ";" ":" llvm_archives "${llvm_archives}") + ROOT_ADD_TEST(test-metacling-llvm-isolation-canary-names + COMMAND root.exe -l -b -q "${macro}(\"names\")" + ENVIRONMENT LLVM_ISOLATION_ARCHIVES=${llvm_archives} + PASSREGEX "LLVM isolation names: missing=0 of") + endif() + + add_library(LLVMIsolationCanary SHARED LLVMIsolationCanary.cxx) + set(canary $) + set(dictionary -f ${CMAKE_CURRENT_BINARY_DIR}/G__LLVMIsolation.cxx + ${CMAKE_CURRENT_SOURCE_DIR}/LLVMIsolation.h ${CMAKE_CURRENT_SOURCE_DIR}/LLVMIsolationLinkDef.h) + + # The canary comes first in the global scope, before libCling is loaded. + ROOT_ADD_TEST(test-metacling-llvm-isolation-jit-canary + COMMAND root.exe -l -b -q ${macro} + ENVIRONMENT LD_PRELOAD=${canary} + PASSREGEX "LLVM isolation: JIT OK" FAILREGEX "LLVM isolation canary") + ROOT_ADD_TEST(test-metacling-llvm-isolation-rootcling-canary + COMMAND rootcling ${dictionary} + ENVIRONMENT LD_PRELOAD=${canary} + FAILREGEX "LLVM isolation canary") + # The canary must catch a library that relies on the LLVM of its host. + add_library(LLVMIsolationTestCALLER SHARED LLVMIsolationTestLibrary.cxx) + target_compile_definitions(LLVMIsolationTestCALLER PRIVATE CALLER) + ROOT_ADD_TEST(test-metacling-llvm-isolation-canary-selftest + COMMAND root.exe -l -b -q ${macro} + ENVIRONMENT LD_PRELOAD=${canary} LLVM_ISOLATION_LOAD=$ + PASSREGEX "LLVM isolation canary: _ZN4llvm4errsEv resolved outside of libCling") + + # Every binding performed by the dynamic linker, including those of the + # canary's references, once it is preloaded or loaded into the global scope + # by the macro. LD_DEBUG=bindings is specific to glibc. + include(CheckSymbolExists) + check_symbol_exists(__GLIBC__ features.h ROOT_HAVE_GLIBC) + if(ROOT_HAVE_GLIBC) + ROOT_ADD_TEST(test-metacling-llvm-isolation-jit-bindings + COMMAND ${CMAKE_COMMAND} -DOUT=${CMAKE_CURRENT_BINARY_DIR}/llvm-isolation-jit -DEXPECT=JIT.OK + -P ${CMAKE_CURRENT_SOURCE_DIR}/CheckLLVMBindings.cmake + -- $ -l -b -q ${macro} + ENVIRONMENT LLVM_ISOLATION_LOAD=${canary}) + ROOT_ADD_TEST(test-metacling-llvm-isolation-rootcling-bindings + COMMAND ${CMAKE_COMMAND} -DOUT=${CMAKE_CURRENT_BINARY_DIR}/llvm-isolation-rootcling + -P ${CMAKE_CURRENT_SOURCE_DIR}/CheckLLVMBindings.cmake + -- $ ${dictionary} + ENVIRONMENT LD_PRELOAD=${canary}) + endif() +endif() diff --git a/core/metacling/test/CheckLLVMBindings.cmake b/core/metacling/test/CheckLLVMBindings.cmake new file mode 100644 index 0000000000000..674f19ce97c3b --- /dev/null +++ b/core/metacling/test/CheckLLVMBindings.cmake @@ -0,0 +1,68 @@ +# Copyright (C) 1995-2026, Rene Brun and Fons Rademakers. +# All rights reserved. +# +# For the licensing terms see $ROOTSYS/LICENSE. +# For the list of contributors see $ROOTSYS/README/CREDITS. + +# Script mode: cmake -DOUT= [-DEXPECT=] -P CheckLLVMBindings.cmake -- [args...] +# +# Runs CMD with glibc's LD_DEBUG=bindings, which logs every symbol binding the +# dynamic linker performs, and fails if a symbol involving LLVM or Clang types +# (apart from the cling API) is bound across libCling's boundary: from libCling +# to another object, or from another object to libCling. + +set(CMD) +set(in_command FALSE) +math(EXPR last "${CMAKE_ARGC} - 1") +foreach(i RANGE ${last}) + if(in_command) + list(APPEND CMD "${CMAKE_ARGV${i}}") + elseif(CMAKE_ARGV${i} STREQUAL "--") + set(in_command TRUE) + endif() +endforeach() + +file(REMOVE_RECURSE "${OUT}") +file(MAKE_DIRECTORY "${OUT}") +execute_process(COMMAND ${CMAKE_COMMAND} -E env LD_DEBUG=bindings LD_DEBUG_OUTPUT=${OUT}/bindings ${CMD} + RESULT_VARIABLE result OUTPUT_VARIABLE output ERROR_VARIABLE output) +message("${output}") +if(NOT result EQUAL 0) + message(FATAL_ERROR "Command failed with ${result}: ${CMD}") +endif() +if(EXPECT AND NOT output MATCHES "${EXPECT}") + message(FATAL_ERROR "Output does not match '${EXPECT}'") +endif() + +file(GLOB logs "${OUT}/bindings.*") +set(n_bindings 0) +set(crossing) +foreach(log IN LISTS logs) + file(STRINGS "${log}" lines REGEX "binding file ") + list(LENGTH lines n) + math(EXPR n_bindings "${n_bindings} + ${n}") + foreach(line IN LISTS lines) + if(NOT line MATCHES "symbol `_Z[^']*(4llvm|5clang)" OR line MATCHES "symbol `_Z[A-Z]*N?K?5cling") + continue() + endif() + if(line MATCHES "binding file ([^ ]+) [^ ]+ to ([^ ]+) ") + string(FIND "${CMAKE_MATCH_1}" "libCling" from_cling) + string(FIND "${CMAKE_MATCH_2}" "libCling" to_cling) + if((from_cling EQUAL -1) AND NOT (to_cling EQUAL -1) OR NOT (from_cling EQUAL -1) AND (to_cling EQUAL -1)) + list(APPEND crossing "${line}") + endif() + endif() + endforeach() +endforeach() + +# Without any bindings logged, LD_DEBUG did not work and the check would be meaningless. +if(n_bindings EQUAL 0) + message(FATAL_ERROR "No symbol bindings were logged; LD_DEBUG=bindings is not supported here") +endif() +if(crossing) + list(LENGTH crossing n_crossing) + list(SUBLIST crossing 0 5 examples) + string(REPLACE ";" "\n" examples "${examples}") + message(FATAL_ERROR "${n_crossing} LLVM/Clang symbol bindings cross libCling's boundary, e.g.:\n${examples}") +endif() +message("${n_bindings} symbol bindings logged, none involving LLVM or Clang crosses libCling's boundary") diff --git a/core/metacling/test/LLVMIsolation.C b/core/metacling/test/LLVMIsolation.C new file mode 100644 index 0000000000000..e1e0ee35c8cd3 --- /dev/null +++ b/core/metacling/test/LLVMIsolation.C @@ -0,0 +1,74 @@ +/// \file +/// Scenarios of the LLVM isolation tests, with the libraries named by the environment +/// variables LLVM_ISOLATION_*: +/// "jit": interpreter and JIT work after loading LOAD, if set, into the global +/// scope (as gSystem->Load does). +/// "names": the static LLVM libraries in ARCHIVES (separated by ':') define every +/// symbol in LLVMIsolationCanary.h, so that these cannot silently become +/// stale. + +#include "LLVMIsolationCanary.h" + +#include +#include +#include +#include + +/// Add the names in the symbol table of a GNU ar archive: its first member, named "/" +/// or "/SYM64/", with the big-endian number of symbols, their offsets and their names. +bool LLVMIsolationArchiveSymbols(const std::string &path, std::set &symbols) +{ + std::ifstream file(path, std::ios::binary); + char header[8 + 60] = {}; + if (!file.read(header, sizeof(header)) || + (std::string(header, 8) != "!\n" && std::string(header, 8) != "!\n")) + return false; + size_t width = std::strncmp(header + 8, "/ ", 2) == 0 ? 4 : std::strncmp(header + 8, "/SYM64/ ", 8) == 0 ? 8 : 0; + std::string table(std::strtoull(header + 8 + 48, nullptr, 10), '\0'); + if (!width || !file.read(&table[0], table.size())) + return false; + uint64_t count = 0; + for (size_t i = 0; i < width; ++i) + count = count << 8 | static_cast(table[i]); + for (size_t i = 0, pos = width * (count + 1); i < count && pos < table.size(); ++i) { + std::string name(table.c_str() + pos); + pos += name.size() + 1; + symbols.insert(name); + } + return true; +} + +void LLVMIsolation(const char *scenario = "jit") +{ + const std::string which = scenario; + if (which == "jit") { + const char *library = gSystem->Getenv("LLVM_ISOLATION_LOAD"); + if (library && gSystem->Load(library) < 0) { + Error("LLVMIsolation", "cannot load %s", library); + return; + } + gInterpreter->Declare("template T LLVMIsolationTwice(T a) { return a + a; }"); + auto result = gInterpreter->Calc("LLVMIsolationTwice(21)"); + TClass *cl = TClass::GetClass("TNamed"); + if (result == 42 && cl && cl->GetListOfMethods()->GetSize() > 0) + printf("LLVM isolation: JIT OK\n"); + } else if (which == "names") { + std::set defined; + std::stringstream archives(gSystem->Getenv("LLVM_ISOLATION_ARCHIVES")); + for (std::string archive; std::getline(archives, archive, ':');) { + if (!LLVMIsolationArchiveSymbols(archive, defined)) + Error("LLVMIsolation", "%s is not an archive with a symbol table", archive.c_str()); + } +#define LLVM_ISOLATION_NAME(name, mangled) mangled, + const char *names[] = {LLVM_ISOLATION_DEFINED_SYMBOLS(LLVM_ISOLATION_NAME) + LLVM_ISOLATION_PROBED_SYMBOLS(LLVM_ISOLATION_NAME)}; + int missing = 0; + for (const char *name : names) { + if (!defined.count(name)) { + Error("LLVMIsolation", "not defined: %s", name); + ++missing; + } + } + printf("LLVM isolation names: missing=%d of %zu\n", missing, std::size(names)); + } +} diff --git a/core/metacling/test/LLVMIsolation.h b/core/metacling/test/LLVMIsolation.h new file mode 100644 index 0000000000000..4c89097b8f4c9 --- /dev/null +++ b/core/metacling/test/LLVMIsolation.h @@ -0,0 +1,3 @@ +struct LLVMIsolation { + int fValue = 42; +}; diff --git a/core/metacling/test/LLVMIsolationCanary.cxx b/core/metacling/test/LLVMIsolationCanary.cxx new file mode 100644 index 0000000000000..f0adb704c16f4 --- /dev/null +++ b/core/metacling/test/LLVMIsolationCanary.cxx @@ -0,0 +1,35 @@ +/// \file +/// Defines LLVM and Clang functions that libCling calls, with default +/// visibility. When this library is preloaded, or loaded with RTLD_GLOBAL, it +/// is a candidate for every LLVM symbol that is resolved through the global +/// scope instead of inside libCling. Any call reaching it means that libCling's +/// LLVM is not isolated, so it aborts loudly. + +#include "LLVMIsolationCanary.h" + +#include +#include + +[[noreturn]] static void Leak(const char *symbol) +{ + std::fprintf(stderr, "LLVM isolation canary: %s resolved outside of libCling\n", symbol); + std::abort(); +} + +#define LLVM_ISOLATION_CANARY(name, mangled) \ + extern "C" __attribute__((visibility("default"))) void canary_##name() __asm__(mangled); \ + extern "C" void canary_##name() \ + { \ + Leak(mangled); \ + } +LLVM_ISOLATION_DEFINED_SYMBOLS(LLVM_ISOLATION_CANARY) + +// Weak references to LLVM symbols that the canary does not define. The dynamic +// linker binds them when the library is loaded; if libCling exports any of them, +// they bind to libCling, which the bindings tests detect. Otherwise they stay null. +#define LLVM_ISOLATION_PROBE(name, mangled) extern "C" __attribute__((weak)) const char probe_##name[] __asm__(mangled); +LLVM_ISOLATION_PROBED_SYMBOLS(LLVM_ISOLATION_PROBE) + +#define LLVM_ISOLATION_PROBE_ADDRESS(name, mangled) probe_##name, +__attribute__((used)) static const void *const gLLVMIsolationProbes[] = { + LLVM_ISOLATION_PROBED_SYMBOLS(LLVM_ISOLATION_PROBE_ADDRESS)}; diff --git a/core/metacling/test/LLVMIsolationCanary.h b/core/metacling/test/LLVMIsolationCanary.h new file mode 100644 index 0000000000000..a48e60880e53a --- /dev/null +++ b/core/metacling/test/LLVMIsolationCanary.h @@ -0,0 +1,23 @@ +/// \file +/// LLVM and Clang symbols used by the LLVM isolation tests, as X(identifier, +/// mangled name). test-metacling-llvm-isolation-canary-names checks that the +/// LLVM ROOT was built with defines all of them, so that they cannot silently +/// become stale when LLVM changes. + +/// Functions libCling calls; the canary defines them to abort. +#define LLVM_ISOLATION_DEFINED_SYMBOLS(X) \ + X(errs, "_ZN4llvm4errsEv") /* llvm::errs() */ \ + X(outs, "_ZN4llvm4outsEv") /* llvm::outs() */ \ + X(nulls, "_ZN4llvm5nullsEv") /* llvm::nulls() */ \ + X(options, "_ZN4llvm2cl20getRegisteredOptionsERNS0_10SubCommandE") /* cl::getRegisteredOptions */ \ + X(is_absolute, "_ZN4llvm3sys4path11is_absoluteERKNS_5TwineENS1_5StyleE") \ + X(module_c1, "_ZN4llvm6ModuleC1ENS_9StringRefERNS_11LLVMContextE") /* Module::Module */ \ + X(module_c2, "_ZN4llvm6ModuleC2ENS_9StringRefERNS_11LLVMContextE") \ + X(type_info, "_ZNK5clang10ASTContext15getTypeInfoImplEPKNS_4TypeE") + +/// Symbols the canary references weakly without defining them. +#define LLVM_ISOLATION_PROBED_SYMBOLS(X) \ + X(anchor, "_ZN4llvm2cl6Option6anchorEv") /* cl::Option::anchor */ \ + X(write, "_ZN4llvm11raw_ostream5writeEPKcm") /* raw_ostream::write */ \ + X(vtable, "_ZTVN4llvm11raw_ostreamE") /* raw_ostream vtable */ \ + X(twine, "_ZNK4llvm5Twine8toVectorERNS_15SmallVectorImplIcEE") /* Twine::toVector */ diff --git a/core/metacling/test/LLVMIsolationLinkDef.h b/core/metacling/test/LLVMIsolationLinkDef.h new file mode 100644 index 0000000000000..68e2866a2d5c8 --- /dev/null +++ b/core/metacling/test/LLVMIsolationLinkDef.h @@ -0,0 +1,3 @@ +#ifdef __ROOTCLING__ +#pragma link C++ class LLVMIsolation+; +#endif diff --git a/core/metacling/test/LLVMIsolationTestLibrary.cxx b/core/metacling/test/LLVMIsolationTestLibrary.cxx new file mode 100644 index 0000000000000..088021394f2cc --- /dev/null +++ b/core/metacling/test/LLVMIsolationTestLibrary.cxx @@ -0,0 +1,21 @@ +/// \file +/// The libraries of the LLVM isolation self-tests, one per role: +/// CALLER: calls llvm::errs() when loaded, without defining it, as a library +/// that +/// relies on the LLVM of its host. The reference is weak, so that it +/// links. + +namespace llvm { +#if defined(CALLER) +class raw_fd_ostream; +__attribute__((weak)) raw_fd_ostream &errs(); +#endif +} // namespace llvm + +#if defined(CALLER) +__attribute__((constructor)) static void LLVMIsolationCallErrs() +{ + if (&llvm::errs) + llvm::errs(); +} +#endif From 6f10d8eaa09ec2e73ad24f2a27b497c1b47ed694 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Sun, 4 Oct 2026 10:29:24 +0000 Subject: [PATCH 2/6] Test that libCling keeps LLVM private on macOS Two-level namespaces keep libCling's imports on macOS private, so a canary cannot reach them as it does on Linux. What another LLVM in the process can reach are libCling's exports: dyld merges weak definitions across images by name, and lookups by name, as for Python extensions linked with -undefined dynamic_lookup or through dlsym(), see every export. Audit libCling's loaded image: it must use two-level namespaces, load no shared LLVM or Clang library, and export no LLVM or Clang symbols. A self-test runs the audit on a library that exports two, one of them weak, so that a broken audit cannot pass. The test fails today: libCling exports 23,030 such symbols, 120 of them weak (arm64, minimal build). --- core/metacling/test/CMakeLists.txt | 18 ++++ core/metacling/test/LLVMIsolation.C | 98 +++++++++++++++++++ .../test/LLVMIsolationTestLibrary.cxx | 12 +++ 3 files changed, 128 insertions(+) diff --git a/core/metacling/test/CMakeLists.txt b/core/metacling/test/CMakeLists.txt index 92eb9f7bce46b..3ee4c7baa010c 100644 --- a/core/metacling/test/CMakeLists.txt +++ b/core/metacling/test/CMakeLists.txt @@ -100,3 +100,21 @@ if(CMAKE_EXECUTABLE_FORMAT STREQUAL "ELF" AND NOT asan) ENVIRONMENT LD_PRELOAD=${canary}) endif() endif() + +# On macOS, two-level namespaces keep libCling's imports private, so a canary +# cannot reach them; what another LLVM can reach are libCling's exports. +if(APPLE) + foreach(role PROVIDER LEAKY) + add_library(LLVMIsolationTest${role} SHARED LLVMIsolationTestLibrary.cxx) + target_compile_definitions(LLVMIsolationTest${role} PRIVATE ${role}) + endforeach() + target_link_libraries(LLVMIsolationTestLEAKY PRIVATE LLVMIsolationTestPROVIDER) + ROOT_ADD_TEST(test-metacling-llvm-isolation-exports + COMMAND root.exe -l -b -q "${macro}(\"exports\")" + PASSREGEX "exports of libCling[.]so: llvm=0 weak=0 shared=0 twolevel=1 exports=[1-9]") + # The check must find what a library that does not keep LLVM private exports. + ROOT_ADD_TEST(test-metacling-llvm-isolation-exports-selftest + COMMAND root.exe -l -b -q "${macro}(\"exports\")" + ENVIRONMENT LLVM_ISOLATION_IMAGE=$ + PASSREGEX "exports of libLLVMIsolationTestLEAKY[^:]*: llvm=2 weak=1 shared=1 twolevel=1 exports=[1-9]") +endif() diff --git a/core/metacling/test/LLVMIsolation.C b/core/metacling/test/LLVMIsolation.C index e1e0ee35c8cd3..264efeb883b16 100644 --- a/core/metacling/test/LLVMIsolation.C +++ b/core/metacling/test/LLVMIsolation.C @@ -6,14 +6,81 @@ /// "names": the static LLVM libraries in ARCHIVES (separated by ':') define every /// symbol in LLVMIsolationCanary.h, so that these cannot silently become /// stale. +/// "exports": on macOS, the image IMAGE (libCling by default, else loaded first) +/// uses two-level namespaces, loads no shared LLVM or Clang library, and +/// exports no LLVM or Clang symbols: dyld merges weak definitions across +/// images by name, and lookups by name, as with -undefined dynamic_lookup +/// or dlsym(), see all of them. #include "LLVMIsolationCanary.h" +#include +#include #include #include #include #include +#ifdef __APPLE__ +#include +#include +#include +#include + +/// Whether `name` is part of LLVM's or libclang's C API, or an Itanium-mangled name +/// involving the namespaces llvm or clang outside the cling API. +bool LLVMIsolationIsLLVMSymbol(const char *name) +{ + if (std::strncmp(name, "_Z", 2) != 0) + return (std::strncmp(name, "LLVM", 4) == 0 && std::isupper(static_cast(name[4]))) || + std::strncmp(name, "clang_", 6) == 0; + const char *p = name + 2; + while (std::isupper(static_cast(*p))) + ++p; + return std::strncmp(p, "5cling", 6) != 0 && (std::strstr(name, "4llvm") || std::strstr(name, "5clang")); +} + +struct LLVMIsolationMachOAudit { + int exports = 0, llvm = 0, weak = 0, shared = 0; + bool twoLevel = false; +}; + +/// Audit a 64-bit Mach-O image: its exported symbols, those involving LLVM or Clang +/// and the weak ones among them, and the shared LLVM or Clang libraries it loads. +/// `linkedit` is where the image's __LINKEDIT contents would start at file offset 0. +LLVMIsolationMachOAudit LLVMIsolationAuditMachO(const mach_header_64 *header, const char *linkedit) +{ + LLVMIsolationMachOAudit audit; + audit.twoLevel = header->flags & MH_TWOLEVEL; + const char *command = reinterpret_cast(header + 1); + for (uint32_t i = 0; i < header->ncmds; ++i, command += reinterpret_cast(command)->cmdsize) { + uint32_t cmd = reinterpret_cast(command)->cmd; + if (cmd == LC_LOAD_DYLIB || cmd == LC_LOAD_WEAK_DYLIB || cmd == LC_REEXPORT_DYLIB) { + const auto *dylib = reinterpret_cast(command); + const char *path = command + dylib->dylib.name.offset; + const char *leaf = std::strrchr(path, '/') ? std::strrchr(path, '/') + 1 : path; + audit.shared += std::strncmp(leaf, "libLLVM", 7) == 0 || std::strncmp(leaf, "libclang-cpp", 12) == 0; + } else if (cmd == LC_SYMTAB && linkedit) { + const auto *symtab = reinterpret_cast(command); + const auto *symbols = reinterpret_cast(linkedit + symtab->symoff); + for (uint32_t s = 0; s < symtab->nsyms; ++s) { + const nlist_64 &symbol = symbols[s]; + if ((symbol.n_type & N_STAB) || (symbol.n_type & (N_EXT | N_PEXT)) != N_EXT || + (symbol.n_type & N_TYPE) != N_SECT) + continue; + ++audit.exports; + // Mach-O prefixes names with an underscore. + if (LLVMIsolationIsLLVMSymbol(linkedit + symtab->stroff + symbol.n_un.n_strx + 1)) { + ++audit.llvm; + audit.weak += (symbol.n_desc & N_WEAK_DEF) != 0; + } + } + } + } + return audit; +} +#endif + /// Add the names in the symbol table of a GNU ar archive: its first member, named "/" /// or "/SYM64/", with the big-endian number of symbols, their offsets and their names. bool LLVMIsolationArchiveSymbols(const std::string &path, std::set &symbols) @@ -70,5 +137,36 @@ void LLVMIsolation(const char *scenario = "jit") } } printf("LLVM isolation names: missing=%d of %zu\n", missing, std::size(names)); + } else if (which == "exports") { +#ifdef __APPLE__ + std::string image = + gSystem->Getenv("LLVM_ISOLATION_IMAGE") ? gSystem->Getenv("LLVM_ISOLATION_IMAGE") : "libCling.so"; + if (image != "libCling.so" && !dlopen(image.c_str(), RTLD_NOW | RTLD_LOCAL)) { + Error("LLVMIsolation", "%s", dlerror()); + return; + } + const std::string leaf = image.substr(image.rfind('/') + 1); + for (uint32_t i = 0; i < _dyld_image_count(); ++i) { + const std::string name = _dyld_get_image_name(i); + if (name.size() < leaf.size() + 1 || + name.compare(name.size() - leaf.size() - 1, std::string::npos, "/" + leaf) != 0) + continue; + const auto *header = reinterpret_cast(_dyld_get_image_header(i)); + const char *linkedit = nullptr; + const char *command = reinterpret_cast(header + 1); + for (uint32_t c = 0; c < header->ncmds; + ++c, command += reinterpret_cast(command)->cmdsize) { + const auto *segment = reinterpret_cast(command); + if (segment->cmd == LC_SEGMENT_64 && std::strcmp(segment->segname, SEG_LINKEDIT) == 0) + linkedit = + reinterpret_cast(_dyld_get_image_vmaddr_slide(i) + segment->vmaddr - segment->fileoff); + } + auto audit = LLVMIsolationAuditMachO(header, linkedit); + printf("LLVM isolation exports of %s: llvm=%d weak=%d shared=%d twolevel=%d exports=%d\n", leaf.c_str(), + audit.llvm, audit.weak, audit.shared, audit.twoLevel, audit.exports); + return; + } + Error("LLVMIsolation", "%s is not loaded", leaf.c_str()); +#endif } } diff --git a/core/metacling/test/LLVMIsolationTestLibrary.cxx b/core/metacling/test/LLVMIsolationTestLibrary.cxx index 088021394f2cc..80cb33e9ca0d2 100644 --- a/core/metacling/test/LLVMIsolationTestLibrary.cxx +++ b/core/metacling/test/LLVMIsolationTestLibrary.cxx @@ -9,6 +9,18 @@ namespace llvm { #if defined(CALLER) class raw_fd_ostream; __attribute__((weak)) raw_fd_ostream &errs(); +#elif defined(PROVIDER) +__attribute__((visibility("default"))) int LLVMIsolationTestFunction() +{ + return 42; +} +#elif defined(LEAKY) +int LLVMIsolationTestFunction(); +__attribute__((visibility("default"))) int LLVMIsolationTestExport() +{ + return LLVMIsolationTestFunction(); +} +__attribute__((weak, visibility("default"))) int LLVMIsolationTestWeak = 0; #endif } // namespace llvm From 2a19f2d27519d1da25f8fc1f84b06579b1da4048 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Sun, 4 Oct 2026 10:30:52 +0000 Subject: [PATCH 3/6] Test that libCling keeps LLVM private on Windows On Windows, imports are bound to the DLL that provides them, so another LLVM cannot interpose on libCling's as it can on Linux. Two LLVMs mix when libCling imports a shared LLVM or Clang DLL instead of linking LLVM statically: once a DLL of a given name is loaded, the loader uses it for every module that imports a DLL of that name, wherever it was found [1]. Check that libCling.dll imports no LLVM or Clang DLL, also with delayed loading, reading its import tables from the file. A self-test runs the check on a library importing from a stand-in "LLVM" DLL, so that a broken check cannot pass. The test logs the DLLs it finds, for diagnosing CI. libCling's exports are not checked: only lookups by name across all modules, as some JITs do, reach them. [1] https://learn.microsoft.com/windows/win32/dlls/dynamic-link-library-search-order --- core/metacling/test/CMakeLists.txt | 24 +++++- core/metacling/test/LLVMIsolation.C | 84 +++++++++++++++++++ .../test/LLVMIsolationTestLibrary.cxx | 12 ++- 3 files changed, 114 insertions(+), 6 deletions(-) diff --git a/core/metacling/test/CMakeLists.txt b/core/metacling/test/CMakeLists.txt index 3ee4c7baa010c..57022eb812290 100644 --- a/core/metacling/test/CMakeLists.txt +++ b/core/metacling/test/CMakeLists.txt @@ -101,20 +101,36 @@ if(CMAKE_EXECUTABLE_FORMAT STREQUAL "ELF" AND NOT asan) endif() endif() -# On macOS, two-level namespaces keep libCling's imports private, so a canary -# cannot reach them; what another LLVM can reach are libCling's exports. -if(APPLE) +# On macOS and Windows, imports are bound to the library that provides them, so +# a canary cannot reach libCling's. A library that does not keep LLVM private +# must be found by the checks below. +if(APPLE OR WIN32) foreach(role PROVIDER LEAKY) add_library(LLVMIsolationTest${role} SHARED LLVMIsolationTestLibrary.cxx) target_compile_definitions(LLVMIsolationTest${role} PRIVATE ${role}) endforeach() target_link_libraries(LLVMIsolationTestLEAKY PRIVATE LLVMIsolationTestPROVIDER) +endif() + +# On macOS, what another LLVM can reach are libCling's exports. +if(APPLE) ROOT_ADD_TEST(test-metacling-llvm-isolation-exports COMMAND root.exe -l -b -q "${macro}(\"exports\")" PASSREGEX "exports of libCling[.]so: llvm=0 weak=0 shared=0 twolevel=1 exports=[1-9]") - # The check must find what a library that does not keep LLVM private exports. ROOT_ADD_TEST(test-metacling-llvm-isolation-exports-selftest COMMAND root.exe -l -b -q "${macro}(\"exports\")" ENVIRONMENT LLVM_ISOLATION_IMAGE=$ PASSREGEX "exports of libLLVMIsolationTestLEAKY[^:]*: llvm=2 weak=1 shared=1 twolevel=1 exports=[1-9]") endif() + +# On Windows, two LLVMs mix if libCling imports a shared LLVM DLL, which the +# loader shares by name with any other module importing a DLL of that name. +if(WIN32) + ROOT_ADD_TEST(test-metacling-llvm-isolation-imports + COMMAND root.exe -l -b -q "${macro}(\"imports\")" + PASSREGEX "imports of libCling[.]dll: shared=0 dlls=[1-9]") + ROOT_ADD_TEST(test-metacling-llvm-isolation-imports-selftest + COMMAND root.exe -l -b -q "${macro}(\"imports\")" + ENVIRONMENT LLVM_ISOLATION_IMAGE=$ + PASSREGEX "imports of (lib)?LLVMIsolationTestLEAKY[.]dll: shared=1 dlls=[1-9]") +endif() diff --git a/core/metacling/test/LLVMIsolation.C b/core/metacling/test/LLVMIsolation.C index 264efeb883b16..3087dcf8928c2 100644 --- a/core/metacling/test/LLVMIsolation.C +++ b/core/metacling/test/LLVMIsolation.C @@ -11,15 +11,21 @@ /// exports no LLVM or Clang symbols: dyld merges weak definitions across /// images by name, and lookups by name, as with -undefined dynamic_lookup /// or dlsym(), see all of them. +/// "imports": on Windows, the DLL IMAGE (libCling by default) imports no LLVM or +/// Clang DLL: the loader shares a DLL by name with every other module +/// that imports a DLL of that name. #include "LLVMIsolationCanary.h" +#include #include #include #include +#include #include #include #include +#include #ifdef __APPLE__ #include @@ -81,6 +87,67 @@ LLVMIsolationMachOAudit LLVMIsolationAuditMachO(const mach_header_64 *header, co } #endif +#ifdef _WIN32 +/// Whether `dll` is a shared LLVM or Clang library, such as LLVM-C.dll, libLLVM-22.dll, +/// libclang.dll or clang-cpp.dll, but not clang's runtime libraries. +bool LLVMIsolationIsLLVMDll(std::string dll) +{ + for (char &c : dll) + c = std::tolower(static_cast(c)); + for (const char *prefix : {"llvm", "libllvm", "libclang", "clang-cpp"}) + if (dll.rfind(prefix, 0) == 0) + return true; + return false; +} + +struct LLVMIsolationPEAudit { + int shared = 0; + std::vector imports; +}; + +/// Audit a PE image, given as the contents of its file: the DLLs it imports, also +/// with delayed loading, and the LLVM or Clang ones among them. +LLVMIsolationPEAudit LLVMIsolationAuditPE(const std::string &file) +{ + LLVMIsolationPEAudit audit; + auto u32 = [&](uint32_t offset) { + uint32_t value = 0; + if (offset + 4 <= file.size()) + std::memcpy(&value, file.data() + offset, 4); + return value; + }; + auto u16 = [&](uint32_t offset) { return u32(offset) & 0xffff; }; + uint32_t nt = u32(0x3c); + if (u32(nt) != 0x4550) // "PE\0\0" + return audit; + uint32_t optional = nt + 24, sections = optional + u16(nt + 20), numSections = u16(nt + 6); + uint32_t directories = optional + (u16(optional) == 0x20b ? 112 : 96); // PE32+ or PE32 + // The file offset of a relative virtual address, through the section table. + auto offset = [&](uint32_t rva) { + for (uint32_t s = 0; s < numSections; ++s) { + uint32_t header = sections + 40 * s, address = u32(header + 12); + if (rva >= address && rva < address + (std::max)(u32(header + 8), u32(header + 16))) + return rva - address + u32(header + 20); + } + return rva; + }; + auto import = [&](uint32_t nameRva) { + const char *name = offset(nameRva) < file.size() ? file.c_str() + offset(nameRva) : ""; + audit.shared += LLVMIsolationIsLLVMDll(name); + audit.imports.push_back(name); + }; + // IMAGE_IMPORT_DESCRIPTOR (20 bytes, Name at 12) and IMAGE_DELAYLOAD_DESCRIPTOR + // (32 bytes, DllNameRVA at 4), each array ending with a zero name. + if (uint32_t imports = u32(directories + 8)) + for (uint32_t d = offset(imports); u32(d + 12); d += 20) + import(u32(d + 12)); + if (uint32_t delayed = u32(directories + 8 * 13)) + for (uint32_t d = offset(delayed); u32(d + 4); d += 32) + import(u32(d + 4)); + return audit; +} +#endif + /// Add the names in the symbol table of a GNU ar archive: its first member, named "/" /// or "/SYM64/", with the big-endian number of symbols, their offsets and their names. bool LLVMIsolationArchiveSymbols(const std::string &path, std::set &symbols) @@ -167,6 +234,23 @@ void LLVMIsolation(const char *scenario = "jit") return; } Error("LLVMIsolation", "%s is not loaded", leaf.c_str()); +#endif + } else if (which == "imports") { +#ifdef _WIN32 + TString image = gSystem->Getenv("LLVM_ISOLATION_IMAGE") ? gSystem->Getenv("LLVM_ISOLATION_IMAGE") : "libCling"; + if (!gSystem->FindDynamicLibrary(image, kTRUE)) { + Error("LLVMIsolation", "cannot find %s", image.Data()); + return; + } + std::ifstream file(image.Data(), std::ios::binary); + const std::string contents{std::istreambuf_iterator(file), std::istreambuf_iterator()}; + auto audit = LLVMIsolationAuditPE(contents); + printf("LLVM isolation imports: reading %s (%zu bytes)\n", image.Data(), contents.size()); + for (const std::string &dll : audit.imports) + printf("LLVM isolation imports: %s%s\n", dll.c_str(), + LLVMIsolationIsLLVMDll(dll) ? " <- LLVM or Clang" : ""); + printf("LLVM isolation imports of %s: shared=%d dlls=%zu\n", gSystem->BaseName(image.Data()), audit.shared, + audit.imports.size()); #endif } } diff --git a/core/metacling/test/LLVMIsolationTestLibrary.cxx b/core/metacling/test/LLVMIsolationTestLibrary.cxx index 80cb33e9ca0d2..3628ad7eabab9 100644 --- a/core/metacling/test/LLVMIsolationTestLibrary.cxx +++ b/core/metacling/test/LLVMIsolationTestLibrary.cxx @@ -5,23 +5,31 @@ /// relies on the LLVM of its host. The reference is weak, so that it /// links. +#ifdef _WIN32 +#define LLVM_ISOLATION_EXPORT __declspec(dllexport) +#else +#define LLVM_ISOLATION_EXPORT __attribute__((visibility("default"))) +#endif + namespace llvm { #if defined(CALLER) class raw_fd_ostream; __attribute__((weak)) raw_fd_ostream &errs(); #elif defined(PROVIDER) -__attribute__((visibility("default"))) int LLVMIsolationTestFunction() +LLVM_ISOLATION_EXPORT int LLVMIsolationTestFunction() { return 42; } #elif defined(LEAKY) int LLVMIsolationTestFunction(); -__attribute__((visibility("default"))) int LLVMIsolationTestExport() +LLVM_ISOLATION_EXPORT int LLVMIsolationTestExport() { return LLVMIsolationTestFunction(); } +#ifndef _WIN32 __attribute__((weak, visibility("default"))) int LLVMIsolationTestWeak = 0; #endif +#endif } // namespace llvm #if defined(CALLER) From 36ff5562a6ae6aaba32e64893bd760795c5ad3a5 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Sun, 4 Oct 2026 10:32:27 +0000 Subject: [PATCH 4/6] Hide libCling's LLVM and Clang symbols on macOS libCling.script hides the LLVM and Clang symbols that -fvisibility=hidden leaves visible, mostly template instantiations, but only on Linux: ld64 takes no version script. On macOS, libCling exports 23,030 of them, 120 as weak definitions. Two-level namespaces protect libCling's own imports, but dyld merges weak definitions across images by name, and lookups by name, as with -undefined dynamic_lookup or dlsym(), see every export: the macOS form of #19889. Generate ld64's exported symbols list from libCling.script, which stays the one list of what libCling exports, and fail the configuration if it yields none. test-metacling-llvm-isolation-exports now passes. In a minimal build, no other ROOT library imports LLVM or Clang symbols from libCling. --- core/metacling/src/CMakeLists.txt | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/core/metacling/src/CMakeLists.txt b/core/metacling/src/CMakeLists.txt index ce830802a9b21..2221160da12c4 100644 --- a/core/metacling/src/CMakeLists.txt +++ b/core/metacling/src/CMakeLists.txt @@ -227,7 +227,8 @@ if(NOT APPLE AND NOT MSVC) # the problem reported for Julia in # https://github.com/JuliaHEP/ROOT.jl/issues/17#issuecomment-882719292 # and by ALICE in https://github.com/root-project/root/issues/19889 - # Only needed for Linux: Mac uses linker namespaces and Windows explicit export/import + # Only needed for Linux: Windows uses explicit export/import, and Mac + # two-level namespaces, but see the exported symbols list below. target_link_libraries(Cling PRIVATE -Wl,-Bsymbolic) # We compile with -fvisibility=hidden in interpreter/, but some symbols of # instantiated templates remain. Use a version script to hide them. @@ -242,6 +243,26 @@ if(NOT APPLE AND NOT MSVC) ARGS $) endif() +if(APPLE) + # Two-level namespaces keep libCling's own imports private, but dyld merges + # weak definitions across images by name, and lookups by name, as with + # -undefined dynamic_lookup or dlsym(), see every export. Export what + # libCling.script does: ld64 takes no version script, but a list of its + # globals in Mach-O spelling. + set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS libCling.script) + file(READ ${CMAKE_CURRENT_SOURCE_DIR}/libCling.script script) + string(REGEX MATCH "global:([^}]*)local:" _ "${script}") + string(REGEX MATCHALL "[^ \t\r\n;]+" exports "${CMAKE_MATCH_1}") + if(NOT exports) + message(FATAL_ERROR "No global symbols found in libCling.script") + endif() + list(TRANSFORM exports PREPEND "_") + list(JOIN exports "\n" exports) + file(CONFIGURE OUTPUT libCling.exports CONTENT "${exports}\n" @ONLY) + target_link_libraries(Cling PRIVATE -Wl,-exported_symbols_list,${CMAKE_CURRENT_BINARY_DIR}/libCling.exports) + set_target_properties(Cling PROPERTIES LINK_DEPENDS ${CMAKE_CURRENT_BINARY_DIR}/libCling.exports) +endif() + if (CMAKE_SYSTEM_NAME MATCHES FreeBSD) target_link_libraries(Cling PRIVATE util procstat) endif() From 5881cad9e386ccdb6592cce4fe091d4e8f8212f0 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Mon, 5 Oct 2026 05:37:28 +0000 Subject: [PATCH 5/6] Do not include Mach-O headers in the LLVM isolation test With LLVM assertions, on x86_64 macOS, the interpreter aborts right after building the module for that the macro of test-metacling-llvm-isolation-exports includes: Assertion failed: (!FoundVal && "Key already in new map?"), function moveFrom, file DenseMap.h, line 448. Define the few Mach-O structures the audit reads, whose layout the ABI fixes, and get dyld's image functions through dlsym(), so that the interpreter needs no Mach-O header. --- core/metacling/test/LLVMIsolation.C | 95 +++++++++++++++++++++-------- 1 file changed, 68 insertions(+), 27 deletions(-) diff --git a/core/metacling/test/LLVMIsolation.C b/core/metacling/test/LLVMIsolation.C index 3087dcf8928c2..9571e4e468463 100644 --- a/core/metacling/test/LLVMIsolation.C +++ b/core/metacling/test/LLVMIsolation.C @@ -29,9 +29,43 @@ #ifdef __APPLE__ #include -#include -#include -#include + +// The Mach-O layouts of and , which are not included: +// building their module in the interpreter fails an LLVM assertion on x86_64. +namespace LLVMIsolationMachO { +struct Header { + uint32_t magic; + int32_t cputype, cpusubtype; + uint32_t filetype, ncmds, sizeofcmds, flags, reserved; +}; +struct Command { + uint32_t cmd, cmdsize; +}; +struct DylibCommand { + uint32_t cmd, cmdsize, nameOffset, timestamp, currentVersion, compatibilityVersion; +}; +struct SymtabCommand { + uint32_t cmd, cmdsize, symoff, nsyms, stroff, strsize; +}; +struct SegmentCommand { + uint32_t cmd, cmdsize; + char segname[16]; + uint64_t vmaddr, vmsize, fileoff, filesize; + int32_t maxprot, initprot; + uint32_t nsects, flags; +}; +struct Symbol { + uint32_t strx; + uint8_t type, sect; + uint16_t desc; + uint64_t value; +}; +constexpr uint32_t kTwoLevel = 0x80; // MH_TWOLEVEL +constexpr uint32_t kSymtab = 0x2, kSegment = 0x19; // LC_SYMTAB, LC_SEGMENT_64 +constexpr uint32_t kLoadDylib = 0xc, kLoadWeakDylib = 0x80000018, kReexport = 0x8000001f; // LC_*_DYLIB +constexpr uint8_t kStab = 0xe0, kPrivate = 0x10, kTypeMask = 0x0e, kExternal = 0x01, kSection = 0x0e; +constexpr uint16_t kWeakDefinition = 0x80; // N_WEAK_DEF +} // namespace LLVMIsolationMachO /// Whether `name` is part of LLVM's or libclang's C API, or an Itanium-mangled name /// involving the namespaces llvm or clang outside the cling API. @@ -54,31 +88,31 @@ struct LLVMIsolationMachOAudit { /// Audit a 64-bit Mach-O image: its exported symbols, those involving LLVM or Clang /// and the weak ones among them, and the shared LLVM or Clang libraries it loads. /// `linkedit` is where the image's __LINKEDIT contents would start at file offset 0. -LLVMIsolationMachOAudit LLVMIsolationAuditMachO(const mach_header_64 *header, const char *linkedit) +LLVMIsolationMachOAudit LLVMIsolationAuditMachO(const LLVMIsolationMachO::Header *header, const char *linkedit) { + using namespace LLVMIsolationMachO; LLVMIsolationMachOAudit audit; - audit.twoLevel = header->flags & MH_TWOLEVEL; + audit.twoLevel = header->flags & kTwoLevel; const char *command = reinterpret_cast(header + 1); - for (uint32_t i = 0; i < header->ncmds; ++i, command += reinterpret_cast(command)->cmdsize) { - uint32_t cmd = reinterpret_cast(command)->cmd; - if (cmd == LC_LOAD_DYLIB || cmd == LC_LOAD_WEAK_DYLIB || cmd == LC_REEXPORT_DYLIB) { - const auto *dylib = reinterpret_cast(command); - const char *path = command + dylib->dylib.name.offset; + for (uint32_t i = 0; i < header->ncmds; ++i, command += reinterpret_cast(command)->cmdsize) { + uint32_t cmd = reinterpret_cast(command)->cmd; + if (cmd == kLoadDylib || cmd == kLoadWeakDylib || cmd == kReexport) { + const char *path = command + reinterpret_cast(command)->nameOffset; const char *leaf = std::strrchr(path, '/') ? std::strrchr(path, '/') + 1 : path; audit.shared += std::strncmp(leaf, "libLLVM", 7) == 0 || std::strncmp(leaf, "libclang-cpp", 12) == 0; - } else if (cmd == LC_SYMTAB && linkedit) { - const auto *symtab = reinterpret_cast(command); - const auto *symbols = reinterpret_cast(linkedit + symtab->symoff); + } else if (cmd == kSymtab && linkedit) { + const auto *symtab = reinterpret_cast(command); + const auto *symbols = reinterpret_cast(linkedit + symtab->symoff); for (uint32_t s = 0; s < symtab->nsyms; ++s) { - const nlist_64 &symbol = symbols[s]; - if ((symbol.n_type & N_STAB) || (symbol.n_type & (N_EXT | N_PEXT)) != N_EXT || - (symbol.n_type & N_TYPE) != N_SECT) + const Symbol &symbol = symbols[s]; + if ((symbol.type & kStab) || (symbol.type & (kExternal | kPrivate)) != kExternal || + (symbol.type & kTypeMask) != kSection) continue; ++audit.exports; // Mach-O prefixes names with an underscore. - if (LLVMIsolationIsLLVMSymbol(linkedit + symtab->stroff + symbol.n_un.n_strx + 1)) { + if (LLVMIsolationIsLLVMSymbol(linkedit + symtab->stroff + symbol.strx + 1)) { ++audit.llvm; - audit.weak += (symbol.n_desc & N_WEAK_DEF) != 0; + audit.weak += (symbol.desc & kWeakDefinition) != 0; } } } @@ -206,6 +240,15 @@ void LLVMIsolation(const char *scenario = "jit") printf("LLVM isolation names: missing=%d of %zu\n", missing, std::size(names)); } else if (which == "exports") { #ifdef __APPLE__ + using namespace LLVMIsolationMachO; + auto imageCount = reinterpret_cast(dlsym(RTLD_DEFAULT, "_dyld_image_count")); + auto imageName = reinterpret_cast(dlsym(RTLD_DEFAULT, "_dyld_get_image_name")); + auto imageHeader = reinterpret_cast(dlsym(RTLD_DEFAULT, "_dyld_get_image_header")); + auto imageSlide = reinterpret_cast(dlsym(RTLD_DEFAULT, "_dyld_get_image_vmaddr_slide")); + if (!imageCount || !imageName || !imageHeader || !imageSlide) { + Error("LLVMIsolation", "cannot find dyld's image functions"); + return; + } std::string image = gSystem->Getenv("LLVM_ISOLATION_IMAGE") ? gSystem->Getenv("LLVM_ISOLATION_IMAGE") : "libCling.so"; if (image != "libCling.so" && !dlopen(image.c_str(), RTLD_NOW | RTLD_LOCAL)) { @@ -213,20 +256,18 @@ void LLVMIsolation(const char *scenario = "jit") return; } const std::string leaf = image.substr(image.rfind('/') + 1); - for (uint32_t i = 0; i < _dyld_image_count(); ++i) { - const std::string name = _dyld_get_image_name(i); + for (uint32_t i = 0; i < imageCount(); ++i) { + const std::string name = imageName(i); if (name.size() < leaf.size() + 1 || name.compare(name.size() - leaf.size() - 1, std::string::npos, "/" + leaf) != 0) continue; - const auto *header = reinterpret_cast(_dyld_get_image_header(i)); + const Header *header = imageHeader(i); const char *linkedit = nullptr; const char *command = reinterpret_cast(header + 1); - for (uint32_t c = 0; c < header->ncmds; - ++c, command += reinterpret_cast(command)->cmdsize) { - const auto *segment = reinterpret_cast(command); - if (segment->cmd == LC_SEGMENT_64 && std::strcmp(segment->segname, SEG_LINKEDIT) == 0) - linkedit = - reinterpret_cast(_dyld_get_image_vmaddr_slide(i) + segment->vmaddr - segment->fileoff); + for (uint32_t c = 0; c < header->ncmds; ++c, command += reinterpret_cast(command)->cmdsize) { + const auto *segment = reinterpret_cast(command); + if (segment->cmd == kSegment && std::strcmp(segment->segname, "__LINKEDIT") == 0) + linkedit = reinterpret_cast(imageSlide(i) + segment->vmaddr - segment->fileoff); } auto audit = LLVMIsolationAuditMachO(header, linkedit); printf("LLVM isolation exports of %s: llvm=%d weak=%d shared=%d twolevel=%d exports=%d\n", leaf.c_str(), From e5fe1036bea3b19646a4b10f67016cab39c5d5d9 Mon Sep 17 00:00:00 2001 From: Vassil Vassilev Date: Mon, 5 Oct 2026 05:23:51 +0000 Subject: [PATCH 6/6] Fix the comment of the LLVM isolation test library. NFC Restore the line breaks of the CALLER role's description, and document the PROVIDER and LEAKY roles that the macOS and Windows tests added. --- core/metacling/test/LLVMIsolationTestLibrary.cxx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/core/metacling/test/LLVMIsolationTestLibrary.cxx b/core/metacling/test/LLVMIsolationTestLibrary.cxx index 3628ad7eabab9..2cd3ad56d4937 100644 --- a/core/metacling/test/LLVMIsolationTestLibrary.cxx +++ b/core/metacling/test/LLVMIsolationTestLibrary.cxx @@ -1,9 +1,11 @@ /// \file /// The libraries of the LLVM isolation self-tests, one per role: -/// CALLER: calls llvm::errs() when loaded, without defining it, as a library -/// that -/// relies on the LLVM of its host. The reference is weak, so that it -/// links. +/// CALLER: calls llvm::errs() when loaded, without defining it, as a library that +/// relies on the LLVM of its host. The reference is weak, so that it links. +/// PROVIDER: a shared "libLLVM" defining llvm::LLVMIsolationTestFunction(). +/// LEAKY: does not keep LLVM private; it uses PROVIDER's function and exports +/// llvm::LLVMIsolationTestExport() and, except on Windows, the weak +/// llvm::LLVMIsolationTestWeak. #ifdef _WIN32 #define LLVM_ISOLATION_EXPORT __declspec(dllexport)