/*
* 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 <sys/types.h>
#include <sys/time.h>
#include <sys/resource.h>
#include <sys/stat.h>
#include <sys/param.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <ctype.h>
#include <syslog.h>
#include <fcntl.h>
#include <dirent.h>
#include <time.h>
#include <errno.h>
/*
* 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 <timespec>] dir1 [dir2 [dir3]...]]\n\n"
"Where time specification <timespec> 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 */
}
syntax highlighted by Code2HTML, v. 0.9.1