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

Revision 13609, 10.8 KB checked in by danw, 25 years ago (diff)
warnsify
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  int written = 0, status;
231
232  memset(zeroes, 0, BUFSIZ);
233
234  while (written < file_stat->st_size)
235    {
236      status = write(file, zeroes, MIN(BUFSIZ, file_stat->st_size - written));
237      if (status == -1)
238        {
239          fprintf(stderr, "%s: write() failed: %s\n",
240                  program_name, strerror(errno));
241          return -1;
242        }
243      written += status;
244    }
245
246  fsync(file);
247  return 0;
248}
249
250/* Return non-zero if it is safe to unlink NAME. */
251static int safe_to_unlink(char *name)
252{
253  struct stat st;
254 
255  if (lstat(name, &st) < 0)
256    {
257      fprintf(stderr, "%s: %s: %s\n", program_name, name, strerror(errno));
258      return 0;
259    }
260 
261  if (S_ISDIR(st.st_mode))
262    {
263      fprintf(stderr, "%s: %s is a directory\n", program_name, name);
264      return 0;
265    }
266
267  return 1;
268}
269
270/* path is the full pathname to a file. We cd into the directory
271 * containing the file, following no symbolic links. A pointer to just
272 * the filename is returned on success (a substring of path), and NULL
273 * is returned on failure.
274 */
275static char *safe_cd(char *path)
276{
277  char *start_ptr, *end_ptr, *path_buf;
278  struct stat above, below;
279
280  if (path == NULL || *path == 0)
281    return NULL;
282
283  /* Make a copy of the path to be removed, since we're going to modify it. */
284  path_buf = malloc(strlen(path) + 1);
285  if (path_buf == NULL)
286    {
287      fprintf(stderr, "%s: out of memory\n", program_name);
288      return NULL;
289    }
290  strcpy(path_buf, path);
291
292  /* Step through each path component and cd into it, verifying
293   * that we wind up where we expect to.
294   *
295   * strchr()ing from start_ptr + 1 handles the absolute path case
296   * as well as beginning to address the double-/ case.
297   */
298  start_ptr = path_buf;
299  while ((end_ptr = strchr(start_ptr + 1, '/')))
300    {
301      /* Include any remaining extra /'s at the end of this component. */
302      while (*(end_ptr + 1) == '/')
303        end_ptr++;
304
305      *end_ptr = 0;
306
307      if (lstat(start_ptr, &above) == -1)
308        {
309          fprintf(stderr, "%s: lstat of \"%s\" failed: %s\n", program_name,
310                  path_buf, strerror(errno));
311          free(path_buf);
312          return NULL;
313        }
314
315      if (!S_ISDIR(above.st_mode))
316        {
317          fprintf(stderr, "%s: final component of \"%s\" is not a directory\n",
318                  program_name, path_buf);
319          free(path_buf);
320          return NULL;
321        }
322
323      if (chdir(start_ptr) == -1)
324        {
325          fprintf(stderr, "%s: chdir to \"%s\" failed: %s\n", program_name,
326                  path_buf, strerror(errno));
327          free(path_buf);
328          return NULL;
329        }
330
331      if (stat(".", &below) == -1)
332        {
333          fprintf(stderr, "%s: stat of \"%s\" failed: %s\n", program_name,
334                  path_buf, strerror(errno));
335          free(path_buf);
336          return NULL;
337        }
338
339      if (above.st_dev != below.st_dev || above.st_ino != below.st_ino)
340        {
341          fprintf(stderr, "%s: final component of \"%s\" changed during "
342                  "traversal\n", program_name, path_buf);
343          free(path_buf);
344          return NULL;
345        }
346
347      /* Advance to next component. */
348      start_ptr = end_ptr + 1;
349
350      /* Restore the / we nulled for diagnostics. */
351      *end_ptr = '/';
352    }
353
354  free(path_buf);
355  return (start_ptr - path_buf) + path;
356}
357
358/* Safely remove and optionally zero a file. -1 is returned for any
359 * level of failure, such as zeroing failed but unlinking succeeded.
360 */
361static int safe_rm(char *path, int zero_file)
362{
363  int file, status = 0;
364  struct stat file_stat;
365  char *name;
366
367  name = safe_cd(path);
368  if (name == NULL)
369    return -1;
370
371  /* Do the unlink before the actual zeroing so that the appearance
372   * is atomic.
373   */
374
375  if (zero_file)
376    {
377      file = safe_open(name, &file_stat);
378      if (file == -1)
379        return -1;
380    }
381
382  if (safe_to_unlink(name) && unlink(name) == -1)
383    {
384      fprintf(stderr, "%s: error unlinking %s: %s\n", program_name,
385              path, strerror(errno));
386      status = -1;
387    }
388
389  if (zero_file && file != -1)
390    {
391      if (zero(file, &file_stat) == -1)
392        status = -1;
393      close(file);
394    }
395
396  return status;
397}
398
399/* Safely remove a directory. -1 is returned for any level of failure. */
400static int safe_rmdir(char *path, int quietflag)
401{
402  char *name;
403
404  name = safe_cd(path);
405  if (name == NULL)
406    return -1;
407
408  if (rmdir(name) == -1)
409    {
410      /* System V systems return EEXIST when a directory isn't empty
411       * but hack the rmdir command to display a better error message.
412       * Well, we can do that too.
413       */
414      if (errno == EEXIST)
415        errno = ENOTEMPTY;
416
417      if (!quietflag || errno != ENOTEMPTY)
418        {
419          fprintf(stderr, "%s: error removing directory %s: %s\n",
420                  program_name, path, strerror(errno));
421        }
422      return -1;
423    }
424
425  return 0;
426}
Note: See TracBrowser for help on using the repository browser.