OpenBSD-4.6/usr.sbin/cron/atrun.c

Compare this file to the similar file:
Show the results in this format:

/*	$OpenBSD: atrun.c,v 1.15 2006/08/13 20:44:00 millert Exp $	*/

/*
 * Copyright (c) 2002-2003 Todd C. Miller <Todd.Miller@courtesan.com>
 *
 * Permission to use, copy, modify, and distribute this software for any
 * purpose with or without fee is hereby granted, provided that the above
 * copyright notice and this permission notice appear in all copies.
 *
 * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
 * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
 *
 * Sponsored in part by the Defense Advanced Research Projects
 * Agency (DARPA) and Air Force Research Laboratory, Air Force
 * Materiel Command, USAF, under agreement number F39502-99-1-0512.
 */

#if !defined(lint) && !defined(LINT)
static const char rcsid[] = "$OpenBSD: atrun.c,v 1.15 2006/08/13 20:44:00 millert Exp $";
#endif

#include "cron.h"
#include <limits.h>
#include <sys/resource.h>

static void unlink_job(at_db *, atjob *);
static void run_job(atjob *, char *);

#ifndef	UID_MAX
#define	UID_MAX	INT_MAX
#endif
#ifndef	GID_MAX
#define	GID_MAX	INT_MAX
#endif

/*
 * Scan the at jobs dir and build up a list of jobs found.
 */
int
scan_atjobs(at_db *old_db, struct timeval *tv)
{
	DIR *atdir = NULL;
	int cwd, queue, pending;
	long l;
	TIME_T run_time;
	char *ep;
	at_db new_db;
	atjob *job, *tjob;
	struct dirent *file;
	struct stat statbuf;

	Debug(DLOAD, ("[%ld] scan_atjobs()\n", (long)getpid()))

	if (stat(AT_DIR, &statbuf) != 0) {
		log_it("CRON", getpid(), "CAN'T STAT", AT_DIR);
		return (0);
	}

	if (old_db->mtime == statbuf.st_mtime) {
		Debug(DLOAD, ("[%ld] at jobs dir mtime unch, no load needed.\n",
		    (long)getpid()))
		return (0);
	}

	/* XXX - would be nice to stash the crontab cwd */
	if ((cwd = open(".", O_RDONLY, 0)) < 0) {
		log_it("CRON", getpid(), "CAN'T OPEN", ".");
		return (0);
	}

	if (chdir(AT_DIR) != 0 || (atdir = opendir(".")) == NULL) {
		if (atdir == NULL)
			log_it("CRON", getpid(), "OPENDIR FAILED", AT_DIR);
		else
			log_it("CRON", getpid(), "CHDIR FAILED", AT_DIR);
		fchdir(cwd);
		close(cwd);
		return (0);
	}

	new_db.mtime = statbuf.st_mtime;	/* stash at dir mtime */
	new_db.head = new_db.tail = NULL;

	pending = 0;
	while ((file = readdir(atdir)) != NULL) {
		if (stat(file->d_name, &statbuf) != 0 ||
		    !S_ISREG(statbuf.st_mode))
			continue;

		/*
		 * at jobs are named as RUNTIME.QUEUE
		 * RUNTIME is the time to run in seconds since the epoch
		 * QUEUE is a letter that designates the job's queue
		 */
		l = strtol(file->d_name, &ep, 10);
		if (ep[0] != '.' || !isalpha((unsigned char)ep[1]) || l < 0 ||
		    l >= INT_MAX)
			continue;
		run_time = (TIME_T)l;
		queue = ep[1];
		if (!isalpha(queue))
			continue;

		job = (atjob *)malloc(sizeof(*job));
		if (job == NULL) {
			for (job = new_db.head; job != NULL; ) {
				tjob = job;
				job = job->next;
				free(tjob);
			}
			closedir(atdir);
			fchdir(cwd);
			close(cwd);
			return (0);
		}
		job->uid = statbuf.st_uid;
		job->gid = statbuf.st_gid;
		job->queue = queue;
		job->run_time = run_time;
		job->prev = new_db.tail;
		job->next = NULL;
		if (new_db.head == NULL)
			new_db.head = job;
		if (new_db.tail != NULL)
			new_db.tail->next = job;
		new_db.tail = job;
		if (tv != NULL && run_time <= tv->tv_sec)
			pending = 1;
	}
	closedir(atdir);

	/* Free up old at db */
	Debug(DLOAD, ("unlinking old at database:\n"))
	for (job = old_db->head; job != NULL; ) {
		Debug(DLOAD, ("\t%ld.%c\n", (long)job->run_time, job->queue))
		tjob = job;
		job = job->next;
		free(tjob);
	}

	/* Change back to the normal cron dir. */
	fchdir(cwd);
	close(cwd);

	/* Install the new database */
	*old_db = new_db;
	Debug(DLOAD, ("scan_atjobs is done\n"))

	return (pending);
}

/*
 * Loop through the at job database and run jobs whose time have come.
 */
void
atrun(at_db *db, double batch_maxload, TIME_T now)
{
	char atfile[MAX_FNAME];
	struct stat statbuf;
	double la;
	atjob *job, *batch;

	Debug(DPROC, ("[%ld] atrun()\n", (long)getpid()))

	for (batch = NULL, job = db->head; job; job = job->next) {
		/* Skip jobs in the future */
		if (job->run_time > now)
			continue;

		snprintf(atfile, sizeof(atfile), "%s/%ld.%c", AT_DIR,
		    (long)job->run_time, job->queue);

		if (stat(atfile, &statbuf) != 0)
			unlink_job(db, job);	/* disapeared */

		if (!S_ISREG(statbuf.st_mode))
			continue;		/* should not happen */

		/*
		 * Pending jobs have the user execute bit set.
		 */
		if (statbuf.st_mode & S_IXUSR) 	{
			/* new job to run */
			if (isupper(job->queue)) {
				/* we run one batch job per atrun() call */
				if (batch == NULL ||
				    job->run_time < batch->run_time)
					batch = job;
			} else {
				/* normal at job */
				run_job(job, atfile);
				unlink_job(db, job);
			}
		}
	}

	/* Run a single batch job if there is one pending. */
	if (batch != NULL
#ifdef HAVE_GETLOADAVG
	    && (batch_maxload == 0.0 ||
	    ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
#endif
	    ) {
		snprintf(atfile, sizeof(atfile), "%s/%ld.%c", AT_DIR,
		    (long)batch->run_time, batch->queue);
		run_job(batch, atfile);
		unlink_job(db, batch);
	}
}

/*
 * Remove the specified at job from the database.
 */
static void
unlink_job(at_db *db, atjob *job)
{
	if (job->prev == NULL)
		db->head = job->next;
	else
		job->prev->next = job->next;

	if (job->next == NULL)
		db->tail = job->prev;
	else
		job->next->prev = job->prev;
}

/*
 * Run the specified job contained in atfile.
 */
static void
run_job(atjob *job, char *atfile)
{
	struct stat statbuf;
	struct passwd *pw;
	pid_t pid;
	long nuid, ngid;
	FILE *fp;
	WAIT_T waiter;
	size_t nread;
	char *cp, *ep, mailto[MAX_UNAME], buf[BUFSIZ];
	int fd, always_mail;
	int output_pipe[2];
	char *nargv[2], *nenvp[1];

	Debug(DPROC, ("[%ld] run_job('%s')\n", (long)getpid(), atfile))

	/* Open the file and unlink it so we don't try running it again. */
	if ((fd = open(atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < OK) {
		log_it("CRON", getpid(), "CAN'T OPEN", atfile);
		return;
	}
	unlink(atfile);

	/* We don't want the atjobs dir in the log messages. */
	if ((cp = strrchr(atfile, '/')) != NULL)
		atfile = cp + 1;

	/* Fork so other pending jobs don't have to wait for us to finish. */
	switch (fork()) {
	case 0:
		/* child */
		break;
	case -1:
		/* error */
		log_it("CRON", getpid(), "error", "can't fork");
		/* FALLTHROUGH */
	default:
		/* parent */
		close(fd);
		return;
	}

	acquire_daemonlock(1);			/* close lock fd */

	/*
	 * We don't want the main cron daemon to wait for our children--
	 * we will do it ourselves via waitpid().
	 */
	(void) signal(SIGCHLD, SIG_DFL);

	/*
	 * Verify the user still exists and their account has not expired.
	 */
	pw = getpwuid(job->uid);
	if (pw == NULL) {
		log_it("CRON", getpid(), "ORPHANED JOB", atfile);
		_exit(ERROR_EXIT);
	}
#if (defined(BSD)) && (BSD >= 199103)
	if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
		log_it(pw->pw_name, getpid(), "ACCOUNT EXPIRED, JOB ABORTED",
		    atfile);
		_exit(ERROR_EXIT);
	}
#endif

	/* Sanity checks */
	if (fstat(fd, &statbuf) < OK) {
		log_it(pw->pw_name, getpid(), "FSTAT FAILED", atfile);
		_exit(ERROR_EXIT);
	}
	if (!S_ISREG(statbuf.st_mode)) {
		log_it(pw->pw_name, getpid(), "NOT REGULAR", atfile);
		_exit(ERROR_EXIT);
	}
	if ((statbuf.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
		log_it(pw->pw_name, getpid(), "BAD FILE MODE", atfile);
		_exit(ERROR_EXIT);
	}
	if (statbuf.st_uid != 0 && statbuf.st_uid != job->uid) {
		log_it(pw->pw_name, getpid(), "WRONG FILE OWNER", atfile);
		_exit(ERROR_EXIT);
	}
	if (statbuf.st_nlink > 1) {
		log_it(pw->pw_name, getpid(), "BAD LINK COUNT", atfile);
		_exit(ERROR_EXIT);
	}

	if ((fp = fdopen(dup(fd), "r")) == NULL) {
		log_it("CRON", getpid(), "error", "dup(2) failed");
		_exit(ERROR_EXIT);
	}

	/*
	 * Check the at job header for sanity and extract the
	 * uid, gid, mailto user and always_mail flag.
	 *
	 * The header should look like this:
	 * #!/bin/sh
	 * # atrun uid=123 gid=123
	 * # mail                         joeuser 0
	 */
	if (fgets(buf, sizeof(buf), fp) == NULL ||
	    strcmp(buf, "#!/bin/sh\n") != 0 ||
	    fgets(buf, sizeof(buf), fp) == NULL ||
	    strncmp(buf, "# atrun uid=", 12) != 0)
		goto bad_file;

	/* Pull out uid */
	cp = buf + 12;
	errno = 0;
	nuid = strtol(cp, &ep, 10);
	if (errno == ERANGE || (uid_t)nuid > UID_MAX || cp == ep ||
	    strncmp(ep, " gid=", 5) != 0)
		goto bad_file;

	/* Pull out gid */
	cp = ep + 5;
	errno = 0;
	ngid = strtol(cp, &ep, 10);
	if (errno == ERANGE || (gid_t)ngid > GID_MAX || cp == ep || *ep != '\n')
		goto bad_file;

	/* Pull out mailto user (and always_mail flag) */
	if (fgets(buf, sizeof(buf), fp) == NULL ||
	    strncmp(buf, "# mail ", 7) != 0)
		goto bad_file;
	cp = buf + 7;
	while (isspace((unsigned char)*cp))
		cp++;
	ep = cp;
	while (!isspace((unsigned char)*ep) && *ep != '\0')
		ep++;
	if (*ep == '\0' || *ep != ' ' || ep - cp >= sizeof(mailto))
		goto bad_file;
	memcpy(mailto, cp, ep - cp);
	mailto[ep - cp] = '\0';
	always_mail = ep[1] == '1';

	(void)fclose(fp);
	if (!safe_p(pw->pw_name, mailto))
		_exit(ERROR_EXIT);
	if ((uid_t)nuid != job->uid) {
		log_it(pw->pw_name, getpid(), "UID MISMATCH", atfile);
		_exit(ERROR_EXIT);
	}
	if ((gid_t)ngid != job->gid) {
		log_it(pw->pw_name, getpid(), "GID MISMATCH", atfile);
		_exit(ERROR_EXIT);
	}

	/* mark ourselves as different to PS command watchers */
	setproctitle("atrun %s", atfile);

	pipe(output_pipe);	/* child's stdout/stderr */

	/* Fork again, child will run the job, parent will catch output. */
	switch ((pid = fork())) {
	case -1:
		log_it("CRON", getpid(), "error", "can't fork");
		_exit(ERROR_EXIT);
		/*NOTREACHED*/
	case 0:
		Debug(DPROC, ("[%ld] grandchild process fork()'ed\n",
			      (long)getpid()))

		/* Write log message now that we have our real pid. */
		log_it(pw->pw_name, getpid(), "ATJOB", atfile);

		/* Close log file (or syslog) */
		log_close();

		/* Connect grandchild's stdin to the at job file. */
		if (lseek(fd, (off_t) 0, SEEK_SET) < 0) {
			perror("lseek");
			_exit(ERROR_EXIT);
		}
		if (fd != STDIN) {
			dup2(fd, STDIN);
			close(fd);
		}

		/* Connect stdout/stderr to the pipe from our parent. */
		if (output_pipe[WRITE_PIPE] != STDOUT) {
			dup2(output_pipe[WRITE_PIPE], STDOUT);
			close(output_pipe[WRITE_PIPE]);
		}
		dup2(STDOUT, STDERR);
		close(output_pipe[READ_PIPE]);

		(void) setsid();

#ifdef LOGIN_CAP
		{
			login_cap_t *lc;
# ifdef BSD_AUTH
			auth_session_t *as;
# endif
			if ((lc = login_getclass(pw->pw_class)) == NULL) {
				fprintf(stderr,
				    "Cannot get login class for %s\n",
				    pw->pw_name);
				_exit(ERROR_EXIT);

			}

			if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
				fprintf(stderr,
				    "setusercontext failed for %s\n",
				    pw->pw_name);
				_exit(ERROR_EXIT);
			}
# ifdef BSD_AUTH
			as = auth_open();
			if (as == NULL || auth_setpwd(as, pw) != 0) {
				fprintf(stderr, "can't malloc\n");
				_exit(ERROR_EXIT);
			}
			if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
				fprintf(stderr, "approval failed for %s\n",
				    pw->pw_name);
				_exit(ERROR_EXIT);
			}
			auth_close(as);
# endif /* BSD_AUTH */
			login_close(lc);
		}
#else
		if (setgid(pw->pw_gid) || initgroups(pw->pw_name, pw->pw_gid)) {
			fprintf(stderr,
			    "unable to set groups for %s\n", pw->pw_name);
			_exit(ERROR_EXIT);
		}
#if (defined(BSD)) && (BSD >= 199103)
		setlogin(pw->pw_name);
#endif
		if (setuid(pw->pw_uid)) {
			fprintf(stderr, "unable to set uid to %lu\n",
			    (unsigned long)pw->pw_uid);
			_exit(ERROR_EXIT);
		}

#endif /* LOGIN_CAP */

		chdir("/");		/* at job will chdir to correct place */

		/* If this is a low priority job, nice ourself. */
		if (job->queue > 'b')
			(void)setpriority(PRIO_PROCESS, 0, job->queue - 'b');

#if DEBUGGING
		if (DebugFlags & DTEST) {
			fprintf(stderr,
			    "debug DTEST is on, not exec'ing at job %s\n",
			    atfile);
			_exit(OK_EXIT);
		}
#endif /*DEBUGGING*/

		/*
		 * Exec /bin/sh with stdin connected to the at job file
		 * and stdout/stderr hooked up to our parent.
		 * The at file will set the environment up for us.
		 */
		nargv[0] = "sh";
		nargv[1] = NULL;
		nenvp[0] = NULL;
		if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
			perror("execve: " _PATH_BSHELL);
			_exit(ERROR_EXIT);
		}
		break;
	default:
		/* parent */
		break;
	}

	Debug(DPROC, ("[%ld] child continues, closing output pipe\n",
	    (long)getpid()))

	/* Close the atfile's fd and the end of the pipe we don't use. */
	close(fd);
	close(output_pipe[WRITE_PIPE]);

	/* Read piped output (if any) from the at job. */
	Debug(DPROC, ("[%ld] child reading output from grandchild\n",
	    (long)getpid()))

	if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
		perror("fdopen");
		(void) _exit(ERROR_EXIT);
	}
	nread = fread(buf, 1, sizeof(buf), fp);
	if (nread != 0 || always_mail) {
		FILE	*mail;
		size_t	bytes = 0;
		int	status = 0;
		char	mailcmd[MAX_COMMAND];
		char	hostname[MAXHOSTNAMELEN];

		Debug(DPROC|DEXT, ("[%ld] got data from grandchild\n",
		    (long)getpid()))

		if (gethostname(hostname, sizeof(hostname)) != 0)
			strlcpy(hostname, "unknown", sizeof(hostname));
		if (snprintf(mailcmd, sizeof mailcmd,  MAILFMT,
		    MAILARG) >= sizeof mailcmd) {
			fprintf(stderr, "mailcmd too long\n");
			(void) _exit(ERROR_EXIT);
		}
		if (!(mail = cron_popen(mailcmd, "w", pw))) {
			perror(mailcmd);
			(void) _exit(ERROR_EXIT);
		}
		fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
		fprintf(mail, "To: %s\n", mailto);
		fprintf(mail, "Subject: Output from \"at\" job\n");
		fprintf(mail, "Auto-Submitted: auto-generated\n");
#ifdef MAIL_DATE
		fprintf(mail, "Date: %s\n", arpadate(&StartTime));
#endif /*MAIL_DATE*/
		fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s/%s\"\n",
		    hostname, CRONDIR, AT_DIR, atfile);
		fprintf(mail, "\nproduced the following output:\n\n");

		/* Pipe the job's output to sendmail. */
		do {
			bytes += nread;
			fwrite(buf, nread, 1, mail);
		} while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);

		/*
		 * If the mailer exits with non-zero exit status, log
		 * this fact so the problem can (hopefully) be debugged.
		 */
		Debug(DPROC, ("[%ld] closing pipe to mail\n",
		    (long)getpid()))
		if ((status = cron_pclose(mail)) != 0) {
			snprintf(buf, sizeof(buf), "mailed %lu byte%s of output"
			    " but got status 0x%04x\n", (unsigned long)bytes,
			    (bytes == 1) ? "" : "s", status);
			log_it(pw->pw_name, getpid(), "MAIL", buf);
		}
	}
	Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long)getpid()))

	fclose(fp);	/* also closes output_pipe[READ_PIPE] */

	/* Wait for grandchild to die.  */
	Debug(DPROC, ("[%ld] waiting for grandchild (%ld) to finish\n",
		      (long)getpid(), (long)pid))
	for (;;) {
		if (waitpid(pid, &waiter, 0) == -1) {
			if (errno == EINTR)
				continue;
			Debug(DPROC,
			    ("[%ld] no grandchild process--mail written?\n",
			    (long)getpid()))
			break;
		} else {
			Debug(DPROC, ("[%ld] grandchild (%ld) finished, status=%04x",
			    (long)getpid(), (long)pid, WEXITSTATUS(waiter)))
			if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
				Debug(DPROC, (", dumped core"))
			Debug(DPROC, ("\n"))
			break;
		}
	}
	_exit(OK_EXIT);

bad_file:
	log_it(pw->pw_name, getpid(), "BAD FILE FORMAT", atfile);
	_exit(ERROR_EXIT);
}