/* Copyright 1998 by the Massachusetts Institute of Technology. * * Permission to use, copy, modify, and distribute this * software and its documentation for any purpose and without * fee is hereby granted, provided that the above copyright * notice appear in all copies and that both that copyright * notice and this permission notice appear in supporting * documentation, and that the name of M.I.T. not be used in * advertising or publicity pertaining to distribution of the * software without specific, written prior permission. * M.I.T. makes no representations about the suitability of * this software for any purpose. It is provided "as is" * without express or implied warranty. */ /* * saferm * * Running as root, remove and optionally zero files in /tmp that * may be owned by anybody, avoiding any possible race conditions * resulting in security problems. */ static const char rcsid[] = "$Id: saferm.c,v 1.6 1999-09-21 20:09:11 danw Exp $"; #include #include #include #include #include #include #include #include char *program_name; #ifndef MIN #define MIN(a, b) ((a) < (b) ? (a) : (b)) #endif static int safe_open(char *filename, struct stat *file_stat); static int zero(int file, struct stat *file_stat); static int safe_to_unlink(char *name); static char *safe_cd(char *path); static int safe_rm(char *path, int zero_file); static int safe_rmdir(char *path, int quietflag); int main(int argc, char **argv) { int c, zeroflag = 0, dirflag = 0, quietflag = 0, errflag = 0; int dir, status = 0; /* Set up our program name. */ if (argv[0] != NULL) { program_name = strrchr(argv[0], '/'); if (program_name != NULL) program_name++; else program_name = argv[0]; } else program_name = "saferm"; while ((c = getopt(argc, argv, "dqz")) != EOF) { switch(c) { case 'd': dirflag = 1; break; case 'q': quietflag = 1; break; case 'z': zeroflag = 1; break; case '?': errflag = 1; break; } } if (errflag || optind == argc || (dirflag && zeroflag)) { fprintf(stderr, "usage: %s [-q] [-d|-z] filename [filename ...]\n", program_name); exit(1); } /* Remember our current directory in case we are being passed * relative paths. */ dir = open(".", O_RDONLY); if (dir == -1) { fprintf(stderr, "%s: error opening current directory: %s\n", program_name, strerror(errno)); exit(1); } while (optind < argc) { /* If we're removing a relative path, be sure to cd to where * we started. */ if (argv[optind][0] != '/') { if (fchdir(dir) == -1) { fprintf(stderr, "%s: error cding to initial directory: %s\n", program_name, strerror(errno)); exit(1); } } if (dirflag && safe_rmdir(argv[optind], quietflag) == -1) status = 1; else if (!dirflag && safe_rm(argv[optind], zeroflag) == -1) status = 1; optind++; } exit(status); } /* Safely open a file with intent to zero. */ static int safe_open(char *filename, struct stat *file_stat) { int file; struct stat name_stat; /* Try to open the file we think we want to zero. */ file = open(filename, O_WRONLY); if (file == -1) { fprintf(stderr, "%s: open(%s) for writing failed: %s\n", program_name, filename, strerror(errno)); return -1; } /* Stat the file we actually opened. */ if (fstat(file, file_stat) == -1) { fprintf(stderr, "%s: fstat(%s) failed: %s\n", program_name, filename, strerror(errno)); close(file); return -1; } /* Make sure it's a regular file. */ if (!S_ISREG(file_stat->st_mode)) { fprintf(stderr, "%s: %s is %s\n", program_name, filename, (S_ISDIR(file_stat->st_mode) ? "a directory" : "not a regular file")); close(file); return -1; } /* Stat the file we think we want to zero. */ if (lstat(filename, &name_stat) == -1) { fprintf(stderr, "%s: lstat(%s) failed: %s\n", program_name, filename, strerror(errno)); close(file); return -1; } /* We must be sure that we are not zeroing a hard link or symbolic * link to something like the password file. To do this, we must * verify that the link count on the file we zero is equal to 1, and * that we did not follow a symbolic link to reach that file. * * Simply lstat()ing the file before or after we open it is subject to * lose in a race condition, as there is no way to know that the file we * opened is the same file as the one we lstat()ed. Simply fstat()ing * the file we actually open can't tell us whether we followed a symbolic * link to get there, and also can't give us a reliable link count also * due to race conditions. * * But if we both lstat() and fstat() and compare the inode numbers from * the two cases, we can verify that the file we have open, at the time * of our lstat(), existed in a unique location identified by the pathname * we used to open it. * * So first, verify that our two stats got the same file. */ if (file_stat->st_dev != name_stat.st_dev || file_stat->st_ino != name_stat.st_ino) { fprintf(stderr, "%s: %s and file opened are not identical\n", program_name, filename); close(file); return -1; } /* Now verify that the link count is 1. Note that file_stat may not be * used here, otherwise we could be beaten by a race condition: * * 1. The attacker creates /tmp/passwd as a symlink to /etc/passwd. * 2. We open /tmp/passwd, which follows the symlink, and so we have * fstat()ed /etc/passwd. The link count as of this stat is 1. * 3. The attacker removes the /tmp/passwd symlink and replaces it * with a hard link to /etc/passwd. * 4. We lstat() /tmp/passwd. The link count now is 2. */ if (name_stat.st_nlink != 1) { fprintf(stderr, "%s: multiple links for file %s, not zeroing\n", program_name, filename); close(file); return -1; } /* Now we know it is safe to muck with the file we opened. */ return file; } /* Zero the file represented by the passed file descriptor. Assumes * that the file pointer is already at the beginning of the file, and * file_stat points to a stat of the file. */ static int zero(int file, struct stat *file_stat) { char zeroes[BUFSIZ]; off_t written = 0; ssize_t status; memset(zeroes, 0, BUFSIZ); while (written < file_stat->st_size) { status = write(file, zeroes, MIN(BUFSIZ, file_stat->st_size - written)); if (status == -1) { fprintf(stderr, "%s: write() failed: %s\n", program_name, strerror(errno)); return -1; } written += status; } fsync(file); return 0; } /* Return non-zero if it is safe to unlink NAME. */ static int safe_to_unlink(char *name) { struct stat st; if (lstat(name, &st) < 0) { fprintf(stderr, "%s: %s: %s\n", program_name, name, strerror(errno)); return 0; } if (S_ISDIR(st.st_mode)) { fprintf(stderr, "%s: %s is a directory\n", program_name, name); return 0; } return 1; } /* path is the full pathname to a file. We cd into the directory * containing the file, following no symbolic links. A pointer to just * the filename is returned on success (a substring of path), and NULL * is returned on failure. */ static char *safe_cd(char *path) { char *start_ptr, *end_ptr, *path_buf; struct stat above, below; if (path == NULL || *path == 0) return NULL; /* Make a copy of the path to be removed, since we're going to modify it. */ path_buf = malloc(strlen(path) + 1); if (path_buf == NULL) { fprintf(stderr, "%s: out of memory\n", program_name); return NULL; } strcpy(path_buf, path); /* Step through each path component and cd into it, verifying * that we wind up where we expect to. * * strchr()ing from start_ptr + 1 handles the absolute path case * as well as beginning to address the double-/ case. */ start_ptr = path_buf; while ((end_ptr = strchr(start_ptr + 1, '/'))) { /* Include any remaining extra /'s at the end of this component. */ while (*(end_ptr + 1) == '/') end_ptr++; *end_ptr = 0; if (lstat(start_ptr, &above) == -1) { fprintf(stderr, "%s: lstat of \"%s\" failed: %s\n", program_name, path_buf, strerror(errno)); free(path_buf); return NULL; } if (!S_ISDIR(above.st_mode)) { fprintf(stderr, "%s: final component of \"%s\" is not a directory\n", program_name, path_buf); free(path_buf); return NULL; } if (chdir(start_ptr) == -1) { fprintf(stderr, "%s: chdir to \"%s\" failed: %s\n", program_name, path_buf, strerror(errno)); free(path_buf); return NULL; } if (stat(".", &below) == -1) { fprintf(stderr, "%s: stat of \"%s\" failed: %s\n", program_name, path_buf, strerror(errno)); free(path_buf); return NULL; } if (above.st_dev != below.st_dev || above.st_ino != below.st_ino) { fprintf(stderr, "%s: final component of \"%s\" changed during " "traversal\n", program_name, path_buf); free(path_buf); return NULL; } /* Advance to next component. */ start_ptr = end_ptr + 1; /* Restore the / we nulled for diagnostics. */ *end_ptr = '/'; } free(path_buf); return (start_ptr - path_buf) + path; } /* Safely remove and optionally zero a file. -1 is returned for any * level of failure, such as zeroing failed but unlinking succeeded. */ static int safe_rm(char *path, int zero_file) { int file, status = 0; struct stat file_stat; char *name; name = safe_cd(path); if (name == NULL) return -1; /* Do the unlink before the actual zeroing so that the appearance * is atomic. */ if (zero_file) { file = safe_open(name, &file_stat); if (file == -1) return -1; } if (safe_to_unlink(name) && unlink(name) == -1) { fprintf(stderr, "%s: error unlinking %s: %s\n", program_name, path, strerror(errno)); status = -1; } if (zero_file && file != -1) { if (zero(file, &file_stat) == -1) status = -1; close(file); } return status; } /* Safely remove a directory. -1 is returned for any level of failure. */ static int safe_rmdir(char *path, int quietflag) { char *name; name = safe_cd(path); if (name == NULL) return -1; if (rmdir(name) == -1) { /* System V systems return EEXIST when a directory isn't empty * but hack the rmdir command to display a better error message. * Well, we can do that too. */ if (errno == EEXIST) errno = ENOTEMPTY; if (!quietflag || errno != ENOTEMPTY) { fprintf(stderr, "%s: error removing directory %s: %s\n", program_name, path, strerror(errno)); } return -1; } return 0; }