master 284bc6a9127: Fix a few bugs in 'make-empty-file'

Philipp Stephani <[email protected]>
Newsgroups gmane.emacs.diffs
Message-ID <[email protected]>
branch: master
commit 284bc6a9127cd3cb20148850ededd7d70e444c60
Author: Philipp Stephani <[email protected]>
Commit: Philipp Stephani <[email protected]>

    Fix a few bugs in 'make-empty-file'
    
    Crucially, we need to pass 'excl' to 'write-region' so that the file is
    opened in exclusive mode; this is the only way to avoid TOCTTOU errors.
    
    Furthermore, the command errorneously created a parent directory even if
    the PARENT argument is nil.
    
    Add unit tests to cover these cases, including a TOCTTOU test.
    
    * lisp/files.el (make-empty-file): Only create parent directories
    when requested.  Open output file in exclusive mode.
    
    * test/lisp/files-tests.el (files-tests--make-empty-file--exists)
    (files-tests--make-empty-file--parent-missing)
    (files-tests--make-empty-file--tocttou): New unit tests.
---
 lisp/files.el            | 11 +++++++----
 test/lisp/files-tests.el | 26 ++++++++++++++++++++++++++
 2 files changed, 33 insertions(+), 4 deletions(-)

diff --git a/lisp/files.el b/lisp/files.el
index 8f2d8e97e4b..c2f030e0b77 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -6759,10 +6759,13 @@ If called interactively, then PARENTS is non-nil."
      (list filename t)))
   (when (and (file-exists-p filename) (null parents))
     (signal 'file-already-exists `("File exists" ,filename)))
-  (let ((paren-dir (file-name-directory filename)))
-    (when (and paren-dir (not (file-exists-p paren-dir)))
-      (make-directory paren-dir parents)))
-  (write-region "" nil filename nil 0))
+  (when parents
+    (let ((paren-dir (file-name-directory filename)))
+      (when (and paren-dir (not (file-exists-p paren-dir)))
+        (make-directory paren-dir parents))))
+  ;; The `excl' is crucial, in case someone else has created the file in
+  ;; the meantime (TOCTTOU).
+  (write-region "" nil filename nil 0 nil 'excl))
 
 (defconst directory-files-no-dot-files-regexp
   "[^.]\\|\\.\\.\\."
diff --git a/test/lisp/files-tests.el b/test/lisp/files-tests.el
index fdfc1f92b47..9f657cf7b26 100644
--- a/test/lisp/files-tests.el
+++ b/test/lisp/files-tests.el
@@ -2302,5 +2302,31 @@ Prompt users for any modified buffer with `buffer-offer-save' non-nil."
   (should (file-expand-wildcards
            (concat (directory-file-name default-directory) "*/"))))
 
+(ert-deftest files-tests--make-empty-file--exists ()
+  (ert-with-temp-directory base
+    (let ((file (file-name-concat base "file.txt")))
+      (write-region "" nil file nil nil nil 'excl)
+      (should-error (make-empty-file file)
+                    :type 'file-already-exists))))
+
+(ert-deftest files-tests--make-empty-file--parent-missing ()
+  (ert-with-temp-directory base
+    (let ((file (file-name-concat base "dir" "file.txt")))
+      (should-error (make-empty-file file) :type 'file-missing))))
+
+(ert-deftest files-tests--make-empty-file--tocttou ()
+  (ert-with-temp-directory base
+    (let ((file (file-name-concat base "one" "two" "file.txt")))
+      (cl-flet ((advice (&rest args)
+                  (write-region "" nil file nil nil nil 'excl)))
+        (unwind-protect
+            (progn
+              ;; Simulate that someone else has created the file after
+              ;; checking for its existence.
+              (advice-add #'make-directory :after #'advice)
+              (should-error (make-empty-file file :parents)
+                            :type 'file-already-exists))
+          (advice-remove #'make-directory #'advice))))))
+
 (provide 'files-tests)
 ;;; files-tests.el ends here
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.