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