+2025-08-11 Paul Eggert <eggert@cs.ucla.edu>
+
+ Prefer readlink to lstat+S_ISLNK when easy
+ To test for a symlink, use readlink, not lstat+S_ISLNK,
+ when the lstat is used only for the symlink test.
+ This avoids EOVERFLOW issues.
+ * lib/lchown.c (rpl_lchown) [CHOWN_CHANGE_TIME_BUG]:
+ * lib/rename.c (rpl_rename):
+ [!(_WIN32 && !__CYGWIN__) && (RENAME_TRAILING_SLASH_SOURCE_BUG
+ || RENAME_DEST_EXISTS_BUG || RENAME_HARD_LINK_BUG)]:
+ * lib/renameatu.c (renameatu):
+ [HAVE_RENAMEAT && RENAME_TRAILING_SLASH_SOURCE_BUG]:
+ * lib/unlink.c (rpl_unlink):
+ * lib/unlinkat.c (rpl_unlinkat):
+ * lib/utimens.c (lutimens) [!HAVE_LUTIMENS]:
+ Prefer readlink to lstat+S_ISLNK.
+ * modules/lchown, modules/rename, modules/unlink, modules/utimens:
+ (Depends-on): Add readlink.
+ * modules/unlinkat (Depends-on): Add fstatat, readlinkat.
+
2025-08-11 Bruno Haible <bruno@clisp.org>
nlcanon tests: Fix test failure on Solaris.
if (gid != (gid_t) -1 || uid != (uid_t) -1)
{
- if (lstat (file, &st))
- return -1;
+ /* Prefer readlink to lstat+S_ISLNK, to avoid EOVERFLOW issues
+ in the common case where FILE is a non-symlink. */
+ char linkbuf[1];
+ int r = readlink (file, linkbuf, 1);
+ if (r < 0)
+ return errno == EINVAL ? chown (file, uid, gid) : r;
+
+ /* Later code can use the status, so get it if possible. */
+ r = lstat (file, &st);
+ if (r < 0)
+ return r;
stat_valid = true;
+ /* An easy check: did FILE change from a symlink to a non-symlink? */
if (!S_ISLNK (st.st_mode))
return chown (file, uid, gid);
}
goto out;
}
strip_trailing_slashes (src_temp);
- if (lstat (src_temp, &src_st))
+ char linkbuf[1];
+ if (0 <= readlink (src_temp, linkbuf, 1))
+ goto out;
+ if (errno != EINVAL)
{
rename_errno = errno;
goto out;
}
- if (S_ISLNK (src_st.st_mode))
- goto out;
}
if (dst_slash)
{
goto out;
}
strip_trailing_slashes (dst_temp);
- if (lstat (dst_temp, &dst_st))
+ char linkbuf[1];
+ if (0 <= readlink (dst_temp, linkbuf, 1))
+ goto out;
+ if (errno != EINVAL && errno != ENOENT)
{
- if (errno != ENOENT)
- {
- rename_errno = errno;
- goto out;
- }
+ rename_errno = errno;
+ goto out;
}
- else if (S_ISLNK (dst_st.st_mode))
- goto out;
}
# endif /* RENAME_TRAILING_SLASH_SOURCE_BUG || RENAME_DEST_EXISTS_BUG
|| RENAME_HARD_LINK_BUG */
goto out;
}
strip_trailing_slashes (src_temp);
- if (fstatat (fd1, src_temp, &src_st, AT_SYMLINK_NOFOLLOW))
+ char linkbuf[1];
+ if (readlinkat (fd1, src_temp, linkbuf, sizeof linkbuf) < 0)
{
- rename_errno = errno;
- goto out;
+ if (errno != EINVAL)
+ {
+ rename_errno = errno;
+ goto out;
+ }
}
- if (S_ISLNK (src_st.st_mode))
+ else
goto out;
}
if (dst_slash)
goto out;
}
strip_trailing_slashes (dst_temp);
- char readlink_buf[1];
- if (readlinkat (fd2, dst_temp, readlink_buf, sizeof readlink_buf) < 0)
+ char linkbuf[1];
+ if (readlinkat (fd2, dst_temp, linkbuf, sizeof linkbuf) < 0)
{
if (errno != ENOENT && errno != EINVAL)
{
symlink instead of the directory. Technically, we could use
realpath to find the canonical directory name to attempt
deletion on. But that is a lot of work for a corner case; so
- we instead just use an lstat on the shortened name, and
+ we instead just use a readlink on the shortened name, and
reject symlinks with trailing slashes. The root user of
unlink(1) will just have to live with the rule that they
can't delete a directory via a symlink. */
memcpy (short_name, name, len);
while (len && ISSLASH (short_name[len - 1]))
short_name[--len] = '\0';
- if (len && (lstat (short_name, &st) || S_ISLNK (st.st_mode)))
+ char linkbuf[1];
+ if (len && ! (readlink (short_name, linkbuf, 1) < 0
+ && errno == EINVAL))
{
free (short_name);
errno = EPERM;
memcpy (short_name, name, len);
while (len && ISSLASH (short_name[len - 1]))
short_name[--len] = '\0';
- if (len && (fstatat (fd, short_name, &st, AT_SYMLINK_NOFOLLOW)
- || S_ISLNK (st.st_mode)))
+ if (len && (readlinkat (fd, short_name, linkbuf, 1) < 0
+ || errno == EINVAL))
{
free (short_name);
errno = EPERM;
# endif /* HAVE_LUTIMES && !HAVE_UTIMENSAT */
/* Out of luck for symlinks, but we still handle regular files. */
- if (!(adjustment_needed || REPLACE_FUNC_STAT_FILE) && lstat (file, &st))
- return -1;
- if (!S_ISLNK (st.st_mode))
+ bool not_symlink;
+ if (adjustment_needed || REPLACE_FUNC_STAT_FILE)
+ not_symlink = !S_ISLNK (st.st_mode);
+ else
+ {
+ char linkbuf[1];
+ not_symlink = readlink (file, linkbuf, 1) < 0;
+ if (not_symlink && errno != EINVAL)
+ return -1;
+ }
+ if (not_symlink)
return fdutimens (-1, file, ts);
errno = ENOSYS;
return -1;
Depends-on:
unistd-h
-readlink [test $HAVE_LCHOWN = 0]
+readlink [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
chown [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
errno-h [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
bool [test $HAVE_LCHOWN = 0 || test $REPLACE_LCHOWN = 1]
dirname-lgpl [test $REPLACE_RENAME = 1]
free-posix [test $REPLACE_RENAME = 1]
lstat [test $REPLACE_RENAME = 1]
+readlink [test $REPLACE_RENAME = 1]
rmdir [test $REPLACE_RENAME = 1]
same-inode [test $REPLACE_RENAME = 1]
stat [test $REPLACE_RENAME = 1]
filename [test $REPLACE_UNLINK = 1]
lstat [test $REPLACE_UNLINK = 1]
malloc-posix [test $REPLACE_UNLINK = 1]
+readlink [test $REPLACE_UNLINK = 1]
configure.ac:
gl_FUNC_UNLINK
rmdir [test $HAVE_UNLINKAT = 0]
save-cwd [test $HAVE_UNLINKAT = 0]
unlink [test $HAVE_UNLINKAT = 0]
+fstatat [test $REPLACE_UNLINKAT = 1]
+readlinkat [test $REPLACE_UNLINKAT = 1]
configure.ac:
gl_FUNC_UNLINKAT
lstat
gettime
msvc-nothrow
+readlink
stat
stat-time
bool