d8307d
commit be6b16d975683e6cca57852cd4cfe715b2a9d8b1
d8307d
Author: Florian Weimer <fweimer@redhat.com>
d8307d
Date:   Thu Nov 7 18:15:18 2019 +0100
d8307d
d8307d
    login: Acquire write lock early in pututline [BZ #24882]
d8307d
    
d8307d
    It has been reported that due to lack of fairness in POSIX file
d8307d
    locking, the current reader-to-writer lock upgrade can result in
d8307d
    lack of forward progress.  Acquiring the write lock directly
d8307d
    hopefully avoids this issue if there are only writers.
d8307d
    
d8307d
    This also fixes bug 24882 due to the cache revalidation in
d8307d
    __libc_pututline.
d8307d
    
d8307d
    Reviewed-by: Carlos O'Donell <carlos@redhat.com>
d8307d
    Change-Id: I57e31ae30719e609a53505a0924dda101d46372e
d8307d
d8307d
diff --git a/login/Makefile b/login/Makefile
d8307d
index 82132c83fd799357..030cf489b2e037d4 100644
d8307d
--- a/login/Makefile
d8307d
+++ b/login/Makefile
d8307d
@@ -44,7 +44,7 @@ subdir-dirs = programs
d8307d
 vpath %.c programs
d8307d
 
d8307d
 tests := tst-utmp tst-utmpx tst-grantpt tst-ptsname tst-getlogin tst-updwtmpx \
d8307d
-  tst-pututxline-lockfail
d8307d
+  tst-pututxline-lockfail tst-pututxline-cache
d8307d
 
d8307d
 # Build the -lutil library with these extra functions.
d8307d
 extra-libs      := libutil
d8307d
@@ -74,3 +74,4 @@ $(inst_libexecdir)/pt_chown: $(objpfx)pt_chown $(+force)
d8307d
 	-$(INSTALL_PROGRAM) -m 4755 -o root $< $@
d8307d
 
d8307d
 $(objpfx)tst-pututxline-lockfail: $(shared-thread-library)
d8307d
+$(objpfx)tst-pututxline-cache: $(shared-thread-library)
d8307d
diff --git a/login/tst-pututxline-cache.c b/login/tst-pututxline-cache.c
d8307d
new file mode 100644
d8307d
index 0000000000000000..3f30dd1776711769
d8307d
--- /dev/null
d8307d
+++ b/login/tst-pututxline-cache.c
d8307d
@@ -0,0 +1,193 @@
d8307d
+/* Test case for cache invalidation after concurrent write (bug 24882).
d8307d
+   Copyright (C) 2019 Free Software Foundation, Inc.
d8307d
+   This file is part of the GNU C Library.
d8307d
+
d8307d
+   The GNU C Library is free software; you can redistribute it and/or
d8307d
+   modify it under the terms of the GNU Lesser General Public License as
d8307d
+   published by the Free Software Foundation; either version 2.1 of the
d8307d
+   License, or (at your option) any later version.
d8307d
+
d8307d
+   The GNU C Library is distributed in the hope that it will be useful,
d8307d
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
d8307d
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
d8307d
+   Lesser General Public License for more details.
d8307d
+
d8307d
+   You should have received a copy of the GNU Lesser General Public
d8307d
+   License along with the GNU C Library; see the file COPYING.LIB.  If
d8307d
+   not, see <http://www.gnu.org/licenses/>.  */
d8307d
+
d8307d
+/* This test writes an entry to the utmpx file, reads it (so that it
d8307d
+   is cached) in process1, and overwrites the same entry in process2
d8307d
+   with something that does not match the search criteria.  At this
d8307d
+   point, the cache of the first process is stale, and when process1
d8307d
+   attempts to write a new record which would have gone to the same
d8307d
+   place (as indicated by the cache), it needs to realize that it has
d8307d
+   to pick a different slot because the old slot is now used for
d8307d
+   something else.  */
d8307d
+
d8307d
+#include <errno.h>
d8307d
+#include <stdlib.h>
d8307d
+#include <string.h>
d8307d
+#include <support/check.h>
d8307d
+#include <support/namespace.h>
d8307d
+#include <support/support.h>
d8307d
+#include <support/temp_file.h>
d8307d
+#include <support/xthread.h>
d8307d
+#include <support/xunistd.h>
d8307d
+#include <utmp.h>
d8307d
+#include <utmpx.h>
d8307d
+
d8307d
+/* Set to the path of the utmp file.  */
d8307d
+static char *utmp_file;
d8307d
+
d8307d
+/* Used to synchronize the subprocesses.  The barrier itself is
d8307d
+   allocated in shared memory.  */
d8307d
+static pthread_barrier_t *barrier;
d8307d
+
d8307d
+/* setutxent with error checking.  */
d8307d
+static void
d8307d
+xsetutxent (void)
d8307d
+{
d8307d
+  errno = 0;
d8307d
+  setutxent ();
d8307d
+  TEST_COMPARE (errno, 0);
d8307d
+}
d8307d
+
d8307d
+/* getutxent with error checking.  */
d8307d
+static struct utmpx *
d8307d
+xgetutxent (void)
d8307d
+{
d8307d
+  errno = 0;
d8307d
+  struct utmpx *result = getutxent ();
d8307d
+  if (result == NULL)
d8307d
+    FAIL_EXIT1 ("getutxent: %m");
d8307d
+  return result;
d8307d
+}
d8307d
+
d8307d
+static void
d8307d
+put_entry (const char *id, pid_t pid, const char *user, const char *line)
d8307d
+{
d8307d
+  struct utmpx ut =
d8307d
+    {
d8307d
+     .ut_type = LOGIN_PROCESS,
d8307d
+     .ut_pid = pid,
d8307d
+     .ut_host = "localhost",
d8307d
+    };
d8307d
+  strcpy (ut.ut_id, id);
d8307d
+  strncpy (ut.ut_user, user, sizeof (ut.ut_user));
d8307d
+  strncpy (ut.ut_line, line, sizeof (ut.ut_line));
d8307d
+  TEST_VERIFY (pututxline (&ut) != NULL);
d8307d
+}
d8307d
+
d8307d
+/* Use two cooperating subprocesses to avoid issues related to
d8307d
+   unlock-on-close semantics of POSIX advisory locks.  */
d8307d
+
d8307d
+static __attribute__ ((noreturn)) void
d8307d
+process1 (void)
d8307d
+{
d8307d
+  TEST_COMPARE (utmpname (utmp_file), 0);
d8307d
+
d8307d
+  /* Create an entry.  */
d8307d
+  xsetutxent ();
d8307d
+  put_entry ("1", 101, "root", "process1");
d8307d
+
d8307d
+  /* Retrieve the entry.  This will fill the internal cache.  */
d8307d
+  {
d8307d
+    errno = 0;
d8307d
+    setutxent ();
d8307d
+    TEST_COMPARE (errno, 0);
d8307d
+    struct utmpx ut =
d8307d
+      {
d8307d
+       .ut_type = LOGIN_PROCESS,
d8307d
+       .ut_line = "process1",
d8307d
+      };
d8307d
+    struct utmpx *result = getutxline (&ut);
d8307d
+    if (result == NULL)
d8307d
+      FAIL_EXIT1 ("getutxline (\"process1\"): %m");
d8307d
+    TEST_COMPARE (result->ut_pid, 101);
d8307d
+  }
d8307d
+
d8307d
+  /* Signal the other process to overwrite the entry.  */
d8307d
+  xpthread_barrier_wait (barrier);
d8307d
+
d8307d
+  /* Wait for the other process to complete the write operation.  */
d8307d
+  xpthread_barrier_wait (barrier);
d8307d
+
d8307d
+  /* Add another entry.  Note: This time, there is no setutxent call.  */
d8307d
+  put_entry ("1", 103, "root", "process1");
d8307d
+
d8307d
+  _exit (0);
d8307d
+}
d8307d
+
d8307d
+static void
d8307d
+process2 (void *closure)
d8307d
+{
d8307d
+  /* Wait for the first process to write its entry.  */
d8307d
+  xpthread_barrier_wait (barrier);
d8307d
+
d8307d
+  /* Truncate the file.  The glibc interface does not support
d8307d
+     re-purposing records, but an external expiration mechanism may
d8307d
+     trigger this.  */
d8307d
+  TEST_COMPARE (truncate64 (utmp_file, 0), 0);
d8307d
+
d8307d
+  /* Write the replacement entry.  */
d8307d
+  TEST_COMPARE (utmpname (utmp_file), 0);
d8307d
+  xsetutxent ();
d8307d
+  put_entry ("2", 102, "user", "process2");
d8307d
+
d8307d
+  /* Signal the other process that the entry has been replaced.  */
d8307d
+  xpthread_barrier_wait (barrier);
d8307d
+}
d8307d
+
d8307d
+static int
d8307d
+do_test (void)
d8307d
+{
d8307d
+  xclose (create_temp_file ("tst-tumpx-cache-write-", &utmp_file));
d8307d
+  {
d8307d
+    pthread_barrierattr_t attr;
d8307d
+    xpthread_barrierattr_init (&attr);
d8307d
+    xpthread_barrierattr_setpshared (&attr, PTHREAD_SCOPE_PROCESS);
d8307d
+    barrier = support_shared_allocate (sizeof (*barrier));
d8307d
+    xpthread_barrier_init (barrier, &attr, 2);
d8307d
+  }
d8307d
+
d8307d
+  /* Run both subprocesses in parallel.  */
d8307d
+  {
d8307d
+    pid_t pid1 = xfork ();
d8307d
+    if (pid1 == 0)
d8307d
+      process1 ();
d8307d
+    support_isolate_in_subprocess (process2, NULL);
d8307d
+    int status;
d8307d
+    xwaitpid (pid1, &status, 0);
d8307d
+    TEST_COMPARE (status, 0);
d8307d
+  }
d8307d
+
d8307d
+  /* Check that the utmpx database contains the expected records.  */
d8307d
+  {
d8307d
+    TEST_COMPARE (utmpname (utmp_file), 0);
d8307d
+    xsetutxent ();
d8307d
+
d8307d
+    struct utmpx *ut = xgetutxent ();
d8307d
+    TEST_COMPARE_STRING (ut->ut_id, "2");
d8307d
+    TEST_COMPARE (ut->ut_pid, 102);
d8307d
+    TEST_COMPARE_STRING (ut->ut_user, "user");
d8307d
+    TEST_COMPARE_STRING (ut->ut_line, "process2");
d8307d
+
d8307d
+    ut = xgetutxent ();
d8307d
+    TEST_COMPARE_STRING (ut->ut_id, "1");
d8307d
+    TEST_COMPARE (ut->ut_pid, 103);
d8307d
+    TEST_COMPARE_STRING (ut->ut_user, "root");
d8307d
+    TEST_COMPARE_STRING (ut->ut_line, "process1");
d8307d
+
d8307d
+    if (getutxent () != NULL)
d8307d
+      FAIL_EXIT1 ("additional utmpx entry");
d8307d
+  }
d8307d
+
d8307d
+  xpthread_barrier_destroy (barrier);
d8307d
+  support_shared_free (barrier);
d8307d
+  free (utmp_file);
d8307d
+
d8307d
+  return 0;
d8307d
+}
d8307d
+
d8307d
+#include <support/test-driver.c>
d8307d
diff --git a/login/utmp_file.c b/login/utmp_file.c
d8307d
index 9ad80364682bae92..6bba120db9cc574e 100644
d8307d
--- a/login/utmp_file.c
d8307d
+++ b/login/utmp_file.c
d8307d
@@ -186,19 +186,11 @@ __libc_getutent_r (struct utmp *buffer, struct utmp **result)
d8307d
 
d8307d
 
d8307d
 /* Search for *ID, updating last_entry and file_offset.  Return 0 on
d8307d
-   success and -1 on failure.  If the locking operation failed, write
d8307d
-   true to *LOCK_FAILED.  */
d8307d
+   success and -1 on failure.  Does not perform locking; for that see
d8307d
+   internal_getut_r below.  */
d8307d
 static int
d8307d
-internal_getut_r (const struct utmp *id, bool *lock_failed)
d8307d
+internal_getut_nolock (const struct utmp *id)
d8307d
 {
d8307d
-  int result = -1;
d8307d
-
d8307d
-  if (try_file_lock (file_fd, F_RDLCK))
d8307d
-    {
d8307d
-      *lock_failed = true;
d8307d
-      return -1;
d8307d
-    }
d8307d
-
d8307d
   if (id->ut_type == RUN_LVL || id->ut_type == BOOT_TIME
d8307d
       || id->ut_type == OLD_TIME || id->ut_type == NEW_TIME)
d8307d
     {
d8307d
@@ -213,7 +205,7 @@ internal_getut_r (const struct utmp *id, bool *lock_failed)
d8307d
 	    {
d8307d
 	      __set_errno (ESRCH);
d8307d
 	      file_offset = -1l;
d8307d
-	      goto unlock_return;
d8307d
+	      return -1;
d8307d
 	    }
d8307d
 	  file_offset += sizeof (struct utmp);
d8307d
 
d8307d
@@ -234,7 +226,7 @@ internal_getut_r (const struct utmp *id, bool *lock_failed)
d8307d
 	    {
d8307d
 	      __set_errno (ESRCH);
d8307d
 	      file_offset = -1l;
d8307d
-	      goto unlock_return;
d8307d
+	      return -1;
d8307d
 	    }
d8307d
 	  file_offset += sizeof (struct utmp);
d8307d
 
d8307d
@@ -243,15 +235,26 @@ internal_getut_r (const struct utmp *id, bool *lock_failed)
d8307d
 	}
d8307d
     }
d8307d
 
d8307d
-  result = 0;
d8307d
+  return 0;
d8307d
+}
d8307d
 
d8307d
-unlock_return:
d8307d
-  file_unlock (file_fd);
d8307d
+/* Search for *ID, updating last_entry and file_offset.  Return 0 on
d8307d
+   success and -1 on failure.  If the locking operation failed, write
d8307d
+   true to *LOCK_FAILED.  */
d8307d
+static int
d8307d
+internal_getut_r (const struct utmp *id, bool *lock_failed)
d8307d
+{
d8307d
+  if (try_file_lock (file_fd, F_RDLCK))
d8307d
+    {
d8307d
+      *lock_failed = true;
d8307d
+      return -1;
d8307d
+    }
d8307d
 
d8307d
+  int result = internal_getut_nolock (id);
d8307d
+  file_unlock (file_fd);
d8307d
   return result;
d8307d
 }
d8307d
 
d8307d
-
d8307d
 /* For implementing this function we don't use the getutent_r function
d8307d
    because we can avoid the reposition on every new entry this way.  */
d8307d
 int
d8307d
@@ -279,7 +282,6 @@ __libc_getutid_r (const struct utmp *id, struct utmp *buffer,
d8307d
   return 0;
d8307d
 }
d8307d
 
d8307d
-
d8307d
 /* For implementing this function we don't use the getutent_r function
d8307d
    because we can avoid the reposition on every new entry this way.  */
d8307d
 int
d8307d
@@ -336,7 +338,6 @@ __libc_pututline (const struct utmp *data)
d8307d
     return NULL;
d8307d
 
d8307d
   struct utmp *pbuf;
d8307d
-  int found;
d8307d
 
d8307d
   if (! file_writable)
d8307d
     {
d8307d
@@ -358,7 +359,12 @@ __libc_pututline (const struct utmp *data)
d8307d
       file_writable = true;
d8307d
     }
d8307d
 
d8307d
+  /* Exclude other writers before validating the cache.  */
d8307d
+  if (try_file_lock (file_fd, F_WRLCK))
d8307d
+    return NULL;
d8307d
+
d8307d
   /* Find the correct place to insert the data.  */
d8307d
+  bool found = false;
d8307d
   if (file_offset > 0
d8307d
       && ((last_entry.ut_type == data->ut_type
d8307d
 	   && (last_entry.ut_type == RUN_LVL
d8307d
@@ -366,23 +372,30 @@ __libc_pututline (const struct utmp *data)
d8307d
 	       || last_entry.ut_type == OLD_TIME
d8307d
 	       || last_entry.ut_type == NEW_TIME))
d8307d
 	  || __utmp_equal (&last_entry, data)))
d8307d
-    found = 1;
d8307d
-  else
d8307d
     {
d8307d
-      bool lock_failed = false;
d8307d
-      found = internal_getut_r (data, &lock_failed);
d8307d
-
d8307d
-      if (__builtin_expect (lock_failed, false))
d8307d
+      if (__lseek64 (file_fd, file_offset, SEEK_SET) < 0)
d8307d
 	{
d8307d
-	  __set_errno (EAGAIN);
d8307d
+	  file_unlock (file_fd);
d8307d
 	  return NULL;
d8307d
 	}
d8307d
+      if (__read_nocancel (file_fd, &last_entry, sizeof (last_entry))
d8307d
+	  != sizeof (last_entry))
d8307d
+	{
d8307d
+	  if (__lseek64 (file_fd, file_offset, SEEK_SET) < 0)
d8307d
+	    {
d8307d
+	      file_unlock (file_fd);
d8307d
+	      return NULL;
d8307d
+	    }
d8307d
+	  found = false;
d8307d
+	}
d8307d
+      else
d8307d
+	found = __utmp_equal (&last_entry, data);
d8307d
     }
d8307d
 
d8307d
-  if (try_file_lock (file_fd, F_WRLCK))
d8307d
-    return NULL;
d8307d
+  if (!found)
d8307d
+    found = internal_getut_nolock (data) >= 0;
d8307d
 
d8307d
-  if (found < 0)
d8307d
+  if (!found)
d8307d
     {
d8307d
       /* We append the next entry.  */
d8307d
       file_offset = __lseek64 (file_fd, 0, SEEK_END);
d8307d
@@ -411,7 +424,7 @@ __libc_pututline (const struct utmp *data)
d8307d
     {
d8307d
       /* If we appended a new record this is only partially written.
d8307d
 	 Remove it.  */
d8307d
-      if (found < 0)
d8307d
+      if (!found)
d8307d
 	(void) __ftruncate64 (file_fd, file_offset);
d8307d
       pbuf = NULL;
d8307d
     }