[libc-commits] [libc] [libc] Allow hermetic tests to use external startup objects (PR #212557)

Simi Pallipurath via libc-commits libc-commits at lists.llvm.org
Wed Oct 7 04:25:42 PDT 2026


https://github.com/simpal01 updated https://github.com/llvm/llvm-project/pull/212557

>From 4f0d4b315be9ff40bec7510522c870544fdc7fd9 Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Tue, 28 Jul 2026 17:33:03 +0100
Subject: [PATCH 1/3] [libc] Allow hermetic tests to use external startup
 objects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Adds LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP to
control whether libc hermetic tests link against LLVM libc’s
own crt1.o.

When the option is OFF, hermetic tests no longer require
or link libc.startup.${LIBC_TARGET_OS}.crt1. This allows
baremetal configurations to provide startup externally through
LIBC_TEST_LINK_OPTIONS_DEFAULT while still running hermetic tests.

For baremetal external-startup mode, the patch also adds the
required process startup/termination dependencies and ensures
exit uses the public packaging object, since external startup
code may reference the public C exit symbol.

Tested on Arm Toolchain for Embedded: https://github.com/arm/arm-toolchain

Assisted-by: Codex
---
 libc/CMakeLists.txt                        |  3 ++
 libc/cmake/modules/LLVMLibCTestRules.cmake | 52 +++++++++++++++++++---
 2 files changed, 50 insertions(+), 5 deletions(-)

diff --git a/libc/CMakeLists.txt b/libc/CMakeLists.txt
index f44a84d5ae48a..86ddcd2df264f 100644
--- a/libc/CMakeLists.txt
+++ b/libc/CMakeLists.txt
@@ -262,6 +262,9 @@ if(LIBC_ENABLE_COVERAGE)
         CACHE INTERNAL "Clang profile runtime")
   endif()
 endif()
+option(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP
+       "Link hermetic tests against LLVM libc's own startup object"
+       ON)
 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)
diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index 917e0ce2a9b05..bcbd4ba5d067d 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -210,6 +210,14 @@ function(get_object_files_for_test result skipped_entrypoints_list)
           if(fq_target_name STREQUAL "libc.test.include.issignaling_c_test" OR fq_target_name STREQUAL "libc.test.include.iscanonical_c_test")
             string(REPLACE ".__internal__" "" object_file_raw ${object_file_raw})
           endif()
+          # Adding libc.src.stdlib.exit normally contributes the internal entrypoint
+          # object. External baremetal startup code can reference the public C exit
+          # symbol, so use the public-packaging object for exit in this configuration.
+          if(dep STREQUAL "libc.src.stdlib.exit" AND
+             NOT LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP AND
+             LIBC_TARGET_OS_IS_BAREMETAL)
+            string(REPLACE ".__internal__" "" object_file_raw ${object_file_raw})
+          endif()
           list(APPEND dep_obj ${object_file_raw})
         endif()
       endif()
@@ -753,10 +761,24 @@ endfunction()
 #     LOADER_ARGS <list of special args to loaders (like the GPU loader)>
 #   )
 function(add_libc_hermetic test_name)
-  if(NOT TARGET libc.startup.${LIBC_TARGET_OS}.crt1)
-    message(VERBOSE "Skipping ${fq_target_name} as it is not available on ${LIBC_TARGET_OS}.")
+  get_fq_target_name(${test_name} fq_target_name)
+  get_fq_target_name(${test_name}.libc fq_libc_target_name)
+
+  set(startup_target libc.startup.${LIBC_TARGET_OS}.crt1)
+  set(startup_deps "")
+  if(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
+    if(NOT TARGET ${startup_target})
+      message(VERBOSE "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
+      return()
+    endif()
+    list(APPEND startup_deps ${startup_target})
+  endif()
+
+  if(NOT startup_deps AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
+    message(VERBOSE "Skipping ${fq_target_name} as it has no startup provider.")
     return()
   endif()
+
   cmake_parse_arguments(
     "HERMETIC_TEST"
     "IS_GPU_BENCHMARK;NO_RUN_POSTBUILD;C_TEST" # Optional arguments
@@ -772,9 +794,6 @@ function(add_libc_hermetic test_name)
     message(FATAL_ERROR "The SRCS list for add_integration_test is missing.")
   endif()
 
-  get_fq_target_name(${test_name} fq_target_name)
-  get_fq_target_name(${test_name}.libc fq_libc_target_name)
-
   get_fq_deps_list(fq_deps_list ${HERMETIC_TEST_DEPENDS})
   list(APPEND fq_deps_list
     # Hermetic tests use the platform's startup object. So, their deps also
@@ -804,6 +823,19 @@ function(add_libc_hermetic test_name)
   if(LIBC_TEST_SUBPROCESS_TESTS AND NOT HERMETIC_TEST_C_TEST)
     list(APPEND fq_deps_list
       libc.test.UnitTest.LibcDeathTestExecutors
+=======
+      ${startup_deps}
+      # We always add the memory functions objects. This is because the
+      # compiler's codegen can emit calls to the C memory functions.
+      libc.src.__support.StringUtil.error_to_string
+      libc.src.string.memcmp
+      libc.src.string.memcpy
+      libc.src.string.memmove
+      libc.src.string.memset
+      libc.src.strings.bcmp
+      libc.src.strings.bzero
+      # Syscalls used by death tests.
+>>>>>>> 968c82934d28 ([libc] Allow hermetic tests to use external startup objects)
       libc.src.poll.poll
       libc.src.signal.kill
       libc.src.stdio.fflush
@@ -818,6 +850,16 @@ function(add_libc_hermetic test_name)
     )
   endif()
 
+  if(NOT LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP AND LIBC_TARGET_OS_IS_BAREMETAL)
+    # External baremetal startup objects still need libc's process startup and
+    # termination entrypoints, even when the test itself does not depend on them.
+    list(APPEND fq_deps_list
+      libc.src.stdlib.atexit
+      libc.src.stdlib.exit
+      libc.startup.baremetal.init
+    )
+  endif()
+
   if(libc.src.compiler.__stack_chk_fail IN_LIST TARGET_LLVMLIBC_ENTRYPOINTS)
     # __stack_chk_fail should always be included if supported to allow building
     # libc with the stack protector enabled.

>From 4c977b16b58035bbf0cc7aba106af43cdc55bd8d Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Tue, 22 Sep 2026 22:18:57 +0100
Subject: [PATCH 2/3] fixup! [libc] Allow hermetic tests to use external
 startup objects

---
 libc/cmake/modules/LLVMLibCTestRules.cmake | 31 ++++++++--------------
 1 file changed, 11 insertions(+), 20 deletions(-)

diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index bcbd4ba5d067d..e7cbdef2358d8 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -765,17 +765,21 @@ function(add_libc_hermetic test_name)
   get_fq_target_name(${test_name}.libc fq_libc_target_name)
 
   set(startup_target libc.startup.${LIBC_TARGET_OS}.crt1)
-  set(startup_deps "")
+  set(startup_target_dep "")
   if(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
     if(NOT TARGET ${startup_target})
-      message(VERBOSE "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
+      if(LIBC_CMAKE_VERBOSE_LOGGING)
+        message(STATUS "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
+      endif()
       return()
     endif()
-    list(APPEND startup_deps ${startup_target})
+    set(startup_target_dep ${startup_target})
   endif()
 
-  if(NOT startup_deps AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
-    message(VERBOSE "Skipping ${fq_target_name} as it has no startup provider.")
+  if(NOT startup_target_dep AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
+    if(LIBC_CMAKE_VERBOSE_LOGGING)
+      message(STATUS "Skipping ${fq_target_name} as it has no startup provider.")
+    endif()
     return()
   endif()
 
@@ -796,9 +800,9 @@ function(add_libc_hermetic test_name)
 
   get_fq_deps_list(fq_deps_list ${HERMETIC_TEST_DEPENDS})
   list(APPEND fq_deps_list
+    ${startup_target_dep}
     # Hermetic tests use the platform's startup object. So, their deps also
     # have to be collected.
-    libc.startup.${LIBC_TARGET_OS}.crt1
     # We always add the memory functions objects. This is because the
     # compiler's codegen can emit calls to the C memory functions.
     libc.src.__support.StringUtil.error_to_string
@@ -823,19 +827,6 @@ function(add_libc_hermetic test_name)
   if(LIBC_TEST_SUBPROCESS_TESTS AND NOT HERMETIC_TEST_C_TEST)
     list(APPEND fq_deps_list
       libc.test.UnitTest.LibcDeathTestExecutors
-=======
-      ${startup_deps}
-      # We always add the memory functions objects. This is because the
-      # compiler's codegen can emit calls to the C memory functions.
-      libc.src.__support.StringUtil.error_to_string
-      libc.src.string.memcmp
-      libc.src.string.memcpy
-      libc.src.string.memmove
-      libc.src.string.memset
-      libc.src.strings.bcmp
-      libc.src.strings.bzero
-      # Syscalls used by death tests.
->>>>>>> 968c82934d28 ([libc] Allow hermetic tests to use external startup objects)
       libc.src.poll.poll
       libc.src.signal.kill
       libc.src.stdio.fflush
@@ -1045,7 +1036,7 @@ function(add_libc_hermetic test_name)
   target_link_libraries(
     ${fq_build_target_name}
     PRIVATE
-      libc.startup.${LIBC_TARGET_OS}.crt1
+      ${startup_target_dep}
       ${HERMETIC_TEST_LINK_LIBRARIES}
       ${fq_target_name}.__libc__
       ${coverage_link_libs}

>From 843ae8fba74041d9e66946078f3b4af49641bad8 Mon Sep 17 00:00:00 2001
From: Simi Pallipurath <simi.pallipurath at arm.com>
Date: Tue, 6 Oct 2026 14:51:02 +0100
Subject: [PATCH 3/3] fixup! [libc] Allow hermetic tests to use external
 startup objects
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

  - Add STARTUP support to add_libc_hermetic.
  - Allow external startup objects when internal startup is disabled.
  - Validate conflicting or missing startup targets.
  - Add a bare-metal link test using a minimal external crt1.
  - Verify LLVM libc’s internal crt1 is not linked accidentally
    by adding a new test that produces a duplicate symbol error if
    LLVM libc's internal crt1 is also linked with the external startup
    object.
---
 libc/cmake/modules/LLVMLibCTestRules.cmake    | 28 +++++++++++++------
 libc/test/src/__support/CMakeLists.txt        | 20 +++++++++++++
 .../__support/external_startup_link_test.cpp  |  8 ++++++
 .../minimal_external_crt1_for_test.cpp        |  7 +++++
 4 files changed, 55 insertions(+), 8 deletions(-)
 create mode 100644 libc/test/src/__support/external_startup_link_test.cpp
 create mode 100644 libc/test/src/__support/minimal_external_crt1_for_test.cpp

diff --git a/libc/cmake/modules/LLVMLibCTestRules.cmake b/libc/cmake/modules/LLVMLibCTestRules.cmake
index e7cbdef2358d8..1911a03777a5f 100644
--- a/libc/cmake/modules/LLVMLibCTestRules.cmake
+++ b/libc/cmake/modules/LLVMLibCTestRules.cmake
@@ -759,14 +759,28 @@ endfunction()
 #     COMPILE_OPTIONS <list of special compile options for the test>
 #     LINK_LIBRARIES <list of linking libraries for this target>
 #     LOADER_ARGS <list of special args to loaders (like the GPU loader)>
+#     STARTUP <startup object target used when internal startup is disabled>
 #   )
 function(add_libc_hermetic test_name)
   get_fq_target_name(${test_name} fq_target_name)
   get_fq_target_name(${test_name}.libc fq_libc_target_name)
 
+  cmake_parse_arguments(
+    "HERMETIC_TEST"
+    "IS_GPU_BENCHMARK;NO_RUN_POSTBUILD" # Optional arguments
+    "SUITE;CXX_STANDARD;STARTUP" # Single value arguments
+    "SRCS;HDRS;DEPENDS;ARGS;ENV;COMPILE_OPTIONS;LINK_LIBRARIES;LOADER_ARGS" # Multi-value arguments
+    ${ARGN}
+  )
+
   set(startup_target libc.startup.${LIBC_TARGET_OS}.crt1)
   set(startup_target_dep "")
   if(LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
+    if(HERMETIC_TEST_STARTUP)
+      message(FATAL_ERROR
+        "Hermetic test ${fq_target_name} provides an external STARTUP target "
+        "while internal startup is enabled.")
+    endif()
     if(NOT TARGET ${startup_target})
       if(LIBC_CMAKE_VERBOSE_LOGGING)
         message(STATUS "Skipping ${fq_target_name} as ${startup_target} is not available on ${LIBC_TARGET_OS}.")
@@ -774,6 +788,12 @@ function(add_libc_hermetic test_name)
       return()
     endif()
     set(startup_target_dep ${startup_target})
+  elseif(HERMETIC_TEST_STARTUP)
+    get_fq_dep_name(startup_target_dep ${HERMETIC_TEST_STARTUP})
+    if(NOT TARGET ${startup_target_dep})
+      message(FATAL_ERROR
+        "Hermetic test ${fq_target_name} startup target ${startup_target_dep} does not exist.")
+    endif()
   endif()
 
   if(NOT startup_target_dep AND NOT LIBC_TEST_LINK_OPTIONS_DEFAULT)
@@ -783,14 +803,6 @@ function(add_libc_hermetic test_name)
     return()
   endif()
 
-  cmake_parse_arguments(
-    "HERMETIC_TEST"
-    "IS_GPU_BENCHMARK;NO_RUN_POSTBUILD;C_TEST" # Optional arguments
-    "SUITE;CXX_STANDARD" # Single value arguments
-    "SRCS;HDRS;DEPENDS;ARGS;ENV;COMPILE_OPTIONS;LINK_LIBRARIES;FLAGS;LOADER_ARGS" # Multi-value arguments
-    ${ARGN}
-  )
-
   if(NOT HERMETIC_TEST_SUITE)
     message(FATAL_ERROR "SUITE not specified for ${fq_target_name}")
   endif()
diff --git a/libc/test/src/__support/CMakeLists.txt b/libc/test/src/__support/CMakeLists.txt
index 2c92302d04b2c..c71385d98898b 100644
--- a/libc/test/src/__support/CMakeLists.txt
+++ b/libc/test/src/__support/CMakeLists.txt
@@ -300,6 +300,26 @@ add_libc_test(
     libc.src.__support.memory_size
 )
 
+if(LIBC_TARGET_OS_IS_BAREMETAL AND
+   NOT LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP)
+  add_object_library(
+    minimal_external_crt1_for_test
+    SRCS
+      minimal_external_crt1_for_test.cpp
+  )
+
+  add_libc_hermetic(
+    external_startup_link_test
+    NO_RUN_POSTBUILD
+    STARTUP
+      .minimal_external_crt1_for_test
+    SUITE
+      libc-support-tests
+    SRCS
+      external_startup_link_test.cpp
+  )
+endif()
+
 add_libc_test(
   str_to_float_comparison_test
   NO_RUN_POSTBUILD
diff --git a/libc/test/src/__support/external_startup_link_test.cpp b/libc/test/src/__support/external_startup_link_test.cpp
new file mode 100644
index 0000000000000..749e34c3f7792
--- /dev/null
+++ b/libc/test/src/__support/external_startup_link_test.cpp
@@ -0,0 +1,8 @@
+// This file intentionally contains no test cases. CMake's add_executable
+// requires a source file, but successfully linking the executable is the test.
+// When LLVM_LIBC_HERMETIC_TEST_USE_INTERNAL_STARTUP is disabled, a hermetic
+// test must link the minimal external startup object supplied by this test and
+// must not link LLVM libc's internal crt1. Both startup objects define _start,
+// so accidentally linking the internal crt1 as well causes a duplicate-symbol
+// error. A successful link therefore verifies that disabling internal startup
+// selects only the provided minimal_external_crt1_for_test.cpp object.
diff --git a/libc/test/src/__support/minimal_external_crt1_for_test.cpp b/libc/test/src/__support/minimal_external_crt1_for_test.cpp
new file mode 100644
index 0000000000000..9bf1852fb6349
--- /dev/null
+++ b/libc/test/src/__support/minimal_external_crt1_for_test.cpp
@@ -0,0 +1,7 @@
+// A minimal external crt1 object used only by link tests.
+// Executables linked with this object must not be run.
+
+extern "C" [[noreturn]] void _start() {
+  for (;;) {
+  }
+}



More information about the libc-commits mailing list