bug-gnulib
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: pending patches?


From: Eric Blake
Subject: Re: pending patches?
Date: Fri, 13 Nov 2009 14:36:51 -0700
User-agent: Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.8.1.23) Gecko/20090812 Thunderbird/2.0.0.23 Mnenhy/0.7.6.666

-----BEGIN PGP SIGNED MESSAGE-----
Hash: SHA1

According to Eric Blake on 11/13/2009 11:35 AM:
>> Are you approaching a good cut-off point?
> 
> I've almost got chown working; expect patches later today.

Here's my candidate under testing.  It passed FreeBSD 7.2, but I still
need to check it on Solaris 9.

- --
Don't work too hard, make some time for fun as well!

Eric Blake             address@hidden
-----BEGIN PGP SIGNATURE-----
Version: GnuPG v1.4.9 (Cygwin)
Comment: Public key at home.comcast.net/~ericblake/eblake.gpg
Comment: Using GnuPG with Mozilla - http://enigmail.mozdev.org/

iEYEARECAAYFAkr90XMACgkQ84KuGfSFAYCWCACfcp9KPvsDq9z9NDfUzxwCKI+b
ycEAniQPeOIE/P2QNwiz1RjDn1kfARvE
=ksWE
-----END PGP SIGNATURE-----
>From 49568536d1ceb5a92b6b3f0e6d6ed9b9e6c7668c Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Thu, 12 Nov 2009 21:45:20 -0700
Subject: [PATCH 1/2] chown: detect Solaris and FreeBSD bug

Solaris 9 and FreeBSD 7.2 chown("link-to-file/",uid,gid)
mistakenly changes ownership of "file".

* lib/chown.c (rpl_chown): Work around bug.
* m4/chown.m4 (gl_FUNC_CHOWN): Check for trailing slash bugs.
(gl_PREREQ_CHOWN): Delete.
* m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witness.
* modules/unistd (Makefile.am): Populate it.
* lib/unistd.in.h (chown): Update declaration.
* lib/lchown.c (chown): Update client.
* modules/lchown (Depends-on): Add lstat.
* doc/posix-functions/chown.texi (chown): Document the bug.
* doc/posix-functions/getgroups.texi (getgroups): Document
getgroups pitfall.
* modules/chown-tests: New file.
* tests/test-chown.h (test_chown): Likewise.
* tests/test-chown.c (main): Likewise.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog                          |   16 ++
 doc/posix-functions/chown.texi     |   11 +-
 doc/posix-functions/getgroups.texi |    6 +
 lib/chown.c                        |   80 +++++++----
 lib/lchown.c                       |   11 +-
 lib/unistd.in.h                    |   10 +-
 m4/chown.m4                        |   61 ++++++---
 m4/unistd_h.m4                     |    3 +-
 modules/chown-tests                |   18 +++
 modules/lchown                     |    1 +
 modules/unistd                     |    1 +
 tests/test-chown.c                 |   56 ++++++++
 tests/test-chown.h                 |  271 ++++++++++++++++++++++++++++++++++++
 13 files changed, 478 insertions(+), 67 deletions(-)
 create mode 100644 modules/chown-tests
 create mode 100644 tests/test-chown.c
 create mode 100644 tests/test-chown.h

diff --git a/ChangeLog b/ChangeLog
index fdb892b..a685a18 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,21 @@
 2009-11-13  Eric Blake  <address@hidden>

+       chown: detect Solaris and FreeBSD bug
+       * lib/chown.c (rpl_chown): Work around bug.
+       * m4/chown.m4 (gl_FUNC_CHOWN): Check for trailing slash bugs.
+       (gl_PREREQ_CHOWN): Delete.
+       * m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witness.
+       * modules/unistd (Makefile.am): Populate it.
+       * lib/unistd.in.h (chown): Update declaration.
+       * lib/lchown.c (chown): Update client.
+       * modules/lchown (Depends-on): Add lstat.
+       * doc/posix-functions/chown.texi (chown): Document the bug.
+       * doc/posix-functions/getgroups.texi (getgroups): Document
+       getgroups pitfall.
+       * modules/chown-tests: New file.
+       * tests/test-chown.h (test_chown): Likewise.
+       * tests/test-chown.c (main): Likewise.
+
        getgroups: avoid compiler warning
        * lib/getgroups.c (rpl_getgroups): Delete shadowed variable.

diff --git a/doc/posix-functions/chown.texi b/doc/posix-functions/chown.texi
index c444f75..e5a80d6 100644
--- a/doc/posix-functions/chown.texi
+++ b/doc/posix-functions/chown.texi
@@ -9,17 +9,22 @@ chown
 Portability problems fixed by Gnulib:
 @itemize
 @item
+Some platforms fail to detect trailing slash on non-directories, as in
address@hidden("link-to-file/",uid,gid)}:
+FreeBSD 7.2, Solaris 9.
address@hidden
 When passed an argument of -1, some implementations really set the owner
 user/group id of the file to this value, rather than leaving that id of the
 file alone.
 @item
 When applied to a symbolic link, some implementations don't dereference
 the symlink, i.e.@: they behave like @code{lchown}.
address@hidden
+This function is missing on some platforms; however, the replacement
+always fails with @code{ENOSYS}:
+mingw.
 @end itemize

 Portability problems not fixed by Gnulib:
 @itemize
address@hidden
-This function is missing on some platforms:
-mingw.
 @end itemize
diff --git a/doc/posix-functions/getgroups.texi 
b/doc/posix-functions/getgroups.texi
index a8478eb..0e838f7 100644
--- a/doc/posix-functions/getgroups.texi
+++ b/doc/posix-functions/getgroups.texi
@@ -25,4 +25,10 @@ getgroups

 Portability problems not fixed by Gnulib:
 @itemize
address@hidden
+It is unspecified whether the effective group id will be included in
+the returned list, nor whether the list will be sorted in any
+particular order.  For that matter, some platforms include the
+effective group id twice, if it is also a member of the current
+supplemental group ids.
 @end itemize
diff --git a/lib/chown.c b/lib/chown.c
index b851cbc..cbc8a89 100644
--- a/lib/chown.c
+++ b/lib/chown.c
@@ -29,13 +29,26 @@
 #include <fcntl.h>
 #include <errno.h>

+#if !HAVE_CHOWN
+
+/* Simple stub that always fails with ENOSYS, for mingw.  */
+int
+chown (const char *file _UNUSED_PARAMETER_, uid_t uid _UNUSED_PARAMETER_,
+       gid_t gid _UNUSED_PARAMETER_)
+{
+  errno = ENOSYS;
+  return -1;
+}
+
+#else /* HAVE_CHOWN */
+
 /* Below we refer to the system's chown().  */
-#undef chown
+# undef chown

 /* The results of open() in this file are not used with fchdir,
    therefore save some unnecessary work in fchdir.c.  */
-#undef open
-#undef close
+# undef open
+# undef close

 /* Provide a more-closely POSIX-conforming version of chown on
    systems with one or both of the following problems:
@@ -46,7 +59,6 @@
 int
 rpl_chown (const char *file, uid_t uid, gid_t gid)
 {
-#if HAVE_CHOWN
 # if CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE
   if (gid == (gid_t) -1 || uid == (uid_t) -1)
     {
@@ -54,13 +66,13 @@ rpl_chown (const char *file, uid_t uid, gid_t gid)

       /* Stat file to get id(s) that should remain unchanged.  */
       if (stat (file, &file_stats))
-       return -1;
+        return -1;

       if (gid == (gid_t) -1)
-       gid = file_stats.st_gid;
+        gid = file_stats.st_gid;

       if (uid == (uid_t) -1)
-       uid = file_stats.st_uid;
+        uid = file_stats.st_uid;
     }
 # endif

@@ -74,36 +86,42 @@ rpl_chown (const char *file, uid_t uid, gid_t gid)
     int open_flags = O_NONBLOCK | O_NOCTTY;
     int fd = open (file, O_RDONLY | open_flags);
     if (0 <= fd
-       || (errno == EACCES
-           && 0 <= (fd = open (file, O_WRONLY | open_flags))))
+        || (errno == EACCES
+            && 0 <= (fd = open (file, O_WRONLY | open_flags))))
       {
-       int result = fchown (fd, uid, gid);
-       int saved_errno = errno;
-
-       /* POSIX says fchown can fail with errno == EINVAL on sockets,
-          so fall back on chown in that case.  */
-       struct stat sb;
-       bool fchown_socket_failure =
-         (result != 0 && saved_errno == EINVAL
-          && fstat (fd, &sb) == 0 && S_ISFIFO (sb.st_mode));
-
-       close (fd);
-
-       if (! fchown_socket_failure)
-         {
-           errno = saved_errno;
-           return result;
-         }
+        int result = fchown (fd, uid, gid);
+        int saved_errno = errno;
+
+        /* POSIX says fchown can fail with errno == EINVAL on sockets,
+           so fall back on chown in that case.  */
+        struct stat sb;
+        bool fchown_socket_failure =
+          (result != 0 && saved_errno == EINVAL
+           && fstat (fd, &sb) == 0 && S_ISFIFO (sb.st_mode));
+
+        close (fd);
+
+        if (! fchown_socket_failure)
+          {
+            errno = saved_errno;
+            return result;
+          }
       }
     else if (errno != EACCES)
       return -1;
   }
 # endif

-  return chown (file, uid, gid);
+# if CHOWN_TRAILING_SLASH_BUG
+  {
+    size_t len = strlen (file);
+    struct stat st;
+    if (file[len - 1] == '/' && stat (file, &st))
+      return -1;
+  }
+# endif

-#else /* !HAVE_CHOWN */
-  errno = ENOSYS;
-  return -1;
-#endif
+  return chown (file, uid, gid);
 }
+
+#endif /* HAVE_CHOWN */
diff --git a/lib/lchown.c b/lib/lchown.c
index 8cf10dd..65434b4 100644
--- a/lib/lchown.c
+++ b/lib/lchown.c
@@ -20,16 +20,17 @@

 #include <config.h>

-/* If the system chown does not follow symlinks, we don't want it
-   replaced by gnulib's chown, which does follow symlinks.  */
-#if CHOWN_MODIFIES_SYMLINK
-# define REPLACE_CHOWN 0
-#endif
 #include <unistd.h>

 #include <errno.h>
 #include <sys/stat.h>

+/* If the system chown does not follow symlinks, we don't want it
+   replaced by gnulib's chown, which does follow symlinks.  */
+#if CHOWN_MODIFIES_SYMLINK
+# undef chown
+#endif
+
 /* Work just like chown, except when FILE is a symbolic link.
    In that case, set errno to EOPNOTSUPP and return -1.
    But if autoconf tests determined that chown modifies
diff --git a/lib/unistd.in.h b/lib/unistd.in.h
index c321987..6b0513b 100644
--- a/lib/unistd.in.h
+++ b/lib/unistd.in.h
@@ -126,18 +126,16 @@ extern "C" {

 #if @GNULIB_CHOWN@
 # if @REPLACE_CHOWN@
-#  ifndef REPLACE_CHOWN
-#   define REPLACE_CHOWN 1
-#  endif
-#  if REPLACE_CHOWN
+#  undef chown
+#  define chown rpl_chown
+# endif
+# if address@hidden@ || @REPLACE_CHOWN@
 /* Change the owner of FILE to UID (if UID is not -1) and the group of FILE
    to GID (if GID is not -1).  Follow symbolic links.
    Return 0 if successful, otherwise -1 and errno set.
    See the POSIX:2001 specification
    <http://www.opengroup.org/susv3xsh/chown.html>.  */
-#   define chown rpl_chown
 extern int chown (const char *file, uid_t uid, gid_t gid);
-#  endif
 # endif
 #elif defined GNULIB_POSIXCHECK
 # undef chown
diff --git a/m4/chown.m4 b/m4/chown.m4
index ac76d3f..d6ec8c6 100644
--- a/m4/chown.m4
+++ b/m4/chown.m4
@@ -1,4 +1,4 @@
-# serial 19
+# serial 20
 # Determine whether we need the chown wrapper.

 dnl Copyright (C) 1997-2001, 2003-2005, 2007, 2009
@@ -20,20 +20,45 @@ AC_DEFUN([gl_FUNC_CHOWN],
   AC_REQUIRE([AC_TYPE_UID_T])
   AC_REQUIRE([AC_FUNC_CHOWN])
   AC_REQUIRE([gl_FUNC_CHOWN_FOLLOWS_SYMLINK])
-  AC_CHECK_FUNCS_ONCE([chown])
+  AC_CHECK_FUNCS_ONCE([chown fchown])

-  if test $ac_cv_func_chown_works = no; then
-    AC_DEFINE([CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE], [1],
-      [Define if chown is not POSIX compliant regarding IDs of -1.])
-  fi
-
-  # If chown has either of the above problems, then we need the wrapper.
-  if test $ac_cv_func_chown_works$gl_cv_func_chown_follows_symlink = yesyes; 
then
-    : # no wrapper needed
-  else
-    REPLACE_CHOWN=1
+  if test $ac_cv_func_chown = no; then
+    HAVE_CHOWN=0
     AC_LIBOBJ([chown])
-    gl_PREREQ_CHOWN
+  else
+    if test $gl_cv_func_chown_follows_symlink = no; then
+      REPLACE_CHOWN=1
+      AC_LIBOBJ([chown])
+    fi
+    if test $ac_cv_func_chown_works = no; then
+      AC_DEFINE([CHOWN_FAILS_TO_HONOR_ID_OF_NEGATIVE_ONE], [1],
+        [Define if chown is not POSIX compliant regarding IDs of -1.])
+      REPLACE_CHOWN=1
+      AC_LIBOBJ([chown])
+    fi
+    AC_CACHE_CHECK([whether chown honors trailing slash],
+      [gl_cv_func_chown_slash_works],
+      [touch conftest.file && rm -f conftest.link
+       AC_RUN_IFELSE([AC_LANG_PROGRAM([[
+#include <unistd.h>
+#include <stdlib.h>
+#include <errno.h>
+]], [[    if (symlink ("conftest.file", "conftest.file")) return 1;
+          if (chown ("conftest.dangle/", getuid (), getgid ()) == 0) return 2;
+        ]])],
+        [gl_cv_func_chown_slash_works=yes],
+        [gl_cv_func_chown_slash_works=no],
+        [gl_cv_func_chown_slash_works="guessing no"])
+      rm -f conftest.link conftest.file])
+    if test "$gl_cv_func_chown_slash_works" != yes; then
+      AC_DEFINE([CHOWN_TRAILING_SLASH_BUG], [1],
+        [Define if chown mishandles trailing slash.])
+      REPLACE_CHOWN=1
+      AC_LIBOBJ([chown])
+    fi
+    if test $REPLACE_CHOWN = 1 && test $ac_cv_func_fchown = no; then
+      AC_LIBOBJ([fchown-stub])
+    fi
   fi
 ])

@@ -41,8 +66,8 @@ AC_DEFUN([gl_FUNC_CHOWN],
 AC_DEFUN([gl_FUNC_CHOWN_FOLLOWS_SYMLINK],
 [
   AC_CACHE_CHECK(
-    [whether chown(2) dereferences symlinks],
-    gl_cv_func_chown_follows_symlink,
+    [whether chown dereferences symlinks],
+    [gl_cv_func_chown_follows_symlink],
     [
       AC_RUN_IFELSE([AC_LANG_SOURCE([[
 #include <unistd.h>
@@ -76,9 +101,3 @@ AC_DEFUN([gl_FUNC_CHOWN_FOLLOWS_SYMLINK],
       [Define if chown modifies symlinks.])
   fi
 ])
-
-# Prerequisites of lib/chown.c.
-AC_DEFUN([gl_PREREQ_CHOWN],
-[
-  AC_CHECK_FUNC([fchown], , [AC_LIBOBJ([fchown-stub])])
-])
diff --git a/m4/unistd_h.m4 b/m4/unistd_h.m4
index 5c94916..bd368c6 100644
--- a/m4/unistd_h.m4
+++ b/m4/unistd_h.m4
@@ -1,4 +1,4 @@
-# unistd_h.m4 serial 32
+# unistd_h.m4 serial 33
 dnl Copyright (C) 2006-2009 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -68,6 +68,7 @@ AC_DEFUN([gl_UNISTD_H_DEFAULTS],
   GNULIB_UNLINKAT=0;         AC_SUBST([GNULIB_UNLINKAT])
   GNULIB_WRITE=0;            AC_SUBST([GNULIB_WRITE])
   dnl Assume proper GNU behavior unless another module says otherwise.
+  HAVE_CHOWN=1;           AC_SUBST([HAVE_CHOWN])
   HAVE_DUP2=1;            AC_SUBST([HAVE_DUP2])
   HAVE_DUP3=1;            AC_SUBST([HAVE_DUP3])
   HAVE_EUIDACCESS=1;      AC_SUBST([HAVE_EUIDACCESS])
diff --git a/modules/chown-tests b/modules/chown-tests
new file mode 100644
index 0000000..8bb3d21
--- /dev/null
+++ b/modules/chown-tests
@@ -0,0 +1,18 @@
+Files:
+tests/test-chown.h
+tests/test-chown.c
+
+Depends-on:
+lstat
+mgetgroups
+sleep
+stat-time
+stdbool
+symlink
+
+configure.ac:
+AC_CHECK_FUNCS_ONCE([getegid usleep])
+
+Makefile.am:
+TESTS += test-chown
+check_PROGRAMS += test-chown
diff --git a/modules/lchown b/modules/lchown
index 65c084c..c50537a 100644
--- a/modules/lchown
+++ b/modules/lchown
@@ -8,6 +8,7 @@ m4/lchown.m4
 Depends-on:
 chown
 errno
+lstat
 sys_stat
 unistd

diff --git a/modules/unistd b/modules/unistd
index 48259ac..031d707 100644
--- a/modules/unistd
+++ b/modules/unistd
@@ -60,6 +60,7 @@ unistd.h: unistd.in.h
              -e 's|@''GNULIB_UNLINK''@|$(GNULIB_UNLINK)|g' \
              -e 's|@''GNULIB_UNLINKAT''@|$(GNULIB_UNLINKAT)|g' \
              -e 's|@''GNULIB_WRITE''@|$(GNULIB_WRITE)|g' \
+             -e 's|@''HAVE_CHOWN''@|$(HAVE_CHOWN)|g' \
              -e 's|@''HAVE_DUP2''@|$(HAVE_DUP2)|g' \
              -e 's|@''HAVE_DUP3''@|$(HAVE_DUP3)|g' \
              -e 's|@''HAVE_EUIDACCESS''@|$(HAVE_EUIDACCESS)|g' \
diff --git a/tests/test-chown.c b/tests/test-chown.c
new file mode 100644
index 0000000..4265970
--- /dev/null
+++ b/tests/test-chown.c
@@ -0,0 +1,56 @@
+/* Tests of chown.
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Written by Eric Blake <address@hidden>, 2009.  */
+
+#include <config.h>
+
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "mgetgroups.h"
+#include "stat-time.h"
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+        {                                                                    \
+          fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__);  \
+          fflush (stderr);                                                   \
+          abort ();                                                          \
+        }                                                                    \
+    }                                                                        \
+  while (0)
+
+#define BASE "test-chown.t"
+
+#include "test-chown.h"
+
+int
+main (void)
+{
+  /* Remove any leftovers from a previous partial run.  */
+  ASSERT (system ("rm -rf " BASE "*") == 0);
+
+  return test_chown (chown, true);
+}
diff --git a/tests/test-chown.h b/tests/test-chown.h
new file mode 100644
index 0000000..4eee7ce
--- /dev/null
+++ b/tests/test-chown.h
@@ -0,0 +1,271 @@
+/* Tests of chown.
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Written by Eric Blake <address@hidden>, 2009.  */
+
+/* Sleep long enough to notice a timestamp difference on the file
+   system in the current directory.  */
+static void
+nap (void)
+{
+#if !HAVE_USLEEP
+  /* Assume the worst case file system of FAT, which has a granularity
+     of 2 seconds.  */
+  sleep (2);
+#else /* HAVE_USLEEP */
+  static long delay;
+  if (!delay)
+    {
+      /* Initialize only once, by sleeping for 20 milliseconds (needed
+         since xfs has a quantization of about 10 milliseconds, even
+         though it has a granularity of 1 nanosecond, and since NTFS
+         has a default quantization of 15.25 milliseconds, even though
+         it has a granularity of 100 nanoseconds).  If the seconds
+         differ, repeat the test one more time (in case we crossed a
+         quantization boundary on a file system with 1 second
+         resolution).  If we can't observe a difference in only the
+         nanoseconds, then fall back to 2 seconds.  However, note that
+         usleep (2000000) is allowed to fail with EINVAL.  */
+      struct stat st1;
+      struct stat st2;
+      ASSERT (close (creat (BASE "tmp", 0600)) == 0);
+      ASSERT (stat (BASE "tmp", &st1) == 0);
+      ASSERT (unlink (BASE "tmp") == 0);
+      delay = 20000;
+      usleep (delay);
+      ASSERT (close (creat (BASE "tmp", 0600)) == 0);
+      ASSERT (stat (BASE "tmp", &st2) == 0);
+      ASSERT (unlink (BASE "tmp") == 0);
+      if (st1.st_mtime != st2.st_mtime)
+        {
+          /* Seconds differ, give it one more shot.  */
+          st1 = st2;
+          usleep (delay);
+          ASSERT (close (creat (BASE "tmp", 0600)) == 0);
+          ASSERT (stat (BASE "tmp", &st2) == 0);
+          ASSERT (unlink (BASE "tmp") == 0);
+        }
+      if (! (st1.st_mtime == st2.st_mtime
+             && get_stat_mtime_ns (&st1) < get_stat_mtime_ns (&st2)))
+        delay = 2000000;
+    }
+  if (delay == 2000000)
+    sleep (2);
+  else
+    usleep (delay);
+#endif /* HAVE_USLEEP */
+}
+
+#if !HAVE_GETEGID
+# define getegid() (-1)
+#endif
+
+/* This file is designed to test chown(n,o,g) and
+   chownat(AT_FDCWD,n,o,g,0).  FUNC is the function to test.  Assumes
+   that BASE and ASSERT are already defined, and that appropriate
+   headers are already included.  If PRINT, warn before skipping
+   symlink tests with status 77.  */
+
+static int
+test_chown (int (*func) (char const *, uid_t, gid_t), bool print)
+{
+  struct stat st1;
+  struct stat st2;
+  gid_t *gids = NULL;
+  int gids_count;
+  int result;
+
+  /* Solaris 8 is interesting - if the current process belongs to
+     multiple groups, the current directory is owned by a a group that
+     the current process belongs to but different than getegid(), and
+     the current directory does not have the S_ISGID bit, then regular
+     files created in the directory belong to the directory's group,
+     but symlinks belong to the current effective group id.  If
+     S_ISGID is set, then both files and symlinks belong to the
+     directory's group.  However, it is possible to run the testsuite
+     from within a directory owned by a group we don't belong to, in
+     which case all things that we create belong to the current
+     effective gid.  So, work around the issues by creating a
+     subdirectory (we are guaranteed that the subdirectory will be
+     owned by one of our current groups), change ownership of that
+     directory to the current effective gid (which will thus succeed),
+     then create all other files within that directory (eliminating
+     questions on whether inheritance or current id triumphs, since
+     the two methods resolve to the same gid).  */
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  ASSERT (stat (BASE "dir", &st1) == 0);
+
+  /* Filter out mingw, which has no concept of groups.  */
+  result = func (BASE "dir", st1.st_uid, getegid ());
+  if (result == -1 && errno == ENOSYS)
+    {
+      ASSERT (rmdir (BASE "dir") == 0);
+      if (print)
+        fputs ("skipping test: no support for ownership\n", stderr);
+      return 77;
+    }
+  ASSERT (result == 0);
+  ASSERT (chdir (BASE "dir") == 0);
+
+  ASSERT (close (creat (BASE "file", 0600)) == 0);
+  ASSERT (stat (BASE "file", &st1) == 0);
+  ASSERT (st1.st_uid != -1);
+  ASSERT (st1.st_gid != -1);
+  ASSERT (st1.st_gid == getegid ());
+
+  /* Sanity check of error cases.  */
+  errno = 0;
+  ASSERT (func ("", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func ("no_such", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func ("no_such/", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "file/", -1, -1) == -1);
+  ASSERT (errno == ENOTDIR);
+
+  /* Check that -1 does not alter ownership.  */
+  ASSERT (func (BASE "file", -1, st1.st_gid) == 0);
+  ASSERT (func (BASE "file", st1.st_uid, -1) == 0);
+  ASSERT (stat (BASE "file", &st2) == 0);
+  ASSERT (st1.st_uid == st2.st_uid);
+  ASSERT (st1.st_gid == st2.st_gid);
+
+  /* Even if the values aren't changing, ctime is required to change
+     if at least one argument is not -1.  */
+  nap ();
+  ASSERT (func (BASE "file", st1.st_uid, st1.st_gid) == 0);
+  ASSERT (stat (BASE "file", &st2) == 0);
+  ASSERT (st1.st_ctime < st2.st_ctime
+          || (st1.st_ctime == st2.st_ctime
+              && get_stat_ctime_ns (&st1) < get_stat_ctime_ns (&st2)));
+
+  /* Test symlink behavior.  */
+  if (symlink (BASE "link", BASE "link2"))
+    {
+      ASSERT (unlink (BASE "file") == 0);
+      ASSERT (chdir ("..") == 0);
+      ASSERT (rmdir (BASE "dir") == 0);
+      if (print)
+        fputs ("skipping test: symlinks not supported on this file system\n",
+               stderr);
+      return 77;
+    }
+  errno = 0;
+  ASSERT (func (BASE "link2", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "link2/", st1.st_uid, st1.st_gid) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (symlink (BASE "file", BASE "link") == 0);
+
+  /* For non-privileged users, chown can only portably succeed at
+     changing group ownership of a file we own.  If we belong to at
+     least two groups, then verifying the correct change is simple.
+     But if we belong to only one group, then we fall back on the
+     other observable effect of chown: the ctime must be updated.
+     Be careful of duplicates returned by getgroups.  */
+  gids_count = mgetgroups (NULL, -1, &gids);
+  if (2 <= gids_count && gids[0] == gids[1] && 2 < gids_count--)
+    gids[1] = gids[2];
+  if (1 < gids_count || (gids_count == 1 && gids[0] != st1.st_gid))
+    {
+      if (gids[0] == st1.st_gid)
+        {
+          ASSERT (1 < gids_count);
+          ASSERT (gids[0] != gids[1]);
+          gids[0] = gids[1];
+        }
+      ASSERT (gids[0] != st1.st_gid);
+      ASSERT (gids[0] != -1);
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+
+      errno = 0;
+      ASSERT (func (BASE "link2/", -1, gids[0]) == -1);
+      ASSERT (errno == ENOTDIR);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+
+      ASSERT (func (BASE "link2", -1, gids[0]) == 0);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (gids[0] == st2.st_gid);
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+    }
+  else
+    {
+      struct stat l1;
+      struct stat l2;
+      ASSERT (stat (BASE "file", &st1) == 0);
+      ASSERT (lstat (BASE "link", &l1) == 0);
+      ASSERT (lstat (BASE "link2", &l2) == 0);
+
+      nap ();
+      errno = 0;
+      ASSERT (func (BASE "link2/", -1, st1.st_gid) == -1);
+      ASSERT (errno == ENOTDIR);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&st1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (l1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (l2.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l2) == get_stat_ctime_ns (&st2));
+
+      ASSERT (func (BASE "link2", -1, gids[0]) == 0);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_ctime < st2.st_ctime
+              || (st1.st_ctime == st2.st_ctime
+                  && get_stat_ctime_ns (&st1) < get_stat_ctime_ns (&st2)));
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (l1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (l2.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l2) == get_stat_ctime_ns (&st2));
+    }
+
+  /* Cleanup.  */
+  free (gids);
+  ASSERT (unlink (BASE "file") == 0);
+  ASSERT (unlink (BASE "link") == 0);
+  ASSERT (unlink (BASE "link2") == 0);
+  ASSERT (chdir ("..") == 0);
+  ASSERT (rmdir (BASE "dir") == 0);
+  return 0;
+}
-- 
1.6.4.2


>From eae509c9a7d8ba4eb76cfe15c2b97faaeea1a63c Mon Sep 17 00:00:00 2001
From: Eric Blake <address@hidden>
Date: Thu, 12 Nov 2009 21:45:20 -0700
Subject: [PATCH 2/2] lchown: detect Solaris and FreeBSD bug

Solaris 9 and FreeBSD 7.2 lchown("link-to-file/",uid,gid)
mistakenly changes ownership of "file".

* lib/lchown.c (rpl_lchown): Work around bug.
* m4/lchown.m4 (gl_FUNC_LCHOWN): Check for trailing slash bugs.
* m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witness.
* modules/unistd (Makefile.am): Populate it.
* lib/unistd.in.h (lchown): Update declaration.
* doc/posix-functions/lchown.texi (lchown): Document the bug.
* modules/lchown-tests: New file.
* tests/test-lchown.h (test_lchown): Likewise.
* tests/test-lchown.c (main): Likewise.

Signed-off-by: Eric Blake <address@hidden>
---
 ChangeLog                       |   11 ++
 doc/posix-functions/lchown.texi |    8 +-
 lib/lchown.c                    |   35 ++++-
 lib/unistd.in.h                 |    5 +-
 m4/lchown.m4                    |   12 +-
 m4/unistd_h.m4                  |    3 +-
 modules/lchown                  |    2 +-
 modules/lchown-tests            |   17 ++
 modules/unistd                  |    1 +
 tests/test-lchown.c             |   56 +++++++
 tests/test-lchown.h             |  313 +++++++++++++++++++++++++++++++++++++++
 11 files changed, 448 insertions(+), 15 deletions(-)
 create mode 100644 modules/lchown-tests
 create mode 100644 tests/test-lchown.c
 create mode 100644 tests/test-lchown.h

diff --git a/ChangeLog b/ChangeLog
index a685a18..6943cef 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -1,5 +1,16 @@
 2009-11-13  Eric Blake  <address@hidden>

+       lchown: detect Solaris and FreeBSD bug
+       * lib/lchown.c (rpl_lchown): Work around bug.
+       * m4/lchown.m4 (gl_FUNC_LCHOWN): Check for trailing slash bugs.
+       * m4/unistd_h.m4 (gl_UNISTD_H_DEFAULTS): Add witness.
+       * modules/unistd (Makefile.am): Populate it.
+       * lib/unistd.in.h (lchown): Update declaration.
+       * doc/posix-functions/lchown.texi (lchown): Document the bug.
+       * modules/lchown-tests: New file.
+       * tests/test-lchown.h (test_lchown): Likewise.
+       * tests/test-lchown.c (main): Likewise.
+
        chown: detect Solaris and FreeBSD bug
        * lib/chown.c (rpl_chown): Work around bug.
        * m4/chown.m4 (gl_FUNC_CHOWN): Check for trailing slash bugs.
diff --git a/doc/posix-functions/lchown.texi b/doc/posix-functions/lchown.texi
index e40be3f..0686bf3 100644
--- a/doc/posix-functions/lchown.texi
+++ b/doc/posix-functions/lchown.texi
@@ -9,7 +9,13 @@ lchown
 Portability problems fixed by Gnulib:
 @itemize
 @item
-This function is missing on some platforms:
+Some platforms fail to detect trailing slash on non-directories, as in
address@hidden("link-to-file/",uid,gid)}:
+FreeBSD 7.2, Solaris 9.
address@hidden
+This function is missing on some platforms; however, the replacement
+fails on symlinks if @code{chown} is supported, and fails altogether
+with @code{ENOSYS} otherwise:
 MacOS X 10.3, mingw, BeOS.
 @end itemize

diff --git a/lib/lchown.c b/lib/lchown.c
index 65434b4..2562bb2 100644
--- a/lib/lchown.c
+++ b/lib/lchown.c
@@ -25,11 +25,13 @@
 #include <errno.h>
 #include <sys/stat.h>

+#if !HAVE_LCHOWN
+
 /* If the system chown does not follow symlinks, we don't want it
    replaced by gnulib's chown, which does follow symlinks.  */
-#if CHOWN_MODIFIES_SYMLINK
-# undef chown
-#endif
+# if CHOWN_MODIFIES_SYMLINK
+#  undef chown
+# endif

 /* Work just like chown, except when FILE is a symbolic link.
    In that case, set errno to EOPNOTSUPP and return -1.
@@ -39,8 +41,8 @@
 int
 lchown (const char *file, uid_t uid, gid_t gid)
 {
-#if HAVE_CHOWN
-# if ! CHOWN_MODIFIES_SYMLINK
+# if HAVE_CHOWN
+#  if ! CHOWN_MODIFIES_SYMLINK
   struct stat stats;

   if (lstat (file, &stats) == 0 && S_ISLNK (stats.st_mode))
@@ -48,12 +50,29 @@ lchown (const char *file, uid_t uid, gid_t gid)
       errno = EOPNOTSUPP;
       return -1;
     }
-# endif
+#  endif

   return chown (file, uid, gid);

-#else /* !HAVE_CHOWN */
+# else /* !HAVE_CHOWN */
   errno = ENOSYS;
   return -1;
-#endif
+# endif
 }
+
+#else /* HAVE_LCHOWN */
+
+# undef lchown
+
+/* Work around trailing slash bugs in lchown.  */
+int
+rpl_lchown (const char *file, uid_t uid, gid_t gid)
+{
+  size_t len = strlen (file);
+  struct stat st;
+  if (file[len - 1] == '/')
+    return chown (file, uid, gid);
+  return lchown (file, uid, gid);
+}
+
+#endif /* HAVE_LCHOWN */
diff --git a/lib/unistd.in.h b/lib/unistd.in.h
index 6b0513b..c026b71 100644
--- a/lib/unistd.in.h
+++ b/lib/unistd.in.h
@@ -565,12 +565,15 @@ extern void endusershell (void);

 #if @GNULIB_LCHOWN@
 # if @REPLACE_LCHOWN@
+#  undef lchown
+#  define lchown rpl_lchown
+# endif
+# if address@hidden@ || @REPLACE_LCHOWN@
 /* Change the owner of FILE to UID (if UID is not -1) and the group of FILE
    to GID (if GID is not -1).  Do not follow symbolic links.
    Return 0 if successful, otherwise -1 and errno set.
    See the POSIX:2001 specification
    <http://www.opengroup.org/susv3xsh/lchown.html>.  */
-#  define lchown rpl_lchown
 extern int lchown (char const *file, uid_t owner, gid_t group);
 # endif
 #elif defined GNULIB_POSIXCHECK
diff --git a/m4/lchown.m4 b/m4/lchown.m4
index f509fde..e40c437 100644
--- a/m4/lchown.m4
+++ b/m4/lchown.m4
@@ -1,14 +1,16 @@
-# serial 13
+# serial 14
 # Determine whether we need the lchown wrapper.

-dnl Copyright (C) 1998, 2001, 2003-2007, 2009 Free Software Foundation, Inc.
+dnl Copyright (C) 1998, 2001, 2003-2007, 2009 Free Software
+dnl Foundation, Inc.

 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
 dnl with or without modifications, as long as this notice is preserved.

 dnl From Jim Meyering.
-dnl Provide lchown on systems that lack it.
+dnl Provide lchown on systems that lack it, and work around trailing
+dnl slash bugs on systems that have it.

 AC_DEFUN([gl_FUNC_LCHOWN],
 [
@@ -16,6 +18,10 @@ AC_DEFUN([gl_FUNC_LCHOWN],
   AC_REQUIRE([gl_FUNC_CHOWN])
   AC_REPLACE_FUNCS([lchown])
   if test $ac_cv_func_lchown = no; then
+    HAVE_LCHOWN=0
+  elif test "$gl_cv_func_chown_slash_works" != yes; then
+    dnl Trailing slash bugs in chown also occur in lchown.
+    AC_LIBOBJ([lchown])
     REPLACE_LCHOWN=1
   fi
 ])
diff --git a/m4/unistd_h.m4 b/m4/unistd_h.m4
index bd368c6..88e60a0 100644
--- a/m4/unistd_h.m4
+++ b/m4/unistd_h.m4
@@ -1,4 +1,4 @@
-# unistd_h.m4 serial 33
+# unistd_h.m4 serial 34
 dnl Copyright (C) 2006-2009 Free Software Foundation, Inc.
 dnl This file is free software; the Free Software Foundation
 dnl gives unlimited permission to copy and/or distribute it,
@@ -82,6 +82,7 @@ AC_DEFUN([gl_UNISTD_H_DEFAULTS],
   HAVE_GETHOSTNAME=1;     AC_SUBST([HAVE_GETHOSTNAME])
   HAVE_GETPAGESIZE=1;     AC_SUBST([HAVE_GETPAGESIZE])
   HAVE_GETUSERSHELL=1;    AC_SUBST([HAVE_GETUSERSHELL])
+  HAVE_LCHOWN=1;          AC_SUBST([HAVE_LCHOWN])
   HAVE_LINK=1;            AC_SUBST([HAVE_LINK])
   HAVE_LINKAT=1;          AC_SUBST([HAVE_LINKAT])
   HAVE_PIPE2=1;           AC_SUBST([HAVE_PIPE2])
diff --git a/modules/lchown b/modules/lchown
index c50537a..233e334 100644
--- a/modules/lchown
+++ b/modules/lchown
@@ -25,4 +25,4 @@ License:
 GPL

 Maintainer:
-Jim Meyering
+Jim Meyering, Eric Blake
diff --git a/modules/lchown-tests b/modules/lchown-tests
new file mode 100644
index 0000000..c71ad20
--- /dev/null
+++ b/modules/lchown-tests
@@ -0,0 +1,17 @@
+Files:
+tests/test-lchown.h
+tests/test-lchown.c
+
+Depends-on:
+mgetgroups
+sleep
+stat-time
+stdbool
+symlink
+
+configure.ac:
+AC_CHECK_FUNCS_ONCE([getegid usleep])
+
+Makefile.am:
+TESTS += test-lchown
+check_PROGRAMS += test-lchown
diff --git a/modules/unistd b/modules/unistd
index 031d707..1cb3b0b 100644
--- a/modules/unistd
+++ b/modules/unistd
@@ -74,6 +74,7 @@ unistd.h: unistd.in.h
              -e 's|@''HAVE_GETHOSTNAME''@|$(HAVE_GETHOSTNAME)|g' \
              -e 's|@''HAVE_GETPAGESIZE''@|$(HAVE_GETPAGESIZE)|g' \
              -e 's|@''HAVE_GETUSERSHELL''@|$(HAVE_GETUSERSHELL)|g' \
+             -e 's|@''HAVE_LCHOWN''@|$(HAVE_LCHOWN)|g' \
              -e 's|@''HAVE_LINK''@|$(HAVE_LINK)|g' \
              -e 's|@''HAVE_LINKAT''@|$(HAVE_LINKAT)|g' \
              -e 's|@''HAVE_PIPE2''@|$(HAVE_PIPE2)|g' \
diff --git a/tests/test-lchown.c b/tests/test-lchown.c
new file mode 100644
index 0000000..78c2940
--- /dev/null
+++ b/tests/test-lchown.c
@@ -0,0 +1,56 @@
+/* Tests of lchown.
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Written by Eric Blake <address@hidden>, 2009.  */
+
+#include <config.h>
+
+#include <unistd.h>
+
+#include <fcntl.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <sys/stat.h>
+
+#include "mgetgroups.h"
+#include "stat-time.h"
+
+#define ASSERT(expr) \
+  do                                                                         \
+    {                                                                        \
+      if (!(expr))                                                           \
+        {                                                                    \
+          fprintf (stderr, "%s:%d: assertion failed\n", __FILE__, __LINE__);  \
+          fflush (stderr);                                                   \
+          abort ();                                                          \
+        }                                                                    \
+    }                                                                        \
+  while (0)
+
+#define BASE "test-lchown.t"
+
+#include "test-lchown.h"
+
+int
+main (void)
+{
+  /* Remove any leftovers from a previous partial run.  */
+  ASSERT (system ("rm -rf " BASE "*") == 0);
+
+  return test_lchown (lchown, true);
+}
diff --git a/tests/test-lchown.h b/tests/test-lchown.h
new file mode 100644
index 0000000..ecbcc77
--- /dev/null
+++ b/tests/test-lchown.h
@@ -0,0 +1,313 @@
+/* Tests of lchown.
+   Copyright (C) 2009 Free Software Foundation, Inc.
+
+   This program is free software: you can redistribute it and/or modify
+   it under the terms of the GNU General Public License as published by
+   the Free Software Foundation; either version 3 of the License, or
+   (at your option) any later version.
+
+   This program is distributed in the hope that it will be useful,
+   but WITHOUT ANY WARRANTY; without even the implied warranty of
+   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+   GNU General Public License for more details.
+
+   You should have received a copy of the GNU General Public License
+   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */
+
+/* Written by Eric Blake <address@hidden>, 2009.  */
+
+/* Sleep long enough to notice a timestamp difference on the file
+   system in the current directory.  */
+static void
+nap (void)
+{
+#if !HAVE_USLEEP
+  /* Assume the worst case file system of FAT, which has a granularity
+     of 2 seconds.  */
+  sleep (2);
+#else /* HAVE_USLEEP */
+  static long delay;
+  if (!delay)
+    {
+      /* Initialize only once, by sleeping for 20 milliseconds (needed
+         since xfs has a quantization of about 10 milliseconds, even
+         though it has a granularity of 1 nanosecond, and since NTFS
+         has a default quantization of 15.25 milliseconds, even though
+         it has a granularity of 100 nanoseconds).  If the seconds
+         differ, repeat the test one more time (in case we crossed a
+         quantization boundary on a file system with 1 second
+         resolution).  If we can't observe a difference in only the
+         nanoseconds, then fall back to 2 seconds.  However, note that
+         usleep (2000000) is allowed to fail with EINVAL.  */
+      struct stat st1;
+      struct stat st2;
+      ASSERT (close (creat (BASE "tmp", 0600)) == 0);
+      ASSERT (stat (BASE "tmp", &st1) == 0);
+      ASSERT (unlink (BASE "tmp") == 0);
+      delay = 20000;
+      usleep (delay);
+      ASSERT (close (creat (BASE "tmp", 0600)) == 0);
+      ASSERT (stat (BASE "tmp", &st2) == 0);
+      ASSERT (unlink (BASE "tmp") == 0);
+      if (st1.st_mtime != st2.st_mtime)
+        {
+          /* Seconds differ, give it one more shot.  */
+          st1 = st2;
+          usleep (delay);
+          ASSERT (close (creat (BASE "tmp", 0600)) == 0);
+          ASSERT (stat (BASE "tmp", &st2) == 0);
+          ASSERT (unlink (BASE "tmp") == 0);
+        }
+      if (! (st1.st_mtime == st2.st_mtime
+             && get_stat_mtime_ns (&st1) < get_stat_mtime_ns (&st2)))
+        delay = 2000000;
+    }
+  if (delay == 2000000)
+    sleep (2);
+  else
+    usleep (delay);
+#endif /* HAVE_USLEEP */
+}
+
+#if !HAVE_GETEGID
+# define getegid() (-1)
+#endif
+
+/* This file is designed to test lchown(n,o,g) and
+   chownat(AT_FDCWD,n,o,g,AT_SYMLINK_NOFOLLOW).  FUNC is the function
+   to test.  Assumes that BASE and ASSERT are already defined, and
+   that appropriate headers are already included.  If PRINT, warn
+   before skipping symlink tests with status 77.  */
+
+static int
+test_lchown (int (*func) (char const *, uid_t, gid_t), bool print)
+{
+  struct stat st1;
+  struct stat st2;
+  gid_t *gids = NULL;
+  int gids_count;
+  int result;
+
+  /* Solaris 8 is interesting - if the current process belongs to
+     multiple groups, the current directory is owned by a a group that
+     the current process belongs to but different than getegid(), and
+     the current directory does not have the S_ISGID bit, then regular
+     files created in the directory belong to the directory's group,
+     but symlinks belong to the current effective group id.  If
+     S_ISGID is set, then both files and symlinks belong to the
+     directory's group.  However, it is possible to run the testsuite
+     from within a directory owned by a group we don't belong to, in
+     which case all things that we create belong to the current
+     effective gid.  So, work around the issues by creating a
+     subdirectory (we are guaranteed that the subdirectory will be
+     owned by one of our current groups), change ownership of that
+     directory to the current effective gid (which will thus succeed),
+     then create all other files within that directory (eliminating
+     questions on whether inheritance or current id triumphs, since
+     the two methods resolve to the same gid).  */
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  ASSERT (stat (BASE "dir", &st1) == 0);
+
+  /* Filter out mingw, which has no concept of groups.  */
+  result = func (BASE "dir", st1.st_uid, getegid ());
+  if (result == -1 && errno == ENOSYS)
+    {
+      ASSERT (rmdir (BASE "dir") == 0);
+      if (print)
+        fputs ("skipping test: no support for ownership\n", stderr);
+      return 77;
+    }
+  ASSERT (result == 0);
+  ASSERT (chdir (BASE "dir") == 0);
+
+  ASSERT (close (creat (BASE "file", 0600)) == 0);
+  ASSERT (stat (BASE "file", &st1) == 0);
+  ASSERT (st1.st_uid != -1);
+  ASSERT (st1.st_gid != -1);
+  ASSERT (st1.st_gid == getegid ());
+
+  /* Sanity check of error cases.  */
+  errno = 0;
+  ASSERT (func ("", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func ("no_such", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func ("no_such/", -1, -1) == -1);
+  ASSERT (errno == ENOENT);
+  errno = 0;
+  ASSERT (func (BASE "file/", -1, -1) == -1);
+  ASSERT (errno == ENOTDIR);
+
+  /* Check that -1 does not alter ownership.  */
+  ASSERT (func (BASE "file", -1, st1.st_gid) == 0);
+  ASSERT (func (BASE "file", st1.st_uid, -1) == 0);
+  ASSERT (stat (BASE "file", &st2) == 0);
+  ASSERT (st1.st_uid == st2.st_uid);
+  ASSERT (st1.st_gid == st2.st_gid);
+
+  /* Even if the values aren't changing, ctime is required to change
+     if at least one argument is not -1.  */
+  nap ();
+  ASSERT (func (BASE "file", st1.st_uid, st1.st_gid) == 0);
+  ASSERT (stat (BASE "file", &st2) == 0);
+  ASSERT (st1.st_ctime < st2.st_ctime
+          || (st1.st_ctime == st2.st_ctime
+              && get_stat_ctime_ns (&st1) < get_stat_ctime_ns (&st2)));
+
+  /* Test symlink behavior.  */
+  if (symlink (BASE "link", BASE "link2"))
+    {
+      ASSERT (unlink (BASE "file") == 0);
+      ASSERT (chdir ("..") == 0);
+      ASSERT (rmdir (BASE "dir") == 0);
+      if (print)
+        fputs ("skipping test: symlinks not supported on this file system\n",
+               stderr);
+      return 77;
+    }
+  result = func (BASE "link2", -1, -1);
+  if (result == -1 && errno == ENOSYS)
+    {
+      ASSERT (unlink (BASE "file") == 0);
+      ASSERT (unlink (BASE "link2") == 0);
+      ASSERT (chdir ("..") == 0);
+      ASSERT (rmdir (BASE "dir") == 0);
+      if (print)
+        fputs ("skipping test: symlink ownership not supported\n", stderr);
+      return 77;
+    }
+  ASSERT (result == 0);
+  errno = 0;
+  ASSERT (func (BASE "link2/", st1.st_uid, st1.st_gid) == -1);
+  ASSERT (errno == ENOENT);
+  ASSERT (symlink (BASE "file", BASE "link") == 0);
+  ASSERT (mkdir (BASE "dir", 0700) == 0);
+  ASSERT (symlink (BASE "dir", BASE "link3") == 0);
+
+  /* For non-privileged users, lchown can only portably succeed at
+     changing group ownership of a file we own.  If we belong to at
+     least two groups, then verifying the correct change is simple.
+     But if we belong to only one group, then we fall back on the
+     other observable effect of lchown: the ctime must be updated.
+     Be careful of duplicates returned by getgroups.  */
+  gids_count = mgetgroups (NULL, -1, &gids);
+  if (2 <= gids_count && gids[0] == gids[1] && 2 < gids_count--)
+    gids[1] = gids[2];
+  if (1 < gids_count || (gids_count == 1 && gids[0] != st1.st_gid))
+    {
+      if (gids[0] == st1.st_gid)
+        {
+          ASSERT (1 < gids_count);
+          ASSERT (gids[0] != gids[1]);
+          gids[0] = gids[1];
+        }
+      ASSERT (gids[0] != st1.st_gid);
+      ASSERT (gids[0] != -1);
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+
+      errno = 0;
+      ASSERT (func (BASE "link2/", -1, gids[0]) == -1);
+      ASSERT (errno == ENOTDIR);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+
+      ASSERT (func (BASE "link2", -1, gids[0]) == 0);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (gids[0] == st2.st_gid);
+
+      /* Trailing slash follows through to directory.  */
+      ASSERT (lstat (BASE "link3", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "dir", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+
+      ASSERT (func (BASE "link3/", -1, gids[0]) == 0);
+      ASSERT (lstat (BASE "link3", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (st1.st_gid == st2.st_gid);
+      ASSERT (lstat (BASE "dir", &st2) == 0);
+      ASSERT (st1.st_uid == st2.st_uid);
+      ASSERT (gids[0] == st2.st_gid);
+    }
+  else
+    {
+      struct stat l1;
+      struct stat l2;
+      ASSERT (stat (BASE "file", &st1) == 0);
+      ASSERT (lstat (BASE "link", &l1) == 0);
+      ASSERT (lstat (BASE "link2", &l2) == 0);
+
+      nap ();
+      errno = 0;
+      ASSERT (func (BASE "link2/", -1, st1.st_gid) == -1);
+      ASSERT (errno == ENOTDIR);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&st1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (l1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (l2.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l2) == get_stat_ctime_ns (&st2));
+
+      ASSERT (func (BASE "link2", -1, gids[0]) == 0);
+      ASSERT (stat (BASE "file", &st2) == 0);
+      ASSERT (st1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&st1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link", &st2) == 0);
+      ASSERT (l1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "link2", &st2) == 0);
+      ASSERT (l2.st_ctime < st2.st_ctime
+              || (l2.st_ctime == st2.st_ctime
+                  && get_stat_ctime_ns (&l2) < get_stat_ctime_ns (&st2)));
+
+      /* Trailing slash follows through to directory.  */
+      ASSERT (lstat (BASE "dir", &st1) == 0);
+      ASSERT (lstat (BASE "link3", &l1) == 0);
+      nap ();
+      ASSERT (func (BASE "link3/", -1, gids[0]) == 0);
+      ASSERT (lstat (BASE "link3", &st2) == 0);
+      ASSERT (l1.st_ctime == st2.st_ctime);
+      ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2));
+      ASSERT (lstat (BASE "dir", &st2) == 0);
+      ASSERT (st1.st_ctime < st2.st_ctime
+              || (st1.st_ctime == st2.st_ctime
+                  && get_stat_ctime_ns (&st1) < get_stat_ctime_ns (&st2)));
+    }
+
+  /* Cleanup.  */
+  free (gids);
+  ASSERT (unlink (BASE "file") == 0);
+  ASSERT (unlink (BASE "link") == 0);
+  ASSERT (unlink (BASE "link2") == 0);
+  ASSERT (unlink (BASE "link3") == 0);
+  ASSERT (rmdir (BASE "dir") == 0);
+  ASSERT (chdir ("..") == 0);
+  ASSERT (rmdir (BASE "dir") == 0);
+  return 0;
+}
-- 
1.6.4.2


reply via email to

[Prev in Thread] Current Thread [Next in Thread]