diff mbox series

[pseudo,2/2] ports/linux/pseudo_wrappers: forward pre-init syscall/prctl to the real function

Message ID 20260929-pseudo-init-recursion-fix-v1-2-34e30f4c5349@peridio.com
State New
Headers show
Series Fix an unbounded recursion in pseudo's own init path | expand

Commit Message

Javier Tia Sept. 29, 2026, 9:45 p.m. UTC
pseudo's syscall() and prctl() wrappers both call pseudo_check_wrappers(),
which runs _libpseudo_init() when _libpseudo_initted is still 0. That is
correct for a wrapped call pseudo actually needs to look at, but init
itself allocates (pseudo_init_util()'s pseudo_set_value() calls
strdup()), and an allocator that statically overrides the process's
global malloc/strdup and uses syscall()/prctl() during its own
not-yet-complete process init re-enters pseudo's own init on the same
thread, before that allocator has anywhere safe to return an allocation
from. Observed with mold 2.42.1's bundled mimalloc, which does exactly
this under pseudo's LD_PRELOAD interception: mi_process_init() calls
syscall(SYS_open, ...) to probe /proc/sys/vm/overcommit_memory.

Forward a pre-init syscall()/prctl() straight to the real libc function
via dlsym(RTLD_NEXT, ...) instead, for every number/option this port
does not itself rewrite (openat2, renameat2, seccomp stay on the
existing path so their behavior is unchanged; PR_SET_SECCOMP is guarded
with #ifdef the same way the syscall() side already guards SYS_renameat2
and SYS_seccomp, since this is now the first unconditional reference to
it - previously it only appeared inside the existing
`#ifdef SECCOMP_SET_MODE_FILTER` block below). The guard is
`!_libpseudo_initted || !real_syscall` rather than just the first half:
_libpseudo_init() sets _libpseudo_initted before it resolves real_syscall
via pseudo_init_wrappers(), so an allocator whose first touch happens
from inside the constructor itself, rather than before it runs, would
otherwise still reach pseudo_enosys() and get ENOSYS instead of a real
result. The resolved pointer is stored directly into real_syscall/
real_prctl, the same globals pseudo_init_wrappers() would otherwise
populate, and pseudo_init_one_wrapper() already skips re-resolving a
wrapper whose real pointer is non-NULL, so this path and the normal init
path cannot race to different results.

This removes the re-entrant call into _libpseudo_init() for the caller
that triggers it before pseudo has any state of its own to protect,
without touching behavior once pseudo is initialized. It covers only
syscall()/prctl(); every other generated wrapper still calls
pseudo_check_wrappers() on first use (templates/wrapfuncs.c), so an
allocator whose first touch is a different wrapped libc call (open(),
readlink(), etc.) would still re-enter init through that path. On glibc
before 2.34, dlsym() itself can allocate on a thread's first
dynamic-linker call (the lazy dlerror() result struct), which would
re-enter the same allocator this patch is trying to protect; this is
not fixed here.

Verified against pseudo's own test suite (no change to the 18/45 tests
that pass in this sandbox, which lacks chroot/openat2/renameat2 support -
confirmed identical to pristine master) and against a real
`gcc -fuse-ld=mold` build, plus a subsequent `chown 0:0` on the output,
run under pseudo with PSEUDO_DEBUG set, matching what bitbake exports
for every fakeroot task.

Signed-off-by: Javier Tia <javier@peridio.com>
---
 ports/linux/pseudo_wrappers.c | 48 +++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 48 insertions(+)
diff mbox series

Patch

diff --git a/ports/linux/pseudo_wrappers.c b/ports/linux/pseudo_wrappers.c
index b920cb2..63912c6 100644
--- a/ports/linux/pseudo_wrappers.c
+++ b/ports/linux/pseudo_wrappers.c
@@ -59,6 +59,31 @@  syscall(long number, ...) {
 	long rc = -1;
 	va_list ap;
 
+	/* Reached before real_syscall is resolved: the caller may be a malloc
+	 * implementation initializing itself, and pseudo_check_wrappers()
+	 * would run _libpseudo_init() -> pseudo_init_util(), which allocates.
+	 * Forward the numbers pseudo never rewrites without running that path.
+	 *
+	 * The !real_syscall half of the guard covers a narrower window than
+	 * !_libpseudo_initted alone would: _libpseudo_init() (below) sets
+	 * _libpseudo_initted before it resolves real_syscall via
+	 * pseudo_init_wrappers(), so an allocator whose first touch happens
+	 * from inside the constructor itself - rather than before it runs -
+	 * would otherwise still fall through to pseudo_enosys() below. */
+	if ((!_libpseudo_initted || !real_syscall) && number != SYS_openat2
+#ifdef SYS_renameat2
+	    && number != SYS_renameat2
+#endif
+#ifdef SYS_seccomp
+	    && number != SYS_seccomp
+#endif
+	    ) {
+		if (!real_syscall)
+			real_syscall = (long (*)(long, ...)) dlsym(RTLD_NEXT, "syscall");
+		if (real_syscall)
+			goto call_syscall;
+	}
+
 	if (!pseudo_check_wrappers() || !real_syscall) {
 		/* rc was initialized to the "failure" value */
 		pseudo_enosys("syscall");
@@ -147,6 +172,20 @@  prctl(int option, ...) {
 	int rc = -1;
 	va_list ap;
 
+	/* See syscall() above: covers both the pre-constructor window and the
+	 * mid-constructor one, for every option except the one prctl() itself
+	 * still special-cases below. */
+	if ((!_libpseudo_initted || !real_prctl)
+#ifdef PR_SET_SECCOMP
+	    && option != PR_SET_SECCOMP
+#endif
+	    ) {
+		if (!real_prctl)
+			real_prctl = (int (*)(int, ...)) dlsym(RTLD_NEXT, "prctl");
+		if (real_prctl)
+			goto call_prctl;
+	}
+
 	if (!pseudo_check_wrappers() || !real_prctl) {
 		/* rc was initialized to the "failure" value */
 		pseudo_enosys("prctl");
@@ -167,6 +206,15 @@  prctl(int option, ...) {
 	}
 #endif
 
+call_prctl:
+	/* On Debian 11 - gcc (Debian 10.2.1-6) this results in:
+	 *   error: a label can only be part of a statement and a declaration
+	 *   is not a statement
+	 *
+	 * adding a ; here resolves this
+	 */
+	;
+
 	/* gcc magic to attempt to just pass these args to prctl. we have to
 	 * guess about the number of args; the docs discuss calling conventions
 	 * up to 5, so let's try that?