From 68f0f1af0f2cc75615d89234494dca043e99a73d Mon Sep 17 00:00:00 2001 From: Eric Blake Date: Thu, 17 Dec 2009 16:57:37 -0700 Subject: [PATCH] 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 --- ChangeLog | 6 +++ lib/utimens.c | 153 ++++++++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 139 insertions(+), 20 deletions(-) diff --git a/ChangeLog b/ChangeLog index 8a387748b..04bdbcc8a 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,5 +1,11 @@ 2009-12-19 Eric Blake + 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. diff --git a/lib/utimens.c b/lib/utimens.c index d8e2f32f9..915f1e704 100644 --- a/lib/utimens.c +++ b/lib/utimens.c @@ -53,27 +53,76 @@ struct utimbuf #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 @@ validate_timespec (struct timespec timespec[2]) 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 @@ fdutimens (char const *file, int fd, struct timespec const timespec[2]) #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 @@ fdutimens (char const *file, int fd, struct timespec const timespec[2]) 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 @@ fdutimens (char const *file, int fd, struct timespec const timespec[2]) 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 @@ lutimens (char const *file, struct timespec const timespec[2]) #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 @@ lutimens (char const *file, struct timespec const timespec[2]) { 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; } } -- 2.11.0