diff mbox series

[pseudo,1/2] pseudo_util: bound pseudo_get_value()'s recovery re-init

Message ID 20260929-pseudo-init-recursion-fix-v1-1-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_get_value() falls back to calling pseudo_init_util() again when a
cached value is empty but the environment variable is actually set - a
recovery path for a value that failed to cache the first time. But
pseudo_init_util()'s own tail calls pseudo_get_value("PSEUDO_DEBUG") and
pseudo_get_value("PSEUDO_EVLOG") while it is still running, and it clears
its own initted guard (pseudo_util_initted = 0) before those calls rather
than after. If storing a value failed (pseudo_set_value()'s strdup()
returned NULL, which happens when init runs re-entrantly on the same
thread, inside another allocator's own not-yet-finished process init),
the recovery branch re-enters pseudo_init_util() unconditionally, which
fails the same strdup() again, calls pseudo_get_value() again, and
recurses until the stack overflows.

Observed with mold 2.42.1: its bundled mimalloc statically overrides
malloc/strdup process-wide, and under pseudo's LD_PRELOAD interception,
mimalloc's own lazy process init can call a libc function pseudo wraps
before mimalloc has anywhere safe to return an allocation from. See the
companion patch to ports/linux/pseudo_wrappers.c for the syscall()/
prctl() half of that same interaction.

Move the initted-guard reset to the end of pseudo_init_util(), after its
own lookups, and gate the recovery re-init in pseudo_get_value() on that
guard. A value that still could not be cached now surfaces through the
existing "failed to save new value" diagnostic and this function returns
NULL, instead of recursing.

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 run under pseudo with PSEUDO_DEBUG set,
matching what bitbake exports for every fakeroot task.

Signed-off-by: Javier Tia <javier@peridio.com>
---
 pseudo_util.c | 11 ++++++++---
 1 file changed, 8 insertions(+), 3 deletions(-)
diff mbox series

Patch

diff --git a/pseudo_util.c b/pseudo_util.c
index afd0266..b8afe34 100644
--- a/pseudo_util.c
+++ b/pseudo_util.c
@@ -159,8 +159,13 @@  pseudo_get_value(const char *key) {
 
 	/* Check if the environment has it and we don't ...
 	 * if so, something went wrong... so we'll attempt to recover
+	 * -- but not while pseudo_init_util() is running.  Its own lookups
+	 * below reach this point, and when a value could not be stored
+	 * (strdup() failed) re-running it fails the same way and recurses
+	 * until the stack overflows.
 	 */
-	if (pseudo_env[i].key && !pseudo_env[i].value && GETENV(pseudo_env[i].key))
+	if (pseudo_util_initted == 0 && pseudo_env[i].key &&
+	    !pseudo_env[i].value && GETENV(pseudo_env[i].key))
 		pseudo_init_util();
 
 	if (pseudo_env[i].value)
@@ -218,8 +223,6 @@  pseudo_init_util(void) {
 			pseudo_set_value(pseudo_env[i].key, GETENV(pseudo_env[i].key));
 	}
 
-	pseudo_util_initted = 0;
-
 	/* Somewhere we have to set the debug level.. */
 	env = pseudo_get_value("PSEUDO_DEBUG");
 	if (env) {
@@ -241,6 +244,8 @@  pseudo_init_util(void) {
 		pseudo_evlog_flags_finalize();
 	}
 	free(env);
+
+	pseudo_util_initted = 0;
 }
 
 unsigned long pseudo_util_debug_flags = 0;