-----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 e...@byu.net -----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 <e...@byu.net> 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 <e...@byu.net> --- 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 <e...@byu.net> + 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 +...@code{chown("link-to-file/",uid,gid)}: +FreeBSD 7.2, Solaris 9. +...@item 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}. +...@item +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 -...@item -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 +...@item +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 !...@have_chown@ || @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 <e...@byu.net>, 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 <e...@byu.net>, 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 <e...@byu.net> 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 <e...@byu.net> --- 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 <e...@byu.net> + 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 +...@code{lchown("link-to-file/",uid,gid)}: +FreeBSD 7.2, Solaris 9. +...@item +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 !...@have_lchown@ || @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 <e...@byu.net>, 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 <e...@byu.net>, 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