summaryrefslogtreecommitdiff
path: root/src/shred.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/shred.c')
-rw-r--r--src/shred.c320
1 files changed, 218 insertions, 102 deletions
diff --git a/src/shred.c b/src/shred.c
index 9b869cdc..bd88e383 100644
--- a/src/shred.c
+++ b/src/shred.c
@@ -1,6 +1,6 @@
/* shred.c - overwrite files and devices to make it harder to recover data
- Copyright (C) 1999-2013 Free Software Foundation, Inc.
+ Copyright (C) 1999-2014 Free Software Foundation, Inc.
Copyright (C) 1997, 1998, 1999 Colin Plumb.
This program is free software: you can redistribute it and/or modify
@@ -80,8 +80,12 @@
#include <assert.h>
#include <setjmp.h>
#include <sys/types.h>
+#ifdef __linux__
+# include <sys/mtio.h>
+#endif
#include "system.h"
+#include "argmatch.h"
#include "xstrtol.h"
#include "error.h"
#include "fcntl--.h"
@@ -104,12 +108,30 @@ enum { SECTOR_SIZE = 512 };
enum { SECTOR_MASK = SECTOR_SIZE - 1 };
verify (0 < SECTOR_SIZE && (SECTOR_SIZE & SECTOR_MASK) == 0);
+enum remove_method
+{
+ remove_none = 0, /* the default: only wipe data. */
+ remove_unlink, /* don't obfuscate name, just unlink. */
+ remove_wipe, /* obfuscate name before unlink. */
+ remove_wipesync /* obfuscate name, syncing each byte, before unlink. */
+};
+
+static char const *const remove_args[] =
+{
+ "unlink", "wipe", "wipesync", NULL
+};
+
+static enum remove_method const remove_methods[] =
+{
+ remove_unlink, remove_wipe, remove_wipesync
+};
+
struct Options
{
bool force; /* -f flag: chmod files if necessary */
size_t n_iterations; /* -n flag: Number of iterations */
off_t size; /* -s flag: size of file */
- bool remove_file; /* -u flag: remove file after shredding */
+ enum remove_method remove_file; /* -u flag: remove file after shredding */
bool verbose; /* -v flag: Print progress */
bool exact; /* -x flag: Do not round up file size */
bool zero_fill; /* -z flag: Add a final zero pass */
@@ -129,7 +151,7 @@ static struct option const long_opts[] =
{"iterations", required_argument, NULL, 'n'},
{"size", required_argument, NULL, 's'},
{"random-source", required_argument, NULL, RANDOM_SOURCE_OPTION},
- {"remove", no_argument, NULL, 'u'},
+ {"remove", optional_argument, NULL, 'u'},
{"verbose", no_argument, NULL, 'v'},
{"zero", no_argument, NULL, 'z'},
{GETOPT_HELP_OPTION_DECL},
@@ -159,7 +181,7 @@ for even very expensive hardware probing to recover the data.\n\
-s, --size=N shred this many bytes (suffixes like K, M, G accepted)\n\
"), DEFAULT_PASSES);
fputs (_("\
- -u, --remove truncate and remove file after overwriting\n\
+ -u, --remove[=HOW] truncate and remove file after overwriting; See below\n\
-v, --verbose show progress\n\
-x, --exact do not round file sizes up to the next full block;\n\
this is the default for non-regular files\n\
@@ -173,8 +195,12 @@ If FILE is -, shred standard output.\n\
\n\
Delete FILE(s) if --remove (-u) is specified. The default is not to remove\n\
the files because it is common to operate on device files like /dev/hda,\n\
-and those files usually should not be removed. When operating on regular\n\
-files, most people use the --remove option.\n\
+and those files usually should not be removed.\n\
+The optional HOW parameter indicates how to remove a directory entry:\n\
+'unlink' => use a standard unlink call.\n\
+'wipe' => also first obfuscate bytes in the name.\n\
+'wipesync' => also sync each obfuscated byte to disk.\n\
+The default mode is 'wipesync', but note it can be expensive.\n\
\n\
"), stdout);
fputs (_("\
@@ -222,6 +248,25 @@ to be recovered later.\n\
exit (status);
}
+/*
+ * Determine if pattern type is periodic or not.
+ */
+static bool
+periodic_pattern (int type)
+{
+ if (type <= 0)
+ return false;
+
+ unsigned char r[3];
+ unsigned int bits = type & 0xfff;
+
+ bits |= bits << 12;
+ r[0] = (bits >> 4) & 255;
+ r[1] = (bits >> 8) & 255;
+ r[2] = bits & 255;
+
+ return (r[0] != r[1]) || (r[0] != r[2]);
+}
/*
* Fill a buffer with a fixed pattern.
@@ -337,19 +382,43 @@ direct_mode (int fd, bool enable)
#endif
}
+/* Rewind FD; its status is ST. */
+static bool
+dorewind (int fd, struct stat const *st)
+{
+ if (S_ISCHR (st->st_mode))
+ {
+#ifdef __linux__
+ /* In the Linux kernel, lseek does not work on tape devices; it
+ returns a randomish value instead. Try the low-level tape
+ rewind operation first. */
+ struct mtop op;
+ op.mt_op = MTREW;
+ op.mt_count = 1;
+ if (ioctl (fd, MTIOCTOP, &op) == 0)
+ return true;
+#endif
+ }
+ off_t offset = lseek (fd, 0, SEEK_SET);
+ if (0 < offset)
+ errno = EINVAL;
+ return offset == 0;
+}
+
/*
- * 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.
*/
static int
-dopass (int fd, char const *qname, off_t *sizep, int type,
- struct randread_source *s, unsigned long int k, unsigned long int n)
+dopass (int fd, struct stat const *st, char const *qname, off_t *sizep,
+ int type, struct randread_source *s,
+ unsigned long int k, unsigned long int n)
{
off_t size = *sizep;
off_t offset; /* Current file posiiton */
@@ -359,37 +428,47 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
size_t soff; /* Offset into buffer for next write */
ssize_t ssize; /* Return value from write */
- /* Fill pattern buffer. Aligning it to a 32-bit boundary speeds up randread
- in some cases. */
- typedef uint32_t fill_pattern_buffer[3 * 1024];
- union
- {
- fill_pattern_buffer buffer;
- char c[sizeof (fill_pattern_buffer)];
- unsigned char u[sizeof (fill_pattern_buffer)];
- } r;
-
- off_t sizeof_r = sizeof r;
+ /* 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)
+#define NONPERIODIC_OUTPUT_SIZE (64 * 1024)
+ verify (PERIODIC_OUTPUT_SIZE % 3 == 0);
+ size_t output_size = periodic_pattern (type)
+ ? PERIODIC_OUTPUT_SIZE : NONPERIODIC_OUTPUT_SIZE;
+#define PAGE_ALIGN_SLOP (page_size - 1) /* So directio works */
+#define FILLPATTERN_SIZE (((output_size + 2) / 3) * 3) /* Multiple of 3 */
+#define PATTERNBUF_SIZE (PAGE_ALIGN_SLOP + FILLPATTERN_SIZE)
+ void *fill_pattern_mem = xmalloc (PATTERNBUF_SIZE);
+ unsigned char *pbuf = ptr_align (fill_pattern_mem, page_size);
+
char pass_string[PASS_NAME_SIZE]; /* Name of current pass */
bool write_error = false;
- bool first_write = true;
+ bool other_error = false;
/* Printable previous offset into the file */
char previous_offset_buf[LONGEST_HUMAN_READABLE + 1];
char const *previous_human_offset IF_LINT ( = 0);
- if (lseek (fd, 0, SEEK_SET) == -1)
+ /* 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);
- return -1;
+ other_error = true;
+ goto free_pattern_mem;
}
/* Constant fill patterns need only be set up once. */
if (type >= 0)
{
- lim = (0 <= size && size < sizeof_r ? size : sizeof_r);
- fillpattern (type, r.u, lim);
- passname (r.u, pass_string);
+ lim = (0 <= size && size < FILLPATTERN_SIZE ? size : FILLPATTERN_SIZE);
+ fillpattern (type, pbuf, lim);
+ passname (pbuf, pass_string);
}
else
{
@@ -408,8 +487,8 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
while (true)
{
/* How much to write this time? */
- lim = sizeof r;
- if (0 <= size && size - offset < sizeof_r)
+ lim = output_size;
+ if (0 <= size && size - offset < output_size)
{
if (size < offset)
break;
@@ -418,11 +497,11 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
break;
}
if (type < 0)
- randread (s, &r, lim);
+ randread (s, pbuf, lim);
/* Loop to retry partial writes. */
- for (soff = 0; soff < lim; soff += ssize, first_write = false)
+ for (soff = 0; soff < lim; soff += ssize)
{
- ssize = write (fd, r.c + soff, lim - soff);
+ ssize = write (fd, pbuf + soff, lim - soff);
if (ssize <= 0)
{
if (size < 0 && (ssize == 0 || errno == ENOSPC))
@@ -436,17 +515,15 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
int errnum = errno;
char buf[INT_BUFSIZE_BOUND (uintmax_t)];
- /* If the first write of the first pass for a given file
- has just failed with EINVAL, turn off direct mode I/O
- and try again. This works around a bug in Linux kernel
- 2.4 whereby opening with O_DIRECT would succeed for some
- file system types (e.g., ext3), but any attempt to
- access a file through the resulting descriptor would
- fail with EINVAL. */
- if (k == 1 && first_write && errno == EINVAL)
+ /* Retry without direct I/O since this may not be supported
+ 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 (! try_without_directio && errno == EINVAL)
{
direct_mode (fd, false);
ssize = 0;
+ try_without_directio = true;
continue;
}
error (0, errnum, _("%s: error writing at offset %s"),
@@ -455,8 +532,10 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
/* 'shred' is often used on bad media, before throwing it
out. Thus, it shouldn't give up on bad blocks. This
code works because lim is always a multiple of
- SECTOR_SIZE, except at the end. */
- verify (sizeof r % SECTOR_SIZE == 0);
+ SECTOR_SIZE, except at the end. This size constraint
+ also enables direct I/O on some (file) systems. */
+ verify (PERIODIC_OUTPUT_SIZE % SECTOR_SIZE == 0);
+ verify (NONPERIODIC_OUTPUT_SIZE % SECTOR_SIZE == 0);
if (errnum == EIO && 0 <= size && (soff | SECTOR_MASK) < lim)
{
size_t soff1 = (soff | SECTOR_MASK) + 1;
@@ -469,7 +548,8 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
}
error (0, errno, _("%s: lseek failed"), qname);
}
- return -1;
+ other_error = true;
+ goto free_pattern_mem;
}
}
}
@@ -479,7 +559,8 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
if (offset > OFF_T_MAX - (off_t) soff)
{
error (0, 0, _("%s: file too large"), qname);
- return -1;
+ other_error = true;
+ goto free_pattern_mem;
}
offset += soff;
@@ -536,7 +617,10 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
if (dosync (fd, qname) != 0)
{
if (errno != EIO)
- return -1;
+ {
+ other_error = true;
+ goto free_pattern_mem;
+ }
write_error = true;
}
}
@@ -547,11 +631,18 @@ dopass (int fd, char const *qname, off_t *sizep, int type,
if (dosync (fd, qname) != 0)
{
if (errno != EIO)
- return -1;
+ {
+ other_error = true;
+ goto free_pattern_mem;
+ }
write_error = true;
}
- return write_error;
+free_pattern_mem:
+ memset (pbuf, 0, FILLPATTERN_SIZE);
+ free (fill_pattern_mem);
+
+ return other_error ? -1 : write_error;
}
/*
@@ -752,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;
@@ -778,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);
@@ -787,19 +882,28 @@ do_wipefd (int fd, char const *qname, struct randint_source *s,
size = flags->size;
if (size == -1)
{
- /* Accept a length of zero only if it's a regular file.
- For any other type of file, try to get the size another way. */
if (S_ISREG (st.st_mode))
{
size = st.st_size;
- if (size < 0)
+
+ if (! flags->exact)
{
- error (0, 0, _("%s: file has negative size"), qname);
- return false;
+ /* 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;
+ size += MIN (size_incr, OFF_T_MAX - size);
+ }
}
}
else
{
+ /* The behavior of lseek is unspecified, but in practice if
+ it returns a positive number that's the size of this
+ device. */
size = lseek (fd, 0, SEEK_END);
if (size <= 0)
{
@@ -808,63 +912,68 @@ do_wipefd (int fd, char const *qname, struct randint_source *s,
size = -1;
}
}
-
- /* Allow 'rounding up' only for regular files. */
- if (0 <= size && !(flags->exact) && S_ISREG (st.st_mode))
- {
- size += ST_BLKSIZE (st) - 1 - (size - 1) % ST_BLKSIZE (st);
-
- /* If in rounding up, we've just overflowed, use the maximum. */
- if (size < 0)
- size = TYPE_MAXIMUM (off_t);
- }
}
+ 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, 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, 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;
}
@@ -926,8 +1035,8 @@ incname (char *name, size_t len)
/*
* Repeatedly rename a file with shorter and shorter names,
- * to obliterate all traces of the file name on any system that
- * adds a trailing delimiter to on-disk file names and reuses
+ * to obliterate all traces of the file name (and length) on any system
+ * that adds a trailing delimiter to on-disk file names and reuses
* the same directory slot. Finally, unlink it.
* The passed-in filename is modified in place to the new filename.
* (Which is unlinked if this function succeeds, but is still present if
@@ -960,13 +1069,15 @@ wipename (char *oldname, char const *qoldname, struct Options const *flags)
char *qdir = xstrdup (quotearg_colon (dir));
bool first = true;
bool ok = true;
+ int dir_fd = -1;
- int dir_fd = open (dir, O_RDONLY | O_DIRECTORY | O_NOCTTY | O_NONBLOCK);
+ if (flags->remove_file == remove_wipesync)
+ dir_fd = open (dir, O_RDONLY | O_DIRECTORY | O_NOCTTY | O_NONBLOCK);
if (flags->verbose)
error (0, 0, _("%s: removing"), qoldname);
- while (len)
+ while ((flags->remove_file != remove_unlink) && len)
{
memset (base, nameset[0], len);
base[len] = 0;
@@ -1120,7 +1231,7 @@ main (int argc, char **argv)
{
uintmax_t tmp;
if (xstrtoumax (optarg, NULL, 10, &tmp, NULL) != LONGINT_OK
- || MIN (UINT32_MAX, SIZE_MAX / sizeof (int)) < tmp)
+ || MIN (ULONG_MAX, SIZE_MAX / sizeof (int)) <= tmp)
{
error (EXIT_FAILURE, 0, _("%s: invalid number of passes"),
quotearg_colon (optarg));
@@ -1136,14 +1247,19 @@ main (int argc, char **argv)
break;
case 'u':
- flags.remove_file = true;
+ if (optarg == NULL)
+ flags.remove_file = remove_wipesync;
+ else
+ flags.remove_file = XARGMATCH ("--remove", optarg,
+ remove_args, remove_methods);
break;
case 's':
{
uintmax_t tmp;
- if (xstrtoumax (optarg, NULL, 0, &tmp, "cbBkKMGTPEZY0")
- != LONGINT_OK)
+ if ((xstrtoumax (optarg, NULL, 0, &tmp, "cbBkKMGTPEZY0")
+ != LONGINT_OK)
+ || OFF_T_MAX < tmp)
{
error (EXIT_FAILURE, 0, _("%s: invalid file size"),
quotearg_colon (optarg));