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