Subject: bug#39683: closed (Re: bug#39683: [PATCH] Add NOFOLLOW flag to set-file-modes etc.)

From: [email-masked] (GNU bug Tracking System)
Date: Mon, 24 Feb 2020 00:51:02 +0000

Your bug report

#39683: [PATCH] Add NOFOLLOW flag to set-file-modes etc.

which was filed against the emacs package, has been closed.

The explanation is attached below, along with your original report.
If you require more details, please reply to [email-masked].

-- 
39683: http://debbugs.gnu.org/cgi/bugreport.cgi?bug=39683
GNU Bug Tracking System
Contact [email-masked] with problems

Subject: Re: bug#39683: [PATCH] Add NOFOLLOW flag to set-file-modes etc.
From: Paul Eggert <[email-masked]>
To: Eli Zaretskii <[email-masked]>
Cc: [email-masked]
Date: Sun, 23 Feb 2020 16:50:09 -0800
List-Id: <debbugs-submit.debbugs.gnu.org>

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.
>From 9d626dffc6ba62c0d7a1a5c712f576ed8684fd66 Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Sun, 23 Feb 2020 16:19:42 -0800
Subject: [PATCH] Add 'nofollow' flag to set-file-modes etc.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

This avoids some race conditions (Bug#39683).  E.g., 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.
* doc/lispref/files.texi (Testing Accessibility, Changing Files):
* doc/lispref/os.texi (File Notifications):
* etc/NEWS:
Adjust documentation accordingly.
* lib/chmodat.c, lib/fchmodat.c, lib/lchmod.c, m4/fchmodat.m4:
* m4/lchmod.m4: New files, copied from Gnulib.
* 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.el (tramp-handle-file-modes):
* lisp/net/tramp-gvfs.el (tramp-gvfs-handle-set-file-modes):
* src/fileio.c (symlink_nofollow_flag): New function.
(Ffile_modes, Fset_file_modes):
Support an optional FLAG arg.  All C callers changed.
* 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):
Accept an optional FLAG 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      |  27 +++++--
 doc/lispref/os.texi         |   2 +-
 etc/NEWS                    |   3 +
 lib/fchmodat.c              | 144 ++++++++++++++++++++++++++++++++++++
 lib/gnulib.mk.in            |  26 +++++++
 lib/lchmod.c                | 110 +++++++++++++++++++++++++++
 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       |   9 ++-
 lisp/net/tramp-gvfs.el      |   4 +-
 lisp/net/tramp-sh.el        |  10 ++-
 lisp/net/tramp-smb.el       |   3 +-
 lisp/net/tramp-sudoedit.el  |   6 +-
 lisp/net/tramp.el           |  13 ++--
 lisp/server.el              |   2 +-
 lisp/url/url-util.el        |   4 +-
 m4/fchmodat.m4              |  82 ++++++++++++++++++++
 m4/gnulib-comp.m4           |  35 +++++++++
 m4/lchmod.m4                |  31 ++++++++
 src/fileio.c                |  46 +++++++-----
 30 files changed, 533 insertions(+), 64 deletions(-)
 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..a69a4e5dd3 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 &optional flag
 @cindex mode bits
 @cindex file permissions
 @cindex permissions, file
@@ -946,12 +946,19 @@ 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{flag} is the symbol @code{nofollow}, 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" 'nofollow)
      @result{} 492               ; @r{Decimal integer.}
 @end group
 @group
@@ -960,7 +967,7 @@ Testing Accessibility
 @end group
 
 @group
-(set-file-modes "~/junk/diffs" #o666)
+(set-file-modes "~/junk/diffs" #o666 'nofollow)
      @result{} nil
 @end group
 
@@ -1801,9 +1808,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 &optional flag
 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{flag} is the symbol @code{nofollow}, 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{flag} is @code{nofollow}.
 
 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 +1826,7 @@ Changing Files
 octal numbers to enter @var{mode}.  For example,
 
 @example
-(set-file-modes #o644)
+(set-file-modes "myfile" #o644 'nofollow)
 @end example
 
 @noindent
diff --git a/doc/lispref/os.texi b/doc/lispref/os.texi
index a034ccdcd5..cf4ef52abf 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) 'nofollow)
      @result{} Event (35025468 attribute-changed "/tmp/foo")
 @end group
 @end example
diff --git a/etc/NEWS b/etc/NEWS
index 0279879836..5ca054363d 100644
--- a/etc/NEWS
+++ b/etc/NEWS
@@ -198,6 +198,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/fchmodat.c b/lib/fchmodat.c
new file mode 100644
index 0000000000..8950168608
--- /dev/null
+++ b/lib/fchmodat.c
@@ -0,0 +1,144 @@
+/* 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
+      /* 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 undesired - 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;
+        }
+
+#   if defined __linux__ || defined __ANDROID__
+      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;
+        }
+#   endif
+      /* /proc is not mounted or would not work as in GNU/Linux.  */
+
+#  else
+      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;
+        }
+#  endif
+
+      /* Fall back on orig_fchmodat with no flags, despite a possible race.  */
+      flags = 0;
+    }
+# 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 3c01e61b26..d4dc6a3df3 100644
--- a/lib/gnulib.mk.in
+++ b/lib/gnulib.mk.in
@@ -95,6 +95,7 @@
 #  execinfo \
 #  explicit_bzero \
 #  faccessat \
+#  fchmodat \
 #  fcntl \
 #  fcntl-h \
 #  fdopendir \
@@ -1082,6 +1083,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@
@@ -1586,6 +1588,17 @@ EXTRA_libgnu_a_SOURCES += at-func.c faccessat.c
 endif
 ## end   gnulib module faccessat
 
+## begin gnulib module fchmodat
+ifeq (,$(OMIT_GNULIB_MODULE_fchmodat))
+
+
+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))
 
@@ -1936,6 +1949,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..e113211623
--- /dev/null
+++ b/lib/lchmod.c
@@ -0,0 +1,110 @@
+/* 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>
+
+/* Specification.  */
+#include <sys/stat.h>
+
+#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 defined O_PATH && defined AT_EMPTY_PATH
+  /* 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 = open (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 undesired - 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;
+    }
+
+# if defined __linux__ || defined __ANDROID__
+  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;
+    }
+# endif
+  /* /proc is not mounted or would not work as in GNU/Linux.  */
+
+#elif HAVE_LSTAT
+  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;
+    }
+#endif
+
+  /* Fall back on chmod, despite a possible race.  */
+  return chmod (file, mode);
+}
diff --git a/lisp/dired-aux.el b/lisp/dired-aux.el
index 0069c1744d..8f00317c2b 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 'nofollow)))
+       'nofollow))
     (dired-do-redisplay arg)))
 
 ;;;###autoload
diff --git a/lisp/doc-view.el b/lisp/doc-view.el
index 3788d79725..8b3d5527f0 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 'nofollow)
        (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..e9f7658327 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 'nofollow))
     (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..24a36393b2 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 'nofollow))
 		  (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..7219af45f5 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 'nofollow)))
        (if modes
 	   (logand ,mode modes)))))
 
diff --git a/lisp/files.el b/lisp/files.el
index 683f4a8ce7..2e7694d767 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-flag)
   (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-flag 'nofollow)
 		   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-flag)))))
 
 (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 (unless follow 'nofollow)))
 	(if times (set-file-times newname times))))))
 
 
diff --git a/lisp/gnus/gnus-util.el b/lisp/gnus/gnus-util.el
index eb0fd2522d..83a85161aa 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 flag)
   "Wrapper for set-file-modes."
   (ignore-errors
-    (set-file-modes filename mode)))
+    (set-file-modes filename mode flag)))
 
 (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..acf35a376a 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 'nofollow))
 	      (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..96695aabfd 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 'nofollow)
 	  (message "Viewing with %s" method)
 	  (cond
 	   (needsterm
diff --git a/lisp/gnus/nnmail.el b/lisp/gnus/nnmail.el
index 6e01b5c4d0..93e4b0e7a8 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 'nofollow)))
 
 ;;;
 ;;; Status functions
diff --git a/lisp/net/ange-ftp.el b/lisp/net/ange-ftp.el
index f28394260d..e2d4d7dd05 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 flag)
+  flag ;; FIXME: Support 'nofollow'.
   (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..96ef95dbe3 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)
+			'nofollow))
       tmpfile)))
 
 (defun tramp-adb-handle-file-writable-p (filename)
@@ -636,7 +637,8 @@ 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)
+			'nofollow))
       (tramp-run-real-handler
        #'write-region (list start end tmpfile append 'no-message lockname))
       (with-tramp-progress-reporter
@@ -665,8 +667,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 flag)
   "Like `set-file-modes' for Tramp files."
+  flag ;; FIXME: Support 'nofollow'.
   (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..79835804bc 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 flag)
   "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 flag "-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..f31d361588 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 flag)
   "Like `set-file-modes' for Tramp files."
   (with-parsed-tramp-file-name filename nil
     (tramp-flush-file-properties v localname)
+    flag ;; FIXME: Support 'nofollow'.
     ;; 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 'nofollow)
 		      (tramp-set-file-uid-gid
 		       tmpfile
 		       (tramp-get-remote-uid v 'integer)
@@ -3221,7 +3222,8 @@ 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)
+			    'nofollow)
 	    ;; Set local user ownership.
 	    (tramp-set-file-uid-gid tmpfile))
 
@@ -3320,7 +3322,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) 'nofollow))
 
 	  ;; 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..95505ea101 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 flag)
   "Like `set-file-modes' for Tramp files."
+  flag ;; FIXME: Support 'nofollow'.
   (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..4654d633fa 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 flag)
   "Like `set-file-modes' for Tramp files."
+  flag ;; FIXME: Support 'nofollow'.
   (with-parsed-tramp-file-name filename nil
     (tramp-flush-file-properties v localname)
     (unless (tramp-sudoedit-send-command
@@ -735,7 +736,8 @@ 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
+			(when (eq mustbenew 'excl) 'nofollow))))))
 
 
 ;; Internal functions.
diff --git a/lisp/net/tramp.el b/lisp/net/tramp.el
index 409e1f7499..64acaa95d4 100644
--- a/lisp/net/tramp.el
+++ b/lisp/net/tramp.el
@@ -3179,10 +3179,13 @@ 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 flag)
   "Like `file-modes' for Tramp files."
-  (when-let ((attrs (file-attributes (or (file-truename filename) filename))))
-    (tramp-mode-string-to-int (tramp-compat-file-attribute-modes attrs))))
+  (when-let ((attrs (file-attributes filename)))
+    (let ((mode-string (tramp-compat-file-attribute-modes attrs)))
+      (if (and (not flag) (eq ?l (aref mode-string 0)))
+	  (tramp-handle-file-modes (file-chase-links filename) 'nofollow)
+	(tramp-mode-string-to-int mode-string)))))
 
 ;; Localname manipulation functions that grok Tramp localnames...
 (defun tramp-handle-file-name-as-directory (file)
@@ -3884,7 +3887,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) 'nofollow)
       ;; 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 +4667,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 'nofollow)))
 
     ;; Return the local part.
     (tramp-file-local-name result)))
diff --git a/lisp/server.el b/lisp/server.el
index e6d8b1783c..1861218147 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 'nofollow)))
                        (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..6dd7a9c2aa 100644
--- a/lisp/url/url-util.el
+++ b/lisp/url/url-util.el
@@ -615,9 +615,7 @@ url-make-private-file
         (with-temp-buffer
           (write-region (point-min) (point-max) file nil 'silent nil 'excl)))
     (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 'nofollow))))
 
 (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 fea32b544f..1465ce811b 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:
@@ -255,6 +257,12 @@ 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_SYS_STAT_MODULE_INDICATOR([fchmodat])
   gl_FUNC_FCNTL
   if test $HAVE_FCNTL = 0 || test $REPLACE_FCNTL = 1; then
     AC_LIBOBJ([fcntl])
@@ -468,6 +476,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
@@ -569,6 +578,18 @@ AC_DEFUN
       fi
     fi
   }
+  func_gl_gnulib_m4code_lchmod ()
+  {
+    if ! $gl_gnulib_enabled_lchmod; then
+      gl_FUNC_LCHMOD
+      if test $HAVE_LCHMOD = 0; 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
@@ -660,6 +681,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
@@ -708,6 +738,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])
@@ -908,6 +939,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
@@ -946,6 +978,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
@@ -1058,6 +1091,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
@@ -1083,6 +1117,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..b9e8a97cb3
--- /dev/null
+++ b/m4/lchmod.m4
@@ -0,0 +1,31 @@
+#serial 7
+
+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([lchmod lstat])
+  if test "$ac_cv_func_lchmod" = no; then
+    HAVE_LCHMOD=0
+  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 6b56c473ab..2532f5233c 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -3332,50 +3332,60 @@ DEFUN ("set-file-acl", Fset_file_acl, Sset_file_acl,
   return Qnil;
 }
 
-DEFUN ("file-modes", Ffile_modes, Sfile_modes, 1, 1, 0,
+static int
+symlink_nofollow_flag (Lisp_Object flag)
+{
+  /* For now, treat all non-nil FLAGs like 'nofollow'.  */
+  return !NILP (flag) ? AT_SYMLINK_NOFOLLOW : 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 FLAG is `nofollow',
+do not follow FILENAME if it is a symbolic link.  */)
+  (Lisp_Object filename, Lisp_Object flag)
 {
   struct stat st;
+  int nofollow = symlink_nofollow_flag (flag);
   Lisp_Object absname = expand_and_dir_to_file (filename);
 
   /* If the file name has special constructs in it,
      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, flag);
 
-  if (emacs_fstatat (AT_FDCWD, SSDATA (ENCODE_FILE (absname)), &st, 0) != 0)
+  char *fname = SSDATA (ENCODE_FILE (absname));
+  if (emacs_fstatat (AT_FDCWD, fname, &st, nofollow) != 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 FLAG is `nofollow',
+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 flag)
 {
-  Lisp_Object absname, encoded_absname;
-  Lisp_Object handler;
-
-  absname = Fexpand_file_name (filename, BVAR (current_buffer, directory));
   CHECK_FIXNUM (mode);
+  int nofollow = symlink_nofollow_flag (flag);
+  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, flag);
 
-  if (chmod (SSDATA (encoded_absname), XFIXNUM (mode) & 07777) < 0)
+  char *fname = SSDATA (ENCODE_FILE (absname));
+  mode_t imode = XFIXNUM (mode) & 07777;
+  if (fchmodat (AT_FDCWD, fname, imode, nofollow) != 0)
     report_file_error ("Doing chmod", absname);
 
   return Qnil;
@@ -5740,7 +5750,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.17.1

Subject: [PATCH] Add NOFOLLOW flag to set-file-modes etc.
From: Paul Eggert <[email-masked]>
To: [email-masked]
Cc: Paul Eggert <[email-masked]>
Date: Wed, 19 Feb 2020 16:34:00 -0800
List-Id: <debbugs-submit.debbugs.gnu.org>

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