v2 [PATCH 3/4] nss: Implement <nss_database.h>

Message ID xnd05eu584.fsf@greed.delorie.com
State Superseded
Delegated to: Carlos O'Donell
Headers
Series v2 [PATCH 3/4] nss: Implement <nss_database.h> |

Commit Message

DJ Delorie July 2, 2020, 12:32 a.m. UTC
  ---
 nss/Makefile             |   2 +-
 nss/nss_database.c       | 433 +++++++++++++++++++++++++++++++++++++++
 nss/nss_database.h       |  73 +++++++
 sysdeps/mach/hurd/fork.c |   8 +
 sysdeps/nptl/fork.c      |   9 +
 5 files changed, 524 insertions(+), 1 deletion(-)
 create mode 100644 nss/nss_database.c
 create mode 100644 nss/nss_database.h
  

Patch

diff --git a/nss/Makefile b/nss/Makefile
index 464655d045..194b183c91 100644
--- a/nss/Makefile
+++ b/nss/Makefile
@@ -29,7 +29,7 @@  routines		= nsswitch getnssent getnssent_r digits_dots \
 			  valid_field valid_list_field rewrite_field \
 			  $(addsuffix -lookup,$(databases)) \
 			  compat-lookup nss_hash nss_module nss_action \
-			  nss_action_parse
+			  nss_action_parse nss_database
 
 # These are the databases that go through nss dispatch.
 # Caution: if you add a database here, you must add its real name
diff --git a/nss/nss_database.c b/nss/nss_database.c
new file mode 100644
index 0000000000..40162e0bc2
--- /dev/null
+++ b/nss/nss_database.c
@@ -0,0 +1,433 @@ 
+/* Mapping NSS services to action lists.
+   Copyright (C) 1996-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;
+}
diff --git a/nss/nss_database.h b/nss/nss_database.h
new file mode 100644
index 0000000000..a157bbdbb0
--- /dev/null
+++ b/nss/nss_database.h
@@ -0,0 +1,73 @@ 
+/* 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/>.  */
+
+#ifndef _NSS_DATABASE_H
+#define _NSS_DATABASE_H
+
+#include <file_change_detection.h>
+
+#include "nss_action.h"
+
+/* The enumeration literal in enum nss_database for the database NAME
+   (e.g., nss_database_hosts for hosts).  */
+#define NSS_DATABASE_LITERAL(name) nss_database_##name
+
+enum nss_database
+{
+#define DEFINE_DATABASE(name) NSS_DATABASE_LITERAL (name),
+#include "databases.def"
+#undef DEFINE_DATABASE
+
+  /* Total number of databases.  */
+  NSS_DATABASE_COUNT
+};
+
+
+/* Looks up the action list for DB and stores it in *ACTIONS.  Returns
+   true on success or false on failure.  Success can mean that
+   *ACTIONS is NULL.  */
+bool __nss_database_get (enum nss_database db, nss_action_list *actions)
+  attribute_hidden;
+
+/* Like __nss_database_get, but does not reload /etc/nsswitch.conf
+   from disk.  This assumes that there has been a previous successful
+   __nss_database_get call (which may not have returned any data).  */
+nss_action_list __nss_database_get_noreload (enum nss_database db)
+  attribute_hidden;
+
+/* Called from __libc_freeres.  */
+void __nss_database_freeres (void) attribute_hidden;
+
+/* Internal type.  Exposed only for fork handling purposes.  */
+struct nss_database_data
+{
+  struct file_change_detection nsswitch_conf;
+  nss_action_list services[NSS_DATABASE_COUNT];
+  int reload_disabled;          /* Actually bool; int for atomic access.  */
+  bool initialized;
+};
+
+/* Called by fork in the parent process, before forking.  */
+void __nss_database_fork_prepare_parent (struct nss_database_data *data)
+  attribute_hidden;
+
+/* Called by fork in the new subprocess, after forking.  */
+void __nss_database_fork_subprocess (struct nss_database_data *data)
+  attribute_hidden;
+
+#endif /* _NSS_DATABASE_H */
diff --git a/sysdeps/mach/hurd/fork.c b/sysdeps/mach/hurd/fork.c
index 32783069ec..1aec951e76 100644
--- a/sysdeps/mach/hurd/fork.c
+++ b/sysdeps/mach/hurd/fork.c
@@ -28,6 +28,7 @@ 
 #include "hurdmalloc.h"		/* XXX */
 #include <tls.h>
 #include <malloc/malloc-internal.h>
+#include <nss/nss_database.h>
 
 #undef __fork
 
@@ -68,6 +69,7 @@  __fork (void)
   size_t i;
   error_t err;
   struct hurd_sigstate *volatile ss;
+  struct nss_database_data nss_database_data;
 
   RUN_HOOK (_hurd_atfork_prepare_hook, ());
 
@@ -109,6 +111,9 @@  __fork (void)
       /* Run things that prepare for forking before we create the task.  */
       RUN_HOOK (_hurd_fork_prepare_hook, ());
 
+      call_function_static_weak (__nss_database_fork_prepare_parent,
+				 &nss_database_data);
+
       /* Lock things that want to be locked before we fork.  */
       {
 	void *const *p;
@@ -666,6 +671,9 @@  __fork (void)
       _hurd_malloc_fork_child ();
       call_function_static_weak (__malloc_fork_unlock_child);
 
+      call_function_static_weak (__nss_database_fork_subprocess,
+				 &nss_database_data);
+
       /* Run things that want to run in the child task to set up.  */
       RUN_HOOK (_hurd_fork_child_hook, ());
 
diff --git a/sysdeps/nptl/fork.c b/sysdeps/nptl/fork.c
index 5091a000e3..964eb1e5a8 100644
--- a/sysdeps/nptl/fork.c
+++ b/sysdeps/nptl/fork.c
@@ -32,6 +32,7 @@ 
 #include <arch-fork.h>
 #include <futex-internal.h>
 #include <malloc/malloc-internal.h>
+#include <nss/nss_database.h>
 
 static void
 fresetlockfiles (void)
@@ -57,6 +58,8 @@  __libc_fork (void)
 
   __run_fork_handlers (atfork_run_prepare, multiple_threads);
 
+  struct nss_database_data nss_database_data;
+
   /* If we are not running multiple threads, we do not have to
      preserve lock state.  If fork runs from a signal handler, only
      async-signal-safe functions can be used in the child.  These data
@@ -64,6 +67,9 @@  __libc_fork (void)
      not matter if fork was called from a signal handler.  */
   if (multiple_threads)
     {
+      call_function_static_weak (__nss_database_fork_prepare_parent,
+				 &nss_database_data);
+
       _IO_list_lock ();
 
       /* Acquire malloc locks.  This needs to come last because fork
@@ -118,6 +124,9 @@  __libc_fork (void)
 
 	  /* Reset locks in the I/O code.  */
 	  _IO_list_resetlock ();
+
+	  call_function_static_weak (__nss_database_fork_subprocess,
+				     &nss_database_data);
 	}
 
       /* Reset the lock the dynamic loader uses to protect its data.  */