[PATCH] misc: Optimize getusershell.c
Adhemerval Zanella Netto
adhemerval.zanella@linaro.org
Wed Apr 22 13:04:34 GMT 2026
On 20/04/26 10:05, Rocket Ma wrote:
> * misc/getusershell.c: Completely rewrite the unit. Only allocate one
> big buffer to store shell names. Add a missing unit test.
>
> The new implementation read the whole file into one buffer, and wipe out
> every byte but shell names. Later when addressing shell names from first
> shell, jump to next '\0' and then jump to next '/'. This could reduce
> memory footprint and shall improve some performance.
>
> Signed-off-by: Rocket Ma <marocketbd@gmail.com>
This has cause CI regressions [1]:
elf-check-localplt.out
Extra PLT reference: libc.so: endusershell
Extra PLT reference: libc.so: fread
Extra PLT reference: libc.so: setbuf
This is due the use of the non-internal function names that generated extra
PLT calls.
[1] https://www.delorie.com/trybots/32bit/59989/
> ---
> misc/Makefile | 1 +
> misc/getusershell.c | 272 +++++++++++++++++++----------------
> misc/tst-getusershell.c | 75 ++++++++++
> misc/tst-getusershell.shells | 7 +
> 4 files changed, 230 insertions(+), 125 deletions(-)
> create mode 100644 misc/tst-getusershell.c
> create mode 100644 misc/tst-getusershell.shells
>
> diff --git a/misc/Makefile b/misc/Makefile
> index 4395366d74..31c930ef64 100644
> --- a/misc/Makefile
> +++ b/misc/Makefile
> @@ -245,6 +245,7 @@ tests := \
> tst-empty \
> tst-error1 \
> tst-fdset \
> + tst-getusershell \
> tst-hsearch \
> tst-insremque \
> tst-ioctl \
> diff --git a/misc/getusershell.c b/misc/getusershell.c
> index 4221095dca..19394852a4 100644
> --- a/misc/getusershell.c
> +++ b/misc/getusershell.c
> @@ -1,143 +1,165 @@
> -/*
> - * Copyright (c) 1985, 1993
> - * The Regents of the University of California. All rights reserved.
> - *
> - * Redistribution and use in source and binary forms, with or without
> - * modification, are permitted provided that the following conditions
> - * are met:
> - * 1. Redistributions of source code must retain the above copyright
> - * notice, this list of conditions and the following disclaimer.
> - * 2. Redistributions in binary form must reproduce the above copyright
> - * notice, this list of conditions and the following disclaimer in the
> - * documentation and/or other materials provided with the distribution.
> - * 4. Neither the name of the University nor the names of its contributors
> - * may be used to endorse or promote products derived from this software
> - * without specific prior written permission.
> - *
> - * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ``AS IS'' AND
> - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
> - * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
> - * ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE
> - * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
> - * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
> - * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
> - * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
> - * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
> - * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
> - * SUCH DAMAGE.
> - */
> -
> -#if defined(LIBC_SCCS) && !defined(lint)
> -static char sccsid[] = "@(#)getusershell.c 8.1 (Berkeley) 6/4/93";
> -#endif /* LIBC_SCCS and not lint */
> -
> -#include <sys/param.h>
> -#include <sys/file.h>
> -#include <sys/stat.h>
> -#include <stdio.h>
> -#include <stdio_ext.h>
> +/* Copyright (C) 2026 The GNU Toolchain Authors.
> + 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/>. */
> +
> #include <ctype.h>
> -#include <stdlib.h>
> +#include <fcntl.h>
> +#include <stddef.h>
> +#include <stdio.h>
> +#include <stdint.h>
> +#include <sys/stat.h>
> #include <unistd.h>
> #include <paths.h>
> +#include <stdlib.h>
> +#include <string.h>
>
> -/*
> - * Local shells should NOT be added here. They should be added in
> - * /etc/shells.
> - */
> -
> -/* NB: we do not initialize okshells here. The initialization needs
> - relocations. These interfaces are used so rarely that this is not
> - justified. Instead explicitly initialize the array when it is
> - used. */
> -#if 0
> -static const char *const okshells[] = { _PATH_BSHELL, _PATH_CSHELL, NULL };
> -#else
> -static const char *okshells[3];
> -#endif
> -static char **curshell, **shells, *strings;
> -static char **initshells (void) __THROW;
> -
> -/*
> - * Get a list of shells from _PATH_SHELLS, if it exists.
> - */
> -char *
> -getusershell (void)
> +#define DEFAULT_SHELLS _PATH_BSHELL "\0" _PATH_CSHELL
> +
> +static char *shellbuf;
> +static char *shellend;
> +static char *nextshell;
> +
> +static char *
> +address_next_shell (void)
> {
> - char *ret;
> -
> - if (curshell == NULL)
> - curshell = initshells();
> - ret = *curshell;
> - if (ret != NULL)
> - curshell++;
> - return (ret);
> + char *curshell;
> + char *next0;
> +
> + curshell = nextshell;
> + if (nextshell == NULL)
> + return curshell;
> +
> + /* init_shells guarantees we have a \0 at the end */
> + next0 = memchr (nextshell, '\0', shellend - nextshell);
> + if (next0 == NULL)
> + /* Unexpected, perhaps user modified our buffer? */
> + nextshell = NULL;
> + else
> + nextshell = memchr (next0, '/', shellend - next0);
> + return curshell;
> }
>
> -void
> -endusershell (void)
> +/* Read /etc/shells, strip unnecessary bytes, and setup nextshell */
> +static void
> +init_shells (void)
> {
> + FILE *fp;
> + struct __stat64_t64 fstat;
> + size_t buflen;
> + char *top;
> + char *line_start, *line_end;
> + char *slash, *discard;
> +
> + endusershell ();
> +
> + if ((fp = fopen (_PATH_SHELLS, "rce")) == NULL)
> + goto default_out;
> + if ((__fstat64_time64 (fileno (fp), &fstat)) == -1)
> + goto close_out;
> + /* Consider if buflen will overflow. */
> + if (fstat.st_size < 2 || fstat.st_size > PTRDIFF_MAX - 1)
> + goto close_out;
> + /* 1 byte for \n (will be overwritten as \0). */
> + buflen = fstat.st_size + 1;
> + if ((shellbuf = malloc (buflen)) == NULL)
> + goto close_out;
> + shellbuf[buflen - 1] = '\n';
> + setbuf (fp, NULL);
> + if ((fread (shellbuf, 1, fstat.st_size, fp)) != fstat.st_size)
> + goto free_out;
> +
> + top = shellbuf + buflen;
> + line_start = shellbuf;
> + while ((line_end = memchr (line_start, '\n', top - line_start)) != NULL)
> + {
> + line_end++; /* include \n */
> + discard = line_start;
> +
> + slash = memchr (line_start, '/', line_end - line_start);
> + if (slash == NULL)
> + goto wipe_line;
> +
> + discard = memchr (line_start, '#', line_end - line_start);
> + if (discard != NULL && discard < slash)
> + goto wipe_line;
> +
> + (void) memset (line_start, '\0', slash - line_start);
> + discard = slash;
> + while (discard < line_end && *discard != '#'
> + && !isspace ((unsigned char) *discard))
> + discard++;
> +
> + wipe_line:
> + (void) memset (discard, '\0', line_end - discard);
> + line_start = line_end;
> + }
> +
> + if ((nextshell = memchr (shellbuf, '/', top - shellbuf)) == NULL)
> + goto free_out;
> + shellend = top;
> + (void) fclose (fp);
> + return;
>
> - free(shells);
> - shells = NULL;
> - free(strings);
> - strings = NULL;
> - curshell = NULL;
> +free_out:
> + free (shellbuf);
> + shellbuf = NULL;
> +close_out:
> + (void) fclose (fp);
> +default_out:
> + shellbuf = malloc (sizeof (DEFAULT_SHELLS));
> + if (shellbuf == NULL)
> + {
> + /* Can't allocate a buffer to store default shells,
> + use read-only string to avoid unexpected user write.
> + Leave shellbuf as NULL so that it can be freed. */
> + nextshell = (char *) DEFAULT_SHELLS;
> + shellend = (char *) DEFAULT_SHELLS + sizeof (DEFAULT_SHELLS);
> + }
> + else
> + {
> + memcpy (shellbuf, DEFAULT_SHELLS, sizeof (DEFAULT_SHELLS));
> + nextshell = shellbuf;
> + shellend = shellbuf + sizeof (DEFAULT_SHELLS);
> + }
> +}
> +
> +char *
> +getusershell (void)
> +{
> + if (shellend == NULL)
> + init_shells ();
> + return address_next_shell ();
> }
>
> void
> setusershell (void)
> {
> -
> - curshell = initshells();
> + if (shellend == NULL)
> + init_shells ();
> + else if (shellbuf != NULL)
> + nextshell = memchr (shellbuf, '/', shellend - shellbuf);
> + else /* shellend != NULL && shellbuf == NULL */
> + nextshell = (char *) DEFAULT_SHELLS;
> }
>
> -static char **
> -initshells (void)
> +void
> +endusershell (void)
> {
> - char **sp, *cp;
> - FILE *fp;
> - struct __stat64_t64 statb;
> - size_t flen;
> -
> - free(shells);
> - shells = NULL;
> - free(strings);
> - strings = NULL;
> - if ((fp = fopen(_PATH_SHELLS, "rce")) == NULL)
> - goto init_okshells_noclose;
> - if (__fstat64_time64(fileno(fp), &statb) == -1) {
> - init_okshells:
> - (void)fclose(fp);
> - init_okshells_noclose:
> - okshells[0] = _PATH_BSHELL;
> - okshells[1] = _PATH_CSHELL;
> - return (char **) okshells;
> - }
> - if (statb.st_size > ~(size_t)0 / sizeof (char *) * 3)
> - goto init_okshells;
> - flen = statb.st_size + 3;
> - if ((strings = malloc(flen)) == NULL)
> - goto init_okshells;
> - shells = malloc(statb.st_size / 3 * sizeof (char *));
> - if (shells == NULL) {
> - free(strings);
> - strings = NULL;
> - goto init_okshells;
> - }
> - sp = shells;
> - cp = strings;
> - while (fgets_unlocked(cp, flen - (cp - strings), fp) != NULL) {
> - while (*cp != '#' && *cp != '/' && *cp != '\0')
> - cp++;
> - if (*cp == '#' || *cp == '\0' || cp[1] == '\0')
> - continue;
> - *sp++ = cp;
> - while (!isspace(*cp) && *cp != '#' && *cp != '\0')
> - cp++;
> - *cp++ = '\0';
> - }
> - *sp = NULL;
> - (void)fclose(fp);
> - return (shells);
> + free (shellbuf);
> + shellbuf = NULL;
> + shellend = NULL;
> + nextshell = NULL;
> }
> diff --git a/misc/tst-getusershell.c b/misc/tst-getusershell.c
> new file mode 100644
> index 0000000000..b2f71804d8
> --- /dev/null
> +++ b/misc/tst-getusershell.c
> @@ -0,0 +1,75 @@
> +/* Test the getusershell series functions.
> + Copyright (C) 2026 The GNU Toolchain Authors.
> + 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/>. */
> +
> +#include "support/temp_file.h"
> +#include <stdlib.h>
> +#include <unistd.h>
> +#include <paths.h>
> +#include <support/check.h>
> +#include <support/xunistd.h>
> +#include <support/support.h>
> +#include <support/namespace.h>
> +#include <support/test-driver.h>
> +
> +static void
> +test_in_chroot (void *chroot_path)
> +{
> + xchroot (chroot_path);
> +
> + TEST_COMPARE_STRING (getusershell (), "/bin/sh");
> + TEST_COMPARE_STRING (getusershell (), "/bin/bash");
> + TEST_COMPARE_STRING (getusershell (), "/usr/bin/zsh");
> + TEST_COMPARE_STRING (getusershell (), NULL);
> + TEST_COMPARE_STRING (getusershell (), NULL);
> +
> + setusershell ();
> + TEST_COMPARE_STRING (getusershell (), "/bin/sh");
> + endusershell ();
> +
> + xunlink ("/etc/shells");
> + TEST_COMPARE_STRING (getusershell (), "/bin/sh");
> + TEST_COMPARE_STRING (getusershell (), "/bin/csh");
> + TEST_COMPARE_STRING (getusershell (), NULL);
> + endusershell ();
> +}
> +
> +static int
> +do_test (void)
> +{
> + support_become_root ();
> + if (!support_can_chroot ())
> + return EXIT_UNSUPPORTED;
> +
> + char *chroot_dir = support_create_temp_directory("tst-getusershell-");
> + char *etc = xasprintf("%s/etc", chroot_dir);
> + add_temp_file(etc);
> + xmkdir(etc, 0777);
> + /* Don't add shells to file list as it will be deleted in test. */
> + char *shells = xasprintf("%s/shells", etc);
> + support_copy_file("tst-getusershell.shells", shells);
> +
> + support_isolate_in_subprocess(test_in_chroot, chroot_dir);
> +
> + free(etc);
> + free(shells);
> + free(chroot_dir);
> +
> + return 0;
> +}
> +
> +#include <support/test-driver.c>
> diff --git a/misc/tst-getusershell.shells b/misc/tst-getusershell.shells
> new file mode 100644
> index 0000000000..fcc7675398
> --- /dev/null
> +++ b/misc/tst-getusershell.shells
> @@ -0,0 +1,7 @@
> +# test hash
> + # indentation
> +
> +/bin/sh
> + /bin/bash # ...
> +
> +xx /usr/bin/zsh#...
> \ No newline at end of file
More information about the Libc-alpha
mailing list