glibc/stdlib/tst-environ-change-skeleton.c
Florian Weimer 12b4a1fc6e stdlib: Re-implement free (environ) compatibility kludge for setenv
For the originally failing application (userhelper from usermode),
it is not actually necessary to call realloc on the environ
pointer.  Yes, there will be a memory leak because the application
assigns a heap-allocated pointer to environ that it never frees,
but this leak was always there: the old realloc-based setenv had
a hidden internal variable, last_environ, that was used in a similar
way to __environ_array_list.  The application is not impacted by
the leak anyway because the relevant operations do not happen in
a loop.

The change here just uses a separte heap allocation and points
environ to that.  This means that if an application calls
free (environ) and restores the environ pointer to the value
at process start, and does not modify the environment further,
nothing bad happens.

This change should not invalidate any previous testing that went into
the original getenv thread safety change, commit 7a61e7f557
("stdlib: Make getenv thread-safe in more cases").

The new test cases are modeled in part on the env -i use case from
bug 32588 (with !DO_MALLOC && !DO_EARLY_SETENV), and the previous
stdlib/tst-setenv-malloc test.  The DO_MALLOC && !DO_EARLY_SETENV
case in the new test should approximate what userhelper from the
usermode package does.

Reviewed-by: Carlos O'Donell <carlos@redhat.com>
2025-01-24 22:37:49 +01:00

118 lines
4.4 KiB
C

/* Test deallocation of the environ pointer.
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
<https://www.gnu.org/licenses/>. */
/* 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. They also try to
save and restore environ in an attempt to undo a temporary
modification of the environment array, but this does not work if
setenv was called before.
Before including this file, these macros need to be defined
to 0 or 1:
DO_EARLY_SETENV If 1, perform a setenv call before changing environ.
DO_MALLOC If 1, use a heap pointer for the empty environment.
Note that this test will produce errors under valgrind and other
memory tracers that call __libc_freeres because free (environ)
deallocates a pointer still used internally. */
#include <stdlib.h>
#include <unistd.h>
#include <support/check.h>
#include <support/support.h>
static void
check_rewritten (void)
{
TEST_COMPARE_STRING (environ[0], "tst_environ_change_a=1");
TEST_COMPARE_STRING (environ[1], "tst_environ_change_b=2");
TEST_COMPARE_STRING (environ[2], NULL);
TEST_COMPARE_STRING (getenv ("tst_environ_change_a"), "1");
TEST_COMPARE_STRING (getenv ("tst_environ_change_b"), "2");
TEST_COMPARE_STRING (getenv ("tst_environ_change_early"), NULL);
TEST_COMPARE_STRING (getenv ("PATH"), NULL);
}
static int
do_test (void)
{
TEST_COMPARE_STRING (getenv ("tst_environ_change_a"), NULL);
TEST_COMPARE_STRING (getenv ("tst_environ_change_b"), NULL);
TEST_COMPARE_STRING (getenv ("tst_environ_change_early_setenv"), NULL);
#if DO_EARLY_SETENV
TEST_COMPARE (setenv ("tst_environ_change_early_setenv", "1", 1), 0);
#else
/* Must come back after environ reset. */
char *original_path = xstrdup (getenv ("PATH"));
#endif
char **save_environ = environ;
#if DO_MALLOC
environ = xmalloc (sizeof (*environ));
#else
char *environ_array[1];
environ = environ_array;
#endif
*environ = NULL;
TEST_COMPARE (setenv ("tst_environ_change_a", "1", 1), 0);
TEST_COMPARE (setenv ("tst_environ_change_b", "2", 1), 0);
#if !DO_EARLY_SETENV
/* Early setenv results in reuse of the heap-allocated environ array
that does not change as more pointers are added to it. */
TEST_VERIFY (environ != save_environ);
#endif
check_rewritten ();
bool check_environ = true;
#if DO_MALLOC
/* Disable further checks if the free call clobbers the environ
contents. Whether that is the case depends on the internal
setenv allocation policy and the heap layout. */
check_environ = environ != save_environ;
/* Invalid: Causes internal use-after-free condition. Yet this has
to be supported for compatibility with some applications. */
free (environ);
#endif
environ = save_environ;
#if DO_EARLY_SETENV
/* With an early setenv, the internal environ array was overwritten.
Historically, this triggered a use-after-free problem because of
the use of realloc internally in setenv, but it may appear as if
the original environment had been restored. In the current code,
we can only support this if the free (environ) above call did not
clobber the array, otherwise getenv will see invalid pointers.
Due to the use-after-free, invalid pointers could be seen with
the old implementation as well, but the triggering conditions
were different. */
if (check_environ)
check_rewritten ();
#else
TEST_VERIFY (check_environ);
TEST_COMPARE_STRING (getenv ("PATH"), original_path);
TEST_COMPARE_STRING (getenv ("tst_environ_change_a"), NULL);
TEST_COMPARE_STRING (getenv ("tst_environ_change_b"), NULL);
#endif
return 0;
}
#include <support/test-driver.c>