From f68ccbe649d7e02c17c61571605fd9cf859e1769 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 26 Sep 2026 05:44:09 +0200 Subject: [PATCH 1/5] feat(cli): use the Clang headers shipped next to the CLI coretrace-compiler compiles in process but needs a clang path to find Clang's own headers (stddef.h, stdarg.h, the intrinsics), so an installed analyzer required a clang on the machine it ran on. cmake/ClangHeaders.cmake installs the resource directory's include/ of the clang coretrace-compiler was configured with at lib/clang//include, next to bin/runtime-analyzer. When those headers are there and CT_CLANG is not set, Main() points CT_CLANG at the CLI itself, from which coretrace-compiler derives the resource directory. CT_CLANG set by the user keeps priority, and a build-tree CLI, which ships nothing, changes nothing. unittests/shipped_toolchain_test.cpp checks the rule on temporary layouts; install_layout also expects the installed headers. Refs #19 --- CMakeLists.txt | 10 +++- cmake/ClangHeaders.cmake | 26 +++++++++ src/runtime_analyzer.cpp | 8 +++ src/shipped_toolchain.cpp | 27 +++++++++ src/shipped_toolchain.hpp | 16 +++++ unittests/shipped_toolchain_test.cpp | 87 ++++++++++++++++++++++++++++ 6 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 cmake/ClangHeaders.cmake create mode 100644 src/shipped_toolchain.cpp create mode 100644 src/shipped_toolchain.hpp create mode 100644 unittests/shipped_toolchain_test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 4d5d1c1..cca628b 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ set(TOOL_SOURCES src/process.cpp src/runtime_analyzer.cpp src/sarif.cpp + src/shipped_toolchain.cpp ) # CLI @@ -195,6 +196,8 @@ install(TARGETS ${TOOL_NAME}_lib ) if(BUILD_CLI) install(TARGETS ${TOOL_NAME} RUNTIME DESTINATION bin) + # Clang's headers next to the CLI, so that no clang is needed where it runs. + include(ClangHeaders) endif() install(DIRECTORY include/ DESTINATION include) install(FILES "${TOOL_GENERATED_INCLUDE_DIR}/runtime_analyzer_version.hpp" DESTINATION include) @@ -231,6 +234,11 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) target_link_libraries(process_test PRIVATE ${TOOL_NAME}_lib) add_test(NAME process_unit COMMAND process_test) + add_executable(shipped_toolchain_test unittests/shipped_toolchain_test.cpp) + target_include_directories(shipped_toolchain_test PRIVATE src) + target_link_libraries(shipped_toolchain_test PRIVATE ${TOOL_NAME}_lib) + add_test(NAME shipped_toolchain_unit COMMAND shipped_toolchain_test) + if(BUILD_CLI) find_package(Python3 COMPONENTS Interpreter) if(Python3_Interpreter_FOUND) @@ -286,7 +294,7 @@ if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) # runs an instrumented program with the runtime archives installed next to it. set(_prefix "${CMAKE_CURRENT_BINARY_DIR}/prefix") add_test(NAME install_layout - COMMAND sh -c "rm -rf \"${_prefix}\" && ${CMAKE_COMMAND} --install \"${CMAKE_CURRENT_BINARY_DIR}\" --prefix \"${_prefix}\" > /dev/null && test -f \"${_prefix}/lib/lib${TOOL_NAME}_lib.a\" && test -f \"${_prefix}/include/runtime_analyzer.hpp\" && test -f \"${_prefix}/include/runtime_analyzer_version.hpp\" && \"${_prefix}/bin/${TOOL_NAME}\" --version && \"${_prefix}/bin/${TOOL_NAME}\" -o \"${_prefix}/hello\" -- test/examples/fixtures/hello.c" + COMMAND sh -c "rm -rf \"${_prefix}\" && ${CMAKE_COMMAND} --install \"${CMAKE_CURRENT_BINARY_DIR}\" --prefix \"${_prefix}\" > /dev/null && test -f \"${_prefix}/lib/lib${TOOL_NAME}_lib.a\" && test -f \"${_prefix}/include/runtime_analyzer.hpp\" && test -f \"${_prefix}/include/runtime_analyzer_version.hpp\" && test -f \"${_prefix}\"/lib/clang/*/include/stddef.h && \"${_prefix}/bin/${TOOL_NAME}\" --version && \"${_prefix}/bin/${TOOL_NAME}\" -o \"${_prefix}/hello\" -- test/examples/fixtures/hello.c" WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}") endif() endif() diff --git a/cmake/ClangHeaders.cmake b/cmake/ClangHeaders.cmake new file mode 100644 index 0000000..2d80a8a --- /dev/null +++ b/cmake/ClangHeaders.cmake @@ -0,0 +1,26 @@ +# SPDX-License-Identifier: Apache-2.0 +# +# Clang's own headers (stddef.h, stdarg.h, the intrinsics...) are needed to compile any C or +# C++ source, and they ship with clang, not with the system. The analyzer compiles programs in +# process through coretrace-compiler, which finds them relative to a clang path: installing them +# at /lib/clang//include, next to bin/runtime-analyzer, lets the CLI point +# coretrace-compiler at itself (UseShippedClangHeaders), so that no clang has to be installed +# where the analyzer runs. +# +# They are taken from the clang coretrace-compiler was configured with (CLANG_EXECUTABLE), the +# one whose headers match the linked Clang libraries. +if(NOT CLANG_EXECUTABLE) + message(FATAL_ERROR "CLANG_EXECUTABLE is not set: cannot locate Clang's headers to install") +endif() +execute_process( + COMMAND ${CLANG_EXECUTABLE} -print-resource-dir + OUTPUT_VARIABLE _clang_resource_dir + OUTPUT_STRIP_TRAILING_WHITESPACE + RESULT_VARIABLE _clang_resource_status) +if(NOT _clang_resource_status EQUAL 0 OR NOT EXISTS "${_clang_resource_dir}/include/stddef.h") + message(FATAL_ERROR "No Clang headers at '${_clang_resource_dir}/include' " + "(${CLANG_EXECUTABLE} -print-resource-dir)") +endif() +get_filename_component(_clang_version "${_clang_resource_dir}" NAME) + +install(DIRECTORY "${_clang_resource_dir}/include" DESTINATION lib/clang/${_clang_version}) diff --git a/src/runtime_analyzer.cpp b/src/runtime_analyzer.cpp index 5e22889..6b7ed40 100644 --- a/src/runtime_analyzer.cpp +++ b/src/runtime_analyzer.cpp @@ -6,6 +6,9 @@ #include "findings.hpp" #include "process.hpp" #include "sarif.hpp" +#include "shipped_toolchain.hpp" + +#include #include #include @@ -981,6 +984,11 @@ namespace coretrace::runtime_analyzer std::cout << "runtime-analyzer " << kVersion << '\n'; return 0; } + + // An installed CLI carries Clang's headers next to itself; a build-tree one does not, + // and coretrace-compiler then falls back on the clang it was configured with. + (void)UseShippedClangHeaders( + llvm::sys::fs::getMainExecutable(argv[0], reinterpret_cast(&Main))); if (!parsed.ok) { std::cerr << "runtime-analyzer: " << parsed.error << '\n'; diff --git a/src/shipped_toolchain.cpp b/src/shipped_toolchain.cpp new file mode 100644 index 0000000..7d7023f --- /dev/null +++ b/src/shipped_toolchain.cpp @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: Apache-2.0 +#include "shipped_toolchain.hpp" + +#include +#include + +namespace coretrace::runtime_analyzer +{ + bool UseShippedClangHeaders(const std::filesystem::path& executable) + { + const char* configured = std::getenv("CT_CLANG"); + if (executable.empty() || (configured != nullptr && *configured != '\0')) + { + return false; + } + std::error_code error; + const std::filesystem::path clang_dir = executable.parent_path() / ".." / "lib" / "clang"; + for (const auto& version : std::filesystem::directory_iterator(clang_dir, error)) + { + if (std::filesystem::is_regular_file(version.path() / "include" / "stddef.h", error)) + { + return setenv("CT_CLANG", executable.string().c_str(), /*overwrite=*/1) == 0; + } + } + return false; + } +} // namespace coretrace::runtime_analyzer diff --git a/src/shipped_toolchain.hpp b/src/shipped_toolchain.hpp new file mode 100644 index 0000000..13d5ecf --- /dev/null +++ b/src/shipped_toolchain.hpp @@ -0,0 +1,16 @@ +// SPDX-License-Identifier: Apache-2.0 +#ifndef CORETRACE_RUNTIME_ANALYZER_SHIPPED_TOOLCHAIN_HPP +#define CORETRACE_RUNTIME_ANALYZER_SHIPPED_TOOLCHAIN_HPP + +#include + +namespace coretrace::runtime_analyzer +{ + // Points coretrace-compiler at the Clang headers installed next to the CLI, at + // /../lib/clang//include, by setting CT_CLANG to the CLI itself: the + // resource directory is derived from that path. Returns whether it did. A CT_CLANG set by + // the user keeps priority, and an empty executable path ships nothing. + [[nodiscard]] bool UseShippedClangHeaders(const std::filesystem::path& executable); +} // namespace coretrace::runtime_analyzer + +#endif // CORETRACE_RUNTIME_ANALYZER_SHIPPED_TOOLCHAIN_HPP diff --git a/unittests/shipped_toolchain_test.cpp b/unittests/shipped_toolchain_test.cpp new file mode 100644 index 0000000..3fe6233 --- /dev/null +++ b/unittests/shipped_toolchain_test.cpp @@ -0,0 +1,87 @@ +// SPDX-License-Identifier: Apache-2.0 +// Checks the rule that points coretrace-compiler at the Clang headers shipped next to the CLI. +#include "shipped_toolchain.hpp" + +#include +#include +#include +#include +#include +#include + +namespace +{ + using coretrace::runtime_analyzer::UseShippedClangHeaders; + + int failures = 0; + + void Expect(bool condition, std::string_view what) + { + std::cout << (condition ? "[PASS] " : "[FAIL] ") << what << '\n'; + if (!condition) + { + ++failures; + } + } + + [[nodiscard]] std::string Env(const char* name) + { + const char* value = std::getenv(name); + return value != nullptr ? value : ""; + } + + // /bin/runtime-analyzer, with the headers at /lib/clang/20/include when asked. + [[nodiscard]] std::filesystem::path Layout(std::string_view name, bool with_headers) + { + const std::filesystem::path root = + std::filesystem::temp_directory_path() / ("shipped_toolchain_" + std::string(name)); + std::filesystem::remove_all(root); + std::filesystem::create_directories(root / "bin"); + if (with_headers) + { + std::filesystem::create_directories(root / "lib" / "clang" / "20" / "include"); + std::ofstream(root / "lib" / "clang" / "20" / "include" / "stddef.h") << "\n"; + } + return root / "bin" / "runtime-analyzer"; + } + + void PointsAtItselfWhenHeadersAreShipped() + { + unsetenv("CT_CLANG"); + const std::filesystem::path executable = Layout("shipped", true); + Expect(UseShippedClangHeaders(executable), "shipped headers: used"); + Expect(Env("CT_CLANG") == executable.string(), "shipped headers: CT_CLANG is the CLI"); + } + + void LeavesTheUsersClangAlone() + { + setenv("CT_CLANG", "/opt/llvm/bin/clang", 1); + const std::filesystem::path executable = Layout("user", true); + Expect(!UseShippedClangHeaders(executable), "user's CT_CLANG: shipped headers not used"); + Expect(Env("CT_CLANG") == "/opt/llvm/bin/clang", "user's CT_CLANG: kept"); + unsetenv("CT_CLANG"); + } + + void DoesNothingWithoutHeaders() + { + unsetenv("CT_CLANG"); + const std::filesystem::path executable = Layout("bare", false); + Expect(!UseShippedClangHeaders(executable), "no headers: not used"); + Expect(Env("CT_CLANG").empty(), "no headers: CT_CLANG stays unset"); + Expect(!UseShippedClangHeaders({}), "unknown executable: not used"); + } +} // namespace + +int main() +{ + PointsAtItselfWhenHeadersAreShipped(); + LeavesTheUsersClangAlone(); + DoesNothingWithoutHeaders(); + if (failures != 0) + { + std::cerr << failures << " check(s) failed\n"; + return 1; + } + std::cout << "shipped_toolchain_test: all checks passed\n"; + return 0; +} From 74f5bfece9d58c2071a3b7a0769719da6c48c788 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 26 Sep 2026 05:44:09 +0200 Subject: [PATCH 2/5] build: ship the LLVM libraries in the Linux install tree The CLI links libLLVM and libclang-cpp dynamically. On Linux, install(RUNTIME_DEPENDENCY_SET) now copies the dependencies found under LLVM's library directory into lib/, with libstdc++ and libgcc_s, as the build compiler may be newer than the target's and they are backward compatible; glibc stays the system's. The CLI gets an INSTALL_RPATH of $ORIGIN/../lib. The CMake minimum moves from 3.16 to 3.21 for RUNTIME_DEPENDENCY_SET. Refs #19 --- CMakeLists.txt | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index cca628b..f725038 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,6 @@ -cmake_minimum_required(VERSION 3.16) +# 3.21 for install(RUNTIME_DEPENDENCY_SET). +cmake_minimum_required(VERSION 3.21) # ============================================================================ # CoreTrace Runtime Analyzer @@ -195,7 +196,31 @@ install(TARGETS ${TOOL_NAME}_lib LIBRARY DESTINATION lib ) if(BUILD_CLI) - install(TARGETS ${TOOL_NAME} RUNTIME DESTINATION bin) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + # The archive is self-contained: the LLVM libraries the CLI links (libLLVM, libclang-cpp) + # are copied into lib/, found through the rpath. libstdc++ and libgcc_s are shipped too, + # as the build compiler may be newer than the target's and they are backward compatible. + # glibc stays the system's: the archive runs on any glibc at least as old as the build + # machine's. + set_target_properties(${TOOL_NAME} PROPERTIES INSTALL_RPATH "$ORIGIN/../lib") + install(TARGETS ${TOOL_NAME} + RUNTIME_DEPENDENCY_SET ${TOOL_NAME}_deps + RUNTIME DESTINATION bin + ) + install(RUNTIME_DEPENDENCY_SET ${TOOL_NAME}_deps + PRE_EXCLUDE_REGEXES + "^libc\\.so" + "^libm\\.so" + "^libdl\\.so" + "^librt\\.so" + "^libpthread\\.so" + "^ld-linux" + DIRECTORIES ${LLVM_LIBRARY_DIRS} + LIBRARY DESTINATION lib + ) + else() + install(TARGETS ${TOOL_NAME} RUNTIME DESTINATION bin) + endif() # Clang's headers next to the CLI, so that no clang is needed where it runs. include(ClangHeaders) endif() From 02f12b445e3808229623375b3041db926343c3fa Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 26 Sep 2026 05:50:17 +0200 Subject: [PATCH 3/5] feat(cli): link instrumented programs against the shipped C++ runtime The runtime archives are built with GCC 13, whose libstdc++ symbols (_M_replace_cold and others) are missing from the libstdc++ 11 of Ubuntu 22.04 or the 12 of Debian 12: linking an instrumented program there failed with undefined references, although the GCC 13 libstdc++ is shipped in the archive's lib/ for the CLI itself. The Linux install tree now also carries the unversioned lib/libstdc++.so link that -lstdc++ resolves, and when that file exists next to the CLI, Main() appends -L and -Wl,-rpath, to the compiler arguments of a linking run, so that instrumented programs are linked against, and load, the shipped runtime. A build-tree CLI, which ships nothing, and a compile-only run are unchanged. The shipped_toolchain unit test covers the arguments with and without the shipped library. The release verification on a bare Ubuntu 22.04 image, which failed at link time, is the end-to-end check. Refs #19 --- CMakeLists.txt | 3 +++ src/runtime_analyzer.cpp | 16 ++++++++++++---- src/shipped_toolchain.cpp | 16 ++++++++++++++++ src/shipped_toolchain.hpp | 10 ++++++++++ unittests/shipped_toolchain_test.cpp | 19 +++++++++++++++++++ 5 files changed, 60 insertions(+), 4 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f725038..86e5fc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -218,6 +218,9 @@ if(BUILD_CLI) DIRECTORIES ${LLVM_LIBRARY_DIRS} LIBRARY DESTINATION lib ) + # Instrumented programs are linked against the shipped libstdc++ (-lstdc++ needs the + # unversioned name), as the runtime archives may use symbols the target's lacks. + install(CODE "file(CREATE_LINK libstdc++.so.6 \"\${CMAKE_INSTALL_PREFIX}/lib/libstdc++.so\" SYMBOLIC)") else() install(TARGETS ${TOOL_NAME} RUNTIME DESTINATION bin) endif() diff --git a/src/runtime_analyzer.cpp b/src/runtime_analyzer.cpp index 6b7ed40..4d5a558 100644 --- a/src/runtime_analyzer.cpp +++ b/src/runtime_analyzer.cpp @@ -985,10 +985,18 @@ namespace coretrace::runtime_analyzer return 0; } - // An installed CLI carries Clang's headers next to itself; a build-tree one does not, - // and coretrace-compiler then falls back on the clang it was configured with. - (void)UseShippedClangHeaders( - llvm::sys::fs::getMainExecutable(argv[0], reinterpret_cast(&Main))); + // An installed CLI carries Clang's headers and the C++ runtime next to itself; a + // build-tree one does not, and coretrace-compiler then falls back on the toolchain it + // was configured with. + const std::filesystem::path executable = + llvm::sys::fs::getMainExecutable(argv[0], reinterpret_cast(&Main)); + (void)UseShippedClangHeaders(executable); + if (!HasCompileOnlyAction(parsed.options.compiler_args)) + { + const std::vector link = ShippedLinkArguments(executable); + parsed.options.compiler_args.insert(parsed.options.compiler_args.end(), link.begin(), + link.end()); + } if (!parsed.ok) { std::cerr << "runtime-analyzer: " << parsed.error << '\n'; diff --git a/src/shipped_toolchain.cpp b/src/shipped_toolchain.cpp index 7d7023f..f618192 100644 --- a/src/shipped_toolchain.cpp +++ b/src/shipped_toolchain.cpp @@ -24,4 +24,20 @@ namespace coretrace::runtime_analyzer } return false; } + + std::vector ShippedLinkArguments(const std::filesystem::path& executable) + { + if (executable.empty()) + { + return {}; + } + std::error_code error; + const std::filesystem::path lib = + (executable.parent_path() / ".." / "lib").lexically_normal(); + if (!std::filesystem::is_regular_file(lib / "libstdc++.so", error)) + { + return {}; + } + return {"-L" + lib.string(), "-Wl,-rpath," + lib.string()}; + } } // namespace coretrace::runtime_analyzer diff --git a/src/shipped_toolchain.hpp b/src/shipped_toolchain.hpp index 13d5ecf..fb063c8 100644 --- a/src/shipped_toolchain.hpp +++ b/src/shipped_toolchain.hpp @@ -3,6 +3,8 @@ #define CORETRACE_RUNTIME_ANALYZER_SHIPPED_TOOLCHAIN_HPP #include +#include +#include namespace coretrace::runtime_analyzer { @@ -11,6 +13,14 @@ namespace coretrace::runtime_analyzer // resource directory is derived from that path. Returns whether it did. A CT_CLANG set by // the user keeps priority, and an empty executable path ships nothing. [[nodiscard]] bool UseShippedClangHeaders(const std::filesystem::path& executable); + + // Compiler arguments that link an instrumented program against the C++ runtime shipped + // next to the CLI, at /../lib: "-L" and "-Wl,-rpath," when that + // directory holds libstdc++.so, nothing otherwise. The runtime archives are built with a + // compiler newer than the target's may be, so its libstdc++ is shipped and linked instead + // of the system's. + [[nodiscard]] std::vector + ShippedLinkArguments(const std::filesystem::path& executable); } // namespace coretrace::runtime_analyzer #endif // CORETRACE_RUNTIME_ANALYZER_SHIPPED_TOOLCHAIN_HPP diff --git a/unittests/shipped_toolchain_test.cpp b/unittests/shipped_toolchain_test.cpp index 3fe6233..f925a8b 100644 --- a/unittests/shipped_toolchain_test.cpp +++ b/unittests/shipped_toolchain_test.cpp @@ -8,9 +8,11 @@ #include #include #include +#include namespace { + using coretrace::runtime_analyzer::ShippedLinkArguments; using coretrace::runtime_analyzer::UseShippedClangHeaders; int failures = 0; @@ -62,6 +64,22 @@ namespace unsetenv("CT_CLANG"); } + void LinksAgainstTheShippedCxxRuntime() + { + const std::filesystem::path executable = Layout("cxx", false); + const std::filesystem::path lib = + (executable.parent_path() / ".." / "lib").lexically_normal(); + std::filesystem::create_directories(lib); + std::ofstream(lib / "libstdc++.so") << "\n"; + const std::vector arguments = ShippedLinkArguments(executable); + Expect(arguments == + std::vector{"-L" + lib.string(), "-Wl,-rpath," + lib.string()}, + "shipped libstdc++: -L and -rpath on the shipped lib directory"); + Expect(ShippedLinkArguments(Layout("nocxx", false)).empty(), + "no shipped libstdc++: no link argument"); + Expect(ShippedLinkArguments({}).empty(), "unknown executable: no link argument"); + } + void DoesNothingWithoutHeaders() { unsetenv("CT_CLANG"); @@ -76,6 +94,7 @@ int main() { PointsAtItselfWhenHeadersAreShipped(); LeavesTheUsersClangAlone(); + LinksAgainstTheShippedCxxRuntime(); DoesNothingWithoutHeaders(); if (failures != 0) { From 66f5cb5ce0e730385f1175c4783db248a77bb4fc Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 26 Sep 2026 05:50:17 +0200 Subject: [PATCH 4/5] ci: build self-contained Linux archives and verify them on bare images Dockerfile.release follows coretrace's Dockerfile.release-binaries: an Ubuntu 22.04 base with GCC 13 and LLVM 20, so that the archive runs on its glibc and on every newer distribution; the build and the test suite; cmake --install --strip with the LLVM libraries, the runtime archives and Clang's headers; patchelf setting $ORIGIN on each shipped library and $ORIGIN/../lib on each executable (cc, installed by coretrace-compiler's rules, has no rpath of its own); an ldd check that nothing is left unresolved; then the archive unpacked and run on bare Ubuntu 22.04, Debian 12, Rocky Linux 9 and Ubuntu 24.04 images with only g++ or gcc-c++ installed and no clang or LLVM, where test/release/check-analysis.sh expects the heap overflow of heap_overflow_write.c to be reported with exit status 1. The artifacts are exported only when every stage passes. The Release workflow builds the archive for amd64 and arm64 on native runners, on v* tags, on demand, and on pull requests touching the release files; on a tag it checks that the tag matches the project version and attaches the archives and their .sha256 to the GitHub Release, named runtime-analyzer--linux-.tar.gz. Refs #19 --- .dockerignore | 5 ++ .github/workflows/release.yml | 129 ++++++++++++++++++++------- Dockerfile.release | 157 +++++++++++++++++++++++++++++++++ test/release/check-analysis.sh | 36 ++++++++ 4 files changed, 293 insertions(+), 34 deletions(-) create mode 100644 .dockerignore create mode 100644 Dockerfile.release create mode 100755 test/release/check-analysis.sh diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..068353d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,5 @@ +build/ +build-*/ +runtime-analyzer-artifacts/ +.git/ +__pycache__/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 7311746..fae8260 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,30 +1,58 @@ # SPDX-License-Identifier: Apache-2.0 name: Release -# On a v* tag: check that the tag matches the project() version, build and test as the Build -# workflow does, then attach the installed tree as runtime-analyzer-linux-x86_64.tar.gz, with -# its SHA256SUMS, to the GitHub Release of the tag. +# Builds the self-contained Linux archives with Dockerfile.release (Ubuntu 22.04 base, LLVM +# libraries and Clang headers shipped, verified on bare images) for amd64 and arm64, on every +# change to the release files and on demand. On a v* tag, the tag must match the project() +# version, and the archives with their checksums are attached to the GitHub Release. on: push: tags: ["v*"] + pull_request: + paths: + - Dockerfile.release + - .dockerignore + - .github/workflows/release.yml + - test/release/** + - cmake/** + - CMakeLists.txt + - src/** + - include/** + workflow_dispatch: + +concurrency: + group: release-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true permissions: - contents: write + contents: read env: - LLVM_VERSION: 20 - ARCHIVE: runtime-analyzer-linux-x86_64.tar.gz + PROJECT_NAME: runtime-analyzer jobs: - release: - name: Build, test and attach the Linux x86_64 tarball - runs-on: ubuntu-24.04 + build-archives: + name: Build the Linux archive (${{ matrix.arch }}) + # Each architecture builds on a native runner: emulating arm64 on an x86 host takes far + # longer for an LLVM-linked tool. + runs-on: ${{ matrix.runner }} + strategy: + fail-fast: false + matrix: + include: + - platform: linux/amd64 + arch: amd64 + runner: ubuntu-24.04 + - platform: linux/arm64 + arch: arm64 + runner: ubuntu-24.04-arm steps: - uses: actions/checkout@v4 - name: Check that the tag matches the project version + if: github.ref_type == 'tag' run: | version="$(sed -n 's/^project(.* VERSION \([0-9][0-9.]*\).*/\1/p' CMakeLists.txt)" test -n "$version" || { echo "no project() VERSION in CMakeLists.txt"; exit 1; } @@ -33,39 +61,72 @@ jobs: exit 1 fi - - name: Install LLVM ${{ env.LLVM_VERSION }} - run: | - sudo apt-get update - sudo apt-get install -y build-essential cmake ninja-build - wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh - sudo bash /tmp/llvm.sh "$LLVM_VERSION" - sudo apt-get install -y "llvm-$LLVM_VERSION-dev" "libclang-$LLVM_VERSION-dev" "clang-$LLVM_VERSION" - echo "LLVM_DIR=/usr/lib/llvm-$LLVM_VERSION/lib/cmake/llvm" >> "$GITHUB_ENV" - echo "Clang_DIR=/usr/lib/llvm-$LLVM_VERSION/lib/cmake/clang" >> "$GITHUB_ENV" - - - name: Configure + - name: Resolve the artifact version + id: version run: | - cmake -S . -B build -G Ninja -DCMAKE_BUILD_TYPE=Release \ - -DLLVM_DIR="$LLVM_DIR" -DClang_DIR="$Clang_DIR" + if [ "$GITHUB_REF_TYPE" = "tag" ]; then + echo "value=$GITHUB_REF_NAME" >> "$GITHUB_OUTPUT" + else + echo "value=sha-$(git rev-parse --short=12 HEAD)" >> "$GITHUB_OUTPUT" + fi - - name: Build - run: cmake --build build --parallel + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 - - name: Test - run: ctest --test-dir build --output-on-failure + - name: Build, verify and export the archive + run: | + set -euo pipefail + out_dir="dist/${{ matrix.arch }}" + mkdir -p "$out_dir" + docker buildx build \ + --platform "${{ matrix.platform }}" \ + --file Dockerfile.release \ + --target artifacts \ + --build-arg VERSION="${{ steps.version.outputs.value }}" \ + --build-arg PROJECT_NAME="$PROJECT_NAME" \ + --output "type=local,dest=$out_dir" \ + . + ls -la "$out_dir" - - name: Package + - name: Validate the artifact files run: | - cmake --install build --prefix dist/runtime-analyzer - tar -C dist -czf "$ARCHIVE" runtime-analyzer - sha256sum "$ARCHIVE" > SHA256SUMS - cat SHA256SUMS + set -euo pipefail + dir="dist/${{ matrix.arch }}" + archive="$dir/$PROJECT_NAME-${{ steps.version.outputs.value }}-linux-${{ matrix.arch }}.tar.gz" + test -f "$archive" || { echo "missing archive: $archive"; exit 1; } + test -f "$archive.sha256" || { echo "missing checksum: $archive.sha256"; exit 1; } + (cd "$dir" && sha256sum -c "$(basename "$archive.sha256")") + + - name: Upload the workflow artifact + uses: actions/upload-artifact@v4 + with: + name: ${{ env.PROJECT_NAME }}-${{ steps.version.outputs.value }}-linux-${{ matrix.arch }} + path: | + dist/${{ matrix.arch }}/*.tar.gz + dist/${{ matrix.arch }}/*.sha256 + if-no-files-found: error + + publish: + name: Attach the archives to the GitHub Release + runs-on: ubuntu-24.04 + needs: build-archives + if: github.ref_type == 'tag' + permissions: + contents: write + + steps: + - name: Download the archives + uses: actions/download-artifact@v4 + with: + pattern: ${{ env.PROJECT_NAME }}-*-linux-* + path: dist + merge-multiple: true - - name: Attach to the GitHub Release + - name: Publish the release assets uses: softprops/action-gh-release@v2 with: files: | - ${{ env.ARCHIVE }} - SHA256SUMS + dist/*.tar.gz + dist/*.sha256 fail_on_unmatched_files: true generate_release_notes: true diff --git a/Dockerfile.release b/Dockerfile.release new file mode 100644 index 0000000..b3535a5 --- /dev/null +++ b/Dockerfile.release @@ -0,0 +1,157 @@ +# syntax=docker/dockerfile:1.7 +# SPDX-License-Identifier: Apache-2.0 + +# The archive is built on Ubuntu 22.04 so that it runs on its glibc and on every newer +# distribution: a binary only needs a glibc at least as old as the one it was linked against. +# Its own toolchain is too old for coretrace-compiler's runtime (C++20 needs GCC 13), +# so GCC 13 is installed alongside; the libstdc++ it brings is shipped in the archive's lib/. +FROM ubuntu:22.04 AS base + +ARG DEBIAN_FRONTEND=noninteractive +ARG LLVM_VERSION=20 +ARG GCC_VERSION=13 + +RUN apt-get update && apt-get install -y --no-install-recommends \ + ca-certificates \ + curl \ + gnupg \ + lsb-release \ + software-properties-common \ + wget \ + build-essential \ + ninja-build \ + git \ + tar \ + patchelf \ + python3 \ + python3-venv \ + && add-apt-repository -y ppa:ubuntu-toolchain-r/test \ + && apt-get update && apt-get install -y --no-install-recommends \ + gcc-${GCC_VERSION} \ + g++-${GCC_VERSION} \ + && rm -rf /var/lib/apt/lists/* + +ENV CC=gcc-${GCC_VERSION} CXX=g++-${GCC_VERSION} + +# LLVM and Clang from apt.llvm.org, with the CMake packages compilerlib needs. +RUN wget -qO /tmp/llvm.sh https://apt.llvm.org/llvm.sh \ + && bash /tmp/llvm.sh ${LLVM_VERSION} \ + && apt-get install -y --no-install-recommends \ + llvm-${LLVM_VERSION}-dev \ + libclang-${LLVM_VERSION}-dev \ + clang-${LLVM_VERSION} \ + && rm -rf /var/lib/apt/lists/* /tmp/llvm.sh + +# Ubuntu 22.04's CMake is 3.22; the project and coretrace-compiler want newer features. +ARG CMAKE_VERSION=3.31.6 +RUN python3 -m venv /opt/build-tools \ + && /opt/build-tools/bin/pip install --no-cache-dir cmake==${CMAKE_VERSION} +ENV PATH=/opt/build-tools/bin:${PATH} + +WORKDIR /repo + +FROM base AS builder + +ARG LLVM_VERSION=20 + +COPY . /repo + +RUN cmake -S . -B build -G Ninja \ + -DCMAKE_BUILD_TYPE=Release \ + -DLLVM_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/llvm \ + -DClang_DIR=/usr/lib/llvm-${LLVM_VERSION}/lib/cmake/clang \ + && cmake --build build -j"$(nproc)" \ + && ctest --test-dir build --output-on-failure + +FROM builder AS packager + +ARG VERSION=dev +ARG TARGETARCH +ARG PROJECT_NAME=runtime-analyzer + +SHELL ["/bin/bash", "-o", "pipefail", "-c"] +RUN set -eux; \ + case "${TARGETARCH}" in \ + amd64|arm64) arch_label="${TARGETARCH}" ;; \ + *) echo "unsupported TARGETARCH: ${TARGETARCH}" >&2; exit 1 ;; \ + esac; \ + package_name="${PROJECT_NAME}-${VERSION}-linux-${arch_label}"; \ + staging="/staging/${package_name}"; \ + \ + # bin/ and lib/ through CMake: the CLI, the LLVM libraries it links, the runtime archives \ + # and Clang's headers. \ + cmake --install build --prefix "${staging}" --strip; \ + \ + # The shipped libraries resolve each other through their own directory, and every \ + # executable resolves them through ../lib (cc, installed by coretrace-compiler's rules, \ + # carries no rpath of its own). \ + find "${staging}/lib" -name '*.so*' -type f -exec patchelf --set-rpath '$ORIGIN' {} \; ; \ + find "${staging}/bin" -type f -exec patchelf --set-rpath '$ORIGIN/../lib' {} \; ; \ + \ + # Everything resolves through the rpath alone: no LD_LIBRARY_PATH, no system LLVM. \ + for exe in "${staging}"/bin/*; do ldd "${exe}" | { ! grep 'not found'; }; done; \ + \ + install -m 0644 /repo/README.md /repo/LICENSE /repo/NOTICE "${staging}/"; \ + \ + mkdir -p /out; \ + tar -C /staging -czf "/out/${package_name}.tar.gz" "${package_name}"; \ + (cd /out && sha256sum "${package_name}.tar.gz" > "${package_name}.tar.gz.sha256"); \ + rm -rf /staging + +# The archive as users receive it, unpacked once and then run on the oldest distributions it +# targets (Ubuntu 22.04, Debian 12, RHEL 9) and on the newest, each image bare: no clang, no +# LLVM. Only a C++ build environment is added, as the analyzer links each instrumented program +# against its C++ runtime, and a C program is analyzed with the shipped Clang headers. Each +# stage depends on the previous one, so the artifacts are only exported when all of them pass. + +FROM base AS unpacked + +COPY --from=packager /out/ /out/ +RUN mkdir /opt/runtime-analyzer \ + && tar -xzf /out/*.tar.gz -C /opt/runtime-analyzer --strip-components=1 + +FROM ubuntu:22.04 AS verify-ubuntu-22.04 + +COPY --from=unpacked /out/ /out/ +COPY --from=unpacked /opt/runtime-analyzer/ /opt/runtime-analyzer/ +COPY test/release/ /tests/release/ +COPY test/findings/heap_overflow_write.c /tests/findings/ +RUN set -eux; \ + apt-get update && apt-get install -y --no-install-recommends g++; \ + if command -v clang || ls /usr/lib/llvm-*; then echo "the verification image must not have clang" >&2; exit 1; fi; \ + /opt/runtime-analyzer/bin/runtime-analyzer --version; \ + sh /tests/release/check-analysis.sh /opt/runtime-analyzer /tmp/check + +FROM debian:12 AS verify-debian-12 + +COPY --from=verify-ubuntu-22.04 /out/ /out/ +COPY --from=unpacked /opt/runtime-analyzer/ /opt/runtime-analyzer/ +COPY test/release/ /tests/release/ +COPY test/findings/heap_overflow_write.c /tests/findings/ +RUN set -eux; \ + apt-get update && apt-get install -y --no-install-recommends g++; \ + sh /tests/release/check-analysis.sh /opt/runtime-analyzer /tmp/check + +FROM rockylinux/rockylinux:9 AS verify-rhel-9 + +COPY --from=verify-debian-12 /out/ /out/ +COPY --from=unpacked /opt/runtime-analyzer/ /opt/runtime-analyzer/ +COPY test/release/ /tests/release/ +COPY test/findings/heap_overflow_write.c /tests/findings/ +RUN set -eux; \ + dnf install -y gcc-c++; \ + sh /tests/release/check-analysis.sh /opt/runtime-analyzer /tmp/check + +FROM ubuntu:24.04 AS verify-ubuntu-24.04 + +COPY --from=verify-rhel-9 /out/ /out/ +COPY --from=unpacked /opt/runtime-analyzer/ /opt/runtime-analyzer/ +COPY test/release/ /tests/release/ +COPY test/findings/heap_overflow_write.c /tests/findings/ +RUN set -eux; \ + apt-get update && apt-get install -y --no-install-recommends g++; \ + sh /tests/release/check-analysis.sh /opt/runtime-analyzer /tmp/check + +FROM scratch AS artifacts + +COPY --from=verify-ubuntu-24.04 /out/ / diff --git a/test/release/check-analysis.sh b/test/release/check-analysis.sh new file mode 100755 index 0000000..f9d426a --- /dev/null +++ b/test/release/check-analysis.sh @@ -0,0 +1,36 @@ +#!/bin/sh +# SPDX-License-Identifier: Apache-2.0 +# +# Checks that an installation laid out as a release archive analyzes a C program without an +# installed clang or LLVM: the CLI runs with an empty environment and a PATH holding only the +# system's linker, so it must compile the program with the Clang headers shipped next to it, +# link it against the runtime archives shipped next to it, run it, and report its heap overflow. +# Only a C++ build environment (g++ or gcc-c++: linker, C library and libstdc++ development +# files) comes from the system, as for building any C++ program. +# +# Usage: check-analysis.sh +set -eu + +prefix=$1 +work=$2 +here=$(cd "$(dirname "$0")" && pwd) + +rm -rf "$work" +mkdir -p "$work" +cp "$here/../findings/heap_overflow_write.c" "$work/heap_overflow_write.c" + +set +e +output=$(cd "$work" && env -i PATH=/usr/bin:/bin "$prefix/bin/runtime-analyzer" \ + -o heap_overflow_write -- --ct-modules=alloc,bounds heap_overflow_write.c 2>&1) +status=$? +set -e +printf '%s\n' "$output" + +fail() { + echo "FAIL: $1" >&2 + exit 1 +} +[ "$status" -eq 1 ] || fail "expected exit status 1 (findings), got $status" +printf '%s\n' "$output" | grep -q 'heap_overflow_write.c:7:15: error: heap-buffer-overflow WRITE of size 4' || + fail "the heap overflow of heap_overflow_write.c is not reported" +echo "PASS: runtime-analyzer analyzes C from $prefix without an installed clang" From 1eb68c2d57f8b777884d1f35e2fd67fb08167970 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sat, 26 Sep 2026 05:50:17 +0200 Subject: [PATCH 5/5] docs: describe the self-contained Linux archives and what they still need Closes #19 --- README.md | 41 ++++++++++++++++++++++++----------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index bef5cb1..ef915db 100644 --- a/README.md +++ b/README.md @@ -36,24 +36,26 @@ static analyzers report paths from. ### From a release -Each [release](https://github.com/CoreTrace/coretrace-runtime-analyzer/releases) carries -`runtime-analyzer-linux-x86_64.tar.gz` and its `SHA256SUMS`: +Each [release](https://github.com/CoreTrace/coretrace-runtime-analyzer/releases) carries a +self-contained Linux archive per architecture, `runtime-analyzer--linux-amd64.tar.gz` +and `runtime-analyzer--linux-arm64.tar.gz`, each with its `.sha256`: ```zsh -sha256sum -c SHA256SUMS -tar -xzf runtime-analyzer-linux-x86_64.tar.gz -./runtime-analyzer/bin/runtime-analyzer --version +sha256sum -c runtime-analyzer-v0.1.0-linux-amd64.tar.gz.sha256 +tar -xzf runtime-analyzer-v0.1.0-linux-amd64.tar.gz +./runtime-analyzer-v0.1.0-linux-amd64/bin/runtime-analyzer --version ``` -The tree holds `bin/runtime-analyzer`, the instrumentation runtime archives in `lib/` that -instrumented programs are linked against, and the library with its headers. The CLI finds the -archives relative to its own location, so the tree can be moved. At run time it needs: +The tree holds `bin/runtime-analyzer`, the LLVM libraries it links and the instrumentation +runtime archives in `lib/`, Clang's own headers in `lib/clang//include`, and the +library with its headers. Everything is found relative to the CLI's own location, so the tree can +be moved. No clang and no LLVM have to be installed: the CLI points coretrace-compiler at the +shipped headers unless `CT_CLANG` names another clang. -- the LLVM 20 shared libraries it was built against, `libLLVM.so.20.1` and `libclang-cpp.so.20.1` - (on Debian and Ubuntu, the `libllvm20` and `libclang-cpp20` packages from - [apt.llvm.org](https://apt.llvm.org)); -- a `clang` to link instrumented programs: `clang-20`, `clang` or `clang++` on `PATH`, or the - executable `CT_CLANG` names. +The archive is built on Ubuntu 22.04 and runs on it and on every newer distribution (Debian 12, +RHEL 9 and rebuilds, Ubuntu 24.04...). What stays the system's is a C++ build environment to +link each instrumented program against the runtime, which is C++: `g++` on Debian and Ubuntu, +`gcc-c++` on RHEL, as for building any C++ program. ### From source @@ -223,7 +225,8 @@ ctest --test-dir build --output-on-failure | `sweep_runs` | every binary runs without a timeout (`--test-dir test --timeout 10 -- --ct-modules=alloc`) | | `version` | `--version` prints the `project()` version | | `llvm_floor` | configuration fails against an LLVM below the floor | -| `install_layout` | `cmake --install` lays out the CLI, library and headers, and the installed CLI builds and runs a program | +| `shipped_toolchain_unit` | the CLI points coretrace-compiler at the Clang headers shipped next to it, unless `CT_CLANG` is set | +| `install_layout` | `cmake --install` lays out the CLI, library, headers and Clang headers, and the installed CLI builds and runs a program | The `Build` workflow runs this suite on Ubuntu 24.04 and macOS against LLVM 20 for every push and pull request. `clang-format` (version 17) is checked by the `clang-format` workflow. @@ -233,9 +236,13 @@ pull request. `clang-format` (version 17) is checked by the `clang-format` workf 1. Set the version in `project(... VERSION x.y.z ...)` in `CMakeLists.txt` and merge it. 2. Tag the commit `vx.y.z` and push the tag. -The `Release` workflow checks that the tag matches the project version, builds and tests on -Ubuntu 24.04, installs the tree with `cmake --install`, and attaches -`runtime-analyzer-linux-x86_64.tar.gz` with `SHA256SUMS` to the GitHub Release. +The `Release` workflow checks that the tag matches the project version, then builds the +archives with `Dockerfile.release` for amd64 and arm64: an Ubuntu 22.04 base with GCC 13 and +LLVM 20, the test suite, `cmake --install` with the LLVM libraries and Clang headers shipped, +`patchelf` for the rpaths, and a run of `test/release/check-analysis.sh` on bare Ubuntu 22.04, +Debian 12, Rocky Linux 9 and Ubuntu 24.04 images with only `g++` installed. The archives and +their `.sha256` are attached to the GitHub Release. The same build runs on every pull request +that touches the release files, without publishing. ## License