]> git.ipfire.org Git - thirdparty/coreutils.git/commitdiff
shred: overwrite inode storage used by some file systems
authorPádraig Brady <P@draigBrady.com>
Fri, 4 Apr 2014 12:35:56 +0000 (13:35 +0100)
committerPádraig Brady <P@draigBrady.com>
Sat, 5 Apr 2014 00:56:07 +0000 (01:56 +0100)
* doc/coreutils.texi (shred invocation): Mention some reasons
why clearing slack space might be useful.
* src/shred.c (do_wipefd): Add initial writes for each pass
for small regular files in case the storage for those is
in the inode, and thus a larger write up to a block size would
bypass that.  Move the direct I/O control to...
(dopass): ... here so we can avoid enabling it for these small
initial writes.  It's better to retry direct I/O for each pass
anyway to handle the case where direct I/O is disabled for only
the last portion of a file when the size is not a multiple of
the block size.  Note we don't avoid the sync for the initial
write as it will be small but more importantly could be on a
different part of the disk and so worth doing independently
to ensure the write is not discarded.
* tests/misc/shred-exact.sh: Check some more direct I/O cases.
* NEWS: Mention the improvements.
The inode storage issue was mentioned by Paul Eggert.

NEWS
doc/coreutils.texi
src/shred.c
tests/misc/shred-exact.sh

diff --git a/NEWS b/NEWS
index c6451b289863e364503ecfa6fa66fc2165431498..06e78a7a99f0400f5cce7a32330128b0dc711147 100644 (file)
--- a/NEWS
+++ b/NEWS
@@ -58,7 +58,9 @@ GNU coreutils NEWS                                    -*- outline -*-
   in case the look-up within the chroot fails due to library conflicts etc.
 
   shred now supports multiple passes on GNU/Linux tape devices by rewinding
-  the tape before each pass.  Also redundant writes to empty files are avoided.
+  the tape before each pass, avoids redundant writes to empty files,
+  uses direct I/O for all passes where possible, and attempts to clear
+  inode storage used for small files on some file systems.
 
   split avoids unnecessary input buffering, immediately writing input to output
   which is significant with --filter or when writing to fifos or stdout etc.
index 75875d88c41f49ba7760e87df7873f6683040e40..6c49385e13c542ea3dc19cc59e078aefe9d9407e 100644 (file)
@@ -9634,8 +9634,9 @@ Display to standard error all status updates as sterilization proceeds.
 @opindex -x
 @opindex --exact
 By default, @command{shred} rounds the size of a regular file up to the next
-multiple of the file system block size to fully erase the last block
-of the file.
+multiple of the file system block size to fully erase the slack space in
+the last block of the file.  This space may contain portions of the current
+system memory on some systems for example.
 Use @option{--exact} to suppress that behavior.
 Thus, by default if you shred a 10-byte regular file on a system with 512-byte
 blocks, the resulting file will be 512 bytes long.  With this option,
index ed370513dd00bbf2a3c27d5de8f6cda446cecea6..607c6be159b51aa49faad993f4254f611da3e6eb 100644 (file)
@@ -406,12 +406,12 @@ dorewind (int fd, struct stat const *st)
 }
 
 /*
- * Do pass number k of n, writing "size" bytes of the given pattern "type"
- * to the file descriptor fd.   Qname, k and n are passed in only for verbose
- * progress message purposes.  If n == 0, no progress messages are printed.
+ * Do pass number K of N, writing *SIZEP bytes of the given pattern TYPE
+ * to the file descriptor FD.  K and N are passed in only for verbose
+ * progress message purposes.  If N == 0, no progress messages are printed.
  *
- * If *sizep == -1, the size is unknown, and it will be filled in as soon
- * as writing fails.
+ * If *SIZEP == -1, the size is unknown, and it will be filled in as soon
+ * as writing fails with ENOSPC.
  *
  * Return 1 on write error, -1 on other error, 0 on success.
  */
@@ -428,10 +428,6 @@ dopass (int fd, struct stat const *st, char const *qname, off_t *sizep,
   size_t soff;                 /* Offset into buffer for next write */
   ssize_t ssize;               /* Return value from write */
 
-  /* Do nothing for --size=0 or regular empty files.  */
-  if (size == 0)
-    return 0;
-
   /* Fill pattern buffer.  Aligning it to a page so we can do direct I/O.  */
   size_t page_size = getpagesize ();
 #define PERIODIC_OUTPUT_SIZE (60 * 1024)
@@ -448,12 +444,18 @@ dopass (int fd, struct stat const *st, char const *qname, off_t *sizep,
   char pass_string[PASS_NAME_SIZE];    /* Name of current pass */
   bool write_error = false;
   bool other_error = false;
-  bool tried_without_directio = false;
 
   /* Printable previous offset into the file */
   char previous_offset_buf[LONGEST_HUMAN_READABLE + 1];
   char const *previous_human_offset IF_LINT ( = 0);
 
+  /* As a performance tweak, avoid direct I/O for small sizes,
+     as it's just a performance rather then security consideration,
+     and direct I/O can often be unsupported for small non aligned sizes.  */
+  bool try_without_directio = 0 < size && size < output_size;
+  if (! try_without_directio)
+    direct_mode (fd, true);
+
   if (! dorewind (fd, st))
     {
       error (0, errno, _("%s: cannot rewind"), qname);
@@ -517,11 +519,11 @@ dopass (int fd, struct stat const *st, char const *qname, off_t *sizep,
                      at all on some (file) systems, or with the current size.
                      I.E. a specified --size that is not aligned, or when
                      dealing with slop at the end of a file with --exact.  */
-                  if (k == 1 && !tried_without_directio && errno == EINVAL)
+                  if (! try_without_directio && errno == EINVAL)
                     {
                       direct_mode (fd, false);
                       ssize = 0;
-                      tried_without_directio = true;
+                      try_without_directio = true;
                       continue;
                     }
                   error (0, errnum, _("%s: error writing at offset %s"),
@@ -841,13 +843,14 @@ do_wipefd (int fd, char const *qname, struct randint_source *s,
 {
   size_t i;
   struct stat st;
-  off_t size;                  /* Size to write, size to read */
-  unsigned long int n;         /* Number of passes for printing purposes */
+  off_t size;          /* Size to write, size to read */
+  off_t i_size = 0;    /* For small files, initial size to overwrite inode */
+  unsigned long int n; /* Number of passes for printing purposes */
   int *passarray;
   bool ok = true;
   struct randread_source *rs;
 
-  n = 0;               /* dopass takes n -- 0 to mean "don't print progress" */
+  n = 0;               /* dopass takes n == 0 to mean "don't print progress" */
   if (flags->verbose)
     n = flags->n_iterations + flags->zero_fill;
 
@@ -867,8 +870,11 @@ do_wipefd (int fd, char const *qname, struct randint_source *s,
       error (0, 0, _("%s: invalid file type"), qname);
       return false;
     }
-
-  direct_mode (fd, true);
+  else if (S_ISREG (st.st_mode) && st.st_size < 0)
+    {
+      error (0, 0, _("%s: file has negative size"), qname);
+      return false;
+    }
 
   /* Allocate pass array */
   passarray = xnmalloc (flags->n_iterations, sizeof *passarray);
@@ -879,23 +885,17 @@ do_wipefd (int fd, char const *qname, struct randint_source *s,
       if (S_ISREG (st.st_mode))
         {
           size = st.st_size;
-          if (size < 0)
-            {
-              error (0, 0, _("%s: file has negative size"), qname);
-              return false;
-            }
 
           if (! flags->exact)
             {
-              /* Round up to the nearest blocksize to clear slack space.  */
+              /* Round up to the nearest block size to clear slack space.  */
               off_t remainder = size % ST_BLKSIZE (st);
+              if (size && size < ST_BLKSIZE (st))
+                i_size = size;
               if (remainder != 0)
                 {
                   off_t size_incr = ST_BLKSIZE (st) - remainder;
-                  if (! INT_ADD_OVERFLOW (size, size_incr))
-                    size += size_incr;
-                  else
-                    size = OFF_T_MAX;
+                  size += MIN (size_incr, OFF_T_MAX - size);
                 }
             }
         }
@@ -913,53 +913,67 @@ do_wipefd (int fd, char const *qname, struct randint_source *s,
             }
         }
     }
+  else if (S_ISREG (st.st_mode)
+           && st.st_size < MIN (ST_BLKSIZE (st), size))
+    i_size = st.st_size;
 
   /* Schedule the passes in random order. */
   genpattern (passarray, flags->n_iterations, s);
 
   rs = randint_get_source (s);
 
-  /* Do the work */
-  for (i = 0; i < flags->n_iterations; i++)
+  while (true)
     {
-      int err = dopass (fd, &st, qname, &size, passarray[i], rs, i + 1, n);
-      if (err)
+      off_t pass_size;
+      unsigned long int pn = n;
+
+      if (i_size)
         {
-          if (err < 0)
-            {
-              memset (passarray, 0, flags->n_iterations * sizeof (int));
-              free (passarray);
-              return false;
-            }
-          ok = false;
+          pass_size = i_size;
+          i_size = 0;
+          pn = 0;
         }
-    }
-
-  memset (passarray, 0, flags->n_iterations * sizeof (int));
-  free (passarray);
+      else if (size)
+        {
+          pass_size = size;
+          size = 0;
+        }
+      /* TODO: consider handling tail packing by
+         writing the tail padding as a separate pass,
+         (that would not rewind).  */
+      else
+        break;
 
-  if (flags->zero_fill)
-    {
-      int err = dopass (fd, &st, qname, &size, 0, rs,
-                        flags->n_iterations + 1, n);
-      if (err)
+      for (i = 0; i < flags->n_iterations + flags->zero_fill; i++)
         {
-          if (err < 0)
-            return false;
-          ok = false;
+          int err = 0;
+          int type = i < flags->n_iterations ? passarray[i] : 0;
+
+          err = dopass (fd, &st, qname, &pass_size, type, rs, i + 1, pn);
+
+          if (err)
+            {
+              ok = false;
+              if (err < 0)
+                goto wipefd_out;
+            }
         }
     }
 
-  /* Okay, now deallocate the data.  The effect of ftruncate on
+  /* Now deallocate the data.  The effect of ftruncate on
      non-regular files is unspecified, so don't worry about any
      errors reported for them.  */
   if (flags->remove_file && ftruncate (fd, 0) != 0
       && S_ISREG (st.st_mode))
     {
       error (0, errno, _("%s: error truncating"), qname);
-      return false;
+      ok = false;
+      goto wipefd_out;
     }
 
+wipefd_out:
+  memset (passarray, 0, flags->n_iterations * sizeof (int));
+  free (passarray);
   return ok;
 }
 
index 5f2848e1d47a5d86cbd6eb0c017335d6ff11b011..5434229c43fe567858ea751affed4f7183380c88 100755 (executable)
@@ -40,6 +40,10 @@ done
 # (i.e. we want to test failed writes not at the start).
 truncate -s1MiB file.slop || framework_failure_
 truncate -s+1 file.slop || framework_failure_
-shred --exact -n1 file.slop || fail=1
+shred --exact -n2 file.slop || fail=1
+
+# make sure direct I/O is handled appropriately at start of file
+truncate -s1 file.slop || framework_failure_
+shred --exact -n2 file.slop || fail=1
 
 Exit $fail