changeset 12467:912bdb1c31fb

utimens: work around Linux ctime bug Force a ctime update by using stat() before any utimensat call with mtime of UTIME_OMIT. But avoid extra stat()s in later calls, by doing extra work on the first instance in order to cache whether the bug is actually present. * lib/utimens.c (detect_ctime_bug): New helper function. (update_timespec): Differentiate between workaround needed for this bug vs. what is needed for systems that lack utimensat. (fdutimens, lutimens): Work around bug. Signed-off-by: Eric Blake <ebb9@byu.net>
author Eric Blake <ebb9@byu.net>
date Thu, 17 Dec 2009 16:57:37 -0700
parents e31a9e90d604
children ff7c15d38975
files ChangeLog lib/utimens.c
diffstat 2 files changed, 139 insertions(+), 20 deletions(-) [+]
line wrap: on
line diff
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,11 @@
 2009-12-19  Eric Blake  <ebb9@byu.net>
 
+	utimens: work around Linux ctime bug
+	* lib/utimens.c (detect_ctime_bug): New helper function.
+	(update_timespec): Differentiate between workaround needed for
+	this bug vs. what is needed for systems that lack utimensat.
+	(fdutimens, lutimens): Work around bug.
+
 	utimens: check for ctime update
 	* tests/test-utimens-common.h (check_ctime): Define.
 	* tests/test-utimens.h (test_utimens): Expose the Linux bug.
--- a/lib/utimens.c
+++ b/lib/utimens.c
@@ -53,27 +53,76 @@
 #undef futimens
 #undef utimensat
 
-#if HAVE_UTIMENSAT || HAVE_FUTIMENS
-/* Cache variables for whether the utimensat syscall works; used to
-   avoid calling the syscall if we know it will just fail with ENOSYS.
-   There are some Linux kernel versions where a flag of 0 passes, but
-   not AT_SYMLINK_NOFOLLOW.  0 = unknown, 1 = yes, -1 = no.  */
-static int utimensat_works_really;
-static int lutimensat_works_really;
-#endif /* HAVE_UTIMENSAT || HAVE_UTIMENSAT */
-
 /* Solaris 9 mistakenly succeeds when given a non-directory with a
    trailing slash.  Force the use of rpl_stat for a fix.  */
 #ifndef REPLACE_FUNC_STAT_FILE
 # define REPLACE_FUNC_STAT_FILE 0
 #endif
 
+#if HAVE_UTIMENSAT || HAVE_FUTIMENS
+/* Cache variables for whether the utimensat syscall works; used to
+   avoid calling the syscall if we know it will just fail with ENOSYS,
+   and to avoid unnecessary work in massaging timestamps if the
+   syscall will work.  Multiple variables are needed, to distinguish
+   between the following scenarios on Linux:
+   utimensat doesn't exist, or is in glibc but kernel 2.6.18 fails with ENOSYS
+   kernel 2.6.22 and earlier rejects AT_SYMLINK_NOFOLLOW
+   kernel 2.6.25 and earlier reject UTIME_NOW/UTIME_OMIT with non-zero tv_sec
+   kernel 2.6.32 and earlier fail to bump ctime if mtime is UTIME_OMIT
+   utimensat completely works
+   For each cache variable: 0 = unknown, 1 = yes, -1 = no.  */
+static int utimensat_works_really;
+static int lutimensat_works_really;
+static int utimensat_ctime_really;
+
+/* Determine whether the kernel has a ctime bug.  ST1 and ST2
+   correspond to stat data before and after a successful time change.
+   TIMES contains the timestamps that were used during the time change
+   (mtime will be UTIME_OMIT).  Update the cache variable if there is
+   conclusive evidence of the kernel working or being buggy.  Return
+   true if TIMES has been updated and another kernel call is needed,
+   whether or not the kernel is known to have the bug.  */
+static bool
+detect_ctime_bug (struct stat *st1, struct stat *st2, struct timespec times[2])
+{
+  struct timespec now;
+  if (st1->st_ctime != st2->st_ctime
+      || get_stat_ctime_ns (st1) != get_stat_ctime_ns (st2))
+    {
+      utimensat_ctime_really = 1;
+      return false;
+    }
+  /* The results are inconclusive if the ctime in st1 is within a file
+     system quantization window of now.  For FAT, this is 2 seconds,
+     for systems with sub-second resolution, a typical resolution is
+     10 milliseconds; to be safe we declare an inconsistent result if
+     ctime is within a 20 millisecond window.  Avoid an extra gettime
+     call if atime makes sense.  It is unlikely that the original
+     ctime is later than now, but rather than deal with the overflow,
+     we treat that as consistent evidence of the bug.  */
+  if (times[0].tv_nsec == UTIME_NOW)
+    now = get_stat_atime (st2);
+  else
+    gettime (&now);
+  if (now.tv_sec < st2->st_ctime
+      || 2 < now.tv_sec - st2->st_ctime
+      || (get_stat_ctime_ns (st2)
+          && now.tv_sec - st2->st_ctime < 2
+          && (20000000 < (1000000000 * (now.tv_sec - st2->st_ctime)
+                          + now.tv_nsec - get_stat_ctime_ns (st2)))))
+    utimensat_ctime_really = -1;
+  times[1] = get_stat_mtime (st2);
+  return true;
+}
+#endif /* HAVE_UTIMENSAT || HAVE_FUTIMENS */
+
 /* Validate the requested timestamps.  Return 0 if the resulting
    timespec can be used for utimensat (after possibly modifying it to
-   work around bugs in utimensat).  Return 1 if the timespec needs
-   further adjustment based on stat results for utimes or other less
-   powerful interfaces.  Return -1, with errno set to EINVAL, if
-   timespec is out of range.  */
+   work around bugs in utimensat).  Return a positive value if the
+   timespec needs further adjustment based on stat results: 1 if any
+   adjustment is needed for utimes, and 2 if mtime was UTIME_OMIT and
+   an adjustment is needed for utimensat.  Return -1, with errno set
+   to EINVAL, if timespec is out of range.  */
 static int
 validate_timespec (struct timespec timespec[2])
 {
@@ -90,20 +139,25 @@
       return -1;
     }
   /* Work around Linux kernel 2.6.25 bug, where utimensat fails with
-     EINVAL if tv_sec is not 0 when using the flag values of
-     tv_nsec.  */
+     EINVAL if tv_sec is not 0 when using the flag values of tv_nsec.
+     Flag a Linux kernel 2.6.32 bug, where an mtime of UTIME_OMIT
+     fails to bump ctime.  */
   if (timespec[0].tv_nsec == UTIME_NOW
       || timespec[0].tv_nsec == UTIME_OMIT)
     {
       timespec[0].tv_sec = 0;
       result = 1;
     }
-  if (timespec[1].tv_nsec == UTIME_NOW
-      || timespec[1].tv_nsec == UTIME_OMIT)
+  if (timespec[1].tv_nsec == UTIME_NOW)
     {
       timespec[1].tv_sec = 0;
       result = 1;
     }
+  else if (timespec[1].tv_nsec == UTIME_OMIT)
+    {
+      timespec[1].tv_sec = 0;
+      result = 2;
+    }
   return result;
 }
 
@@ -205,10 +259,25 @@
 #if HAVE_UTIMENSAT || HAVE_FUTIMENS
   if (0 <= utimensat_works_really)
     {
+      int result;
+      struct stat st1;
+      struct stat st2;
+      /* Linux kernel 2.6.32 has a bug where it fails to bump ctime if
+         UTIME_OMIT was used for mtime.  It costs time to do an extra
+         [f]stat up front, so we cache whether the function works.  */
+      if (utimensat_ctime_really <= 0 && adjustment_needed == 2)
+        {
+          if (fd < 0 ? stat (file, &st1) : fstat (fd, &st1))
+            return -1;
+          if (ts[0].tv_nsec == UTIME_OMIT)
+            return 0;
+          if (utimensat_ctime_really < 0)
+            ts[1] = get_stat_mtime (&st1);
+        }
 # if HAVE_UTIMENSAT
       if (fd < 0)
         {
-          int result = utimensat (AT_FDCWD, file, ts, 0);
+          result = utimensat (AT_FDCWD, file, ts, 0);
 #  ifdef __linux__
           /* Work around a kernel bug:
              http://bugzilla.redhat.com/442352
@@ -223,13 +292,23 @@
           if (result == 0 || errno != ENOSYS)
             {
               utimensat_works_really = 1;
+              if (result == 0 && utimensat_ctime_really == 0
+                  && adjustment_needed == 2)
+                {
+                  /* Perform a followup stat to see if the kernel has
+                     a ctime bug.  */
+                  if (stat (file, &st2))
+                    return -1;
+                  if (detect_ctime_bug (&st1, &st2, ts))
+                    result = utimensat (AT_FDCWD, file, ts, 0);
+                }
               return result;
             }
         }
 # endif /* HAVE_UTIMENSAT */
 # if HAVE_FUTIMENS
       {
-        int result = futimens (fd, ts);
+        result = futimens (fd, ts);
 #  ifdef __linux__
         /* Work around the same bug as above.  */
         if (0 < result)
@@ -238,6 +317,15 @@
         if (result == 0 || errno != ENOSYS)
           {
             utimensat_works_really = 1;
+            /* Work around the same bug as above.  */
+            if (result == 0 && utimensat_ctime_really == 0
+                && adjustment_needed == 2)
+              {
+                if (fstat (fd, &st2))
+                  return -1;
+                if (detect_ctime_bug (&st1, &st2, ts))
+                  result = futimens (fd, ts);
+              }
             return result;
           }
       }
@@ -386,7 +474,22 @@
 #if HAVE_UTIMENSAT
   if (0 <= lutimensat_works_really)
     {
-      int result = utimensat (AT_FDCWD, file, ts, AT_SYMLINK_NOFOLLOW);
+      int result;
+      struct stat st1;
+      struct stat st2;
+      /* Linux kernel 2.6.32 has a bug where it fails to bump ctime if
+         UTIME_OMIT was used for mtime.  It costs time to do an extra
+         lstat up front, so we cache whether the function works.  */
+      if (utimensat_ctime_really <= 0 && adjustment_needed == 2)
+        {
+          if (lstat (file, &st1))
+            return -1;
+          if (ts[0].tv_nsec == UTIME_OMIT)
+            return 0;
+          if (utimensat_ctime_really < 0)
+            ts[1] = get_stat_mtime (&st1);
+        }
+      result = utimensat (AT_FDCWD, file, ts, AT_SYMLINK_NOFOLLOW);
 # ifdef __linux__
       /* Work around a kernel bug:
          http://bugzilla.redhat.com/442352
@@ -402,6 +505,16 @@
         {
           utimensat_works_really = 1;
           lutimensat_works_really = 1;
+          if (result == 0 && utimensat_ctime_really == 0
+              && adjustment_needed == 2)
+            {
+              /* Perform a followup stat to see if the kernel has a
+                 ctime bug.  */
+              if (lstat (file, &st2))
+                return -1;
+              if (detect_ctime_bug (&st1, &st2, ts))
+                result = utimensat (AT_FDCWD, file, ts, AT_SYMLINK_NOFOLLOW);
+            }
           return result;
         }
     }