/* Copyright (c) 1997-1998 Karl M. Hegbloom * (note: above email address is defunct) * * Originally Based on `tmpwatch-1.2/1.4' RHS, Erik Troan * * This program may be freely redistributed under the terms of the GNU * Public License. You should be able to find a copy of the GPL in * your "/usr/doc/copyright" directory on most GNU/Linux installations. * * Now maintained by ps - Paul Slootman * 2001-12-03 1.4.15 * Patches from * mb - Marcus Brinkmann * for using get_current_dir_name() and portability on HURD. ps hacked this * so that it should (still?) compile on non-glibc systems. * 2001-12-02 1.5.0 * Added autoconf support. Also added getopt.c, getopt1.c, getopt.h so that * tmpreaper can be compiled on e.g. Solaris. It builds and runs on Solaris * and netBSD in addition to Linux now. * Ignore ext2fs immutable files. * 2002-02-05 1.6.0 * ctime option added, check ctime in addition to atime, for those cases where * files get created with an old date in atime and mtime (e.g. via Samba from * a PC). * See changelog for later changes. */ #define _LARGEFILE_SOURCE 1 /* handle >2GB files where possible */ #define _FILE_OFFSET_BITS 64 #ifdef HAVE_CONFIG_H #include "config.h" #endif #define _GNU_SOURCE /* For get_current_dir_name() */ #include #include #include #if HAVE_DIRENT_H # include # define NAMLEN(dirent) strlen((dirent)->d_name) #else # define dirent direct # define NAMLEN(dirent) (dirent)->d_namlen # if HAVE_SYS_NDIR_H # include # endif # if HAVE_SYS_DIR_H # include # endif # if HAVE_NDIR_H # include # endif #endif #ifdef HAVE_ERRNO_H # include #endif #include #include #ifdef HAVE_LIMITS_H # include #endif #include #include #if STDC_HEADERS # include #else # ifndef HAVE_MEMCPY # define memmove(d, s, n) bcopy ((s), (d), (n)) # endif #endif #ifdef HAVE_SYS_WAIT_H #include #endif #include #ifdef HAVE_UNISTD_H # include #endif #ifdef HAVE_STDLIB_H # include #endif #include #ifdef __linux__ # include # include # include #endif /* do we need to work around a glibc dependency? */ #ifndef __USE_GNU # ifdef __linux__ /* sanity check */ # error "__USE_GNU not defined on a linux system, investigate! And report to tmpreaper@packages.debian.org" # else # define NO_get_current_dir_name /* XXX TODO: a workaround for missing get_current_dir_name() */ # endif #endif #ifndef MAX_FORKS # define MAX_FORKS 200 #endif #ifdef GLOB_BRACE # define TMPREAPER_GLOB_BRACE GLOB_BRACE #else # define TMPREAPER_GLOB_BRACE 0 #endif /* * tmpreaper.c -- remove files in a directory, but do it carefully */ /* `--force': also rm files that are mode `-w EUID $$' */ #define FLAGS_FORCE (1 << 0) #define FLAGS_FORCE_P(fl) ((fl) & FLAGS_FORCE) /* `--mtime': Use `mtime' rather than `atime' */ #define FLAGS_MTIME (1 << 1) #define FLAGS_MTIME_P(fl) ((fl) & FLAGS_MTIME) /* `--mtime-dir': rm empty directories based on mtime */ #define FLAGS_MTIME_DIR (1 << 2) #define FLAGS_MTIME_DIR_P(fl) ((fl) & FLAGS_MTIME_DIR) /* `--symlinks': also rm symlinks like files and directories */ #define FLAGS_SYMLINKS (1 << 3) #define FLAGS_SYMLINKS_P(fl) ((fl) & FLAGS_SYMLINKS) /* `--all': also rm symlinks, fifos, devices, and sockets... */ #define FLAGS_ALLFILES (1 << 4) #define FLAGS_ALLFILES_P(fl) ((fl) & FLAGS_ALLFILES) /* the `--protect' option was given */ #define FLAGS_PROTECT (1 << 5) #define FLAGS_PROTECT_P(fl) ((fl) & FLAGS_PROTECT) /* the `--test' option was given */ #define FLAGS_TEST (1 << 6) #define FLAGS_TEST_P(fl) ((fl) & FLAGS_TEST) /* `--ctime': Use `ctime' in addition to `atime' */ #define FLAGS_CTIME (1 << 7) #define FLAGS_CTIME_P(fl) ((fl) & FLAGS_CTIME) /* The `--showdeleted' option was given */ #define FLAGS_SHOWDEL (1 << 8) #define FLAGS_SHOWDEL_P(fl) ((fl) & FLAGS_SHOWDEL) #define TIME_SECONDS 1 #define TIME_MINUTES 60 #define TIME_HOURS 60 * 60 #define TIME_DAYS 24 * 60 * 60 glob_t protect_glob; /* list of files to not rm */ typedef struct protect_entry { ino_t inode; char * name; } protect_entry; protect_entry * protect_table; /* Global */ #define LOG_REALDEBUG 1 #define LOG_DEBUG 2 #define LOG_VERBOSE 3 #define LOG_NORMAL 4 #define LOG_ERROR 5 #define LOG_FATAL 6 int logLevel = LOG_NORMAL; int delayMax = 0; int runtimeMax = 55; void message (const int level, const char * format, ...) /*VARARGS*/ { va_list args; FILE * where = stderr; if (level >= logLevel) { va_start (args, format); switch (level) { case LOG_DEBUG: where = stdout; /*FALLTHRU*/ case LOG_REALDEBUG: fprintf (where, "debug: "); break; case LOG_ERROR: case LOG_FATAL: fprintf (where, "error: "); break; case LOG_NORMAL: case LOG_VERBOSE: where = stdout; break; default: break; } vfprintf (where, format, args); if (level == LOG_FATAL) exit (1); } } void sig_alarmhandler(int sig) { message(LOG_FATAL, "run time exceeded!\n" "This may be indicative of an attack to use tmpreaper to remove critical files;\n" "or the directories to clean up are excessive large and/or messed up.\n" "Please investigate.\n"); exit(1); /* redundant */ } int safe_chdir (const char * dirname) { struct stat sb1, sb2; if (lstat (dirname, &sb1)) { message (LOG_ERROR, "lstat() of directory `%s' failed: %s\n", dirname, strerror (errno)); return 1; } if (! S_ISDIR (sb1.st_mode)) { if (S_ISLNK (sb1.st_mode)) { message (LOG_ERROR, "safe_chdir(): Will not chdir across symlink `%s' (inode %lu).\n", dirname, (u_long) sb1.st_ino); return 1; } message (LOG_ERROR, "Not a directory: `%s' (inode %lu).\n", dirname, (u_long) sb1.st_ino); return 1; } message (LOG_DEBUG, "safe_chdir() : Before chdir(), `dirname' is inode `%lu'.\n", (u_long) sb1.st_ino); message (LOG_DEBUG, "safe_chdir() : Before chdir(), `dirname' on device `%lu'.\n", (u_long) sb1.st_dev); if (chdir (dirname)) { message (LOG_ERROR, "chdir() to directory `%s' (inode %lu) failed: %s\n", dirname, (u_long) sb1.st_ino, strerror (errno)); return 1; } if (lstat (".", &sb2)) { message (LOG_ERROR, "lstat() of directory `%s' after chdir() failed: %s\n", dirname, strerror (errno)); return 1; } message (LOG_DEBUG, "safe_chdir() : After chdir(), `dirname' is inode `%lu'.\n", (u_long) sb2.st_ino); message (LOG_DEBUG, "safe_chdir() : After chdir(), `dirname' on device `%lu'.\n", (u_long) sb2.st_dev); if (sb1.st_ino != sb2.st_ino) { message (LOG_ERROR, "Inode mismatch!!! : `%s' (inode %lu) != `.' (inode %lu)\n", dirname, (u_long) sb1.st_ino, (u_long) sb2.st_ino); message (LOG_FATAL, "This indicates a possible subversion attempt.\n"); /*NOTREACHED*/ } else if (sb1.st_dev != sb2.st_dev) { message (LOG_ERROR, "Device mismatch!!!\n"); message (LOG_ERROR, "`%s' (inode %lu), device `%lu' != `.' (inode %lu), device `%lu'\n", dirname, (u_long) sb1.st_ino, (u_long) sb1.st_dev, (u_long) sb2.st_ino, (u_long) sb2.st_dev); message (LOG_FATAL, "This indicates a possible subversion attempt.\n"); /*NOTREACHED*/ } return 0; } int dir_empty_p (const char * dirname) { DIR * dir; struct dirent * ent; int count = 0; if (! (dir = opendir(dirname))) { message(LOG_ERROR, "Cannot opendir(%s): %s\n", dirname, strerror(errno)); return 0; } while ((ent = readdir(dir))) { if ((ent->d_name[0] == '.') && ( (ent->d_name[1] == '\0') || ((ent->d_name[1] == '.') && (ent->d_name[2] == '\0')) ) ) continue; count++; } closedir(dir); return count != 0 ? 0 : 1; /* #t when empty. */ } int cleanupDirectory (const char * dirname , const unsigned int killTime, const int flags) { DIR * dir; struct dirent * ent; struct stat sb, here; int status, pid, skip = 0; int i; static int fork_count = 0; if (FLAGS_TEST_P (flags)) { message (LOG_VERBOSE, "(PID %u) Pretending to clean up directory `%s'.\n", (u_int) getpid(), dirname); } else { message(LOG_DEBUG, "Cleaning up directory `%s'.\n", dirname); } if (fork_count > MAX_FORKS) { #ifdef NO_get_current_dir_name char path[PATH_MAX], *p; #else char *path, *p; #endif int l; /* the admin will want to know where the problem lies */ #ifdef NO_get_current_dir_name if (getcwd(path, PATH_MAX) < 0) /* too long even for PATH_MAX, show something anyway */ #else if ((path = get_current_dir_name()) < 0) #endif { p = "..../"; } else { l = strlen(path); if (l > 65) { /* remove middle part of the path */ p = path + l - 30; strcpy(&path[30], " ... "); memmove(&path[35], p, 30); path[65] = 0; } p = path; } message(LOG_FATAL, "Too deep directory nesting detected while trying to clean up\n`%s/%s',\npossible attack?\n", p, dirname); exit(2); } /* * Do everything in a child process so we don't have to chdir(".."), * which would lead to a race condition. fork() on Linux is very efficient * so this shouldn't be a big deal (probably just a exception on one page * of stack, not bad). I should probably just keep a directory stack * and fchdir() back up it, but it's not worth changing now. - ewt * * After reading "http://www.geek-girl.com/bugtraq/1996_2/0054.html" (look * for "cd .."), I think it is best to use this method rather than the * contrasting algorithm mentioned in Eric's comment. IMHO he has made the * right choice by having chosen the `recursive forking' algorithm. It is * much easier to implement and understand as well as being more secure * through avoiding `cd ..'. - karlheg * * The geek girl link is dead. fork() is not efficient on all systems, * so it would still be better to use a directory stack + fchdir(). - mb * * This is another link to what geek-girl had: * http://www.geocrawler.com/mail/msg.php3?msg_id=184906&list=91 * - ps */ if (! (pid = fork())) { if (safe_chdir (dirname)) exit(1); fork_count++; if (! (dir = opendir ("."))) { message (LOG_FATAL, "Opening directory, `%s' as `.' : %s\n", dirname, strerror (errno)); } if (lstat (".", &here)) { message (LOG_FATAL, "Statting current directory, `%s' as `.' : %s\n", dirname, strerror (errno)); } do { errno = 0; ent = readdir (dir); if (errno) { message (LOG_ERROR, "Reading directory entry. : %s\n", strerror (errno)); } if (!ent) break; /* * if name starts with dot, and * is 1 byte long, or * has a second dot and is 2 bytes long, * skip it. */ if ((ent->d_name[0] == '.') && ( (ent->d_name[1] == '\0') || ((ent->d_name[1] == '.') && (ent->d_name[2] == '\0')) ) ) continue; message (LOG_DEBUG, "(PID %u) Found directory entry `%s'.\n", (u_int) getpid(), ent->d_name); if (lstat (ent->d_name, &sb)) { message (LOG_ERROR, "Failed to lstat() `%s/%s': %s\n", dirname, ent->d_name, strerror (errno)); continue; } if (((!getuid() && !sb.st_uid) || (sb.st_uid == geteuid())) && !FLAGS_FORCE_P (flags) && !(sb.st_mode & S_IWUSR)) { message (LOG_VERBOSE, "Non-writeable file, owned by UID (%u) skipped: `%s'\n", (u_int) sb.st_uid, ent->d_name); continue; } if (sb.st_dev != here.st_dev) { message (LOG_VERBOSE, "File on different device skipped: `%s/%s'\n", dirname, ent->d_name); continue; } if (S_ISDIR (sb.st_mode)) { char *dst; if ((dst = malloc(strlen(ent->d_name) + 3)) == NULL) message (LOG_FATAL, "malloc failed.\n"); strcpy(dst, ent->d_name); strcat(dst, "/X"); rename(ent->d_name, dst); if (errno == EXDEV) { free(dst); message (LOG_VERBOSE, "File on different device skipped: `%s/%s'\n", dirname, ent->d_name); continue; } free(dst); cleanupDirectory (ent->d_name, killTime, flags); message (LOG_VERBOSE, "(PID %u) Back from recursing down `%s'.\n", (u_int) getpid(), ent->d_name); } if (FLAGS_PROTECT_P (flags)) { skip = i = 0; do { if (sb.st_ino == protect_table[i].inode) { message (LOG_VERBOSE, "Entry matching `--protect' pattern skipped. `%s'\n", protect_table[i].name); skip = 1; break; } } while (protect_table[i++].name); if (skip) continue; } /* Decide whether to remove the file or not */ /* check for mtime on directory instead of atime if requested */ if ( FLAGS_MTIME_P(flags) || (FLAGS_MTIME_DIR_P(flags) && S_ISDIR(sb.st_mode))) { if (sb.st_mtime >= killTime) continue; } else { if (sb.st_atime >= killTime) continue; /* If ctime option is given, the ctime must also be so */ /* long ago. This is e.g. for samba as DOS preserves */ /* mtime when copying, so such files get removed too soon */ /* Don't do it for directories though, as ctime may get */ /* reset below through utime() */ if (!S_ISDIR(sb.st_mode) && FLAGS_CTIME_P(flags) && sb.st_ctime >= killTime) continue; } if (S_ISDIR (sb.st_mode)) { if (dir_empty_p (ent->d_name)) { if (FLAGS_TEST_P (flags)) { message (LOG_VERBOSE, "Pretending to remove empty directory `%s'.\n", ent->d_name); if (FLAGS_SHOWDEL_P(flags)) message(LOG_NORMAL, "rmdir %s/%s\n", dirname, ent->d_name); } else { message (LOG_VERBOSE, "Removing directory `%s'.\n", ent->d_name); if (rmdir (ent->d_name)) { message (LOG_ERROR, "Failed to rmdir `%s': %s\n", ent->d_name, strerror (errno)); } else { if (FLAGS_SHOWDEL_P(flags)) message(LOG_NORMAL, "rmdir %s/%s\n", dirname, ent->d_name); } } } } else if (FLAGS_ALLFILES_P (flags) || S_ISREG (sb.st_mode) || (FLAGS_SYMLINKS_P (flags) && S_ISLNK (sb.st_mode))) { if (FLAGS_TEST_P (flags)) { message (LOG_VERBOSE, "Pretending to remove file `%s/%s'.\n", dirname, ent->d_name); if (FLAGS_SHOWDEL_P(flags)) message(LOG_NORMAL, "rm %s/%s\n", dirname, ent->d_name); } else { message (LOG_VERBOSE, "Removing file `%s/%s'.\n", dirname, ent->d_name); if (unlink (ent->d_name)) #if defined(__linux__) { int give_error = 1; int fd; if ((fd = open(ent->d_name, O_RDONLY)) >= 0) { unsigned int flags; if (ioctl(fd, EXT2_IOC_GETFLAGS, &flags) >= 0) { if (flags & EXT2_IMMUTABLE_FL) { give_error = 0; } } close(fd); /* Restore access time! */ /* However, only bother if we give a warning. */ /* With no warning, it doesn't matter. */ /* And it delays the next attempt :-) */ if (give_error) { struct utimbuf ts; ts.actime = sb.st_atime; ts.modtime = sb.st_mtime; utime(ent->d_name, &ts); } } if (give_error) #endif message (LOG_ERROR, "Failed to unlink `%s': %s\n", ent->d_name, strerror (errno)); #if defined(__linux__) else message (LOG_VERBOSE, "file `%s' marked immutable -- skipping.\n", ent->d_name); } #endif else { if (FLAGS_SHOWDEL_P(flags)) message(LOG_NORMAL, "rm %s/%s\n", dirname, ent->d_name); } } } else { if (FLAGS_SYMLINKS_P (flags)) { message (LOG_VERBOSE, "Not a regular file, symlink, or directory `%s' -- skipping.\n", ent->d_name); } else { message (LOG_VERBOSE, "Not a regular file or directory `%s' -- skipping.\n", ent->d_name); } } } while (ent); closedir (dir); exit (0); } if (pid < 0) { message(LOG_ERROR, "fork failed: %s\n", strerror(errno)); return 1; } if (waitpid(pid, &status, 0) < 0) { message(LOG_ERROR, "waitpid failed: %s\n", strerror(errno)); return 1; } if (WIFEXITED (status)) return WEXITSTATUS (status); return 0; } void printCopyright (void) { fprintf (stderr, #ifdef DEBIAN "tmpreaper -- Version: " VERSION "-DEB\n" #else "tmpreaper -- Version: " VERSION "\n" #endif "(c) 1997 Karl M. Hegbloom \n" "(c) 2006 Paul Slootman \n" "This may be freely redistributed under the terms of the GNU Public License.\n"); } /* 1 2 3 4 5 6 7 8 02345678901234567890123456789012345678901234567890123456789012345678901234567890 */ void usage(void) { printCopyright (); fprintf (stderr, "\n" "tmpreaper [-htvfmMsadVT] [--help] [--test] [--verbose] [--force]\n" " [--delay=99] [--runtime=99] [--showdeleted]\n" " [--ctime] [--mtime] [--mtime-dir] [--symlinks] [--all]\n" " [[--protect '']...]