[libc-commits] [libc] [libc] Enable freestanding test coverage via Linux syscalls (PR #213271)

Tapiwa Gonga via libc-commits libc-commits at lists.llvm.org
Fri Jul 31 06:07:45 PDT 2026


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

This PR enables C++ source-level code coverage for llvm-libc unit tests by introducing a custom profiling data dumper that operates completely independently of the host's standard C library.

llvm-libc unit tests are built as freestanding binaries (-nostdlib). Standard compiler-rt coverage workflows fail in this environment because they inherently rely on the host's standard C library (fopen, fwrite), which is intentionally omitted to prevent host contamination.

To bypass this constraint, LibcTestMain.cpp implements a custom profiling dumper:

**How It Works**
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 direct SYS_* Linux system calls, 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.

**How to Run Codebase Coverage Locally**

In terminal run:

# 1. Clear previous profile artifacts
```
find . -name "libc_cov_*.profraw" -delete
rm -f libc_full.profdata profraw_list.txt
```

# 2. Configure and run the test suite
```
cmake -G Ninja -B build-cov \
  -DLLVM_ENABLE_RUNTIMES="libc" \
  -DLLVM_LIBC_FULL_BUILD=ON \
  -DLIBC_ENABLE_COVERAGE=ON \
  -DCMAKE_CXX_COMPILER=clang++-19 \
  -DCMAKE_C_COMPILER=clang-19
  
ninja -C build-cov check-libc
```

# 3. 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
```

# 4. Generate coverage report
```
EXECUTABLES=($(find build-cov -type f -executable -name "*__build__"))
OBJECTS=("${EXECUTABLES[@]:1}")
OBJECTS=("${OBJECTS[@]/#/-object=}")
llvm-cov-19 report -instr-profile=libc_full.profdata "${EXECUTABLES[0]}" "${OBJECTS[@]}"
```





>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] [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;
 }



More information about the libc-commits mailing list