diff --git a/csu/init-first.c b/csu/init-first.c
index e35e4ce84f..0ad6f75dcd 100644
--- a/csu/init-first.c
+++ b/csu/init-first.c
@@ -61,6 +61,7 @@ _init_first (int argc, char **argv, char **envp)
__libc_argc = argc;
__libc_argv = argv;
__environ = envp;
+ __environ_startup = envp;
#ifndef SHARED
/* First the initialization which normally would be done by the
diff --git a/csu/libc-start.c b/csu/libc-start.c
index 6f3d52e223..4e15b6191d 100644
--- a/csu/libc-start.c
+++ b/csu/libc-start.c
@@ -244,6 +244,7 @@ LIBC_START_MAIN (int (*main) (int, char **, char ** MAIN_AUXVEC_DECL),
char **ev = &argv[argc + 1];
__environ = ev;
+ __environ_startup = ev;
/* Store the lowest stack address. This is done in ld.so if this is
the code for the DSO. */
diff --git a/include/unistd.h b/include/unistd.h
index e241603b81..ada957f9d0 100644
--- a/include/unistd.h
+++ b/include/unistd.h
@@ -203,6 +203,9 @@ libc_hidden_proto (__tcsetpgrp)
extern int __libc_enable_secure attribute_relro;
rtld_hidden_proto (__libc_enable_secure)
+/* Original value of __environ. Initialized by _init_first (dynamic)
+ or __libc_start_main (static). */
+extern char **__environ_startup attribute_hidden;
/* Various internal function. */
extern void __libc_check_standard_fds (void) attribute_hidden;
diff --git a/posix/environ.c b/posix/environ.c
index a0ed0d80ea..2430b47d8e 100644
--- a/posix/environ.c
+++ b/posix/environ.c
@@ -10,3 +10,5 @@ weak_alias (__environ, environ)
/* The SVR4 ABI says `_environ' will be the name to use
in case the user overrides the weak alias `environ'. */
weak_alias (__environ, _environ)
+
+char **__environ_startup;
diff --git a/stdlib/Makefile b/stdlib/Makefile
index a5fbc1a27e..ee95b2e79a 100644
--- a/stdlib/Makefile
+++ b/stdlib/Makefile
@@ -316,6 +316,7 @@ tests := \
tst-setcontext9 \
tst-setcontext10 \
tst-setcontext11 \
+ tst-setenv-malloc \
tst-stdbit-Wconversion \
tst-stdbit-builtins \
tst-stdc_bit_ceil \
diff --git a/stdlib/setenv.c b/stdlib/setenv.c
index 2a2eec9c98..c6dc9f7945 100644
--- a/stdlib/setenv.c
+++ b/stdlib/setenv.c
@@ -191,52 +191,52 @@ __add_to_environ (const char *name, const char *value, const char *combined,
ep[1] = NULL;
else
{
- /* We cannot use __environ as is and need to copy over the
- __environ contents into an array managed via
- __environ_array_list. */
+ /* We cannot use __environ as is and need a larger allocation. */
- struct environ_array *target_array;
- if (__environ_array_list != NULL
- && required_size <= __environ_array_list->allocated)
- /* Existing array has enough room. Contents is copied below. */
- target_array = __environ_array_list;
+ if (start_environ == __environ_startup
+ || __environ_is_from_array_list (start_environ))
+ {
+ /* Allocate a new array, managed in the list. */
+ struct environ_array *target_array
+ = __environ_new_array (required_size);
+ if (target_array == NULL)
+ {
+ UNLOCK;
+ return -1;
+ }
+ result_environ = &target_array->array[0];
+
+ /* Copy over the __environ array contents. This code
+ handles the case start_environ == ep == NULL, too. */
+ size_t i;
+ for (i = 0; start_environ + i < ep; ++i)
+ /* Regular store because unless there has been direct
+ manipulation of the environment, target_array is still
+ a private copy. */
+ result_environ[i] = atomic_load_relaxed (start_environ + i);
+ }
else
{
- /* Allocate a new array. */
- target_array = __environ_new_array (required_size);
- if (target_array == NULL)
+ /* Otherwise the application installed its own pointer.
+ Historically, this pointer was managed using realloc.
+ Continue doing so. This disables multi-threading
+ support. */
+ result_environ = __libc_reallocarray (start_environ,
+ required_size,
+ sizeof (*result_environ));
+ if (result_environ == NULL)
{
UNLOCK;
return -1;
}
}
- /* Copy over the __environ array contents. This forward
- copy slides backwards part of the array if __environ
- points into target_array->array. This happens if an
- application makes an assignment like:
-
- environ = &environ[1];
-
- The forward copy avoids clobbering values that still
- needing copying. This code handles the case
- start_environ == ep == NULL, too. */
- size_t i;
- for (i = 0; start_environ + i < ep; ++i)
- /* Regular store because unless there has been direct
- manipulation of the environment, target_array is still
- a private copy. */
- target_array->array[i] = atomic_load_relaxed (start_environ + i);
-
/* This is the new place where we should add the element. */
- ep = target_array->array + i;
+ ep = result_environ + (required_size - 2);
/* Add the null terminator in case there was a pointer there
previously. */
ep[1] = NULL;
-
- /* And __environ should be repointed to our array. */
- result_environ = &target_array->array[0];
}
}
diff --git a/stdlib/tst-setenv-malloc.c b/stdlib/tst-setenv-malloc.c
new file mode 100644
index 0000000000..18a9d36842
--- /dev/null
+++ b/stdlib/tst-setenv-malloc.c
@@ -0,0 +1,64 @@
+/* Test using setenv with a malloc-allocated environ variable.
+ Copyright (C) 2025 Free Software Foundation, Inc.
+ This file is part of the GNU C Library.
+
+ The GNU C Library is free software; you can redistribute it and/or
+ modify it under the terms of the GNU Lesser General Public
+ License as published by the Free Software Foundation; either
+ version 2.1 of the License, or (at your option) any later version.
+
+ The GNU C Library is distributed in the hope that it will be useful,
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
+ Lesser General Public License for more details.
+
+ You should have received a copy of the GNU Lesser General Public
+ License along with the GNU C Library; if not, see
+ . */
+
+/* This test is not in the scope for POSIX or any other standard, but
+ some applications assume that environ is a heap-allocated pointer
+ after a call to setenv on an empty environment. */
+
+#include
+#include
+#include
+#include
+
+static const char *original_path;
+static char **save_environ;
+
+static void
+rewrite_environ (void)
+{
+ save_environ = environ;
+ environ = xmalloc (sizeof (*environ));
+ *environ = NULL;
+ TEST_COMPARE (setenv ("A", "1", 1), 0);
+ TEST_COMPARE (setenv ("B", "2", 1), 0);
+ TEST_VERIFY (environ != save_environ);
+ TEST_COMPARE_STRING (environ[0], "A=1");
+ TEST_COMPARE_STRING (environ[1], "B=2");
+ TEST_COMPARE_STRING (environ[2], NULL);
+ TEST_COMPARE_STRING (getenv ("PATH"), NULL);
+ free (environ);
+ environ = save_environ;
+ TEST_COMPARE_STRING (getenv ("PATH"), original_path);
+}
+
+static int
+do_test (void)
+{
+ original_path = getenv ("PATH");
+ rewrite_environ ();
+
+ /* Test again after reallocated the environment due to an initial
+ setenv call. */
+ TEST_COMPARE (setenv ("TST_SETENV_MALLOC", "1", 1), 0);
+ TEST_VERIFY (environ != save_environ);
+ rewrite_environ ();
+
+ return 0;
+}
+
+#include