From 9d626dffc6ba62c0d7a1a5c712f576ed8684fd66 Mon Sep 17 00:00:00 2001 From: Paul Eggert Date: Sun, 23 Feb 2020 16:19:42 -0800 Subject: [PATCH] Add 'nofollow' flag to set-file-modes etc. MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This avoids some race conditions (Bug#39683). E.g., if some other program changes a file to a symlink between the time Emacs creates the file and the time it changes the file’s permissions, using the new flag prevents Emacs from inadvertently changing the permissions of a victim in some completely unrelated directory. * admin/merge-gnulib (GNULIB_MODULES): Add fchmodat. * doc/lispref/files.texi (Testing Accessibility, Changing Files): * doc/lispref/os.texi (File Notifications): * etc/NEWS: Adjust documentation accordingly. * lib/chmodat.c, lib/fchmodat.c, lib/lchmod.c, m4/fchmodat.m4: * m4/lchmod.m4: New files, copied from Gnulib. * lib/gnulib.mk.in: Regenerate. * lisp/dired-aux.el (dired-do-chmod): * lisp/doc-view.el (doc-view-make-safe-dir): * lisp/emacs-lisp/autoload.el (autoload--save-buffer): * lisp/emacs-lisp/bytecomp.el (byte-compile-file): * lisp/eshell/em-pred.el (eshell-pred-file-mode): * lisp/files.el (backup-buffer-copy, copy-directory): * lisp/gnus/mail-source.el (mail-source-movemail): * lisp/gnus/mm-decode.el (mm-display-external): * lisp/gnus/nnmail.el (nnmail-write-region): * lisp/net/tramp-adb.el (tramp-adb-handle-file-local-copy) (tramp-adb-handle-write-region): * lisp/net/tramp-sh.el (tramp-do-copy-or-rename-file-directly): * lisp/net/tramp-sudoedit.el (tramp-sudoedit-handle-write-region): * lisp/net/tramp.el (tramp-handle-write-region) (tramp-make-tramp-temp-file): * lisp/server.el (server-ensure-safe-dir): * lisp/url/url-util.el (url-make-private-file): When getting or setting file modes, avoid following symbolic links when the file is not supposed to be a symbolic link. * lisp/doc-view.el (doc-view-make-safe-dir): Omit no-longer-needed separate symlink test. * lisp/gnus/gnus-util.el (gnus-set-file-modes): * lisp/net/tramp.el (tramp-handle-file-modes): * lisp/net/tramp-gvfs.el (tramp-gvfs-handle-set-file-modes): * src/fileio.c (symlink_nofollow_flag): New function. (Ffile_modes, Fset_file_modes): Support an optional FLAG arg. All C callers changed. * lisp/net/ange-ftp.el (ange-ftp-set-file-modes): * lisp/net/tramp-adb.el (tramp-adb-handle-set-file-modes): * lisp/net/tramp-sh.el (tramp-sh-handle-set-file-modes): * lisp/net/tramp-smb.el (tramp-smb-handle-set-file-modes): * lisp/net/tramp-sudoedit.el (tramp-sudoedit-handle-set-file-modes): Accept an optional FLAG arg that is currently ignored, and add a FIXME comment for it. * m4/gnulib-comp.m4: Regenerate. --- admin/merge-gnulib | 2 +- doc/lispref/files.texi | 27 +++++-- doc/lispref/os.texi | 2 +- etc/NEWS | 3 + lib/fchmodat.c | 144 ++++++++++++++++++++++++++++++++++++ lib/gnulib.mk.in | 26 +++++++ lib/lchmod.c | 110 +++++++++++++++++++++++++++ lisp/dired-aux.el | 3 +- lisp/doc-view.el | 4 +- lisp/emacs-lisp/autoload.el | 2 +- lisp/emacs-lisp/bytecomp.el | 2 +- lisp/eshell/em-pred.el | 2 +- lisp/files.el | 12 ++- lisp/gnus/gnus-util.el | 4 +- lisp/gnus/mail-source.el | 2 +- lisp/gnus/mm-decode.el | 2 +- lisp/gnus/nnmail.el | 2 +- lisp/net/ange-ftp.el | 3 +- lisp/net/tramp-adb.el | 9 ++- lisp/net/tramp-gvfs.el | 4 +- lisp/net/tramp-sh.el | 10 ++- lisp/net/tramp-smb.el | 3 +- lisp/net/tramp-sudoedit.el | 6 +- lisp/net/tramp.el | 13 ++-- lisp/server.el | 2 +- lisp/url/url-util.el | 4 +- m4/fchmodat.m4 | 82 ++++++++++++++++++++ m4/gnulib-comp.m4 | 35 +++++++++ m4/lchmod.m4 | 31 ++++++++ src/fileio.c | 46 +++++++----- 30 files changed, 533 insertions(+), 64 deletions(-) create mode 100644 lib/fchmodat.c create mode 100644 lib/lchmod.c create mode 100644 m4/fchmodat.m4 create mode 100644 m4/lchmod.m4 diff --git a/admin/merge-gnulib b/admin/merge-gnulib index 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 . */ + +/* written by Jim Meyering and Paul Eggert */ + +/* If the user's config.h happens to include , let it include only + the system's here, so that orig_fchmodat doesn't recurse to + rpl_fchmodat. */ +#define __need_system_sys_stat_h +#include + +/* Specification. */ +#include +#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 +#include +#include +#include +#include + +#ifdef __osf__ +/* Write "sys/stat.h" here, not , otherwise OSF/1 5.1 DTK cc + eliminates this include because of the preliminary #include + above. */ +# include "sys/stat.h" +#else +# include +#endif + +#include + +/* 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. */ + 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 . */ + +/* written by Paul Eggert */ + +#include + +/* Specification. */ +#include + +#include +#include +#include +#include + +#ifdef __osf__ +/* Write "sys/stat.h" here, not , otherwise OSF/1 5.1 DTK cc + eliminates this include because of the preliminary #include + above. */ +# include "sys/stat.h" +#else +# include +#endif + +#include + +/* 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. */ + 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 + #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 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); + return call4 (handler, Qset_file_modes, absname, mode, flag); - encoded_absname = ENCODE_FILE (absname); - - 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;