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() diff --git a/core/metacling/test/CMakeLists.txt b/core/metacling/test/CMakeLists.txt index eaec1bfd31863..57022eb812290 100644 --- a/core/metacling/test/CMakeLists.txt +++ b/core/metacling/test/CMakeLists.txt @@ -35,3 +35,102 @@ 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() + +# 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]") + 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/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..9571e4e468463 --- /dev/null +++ b/core/metacling/test/LLVMIsolation.C @@ -0,0 +1,297 @@ +/// \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. +/// "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. +/// "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 + +// 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. +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 LLVMIsolationMachO::Header *header, const char *linkedit) +{ + using namespace LLVMIsolationMachO; + LLVMIsolationMachOAudit audit; + 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 == 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 == 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 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.strx + 1)) { + ++audit.llvm; + audit.weak += (symbol.desc & kWeakDefinition) != 0; + } + } + } + } + return audit; +} +#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) +{ + 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)); + } 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)) { + Error("LLVMIsolation", "%s", dlerror()); + return; + } + const std::string leaf = image.substr(image.rfind('/') + 1); + 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 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 == 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(), + audit.llvm, audit.weak, audit.shared, audit.twoLevel, audit.exports); + 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/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..2cd3ad56d4937 --- /dev/null +++ b/core/metacling/test/LLVMIsolationTestLibrary.cxx @@ -0,0 +1,43 @@ +/// \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. +/// 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) +#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) +LLVM_ISOLATION_EXPORT int LLVMIsolationTestFunction() +{ + return 42; +} +#elif defined(LEAKY) +int LLVMIsolationTestFunction(); +LLVM_ISOLATION_EXPORT int LLVMIsolationTestExport() +{ + return LLVMIsolationTestFunction(); +} +#ifndef _WIN32 +__attribute__((weak, visibility("default"))) int LLVMIsolationTestWeak = 0; +#endif +#endif +} // namespace llvm + +#if defined(CALLER) +__attribute__((constructor)) static void LLVMIsolationCallErrs() +{ + if (&llvm::errs) + llvm::errs(); +} +#endif