From: Florian Weimer Date: Fri, 13 Feb 2026 08:02:07 +0000 (+0100) Subject: nss: Missing checks in __nss_configure_lookup, __nss_database_get (bug 28940) X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=5b713b49443eb6a4e54e50e2f0147105f86dab02;p=thirdparty%2Fglibc.git nss: Missing checks in __nss_configure_lookup, __nss_database_get (bug 28940) This avoids a null pointer dereference in the nss_database_check_reload_and_get function, and assertion failures. Reviewed-by: Sam James --- diff --git a/nss/Makefile b/nss/Makefile index b0d80bd642..1c48bd0876 100644 --- a/nss/Makefile +++ b/nss/Makefile @@ -326,6 +326,7 @@ tests := \ tst-gshadow \ tst-nss-getpwent \ tst-nss-hash \ + tst-nss-malloc-failure-getlogin_r \ tst-nss-test1 \ tst-nss-test2 \ tst-nss-test4 \ diff --git a/nss/nss_database.c b/nss/nss_database.c index 19e752ef65..076d5a63fe 100644 --- a/nss/nss_database.c +++ b/nss/nss_database.c @@ -241,9 +241,12 @@ __nss_configure_lookup (const char *dbname, const char *service_line) /* Force any load/cache/read whatever to happen, so we can override it. */ - __nss_database_get (db, &result); + if (!__nss_database_get (db, &result)) + return -1; local = nss_database_state_get (); + if (local == NULL) + return -1; result = __nss_action_parse (service_line); if (result == NULL) @@ -465,6 +468,8 @@ bool __nss_database_get (enum nss_database db, nss_action_list *actions) { struct nss_database_state *local = nss_database_state_get (); + if (local == NULL) + return false; return nss_database_check_reload_and_get (local, actions, db); } libc_hidden_def (__nss_database_get) diff --git a/nss/tst-nss-malloc-failure-getlogin_r.c b/nss/tst-nss-malloc-failure-getlogin_r.c new file mode 100644 index 0000000000..0e2985ad57 --- /dev/null +++ b/nss/tst-nss-malloc-failure-getlogin_r.c @@ -0,0 +1,345 @@ +/* Test NSS/getlogin_r with injected allocation failures (bug 28940). + Copyright (C) 2026 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 + . */ + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +/* This test calls getpwuid_r via getlogin_r (on Linux). + + This test uses the NSS system configuration to exercise that code + path. It means that it can fail (crash) if malloc failure is not + handled by NSS modules for the passwd database. */ + +/* Data structure allocated via MAP_SHARED, so that writes from the + subprocess are visible. */ +struct shared_data +{ + /* Number of tracked allocations performed so far. */ + volatile unsigned int allocation_count; + + /* If this number is reached, one allocation fails. */ + volatile unsigned int failing_allocation; + + /* The number of allocations performed during initialization + (before the actual getlogin_r call). */ + volatile unsigned int init_allocation_count; + + /* Error code of an expected getlogin_r failure. */ + volatile int expected_failure; + + /* The subprocess stores the expected name here. */ + char name[100]; +}; + +/* Allocation count in shared mapping. */ +static struct shared_data *shared; + +/* Returns true if a failure should be injected for this allocation. */ +static bool +fail_this_allocation (void) +{ + if (shared != NULL) + { + unsigned int count = shared->allocation_count; + shared->allocation_count = count + 1; + return count == shared->failing_allocation; + } + else + return false; +} + +/* Failure-injecting wrappers for allocation functions used by glibc. */ + +void * +malloc (size_t size) +{ + if (fail_this_allocation ()) + { + errno = ENOMEM; + return NULL; + } + extern __typeof (malloc) __libc_malloc; + return __libc_malloc (size); +} + +void * +calloc (size_t a, size_t b) +{ + if (fail_this_allocation ()) + { + errno = ENOMEM; + return NULL; + } + extern __typeof (calloc) __libc_calloc; + return __libc_calloc (a, b); +} + +void * +realloc (void *ptr, size_t size) +{ + if (fail_this_allocation ()) + { + errno = ENOMEM; + return NULL; + } + extern __typeof (realloc) __libc_realloc; + return __libc_realloc (ptr, size); +} + +/* No-op subprocess to verify that support_isolate_in_subprocess does + not perform any heap allocations. */ +static void +no_op (void *ignored) +{ +} + +/* Perform a getlogin_r call in a subprocess, to obtain the number of + allocations used and the expected result of a successful call. */ +static void +initialize (void *configure_lookup) +{ + shared->init_allocation_count = 0; + if (configure_lookup != NULL) + { + TEST_COMPARE (__nss_configure_lookup ("passwd", configure_lookup), 0); + shared->init_allocation_count = shared->allocation_count; + } + + shared->name[0] = '\0'; + int ret = getlogin_r (shared->name, sizeof (shared->name)); + if (ret != 0) + { + printf ("info: getlogin_r failed: %s (%d)\n", + strerrorname_np (ret), ret); + shared->expected_failure = ret; + } + else + { + shared->expected_failure = 0; + if (shared->name[0] == '\0') + FAIL ("error: getlogin_r succeeded without result\n"); + else + printf ("info: getlogin_r: \"%s\"\n", shared->name); + } +} + +/* Perform getlogin_r in a subprocess with fault injection. */ +static void +test_in_subprocess (void *configure_lookup) +{ + if (configure_lookup != NULL + && __nss_configure_lookup ("passwd", configure_lookup) < 0) + { + printf ("info: __nss_configure_lookup failed: %s (%d)\n", + strerrorname_np (errno), errno); + TEST_COMPARE (errno, ENOMEM); + TEST_VERIFY (shared->allocation_count <= shared->init_allocation_count); + return; + } + + unsigned int inject_at = shared->failing_allocation; + char name[sizeof (shared->name)] = "name not set"; + int ret = getlogin_r (name, sizeof (name)); + shared->failing_allocation = ~0U; + + if (ret == 0) + { + TEST_COMPARE (shared->expected_failure, 0); + TEST_COMPARE_STRING (name, shared->name); + } + else + { + printf ("info: allocation %u failure results in error %s (%d)\n", + inject_at, strerrorname_np (ret), ret); + + if (ret != ENOMEM) + { + if (shared->expected_failure != 0) + TEST_COMPARE (ret, shared->expected_failure); + else if (configure_lookup == NULL) + /* The ENOENT failure can happen due to an issue related + to bug 22041: dlopen failure does not result in ENOMEM. */ + TEST_COMPARE (ret, ENOENT); + else + FAIL ("unexpected getlogin_r error"); + } + } + + if (shared->expected_failure == 0) + { + /* The second call should succeed. */ + puts ("info: about to perform second getlogin_r call"); + ret = getlogin_r (name, sizeof (name)); + if (configure_lookup == NULL) + { + /* This check can fail due to bug 22041 if the malloc error + injection causes a failure internally in dlopen. */ + if (ret != 0) + { + printf ("warning: second getlogin_r call failed with %s (%d)\n", + strerrorname_np (ret), ret); + TEST_COMPARE (ret, ENOENT); + } + } + else + /* If __nss_configure_lookup has been called, the error caching + bug does not happen because nss_files is built-in, and the + second getlogin_r is expected to succeed. */ + TEST_COMPARE (ret, 0); + if (ret == 0) + TEST_COMPARE_STRING (name, shared->name); + } +} + +/* Set by the --failing-allocation command line option. Together with + --direct, this can be used to trigger an allocation failure in the + original process, which may help with debugging. */ +static int option_failing_allocation = -1; + +/* Set by --override, to be used with --failing-allocation. Turns on + the __nss_configure_lookup call for passwd/files, which is disabled + by default. */ +static int option_override = 0; + +static int +do_test (void) +{ + char files[] = "files"; + + if (option_failing_allocation >= 0) + { + /* The test was invoked with --failing-allocation. Perform just + one test, using the original nsswitch.conf. This is a + condensed version of the probing/testing loop below. */ + printf ("info: testing with failing allocation %d\n", + option_failing_allocation); + shared = support_shared_allocate (sizeof (*shared)); + shared->failing_allocation = ~0U; + char *configure_lookup = option_override ? files : NULL; + support_isolate_in_subprocess (initialize, configure_lookup); + shared->allocation_count = 0; + shared->failing_allocation = option_failing_allocation; + test_in_subprocess (configure_lookup); /* No subprocess. */ + support_shared_free (shared); + shared = NULL; + return 0; + } + + bool any_success = false; + + for (int do_configure_lookup = 0; do_configure_lookup < 2; + ++do_configure_lookup) + { + if (do_configure_lookup) + puts ("info: testing with nsswitch.conf override"); + else + puts ("info: testing with original nsswitch.conf"); + + char *configure_lookup = do_configure_lookup ? files : NULL; + + shared = support_shared_allocate (sizeof (*shared)); + + /* Disable fault injection. */ + shared->failing_allocation = ~0U; + + support_isolate_in_subprocess (no_op, NULL); + TEST_COMPARE (shared->allocation_count, 0); + + support_isolate_in_subprocess (initialize, configure_lookup); + + if (shared->name[0] != '\0') + any_success = true; + + /* The number of allocations in the successful case. Once the + number of expected allocations is exceeded, injecting further + failures does not make a difference (assuming that the number + of malloc calls is deterministic). */ + unsigned int maximum_allocation_count = shared->allocation_count; + printf ("info: initial getlogin_r performed %u allocations\n", + maximum_allocation_count); + + for (unsigned int inject_at = 0; inject_at <= maximum_allocation_count; + ++inject_at) + { + printf ("info: running fault injection at allocation %u\n", + inject_at); + shared->allocation_count = 0; + shared->failing_allocation = inject_at; + support_isolate_in_subprocess (test_in_subprocess, configure_lookup); + } + + support_shared_free (shared); + shared = NULL; + } + + { + FILE *fp = fopen (_PATH_NSSWITCH_CONF, "r"); + if (fp == NULL) + printf ("info: no %s file\n", _PATH_NSSWITCH_CONF); + else + { + printf ("info: %s contents follows\n", _PATH_NSSWITCH_CONF); + int last_ch = '\n'; + while (true) + { + int ch = fgetc (fp); + if (ch == EOF) + break; + putchar (ch); + last_ch = ch; + } + if (last_ch != '\n') + putchar ('\n'); + printf ("(end of %s contents)\n", _PATH_NSSWITCH_CONF); + xfclose (fp); + } + } + + support_record_failure_barrier (); + + if (!any_success) + FAIL_UNSUPPORTED ("no successful getlogin_r calls"); + + return 0; +} + +static void +cmdline_process (int c) +{ + if (c == 'F') + option_failing_allocation = atoi (optarg); +} + +#define CMDLINE_OPTIONS \ + { "failing-allocation", required_argument, NULL, 'F' }, \ + { "override", no_argument, &option_override, 1 }, + +#define CMDLINE_PROCESS cmdline_process + +#include