source: trunk/athena/bin/saferm/saferm.c @ 24867

Revision 24867, 10.8 KB checked in by geofft, 13 years ago (diff)
Add largefile support in a more portable way
Line 
1/* Copyright 1998 by the Massachusetts Institute of Technology.
2 *
3 * Permission to use, copy, modify, and distribute this
4 * software and its documentation for any purpose and without
5 * fee is hereby granted, provided that the above copyright
6 * notice appear in all copies and that both that copyright
7 * notice and this permission notice appear in supporting
8 * documentation, and that the name of M.I.T. not be used in
9 * advertising or publicity pertaining to distribution of the
10 * software without specific, written prior permission.
11 * M.I.T. makes no representations about the suitability of
12 * this software for any purpose.  It is provided "as is"
13 * without express or implied warranty.
14 */
15
16/*
17 * saferm
18 *
19 * Running as root, remove and optionally zero files in /tmp that
20 * may be owned by anybody, avoiding any possible race conditions
21 * resulting in security problems.
22 */
23
24static const char rcsid[] = "$Id: saferm.c,v 1.6 1999-09-21 20:09:11 danw Exp $";
25
26#include <sys/types.h>
27#include <sys/stat.h>
28#include <fcntl.h>
29#include <string.h>
30#include <stdio.h>
31#include <unistd.h>
32#include <stdlib.h>
33#include <errno.h>
34
35char *program_name;
36
37#ifndef MIN
38#define MIN(a, b) ((a) < (b) ? (a) : (b))
39#endif
40
41static int safe_open(char *filename, struct stat *file_stat);
42static int zero(int file, struct stat *file_stat);
43static int safe_to_unlink(char *name);
44static char *safe_cd(char *path);
45static int safe_rm(char *path, int zero_file);
46static int safe_rmdir(char *path, int quietflag);
47
48int main(int argc, char **argv)
49{
50  int c, zeroflag = 0, dirflag = 0, quietflag = 0, errflag = 0;
51  int dir, status = 0;
52
53  /* Set up our program name. */
54  if (argv[0] != NULL)
55    {
56      program_name = strrchr(argv[0], '/');
57      if (program_name != NULL)
58        program_name++;
59      else
60        program_name = argv[0];
61    }
62  else
63    program_name = "saferm";
64
65  while ((c = getopt(argc, argv, "dqz")) != EOF)
66    {
67      switch(c)
68        {
69        case 'd':
70          dirflag = 1;
71          break;
72        case 'q':
73          quietflag = 1;
74          break;
75        case 'z':
76          zeroflag = 1;
77          break;
78        case '?':
79          errflag = 1;
80          break;
81        }
82    }
83
84  if (errflag || optind == argc || (dirflag && zeroflag))
85    {
86      fprintf(stderr, "usage: %s [-q] [-d|-z] filename [filename ...]\n",
87              program_name);
88      exit(1);
89    }
90
91  /* Remember our current directory in case we are being passed
92   * relative paths.
93   */
94  dir = open(".", O_RDONLY);
95  if (dir == -1)
96    {
97      fprintf(stderr, "%s: error opening current directory: %s\n",
98              program_name, strerror(errno));
99      exit(1);
100    }
101
102  while (optind < argc)
103    {
104      /* If we're removing a relative path, be sure to cd to where
105       * we started.
106       */
107      if (argv[optind][0] != '/')
108        {
109          if (fchdir(dir) == -1)
110            {
111              fprintf(stderr, "%s: error cding to initial directory: %s\n",
112                      program_name, strerror(errno));
113              exit(1);
114            }
115        }
116
117      if (dirflag && safe_rmdir(argv[optind], quietflag) == -1)
118        status = 1;
119      else if (!dirflag && safe_rm(argv[optind], zeroflag) == -1)
120        status = 1;
121
122      optind++;
123    }
124
125  exit(status);
126}
127
128/* Safely open a file with intent to zero. */
129static int safe_open(char *filename, struct stat *file_stat)
130{
131  int file;
132  struct stat name_stat;
133
134  /* Try to open the file we think we want to zero. */
135  file = open(filename, O_WRONLY);
136  if (file == -1)
137    {
138      fprintf(stderr, "%s: open(%s) for writing failed: %s\n",
139              program_name, filename, strerror(errno));
140      return -1;
141    }
142
143  /* Stat the file we actually opened. */
144  if (fstat(file, file_stat) == -1)
145    {
146      fprintf(stderr, "%s: fstat(%s) failed: %s\n",
147              program_name, filename, strerror(errno));
148      close(file);
149      return -1;
150    }
151
152  /* Make sure it's a regular file. */
153  if (!S_ISREG(file_stat->st_mode))
154    {
155      fprintf(stderr, "%s: %s is %s\n",
156              program_name, filename,
157              (S_ISDIR(file_stat->st_mode)
158               ? "a directory"
159               : "not a regular file"));
160      close(file);
161      return -1;
162    }
163
164  /* Stat the file we think we want to zero. */
165  if (lstat(filename, &name_stat) == -1)
166    {
167      fprintf(stderr, "%s: lstat(%s) failed: %s\n",
168              program_name, filename, strerror(errno));
169      close(file);
170      return -1;
171    }
172
173  /* We must be sure that we are not zeroing a hard link or symbolic
174   * link to something like the password file. To do this, we must
175   * verify that the link count on the file we zero is equal to 1, and
176   * that we did not follow a symbolic link to reach that file.
177   *
178   * Simply lstat()ing the file before or after we open it is subject to
179   * lose in a race condition, as there is no way to know that the file we
180   * opened is the same file as the one we lstat()ed. Simply fstat()ing
181   * the file we actually open can't tell us whether we followed a symbolic
182   * link to get there, and also can't give us a reliable link count also
183   * due to race conditions.
184   *
185   * But if we both lstat() and fstat() and compare the inode numbers from
186   * the two cases, we can verify that the file we have open, at the time
187   * of our lstat(), existed in a unique location identified by the pathname
188   * we used to open it.
189   *
190   * So first, verify that our two stats got the same file.
191   */
192  if (file_stat->st_dev != name_stat.st_dev
193      || file_stat->st_ino != name_stat.st_ino)
194    {
195      fprintf(stderr, "%s: %s and file opened are not identical\n",
196              program_name, filename);
197      close(file);
198      return -1;
199    }
200
201  /* Now verify that the link count is 1. Note that file_stat may not be
202   * used here, otherwise we could be beaten by a race condition:
203   *
204   *   1. The attacker creates /tmp/passwd as a symlink to /etc/passwd.
205   *   2. We open /tmp/passwd, which follows the symlink, and so we have
206   *      fstat()ed /etc/passwd. The link count as of this stat is 1.
207   *   3. The attacker removes the /tmp/passwd symlink and replaces it
208   *      with a hard link to /etc/passwd.
209   *   4. We lstat() /tmp/passwd. The link count now is 2.
210   */
211  if (name_stat.st_nlink != 1)
212    {
213      fprintf(stderr, "%s: multiple links for file %s, not zeroing\n",
214              program_name, filename);
215      close(file);
216      return -1;
217    }
218
219  /* Now we know it is safe to muck with the file we opened. */
220  return file;
221}
222
223/* Zero the file represented by the passed file descriptor. Assumes
224 * that the file pointer is already at the beginning of the file, and
225 * file_stat points to a stat of the file.
226 */
227static int zero(int file, struct stat *file_stat)
228{
229  char zeroes[BUFSIZ];
230  off_t written = 0;
231  ssize_t status;
232
233  memset(zeroes, 0, BUFSIZ);
234
235  while (written < file_stat->st_size)
236    {
237      status = write(file, zeroes, MIN(BUFSIZ, file_stat->st_size - written));
238      if (status == -1)
239        {
240          fprintf(stderr, "%s: write() failed: %s\n",
241                  program_name, strerror(errno));
242          return -1;
243        }
244      written += status;
245    }
246
247  fsync(file);
248  return 0;
249}
250
251/* Return non-zero if it is safe to unlink NAME. */
252static int safe_to_unlink(char *name)
253{
254  struct stat st;
255 
256  if (lstat(name, &st) < 0)
257    {
258      fprintf(stderr, "%s: %s: %s\n", program_name, name, strerror(errno));
259      return 0;
260    }
261 
262  if (S_ISDIR(st.st_mode))
263    {
264      fprintf(stderr, "%s: %s is a directory\n", program_name, name);
265      return 0;
266    }
267
268  return 1;
269}
270
271/* path is the full pathname to a file. We cd into the directory
272 * containing the file, following no symbolic links. A pointer to just
273 * the filename is returned on success (a substring of path), and NULL
274 * is returned on failure.
275 */
276static char *safe_cd(char *path)
277{
278  char *start_ptr, *end_ptr, *path_buf;
279  struct stat above, below;
280
281  if (path == NULL || *path == 0)
282    return NULL;
283
284  /* Make a copy of the path to be removed, since we're going to modify it. */
285  path_buf = malloc(strlen(path) + 1);
286  if (path_buf == NULL)
287    {
288      fprintf(stderr, "%s: out of memory\n", program_name);
289      return NULL;
290    }
291  strcpy(path_buf, path);
292
293  /* Step through each path component and cd into it, verifying
294   * that we wind up where we expect to.
295   *
296   * strchr()ing from start_ptr + 1 handles the absolute path case
297   * as well as beginning to address the double-/ case.
298   */
299  start_ptr = path_buf;
300  while ((end_ptr = strchr(start_ptr + 1, '/')))
301    {
302      /* Include any remaining extra /'s at the end of this component. */
303      while (*(end_ptr + 1) == '/')
304        end_ptr++;
305
306      *end_ptr = 0;
307
308      if (lstat(start_ptr, &above) == -1)
309        {
310          fprintf(stderr, "%s: lstat of \"%s\" failed: %s\n", program_name,
311                  path_buf, strerror(errno));
312          free(path_buf);
313          return NULL;
314        }
315
316      if (!S_ISDIR(above.st_mode))
317        {
318          fprintf(stderr, "%s: final component of \"%s\" is not a directory\n",
319                  program_name, path_buf);
320          free(path_buf);
321          return NULL;
322        }
323
324      if (chdir(start_ptr) == -1)
325        {
326          fprintf(stderr, "%s: chdir to \"%s\" failed: %s\n", program_name,
327                  path_buf, strerror(errno));
328          free(path_buf);
329          return NULL;
330        }
331
332      if (stat(".", &below) == -1)
333        {
334          fprintf(stderr, "%s: stat of \"%s\" failed: %s\n", program_name,
335                  path_buf, strerror(errno));
336          free(path_buf);
337          return NULL;
338        }
339
340      if (above.st_dev != below.st_dev || above.st_ino != below.st_ino)
341        {
342          fprintf(stderr, "%s: final component of \"%s\" changed during "
343                  "traversal\n", program_name, path_buf);
344          free(path_buf);
345          return NULL;
346        }
347
348      /* Advance to next component. */
349      start_ptr = end_ptr + 1;
350
351      /* Restore the / we nulled for diagnostics. */
352      *end_ptr = '/';
353    }
354
355  free(path_buf);
356  return (start_ptr - path_buf) + path;
357}
358
359/* Safely remove and optionally zero a file. -1 is returned for any
360 * level of failure, such as zeroing failed but unlinking succeeded.
361 */
362static int safe_rm(char *path, int zero_file)
363{
364  int file, status = 0;
365  struct stat file_stat;
366  char *name;
367
368  name = safe_cd(path);
369  if (name == NULL)
370    return -1;
371
372  /* Do the unlink before the actual zeroing so that the appearance
373   * is atomic.
374   */
375
376  if (zero_file)
377    {
378      file = safe_open(name, &file_stat);
379      if (file == -1)
380        return -1;
381    }
382
383  if (safe_to_unlink(name) && unlink(name) == -1)
384    {
385      fprintf(stderr, "%s: error unlinking %s: %s\n", program_name,
386              path, strerror(errno));
387      status = -1;
388    }
389
390  if (zero_file && file != -1)
391    {
392      if (zero(file, &file_stat) == -1)
393        status = -1;
394      close(file);
395    }
396
397  return status;
398}
399
400/* Safely remove a directory. -1 is returned for any level of failure. */
401static int safe_rmdir(char *path, int quietflag)
402{
403  char *name;
404
405  name = safe_cd(path);
406  if (name == NULL)
407    return -1;
408
409  if (rmdir(name) == -1)
410    {
411      /* System V systems return EEXIST when a directory isn't empty
412       * but hack the rmdir command to display a better error message.
413       * Well, we can do that too.
414       */
415      if (errno == EEXIST)
416        errno = ENOTEMPTY;
417
418      if (!quietflag || errno != ENOTEMPTY)
419        {
420          fprintf(stderr, "%s: error removing directory %s: %s\n",
421                  program_name, path, strerror(errno));
422        }
423      return -1;
424    }
425
426  return 0;
427}
Note: See TracBrowser for help on using the repository browser.