git: 3de9dc5bf4b4 - main - libc: gen: add a test for rtld underflowing our posix_spawn thread

From: Kyle Evans <kevans_at_FreeBSD.org>
Date: Tue, 30 Jun 2026 13:13:27 UTC
The branch main has been updated by kevans:

URL: https://cgit.FreeBSD.org/src/commit/?id=3de9dc5bf4b438e0d98222dc028982380d5a25d2

commit 3de9dc5bf4b438e0d98222dc028982380d5a25d2
Author:     Kyle Evans <kevans@FreeBSD.org>
AuthorDate: 2026-06-30 13:12:51 +0000
Commit:     Kyle Evans <kevans@FreeBSD.org>
CommitDate: 2026-06-30 13:12:51 +0000

    libc: gen: add a test for rtld underflowing our posix_spawn thread
    
    This is a distillation of the environment described in the PR, using
    a dummy shlib and mapping it repeatedly.  This takes advantage of the
    guard page added in 2767a1f3686e5b16 to reliably crash if rtld tries to
    scale its stack usage excessively with the # DSOs loaded.
    
    PR:             295991
    Reviewed by:    kib
    Differential Revision:  https://reviews.freebsd.org/D57954
---
 lib/libc/tests/gen/Makefile            |  2 +
 lib/libc/tests/gen/libdummy/Makefile   |  9 +++++
 lib/libc/tests/gen/libdummy/libdummy.c | 14 +++++++
 lib/libc/tests/gen/posix_spawn_test.c  | 68 ++++++++++++++++++++++++++++++++++
 4 files changed, 93 insertions(+)

diff --git a/lib/libc/tests/gen/Makefile b/lib/libc/tests/gen/Makefile
index f5d57275cb44..e451fbea644c 100644
--- a/lib/libc/tests/gen/Makefile
+++ b/lib/libc/tests/gen/Makefile
@@ -1,5 +1,7 @@
 .include <bsd.own.mk>
 
+SUBDIR+=		libdummy
+
 ATF_TESTS_C+=		arc4random_test
 ATF_TESTS_C+=		dir2_test
 ATF_TESTS_C+=		dlopen_empty_test
diff --git a/lib/libc/tests/gen/libdummy/Makefile b/lib/libc/tests/gen/libdummy/Makefile
new file mode 100644
index 000000000000..58a22f1a6c32
--- /dev/null
+++ b/lib/libc/tests/gen/libdummy/Makefile
@@ -0,0 +1,9 @@
+SHLIB?=		dummy
+SHLIB_MAJOR=	0
+
+LIBDIR=		${TESTSBASE}/lib/libc/gen
+SHLIBDIR=	${TESTSBASE}/lib/libc/gen
+
+SRCS=		libdummy.c
+
+.include <bsd.lib.mk>
diff --git a/lib/libc/tests/gen/libdummy/libdummy.c b/lib/libc/tests/gen/libdummy/libdummy.c
new file mode 100644
index 000000000000..89e94c016311
--- /dev/null
+++ b/lib/libc/tests/gen/libdummy/libdummy.c
@@ -0,0 +1,14 @@
+/*
+ *
+ * Copyright (C) 2026 Kyle Evans <kevans@FreeBSD.org>
+ *
+ * SPDX-license-Identifier: BSD-2-Clause
+ */
+
+int dummy_random(void);
+
+int
+dummy_random(void)
+{
+	return (42);
+}
diff --git a/lib/libc/tests/gen/posix_spawn_test.c b/lib/libc/tests/gen/posix_spawn_test.c
index 22133cf1d59a..9edd81b57d30 100644
--- a/lib/libc/tests/gen/posix_spawn_test.c
+++ b/lib/libc/tests/gen/posix_spawn_test.c
@@ -30,8 +30,10 @@
  */
 
 #include <sys/param.h>
+#include <sys/mman.h>
 #include <sys/stat.h>
 #include <sys/wait.h>
+#include <dlfcn.h>
 #include <errno.h>
 #include <fcntl.h>
 #include <stdio.h>
@@ -173,6 +175,71 @@ ATF_TC_BODY(posix_spawnp_eacces, tc)
 	}
 }
 
+#define	NUM_DSO	512
+ATF_TC_WITHOUT_HEAD(posix_spawnp_stackunderflow);
+ATF_TC_BODY(posix_spawnp_stackunderflow, tc)
+{
+	struct stat sb;
+	char dsopath[MAXPATHLEN];
+	char *myargs[] = { "true", NULL };
+	void **handles;
+	char *dsomap;
+	size_t dsosz;
+	int error, fd, nfd, status;
+	pid_t pid, waitres;
+
+	/* Make sure we have no child processes. */
+	while (waitpid(-1, NULL, 0) != -1)
+		;
+	ATF_REQUIRE_MSG(errno == ECHILD, "errno was not ECHILD: %d", errno);
+
+	(void)snprintf(dsopath, sizeof(dsopath), "%s/libdummy.so",
+	    atf_tc_get_config_var(tc, "srcdir"));
+
+	fd = open(dsopath, O_RDONLY);
+	ATF_REQUIRE(fd >= 0);
+
+	/*
+	 * We'll open our original shlib and fdlopen() it repeatedly until we
+	 * have a lot of DSOs open, then we'll trigger a posix_spawnp.  This
+	 * previously unearthed suboptimal stack usage in rtld that caused
+	 * posix_spawnp()'s effectively-vforked environment to underflow its
+	 * stack.
+	 *
+	 * We only get one shot to trigger the underflow, as rtld binding the
+	 * symbols in the exec path in the rfork-child will affect the main
+	 * process, so we only test that we don't have a problem with a large
+	 * number of DSOs loaded.
+	 */
+	ATF_REQUIRE(fstat(fd, &sb) == 0);
+	dsosz = sb.st_size;
+	dsomap = mmap(NULL, dsosz, PROT_READ, MAP_SHARED, fd, 0);
+	ATF_REQUIRE(dsomap != MAP_FAILED);
+
+	handles = calloc(sizeof(*handles), NUM_DSO);
+	ATF_REQUIRE(handles != NULL);
+
+	for (int i = 0; i < NUM_DSO; i++) {
+		nfd = memfd_create("dsobase", MFD_CLOEXEC);
+		ATF_REQUIRE(nfd >= 0);
+		ATF_REQUIRE(ftruncate(nfd, dsosz) == 0);
+		ATF_REQUIRE(write(nfd, dsomap, dsosz) == dsosz);
+
+		handles[i] = fdlopen(nfd, RTLD_LAZY);
+		ATF_REQUIRE(handles[i] != NULL);
+		if (i > 0)
+			ATF_REQUIRE(handles[i] != handles[i - 1]);
+
+		close(nfd);
+	}
+
+	error = posix_spawnp(&pid, myargs[0], NULL, NULL, myargs, myenv);
+	ATF_REQUIRE(error == 0);
+	waitres = waitpid(pid, &status, 0);
+	ATF_REQUIRE(waitres == pid);
+	ATF_REQUIRE(WIFEXITED(status) && WEXITSTATUS(status) == 0);
+}
+
 ATF_TP_ADD_TCS(tp)
 {
 
@@ -181,6 +248,7 @@ ATF_TP_ADD_TCS(tp)
 	ATF_TP_ADD_TC(tp, posix_spawnp_enoexec_fallback);
 	ATF_TP_ADD_TC(tp, posix_spawnp_enoexec_fallback_null_argv0);
 	ATF_TP_ADD_TC(tp, posix_spawnp_eacces);
+	ATF_TP_ADD_TC(tp, posix_spawnp_stackunderflow);
 
 	return (atf_no_error());
 }