summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPaul Eggert <eggert@cs.ucla.edu>2020-02-23 16:19:42 -0800
committerPaul Eggert <eggert@cs.ucla.edu>2020-02-23 16:45:50 -0800
commit9d626dffc6ba62c0d7a1a5c712f576ed8684fd66 (patch)
tree6cc8fbe8e5bc02c3bb74139710814a0400e91a8a
parentc4ca8219dd6b8f06e67a0b767475b1259653b8e0 (diff)
downloademacs-9d626dffc6ba62c0d7a1a5c712f576ed8684fd66.tar.gz
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.
-rwxr-xr-xadmin/merge-gnulib2
-rw-r--r--doc/lispref/files.texi27
-rw-r--r--doc/lispref/os.texi2
-rw-r--r--etc/NEWS3
-rw-r--r--lib/fchmodat.c144
-rw-r--r--lib/gnulib.mk.in26
-rw-r--r--lib/lchmod.c110
-rw-r--r--lisp/dired-aux.el3
-rw-r--r--lisp/doc-view.el4
-rw-r--r--lisp/emacs-lisp/autoload.el2
-rw-r--r--lisp/emacs-lisp/bytecomp.el2
-rw-r--r--lisp/eshell/em-pred.el2
-rw-r--r--lisp/files.el12
-rw-r--r--lisp/gnus/gnus-util.el4
-rw-r--r--lisp/gnus/mail-source.el2
-rw-r--r--lisp/gnus/mm-decode.el2
-rw-r--r--lisp/gnus/nnmail.el2
-rw-r--r--lisp/net/ange-ftp.el3
-rw-r--r--lisp/net/tramp-adb.el9
-rw-r--r--lisp/net/tramp-gvfs.el4
-rw-r--r--lisp/net/tramp-sh.el10
-rw-r--r--lisp/net/tramp-smb.el3
-rw-r--r--lisp/net/tramp-sudoedit.el6
-rw-r--r--lisp/net/tramp.el13
-rw-r--r--lisp/server.el2
-rw-r--r--lisp/url/url-util.el4
-rw-r--r--m4/fchmodat.m482
-rw-r--r--m4/gnulib-comp.m435
-rw-r--r--m4/lchmod.m431
-rw-r--r--src/fileio.c46
30 files changed, 533 insertions, 64 deletions
diff --git a/admin/merge-gnulib b/admin/merge-gnulib
index 48c81e61e2a..557119441e4 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 a93da39f174..a69a4e5dd38 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 a034ccdcd5c..cf4ef52abfb 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 02798798367..5ca054363d2 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 00000000000..8950168608f
--- /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 3c01e61b266..d4dc6a3df33 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 00000000000..e1132116234
--- /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 0069c1744dc..8f00317c2b0 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 3788d797258..8b3d5527f08 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 785e350e0e5..e9f76583272 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 fce5e4aed6d..24a36393b2e 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 04bf3ff8998..7219af45f54 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 683f4a8ce7c..2e7694d7677 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 eb0fd2522d3..83a85161aa0 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 f5b68789b85..acf35a376a9 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 2dab278b373..96695aabfde 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 6e01b5c4d0b..93e4b0e7a8f 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 f28394260dd..e2d4d7dd057 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 aa7fe147c20..96ef95dbe30 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 762c4fe4b3b..79835804bc0 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 5a3abc31ea6..f31d3615884 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 f02be394a7b..95505ea101f 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 f258ad6b931..4654d633fab 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 409e1f7499a..64acaa95d47 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 e6d8b1783c9..18612181477 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 645011a5783..6dd7a9c2aac 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 00000000000..e3f2f048162
--- /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 fea32b544f9..1465ce811b8 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 00000000000..b9e8a97cb31
--- /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 6b56c473abf..2532f5233c4 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;