/* Tests for lchmod and fchmodat with AT_SYMLINK_NOFOLLOW. Copyright (C) 2020-2021 Free Software Foundation, Inc. This file is part of the GNU C Library. The GNU C Library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2.1 of the License, or (at your option) any later version. The GNU C Library 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 Lesser General Public License for more details. You should have received a copy of the GNU Lesser General Public License along with the GNU C Library; if not, see . */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if __has_include () # include #endif /* Array of file descriptors. */ #define DYNARRAY_STRUCT fd_list #define DYNARRAY_ELEMENT int #define DYNARRAY_INITIAL_SIZE 0 #define DYNARRAY_PREFIX fd_list_ #include static int fchmodat_with_lchmod (int fd, const char *path, mode_t mode, int flags) { TEST_COMPARE (fd, AT_FDCWD); if (flags == 0) return chmod (path, mode); else { TEST_COMPARE (flags, AT_SYMLINK_NOFOLLOW); return lchmod (path, mode); } } /* Chose the appropriate path to pass as the path argument to the *at functions. */ static const char * select_path (bool do_relative_path, const char *full_path, const char *relative_path) { if (do_relative_path) return relative_path; else return full_path; } static void update_file_time_to_y2038 (const char *fname, int flags) { #ifdef CHECK_TIME64 /* Y2038 threshold plus 1 second. */ const struct timespec ts[] = { { 0x80000001LL, 0}, { 0x80000001LL } }; TEST_VERIFY_EXIT (utimensat (AT_FDCWD, fname, ts, flags) == 0); #endif } static void test_1 (bool do_relative_path, int (*chmod_func) (int fd, const char *, mode_t, int)) { char *tempdir = support_create_temp_directory ("tst-lchmod-"); #ifdef CHECK_TIME64 if (!support_path_support_time64 (tempdir)) { puts ("info: test skipped, filesystem does not support 64 bit time_t"); return; } #endif char *path_dangling = xasprintf ("%s/dangling", tempdir); char *path_file = xasprintf ("%s/file", tempdir); char *path_loop = xasprintf ("%s/loop", tempdir); char *path_missing = xasprintf ("%s/missing", tempdir); char *path_to_file = xasprintf ("%s/to-file", tempdir); int fd; if (do_relative_path) fd = xopen (tempdir, O_DIRECTORY | O_RDONLY, 0); else fd = AT_FDCWD; add_temp_file (path_dangling); add_temp_file (path_loop); add_temp_file (path_file); add_temp_file (path_to_file); support_write_file_string (path_file, ""); xsymlink ("file", path_to_file); xsymlink ("loop", path_loop); xsymlink ("target-does-not-exist", path_dangling); update_file_time_to_y2038 (path_file, 0); update_file_time_to_y2038 (path_to_file, AT_SYMLINK_NOFOLLOW); /* Check that the modes do not collide with what we will use in the test. */ struct stat st; xstat (path_file, &st); TEST_VERIFY ((st.st_mode & 0777) != 1); xlstat (path_to_file, &st); TEST_VERIFY ((st.st_mode & 0777) != 2); mode_t original_symlink_mode = st.st_mode; /* We should be able to change the mode of a file, including through the symbolic link to-file. */ const char *arg = select_path (do_relative_path, path_file, "file"); TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0); xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 1); arg = select_path (do_relative_path, path_to_file, "to-file"); TEST_COMPARE (chmod_func (fd, arg, 2, 0), 0); xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 2); xlstat (path_to_file, &st); TEST_COMPARE (original_symlink_mode, st.st_mode); arg = select_path (do_relative_path, path_file, "file"); TEST_COMPARE (chmod_func (fd, arg, 1, 0), 0); xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 1); xlstat (path_to_file, &st); TEST_COMPARE (original_symlink_mode, st.st_mode); /* Changing the mode of a symbolic link should fail. */ arg = select_path (do_relative_path, path_to_file, "to-file"); int ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW); TEST_COMPARE (ret, -1); TEST_COMPARE (errno, EOPNOTSUPP); /* The modes should remain unchanged. */ xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 1); xlstat (path_to_file, &st); TEST_COMPARE (original_symlink_mode, st.st_mode); /* Likewise, changing dangling and looping symbolic links must fail. */ const char *paths[] = { path_dangling, path_loop }; for (size_t i = 0; i < array_length (paths); ++i) { const char *path = paths[i]; const char *filename = strrchr (path, '/'); TEST_VERIFY_EXIT (filename != NULL); ++filename; mode_t new_mode = 010 + i; xlstat (path, &st); TEST_VERIFY ((st.st_mode & 0777) != new_mode); original_symlink_mode = st.st_mode; arg = select_path (do_relative_path, path, filename); ret = chmod_func (fd, arg, new_mode, AT_SYMLINK_NOFOLLOW); TEST_COMPARE (ret, -1); TEST_COMPARE (errno, EOPNOTSUPP); xlstat (path, &st); TEST_COMPARE (st.st_mode, original_symlink_mode); } /* A missing file should always result in ENOENT. The presence of /proc does not matter. */ arg = select_path (do_relative_path, path_missing, "missing"); TEST_COMPARE (chmod_func (fd, arg, 020, 0), -1); TEST_COMPARE (errno, ENOENT); TEST_COMPARE (chmod_func (fd, arg, 020, AT_SYMLINK_NOFOLLOW), -1); TEST_COMPARE (errno, ENOENT); /* Test without available file descriptors. */ { struct fd_list fd_list; fd_list_init (&fd_list); while (true) { int ret = dup (STDOUT_FILENO); if (ret == -1) { if (errno == ENFILE || errno == EMFILE) break; FAIL_EXIT1 ("dup: %m"); } fd_list_add (&fd_list, ret); TEST_VERIFY_EXIT (!fd_list_has_failed (&fd_list)); } /* Without AT_SYMLINK_NOFOLLOW, changing the permissions should work as before. */ arg = select_path (do_relative_path, path_file, "file"); TEST_COMPARE (chmod_func (fd, arg, 3, 0), 0); xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 3); /* But with AT_SYMLINK_NOFOLLOW, even if we originally had support, we may have lost it. */ ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW); if (ret == 0) { xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 2); } else { TEST_COMPARE (ret, -1); /* The error code from the openat fallback leaks out. */ if (errno != ENFILE && errno != EMFILE) TEST_COMPARE (errno, EOPNOTSUPP); } xstat (path_file, &st); TEST_COMPARE (st.st_mode & 0777, 3); /* Close the descriptors. */ for (int *pfd = fd_list_begin (&fd_list); pfd < fd_list_end (&fd_list); ++pfd) xclose (*pfd); fd_list_free (&fd_list); } if (do_relative_path) xclose (fd); free (path_dangling); free (path_file); free (path_loop); free (path_missing); free (path_to_file); free (tempdir); } static void test_3 (void) { puts ("info: testing lchmod"); test_1 (false, fchmodat_with_lchmod); puts ("info: testing fchmodat with AT_FDCWD"); test_1 (false, fchmodat); puts ("info: testing fchmodat with relative path"); test_1 (true, fchmodat); } static int do_test (void) { struct support_descriptors *descriptors = support_descriptors_list (); /* Run the three tests in the default environment. */ test_3 (); /* Try to set up a /proc-less environment and re-test. */ #if __has_include () if (!support_become_root ()) puts ("warning: could not obtain root-like privileges"); if (!support_enter_mount_namespace ()) puts ("warning: could enter a mount namespace"); else { /* Attempt to mount an empty directory over /proc. */ char *tempdir = support_create_temp_directory ("tst-lchmod-"); bool proc_emptied = mount (tempdir, "/proc", "none", MS_BIND, NULL) == 0; if (!proc_emptied) printf ("warning: bind-mounting /proc failed: %m"); free (tempdir); puts ("info: re-running tests (after trying to empty /proc)"); test_3 (); if (proc_emptied) /* Reveal the original /proc, which is needed by the descriptors check below. */ TEST_COMPARE (umount ("/proc"), 0); } #endif /* . */ support_descriptors_check (descriptors); support_descriptors_free (descriptors); return 0; } #include