Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion core/metacling/src/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -242,6 +243,26 @@ if(NOT APPLE AND NOT MSVC)
ARGS $<TARGET_FILE:Cling>)
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()
Expand Down
99 changes: 99 additions & 0 deletions core/metacling/test/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 $<TARGET_FILE:${lib}>)
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 $<TARGET_FILE:LLVMIsolationCanary>)
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=$<TARGET_FILE:LLVMIsolationTestCALLER>
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
-- $<TARGET_FILE:root.exe> -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
-- $<TARGET_FILE:rootcling> ${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=$<TARGET_FILE:LLVMIsolationTestLEAKY>
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=$<TARGET_FILE:LLVMIsolationTestLEAKY>
PASSREGEX "imports of (lib)?LLVMIsolationTestLEAKY[.]dll: shared=1 dlls=[1-9]")
endif()
68 changes: 68 additions & 0 deletions core/metacling/test/CheckLLVMBindings.cmake
Original file line number Diff line number Diff line change
@@ -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=<directory> [-DEXPECT=<regex>] -P CheckLLVMBindings.cmake -- <command> [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")
Loading
Loading