stmpclean-0.3/000075500000000000000000000000001227421170200133365ustar00rootroot00000000000000stmpclean-0.3/FAQ000064400000000000000000000026321227421170200136730ustar00rootroot00000000000000This is the list of frequently asked questions about stmpclean. Before you ask your question, please consult this. $Id: FAQ,v 1.3 2000/10/17 17:53:58 shalunov Exp $ ------------------------------------------------------------ > I get this error when I run ./stmpclean -t 3w /var/spool/mail > stmpclean[24030]: RACE?: unlink("tcal") in /var/spool/mail: > Permission denied, exiting Stmpclean has not been designed to be run over mail spools or home directories! You should *not* run stmpclean on /var/spool/mail. You are risking deleting precious mail files. You're going to delete *all* mail in the system mailbox of all users who haven't gotten new mail (or changed their mailbox by reading it and, say, deleting a message) in three weeks. If you insist on doing exactly that, you can use stmpclean, but you must set permissions on /var/spool/mail as they are normally set on /tmp. You better do a backup before you delete people's mail. * Do "ls -ld /var/spool/mail"; notice the permissions on the directory. * Change the permissions: "chmod 1777 /var/spool/mail". * Run stmpclean. * Set permissions back to what they were. E.g., for "drwxrwxr-x" you'd say "chmod 775 /var/spool/mail". * Respond to angry users' questions as to where their mail is now gone. > Will stmpclean run on ? See list of supported platforms in the README file, which accompanies the distribution. stmpclean-0.3/Makefile000064400000000000000000000014131227421170200147750ustar00rootroot00000000000000# Makefile for stmpclean. # Written by Stanislav Shalunov. # $Id: Makefile,v 1.6 2003/03/21 21:58:00 shalunov Exp $ PREFIX=/usr/local BINDIR=$(PREFIX)/sbin MANDIR=$(PREFIX)/man/man8 CFLAGS += -O6 -Wall -W -pedantic all: stmpclean stmpclean.0 install: all if [ ! -d $(BINDIR) ] ; then mkdir -p -m 0755 $(BINDIR); fi if [ ! -d $(MANDIR) ] ; then mkdir -p -m 0755 $(MANDIR); fi install -c -o 0 -g 0 -m 0555 stmpclean $(BINDIR)/ install -c -o 0 -g 0 -m 0444 stmpclean.8 $(MANDIR)/ stmpclean.o: stmpclean.c stmpclean: stmpclean.o $(CC) -o stmpclean stmpclean.o && strip stmpclean stmpclean.0: stmpclean.8 nroff -mandoc stmpclean.8 > stmpclean.0 indent: indent stmpclean.c clean: -rm -f *.o stmpclean a.out core *.core ktrace.out *.orig *~ *.BAK \ junk stmpclean.0 stmpclean-0.3/README000064400000000000000000000030031227421170200142120ustar00rootroot00000000000000 Supported Platforms At this time, stmpclean works on BSD and Linux. It has been reported to work on Solaris, IRIX, and SCO, but I have no way of verifying this. Installation To build, say ``make''. To install, execute the command ``make install'' and then read the manual page. You can also read the pre-formatted manual page before installation in the file stmpclean.0. Rationale People do stuff like find /tmp -type f -atime +3 -ctime +3 ! -name '.X*-lock' -exec rm -f -- {} \; find -d /tmp ! -name . -type d -mtime +1 -exec rmdir -- {} \; >/dev/null 2>&1 as root in /etc/crontab (or in daily maintenance scripts). Don't ever do this. This can be easily tricked into deleting arbitrary files on your system! People also run nifty Perl scripts that overcome this problem. Well, in case there is an attack, they won't delete your precious /etc/ftpusers or whatever you guard most. They'll just fork 10000 children each trying to allocate 2MB of memory. Good luck. This program solves these problems. Further Information The distribution is accompanied by a FAQ file. It should answer most common questions. If your question isn't answered, or if you have a problem with stmpclean, or a suggestion, or a bug report, or a patch, send it to me (see address below). Author The stmpclean utility is written by Stanislav Shalunov. You can send comments, bug reports, suggestions, and criticism to shalunov@internet2.edu. http://www.internet2.edu/~shalunov/ $Id: README,v 1.7 2003/03/21 21:37:43 shalunov Exp $ stmpclean-0.3/stmpclean.8000064400000000000000000000061721227421170200154230ustar00rootroot00000000000000.\" Copyright (C) 1999 Stanislav Shalunov. .\" http://www.internet2.edu/~shalunov/ .\" See stmpclean.c for copyright notice and legal conditions. .\" .\" $Id: stmpclean.8,v 1.6 2003/03/21 21:44:09 shalunov Exp $ .\" .Dd August 1999 .Dt STMPCLEAN 8 .Os .Sh NAME .Nm stmpclean .Nd remove old files from a world-writable directory .Sh SYNOPSIS .Nm stmpclean .Op Fl "t" .Op Fl "v" .Ar dir1 .Op "dir2 ..." .Sh DESCRIPTION The .Nm utility removes old files (and old empty directories) from the specified directory. It'll be typically used to clean directories such as ``/tmp'' where old files tend to accumulate. .Pp The .Nm utility never removes files or directories owned by root. It is a feature, not a bug. Great care is taken while descending into the directory, and the operation is secure. Anything that's not a directory, regular file, or symbolic link is also left alone (because programs like .Xr screen 1 create sockets and FIFOs under /tmp and expect them to be long-lived; we accomodate this practice). Unlike floating around Perl scripts that do the same task .Nm never forks and consumes limited amount of memory (these Perl scripts easily turn into forking bombs when someone creates a lot a directories under ``/tmp''). If your system is attacked and the attacker creates an extremely deep file hierarchy, .Nm won't add to the problem by crashing your system trying to remove it. But it won't help you in fighting the attack, either, because it descends only to a limited depth (currently, 30 levels). If .Nm determines a race condition it'll log the situation (you can look for the word ``RACE'' in log files) and exit with a failure. .Pp So, .Nm will clean temporary directories for you fine when there are no attacks, and, when there is an attack, .Nm won't make the situation worse (in particular, it cannot be tricked into removing files outside specified directories or consume unlimited amount of resources). .Pp The following option is available: .Bl -tag -width flag .It Fl "t" The time specification that follows the .Fl t flag specifies how old a file or a directory has to be before it will be removed. It can be a string like `1w' (one week) or `4d5h' (four days plus five hours) or `2m3s' (two minutes plus three seconds). The default is `3d' (three days). .It Fl "v" Be verbose: list each file deleted. .El .Pp The .Nm utility exits 0 on success, and >0 if an error occurs. .Sh EXAMPLES The .Nm utility will typically be run nightly from .Xr cron 8 as .Bl -tag -width example .It Li "stmpclean /tmp /var/tmp" .El .Pp In FreeBSD .Nm invokation should be placed into the file .Pa /etc/periodic/daily/110.clean-tmps . In other versions of BSD it should go into the .Pa /etc/daily script. In Linux, check if you have .Pa /etc/periodic , and if not, you can just run it from cron; usually you'd have to edit .Pa /etc/crontab . .Sh SEE ALSO .Xr cron 8 .Sh BUGS When .Nm removes a file from a directory, modification time of the directory changes and it looks new to .Nm when it examines it later (if the directory became empty). Thus, removing a deep hierarchy can take some time. Notice that this only delays removal of some empty directories. stmpclean-0.3/stmpclean.c000064400000000000000000000257051227421170200155010ustar00rootroot00000000000000/* * stmpclean.c -- remove old files from a world-writable directory. * Written by Stanislav Shalunov, http://www.internet2.edu/~shalunov/ * * Copyright (C) 1999, Stanislav Shalunov. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * 2. Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * 3. Neither the name of Stanislav Shalunov nor the names of his * contributors may be used to endorse or promote products derived * from this software without explicit prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND WITH ALL FAULTS. ANY EXPRESS OR IMPLIED WARRANTIES, * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, AND * NON-INFRINGEMENT ARE DISCLAIMED AND THE ENTIRE RISK OF SATISFACTORY * QUALITY, PERFORMANCE, ACCURACY, AND EFFORT IS WITH LICENSEE. IN NO * EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE * GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR * DISTRIBUTION OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY * OF SUCH DAMAGE. */ #ifndef lint static const char rcsid[] = "$Id: stmpclean.c,v 1.13 2003/06/10 19:07:45 shalunov Exp $"; #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include /* * How deep to descend into directories? Won't go any deeper than MAX_DEPTH * levels. */ #define MAX_DEPTH (30) #define SECONDS_IN_A_MINUTE (60) #define SECONDS_IN_AN_HOUR (SECONDS_IN_A_MINUTE * 60) #define SECONDS_IN_A_DAY (SECONDS_IN_AN_HOUR * 24) #define SECONDS_IN_A_WEEK (SECONDS_IN_A_DAY * 7) #define GETCWD {if (getcwd(cwd, MAXPATHLEN) == NULL)\ strcpy(cwd, "/FULL/PATH/TOO/LONG");} /* Time at the start of the program, in seconds since beginning of epoch. */ static time_t now; /* Minimum age (mtime) of a file or empty directory to be deleted. */ int minage; /* Current working directory is used for logging purposes only. */ static char cwd[MAXPATHLEN]; /* Flag: be verbose? */ static int verbose = 0; /* Print usage message, exit with a failure. */ void usage() { fprintf(stderr, "Usage: stmpclean [-t ] dir1 [dir2 [dir3]...]]\n\n" "Where time specification is a string like 1w\n" "(one week) or 4d5h (four days plus five hours) or 2m3s\n" "(two minutes plus three seconds). The default is 3d.\n\n" "Arguments specify which directories are to be cleaned.\n\n" "Typical usage: stmpclean /tmp /var/tmp\n"); exit(1); } /* * Parse time specification (a la sendmail queue time), return its value in * seconds, or -1 if the spec is invalid. * * Side effects: Modifies contents of timespec. */ int parse_time(timespec) char *timespec; { char *p, *q; char symbol; int result, num, multiple; result = 0; p = timespec; while (*p) { if (!isdigit(*p)) return -1; for (q = p; isdigit(*q); q++); symbol = *q; *q = 0; num = atoi(p); /* * Put it back after atoi() in case someone doesn't read the * comments and decides to use the value again anyway. I * didn't want to have strdup()s here all around, did you? */ *q = symbol; switch (symbol) { case 'w': multiple = SECONDS_IN_A_WEEK; break; case 'd': multiple = SECONDS_IN_A_DAY; break; case 'h': multiple = SECONDS_IN_AN_HOUR; break; case 'm': multiple = SECONDS_IN_A_MINUTE; break; case 's': multiple = 1; break; default: return -1; } result += num * multiple; if (result < 0) return -1; p = q + 1; } return result; } /* Set euid to UID, egid to GID. Exit unsuccessfully on error. */ void setecreds(uid, gid) uid_t uid; gid_t gid; { if (geteuid()) { if ((seteuid(uid) == -1) || (setegid(gid) == -1)) { syslog(LOG_ERR, "cannot set EUID/EGID to %d/%d, exiting", uid, gid); exit(1); } } else { if ((setegid(gid) == -1) || (seteuid(uid) == -1)) { syslog(LOG_ERR, "cannot set EUID/EGID to %d/%d, exiting", uid, gid); exit(1); } } return; } /* * Return 1 if DIR is an empty directory, 0 otherwise. Assumes nothing * changes while we are looking. Exit unsuccessfully on error. */ int isemptydir(dir) char *dir; { DIR *dirp; struct dirent *dp; int result = 1; if ((dirp = opendir(dir)) == NULL) { GETCWD; syslog(LOG_ERR, "RACE?: isemptydir(): opendir(\"%s\") in %s: " "%m, exiting", dir, cwd); exit(1); } while ((dp = readdir(dirp)) != NULL) if (strcmp(dp->d_name, ".") && strcmp(dp->d_name, "..")) { result = 0; break; } closedir(dirp); return result; } /* * Recursively clean directory DIR, descending no deeper than MAX_DEPTH. * Exit with a failure if a race condition is detected. */ void clean_dir(dir, depth) char *dir; int depth; { struct stat st, st_after; int dir_fd, dot_dot_fd; DIR *dirp; struct dirent *dp; if (depth < 0) { /* * We do getcwd() inside error handling blocks for * efficiency. */ GETCWD; syslog(LOG_WARNING, "won't descend to %s from %s: " "reached maximum depth (%d)", dir, cwd, MAX_DEPTH); return; } if (lstat(dir, &st) == -1) { GETCWD; syslog(LOG_ERR, "RACE?: lstat(\"%s\") in %s failed: %m, " "exiting", dir, cwd); exit(1); } if ((st.st_mode & S_IFMT) != S_IFDIR) { GETCWD; syslog(LOG_ERR, "RACE?: %s in %s is not a directory, exiting", dir, cwd); exit(1); } dir_fd = open(dir, O_RDONLY); if (dir_fd == -1) { GETCWD; syslog(LOG_ERR, "RACE?: cannot open(\"%s\"): %m (lstat was OK), " "exiting", dir); exit(1); } if (fstat(dir_fd, &st_after) == -1) { GETCWD; syslog(LOG_ERR, "cannot fstat(%d), pointing to %s in %s: %m, " "exiting", dir_fd, dir, cwd); exit(1); } if (st.st_dev != st_after.st_dev || st.st_ino != st_after.st_ino || st.st_rdev != st_after.st_rdev || st.st_uid != st_after.st_uid || st.st_gid != st_after.st_gid) { GETCWD; syslog(LOG_CRIT, "RACE: %s in %s changed between lstat and " "open, exiting", dir, cwd); exit(1); } /* * We'll chdir up later once done with recursive descend. Hence the * name. */ dot_dot_fd = open(".", O_RDONLY); if (dot_dot_fd == -1) { GETCWD; syslog(LOG_ERR, "open(\".\") in %s: %m, exiting", cwd); exit(1); } if (fchdir(dir_fd) == -1) { GETCWD; syslog(LOG_ERR, "fchdir(\"%d\") [fd to %s] failed in %s: %m, " "exiting", dir_fd, dir, cwd); exit(1); } if (close(dir_fd) == -1) { GETCWD; syslog(LOG_ERR, "close(\"%d\") [fd to ../%s] failed in %s: " "%m, exiting", dir_fd, dir, cwd); exit(1); } /* OK, we are now in the directory to clean. */ if ((dirp = opendir(".")) == NULL) { GETCWD; syslog(LOG_ERR, "RACE?: opendir(\".\") in %s: %m, " "exiting", cwd); exit(1); } while ((dp = readdir(dirp)) != NULL) { /* Ignore "." and ".." entries. */ if (!strcmp(dp->d_name, ".") || !strcmp(dp->d_name, "..")) continue; if (lstat(dp->d_name, &st) == -1) { GETCWD; syslog(LOG_ERR, "RACE?: lstat(\"%s\") in %s: %m, " "exiting", dp->d_name, cwd); exit(1); } if ((st.st_mode & S_IFMT) == S_IFDIR) { /* Looking at a directory. */ if (isemptydir(dp->d_name)) { /* Looking at an empty directory. */ if (now - st.st_mtime > minage && st.st_uid) { /* An old non-root owned directory. */ setecreds(st.st_uid, st.st_gid); if (rmdir(dp->d_name) == -1 && errno != EACCES) { GETCWD; syslog(LOG_ERR, "RACE?: rmdir" "(\"%s\") in %s: %m, " "exiting", dp->d_name, cwd); exit(1); } else if (verbose) { GETCWD; syslog(LOG_INFO, "removed dir %s/%s", cwd, dp->d_name); } setecreds(0, 0); } } else { /* * Looking at a non-empty directory. Clean it * recursively (call ourselves). */ clean_dir(dp->d_name, depth - 1); } } else { /* Looking at a non-directory. */ if ((now - st.st_mtime > minage) && (now - st.st_ctime > minage) && st.st_uid && (st.st_nlink == 1) && (((st.st_mode & S_IFMT) == S_IFREG) || ((st.st_mode & S_IFMT) == S_IFLNK))) { /* * Old non-root owned regular file or * symlink. */ setecreds(st.st_uid, st.st_gid); if (unlink(dp->d_name) == -1) { GETCWD; syslog(LOG_ERR, "RACE?: unlink(\"%s\")" "in %s: %m, exiting", dp->d_name, cwd); /* It's actually safe to continue... */ exit(1); } else if (verbose) { GETCWD; syslog(LOG_INFO, "removed file %s/%s", cwd, dp->d_name); } setecreds(0, 0); } } } closedir(dirp); if (fchdir(dot_dot_fd) == -1) { GETCWD; syslog(LOG_ERR, "fchdir(%d) [fd to \"..\"] in %s: %m, exiting", dot_dot_fd, cwd); exit(1); } close(dot_dot_fd); return; } int main(argc, argv) int argc; char *argv[]; { /* By default, delete files older than three days. */ extern char *optarg; extern int optind; int c, i; struct rlimit rlp; if (argc <= 0) usage(); openlog("stmpclean", LOG_PID | LOG_CONS | LOG_PERROR, LOG_DAEMON); minage = SECONDS_IN_A_DAY * 3; while ((c = getopt(argc, argv, "vt:")) != -1) switch (c) { case 't': minage = parse_time(optarg); if (minage == -1) usage(); break; case 'v': verbose++; break; default: usage(); } argc -= optind; argv += optind; if (argc <= 0) usage(); /* * For logging niceties in case one of the directories on the command * line is bad. */ chdir("/"); now = time(NULL); rlp.rlim_max = 0; rlp.rlim_cur = 0; if (setrlimit(RLIMIT_CORE, &rlp) == -1) { syslog(LOG_ERR, "cannot disable core dumps: setrlimit: %m, exiting"); exit(1); } for (i = 0; i < argc; i++) if (argv[i][0] != '/') { syslog(LOG_ERR, "directories to clean must be" " absolute pathnames, `%s' is not, exiting", argv[i]); exit(1); } for (i = 0; i < argc; i++) clean_dir(argv[i], MAX_DEPTH); exit(0); /* NOTREACHED */ }