[libc-commits] [libc] [libc] Enabling code coverage via Linux syscalls (PR #213271)
Tapiwa Gonga via libc-commits
libc-commits at lists.llvm.org
Fri Aug 7 03:23:24 PDT 2026
https://github.com/tapiwagonga updated https://github.com/llvm/llvm-project/pull/213271
>From fe3b3d91b05b46451f2e98f7ebffd2d8e0808bdd Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Fri, 7 Aug 2026 10:22:56 +0000
Subject: [PATCH] [libc][test] Add freestanding code coverage support to
LibcTestMain
---
libc/CMakeLists.txt | 104 ++++++++------
libc/test/UnitTest/ExecuteFunctionUnix.cpp | 39 ++++++
libc/test/UnitTest/LibcTestMain.cpp | 156 ++++++++++++++++++++-
3 files changed, 254 insertions(+), 45 deletions(-)
diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index 7e42ba2a76df2..babb0e73e686c 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -91,52 +91,81 @@ endif()
set(LIBC_LINK_OPTIONS_DEFAULT "" CACHE STRING "Arguments used when linking.")
set(LIBC_TEST_LINK_OPTIONS_DEFAULT "" CACHE STRING "Common link options for all the tests.")
+option(LLVM_LIBC_ENABLE_COVERAGE "Build libc with coverage instrumentation" OFF)
+if(LLVM_LIBC_ENABLE_COVERAGE)
+ list(APPEND LIBC_COMPILE_OPTIONS_DEFAULT "-fprofile-instr-generate" "-fcoverage-mapping")
+ list(APPEND LIBC_TEST_COMPILE_OPTIONS_DEFAULT "-fprofile-instr-generate" "-fcoverage-mapping")
+ list(APPEND LIBC_TEST_LINK_OPTIONS_DEFAULT "-fprofile-instr-generate" "-fcoverage-mapping")
+
+ # When building with -nostdlib, the compiler does not automatically link the profiling runtime.
+ # We must explicitly query the compiler for the exact architecture-specific profile library path.
+ execute_process(
+ COMMAND ${CMAKE_CXX_COMPILER} --print-libgcc-file-name --rtlib=compiler-rt
+ OUTPUT_VARIABLE COMPILER_RT_BUILTINS
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ RESULT_VARIABLE COMPILER_RT_RETURN_CODE
+ )
+ if(COMPILER_RT_RETURN_CODE EQUAL 0)
+ string(REPLACE "builtins" "profile" COMPILER_RT_PROFILE "${COMPILER_RT_BUILTINS}")
+ if(EXISTS "${COMPILER_RT_PROFILE}")
+ list(APPEND LIBC_TEST_LINK_OPTIONS_DEFAULT "${COMPILER_RT_PROFILE}")
+ else()
+ message(WARNING "Coverage profiling runtime not found at ${COMPILER_RT_PROFILE}")
+ endif()
+ else()
+ message(WARNING "Failed to locate compiler-rt builtins library for coverage")
+ endif()
+endif()
+
set(LIBC_TEST_CMD "" CACHE STRING
"The full test command in the form <command> binary=@BINARY@, if using another program to test (e.g. QEMU)")
set(LIBC_TEST_HERMETIC_ONLY "" OFF CACHE BOOL "Only enable hermetic tests.")
list(APPEND LIBC_COMPILE_OPTIONS_DEFAULT ${LIBC_COMMON_TUNE_OPTIONS})
-# Check --print-resource-dir to find the compiler resource dir if this flag
-# is supported by the compiler.
-execute_process(
- OUTPUT_STRIP_TRAILING_WHITESPACE
- COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir
- RESULT_VARIABLE COMMAND_RETURN_CODE
- OUTPUT_VARIABLE COMPILER_RESOURCE_DIR
-)
-# Retrieve the host compiler's resource dir.
-if(COMMAND_RETURN_CODE EQUAL 0)
- set(COMPILER_RESOURCE_DIR
- "${COMPILER_RESOURCE_DIR}" CACHE PATH "path to compiler resource dir"
- )
- message(STATUS "Set COMPILER_RESOURCE_DIR to "
- "${COMPILER_RESOURCE_DIR} using --print-resource-dir")
-else()
- # Try with GCC option: -print-search-dirs, which will output in the form:
- # install: <path>
- # programs: ........
- # So we try to capture the <path> after "install: " in the first line of the
- # output.
+if(NOT DEFINED COMPILER_RESOURCE_DIR)
execute_process(
OUTPUT_STRIP_TRAILING_WHITESPACE
- COMMAND ${CMAKE_CXX_COMPILER} -print-search-dirs
+ COMMAND ${CMAKE_CXX_COMPILER} --print-resource-dir
RESULT_VARIABLE COMMAND_RETURN_CODE
- OUTPUT_VARIABLE COMPILER_RESOURCE_DIR
+ OUTPUT_VARIABLE COMPILER_RESOURCE_DIR_RAW
)
+ # Retrieve the host compiler's resource dir.
if(COMMAND_RETURN_CODE EQUAL 0)
- string(REPLACE " " ";" COMPILER_RESOURCE_DIR ${COMPILER_RESOURCE_DIR})
- string(REPLACE "\n" ";" COMPILER_RESOURCE_DIR "${COMPILER_RESOURCE_DIR}")
- list(GET COMPILER_RESOURCE_DIR 1 COMPILER_RESOURCE_DIR)
+ set(COMPILER_RESOURCE_DIR
+ "${COMPILER_RESOURCE_DIR_RAW}" CACHE PATH "path to compiler resource dir"
+ )
message(STATUS "Set COMPILER_RESOURCE_DIR to "
- "${COMPILER_RESOURCE_DIR} using --print-search-dirs")
-else()
- if (LIBC_TARGET_OS_IS_GPU)
- message(FATAL_ERROR "COMPILER_RESOURCE_DIR must be set for GPU builds")
+ "${COMPILER_RESOURCE_DIR} using --print-resource-dir")
+ else()
+ # Try with GCC option: -print-search-dirs, which will output in the form:
+ # install: <path>
+ # programs: ........
+ # So we try to capture the <path> after "install: " in the first line of the
+ # output.
+ execute_process(
+ OUTPUT_STRIP_TRAILING_WHITESPACE
+ COMMAND ${CMAKE_CXX_COMPILER} -print-search-dirs
+ RESULT_VARIABLE COMMAND_RETURN_CODE
+ OUTPUT_VARIABLE COMPILER_RESOURCE_DIR_RAW
+ )
+ if(COMMAND_RETURN_CODE EQUAL 0)
+ string(REPLACE " " ";" COMPILER_RESOURCE_DIR_RAW ${COMPILER_RESOURCE_DIR_RAW})
+ string(REPLACE "\n" ";" COMPILER_RESOURCE_DIR_RAW "${COMPILER_RESOURCE_DIR_RAW}")
+ list(GET COMPILER_RESOURCE_DIR_RAW 1 COMPILER_RESOURCE_DIR_RAW)
+ set(COMPILER_RESOURCE_DIR
+ "${COMPILER_RESOURCE_DIR_RAW}" CACHE PATH "path to compiler resource dir"
+ )
+ message(STATUS "Set COMPILER_RESOURCE_DIR to "
+ "${COMPILER_RESOURCE_DIR} using --print-search-dirs")
else()
- set(COMPILER_RESOURCE_DIR OFF)
- message(STATUS "COMPILER_RESOURCE_DIR not set
- --print-resource-dir not supported by host compiler")
+ if (LIBC_TARGET_OS_IS_GPU)
+ message(FATAL_ERROR "COMPILER_RESOURCE_DIR must be set for GPU builds")
+ else()
+ set(COMPILER_RESOURCE_DIR OFF CACHE PATH "path to compiler resource dir")
+ message(STATUS "COMPILER_RESOURCE_DIR not set
+ --print-resource-dir not supported by host compiler")
+ endif()
endif()
endif()
endif()
@@ -152,11 +181,6 @@ if(LIBC_TARGET_OS_IS_GPU)
endif()
option(LLVM_LIBC_FULL_BUILD "Build and test LLVM libc as if it is the full libc" ${default_to_full_build})
-if(LLVM_LIBC_FULL_BUILD)
- set(LLVM_LIBC_OVERLAY OFF)
-else()
- set(LLVM_LIBC_OVERLAY ON)
-endif()
option(LLVM_LIBC_IMPLEMENTATION_DEFINED_TEST_BEHAVIOR "Build LLVM libc tests assuming our implementation-defined behavior" ON)
option(LLVM_LIBC_ENABLE_LINTING "Enables linting of libc source files" OFF)
option(LLVM_LIBC_ALL_HEADERS "Outputs all functions in header files, regardless of whether they are enabled on this target" OFF)
@@ -298,10 +322,6 @@ endif()
if(LIBC_TARGET_OS_IS_GPU)
include(prepare_libc_gpu_build)
set(LIBC_ENABLE_UNITTESTS OFF)
- find_program(LIBC_LLVM_LINK llvm-link HINTS ${LLVM_TOOLS_BINARY_DIR})
- if(NOT LIBC_LLVM_LINK)
- message(FATAL_ERROR "llvm-link not found in ${LLVM_TOOLS_BINARY_DIR} or system path")
- endif()
elseif(LIBC_TARGET_OS_IS_BAREMETAL)
set(LIBC_ENABLE_UNITTESTS OFF)
endif()
diff --git a/libc/test/UnitTest/ExecuteFunctionUnix.cpp b/libc/test/UnitTest/ExecuteFunctionUnix.cpp
index a07c92f61225c..402c59cdbcb3c 100644
--- a/libc/test/UnitTest/ExecuteFunctionUnix.cpp
+++ b/libc/test/UnitTest/ExecuteFunctionUnix.cpp
@@ -18,6 +18,7 @@
#include "include/llvm-libc-types/struct_pollfd.h"
#include "src/poll/poll.h"
#include "src/signal/kill.h"
+#include "src/signal/sigaction.h"
#include "src/stdio/fflush.h"
#include "src/stdio/stderr.h"
#include "src/stdio/stdout.h"
@@ -26,6 +27,7 @@
#include "src/sys/wait/waitpid.h"
#include "src/unistd/close.h"
#include "src/unistd/fork.h"
+#include "src/unistd/getpid.h"
#include "src/unistd/pipe.h"
#define LIBC_IMPL LIBC_NAMESPACE
@@ -42,6 +44,8 @@
#define LIBC_IMPL
#endif
+extern "C" __attribute__((weak)) void write_raw_profile();
+
namespace LIBC_NAMESPACE_DECL {
namespace testutils {
@@ -58,6 +62,23 @@ int ProcessStatus::get_fatal_signal() {
return WTERMSIG(platform_defined);
}
+static void coverage_fatal_signal_handler(int sig) {
+ if (write_raw_profile)
+ write_raw_profile();
+
+ // Restore default signal handler
+#ifdef LIBC_FULL_BUILD
+ struct sigaction sa = {};
+ sa.sa_handler = SIG_DFL;
+ LIBC_IMPL::sigaction(sig, &sa, nullptr);
+#else
+ ::signal(sig, SIG_DFL);
+#endif
+
+ // Re-raise the signal
+ LIBC_IMPL::kill(LIBC_IMPL::getpid(), sig);
+}
+
ProcessStatus invoke_in_subprocess(FunctionCaller *func, int timeout_ms) {
int pipe_fds[2];
if (LIBC_IMPL::pipe(pipe_fds) == -1) {
@@ -75,8 +96,26 @@ ProcessStatus invoke_in_subprocess(FunctionCaller *func, int timeout_ms) {
}
if (!pid) {
+#ifdef LIBC_FULL_BUILD
+ struct sigaction sa = {};
+ sa.sa_handler = coverage_fatal_signal_handler;
+ LIBC_IMPL::sigaction(SIGABRT, &sa, nullptr);
+ LIBC_IMPL::sigaction(SIGSEGV, &sa, nullptr);
+ LIBC_IMPL::sigaction(SIGILL, &sa, nullptr);
+ LIBC_IMPL::sigaction(SIGFPE, &sa, nullptr);
+ LIBC_IMPL::sigaction(SIGBUS, &sa, nullptr);
+#else
+ ::signal(SIGABRT, coverage_fatal_signal_handler);
+ ::signal(SIGSEGV, coverage_fatal_signal_handler);
+ ::signal(SIGILL, coverage_fatal_signal_handler);
+ ::signal(SIGFPE, coverage_fatal_signal_handler);
+ ::signal(SIGBUS, coverage_fatal_signal_handler);
+#endif
+
(*func)();
delete func;
+ if (write_raw_profile)
+ write_raw_profile();
LIBC_IMPL::exit(0);
}
LIBC_IMPL::close(pipe_fds[1]);
diff --git a/libc/test/UnitTest/LibcTestMain.cpp b/libc/test/UnitTest/LibcTestMain.cpp
index c348d5ef1aa1b..2b17e41a05e95 100644
--- a/libc/test/UnitTest/LibcTestMain.cpp
+++ b/libc/test/UnitTest/LibcTestMain.cpp
@@ -43,8 +43,155 @@ TestOptions parseOptions(int argc, char **argv) {
} // anonymous namespace
-// The C++ standard forbids declaring the main function with a linkage specifier
-// outisde of 'freestanding' mode, only define the linkage for hermetic tests.
+#include "src/__support/macros/properties/os.h"
+#if defined(LIBC_TARGET_OS_IS_LINUX)
+#include "hdr/errno_macros.h"
+#include "hdr/fcntl_macros.h"
+#include "hdr/sys_mman_macros.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/close.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/mmap.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/munmap.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/open.h"
+#include "src/__support/OSUtil/linux/syscall_wrappers/write.h"
+#include "src/__support/OSUtil/syscall.h"
+#include "src/__support/CPP/optional.h"
+#include "src/__support/CPP/span.h"
+#include "src/__support/integer_to_string.h"
+#include "src/string/memory_utils/inline_memcpy.h"
+#include <sys/syscall.h>
+
+//===----------------------------------------------------------------------===//
+// Freestanding Linux Code Coverage Profile Writer
+//
+// Freestanding (-nostdlib) libc binaries cannot link standard compiler-rt
+// file I/O (fopen/fwrite). Here we override compiler-rt's default filename
+// to "/dev/null" and invoke write_raw_profile() directly before main()
+// returns (and within death test subprocesses in ExecuteFunctionUnix.cpp)
+// to dump raw coverage counters (libc_cov_<pid>.profraw) using direct Linux
+// system calls (SYS_mmap, SYS_openat, SYS_write, SYS_close, SYS_munmap).
+//===----------------------------------------------------------------------===//
+extern "C" {
+__attribute__((weak)) uint64_t __llvm_profile_get_size_for_buffer();
+__attribute__((weak)) int __llvm_profile_write_buffer(char *buffer);
+__attribute__((weak)) void
+__llvm_profile_set_filename(const char *filename_pat);
+
+// Override compiler-rt's weak filename symbol. This redirects the default
+// filename to /dev/null to silence the default dumper by default.
+__attribute__((weak)) char __llvm_profile_filename[] = "/dev/null";
+}
+
+namespace {
+struct FixedSizeBuffer {
+ char data[64];
+ size_t idx = 0;
+
+ FixedSizeBuffer() { data[0] = '\0'; }
+
+ bool append(string_view str) {
+ size_t len = str.size();
+ if (idx + len >= sizeof(data))
+ return false;
+ LIBC_NAMESPACE::inline_memcpy(data + idx, str.data(), len);
+ idx += len;
+ data[idx] = '\0';
+ return true;
+ }
+
+ template <size_t N> bool append(const char (&str)[N]) {
+ size_t len = N - 1;
+ if (idx + len >= sizeof(data))
+ return false;
+ LIBC_NAMESPACE::inline_memcpy(data + idx, str, len);
+ idx += len;
+ data[idx] = '\0';
+ return true;
+ }
+};
+
+LIBC_INLINE void report_error(string_view msg) {
+ LIBC_NAMESPACE::syscall_impl<long>(SYS_write, 2, msg.data(), msg.size());
+}
+} // anonymous namespace
+
+extern "C" void write_raw_profile() {
+ if (!__llvm_profile_get_size_for_buffer || !__llvm_profile_write_buffer)
+ return;
+
+ size_t required_size =
+ static_cast<size_t>(__llvm_profile_get_size_for_buffer());
+ if (required_size == 0)
+ return;
+
+ auto mmap_or_error = LIBC_NAMESPACE::linux_syscalls::mmap(
+ nullptr, required_size, PROT_READ | PROT_WRITE,
+ MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+ if (!mmap_or_error)
+ return report_error("error: libc coverage failed to mmap buffer\n");
+ char *profile_buffer = static_cast<char *>(mmap_or_error.value());
+
+ if (__llvm_profile_write_buffer(profile_buffer) != 0) {
+ LIBC_NAMESPACE::linux_syscalls::munmap(profile_buffer, required_size);
+ return report_error(
+ "error: libc coverage failed to write profile buffer\n");
+ }
+
+ // Create a minimal filename: libc_cov_<pid>.profraw
+ long pid = LIBC_NAMESPACE::syscall_impl<long>(SYS_getpid);
+ if (pid <= 0)
+ pid = 1;
+
+ FixedSizeBuffer filename;
+ char pid_buf[LIBC_NAMESPACE::IntegerToString<long>::buffer_size()];
+ auto pid_str = LIBC_NAMESPACE::IntegerToString<long>::format_to(pid_buf, pid);
+ if (!pid_str || !filename.append("libc_cov_") || !filename.append(*pid_str) ||
+ !filename.append(".profraw")) {
+ LIBC_NAMESPACE::linux_syscalls::munmap(profile_buffer, required_size);
+ return report_error("error: libc coverage filename buffer overflow\n");
+ }
+
+ auto fd_or_error = LIBC_NAMESPACE::linux_syscalls::open(
+ filename.data, O_WRONLY | O_CREAT | O_TRUNC, 0644);
+ if (!fd_or_error) {
+ LIBC_NAMESPACE::linux_syscalls::munmap(profile_buffer, required_size);
+ return report_error("error: libc coverage failed to open output file\n");
+ }
+ int fd = fd_or_error.value();
+
+ size_t bytes_written = 0;
+ bool write_error_occurred = false;
+ while (bytes_written < required_size) {
+ auto write_or_error = LIBC_NAMESPACE::linux_syscalls::write(
+ fd, profile_buffer + bytes_written, required_size - bytes_written);
+ if (!write_or_error) {
+ if (write_or_error.error() == EINTR)
+ continue;
+ write_error_occurred = true;
+ break;
+ }
+ ssize_t ret = write_or_error.value();
+ if (ret == 0) {
+ write_error_occurred = true;
+ break;
+ }
+ bytes_written += ret;
+ }
+
+ LIBC_NAMESPACE::linux_syscalls::close(fd);
+ LIBC_NAMESPACE::linux_syscalls::munmap(profile_buffer, required_size);
+
+ if (write_error_occurred || bytes_written < required_size)
+ return report_error(
+ "error: libc coverage failed to write all data to file\n");
+
+ // Clear the filename pattern to prevent compiler-rt from writing at exit.
+ if (__llvm_profile_set_filename)
+ __llvm_profile_set_filename("/dev/null");
+}
+#else
+extern "C" void write_raw_profile() {}
+#endif
+
#if __STDC_HOSTED__
#define TEST_MAIN int main
#else
@@ -56,5 +203,8 @@ TEST_MAIN(int argc, char **argv, char **envp) {
LIBC_NAMESPACE::testing::argv = argv;
LIBC_NAMESPACE::testing::envp = envp;
- return LIBC_NAMESPACE::testing::Test::runTests(parseOptions(argc, argv));
+ int result =
+ LIBC_NAMESPACE::testing::Test::runTests(parseOptions(argc, argv));
+ write_raw_profile();
+ return result;
}
More information about the libc-commits
mailing list