Skip to content

[libc][test] Condition out tests that can't work on bare metal - #215830

Open
statham-arm wants to merge 7 commits into
llvm:mainfrom
statham-arm:baremetal-libc-testing
Open

[libc][test] Condition out tests that can't work on bare metal#215830
statham-arm wants to merge 7 commits into
llvm:mainfrom
statham-arm:baremetal-libc-testing

Conversation

@statham-arm

Copy link
Copy Markdown
Contributor

In bare-metal builds of libc, the EXPECT_DEATH macro may not be defined. Also, signal-macros.h may not define the values needed for the rest of <signal.h> to work. So tests that rely on either of those things will fail to compile.

I've conditioned out the EXPECT_DEATH tests completely if EXPECT_DEATH isn't defined. There's inherently no reliable way to define it: you can't rely on finding out about segmentation faults by a signal, because accessing memory outside valid C objects might silently succeed (valid unused memory), or generate a CPU fault that no kernel traps for you, or overwrite something important outside your program.

The check for signals in FPExceptMatcher.cpp can be more lenient, and just condition out the signal-handling code, leaving the test of cumulative FP exception flags in place, so that the checker simply returns "no signal was caught" unconditionally.

In bare-metal builds of libc, the `EXPECT_DEATH` macro may not be
defined. Also, `signal-macros.h` may not define the values needed for
the rest of `<signal.h>` to work. So tests that rely on either of
those things will fail to compile.

I've conditioned out the `EXPECT_DEATH` tests completely if
`EXPECT_DEATH` isn't defined. There's inherently no reliable way to
define it: you can't rely on finding out about segmentation faults by
a signal, because accessing memory outside valid C objects might
silently succeed (valid unused memory), or generate a CPU fault that
no kernel traps for you, or overwrite something important outside your
program.

The check for signals in `FPExceptMatcher.cpp` can be more lenient,
and just condition out the signal-handling code, leaving the test of
cumulative FP exception flags in place, so that the checker simply
returns "no signal was caught" unconditionally.
@statham-arm
statham-arm requested a review from a team as a code owner August 12, 2026 15:41
@statham-arm

Copy link
Copy Markdown
Contributor Author

The tests affected by these patches were enabled recently by #213860. See also #215509 which is also causing some libc tests to fail to compile in our bare-metal environment.

@llvmorg-github-actions

Copy link
Copy Markdown

@llvm/pr-subscribers-libc

Author: Simon Tatham (statham-arm)

Changes

In bare-metal builds of libc, the EXPECT_DEATH macro may not be defined. Also, signal-macros.h may not define the values needed for the rest of &lt;signal.h&gt; to work. So tests that rely on either of those things will fail to compile.

I've conditioned out the EXPECT_DEATH tests completely if EXPECT_DEATH isn't defined. There's inherently no reliable way to define it: you can't rely on finding out about segmentation faults by a signal, because accessing memory outside valid C objects might silently succeed (valid unused memory), or generate a CPU fault that no kernel traps for you, or overwrite something important outside your program.

The check for signals in FPExceptMatcher.cpp can be more lenient, and just condition out the signal-handling code, leaving the test of cumulative FP exception flags in place, so that the checker simply returns "no signal was caught" unconditionally.


Full diff: https://github.com/llvm/llvm-project/pull/215830.diff

4 Files Affected:

  • (modified) libc/test/UnitTest/FPExceptMatcher.cpp (+20-1)
  • (modified) libc/test/src/compiler/stack_chk_guard_test.cpp (+7)
  • (modified) libc/test/src/time/localtime_r_test.cpp (+2)
  • (modified) libc/test/src/time/localtime_test.cpp (+2)
diff --git a/libc/test/UnitTest/FPExceptMatcher.cpp b/libc/test/UnitTest/FPExceptMatcher.cpp
index 10b6ff9f91da1..863811841124f 100644
--- a/libc/test/UnitTest/FPExceptMatcher.cpp
+++ b/libc/test/UnitTest/FPExceptMatcher.cpp
@@ -20,7 +20,15 @@
 #include "hdr/types/fenv_t.h"
 #include "src/__support/FPUtil/FEnvImpl.h"
 #include <setjmp.h>
+
+// To make this test work on bare-metal targets without working signal.h, find
+// out if signal-macros.h did anything, before including the full signal.h. It
+// doesn't define a specific macro of the form HAVE_SIGNALS, so we just test
+// for one of the macros it _does_ define.
+#include "llvm-libc-macros/signal-macros.h"
+#ifdef __NSIGSET_WORDS
 #include <signal.h>
+#endif
 
 #if LIBC_TEST_HAS_MATCHERS()
 
@@ -37,27 +45,38 @@ namespace testing {
 using sighandler_t = __sighandler_t *;
 #endif
 
-static thread_local sigjmp_buf jumpBuffer;
 static thread_local bool caughtExcept;
 
+#ifdef __NSIGSET_WORDS
+
+static thread_local sigjmp_buf jumpBuffer;
+
 static void sigfpeHandler([[maybe_unused]] int sig) {
   caughtExcept = true;
   siglongjmp(jumpBuffer, -1);
 }
 
+#endif // __NSIGSET_WORDS
+
 FPExceptMatcher::FPExceptMatcher(FunctionCaller *func) {
+#ifdef __NSIGSET_WORDS
   auto *oldSIGFPEHandler = signal(SIGFPE, &sigfpeHandler);
+#endif
 
   caughtExcept = false;
   fenv_t oldEnv;
   fputil::get_env(&oldEnv);
+#ifdef __NSIGSET_WORDS
   if (sigsetjmp(jumpBuffer, 1) == 0)
+#endif
     func->call();
   delete func;
   // We restore the previous floating point environment after
   // the call to the function which can potentially raise SIGFPE.
   fputil::set_env(&oldEnv);
+#ifdef __NSIGSET_WORDS
   signal(SIGFPE, oldSIGFPEHandler);
+#endif
   exceptionRaised = caughtExcept;
 }
 
diff --git a/libc/test/src/compiler/stack_chk_guard_test.cpp b/libc/test/src/compiler/stack_chk_guard_test.cpp
index 301031ff47bd5..978e889263405 100644
--- a/libc/test/src/compiler/stack_chk_guard_test.cpp
+++ b/libc/test/src/compiler/stack_chk_guard_test.cpp
@@ -10,6 +10,13 @@
 #include "src/compiler/__stack_chk_fail.h"
 #include "test/UnitTest/Test.h"
 
+#ifdef EXPECT_DEATH
 TEST(LlvmLibcStackChkFail, Death) {
   EXPECT_DEATH([] { __stack_chk_fail(); }, WITH_SIGNAL(SIGABRT));
 }
+#else
+TEST(LlvmLibcStackChkFail, Dummy) {
+  // Need at least one test, because a completely empty test file
+  // counts as failure
+}
+#endif // EXPECT_DEATH
diff --git a/libc/test/src/time/localtime_r_test.cpp b/libc/test/src/time/localtime_r_test.cpp
index bc71419d68c64..e3c3b96278b89 100644
--- a/libc/test/src/time/localtime_r_test.cpp
+++ b/libc/test/src/time/localtime_r_test.cpp
@@ -27,6 +27,7 @@ TEST(LlvmLibcLocaltimeR, ValidUnixTimestamp0) {
   ASSERT_EQ(0, result->tm_isdst);
 }
 
+#ifdef EXPECT_DEATH
 TEST(LlvmLibcLocaltimeR, NullPtr) {
   struct tm input;
   time_t timer = 0;
@@ -37,6 +38,7 @@ TEST(LlvmLibcLocaltimeR, NullPtr) {
   EXPECT_DEATH([&] { LIBC_NAMESPACE::localtime_r(&timer, nullptr); },
                WITH_SIGNAL(-1));
 }
+#endif // EXPECT_DEATH
 
 // TODO(zimirza): These tests does not expect the correct output of localtime as
 // per specification. This is due to timezone functions removed from
diff --git a/libc/test/src/time/localtime_test.cpp b/libc/test/src/time/localtime_test.cpp
index 37974d27771dc..6e1116b98eeb1 100644
--- a/libc/test/src/time/localtime_test.cpp
+++ b/libc/test/src/time/localtime_test.cpp
@@ -25,9 +25,11 @@ TEST(LlvmLibcLocaltime, ValidUnixTimestamp0) {
   ASSERT_EQ(0, result->tm_isdst);
 }
 
+#ifdef EXPECT_DEATH
 TEST(LlvmLibcLocaltime, NullPtr) {
   EXPECT_DEATH([] { LIBC_NAMESPACE::localtime(nullptr); }, WITH_SIGNAL(-1));
 }
+#endif // EXPECT_DEATH
 
 // TODO(zimirza): These tests does not expect the correct output of localtime as
 // per specification. This is due to timezone functions removed from

@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

🐧 Linux x64 Test Results

  • 1923 tests passed

✅ The build succeeded and all tests passed.

#include "src/compiler/__stack_chk_fail.h"
#include "test/UnitTest/Test.h"

#ifdef EXPECT_DEATH

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What if you changed the macro definition to be empty in case the target is baremetal (LIBC_TARGET_OS_IS_BAREMETAL)? Instead of adding these ifdefs.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already set them to no-op when LIBC_TEST_SKIP_DEATH_TESTS is set:
https://github.com/llvm/llvm-project/blob/main/libc/test/UnitTest/LibcTest.h#L510

@statham-arm

Copy link
Copy Markdown
Contributor Author

Hmm, this doesn't seem to work, because in the CI build, the only -I option given to the compiler is pointing at llvm-project/libc. (Not even llvm-project/libc/include, which seems odd.) In the downstream bare-metal project where I tested that patch, the corresponding compile command also includes $build_dir/libc/include. I'll have to rethink.

@lntue

lntue commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

for the death tests, can we set LIBC_TEST_SKIP_DEATH_TESTS in cmake by default for baremetal configs?
https://github.com/llvm/llvm-project/blob/main/libc/cmake/modules/LLVMLibCTestRules.cmake#L36

@nickdesaulniers
nickdesaulniers removed their request for review August 12, 2026 16:52
@statham-arm

Copy link
Copy Markdown
Contributor Author

Ah, thanks. Yes, that looks like a better plan, and avoids the special case of the otherwise empty file. But I'm still stuck on figuring out how to avoid using signal.h in the general case.

Apparently there are platforms which don't define
LIBC_TEST_SUBPROCESS_TESTS, which means that EXPECT_DEATH wouldn't
have been defined in that configuration at all. I assume those
platforms avoided a compile failure by not _calling_ EXPECT_DEATH. In
that case my #error was overzealous: it's only an error not to define
EXPECT_DEATH if you're also running a test that calls it.
@statham-arm

Copy link
Copy Markdown
Contributor Author

Sorry for all the spam while I fixed the CI failures! On the plus side, I'm impressed by the thoroughness of libc CI. 🙂

I think this is now ready for a human to look at again. The conditioning-out of signal.h is done by checking whether this is a LIBC_FULL_BUILD with signal.h not among the headers being built. And EXPECT_DEATH is dealt with by defining it to an empty macro, although I also had to make that mechanism work even when LIBC_TEST_SUBPROCESS_TESTS isn't defined.

@labath labath left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm sorry about the breakage. I'm still fairly new here, so take my comments with a grain of salt.

Comment thread libc/test/CMakeLists.txt Outdated
add_custom_target(libc-integration-tests-build)
add_custom_target(libc_include_tests-build)

# Allow compiled tests to check for the existence of particular headers in this

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better to have a dedicated macro for what you're trying to achieve (if we need one), instead of full suite of definitions like this. This does not even precisely describe the property you want to capture. We allow targets to customize the set of entry points in a header, so you could hypothetically have a target which defines signal.h, but still does not have all the required libc functions.

# Set LIBC_TEST_SKIP_DEATH_TESTS to skip running tests that use EXPECT_DEATH
# and ASSERT_DEATH. On platforms where they work, they can be slow; on
# bare-metal platforms it might not be possible to implement them at all.
if(LIBC_TEST_SKIP_DEATH_TESTS)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it'd be better to keep this inside the LIBC_TEST_SUBPROCESS_TESTS branch since death tests (at least how they are implemented right now) are a special kind of a subprocess test.

What is the story with overall subprocess (e.g. EXPECT_EXIT) tests for bare metal targets. If you don't support those either, maybe we should be the behavior for subprocess tests in general?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean, leave the existing ifdefs as they originally were, but whenever LIBC_TEST_SUBPROCESS_TESTS isn't defined, we have a second #else branch where we define EXPECT_DEATH to be empty? OK.

We aren't seeing any test-program build failures relating to EXPECT_EXIT (in that with this patch and Mark's linked one we see no failures at all). I think that's just because the EXPECT_EXIT tests are all specific to functions that deal with complicated exiting behaviour, like atexit, and the bare-metal libc configuration doesn't include those functions, hence the tests aren't built or run either.

Whereas EXPECT_DEATH is more widespread, because it appears in tests of functions that have nothing per se to do with exiting, such as testing that if you pass a null pointer to localtime then something appropriately horrible happens.

@SchrodingerZhu

Copy link
Copy Markdown
Contributor

Sorry for all the spam while I fixed the CI failures! On the plus side, I'm impressed by the thoroughness of libc CI. 🙂

I think this is now ready for a human to look at again. The conditioning-out of signal.h is done by checking whether this is a LIBC_FULL_BUILD with signal.h not among the headers being built. And EXPECT_DEATH is dealt with by defining it to an empty macro, although I also had to make that mechanism work even when LIBC_TEST_SUBPROCESS_TESTS isn't defined.

Sorry for the FreeBSD pipeline being "annoying" :D. I added that for the team to track "does libc work on other POSIX platforms". Do let me know if any freebsd stuff is being naughty again -- I will try to improve them later.

Comment thread libc/test/CMakeLists.txt
Comment on lines +13 to +28
# Check if tests can safely include signal.h. (FPExceptMatcher.cpp can
# use it, but can also do without it.)
if(NOT LLVM_LIBC_FULL_BUILD)
# In an overlay build of libc, assume signal.h is available: the host libc
# will provide it even if we don't.
add_compile_definitions(HAVE_SIGNAL_H)
else()
# In a full build, signal.h only exists if we provide it ourselves.
# In a bare-metal full build we might not, so check which headers
# we're shipping.
list(FIND TARGET_PUBLIC_HEADERS libc.include.signal SIGNAL_H_idx)
if(SIGNAL_H_idx GREATER -1)
add_compile_definitions(HAVE_SIGNAL_H)
endif()
endif()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be better to check the presence of the actual entry points rather than of the header (this is what I meant with the first comment -- sorry I wasn't clear). Also, would it be possible to put this next to the clock checking code in

if(TARGET libc.src.time.clock)
target_compile_definitions(${lib} PRIVATE TARGET_SUPPORTS_CLOCK)
endif()

.. and call it something like TARGET_SUPPORTS_SIGNAL_CATCHING?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants