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

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