emacs-diffs
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

master 9d626df: Add 'nofollow' flag to set-file-modes etc.


From: Paul Eggert
Subject: master 9d626df: Add 'nofollow' flag to set-file-modes etc.
Date: Sun, 23 Feb 2020 19:46:18 -0500 (EST)

branch: master
commit 9d626dffc6ba62c0d7a1a5c712f576ed8684fd66
Author: Paul Eggert <address@hidden>
Commit: Paul Eggert <address@hidden>

    Add 'nofollow' flag to set-file-modes etc.
    
    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(-)

diff --git a/admin/merge-gnulib b/admin/merge-gnulib
index 48c81e6..5571194 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 a93da39..a69a4e5 100644
--- a/doc/lispref/files.texi
+++ b/doc/lispref/files.texi
@@ -928,7 +928,7 @@ also checks that the file's group would be unchanged.
 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 @@ The highest possible value is 4095 (7777 octal), meaning 
that everyone
 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 @@ can be used to set these permissions.
 @end group
 
 @group
-(set-file-modes "~/junk/diffs" #o666)
+(set-file-modes "~/junk/diffs" #o666 'nofollow)
      @result{} nil
 @end group
 
@@ -1801,9 +1808,17 @@ See also @code{delete-directory} in @ref{Create/Delete 
Dirs}.
 @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 @@ lowest 9 bits are meaningful.  You can use the Lisp 
construct for
 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 a034ccd..cf4ef52 100644
--- a/doc/lispref/os.texi
+++ b/doc/lispref/os.texi
@@ -3127,7 +3127,7 @@ being reported.  For example:
 @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 0279879..5ca0543 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 0000000..8950168
--- /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 3c01e61..d4dc6a3 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 0000000..e113211
--- /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 0069c17..8f00317 100644
--- a/lisp/dired-aux.el
+++ b/lisp/dired-aux.el
@@ -409,7 +409,8 @@ has no effect on MS-Windows."
       (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 3788d79..8b3d552 100644
--- a/lisp/doc-view.el
+++ b/lisp/doc-view.el
@@ -683,8 +683,6 @@ at the top edge of the page moves to the previous page."
       ;; 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 @@ at the top edge of the page moves to the previous page."
      ;; 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 785e350..e9f7658 100644
--- a/lisp/emacs-lisp/autoload.el
+++ b/lisp/emacs-lisp/autoload.el
@@ -895,7 +895,7 @@ FILE's modification time."
           (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 fce5e4a..24a3639 100644
--- a/lisp/emacs-lisp/bytecomp.el
+++ b/lisp/emacs-lisp/bytecomp.el
@@ -2008,7 +2008,7 @@ The value is non-nil if there were no errors, nil if 
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)
                  ;; 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 04bf3ff..7219af4 100644
--- a/lisp/eshell/em-pred.el
+++ b/lisp/eshell/em-pred.el
@@ -478,7 +478,7 @@ that `ls -l' will show in the first column of its display."
 (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 683f4a8..2e7694d 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -4672,6 +4672,7 @@ BACKUPNAME is the backup file name, which is the old file 
renamed."
   ;; 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 @@ BACKUPNAME is the backup file name, which is the old file 
renamed."
                   (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 @@ BACKUPNAME is the backup file name, which is the old file 
renamed."
               (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 @@ into NEWNAME instead."
   ;; 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 @@ into NEWNAME instead."
                 (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 @@ into NEWNAME instead."
       (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 eb0fd25..83a8516 100644
--- a/lisp/gnus/gnus-util.el
+++ b/lisp/gnus/gnus-util.el
@@ -1601,10 +1601,10 @@ empty directories from OLD-PATH."
                         (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 f5b6878..acf35a3 100644
--- a/lisp/gnus/mail-source.el
+++ b/lisp/gnus/mail-source.el
@@ -695,7 +695,7 @@ Deleting old (> %s day(s)) incoming mail file `%s'." diff 
bfile)
                         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 2dab278..96695aa 100644
--- a/lisp/gnus/mm-decode.el
+++ b/lisp/gnus/mm-decode.el
@@ -948,7 +948,7 @@ external if displayed 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 6e01b5c..93e4b0e 100644
--- a/lisp/gnus/nnmail.el
+++ b/lisp/gnus/nnmail.el
@@ -1958,7 +1958,7 @@ If TIME is nil, then return the cutoff time for oldness 
instead."
   (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 f283942..e2d4d7d 100644
--- a/lisp/net/ange-ftp.el
+++ b/lisp/net/ange-ftp.el
@@ -4740,7 +4740,8 @@ NEWNAME should be the name to give the new compressed or 
uncompressed file.")
   (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 aa7fe14..96ef95d 100644
--- a/lisp/net/tramp-adb.el
+++ b/lisp/net/tramp-adb.el
@@ -591,7 +591,8 @@ Emacs dired can't find files."
          (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 @@ But handle the case, if the \"test\" command is not 
available."
           (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 @@ But handle the case, if the \"test\" command is not 
available."
        (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 762c4fe..7983580 100644
--- a/lisp/net/tramp-gvfs.el
+++ b/lisp/net/tramp-gvfs.el
@@ -1562,12 +1562,12 @@ If FILE-SYSTEM is non-nil, return file system 
attributes."
     (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 5a3abc3..f31d361 100644
--- a/lisp/net/tramp-sh.el
+++ b/lisp/net/tramp-sh.el
@@ -1478,10 +1478,11 @@ of."
             ;; 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 @@ the uid and gid from FILENAME."
                      ;; 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 @@ STDERR can also be a file name."
                    (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 @@ STDERR can also be a file name."
          ;; 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 f02be39..95505ea 100644
--- a/lisp/net/tramp-smb.el
+++ b/lisp/net/tramp-smb.el
@@ -1464,8 +1464,9 @@ component is used as the target of the symlink."
            (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 f258ad6..4654d63 100644
--- a/lisp/net/tramp-sudoedit.el
+++ b/lisp/net/tramp-sudoedit.el
@@ -463,8 +463,9 @@ the result will be a local, non-Tramp, file name."
       (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 @@ ID-FORMAT valid values are `string' and `integer'."
                         (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 409e1f7..64acaa9 100644
--- a/lisp/net/tramp.el
+++ b/lisp/net/tramp.el
@@ -3179,10 +3179,13 @@ User is always nil."
       (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 @@ of."
       ;; 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 @@ Return the local name of the temporary 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 e6d8b17..1861218 100644
--- a/lisp/server.el
+++ b/lisp/server.el
@@ -563,7 +563,7 @@ See variable `server-auth-dir' for details."
                      (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 645011a..6dd7a9c 100644
--- a/lisp/url/url-util.el
+++ b/lisp/url/url-util.el
@@ -615,9 +615,7 @@ Creates FILE and its parent directories if they do not 
exist."
         (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 0000000..e3f2f04
--- /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 fea32b5..1465ce8 100644
--- a/m4/gnulib-comp.m4
+++ b/m4/gnulib-comp.m4
@@ -82,6 +82,7 @@ AC_DEFUN([gl_EARLY],
   # 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([gl_EARLY],
   # 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([gl_INIT],
   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_INIT],
   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([gl_INIT],
       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([gl_INIT],
   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([gl_INIT],
   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([gl_FILE_LIST], [
   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([gl_FILE_LIST], [
   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([gl_FILE_LIST], [
   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([gl_FILE_LIST], [
   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 0000000..b9e8a97
--- /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 6b56c47..2532f52 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -3332,50 +3332,60 @@ support.  */)
   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;



reply via email to

[Prev in Thread] Current Thread [Next in Thread]