Subject: Re: 24.4; Data lost when modifying file on filesystem with no space left on device / disc full

From: Paul Eggert <[email-masked]>
Date: Fri, 29 May 2015 23:24:56 -0700

Thank you for reporting this, and for the recipe for reproducing this important 
bug in Emacs.  I installed the attached patches into the GNU Emacs master to fix 
the problem.  The 1st patch doesn't actually fix the bug, but it fixes some 
related ones.  The 2nd patch fixes the bug -- at least, it worked for me on your 
test case.  I generated the 2nd patch with "diff -b" so its indenting won't 
match the source code; you can pick up the full gory details in savannah git 
master, around commit ab27722721afca4647a7eec0933ac9209e0eac30.
>From 2b51de993a5444177f537f22a5de926e056b6add Mon Sep 17 00:00:00 2001
From: Paul Eggert <[email-masked]>
Date: Fri, 29 May 2015 22:55:25 -0700
Subject: [PATCH 1/2] copy-file now truncates output after writing

* src/fileio.c (Fcopy_file): Truncate output after writing rather
than before.  This is more likely to work than truncation before
writing, if the file system is out of space or the user is over
disk quota (Bug#20595).  Also, check for read errors.
---
 src/fileio.c | 30 +++++++++++++++++++++++-------
 1 file changed, 23 insertions(+), 7 deletions(-)

diff --git a/src/fileio.c b/src/fileio.c
index 796f08d..a969d3b 100644
--- a/src/fileio.c
+++ b/src/fileio.c
@@ -1871,8 +1871,6 @@ permissions.  */)
   bool already_exists = false;
   mode_t new_mask;
   int ifd, ofd;
-  int n;
-  char buf[16 * 1024];
   struct stat st;
 #endif
 
@@ -1974,6 +1972,8 @@ permissions.  */)
 
   record_unwind_protect_int (close_file_unwind, ofd);
 
+  off_t oldsize = 0, newsize = 0;
+
   if (already_exists)
     {
       struct stat out_st;
@@ -1982,15 +1982,31 @@ permissions.  */)
       if (st.st_dev == out_st.st_dev && st.st_ino == out_st.st_ino)
 	report_file_errno ("Input and output files are the same",
 			   list2 (file, newname), 0);
-      if (ftruncate (ofd, 0) != 0)
-	report_file_error ("Truncating output file", newname);
+      if (S_ISREG (out_st.st_mode))
+	oldsize = out_st.st_size;
     }
 
   immediate_quit = 1;
   QUIT;
-  while ((n = emacs_read (ifd, buf, sizeof buf)) > 0)
-    if (emacs_write_sig (ofd, buf, n) != n)
-      report_file_error ("Write error", newname);
+  while (true)
+    {
+      char buf[MAX_ALLOCA];
+      ptrdiff_t n = emacs_read (ifd, buf, sizeof buf);
+      if (n < 0)
+	report_file_error ("Read error", file);
+      if (n == 0)
+	break;
+      if (emacs_write_sig (ofd, buf, n) != n)
+	report_file_error ("Write error", newname);
+      newsize += n;
+    }
+
+  /* Truncate any existing output file after writing the data.  This
+     is more likely to work than truncation before writing, if the
+     file system is out of space or the user is over disk quota.  */
+  if (newsize < oldsize && ftruncate (ofd, newsize) != 0)
+    report_file_error ("Truncating output file", newname);
+
   immediate_quit = 0;
 
 #ifndef MSDOS
-- 
2.1.0

diff --git a/lisp/files.el b/lisp/files.el
index 16ac956..6939f2b 100644
--- a/lisp/files.el
+++ b/lisp/files.el
@@ -4077,80 +4077,75 @@ on the original file; this means that the caller, after saving
 the buffer, should change the extended attributes of the new file
 to agree with the old attributes.
 BACKUPNAME is the backup file name, which is the old file renamed."
-  (if (and make-backup-files (not backup-inhibited)
-	   (not buffer-backed-up)
-	   (file-exists-p buffer-file-name)
-	   (memq (aref (elt (file-attributes buffer-file-name) 8) 0)
-		 '(?- ?l)))
-      (let ((real-file-name buffer-file-name)
-	    backup-info backupname targets setmodes)
+  (when (and make-backup-files (not backup-inhibited) (not buffer-backed-up))
+    (let ((attributes (file-attributes buffer-file-name)))
+      (when (and attributes (memq (aref (elt attributes 8) 0) '(?- ?l)))
 	;; If specified name is a symbolic link, chase it to the target.
-	;; Thus we make the backups in the directory where the real file is.
-	(setq real-file-name (file-chase-links real-file-name))
-	(setq backup-info (find-backup-file-name real-file-name)
-	      backupname (car backup-info)
-	      targets (cdr backup-info))
-	;; (if (file-directory-p buffer-file-name)
-	;;     (error "Cannot save buffer in directory %s" buffer-file-name))
-	(if backup-info
-	    (condition-case ()
-		(let ((delete-old-versions
+	;; This makes backups in the directory where the real file is.
+	(let* ((real-file-name (file-chase-links buffer-file-name))
+	       (backup-info (find-backup-file-name real-file-name)))
+	  (when backup-info
+	    (let* ((backupname (car backup-info))
+		   (targets (cdr backup-info))
+		   (old-versions
 		    ;; If have old versions to maybe delete,
 		    ;; ask the user to confirm now, before doing anything.
 		    ;; But don't actually delete til later.
 		    (and targets
-			    (or (eq delete-old-versions t) (eq delete-old-versions nil))
+			 (booleanp delete-old-versions)
 			 (or delete-old-versions
-				(y-or-n-p (format "Delete excess backup versions of %s? "
-						  real-file-name)))))
+			     (y-or-n-p
+			      (format "Delete excess backup versions of %s? "
+				      real-file-name)))
+			 targets))
 		   (modes (file-modes buffer-file-name))
 		   (extended-attributes
-		       (file-extended-attributes buffer-file-name)))
-		  ;; Actually write the back up file.
-		  (condition-case ()
-		      (if (or file-precious-flag
-    ;			      (file-symlink-p buffer-file-name)
-			      backup-by-copying
+		    (file-extended-attributes buffer-file-name))
+		   (copy-when-priv-mismatch
+		    backup-by-copying-when-privileged-mismatch)
+		   (make-copy
+		    (or file-precious-flag backup-by-copying
 			;; Don't rename a suid or sgid file.
 			(and modes (< 0 (logand modes #o6000)))
-			      (not (file-writable-p (file-name-directory real-file-name)))
+			(not (file-writable-p
+			      (file-name-directory real-file-name)))
 			(and backup-by-copying-when-linked
-				   (> (file-nlinks real-file-name) 1))
-			      (and (or backup-by-copying-when-mismatch
-				       (integerp backup-by-copying-when-privileged-mismatch))
-				   (let ((attr (file-attributes real-file-name)))
+			     (< 1 (file-nlinks real-file-name)))
 			(and (or backup-by-copying-when-mismatch
-					      (and (integerp (nth 2 attr))
-						   (integerp backup-by-copying-when-privileged-mismatch)
-						   (<= (nth 2 attr) backup-by-copying-when-privileged-mismatch)))
-					  (not (file-ownership-preserved-p
-						real-file-name t))))))
-			  (backup-buffer-copy real-file-name
-					      backupname modes
-					      extended-attributes)
+				 (and (integerp copy-when-priv-mismatch)
+				      (let ((attr (file-attributes
+						   real-file-name
+						   'integer)))
+					(<= (nth 2 attr)
+					    copy-when-priv-mismatch))))
+			     (not (file-ownership-preserved-p real-file-name
+							      t)))))
+		   setmodes)
+	      (condition-case ()
+		  (progn
+		    ;; Actually make the backup file.
+		    (if make-copy
+			(backup-buffer-copy real-file-name backupname
+					    modes extended-attributes)
 		      ;; rename-file should delete old backup.
 		      (rename-file real-file-name backupname t)
 		      (setq setmodes (list modes extended-attributes
 					   backupname)))
-		    (file-error
-		     ;; If trouble writing the backup, write it in
-		     ;; .emacs.d/%backup%.
+		    (setq buffer-backed-up t)
+		    ;; Now delete the old versions, if desired.
+		    (dolist (old-version old-versions)
+		      (delete-file old-version)))
+		(file-error nil))
+	      ;; If trouble writing the backup, write it in .emacs.d/%backup%.
+	      (when (not buffer-backed-up)
 		(setq backupname (locate-user-emacs-file "%backup%~"))
 		(message "Cannot write backup file; backing up in %s"
 			 backupname)
 		(sleep-for 1)
 		(backup-buffer-copy real-file-name backupname
-					 modes extended-attributes)))
-		  (setq buffer-backed-up t)
-		  ;; Now delete the old versions, if desired.
-		  (if delete-old-versions
-		      (while targets
-			(condition-case ()
-			    (delete-file (car targets))
-			  (file-error nil))
-			(setq targets (cdr targets))))
-		  setmodes)
-	    (file-error nil))))))
+				    modes extended-attributes)
+		(setq buffer-backed-up t))
+	      setmodes)))))))
 
 (defun backup-buffer-copy (from-name to-name modes extended-attributes)
   ;; Create temp files with strict access rights.  It's easy to