diff options
Diffstat (limited to 'src/shred.c')
-rw-r--r-- | src/shred.c | 320 |
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)); |