[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 02:56:34 PDT 2026


https://github.com/tapiwagonga updated https://github.com/llvm/llvm-project/pull/213271

>From 7705bb99a3bf3a26e06a7b529646f900379d70ca Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Fri, 31 Jul 2026 10:27:15 +0000
Subject: [PATCH 1/4] [libc] Enable freestanding test coverage via Linux
 syscalls

---
 libc/CMakeLists.txt                 |  26 +++++
 libc/test/UnitTest/LibcTestMain.cpp | 151 +++++++++++++++++++++++++++-
 2 files changed, 174 insertions(+), 3 deletions(-)

diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index 3aa975ba51416..9bcee433f9207 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -91,6 +91,32 @@ 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.")
diff --git a/libc/test/UnitTest/LibcTestMain.cpp b/libc/test/UnitTest/LibcTestMain.cpp
index c348d5ef1aa1b..9d9249d30f43b 100644
--- a/libc/test/UnitTest/LibcTestMain.cpp
+++ b/libc/test/UnitTest/LibcTestMain.cpp
@@ -43,8 +43,151 @@ 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.
+#if defined(__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/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 register write_raw_profile() via atexit() 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.
+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) {
+    if (idx + str.size() >= sizeof(data))
+      return false;
+    LIBC_NAMESPACE::inline_memcpy(data + idx, str.data(), str.size());
+    idx += str.size();
+    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());
+}
+
+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 = reinterpret_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;
+  LIBC_NAMESPACE::IntegerToString<long> pid_str(pid);
+  if (!filename.append("libc_cov_") || !filename.append(pid_str.view()) ||
+      !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");
+}
+} // anonymous namespace
+#else
+namespace {
+void write_raw_profile() {}
+} // anonymous namespace
+#endif
+
+extern "C" int atexit(void (*func)());
+
+// We must use a global constructor to register the atexit hook instead of
+// calling write_raw_profile() at the end of TEST_MAIN. This is because the
+// LLVM libc test framework spawns child processes for Death Tests which
+// terminate via exit(). If we don't intercept exit(), we lose their coverage.
+__attribute__((constructor)) void __register_libc_coverage() {
+  atexit(write_raw_profile);
+}
+
 #if __STDC_HOSTED__
 #define TEST_MAIN int main
 #else
@@ -56,5 +199,7 @@ 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));
+  return result;
 }

>From 77fe1143e06461d69f6a9b450df3604d326d2825 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Mon, 3 Aug 2026 09:52:06 +0000
Subject: [PATCH 2/4] [libc][docs] Add developer guide for running code
 coverage locally

---
 libc/docs/dev/code_coverage.md | 110 +++++++++++++++++++++++++++++++++
 libc/docs/dev/index.md         |   1 +
 2 files changed, 111 insertions(+)
 create mode 100644 libc/docs/dev/code_coverage.md

diff --git a/libc/docs/dev/code_coverage.md b/libc/docs/dev/code_coverage.md
new file mode 100644
index 0000000000000..9d96d5634fc53
--- /dev/null
+++ b/libc/docs/dev/code_coverage.md
@@ -0,0 +1,110 @@
+(code_coverage)=
+
+# Source-Level Code Coverage
+
+LLVM-libc supports source-level code coverage for its unit tests.
+
+Because `llvm-libc` unit tests are built as freestanding binaries (`-nostdlib`), standard `compiler-rt` coverage workflows fail in this environment as they inherently rely on the host's standard C library (`fopen`, `fwrite`), which is intentionally omitted to prevent host contamination.
+
+To bypass this constraint, the `LibcTestMain.cpp` test harness implements a custom profiling dumper:
+
+1. It silences the default `compiler-rt` dumper by overriding the global symbol: `extern "C" char __llvm_profile_filename[] = "/dev/null";`.
+2. It hooks into `atexit()` to dump coverage before the process ends. *(Note: Death tests that terminate via `_exit()`, `abort()`, or unhandled signals inherently bypass this hook. The parent test runner still correctly dumps its overall profile).*
+3. It determines the required buffer size via `__llvm_profile_get_size_for_buffer()` and allocates memory using `LIBC_NAMESPACE::linux_syscalls::mmap`.
+4. It extracts the raw profiling data from the compiler into the memory segment using `__llvm_profile_write_buffer()`.
+5. It writes the segment to a `.profraw` file using internal Linux syscall wrappers (`open`, `write`, `close`, `munmap`).
+
+## Limitations
+
+- **OS Support:** Because this relies on Linux system call wrappers, coverage extraction is strictly gated behind `#if defined(__linux__)`. On macOS or Windows builds, the coverage dumping step is gracefully bypassed, allowing the tests to compile normally.
+
+## 1. Setup and Configuration
+
+Before running any tests, you must clear old profile data and configure your CMake build directory to generate coverage instrumentation.
+
+```bash
+# 1. Clear previous profile artifacts
+find . -name "libc_cov_*.profraw" -delete
+rm -f libc_full.profdata profraw_list.txt
+
+# 2. Configure the build directory
+cmake -G Ninja -S runtimes -B build-cov \
+  -DLLVM_ENABLE_RUNTIMES="libc" \
+  -DLLVM_LIBC_FULL_BUILD=ON \
+  -DLLVM_LIBC_ENABLE_COVERAGE=ON \
+  -DCMAKE_CXX_COMPILER=clang++-19 \
+  -DCMAKE_C_COMPILER=clang-19
+```
+
+## 2. Build and Run Tests
+
+You have three options depending on how much of the library you want to test. (Targeted testing is significantly faster for local debugging).
+
+Test targets in `llvm-libc` follow a strict naming convention based on their directory structure: `libc.test.<directory_path>.<test_name>.__unit__`.
+
+### Option A: Whole Codebase
+To run the entire unit test suite (approx. 1,776 tests) and generate a massive, whole-codebase report:
+
+```bash
+ninja -C build-cov check-libc
+
+# If using Option A, do not define FILES_TO_REPORT in Step 3.
+```
+
+### Option B: Single Target
+To instantly run coverage on a single test, specify its exact Ninja target. For example, to test `isalpha`:
+
+```bash
+# Run only the specific target
+ninja -C build-cov libc.test.src.ctype.isalpha_test.__unit__
+
+# Define the source file to filter the report in Step 3
+FILES_TO_REPORT="libc/src/ctype/isalpha.cpp"
+```
+
+### Option C: Multiple Targets
+To run multiple isolated tests simultaneously, pass them as a space-separated list. For example, testing `isalpha` and `isdigit`:
+
+```bash
+# Define your targets 
+TARGETS="libc.test.src.ctype.isalpha_test.__unit__ libc.test.src.ctype.isdigit_test.__unit__"
+
+# Run the targets
+ninja -C build-cov $TARGETS
+
+# Define the source files to filter the report in Step 3
+FILES_TO_REPORT="libc/src/ctype/isalpha.cpp libc/src/ctype/isdigit.cpp"
+```
+
+## 3. Generate the Report
+
+Once your tests have finished running, merge the raw profile data and extract the executables to map the coverage back to the source code.
+
+```bash
+# 1. Merge raw profiles
+find . -name "libc_cov_*.profraw" > profraw_list.txt
+llvm-profdata-19 merge -sparse --input-files=profraw_list.txt -o libc_full.profdata
+
+# 2. Extract executables
+EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
+OBJECTS=("${EXECUTABLES[@]:1}")
+OBJECTS=("${OBJECTS[@]/#/-object=}")
+```
+
+### Choose Your Output Type
+
+You can specify the format of your final coverage report by changing the output command.
+
+**Option A: Terminal Summary Table (Text)**
+This provides a quick text-based summary of your coverage percentages directly in the terminal:
+```bash
+llvm-cov-19 report -instr-profile=libc_full.profdata "${EXECUTABLES[0]}" "${OBJECTS[@]}" $FILES_TO_REPORT
+```
+
+**Option B: Line-by-Line Interactive Webpage (HTML)**
+This generates an interactive HTML website so you can visually inspect exactly which lines of code are missing coverage. You can change the `OUTPUT_DIR` variable to save it wherever you prefer:
+```bash
+OUTPUT_DIR="coverage_html"
+llvm-cov-19 show -instr-profile=libc_full.profdata -format=html -output-dir=$OUTPUT_DIR "${EXECUTABLES[0]}" "${OBJECTS[@]}" $FILES_TO_REPORT
+```
+*(After running this, open `coverage_html/index.html` in your web browser).*
diff --git a/libc/docs/dev/index.md b/libc/docs/dev/index.md
index 4ce78540c8fad..33c8c91ac32d3 100644
--- a/libc/docs/dev/index.md
+++ b/libc/docs/dev/index.md
@@ -15,6 +15,7 @@ implementing_a_function
 config_options
 date_and_time
 fuzzing
+code_coverage
 header_generation
 implementation_standard
 undefined_behavior

>From 5f44d19dc41727a891f100e72c5cff5c871abfd7 Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Mon, 3 Aug 2026 11:24:56 +0000
Subject: [PATCH 3/4] [libc][ci] Test Pre-Commit Delta Bot Execution

---
 libc/src/ctype/isalpha.cpp | 1 +
 1 file changed, 1 insertion(+)

diff --git a/libc/src/ctype/isalpha.cpp b/libc/src/ctype/isalpha.cpp
index 7c874bf373866..e36ba3720a692 100644
--- a/libc/src/ctype/isalpha.cpp
+++ b/libc/src/ctype/isalpha.cpp
@@ -15,6 +15,7 @@
 
 namespace LIBC_NAMESPACE_DECL {
 
+// Testing the Pre-Commit Delta Coverage Bot
 LLVM_LIBC_FUNCTION(int, isalpha, (int c)) {
   if (c < 0 || c > cpp::numeric_limits<unsigned char>::max())
     return 0;

>From 6fab312366a82adda82feded5d93edf6403d954a Mon Sep 17 00:00:00 2001
From: Tapiwa Gonga <tapiwagonga at google.com>
Date: Fri, 7 Aug 2026 09:56:15 +0000
Subject: [PATCH 4/4] [libc][test] Address review comments: explicit flushing
 at main exit, death test hooks, and stack buffer formatting

---
 libc/CMakeLists.txt                        | 69 +++++++++++-----------
 libc/test/UnitTest/ExecuteFunctionUnix.cpp | 39 ++++++++++++
 libc/test/UnitTest/LibcTestMain.cpp        | 55 +++++++++--------
 3 files changed, 105 insertions(+), 58 deletions(-)

diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index 9bcee433f9207..babb0e73e686c 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -123,46 +123,49 @@ 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()
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 9d9249d30f43b..2b17e41a05e95 100644
--- a/libc/test/UnitTest/LibcTestMain.cpp
+++ b/libc/test/UnitTest/LibcTestMain.cpp
@@ -43,7 +43,8 @@ TestOptions parseOptions(int argc, char **argv) {
 
 } // anonymous namespace
 
-#if defined(__linux__)
+#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"
@@ -53,6 +54,8 @@ TestOptions parseOptions(int argc, char **argv) {
 #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>
@@ -62,8 +65,9 @@ TestOptions parseOptions(int argc, char **argv) {
 //
 // 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 register write_raw_profile() via atexit() to dump
-// raw coverage counters (libc_cov_<pid>.profraw) using direct Linux
+// 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" {
@@ -74,7 +78,7 @@ __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.
-char __llvm_profile_filename[] = "/dev/null";
+__attribute__((weak)) char __llvm_profile_filename[] = "/dev/null";
 }
 
 namespace {
@@ -85,10 +89,21 @@ struct FixedSizeBuffer {
   FixedSizeBuffer() { data[0] = '\0'; }
 
   bool append(string_view str) {
-    if (idx + str.size() >= sizeof(data))
+    size_t len = str.size();
+    if (idx + len >= sizeof(data))
       return false;
-    LIBC_NAMESPACE::inline_memcpy(data + idx, str.data(), str.size());
-    idx += str.size();
+    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;
   }
@@ -97,8 +112,9 @@ struct FixedSizeBuffer {
 LIBC_INLINE void report_error(string_view msg) {
   LIBC_NAMESPACE::syscall_impl<long>(SYS_write, 2, msg.data(), msg.size());
 }
+} // anonymous namespace
 
-void write_raw_profile() {
+extern "C" void write_raw_profile() {
   if (!__llvm_profile_get_size_for_buffer || !__llvm_profile_write_buffer)
     return;
 
@@ -112,7 +128,7 @@ void write_raw_profile() {
       MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
   if (!mmap_or_error)
     return report_error("error: libc coverage failed to mmap buffer\n");
-  char *profile_buffer = reinterpret_cast<char *>(mmap_or_error.value());
+  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);
@@ -126,8 +142,9 @@ void write_raw_profile() {
     pid = 1;
 
   FixedSizeBuffer filename;
-  LIBC_NAMESPACE::IntegerToString<long> pid_str(pid);
-  if (!filename.append("libc_cov_") || !filename.append(pid_str.view()) ||
+  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");
@@ -171,23 +188,10 @@ void write_raw_profile() {
   if (__llvm_profile_set_filename)
     __llvm_profile_set_filename("/dev/null");
 }
-} // anonymous namespace
 #else
-namespace {
-void write_raw_profile() {}
-} // anonymous namespace
+extern "C" void write_raw_profile() {}
 #endif
 
-extern "C" int atexit(void (*func)());
-
-// We must use a global constructor to register the atexit hook instead of
-// calling write_raw_profile() at the end of TEST_MAIN. This is because the
-// LLVM libc test framework spawns child processes for Death Tests which
-// terminate via exit(). If we don't intercept exit(), we lose their coverage.
-__attribute__((constructor)) void __register_libc_coverage() {
-  atexit(write_raw_profile);
-}
-
 #if __STDC_HOSTED__
 #define TEST_MAIN int main
 #else
@@ -201,5 +205,6 @@ TEST_MAIN(int argc, char **argv, char **envp) {
 
   int result =
       LIBC_NAMESPACE::testing::Test::runTests(parseOptions(argc, argv));
+  write_raw_profile();
   return result;
 }



More information about the libc-commits mailing list