diff options
author | Eric Blake <ebb9@byu.net> | 2009-11-12 21:45:20 -0700 |
---|---|---|
committer | Eric Blake <ebb9@byu.net> | 2009-11-14 10:33:28 -0700 |
commit | 9cd8acbae7f8ca3985001de0752b22e2a28d0cd7 (patch) | |
tree | e4e6533b153eb4537063a328635b014719e6bd4e /tests/test-chown.h | |
parent | fb1b608caaa3de195f3d97356e95a165ef5517da (diff) | |
download | gnulib-9cd8acbae7f8ca3985001de0752b22e2a28d0cd7.tar.gz |
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 <ebb9@byu.net>
Diffstat (limited to 'tests/test-chown.h')
-rw-r--r-- | tests/test-chown.h | 269 |
1 files changed, 269 insertions, 0 deletions
diff --git a/tests/test-chown.h b/tests/test-chown.h new file mode 100644 index 0000000000..12082e4ef6 --- /dev/null +++ b/tests/test-chown.h @@ -0,0 +1,269 @@ +/* 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 <ebb9@byu.net>, 2009. */ + +#define TEST_CHOWN_NAP +/* 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 (close (creat (BASE "dir/file", 0600)) == 0); + ASSERT (stat (BASE "dir/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 "dir/file/", -1, -1) == -1); + ASSERT (errno == ENOTDIR); + + /* Check that -1 does not alter ownership. */ + ASSERT (func (BASE "dir/file", -1, st1.st_gid) == 0); + ASSERT (func (BASE "dir/file", st1.st_uid, -1) == 0); + ASSERT (stat (BASE "dir/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 "dir/file", st1.st_uid, st1.st_gid) == 0); + ASSERT (stat (BASE "dir/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 ("link", BASE "dir/link2")) + { + ASSERT (unlink (BASE "dir/file") == 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 "dir/link2", -1, -1) == -1); + ASSERT (errno == ENOENT); + errno = 0; + ASSERT (func (BASE "dir/link2/", st1.st_uid, st1.st_gid) == -1); + ASSERT (errno == ENOENT); + ASSERT (symlink ("file", BASE "dir/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 "dir/link", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (st1.st_gid == st2.st_gid); + ASSERT (lstat (BASE "dir/link2", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (st1.st_gid == st2.st_gid); + + errno = 0; + ASSERT (func (BASE "dir/link2/", -1, gids[0]) == -1); + ASSERT (errno == ENOTDIR); + ASSERT (stat (BASE "dir/file", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (st1.st_gid == st2.st_gid); + ASSERT (lstat (BASE "dir/link", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (st1.st_gid == st2.st_gid); + ASSERT (lstat (BASE "dir/link2", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (st1.st_gid == st2.st_gid); + + ASSERT (func (BASE "dir/link2", -1, gids[0]) == 0); + ASSERT (stat (BASE "dir/file", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (gids[0] == st2.st_gid); + ASSERT (lstat (BASE "dir/link", &st2) == 0); + ASSERT (st1.st_uid == st2.st_uid); + ASSERT (st1.st_gid == st2.st_gid); + ASSERT (lstat (BASE "dir/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 "dir/file", &st1) == 0); + ASSERT (lstat (BASE "dir/link", &l1) == 0); + ASSERT (lstat (BASE "dir/link2", &l2) == 0); + + nap (); + errno = 0; + ASSERT (func (BASE "dir/link2/", -1, st1.st_gid) == -1); + ASSERT (errno == ENOTDIR); + ASSERT (stat (BASE "dir/file", &st2) == 0); + ASSERT (st1.st_ctime == st2.st_ctime); + ASSERT (get_stat_ctime_ns (&st1) == get_stat_ctime_ns (&st2)); + ASSERT (lstat (BASE "dir/link", &st2) == 0); + ASSERT (l1.st_ctime == st2.st_ctime); + ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2)); + ASSERT (lstat (BASE "dir/link2", &st2) == 0); + ASSERT (l2.st_ctime == st2.st_ctime); + ASSERT (get_stat_ctime_ns (&l2) == get_stat_ctime_ns (&st2)); + + ASSERT (func (BASE "dir/link2", -1, gids[0]) == 0); + ASSERT (stat (BASE "dir/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 "dir/link", &st2) == 0); + ASSERT (l1.st_ctime == st2.st_ctime); + ASSERT (get_stat_ctime_ns (&l1) == get_stat_ctime_ns (&st2)); + ASSERT (lstat (BASE "dir/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 "dir/file") == 0); + ASSERT (unlink (BASE "dir/link") == 0); + ASSERT (unlink (BASE "dir/link2") == 0); + ASSERT (rmdir (BASE "dir") == 0); + return 0; +} |