diff options
Diffstat (limited to 'nss/nss_database.c')
-rw-r--r-- | nss/nss_database.c | 433 |
1 files changed, 433 insertions, 0 deletions
diff --git a/nss/nss_database.c b/nss/nss_database.c new file mode 100644 index 0000000000..329bfb1262 --- /dev/null +++ b/nss/nss_database.c @@ -0,0 +1,433 @@ +/* Mapping NSS services to action lists. + Copyright (C) 2020 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/>. */ + +#include "nss_database.h" + +#include <allocate_once.h> +#include <array_length.h> +#include <assert.h> +#include <atomic.h> +#include <ctype.h> +#include <file_change_detection.h> +#include <libc-lock.h> +#include <netdb.h> +#include <stdio_ext.h> +#include <string.h> + +struct nss_database_state +{ + struct nss_database_data data; + __libc_lock_define (, lock); +}; + + +/* Global NSS database state. Underlying type is "struct + nss_database_state *" but the allocate_once API requires + "void *". */ +static void *global_database_state; + +/* Allocate and return pointer to nss_database_state object or + on failure return NULL. */ +static void * +global_state_allocate (void *closure) +{ + struct nss_database_state *result = malloc (sizeof (*result)); + if (result != NULL) + { + result->data.nsswitch_conf.size = -1; /* Force reload. */ + memset (result->data.services, 0, sizeof (result->data.services)); + result->data.initialized = true; + result->data.reload_disabled = false; + __libc_lock_init (result->lock); + } + return result; +} + +/* Return pointer to global NSS database state, allocating as + required, or returning NULL on failure. */ +static struct nss_database_state * +nss_database_state_get (void) +{ + return allocate_once (&global_database_state, global_state_allocate, + NULL, NULL); +} + +/* Database default selections. nis/compat mappings get turned into + "files" for !LINK_OBSOLETE_NSL configurations. */ +enum nss_database_default +{ + nss_database_default_defconfig = 0, /* "nis [NOTFOUND=return] files". */ + nss_database_default_compat, /* "compat [NOTFOUND=return] files". */ + nss_database_default_dns, /* "dns [!UNAVAIL=return] files". */ + nss_database_default_files, /* "files". */ + nss_database_default_nis, /* "nis". */ + nss_database_default_nis_nisplus, /* "nis nisplus". */ + nss_database_default_none, /* Empty list. */ + + NSS_DATABASE_DEFAULT_COUNT /* Number of defaults. */ +}; + +/* Databases not listed default to nss_database_default_defconfig. */ +static const char per_database_defaults[NSS_DATABASE_COUNT] = + { + [nss_database_group] = nss_database_default_compat, + [nss_database_gshadow] = nss_database_default_files, + [nss_database_hosts] = nss_database_default_dns, + [nss_database_initgroups] = nss_database_default_none, + [nss_database_networks] = nss_database_default_dns, + [nss_database_passwd] = nss_database_default_compat, + [nss_database_publickey] = nss_database_default_nis_nisplus, + [nss_database_shadow] = nss_database_default_compat, + }; + +struct nss_database_default_cache +{ + nss_action_list caches[NSS_DATABASE_DEFAULT_COUNT]; +}; + +static bool +nss_database_select_default (struct nss_database_default_cache *cache, + enum nss_database db, nss_action_list *result) +{ + enum nss_database_default def = per_database_defaults[db]; + *result = cache->caches[def]; + if (*result != NULL) + return true; + + /* Determine the default line string. */ + const char *line; + switch (def) + { +#ifdef LINK_OBSOLETE_NSL + case nss_database_default_defconfig: + line = "nis [NOTFOUND=return] files"; + break; + case nss_database_default_compat: + line = "compat [NOTFOUND=return] files"; + break; +#endif + + case nss_database_default_dns: + line = "dns [!UNAVAIL=return] files"; + break; + + case nss_database_default_files: +#ifndef LINK_OBSOLETE_NSL + case nss_database_default_defconfig: + case nss_database_default_compat: +#endif + line = "files"; + break; + + case nss_database_default_nis: + line = "nis"; + break; + + case nss_database_default_nis_nisplus: + line = "nis nisplus"; + break; + + case nss_database_default_none: + /* Very special case: Leave *result as NULL. */ + return true; + + case NSS_DATABASE_DEFAULT_COUNT: + __builtin_unreachable (); + } + if (def < 0 || def >= NSS_DATABASE_DEFAULT_COUNT) + /* Tell GCC that line is initialized. */ + __builtin_unreachable (); + + *result = __nss_action_parse (line); + if (*result == NULL) + { + assert (errno == ENOMEM); + return false; + } + else + return true; +} + +/* database_name must be large enough for each individual name plus a + null terminator. */ +typedef char database_name[11]; +#define DEFINE_DATABASE(name) \ + _Static_assert (sizeof (#name) <= sizeof (database_name), #name); +#include "databases.def" +#undef DEFINE_DATABASE + +static const database_name nss_database_name_array[] = + { +#define DEFINE_DATABASE(name) #name, +#include "databases.def" +#undef DEFINE_DATABASE + }; + +static int +name_search (const void *left, const void *right) +{ + return strcmp (left, right); +} + +static int +name_to_database_index (const char *name) +{ + database_name *name_entry = bsearch (name, nss_database_name_array, + array_length (nss_database_name_array), + sizeof (database_name), name_search); + if (name_entry == NULL) + return -1; + return name_entry - nss_database_name_array; +} + +static bool +process_line (struct nss_database_data *data, char *line) +{ + /* Ignore leading white spaces. ATTENTION: this is different from + what is implemented in Solaris. The Solaris man page says a line + beginning with a white space character is ignored. We regard + this as just another misfeature in Solaris. */ + while (isspace (line[0])) + ++line; + + /* Recognize `<database> ":"'. */ + char *name = line; + while (line[0] != '\0' && !isspace (line[0]) && line[0] != ':') + ++line; + if (line[0] == '\0' || name == line) + /* Syntax error. Skip this line. */ + return true; + *line++ = '\0'; + + int db = name_to_database_index (name); + if (db < 0) + /* Not our database e.g. sudoers, automount, etc. */ + return true; + + nss_action_list result = __nss_action_parse (line); + if (result == NULL) + return false; + data->services[db] = result; + return true; +} + +/* Iterate over the lines in FP, parse them, and store them in DATA. + Return false on memory allocation failure, true on success. */ +static bool +nss_database_reload_1 (struct nss_database_data *data, FILE *fp) +{ + char *line = NULL; + size_t line_allocated = 0; + bool result = false; + + while (true) + { + ssize_t ret = __getline (&line, &line_allocated, fp); + if (ferror_unlocked (fp)) + break; + if (feof_unlocked (fp)) + { + result = true; + break; + } + assert (ret > 0); + (void) ret; /* For NDEBUG builds. */ + + if (!process_line (data, line)) + break; + } + + free (line); + return result; +} + +static bool +nss_database_reload (struct nss_database_data *staging, + struct file_change_detection *initial) +{ + FILE *fp = fopen (_PATH_NSSWITCH_CONF, "rce"); + if (fp == NULL) + switch (errno) + { + case EACCES: + case EISDIR: + case ELOOP: + case ENOENT: + case ENOTDIR: + case EPERM: + /* Ignore these errors. They are persistent errors caused + by file system contents. */ + break; + default: + /* Other errors refer to resource allocation problems and + need to be handled by the application. */ + return false; + } + else + /* No other threads have access to fp. */ + __fsetlocking (fp, FSETLOCKING_BYCALLER); + + bool ok = true; + if (fp != NULL) + ok = nss_database_reload_1 (staging, fp); + + /* Apply defaults. */ + if (ok) + { + struct nss_database_default_cache cache = { }; + for (int i = 0; i < NSS_DATABASE_COUNT; ++i) + if (staging->services[i] == NULL) + { + ok = nss_database_select_default (&cache, i, + &staging->services[i]); + if (!ok) + break; + } + } + + if (ok) + ok = __file_change_detection_for_fp (&staging->nsswitch_conf, fp); + + if (fp != NULL) + { + int saved_errno = errno; + fclose (fp); + __set_errno (saved_errno); + } + + if (ok && !__file_is_unchanged (&staging->nsswitch_conf, initial)) + /* Reload is required because the file changed while reading. */ + staging->nsswitch_conf.size = -1; + + return ok; +} + +static bool +nss_database_check_reload_and_get (struct nss_database_state *local, + nss_action_list *result, + enum nss_database database_index) +{ + /* Acquire MO is needed because the thread that sets reload_disabled + may have loaded the configuration first, so synchronize with the + Release MO store there. */ + if (atomic_load_acquire (&local->data.reload_disabled)) + /* No reload, so there is no error. */ + return true; + + struct file_change_detection initial; + if (!__file_change_detection_for_path (&initial, _PATH_NSSWITCH_CONF)) + return false; + + __libc_lock_lock (local->lock); + if (__file_is_unchanged (&initial, &local->data.nsswitch_conf)) + { + /* Configuration is up-to-date. Read it and return it to the + caller. */ + *result = local->data.services[database_index]; + __libc_lock_unlock (local->lock); + return true; + } + __libc_lock_unlock (local->lock); + + /* Avoid overwriting the global configuration until we have loaded + everything successfully. Otherwise, if the file change + information changes back to what is in the global configuration, + the lookups would use the partially-written configuration. */ + struct nss_database_data staging = { .initialized = true, }; + + bool ok = nss_database_reload (&staging, &initial); + + if (ok) + { + __libc_lock_lock (local->lock); + + /* See above for memory order. */ + if (!atomic_load_acquire (&local->data.reload_disabled)) + /* This may go back in time if another thread beats this + thread with the update, but in this case, a reload happens + on the next NSS call. */ + local->data = staging; + + *result = local->data.services[database_index]; + __libc_lock_unlock (local->lock); + } + + return ok; +} + +bool +__nss_database_get (enum nss_database db, nss_action_list *actions) +{ + struct nss_database_state *local = nss_database_state_get (); + return nss_database_check_reload_and_get (local, actions, db); +} + +nss_action_list +__nss_database_get_noreload (enum nss_database db) +{ + /* There must have been a previous __nss_database_get call. */ + struct nss_database_state *local = atomic_load_acquire (&global_database_state); + assert (local != NULL); + + __libc_lock_lock (local->lock); + nss_action_list result = local->data.services[db]; + __libc_lock_unlock (local->lock); + return result; +} + +void __libc_freeres_fn_section +__nss_database_freeres (void) +{ + free (global_database_state); + global_database_state = NULL; +} + +void +__nss_database_fork_prepare_parent (struct nss_database_data *data) +{ + /* Do not use allocate_once to trigger loading unnecessarily. */ + struct nss_database_state *local = atomic_load_acquire (&global_database_state); + if (local == NULL) + data->initialized = false; + else + { + /* Make a copy of the configuration. This approach was chosen + because it avoids acquiring the lock during the actual + fork. */ + __libc_lock_lock (local->lock); + *data = local->data; + __libc_lock_unlock (local->lock); + } +} + +void +__nss_database_fork_subprocess (struct nss_database_data *data) +{ + struct nss_database_state *local = atomic_load_acquire (&global_database_state); + if (data->initialized) + { + /* Restore the state at the point of the fork. */ + assert (local != NULL); + local->data = *data; + __libc_lock_init (local->lock); + } + else if (local != NULL) + /* The NSS configuration was loaded concurrently during fork. We + do not know its state, so we need to discard it. */ + global_database_state = NULL; +} |