Linux: Work around kernel bugs in chmod on /proc/self/fd paths [BZ #14578]

It appears that the ability to change symbolic link modes through such
paths is unintended.  On several file systems, the operation fails with
EOPNOTSUPP, even though the symbolic link permissions are updated.
The expected behavior is a failure to update the permissions, without
file system changes.

Reviewed-by: Matheus Castanho <msc@linux.ibm.com>
This commit is contained in:
Florian Weimer 2020-02-18 17:52:27 +01:00
parent f4349837d9
commit a492b1e5ef
2 changed files with 45 additions and 63 deletions

View File

@ -102,68 +102,39 @@ test_1 (bool do_relative_path, int (*chmod_func) (int fd, const char *, mode_t,
TEST_VERIFY ((st.st_mode & 0777) != 2);
mode_t original_symlink_mode = st.st_mode;
/* Set to true if AT_SYMLINK_NOFOLLOW is supported. */
bool nofollow;
/* 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);
int ret = chmod_func (fd, path_file, 2, AT_SYMLINK_NOFOLLOW);
if (ret == 0)
{
printf ("info: AT_SYMLINK_NOFOLLOW support in %s\n", tempdir);
nofollow = true;
}
else
{
printf ("info: no AT_SYMLINK_NOFOLLOW support in %s\n", tempdir);
nofollow = false;
/* Set up things for the code below. */
TEST_COMPARE (chmod_func (fd, path_file, 2, 0), 0);
}
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);
arg = select_path (do_relative_path, path_to_file, "to-file");
TEST_COMPARE (chmod_func (fd, path_to_file, 1, 0), 0);
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 may fail. */
/* Changing the mode of a symbolic link should fail. */
arg = select_path (do_relative_path, path_to_file, "to-file");
ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW);
if (nofollow)
{
TEST_COMPARE (ret, 0);
int ret = chmod_func (fd, arg, 2, AT_SYMLINK_NOFOLLOW);
TEST_COMPARE (ret, -1);
TEST_COMPARE (errno, EOPNOTSUPP);
/* The mode of the link changed. */
xlstat (path_to_file, &st);
TEST_COMPARE (st.st_mode & 0777, 2);
/* 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);
/* But the mode of the file is unchanged. */
xstat (path_file, &st);
TEST_COMPARE (st.st_mode & 0777, 1);
}
else
{
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);
}
/* If we have NOFOLLOW support, we should be able to change the mode
of a dangling symbolic link or a symbolic link loop. */
/* 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)
{
@ -178,19 +149,10 @@ test_1 (bool do_relative_path, int (*chmod_func) (int fd, const char *, mode_t,
original_symlink_mode = st.st_mode;
arg = select_path (do_relative_path, path, filename);
ret = chmod_func (fd, arg, new_mode, AT_SYMLINK_NOFOLLOW);
if (nofollow)
{
TEST_COMPARE (ret, 0);
xlstat (path, &st);
TEST_COMPARE (st.st_mode & 0777, new_mode);
}
else /* !nofollow. */
{
TEST_COMPARE (ret, -1);
TEST_COMPARE (errno, EOPNOTSUPP);
xlstat (path, &st);
TEST_COMPARE (st.st_mode, original_symlink_mode);
}
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

View File

@ -45,6 +45,30 @@ fchmodat (int fd, const char *file, mode_t mode, int flag)
caller can treat them as temporary if necessary. */
return pathfd;
/* Use fstatat because fstat does not work on O_PATH descriptors
before Linux 3.6. */
struct stat64 st;
if (fstatat64 (pathfd, "", &st, AT_EMPTY_PATH) != 0)
{
__close_nocancel (pathfd);
return -1;
}
/* Some Linux versions with some file systems can actually
change symbolic link permissions via /proc, but this is not
intentional, and it gives inconsistent results (e.g., error
return despite mode change). The expected behavior is that
symbolic link modes cannot be changed at all, and this check
enforces that. */
if (S_ISLNK (st.st_mode))
{
__close_nocancel (pathfd);
__set_errno (EOPNOTSUPP);
return -1;
}
/* For most file systems, fchmod does not operate on O_PATH
descriptors, so go through /proc. */
char buf[32];
if (__snprintf (buf, sizeof (buf), "/proc/self/fd/%d", pathfd) < 0)
{
@ -54,10 +78,6 @@ fchmodat (int fd, const char *file, mode_t mode, int flag)
return -1;
}
/* This operates directly on the symbolic link if it is one.
/proc/self/fd files look like symbolic links, but they are
not. (fchmod and fchmodat do not work on O_PATH descriptors,
similar to fstat before Linux 3.6.) */
int ret = __chmod (buf, mode);
if (ret != 0)
{