GNU bug report logs - #39683
[PATCH] Add NOFOLLOW flag to set-file-modes etc.

Previous Next

Package: emacs;

Reported by: Paul Eggert <eggert <at> cs.ucla.edu>

Date: Thu, 20 Feb 2020 00:35:01 UTC

Severity: normal

Tags: patch

Done: Paul Eggert <eggert <at> cs.ucla.edu>

Bug is archived. No further changes may be made.

To add a comment to this bug, you must first unarchive it, by sending
a message to control AT debbugs.gnu.org, with unarchive 39683 in the body.
You can then email your comments to 39683 AT debbugs.gnu.org in the normal way.

Toggle the display of automated, internal messages from the tracker.

View this report as an mbox folder, status mbox, maintainer mbox


Report forwarded to bug-gnu-emacs <at> gnu.org:
bug#39683; Package emacs. (Thu, 20 Feb 2020 00:35:02 GMT) Full text and rfc822 format available.

Acknowledgement sent to Paul Eggert <eggert <at> cs.ucla.edu>:
New bug report received and forwarded. Copy sent to bug-gnu-emacs <at> gnu.org. (Thu, 20 Feb 2020 00:35:02 GMT) Full text and rfc822 format available.

Message #5 received at submit <at> debbugs.gnu.org (full text, mbox):

From: Paul Eggert <eggert <at> cs.ucla.edu>
To: bug-gnu-emacs <at> gnu.org
Cc: Paul Eggert <eggert <at> cs.ucla.edu>
Subject: [PATCH] Add NOFOLLOW flag to set-file-modes etc.
Date: Wed, 19 Feb 2020 16:34:00 -0800
This avoids some race conditions.  For example, if some other
program changes a file to a symlink between the time Emacs creates
the file and the time it changes the file’s permissions, using the
new flag prevents Emacs from inadvertently changing the
permissions of a victim in some completely unrelated directory.
* admin/merge-gnulib (GNULIB_MODULES): Add fchmodat.
* lib/chmodat.c, lib/fchmodat.c, lib/lchmod.c, m4/fchmodat.m4:
* m4/lchmod.m4: New files, copied from Gnulib.
* doc/lispref/files.texi (Testing Accessibility, Changing Files):
* doc/lispref/os.texi (File Notifications):
* etc/NEWS:
Adjust documentation accordingly.
* lib/gnulib.mk.in: Regenerate.
* lisp/dired-aux.el (dired-do-chmod):
* lisp/doc-view.el (doc-view-make-safe-dir):
* lisp/emacs-lisp/autoload.el (autoload--save-buffer):
* lisp/emacs-lisp/bytecomp.el (byte-compile-file):
* lisp/eshell/em-pred.el (eshell-pred-file-mode):
* lisp/files.el (backup-buffer-copy, copy-directory):
* lisp/gnus/mail-source.el (mail-source-movemail):
* lisp/gnus/mm-decode.el (mm-display-external):
* lisp/gnus/nnmail.el (nnmail-write-region):
* lisp/net/tramp-adb.el (tramp-adb-handle-file-local-copy)
(tramp-adb-handle-write-region):
* lisp/net/tramp-sh.el (tramp-do-copy-or-rename-file-directly):
* lisp/net/tramp-sudoedit.el (tramp-sudoedit-handle-write-region):
* lisp/net/tramp.el (tramp-handle-write-region)
(tramp-make-tramp-temp-file):
* lisp/server.el (server-ensure-safe-dir):
* lisp/url/url-util.el (url-make-private-file):
When getting or setting file modes, avoid following symbolic links
when the file is not supposed to be a symbolic link.
* lisp/doc-view.el (doc-view-make-safe-dir):
Omit no-longer-needed separate symlink test.
* lisp/gnus/gnus-util.el (gnus-set-file-modes):
* lisp/net/tramp-gvfs.el (tramp-gvfs-handle-set-file-modes):
* src/fileio.c (Ffile_modes, Fset_file_modes):
Support new optional arg NOFOLLOW.
* lisp/net/ange-ftp.el (ange-ftp-set-file-modes):
* lisp/net/tramp-adb.el (tramp-adb-handle-set-file-modes):
* lisp/net/tramp-sh.el (tramp-sh-handle-set-file-modes):
* lisp/net/tramp-smb.el (tramp-smb-handle-set-file-modes):
* lisp/net/tramp-sudoedit.el (tramp-sudoedit-handle-set-file-modes):
* lisp/net/tramp.el (tramp-handle-file-modes):
Accept an optional NOFOLLOW arg that is currently ignored,
and add a FIXME comment for it.
* m4/gnulib-comp.m4: Regenerate.
---
 admin/merge-gnulib          |   2 +-
 doc/lispref/files.texi      |  26 +++++--
 doc/lispref/os.texi         |   2 +-
 etc/NEWS                    |   3 +
 lib/chmodat.c               |   3 +
 lib/fchmodat.c              | 145 ++++++++++++++++++++++++++++++++++++
 lib/gnulib.mk.in            |  27 +++++++
 lib/lchmod.c                | 138 ++++++++++++++++++++++++++++++++++
 lisp/dired-aux.el           |   3 +-
 lisp/doc-view.el            |   4 +-
 lisp/emacs-lisp/autoload.el |   2 +-
 lisp/emacs-lisp/bytecomp.el |   2 +-
 lisp/eshell/em-pred.el      |   2 +-
 lisp/files.el               |  12 ++-
 lisp/gnus/gnus-util.el      |   4 +-
 lisp/gnus/mail-source.el    |   2 +-
 lisp/gnus/mm-decode.el      |   2 +-
 lisp/gnus/nnmail.el         |   2 +-
 lisp/net/ange-ftp.el        |   3 +-
 lisp/net/tramp-adb.el       |   8 +-
 lisp/net/tramp-gvfs.el      |   4 +-
 lisp/net/tramp-sh.el        |   9 ++-
 lisp/net/tramp-smb.el       |   3 +-
 lisp/net/tramp-sudoedit.el  |   5 +-
 lisp/net/tramp.el           |   7 +-
 lisp/server.el              |   2 +-
 lisp/url/url-util.el        |   2 +-
 m4/fchmodat.m4              |  82 ++++++++++++++++++++
 m4/gnulib-comp.m4           |  37 +++++++++
 m4/lchmod.m4                |  84 +++++++++++++++++++++
 src/fileio.c                |  39 +++++-----
 31 files changed, 606 insertions(+), 60 deletions(-)
 create mode 100644 lib/chmodat.c
 create mode 100644 lib/fchmodat.c
 create mode 100644 lib/lchmod.c
 create mode 100644 m4/fchmodat.m4
 create mode 100644 m4/lchmod.m4

diff --git a/admin/merge-gnulib b/admin/merge-gnulib
index 48c81e61e2..557119441e 100755
--- a/admin/merge-gnulib
+++ b/admin/merge-gnulib
@@ -33,7 +33,7 @@ GNULIB_MODULES=
   crypto/md5-buffer crypto/sha1-buffer crypto/sha256-buffer crypto/sha512-buffer
   d-type diffseq dosname double-slash-root dtoastr dtotimespec dup2
   environ execinfo explicit_bzero faccessat
-  fcntl fcntl-h fdopendir
+  fchmodat fcntl fcntl-h fdopendir
   filemode filevercmp flexmember fpieee fstatat fsusage fsync
   getloadavg getopt-gnu gettime gettimeofday gitlog-to-changelog
   ieee754-h ignore-value intprops largefile lstat
diff --git a/doc/lispref/files.texi b/doc/lispref/files.texi
index a93da39f17..b3fae0b2a5 100644
--- a/doc/lispref/files.texi
+++ b/doc/lispref/files.texi
@@ -928,7 +928,7 @@ Testing Accessibility
 This function does not follow symbolic links.
 @end defun
 
-@defun file-modes filename
+@defun file-modes filename nofollow
 @cindex mode bits
 @cindex file permissions
 @cindex permissions, file
@@ -946,12 +946,18 @@ Testing Accessibility
 has read, write, and execute permission, the @acronym{SUID} bit is set
 for both others and group, and the sticky bit is set.
 
+By default this function follows symbolic links.  However, if the
+optional argument @var{nofollow} is @code{t}, this function does not
+follow @var{filename} if it is a symbolic link; this can help prevent
+inadvertently obtaining the mode bits of a file somewhere else, and is
+more consistent with @code{file-attributes} (@pxref{File Attributes}).
+
 @xref{Changing Files}, for the @code{set-file-modes} function, which
 can be used to set these permissions.
 
 @example
 @group
-(file-modes "~/junk/diffs")
+(file-modes "~/junk/diffs" t)
      @result{} 492               ; @r{Decimal integer.}
 @end group
 @group
@@ -960,7 +966,7 @@ Testing Accessibility
 @end group
 
 @group
-(set-file-modes "~/junk/diffs" #o666)
+(set-file-modes "~/junk/diffs" #o666 t)
      @result{} nil
 @end group
 
@@ -1801,9 +1807,17 @@ Changing Files
 @cindex file permissions, setting
 @cindex permissions, file
 @cindex file modes, setting
-@deffn Command set-file-modes filename mode
+@deffn Command set-file-modes filename mode nofollow
 This function sets the @dfn{file mode} (or @dfn{permissions}) of
-@var{filename} to @var{mode}.  This function follows symbolic links.
+@var{filename} to @var{mode}.
+
+By default this function follows symbolic links.  However, if the
+optional argument @var{nofollow} is @code{t}, this function does not
+follow @var{filename} if it is a symbolic link; this can help prevent
+inadvertently changing the mode bits of a file somewhere else.  On
+platforms that do not support changing mode bits on a symbolic link,
+this function signals an error when @var{filename} is a symbolic link
+and @var{nofollow} is @code{t}.
 
 If called non-interactively, @var{mode} must be an integer.  Only the
 lowest 12 bits of the integer are used; on most systems, only the
@@ -1811,7 +1825,7 @@ Changing Files
 octal numbers to enter @var{mode}.  For example,
 
 @example
-(set-file-modes #o644)
+(set-file-modes "myfile" #o644 t)
 @end example
 
 @noindent
diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi
index a034ccdcd5..991c50a63b 100644
--- a/doc/lispref/os.texi
+++ b/doc/lispref/os.texi
@@ -3127,7 +3127,7 @@ File Notifications
 @end group
 
 @group
-(set-file-modes "/tmp/foo" (default-file-modes))
+(set-file-modes "/tmp/foo" (default-file-modes) t)
      @result{} Event (35025468 attribute-changed "/tmp/foo")
 @end group
 @end example
diff --git a/etc/NEWS b/etc/NEWS
index 1a51a90636..6e55bb6e93 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -190,6 +190,9 @@ called when the function object is garbage-collected.  Use
 'set_function_finalizer' to set the finalizer and
 'get_function_finalizer' to retrieve it.
 
+** 'file-modes' and 'set-file-modes' now have an optional argument
+specifying whether to follow symbolic links.
+
 ** 'parse-time-string' can now parse ISO 8601 format strings,
 such as "2020-01-15T16:12:21-08:00".
 
diff --git a/lib/chmodat.c b/lib/chmodat.c
new file mode 100644
index 0000000000..3c69689928
--- /dev/null
+++ b/lib/chmodat.c
@@ -0,0 +1,3 @@
+#include <config.h>
+#define FCHMODAT_INLINE _GL_EXTERN_INLINE
+#include "openat.h"
diff --git a/lib/fchmodat.c b/lib/fchmodat.c
new file mode 100644
index 0000000000..bb48b44f53
--- /dev/null
+++ b/lib/fchmodat.c
@@ -0,0 +1,145 @@
+/* Change the protections of file relative to an open directory.
+   Copyright (C) 2006, 2009-2020 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 <https://www.gnu.org/licenses/>.  */
+
+/* written by Jim Meyering and Paul Eggert */
+
+/* If the user's config.h happens to include <sys/stat.h>, let it include only
+   the system's <sys/stat.h> here, so that orig_fchmodat doesn't recurse to
+   rpl_fchmodat.  */
+#define __need_system_sys_stat_h
+#include <config.h>
+
+/* Specification.  */
+#include <sys/stat.h>
+#undef __need_system_sys_stat_h
+
+#if HAVE_FCHMODAT
+static int
+orig_fchmodat (int dir, char const *file, mode_t mode, int flags)
+{
+  return fchmodat (dir, file, mode, flags);
+}
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#ifdef __osf__
+/* Write "sys/stat.h" here, not <sys/stat.h>, otherwise OSF/1 5.1 DTK cc
+   eliminates this include because of the preliminary #include <sys/stat.h>
+   above.  */
+# include "sys/stat.h"
+#else
+# include <sys/stat.h>
+#endif
+
+#include <intprops.h>
+
+/* Invoke chmod or lchmod on FILE, using mode MODE, in the directory
+   open on descriptor FD.  If possible, do it without changing the
+   working directory.  Otherwise, resort to using save_cwd/fchdir,
+   then (chmod|lchmod)/restore_cwd.  If either the save_cwd or the
+   restore_cwd fails, then give a diagnostic and exit nonzero.
+   Note that an attempt to use a FLAG value of AT_SYMLINK_NOFOLLOW
+   on a system without lchmod support causes this function to fail.  */
+
+#if HAVE_FCHMODAT
+int
+fchmodat (int dir, char const *file, mode_t mode, int flags)
+{
+# if NEED_FCHMODAT_NONSYMLINK_FIX
+  if (flags == AT_SYMLINK_NOFOLLOW)
+    {
+      struct stat st;
+
+#  if defined O_PATH && defined AT_EMPTY_PATH \
+      && (defined __linux__ || defined __ANDROID__)
+      /* Open a file descriptor with O_NOFOLLOW, to make sure we don't
+         follow symbolic links, if /proc is mounted.  O_PATH is used to
+         avoid a failure if the file is not readable.
+         Cf. <https://sourceware.org/bugzilla/show_bug.cgi?id=14578>  */
+      int fd = openat (dir, file, O_PATH | O_NOFOLLOW | O_CLOEXEC);
+      if (fd < 0)
+        return fd;
+
+      /* Up to Linux 5.3 at least, when FILE refers to a symbolic link, the
+         chmod call below will change the permissions of the symbolic link
+         - which is undersired - and on many file systems (ext4, btrfs, jfs,
+         xfs, ..., but not reiserfs) fail with error EOPNOTSUPP - which is
+         misleading.  Therefore test for a symbolic link explicitly.
+         Use fstatat because fstat does not work on O_PATH descriptors
+         before Linux 3.6.  */
+      if (fstatat (fd, "", &st, AT_EMPTY_PATH) != 0)
+        {
+          int stat_errno = errno;
+          close (fd);
+          errno = stat_errno;
+          return -1;
+        }
+      if (S_ISLNK (st.st_mode))
+        {
+          close (fd);
+          errno = EOPNOTSUPP;
+          return -1;
+        }
+
+      static char const fmt[] = "/proc/self/fd/%d";
+      char buf[sizeof fmt - sizeof "%d" + INT_BUFSIZE_BOUND (int)];
+      sprintf (buf, fmt, fd);
+      int chmod_result = chmod (buf, mode);
+      int chmod_errno = errno;
+      close (fd);
+      if (chmod_result == 0)
+        return chmod_result;
+      if (chmod_errno != ENOENT)
+        {
+          errno = chmod_errno;
+          return chmod_result;
+        }
+      /* /proc is not mounted.  */
+      /* Fall back on orig_fchmodat, despite the race.  */
+      return orig_fchmodat (dir, file, mode, 0);
+#  elif (defined __linux__ || defined __ANDROID__) || !HAVE_LCHMOD
+      int fstatat_result = fstatat (dir, file, &st, AT_SYMLINK_NOFOLLOW);
+      if (fstatat_result != 0)
+        return fstatat_result;
+      if (S_ISLNK (st.st_mode))
+        {
+          errno = EOPNOTSUPP;
+          return -1;
+        }
+      /* Fall back on orig_fchmodat, despite the race.  */
+      return orig_fchmodat (dir, file, mode, 0);
+#  else
+      return orig_fchmodat (dir, file, mode, 0);
+#  endif
+    }
+# endif
+
+  return orig_fchmodat (dir, file, mode, flags);
+}
+#else
+# define AT_FUNC_NAME fchmodat
+# define AT_FUNC_F1 lchmod
+# define AT_FUNC_F2 chmod
+# define AT_FUNC_USE_F1_COND AT_SYMLINK_NOFOLLOW
+# define AT_FUNC_POST_FILE_PARAM_DECLS , mode_t mode, int flag
+# define AT_FUNC_POST_FILE_ARGS        , mode
+# include "at-func.c"
+#endif
diff --git a/lib/gnulib.mk.in b/lib/gnulib.mk.in
index 6775db0001..d6ebf42fc6 100644
--- a/lib/gnulib.mk.in
+++ b/lib/gnulib.mk.in
@@ -95,6 +95,7 @@
 #  execinfo \
 #  explicit_bzero \
 #  faccessat \
+#  fchmodat \
 #  fcntl \
 #  fcntl-h \
 #  fdopendir \
@@ -1083,6 +1084,7 @@ gl_GNULIB_ENABLED_dirfd = @gl_GNULIB_ENABLED_dirfd@
 gl_GNULIB_ENABLED_euidaccess = @gl_GNULIB_ENABLED_euidaccess@
 gl_GNULIB_ENABLED_getdtablesize = @gl_GNULIB_ENABLED_getdtablesize@
 gl_GNULIB_ENABLED_getgroups = @gl_GNULIB_ENABLED_getgroups@
+gl_GNULIB_ENABLED_lchmod = @gl_GNULIB_ENABLED_lchmod@
 gl_GNULIB_ENABLED_malloca = @gl_GNULIB_ENABLED_malloca@
 gl_GNULIB_ENABLED_open = @gl_GNULIB_ENABLED_open@
 gl_GNULIB_ENABLED_strtoll = @gl_GNULIB_ENABLED_strtoll@
@@ -1587,6 +1589,18 @@ EXTRA_libgnu_a_SOURCES += at-func.c faccessat.c
 endif
 ## end   gnulib module faccessat
 
+## begin gnulib module fchmodat
+ifeq (,$(OMIT_GNULIB_MODULE_fchmodat))
+
+libgnu_a_SOURCES += chmodat.c
+
+EXTRA_DIST += at-func.c fchmodat.c
+
+EXTRA_libgnu_a_SOURCES += at-func.c fchmodat.c
+
+endif
+## end   gnulib module fchmodat
+
 ## begin gnulib module fcntl
 ifeq (,$(OMIT_GNULIB_MODULE_fcntl))
 
@@ -1937,6 +1951,19 @@ EXTRA_DIST += inttypes.in.h
 endif
 ## end   gnulib module inttypes-incomplete
 
+## begin gnulib module lchmod
+ifeq (,$(OMIT_GNULIB_MODULE_lchmod))
+
+ifneq (,$(gl_GNULIB_ENABLED_lchmod))
+
+endif
+EXTRA_DIST += lchmod.c
+
+EXTRA_libgnu_a_SOURCES += lchmod.c
+
+endif
+## end   gnulib module lchmod
+
 ## begin gnulib module libc-config
 ifeq (,$(OMIT_GNULIB_MODULE_libc-config))
 
diff --git a/lib/lchmod.c b/lib/lchmod.c
new file mode 100644
index 0000000000..57f75da8cf
--- /dev/null
+++ b/lib/lchmod.c
@@ -0,0 +1,138 @@
+/* Implement lchmod on platforms where it does not work correctly.
+
+   Copyright 2020 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 <https://www.gnu.org/licenses/>.  */
+
+/* written by Paul Eggert */
+
+#include <config.h>
+
+/* If the user's config.h happens to include <sys/stat.h>, let it include only
+   the system's <sys/stat.h> here, so that orig_fchmodat doesn't recurse to
+   rpl_fchmodat.  */
+#define __need_system_sys_stat_h
+#include <config.h>
+
+/* Specification.  */
+#include <sys/stat.h>
+#undef __need_system_sys_stat_h
+
+#if HAVE_LCHMOD
+static inline int
+orig_lchmod (char const *file, mode_t mode)
+{
+  return lchmod (file, mode);
+}
+#endif
+
+#include <errno.h>
+#include <fcntl.h>
+#include <stdio.h>
+#include <unistd.h>
+
+#ifdef __osf__
+/* Write "sys/stat.h" here, not <sys/stat.h>, otherwise OSF/1 5.1 DTK cc
+   eliminates this include because of the preliminary #include <sys/stat.h>
+   above.  */
+# include "sys/stat.h"
+#else
+# include <sys/stat.h>
+#endif
+
+#include <intprops.h>
+
+/* Work like chmod, except when FILE is a symbolic link.
+   In that case, on systems where permissions on symbolic links are unsupported
+   (such as Linux), set errno to EOPNOTSUPP and return -1.  */
+
+int
+lchmod (char const *file, mode_t mode)
+{
+#if HAVE_FCHMODAT
+  /* Gnulib's fchmodat contains the workaround.  No need to duplicate it
+     here.  */
+  return fchmodat (AT_FDCWD, file, mode, AT_SYMLINK_NOFOLLOW);
+#elif NEED_LCHMOD_NONSYMLINK_FIX
+# if defined AT_FDCWD && defined O_PATH && defined AT_EMPTY_PATH \
+     && (defined __linux__ || defined __ANDROID__)
+  /* Open a file descriptor with O_NOFOLLOW, to make sure we don't
+     follow symbolic links, if /proc is mounted.  O_PATH is used to
+     avoid a failure if the file is not readable.
+     Cf. <https://sourceware.org/bugzilla/show_bug.cgi?id=14578>  */
+  int fd = openat (AT_FDCWD, file, O_PATH | O_NOFOLLOW | O_CLOEXEC);
+  if (fd < 0)
+    return fd;
+
+  /* Up to Linux 5.3 at least, when FILE refers to a symbolic link, the
+     chmod call below will change the permissions of the symbolic link
+     - which is undersired - and on many file systems (ext4, btrfs, jfs,
+     xfs, ..., but not reiserfs) fail with error EOPNOTSUPP - which is
+     misleading.  Therefore test for a symbolic link explicitly.
+     Use fstatat because fstat does not work on O_PATH descriptors
+     before Linux 3.6.  */
+  struct stat st;
+  if (fstatat (fd, "", &st, AT_EMPTY_PATH) != 0)
+    {
+      int stat_errno = errno;
+      close (fd);
+      errno = stat_errno;
+      return -1;
+    }
+  if (S_ISLNK (st.st_mode))
+    {
+      close (fd);
+      errno = EOPNOTSUPP;
+      return -1;
+    }
+
+  static char const fmt[] = "/proc/self/fd/%d";
+  char buf[sizeof fmt - sizeof "%d" + INT_BUFSIZE_BOUND (int)];
+  sprintf (buf, fmt, fd);
+  int chmod_result = chmod (buf, mode);
+  int chmod_errno = errno;
+  close (fd);
+  if (chmod_result == 0)
+    return chmod_result;
+  if (chmod_errno != ENOENT)
+    {
+      errno = chmod_errno;
+      return chmod_result;
+    }
+  /* /proc is not mounted.  */
+  /* Fall back on chmod, despite the race.  */
+  return chmod (file, mode);
+# elif HAVE_LSTAT
+#  if (defined __linux__ || defined __ANDROID__) || !HAVE_LCHMOD
+  struct stat st;
+  int lstat_result = lstat (file, &st);
+  if (lstat_result != 0)
+    return lstat_result;
+  if (S_ISLNK (st.st_mode))
+    {
+      errno = EOPNOTSUPP;
+      return -1;
+    }
+  /* Fall back on chmod, despite the race.  */
+  return chmod (file, mode);
+#  else              /* GNU/kFreeBSD, GNU/Hurd, macOS, FreeBSD, NetBSD, HP-UX */
+  return orig_lchmod (file, mode);
+#  endif
+# else                                                      /* native Windows */
+  return chmod (file, mode);
+# endif
+#else
+  return orig_lchmod (file, mode);
+#endif
+}
diff --git a/lisp/dired-aux.el b/lisp/dired-aux.el
index 0069c1744d..de5e8bb567 100644
--- a/lisp/dired-aux.el
+++ b/lisp/dired-aux.el
@@ -409,7 +409,8 @@ dired-do-chmod
       (set-file-modes
        file
        (if num-modes num-modes
-	 (file-modes-symbolic-to-number modes (file-modes file)))))
+	 (file-modes-symbolic-to-number modes (file-modes file t)))
+       t))
     (dired-do-redisplay arg)))
 
 ;;;###autoload
diff --git a/lisp/doc-view.el b/lisp/doc-view.el
index 3788d79725..8ab112e092 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -683,8 +683,6 @@ doc-view-make-safe-dir
       ;; time-window of loose permissions otherwise.
       (with-file-modes #o0700 (make-directory dir))
     (file-already-exists
-     (when (file-symlink-p dir)
-       (error "Danger: %s points to a symbolic link" dir))
      ;; In case it was created earlier with looser rights.
      ;; We could check the mode info returned by file-attributes, but it's
      ;; a pain to parse and it may not tell you what we want under
@@ -694,7 +692,7 @@ doc-view-make-safe-dir
      ;; sure we have write-access to the directory and that we own it, thus
      ;; closing a bunch of security holes.
      (condition-case error
-	 (set-file-modes dir #o0700)
+	 (set-file-modes dir #o0700 t)
        (file-error
 	(error
 	 (format "Unable to use temporary directory %s: %s"
diff --git a/lisp/emacs-lisp/autoload.el b/lisp/emacs-lisp/autoload.el
index 785e350e0e..adfe8de274 100644
--- a/lisp/emacs-lisp/autoload.el
+++ b/lisp/emacs-lisp/autoload.el
@@ -895,7 +895,7 @@ autoload--save-buffer
           (cons (lambda () (ignore-errors (delete-file tempfile)))
                 kill-emacs-hook)))
     (unless (= temp-modes desired-modes)
-      (set-file-modes tempfile desired-modes))
+      (set-file-modes tempfile desired-modes t))
     (write-region (point-min) (point-max) tempfile nil 1)
     (backup-buffer)
     (rename-file tempfile buffer-file-name t))
diff --git a/lisp/emacs-lisp/bytecomp.el b/lisp/emacs-lisp/bytecomp.el
index fce5e4aed6..9b792f04fc 100644
--- a/lisp/emacs-lisp/bytecomp.el
+++ b/lisp/emacs-lisp/bytecomp.el
@@ -2008,7 +2008,7 @@ byte-compile-file
 					   (delete-file tempfile)))
 			      kill-emacs-hook)))
 		  (unless (= temp-modes desired-modes)
-		    (set-file-modes tempfile desired-modes))
+		    (set-file-modes tempfile desired-modes t))
 		  (write-region (point-min) (point-max) tempfile nil 1)
 		  ;; This has the intentional side effect that any
 		  ;; hard-links to target-file continue to
diff --git a/lisp/eshell/em-pred.el b/lisp/eshell/em-pred.el
index 04bf3ff899..a7c670933a 100644
--- a/lisp/eshell/em-pred.el
+++ b/lisp/eshell/em-pred.el
@@ -478,7 +478,7 @@ eshell-pred-file-type
 (defsubst eshell-pred-file-mode (mode)
   "Return a test which tests that MODE pertains to the file."
   `(lambda (file)
-     (let ((modes (file-modes file)))
+     (let ((modes (file-modes file t)))
        (if modes
 	   (logand ,mode modes)))))
 
diff --git a/lisp/files.el b/lisp/files.el
index 683f4a8ce7..300049c52e 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -4672,6 +4672,7 @@ backup-buffer-copy
   ;; Create temp files with strict access rights.  It's easy to
   ;; loosen them later, whereas it's impossible to close the
   ;; time-window of loose permissions otherwise.
+ (let (nofollow)
   (with-file-modes ?\700
     (when (condition-case nil
 	      ;; Try to overwrite old backup first.
@@ -4682,6 +4683,7 @@ backup-buffer-copy
 		   (when (file-exists-p to-name)
 		     (delete-file to-name))
 		   (copy-file from-name to-name nil t t)
+		   (setq nofollow t)
 		   nil)
 	       (file-already-exists t))
 	;; The file was somehow created by someone else between
@@ -4694,7 +4696,7 @@ backup-buffer-copy
 	       (with-demoted-errors
 		 (set-file-extended-attributes to-name extended-attributes)))
     (and modes
-	 (set-file-modes to-name (logand modes #o1777)))))
+	 (set-file-modes to-name (logand modes #o1777) nofollow)))))
 
 (defvar file-name-version-regexp
   "\\(?:~\\|\\.~[-[:alnum:]:#@^._]+\\(?:~[[:digit:]]+\\)?~\\)"
@@ -5900,7 +5902,8 @@ copy-directory
   ;; If default-directory is a remote directory, make sure we find its
   ;; copy-directory handler.
   (let ((handler (or (find-file-name-handler directory 'copy-directory)
-		     (find-file-name-handler newname 'copy-directory))))
+		     (find-file-name-handler newname 'copy-directory)))
+	(follow parents))
     (if handler
 	(funcall handler 'copy-directory directory
                  newname keep-time parents copy-contents)
@@ -5920,7 +5923,8 @@ copy-directory
 		 (or parents (not (file-directory-p newname)))
 	       (setq newname (concat newname
 				     (file-name-nondirectory directory))))
-	     (make-directory (directory-file-name newname) parents)))
+	     (make-directory (directory-file-name newname) parents))
+	    (t (setq follow t)))
 
       ;; Copy recursively.
       (dolist (file
@@ -5941,7 +5945,7 @@ copy-directory
       (let ((modes (file-modes directory))
 	    (times (and keep-time (file-attribute-modification-time
 				   (file-attributes directory)))))
-	(if modes (set-file-modes newname modes))
+	(if modes (set-file-modes newname modes (not follow)))
 	(if times (set-file-times newname times))))))
 
 
diff --git a/lisp/gnus/gnus-util.el b/lisp/gnus/gnus-util.el
index eb0fd2522d..e73d0bc110 100644
--- a/lisp/gnus/gnus-util.el
+++ b/lisp/gnus/gnus-util.el
@@ -1601,10 +1601,10 @@ gnus-rename-file
 			 (file-truename
 			  (concat old-dir "..")))))))))
 
-(defun gnus-set-file-modes (filename mode)
+(defun gnus-set-file-modes (filename mode &optional nofollow)
   "Wrapper for set-file-modes."
   (ignore-errors
-    (set-file-modes filename mode)))
+    (set-file-modes filename mode nofollow)))
 
 (defun gnus-rescale-image (image size)
   "Rescale IMAGE to SIZE if possible.
diff --git a/lisp/gnus/mail-source.el b/lisp/gnus/mail-source.el
index f5b68789b8..1862d38272 100644
--- a/lisp/gnus/mail-source.el
+++ b/lisp/gnus/mail-source.el
@@ -695,7 +695,7 @@ mail-source-movemail
 			 mail-source-movemail-program
 			 nil errors nil from to)))))
 	      (when (file-exists-p to)
-		(set-file-modes to mail-source-default-file-modes))
+		(set-file-modes to mail-source-default-file-modes t))
 	      (if (and (or (not (buffer-modified-p errors))
 			   (zerop (buffer-size errors)))
 		       (and (numberp result)
diff --git a/lisp/gnus/mm-decode.el b/lisp/gnus/mm-decode.el
index 2dab278b37..d3477d869e 100644
--- a/lisp/gnus/mm-decode.el
+++ b/lisp/gnus/mm-decode.el
@@ -948,7 +948,7 @@ mm-display-external
 	  ;; The file is deleted after the viewer exists.  If the users edits
 	  ;; the file, changes will be lost.  Set file to read-only to make it
 	  ;; clear.
-	  (set-file-modes file #o400)
+	  (set-file-modes file #o400 t)
 	  (message "Viewing with %s" method)
 	  (cond
 	   (needsterm
diff --git a/lisp/gnus/nnmail.el b/lisp/gnus/nnmail.el
index 6e01b5c4d0..f0591c6b5b 100644
--- a/lisp/gnus/nnmail.el
+++ b/lisp/gnus/nnmail.el
@@ -1958,7 +1958,7 @@ nnmail-write-region
   (let ((coding-system-for-write nnmail-file-coding-system)
 	(file-name-coding-system nnmail-pathname-coding-system))
     (write-region start end filename append visit lockname)
-    (set-file-modes filename nnmail-default-file-modes)))
+    (set-file-modes filename nnmail-default-file-modes t)))
 
 ;;;
 ;;; Status functions
diff --git a/lisp/net/ange-ftp.el b/lisp/net/ange-ftp.el
index f28394260d..3c720fdcdd 100644
--- a/lisp/net/ange-ftp.el
+++ b/lisp/net/ange-ftp.el
@@ -4740,7 +4740,8 @@ ange-ftp-call-chmod
   (setq ange-ftp-ls-cache-file nil)	;Stop confusing Dired.
   0)
 
-(defun ange-ftp-set-file-modes (filename mode)
+(defun ange-ftp-set-file-modes (filename mode &optional nofollow)
+  nofollow ;; FIXME: Support the NOFOLLOW argument.
   (ange-ftp-call-chmod (list (format "%o" mode) filename)))
 
 (defun ange-ftp-make-symbolic-link (&rest _arguments)
diff --git a/lisp/net/tramp-adb.el b/lisp/net/tramp-adb.el
index aa7fe147c2..ae81dfd7c8 100644
--- a/lisp/net/tramp-adb.el
+++ b/lisp/net/tramp-adb.el
@@ -591,7 +591,8 @@ tramp-adb-handle-file-local-copy
 	  (ignore-errors (delete-file tmpfile))
 	  (tramp-error
 	   v 'file-error "Cannot make local copy of file `%s'" filename))
-	(set-file-modes tmpfile (logior (or (file-modes filename) 0) #o0400)))
+	(set-file-modes tmpfile (logior (or (file-modes filename) 0) #o0400)
+			t))
       tmpfile)))
 
 (defun tramp-adb-handle-file-writable-p (filename)
@@ -636,7 +637,7 @@ tramp-adb-handle-write-region
 	   (tmpfile (tramp-compat-make-temp-file filename)))
       (when (and append (file-exists-p filename))
 	(copy-file filename tmpfile 'ok)
-	(set-file-modes tmpfile (logior (or (file-modes tmpfile) 0) #o0600)))
+	(set-file-modes tmpfile (logior (or (file-modes tmpfile) 0) #o0600) t))
       (tramp-run-real-handler
        #'write-region (list start end tmpfile append 'no-message lockname))
       (with-tramp-progress-reporter
@@ -665,8 +666,9 @@ tramp-adb-handle-write-region
 	(tramp-message v 0 "Wrote %s" filename))
       (run-hooks 'tramp-handle-write-region-hook))))
 
-(defun tramp-adb-handle-set-file-modes (filename mode)
+(defun tramp-adb-handle-set-file-modes (filename mode &optional nofollow)
   "Like `set-file-modes' for Tramp files."
+  nofollow ;; FIXME: Support the NOFOLLOW flag.
   (with-parsed-tramp-file-name filename nil
     (tramp-flush-file-properties v localname)
     (tramp-adb-send-command-and-check v (format "chmod %o %s" mode localname))))
diff --git a/lisp/net/tramp-gvfs.el b/lisp/net/tramp-gvfs.el
index 762c4fe4b3..3127901a63 100644
--- a/lisp/net/tramp-gvfs.el
+++ b/lisp/net/tramp-gvfs.el
@@ -1562,12 +1562,12 @@ tramp-gvfs-handle-rename-file
     (tramp-run-real-handler
      #'rename-file (list filename newname ok-if-already-exists))))
 
-(defun tramp-gvfs-handle-set-file-modes (filename mode)
+(defun tramp-gvfs-handle-set-file-modes (filename mode &optional nofollow)
   "Like `set-file-modes' for Tramp files."
   (with-parsed-tramp-file-name filename nil
     (tramp-flush-file-properties v localname)
     (tramp-gvfs-send-command
-     v "gvfs-set-attribute" "-t" "uint32"
+     v "gvfs-set-attribute" (if nofollow "-nt" "-t") "uint32"
      (tramp-gvfs-url-file-name (tramp-make-tramp-file-name v))
      "unix::mode" (number-to-string mode))))
 
diff --git a/lisp/net/tramp-sh.el b/lisp/net/tramp-sh.el
index 5a3abc31ea..a1dea4478e 100644
--- a/lisp/net/tramp-sh.el
+++ b/lisp/net/tramp-sh.el
@@ -1478,10 +1478,11 @@ tramp-sh-handle-verify-visited-file-modtime
 	     ;; only if that agrees with the buffer's record.
 	     (t (tramp-compat-time-equal-p mt tramp-time-doesnt-exist)))))))))
 
-(defun tramp-sh-handle-set-file-modes (filename mode)
+(defun tramp-sh-handle-set-file-modes (filename mode &optional nofollow)
   "Like `set-file-modes' for Tramp files."
   (with-parsed-tramp-file-name filename nil
     (tramp-flush-file-properties v localname)
+    nofollow ;; FIXME: Support the NOFOLLOW flag.
     ;; FIXME: extract the proper text from chmod's stderr.
     (tramp-barf-unless-okay
      v
@@ -2279,7 +2280,7 @@ tramp-do-copy-or-rename-file-directly
 		      ;; We must change the ownership as local user.
 		      ;; Since this does not work reliable, we also
 		      ;; give read permissions.
-		      (set-file-modes tmpfile #o0777)
+		      (set-file-modes tmpfile #o0777 t)
 		      (tramp-set-file-uid-gid
 		       tmpfile
 		       (tramp-get-remote-uid v 'integer)
@@ -3221,7 +3222,7 @@ tramp-sh-handle-file-local-copy
 		    (delete-file tmpfile2)))))
 
 	    ;; Set proper permissions.
-	    (set-file-modes tmpfile (tramp-default-file-modes filename))
+	    (set-file-modes tmpfile (tramp-default-file-modes filename) t)
 	    ;; Set local user ownership.
 	    (tramp-set-file-uid-gid tmpfile))
 
@@ -3320,7 +3321,7 @@ tramp-sh-handle-write-region
 	  ;; handles permissions.
 	  ;; Ensure that it is still readable.
 	  (when modes
-	    (set-file-modes tmpfile (logior (or modes 0) #o0400)))
+	    (set-file-modes tmpfile (logior (or modes 0) #o0400) t))
 
 	  ;; This is a bit lengthy due to the different methods
 	  ;; possible for file transfer.  First, we check whether the
diff --git a/lisp/net/tramp-smb.el b/lisp/net/tramp-smb.el
index f02be394a7..b4b56b13cb 100644
--- a/lisp/net/tramp-smb.el
+++ b/lisp/net/tramp-smb.el
@@ -1464,8 +1464,9 @@ tramp-smb-handle-set-file-acl
 	    (tramp-flush-connection-property v "process-name")
 	    (tramp-flush-connection-property v "process-buffer")))))))
 
-(defun tramp-smb-handle-set-file-modes (filename mode)
+(defun tramp-smb-handle-set-file-modes (filename mode &optional nofollow)
   "Like `set-file-modes' for Tramp files."
+  nofollow ;; FIXME: Support the NOFOLLOW flag.
   (with-parsed-tramp-file-name filename nil
     (when (tramp-smb-get-cifs-capabilities v)
       (tramp-flush-file-properties v localname)
diff --git a/lisp/net/tramp-sudoedit.el b/lisp/net/tramp-sudoedit.el
index f258ad6b93..796a4ac84a 100644
--- a/lisp/net/tramp-sudoedit.el
+++ b/lisp/net/tramp-sudoedit.el
@@ -463,8 +463,9 @@ tramp-sudoedit-handle-file-readable-p
       (tramp-sudoedit-send-command
        v "test" "-r" (tramp-compat-file-name-unquote localname)))))
 
-(defun tramp-sudoedit-handle-set-file-modes (filename mode)
+(defun tramp-sudoedit-handle-set-file-modes (filename mode &optional nofollow)
   "Like `set-file-modes' for Tramp files."
+  nofollow ;; FIXME: Support the NOFOLLOW flag.
   (with-parsed-tramp-file-name filename nil
     (tramp-flush-file-properties v localname)
     (unless (tramp-sudoedit-send-command
@@ -735,7 +736,7 @@ tramp-sudoedit-handle-write-region
 			 (file-attributes filename 'integer))
 			gid))
           (tramp-set-file-uid-gid filename uid gid))
-	(set-file-modes filename modes)))))
+	(set-file-modes filename modes (eq mustbenew 'excl))))))
 
 
 ;; Internal functions.
diff --git a/lisp/net/tramp.el b/lisp/net/tramp.el
index 409e1f7499..430811adcd 100644
--- a/lisp/net/tramp.el
+++ b/lisp/net/tramp.el
@@ -3179,8 +3179,9 @@ tramp-handle-file-local-copy
       (copy-file filename tmpfile 'ok-if-already-exists 'keep-time)
       tmpfile)))
 
-(defun tramp-handle-file-modes (filename)
+(defun tramp-handle-file-modes (filename &optional nofollow)
   "Like `file-modes' for Tramp files."
+  nofollow ;; FIXME: Support the NOFOLLOW flag.
   (when-let ((attrs (file-attributes (or (file-truename filename) filename))))
     (tramp-mode-string-to-int (tramp-compat-file-attribute-modes attrs))))
 
@@ -3884,7 +3885,7 @@ tramp-handle-write-region
       ;; renamed to the backup file.  This case `save-buffer'
       ;; handles permissions.
       ;; Ensure that it is still readable.
-      (set-file-modes tmpfile (logior (or modes 0) #o0400))
+      (set-file-modes tmpfile (logior (or modes 0) #o0400) t)
       ;; We say `no-message' here because we don't want the visited file
       ;; modtime data to be clobbered from the temp file.  We call
       ;; `set-visited-file-modtime' ourselves later on.
@@ -4664,7 +4665,7 @@ tramp-make-tramp-temp-file
 	  (setq result nil)
 	;; This creates the file by side effect.
 	(set-file-times result)
-	(set-file-modes result #o0700)))
+	(set-file-modes result #o0700 t)))
 
     ;; Return the local part.
     (tramp-file-local-name result)))
diff --git a/lisp/server.el b/lisp/server.el
index e6d8b1783c..1c26c122eb 100644
--- a/lisp/server.el
+++ b/lisp/server.el
@@ -563,7 +563,7 @@ server-ensure-safe-dir
                      (format "it is not owned by you (owner = %s (%d))"
                              (user-full-name uid) uid))
                     (w32 nil)           ; on NTFS?
-                    ((let ((modes (file-modes dir)))
+                    ((let ((modes (file-modes dir t)))
                        (unless (zerop (logand (or modes 0) #o077))
                          (format "it is accessible by others (%03o)" modes))))
                     (t nil))))
diff --git a/lisp/url/url-util.el b/lisp/url/url-util.el
index 645011a578..125fb0349d 100644
--- a/lisp/url/url-util.el
+++ b/lisp/url/url-util.el
@@ -617,7 +617,7 @@ url-make-private-file
     (file-already-exists
      (if (file-symlink-p file)
          (error "Danger: `%s' is a symbolic link" file))
-     (set-file-modes file #o0600))))
+     (set-file-modes file #o0600 t))))
 
 (autoload 'puny-encode-domain "puny")
 (autoload 'url-domsuf-cookie-allowed-p "url-domsuf")
diff --git a/m4/fchmodat.m4 b/m4/fchmodat.m4
new file mode 100644
index 0000000000..e3f2f04816
--- /dev/null
+++ b/m4/fchmodat.m4
@@ -0,0 +1,82 @@
+# fchmodat.m4 serial 4
+dnl Copyright (C) 2004-2020 Free Software 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.
+
+# Written by Jim Meyering.
+
+AC_DEFUN([gl_FUNC_FCHMODAT],
+[
+  AC_REQUIRE([gl_SYS_STAT_H_DEFAULTS])
+  AC_REQUIRE([gl_USE_SYSTEM_EXTENSIONS])
+  AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles
+  AC_CHECK_FUNCS_ONCE([fchmodat lchmod])
+  if test $ac_cv_func_fchmodat != yes; then
+    HAVE_FCHMODAT=0
+  else
+    AC_CACHE_CHECK(
+      [whether fchmodat+AT_SYMLINK_NOFOLLOW works on non-symlinks],
+      [gl_cv_func_fchmodat_works],
+      [dnl This test fails on GNU/Linux with glibc 2.31 (but not on
+       dnl GNU/kFreeBSD nor GNU/Hurd) and Cygwin 2.9.
+       AC_RUN_IFELSE(
+         [AC_LANG_PROGRAM(
+            [
+              AC_INCLUDES_DEFAULT[
+              #include <fcntl.h>
+              #ifndef S_IRUSR
+               #define S_IRUSR 0400
+              #endif
+              #ifndef S_IWUSR
+               #define S_IWUSR 0200
+              #endif
+              #ifndef S_IRWXU
+               #define S_IRWXU 0700
+              #endif
+              #ifndef S_IRWXG
+               #define S_IRWXG 0070
+              #endif
+              #ifndef S_IRWXO
+               #define S_IRWXO 0007
+              #endif
+            ]],
+            [[
+              int permissive = S_IRWXU | S_IRWXG | S_IRWXO;
+              int desired = S_IRUSR | S_IWUSR;
+              static char const f[] = "conftest.fchmodat";
+              struct stat st;
+              if (creat (f, permissive) < 0)
+                return 1;
+              if (fchmodat (AT_FDCWD, f, desired, AT_SYMLINK_NOFOLLOW) != 0)
+                return 1;
+              if (stat (f, &st) != 0)
+                return 1;
+              return ! ((st.st_mode & permissive) == desired);
+            ]])],
+         [gl_cv_func_fchmodat_works=yes],
+         [gl_cv_func_fchmodat_works=no],
+         [case "$host_os" in
+            dnl Guess no on Linux with glibc and Cygwin, yes otherwise.
+            linux-gnu* | cygwin*) gl_cv_func_fchmodat_works="guessing no" ;;
+            *)                    gl_cv_func_fchmodat_works="$gl_cross_guess_normal" ;;
+          esac
+         ])
+       rm -f conftest.fchmodat])
+    case $gl_cv_func_fchmodat_works in
+      *yes) ;;
+      *)
+        AC_DEFINE([NEED_FCHMODAT_NONSYMLINK_FIX], [1],
+          [Define to 1 if fchmodat+AT_SYMLINK_NOFOLLOW does not work right on non-symlinks.])
+        REPLACE_FCHMODAT=1
+        ;;
+    esac
+  fi
+])
+
+# Prerequisites of lib/fchmodat.c.
+AC_DEFUN([gl_PREREQ_FCHMODAT],
+[
+  AC_CHECK_FUNCS_ONCE([lchmod])
+  :
+])
diff --git a/m4/gnulib-comp.m4 b/m4/gnulib-comp.m4
index 48d8030f53..4fb5edb145 100644
--- a/m4/gnulib-comp.m4
+++ b/m4/gnulib-comp.m4
@@ -82,6 +82,7 @@ AC_DEFUN
   # Code from module extensions:
   # Code from module extern-inline:
   # Code from module faccessat:
+  # Code from module fchmodat:
   # Code from module fcntl:
   # Code from module fcntl-h:
   # Code from module fdopendir:
@@ -111,6 +112,7 @@ AC_DEFUN
   # Code from module inttypes-incomplete:
   # Code from module largefile:
   AC_REQUIRE([AC_SYS_LARGEFILE])
+  # Code from module lchmod:
   # Code from module libc-config:
   # Code from module limits-h:
   # Code from module localtime-buffer:
@@ -250,6 +252,13 @@ AC_DEFUN
   fi
   gl_MODULE_INDICATOR([faccessat])
   gl_UNISTD_MODULE_INDICATOR([faccessat])
+  gl_FUNC_FCHMODAT
+  if test $HAVE_FCHMODAT = 0 || test $REPLACE_FCHMODAT = 1; then
+    AC_LIBOBJ([fchmodat])
+    gl_PREREQ_FCHMODAT
+  fi
+  gl_MODULE_INDICATOR([fchmodat]) dnl for lib/openat.h
+  gl_SYS_STAT_MODULE_INDICATOR([fchmodat])
   gl_FUNC_FCNTL
   if test $HAVE_FCNTL = 0 || test $REPLACE_FCNTL = 1; then
     AC_LIBOBJ([fcntl])
@@ -463,6 +472,7 @@ AC_DEFUN
   gl_gnulib_enabled_getgroups=false
   gl_gnulib_enabled_be453cec5eecf5731a274f2de7f2db36=false
   gl_gnulib_enabled_a9786850e999ae65a836a6041e8e5ed1=false
+  gl_gnulib_enabled_lchmod=false
   gl_gnulib_enabled_21ee726a3540c09237a8e70c0baf7467=false
   gl_gnulib_enabled_2049e887c7e5308faad27b3f894bb8c9=false
   gl_gnulib_enabled_malloca=false
@@ -564,6 +574,18 @@ AC_DEFUN
       fi
     fi
   }
+  func_gl_gnulib_m4code_lchmod ()
+  {
+    if ! $gl_gnulib_enabled_lchmod; then
+      gl_FUNC_LCHMOD
+      if test $HAVE_LCHMOD = 0 || test $REPLACE_LCHMOD = 1; then
+        AC_LIBOBJ([lchmod])
+        gl_PREREQ_LCHMOD
+      fi
+      gl_SYS_STAT_MODULE_INDICATOR([lchmod])
+      gl_gnulib_enabled_lchmod=true
+    fi
+  }
   func_gl_gnulib_m4code_21ee726a3540c09237a8e70c0baf7467 ()
   {
     if ! $gl_gnulib_enabled_21ee726a3540c09237a8e70c0baf7467; then
@@ -655,6 +677,15 @@ AC_DEFUN
   if test $HAVE_FACCESSAT = 0 || test $REPLACE_FACCESSAT = 1; then
     func_gl_gnulib_m4code_03e0aaad4cb89ca757653bd367a6ccb7
   fi
+  if test $HAVE_FCHMODAT = 0; then
+    func_gl_gnulib_m4code_260941c0e5dc67ec9e87d1fb321c300b
+  fi
+  if test $HAVE_FCHMODAT = 0; then
+    func_gl_gnulib_m4code_lchmod
+  fi
+  if test $HAVE_FCHMODAT = 0; then
+    func_gl_gnulib_m4code_03e0aaad4cb89ca757653bd367a6ccb7
+  fi
   if test $HAVE_FCNTL = 0 || test $REPLACE_FCNTL = 1; then
     func_gl_gnulib_m4code_getdtablesize
   fi
@@ -703,6 +734,7 @@ AC_DEFUN
   AM_CONDITIONAL([gl_GNULIB_ENABLED_getgroups], [$gl_gnulib_enabled_getgroups])
   AM_CONDITIONAL([gl_GNULIB_ENABLED_be453cec5eecf5731a274f2de7f2db36], [$gl_gnulib_enabled_be453cec5eecf5731a274f2de7f2db36])
   AM_CONDITIONAL([gl_GNULIB_ENABLED_a9786850e999ae65a836a6041e8e5ed1], [$gl_gnulib_enabled_a9786850e999ae65a836a6041e8e5ed1])
+  AM_CONDITIONAL([gl_GNULIB_ENABLED_lchmod], [$gl_gnulib_enabled_lchmod])
   AM_CONDITIONAL([gl_GNULIB_ENABLED_21ee726a3540c09237a8e70c0baf7467], [$gl_gnulib_enabled_21ee726a3540c09237a8e70c0baf7467])
   AM_CONDITIONAL([gl_GNULIB_ENABLED_2049e887c7e5308faad27b3f894bb8c9], [$gl_gnulib_enabled_2049e887c7e5308faad27b3f894bb8c9])
   AM_CONDITIONAL([gl_GNULIB_ENABLED_malloca], [$gl_gnulib_enabled_malloca])
@@ -879,6 +911,7 @@ AC_DEFUN
   lib/careadlinkat.c
   lib/careadlinkat.h
   lib/cdefs.h
+  lib/chmodat.c
   lib/cloexec.c
   lib/cloexec.h
   lib/close-stream.c
@@ -903,6 +936,7 @@ AC_DEFUN
   lib/execinfo.in.h
   lib/explicit_bzero.c
   lib/faccessat.c
+  lib/fchmodat.c
   lib/fcntl.c
   lib/fcntl.in.h
   lib/fdopendir.c
@@ -941,6 +975,7 @@ AC_DEFUN
   lib/ignore-value.h
   lib/intprops.h
   lib/inttypes.in.h
+  lib/lchmod.c
   lib/libc-config.h
   lib/limits.in.h
   lib/localtime-buffer.c
@@ -1053,6 +1088,7 @@ AC_DEFUN
   m4/extensions.m4
   m4/extern-inline.m4
   m4/faccessat.m4
+  m4/fchmodat.m4
   m4/fcntl-o.m4
   m4/fcntl.m4
   m4/fcntl_h.m4
@@ -1078,6 +1114,7 @@ AC_DEFUN
   m4/include_next.m4
   m4/inttypes.m4
   m4/largefile.m4
+  m4/lchmod.m4
   m4/limits-h.m4
   m4/localtime-buffer.m4
   m4/lstat.m4
diff --git a/m4/lchmod.m4 b/m4/lchmod.m4
new file mode 100644
index 0000000000..61e3f11228
--- /dev/null
+++ b/m4/lchmod.m4
@@ -0,0 +1,84 @@
+#serial 6
+
+dnl Copyright (C) 2005-2006, 2008-2020 Free Software 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 Paul Eggert.
+dnl Provide a replacement for lchmod on hosts that lack a working version.
+
+AC_DEFUN([gl_FUNC_LCHMOD],
+[
+  AC_REQUIRE([gl_SYS_STAT_H_DEFAULTS])
+
+  dnl Persuade glibc <sys/stat.h> to declare lchmod().
+  AC_REQUIRE([AC_USE_SYSTEM_EXTENSIONS])
+
+  AC_REQUIRE([AC_CANONICAL_HOST]) dnl for cross-compiles
+
+  AC_CHECK_FUNCS_ONCE([fchmodat lchmod lstat])
+  if test "$ac_cv_func_lchmod" = no; then
+    HAVE_LCHMOD=0
+  else
+    AC_CACHE_CHECK([whether lchmod works on non-symlinks],
+      [gl_cv_func_lchmod_works],
+      [AC_RUN_IFELSE(
+         [AC_LANG_PROGRAM(
+            [
+              AC_INCLUDES_DEFAULT[
+              #ifndef S_IRUSR
+               #define S_IRUSR 0400
+              #endif
+              #ifndef S_IWUSR
+               #define S_IWUSR 0200
+              #endif
+              #ifndef S_IRWXU
+               #define S_IRWXU 0700
+              #endif
+              #ifndef S_IRWXG
+               #define S_IRWXG 0070
+              #endif
+              #ifndef S_IRWXO
+               #define S_IRWXO 0007
+              #endif
+            ]],
+            [[
+              int permissive = S_IRWXU | S_IRWXG | S_IRWXO;
+              int desired = S_IRUSR | S_IWUSR;
+              static char const f[] = "conftest.lchmod";
+              struct stat st;
+              if (creat (f, permissive) < 0)
+                return 1;
+              if (lchmod (f, desired) != 0)
+                return 1;
+              if (stat (f, &st) != 0)
+                return 1;
+              return ! ((st.st_mode & permissive) == desired);
+            ]])],
+         [gl_cv_func_lchmod_works=yes],
+         [gl_cv_func_lchmod_works=no],
+         [case "$host_os" in
+            dnl Guess no on Linux with glibc, yes otherwise.
+            linux-gnu*) gl_cv_func_lchmod_works="guessing no" ;;
+            *)          gl_cv_func_lchmod_works="$gl_cross_guess_normal" ;;
+          esac
+         ])
+       rm -f conftest.lchmod])
+    case $gl_cv_func_lchmod_works in
+      *yes) ;;
+      *)
+        AC_DEFINE([NEED_LCHMOD_NONSYMLINK_FIX], [1],
+          [Define to 1 if lchmod does not work right on non-symlinks.])
+        REPLACE_LCHMOD=1
+        ;;
+    esac
+  fi
+])
+
+# Prerequisites of lib/lchmod.c.
+AC_DEFUN([gl_PREREQ_LCHMOD],
+[
+  AC_REQUIRE([AC_C_INLINE])
+  :
+])
diff --git a/src/fileio.c b/src/fileio.c
index 87a17eab42..9383f2606b 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -3332,10 +3332,11 @@ DEFUN ("set-file-acl", Fset_file_acl, Sset_file_acl,
   return Qnil;
 }
 
-DEFUN ("file-modes", Ffile_modes, Sfile_modes, 1, 1, 0,
+DEFUN ("file-modes", Ffile_modes, Sfile_modes, 1, 2, 0,
        doc: /* Return mode bits of file named FILENAME, as an integer.
-Return nil if FILENAME does not exist.  */)
-  (Lisp_Object filename)
+Return nil if FILENAME does not exist.  If optional NOFOLLOW is t, then
+do not follow FILENAME if it is a symbolic link.  */)
+  (Lisp_Object filename, Lisp_Object nofollow)
 {
   struct stat st;
   Lisp_Object absname = expand_and_dir_to_file (filename);
@@ -3344,38 +3345,40 @@ DEFUN ("file-modes", Ffile_modes, Sfile_modes, 1, 1, 0,
      call the corresponding file name handler.  */
   Lisp_Object handler = Ffind_file_name_handler (absname, Qfile_modes);
   if (!NILP (handler))
-    return call2 (handler, Qfile_modes, absname);
+    return call3 (handler, Qfile_modes, absname, nofollow);
 
-  if (emacs_fstatat (AT_FDCWD, SSDATA (ENCODE_FILE (absname)), &st, 0) != 0)
+  char *fname = SSDATA (ENCODE_FILE (absname));
+  int flags = !NILP (nofollow) ? AT_SYMLINK_NOFOLLOW : 0;
+  if (emacs_fstatat (AT_FDCWD, fname, &st, flags) != 0)
     return file_attribute_errno (absname, errno);
   return make_fixnum (st.st_mode & 07777);
 }
 
-DEFUN ("set-file-modes", Fset_file_modes, Sset_file_modes, 2, 2,
+DEFUN ("set-file-modes", Fset_file_modes, Sset_file_modes, 2, 3,
        "(let ((file (read-file-name \"File: \")))			\
 	  (list file (read-file-modes nil file)))",
        doc: /* Set mode bits of file named FILENAME to MODE (an integer).
-Only the 12 low bits of MODE are used.
+Only the 12 low bits of MODE are used.  If optional NOFOLLOW is t, then
+do not follow FILENAME if it is a symbolic link.
 
 Interactively, mode bits are read by `read-file-modes', which accepts
 symbolic notation, like the `chmod' command from GNU Coreutils.  */)
-  (Lisp_Object filename, Lisp_Object mode)
+  (Lisp_Object filename, Lisp_Object mode, Lisp_Object nofollow)
 {
-  Lisp_Object absname, encoded_absname;
-  Lisp_Object handler;
-
-  absname = Fexpand_file_name (filename, BVAR (current_buffer, directory));
   CHECK_FIXNUM (mode);
+  Lisp_Object absname = Fexpand_file_name (filename,
+					   BVAR (current_buffer, directory));
 
   /* If the file name has special constructs in it,
      call the corresponding file name handler.  */
-  handler = Ffind_file_name_handler (absname, Qset_file_modes);
+  Lisp_Object handler = Ffind_file_name_handler (absname, Qset_file_modes);
   if (!NILP (handler))
-    return call3 (handler, Qset_file_modes, absname, mode);
-
-  encoded_absname = ENCODE_FILE (absname);
+    return call4 (handler, Qset_file_modes, absname, mode, nofollow);
 
-  if (chmod (SSDATA (encoded_absname), XFIXNUM (mode) & 07777) < 0)
+  char *fname = SSDATA (ENCODE_FILE (absname));
+  mode_t imode = XFIXNUM (mode) & 07777;
+  int flags = !NILP (nofollow) ? AT_SYMLINK_NOFOLLOW : 0;
+  if (fchmodat (AT_FDCWD, fname, imode, flags) < 0)
     report_file_error ("Doing chmod", absname);
 
   return Qnil;
@@ -5740,7 +5743,7 @@ auto_save_1 (void)
 	  == 0)
 	/* But make sure we can overwrite it later!  */
 	auto_save_mode_bits = (st.st_mode | 0600) & 0777;
-      else if (modes = Ffile_modes (BVAR (current_buffer, filename)),
+      else if (modes = Ffile_modes (BVAR (current_buffer, filename), Qnil),
 	       FIXNUMP (modes))
 	/* Remote files don't cooperate with fstatat.  */
 	auto_save_mode_bits = (XFIXNUM (modes) | 0600) & 0777;
-- 
2.24.1






Information forwarded to bug-gnu-emacs <at> gnu.org:
bug#39683; Package emacs. (Thu, 20 Feb 2020 14:53:01 GMT) Full text and rfc822 format available.

Message #8 received at 39683 <at> debbugs.gnu.org (full text, mbox):

From: Eli Zaretskii <eliz <at> gnu.org>
To: Paul Eggert <eggert <at> cs.ucla.edu>
Cc: 39683 <at> debbugs.gnu.org
Subject: Re: bug#39683: [PATCH] Add NOFOLLOW flag to set-file-modes etc.
Date: Thu, 20 Feb 2020 16:51:53 +0200
> From: Paul Eggert <eggert <at> cs.ucla.edu>
> Date: Wed, 19 Feb 2020 16:34:00 -0800
> Cc: Paul Eggert <eggert <at> cs.ucla.edu>
> 
> diff --git a/lisp/dired-aux.el b/lisp/dired-aux.el
> index 0069c1744d..de5e8bb567 100644
> --- a/lisp/dired-aux.el
> +++ b/lisp/dired-aux.el
> @@ -409,7 +409,8 @@ dired-do-chmod
>        (set-file-modes
>         file
>         (if num-modes num-modes
> -	 (file-modes-symbolic-to-number modes (file-modes file)))))
> +	 (file-modes-symbolic-to-number modes (file-modes file t)))

Can we please use some descriptive symbol, like 'nofollow, instead of
just t?  I expect that to save at least some of us an extra read of
the doc string each time we see such code.

TIA




Reply sent to Paul Eggert <eggert <at> cs.ucla.edu>:
You have taken responsibility. (Mon, 24 Feb 2020 00:51:02 GMT) Full text and rfc822 format available.

Notification sent to Paul Eggert <eggert <at> cs.ucla.edu>:
bug acknowledged by developer. (Mon, 24 Feb 2020 00:51:02 GMT) Full text and rfc822 format available.

Message #13 received at 39683-done <at> debbugs.gnu.org (full text, mbox):

From: Paul Eggert <eggert <at> cs.ucla.edu>
To: Eli Zaretskii <eliz <at> gnu.org>
Cc: 39683-done <at> debbugs.gnu.org
Subject: Re: bug#39683: [PATCH] Add NOFOLLOW flag to set-file-modes etc.
Date: Sun, 23 Feb 2020 16:50:09 -0800
[Message part 1 (text/plain, inline)]
On 2/20/20 6:51 AM, Eli Zaretskii wrote:
> Can we please use some descriptive symbol, like 'nofollow, instead of
> just t?

Sure, I did that and installed the attached (revised) patch into master. This 
also contains some minor improvements, notably one that adds support for the new 
flag to tramp-handle-file-modes.
[0001-Add-nofollow-flag-to-set-file-modes-etc.patch (text/x-patch, attachment)]

bug archived. Request was from Debbugs Internal Request <help-debbugs <at> gnu.org> to internal_control <at> debbugs.gnu.org. (Mon, 23 Mar 2020 11:24:06 GMT) Full text and rfc822 format available.

This bug report was last modified 4 years and 35 days ago.

Previous Next


GNU bug tracking system
Copyright (C) 1999 Darren O. Benham, 1997,2003 nCipher Corporation Ltd, 1994-97 Ian Jackson.