xref: /openbsd-src/usr.sbin/cron/atrun.c (revision 3a50f0a93a2072911d0ba6ababa815fb04bf9a71)
1*3a50f0a9Sjmc /*	$OpenBSD: atrun.c,v 1.54 2022/12/28 21:30:16 jmc Exp $	*/
22b139a93Smillert 
32b139a93Smillert /*
4bf198cc6Smillert  * Copyright (c) 2002-2003 Todd C. Miller <millert@openbsd.org>
52b139a93Smillert  *
6e134e629Smillert  * Permission to use, copy, modify, and distribute this software for any
7e134e629Smillert  * purpose with or without fee is hereby granted, provided that the above
8e134e629Smillert  * copyright notice and this permission notice appear in all copies.
92b139a93Smillert  *
10328f1f07Smillert  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11328f1f07Smillert  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12328f1f07Smillert  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13328f1f07Smillert  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14328f1f07Smillert  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15328f1f07Smillert  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16328f1f07Smillert  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17328f1f07Smillert  *
18328f1f07Smillert  * Sponsored in part by the Defense Advanced Research Projects
19328f1f07Smillert  * Agency (DARPA) and Air Force Research Laboratory, Air Force
20328f1f07Smillert  * Materiel Command, USAF, under agreement number F39502-99-1-0512.
212b139a93Smillert  */
222b139a93Smillert 
23fa575ea2Smillert #include <sys/types.h>
242b139a93Smillert #include <sys/resource.h>
25fa575ea2Smillert #include <sys/stat.h>
2695f2b4e2Smillert #include <sys/time.h>
27fa575ea2Smillert #include <sys/wait.h>
28fa575ea2Smillert 
29fa575ea2Smillert #include <bitstring.h>		/* for structs.h */
30fa575ea2Smillert #include <bsd_auth.h>
31fa575ea2Smillert #include <ctype.h>
32fa575ea2Smillert #include <dirent.h>
33b8c5c73dSmillert #include <err.h>
34fa575ea2Smillert #include <errno.h>
35fa575ea2Smillert #include <fcntl.h>
36fa575ea2Smillert #include <limits.h>
37fa575ea2Smillert #include <login_cap.h>
38fa575ea2Smillert #include <pwd.h>
39fa575ea2Smillert #include <signal.h>
40fa575ea2Smillert #include <stdio.h>
41fa575ea2Smillert #include <stdlib.h>
42fa575ea2Smillert #include <string.h>
43a38a3395Smillert #include <syslog.h>
44fa575ea2Smillert #include <time.h>
45fa575ea2Smillert #include <unistd.h>
46fa575ea2Smillert 
47fa575ea2Smillert #include "config.h"
48fa575ea2Smillert #include "pathnames.h"
49fa575ea2Smillert #include "macros.h"
50fa575ea2Smillert #include "structs.h"
51fa575ea2Smillert #include "funcs.h"
52fa575ea2Smillert #include "globals.h"
532b139a93Smillert 
54f64f78fdSmillert static void run_job(const atjob *, int, const char *);
552b139a93Smillert 
562b139a93Smillert /*
572b139a93Smillert  * Scan the at jobs dir and build up a list of jobs found.
582b139a93Smillert  */
592b139a93Smillert int
scan_atjobs(at_db ** db,struct timespec * ts)608a6f8ff3Smillert scan_atjobs(at_db **db, struct timespec *ts)
612b139a93Smillert {
622b139a93Smillert 	DIR *atdir = NULL;
63a8f5ee9fSmillert 	int dfd, pending;
64a8f5ee9fSmillert 	const char *errstr;
651e5c894eSderaadt 	time_t run_time;
66a8f5ee9fSmillert 	char *queue;
678a6f8ff3Smillert 	at_db *new_db, *old_db = *db;
688a6f8ff3Smillert 	atjob *job;
692b139a93Smillert 	struct dirent *file;
70a642adccSmillert 	struct stat sb;
712b139a93Smillert 
72558163bfSjca 	dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
73558163bfSjca 	if (dfd == -1) {
74a38a3395Smillert 		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
75a642adccSmillert 		return (0);
76a642adccSmillert 	}
77a642adccSmillert 	if (fstat(dfd, &sb) != 0) {
78a38a3395Smillert 		syslog(LOG_ERR, "(CRON) FSTAT FAILED (%s)", _PATH_AT_SPOOL);
79a642adccSmillert 		close(dfd);
80a642adccSmillert 		return (0);
81a642adccSmillert 	}
8295f2b4e2Smillert 	if (old_db != NULL && timespeccmp(&old_db->mtime, &sb.st_mtim, ==)) {
83a642adccSmillert 		close(dfd);
842b139a93Smillert 		return (0);
852b139a93Smillert 	}
862b139a93Smillert 
87a642adccSmillert 	if ((atdir = fdopendir(dfd)) == NULL) {
88a38a3395Smillert 		syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_AT_SPOOL);
89a642adccSmillert 		close(dfd);
902b139a93Smillert 		return (0);
912b139a93Smillert 	}
922b139a93Smillert 
938a6f8ff3Smillert 	if ((new_db = malloc(sizeof(*new_db))) == NULL) {
948a6f8ff3Smillert 		closedir(atdir);
958a6f8ff3Smillert 		return (0);
968a6f8ff3Smillert 	}
9795f2b4e2Smillert 	new_db->mtime = sb.st_mtim;	/* stash at dir mtime */
988a6f8ff3Smillert 	TAILQ_INIT(&new_db->jobs);
992b139a93Smillert 
1002b139a93Smillert 	pending = 0;
10120bdca44Smillert 	while ((file = readdir(atdir)) != NULL) {
102a642adccSmillert 		if (fstatat(dfd, file->d_name, &sb, AT_SYMLINK_NOFOLLOW) != 0 ||
103a642adccSmillert 		    !S_ISREG(sb.st_mode))
1042b139a93Smillert 			continue;
1052b139a93Smillert 
1062b139a93Smillert 		/*
1072b139a93Smillert 		 * at jobs are named as RUNTIME.QUEUE
1082b139a93Smillert 		 * RUNTIME is the time to run in seconds since the epoch
1092b139a93Smillert 		 * QUEUE is a letter that designates the job's queue
1102b139a93Smillert 		 */
111a8f5ee9fSmillert 		if ((queue = strchr(file->d_name, '.')) == NULL)
1122b139a93Smillert 			continue;
113a8f5ee9fSmillert 		*queue++ = '\0';
114a8f5ee9fSmillert 		run_time = strtonum(file->d_name, 0, LLONG_MAX, &errstr);
115a8f5ee9fSmillert 		if (errstr != NULL)
1161e5c894eSderaadt 			continue;
117a8f5ee9fSmillert 		if (!isalpha((unsigned char)*queue))
118a8f5ee9fSmillert 			continue;
1192b139a93Smillert 
1204d4e2f5aStedu 		job = malloc(sizeof(*job));
1212b139a93Smillert 		if (job == NULL) {
1228a6f8ff3Smillert 			while ((job = TAILQ_FIRST(&new_db->jobs))) {
1238a6f8ff3Smillert 				TAILQ_REMOVE(&new_db->jobs, job, entries);
1248a6f8ff3Smillert 				free(job);
1252b139a93Smillert 			}
1268a6f8ff3Smillert 			free(new_db);
12720bdca44Smillert 			closedir(atdir);
1282b139a93Smillert 			return (0);
1292b139a93Smillert 		}
130a642adccSmillert 		job->uid = sb.st_uid;
131a642adccSmillert 		job->gid = sb.st_gid;
132a8f5ee9fSmillert 		job->queue = *queue;
1332b139a93Smillert 		job->run_time = run_time;
1348a6f8ff3Smillert 		TAILQ_INSERT_TAIL(&new_db->jobs, job, entries);
135f3ce7c4eSmillert 		if (ts != NULL && run_time <= ts->tv_sec)
1362b139a93Smillert 			pending = 1;
1372b139a93Smillert 	}
1382b139a93Smillert 	closedir(atdir);
1392b139a93Smillert 
1408a6f8ff3Smillert 	/* Free up old at db and install new one */
1418a6f8ff3Smillert 	if (old_db != NULL) {
1428a6f8ff3Smillert 		while ((job = TAILQ_FIRST(&old_db->jobs))) {
1438a6f8ff3Smillert 			TAILQ_REMOVE(&old_db->jobs, job, entries);
1448a6f8ff3Smillert 			free(job);
1452b139a93Smillert 		}
1468a6f8ff3Smillert 		free(old_db);
1478a6f8ff3Smillert 	}
1488a6f8ff3Smillert 	*db = new_db;
1492b139a93Smillert 
1502b139a93Smillert 	return (pending);
1512b139a93Smillert }
1522b139a93Smillert 
1532b139a93Smillert /*
1542b139a93Smillert  * Loop through the at job database and run jobs whose time have come.
1552b139a93Smillert  */
1562b139a93Smillert void
atrun(at_db * db,double batch_maxload,time_t now)1571e5c894eSderaadt atrun(at_db *db, double batch_maxload, time_t now)
1582b139a93Smillert {
15988959323Smillert 	char atfile[PATH_MAX];
160a642adccSmillert 	struct stat sb;
1612b139a93Smillert 	double la;
162f64f78fdSmillert 	int dfd, len;
1638a6f8ff3Smillert 	atjob *job, *tjob, *batch = NULL;
1642b139a93Smillert 
1658a6f8ff3Smillert 	if (db == NULL)
1668a6f8ff3Smillert 		return;
1678a6f8ff3Smillert 
168558163bfSjca 	dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
169558163bfSjca 	if (dfd == -1) {
170f64f78fdSmillert 		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
171f64f78fdSmillert 		return;
172f64f78fdSmillert 	}
173f64f78fdSmillert 
1748a6f8ff3Smillert 	TAILQ_FOREACH_SAFE(job, &db->jobs, entries, tjob) {
1752b139a93Smillert 		/* Skip jobs in the future */
1762b139a93Smillert 		if (job->run_time > now)
1772b139a93Smillert 			continue;
1782b139a93Smillert 
179f64f78fdSmillert 		len = snprintf(atfile, sizeof(atfile), "%lld.%c",
1801e5c894eSderaadt 		    (long long)job->run_time, job->queue);
181515e489cSderaadt 		if (len < 0 || len >= sizeof(atfile)) {
182f64f78fdSmillert 			TAILQ_REMOVE(&db->jobs, job, entries);
183f64f78fdSmillert 			free(job);
184f64f78fdSmillert 			continue;
185f64f78fdSmillert 		}
1862b139a93Smillert 
187e1b228cdSmillert 		if (fstatat(dfd, atfile, &sb, AT_SYMLINK_NOFOLLOW) != 0) {
1888a6f8ff3Smillert 			TAILQ_REMOVE(&db->jobs, job, entries);
1898a6f8ff3Smillert 			free(job);
190*3a50f0a9Sjmc 			continue;		/* disappeared from queue */
191e1b228cdSmillert 		}
192e1b228cdSmillert 		if (!S_ISREG(sb.st_mode)) {
193e1b228cdSmillert 			syslog(LOG_WARNING, "(CRON) NOT REGULAR (%s)",
194e1b228cdSmillert 			    atfile);
195e1b228cdSmillert 			TAILQ_REMOVE(&db->jobs, job, entries);
196e1b228cdSmillert 			free(job);
197e1b228cdSmillert 			continue;		/* was a file, no longer is */
1988a6f8ff3Smillert 		}
1992b139a93Smillert 
2002b139a93Smillert 		/*
2012b139a93Smillert 		 * Pending jobs have the user execute bit set.
2022b139a93Smillert 		 */
203a642adccSmillert 		if (sb.st_mode & S_IXUSR) {
2042b139a93Smillert 			/* new job to run */
2052b139a93Smillert 			if (isupper(job->queue)) {
2062b139a93Smillert 				/* we run one batch job per atrun() call */
2072b139a93Smillert 				if (batch == NULL ||
2082b139a93Smillert 				    job->run_time < batch->run_time)
2092b139a93Smillert 					batch = job;
2102b139a93Smillert 			} else {
2112b139a93Smillert 				/* normal at job */
212f64f78fdSmillert 				run_job(job, dfd, atfile);
2138a6f8ff3Smillert 				TAILQ_REMOVE(&db->jobs, job, entries);
2148a6f8ff3Smillert 				free(job);
2152b139a93Smillert 			}
2162b139a93Smillert 		}
2172b139a93Smillert 	}
2182b139a93Smillert 
2192b139a93Smillert 	/* Run a single batch job if there is one pending. */
220e134e629Smillert 	if (batch != NULL
221e134e629Smillert 	    && (batch_maxload == 0.0 ||
222e134e629Smillert 	    ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
223e134e629Smillert 	    ) {
224f64f78fdSmillert 		len = snprintf(atfile, sizeof(atfile), "%lld.%c",
2251e5c894eSderaadt 		    (long long)batch->run_time, batch->queue);
226515e489cSderaadt 		if (len < 0 || len >= sizeof(atfile))
227515e489cSderaadt 			;
228515e489cSderaadt 		else
229f64f78fdSmillert 			run_job(batch, dfd, atfile);
2308a6f8ff3Smillert 		TAILQ_REMOVE(&db->jobs, batch, entries);
2318a6f8ff3Smillert 		free(job);
2322b139a93Smillert 	}
233f64f78fdSmillert 
234f64f78fdSmillert 	close(dfd);
2352b139a93Smillert }
2362b139a93Smillert 
2372b139a93Smillert /*
238a8f5ee9fSmillert  * Check the at job header for sanity and extract the
239a8f5ee9fSmillert  * uid, gid, mailto user and always_mail flag.
240a8f5ee9fSmillert  *
241a8f5ee9fSmillert  * The header should look like this:
242a8f5ee9fSmillert  * #!/bin/sh
243a8f5ee9fSmillert  * # atrun uid=123 gid=123
244a8f5ee9fSmillert  * # mail                         joeuser 0
245a8f5ee9fSmillert  */
246a8f5ee9fSmillert static int
parse_header(FILE * fp,uid_t * nuid,gid_t * ngid,char * mailto,int * always_mail)247a8f5ee9fSmillert parse_header(FILE *fp, uid_t *nuid, gid_t *ngid, char *mailto, int *always_mail)
248a8f5ee9fSmillert {
249a8f5ee9fSmillert 	char *cp, *ep, *line = NULL;
250a8f5ee9fSmillert 	const char *errstr;
251a8f5ee9fSmillert 	size_t size = 0;
252a8f5ee9fSmillert 	int lineno = 0;
253a8f5ee9fSmillert 	ssize_t len;
254a8f5ee9fSmillert 	int ret = -1;
255a8f5ee9fSmillert 
256a8f5ee9fSmillert 	for (lineno = 1; (len = getline(&line, &size, fp)) != -1; lineno++) {
257a8f5ee9fSmillert 		if (line[--len] != '\n')
258a8f5ee9fSmillert 			break;
259a8f5ee9fSmillert 		line[len] = '\0';
260a8f5ee9fSmillert 
261a8f5ee9fSmillert 		switch (lineno) {
262a8f5ee9fSmillert 		case 1:
263a8f5ee9fSmillert 			if (strcmp(line, "#!/bin/sh") != 0)
264a8f5ee9fSmillert 			    goto done;
265a8f5ee9fSmillert 			break;
266a8f5ee9fSmillert 		case 2:
267a8f5ee9fSmillert 			if (strncmp(line, "# atrun uid=", 12) != 0)
268a8f5ee9fSmillert 			    goto done;
269a8f5ee9fSmillert 
270a8f5ee9fSmillert 			/* Pull out uid */
271a8f5ee9fSmillert 			cp = line + 12;
272a8f5ee9fSmillert 			if ((ep = strchr(cp, ' ')) == NULL)
273a8f5ee9fSmillert 				goto done;
274a8f5ee9fSmillert 			*ep++ = '\0';
275a8f5ee9fSmillert 			*nuid = strtonum(cp, 0, UID_MAX - 1, &errstr);
276a8f5ee9fSmillert 			if (errstr != NULL)
277a8f5ee9fSmillert 				goto done;
278a8f5ee9fSmillert 
279a8f5ee9fSmillert 			/* Pull out gid */
280a8f5ee9fSmillert 			if (strncmp(ep, "gid=", 4) != 0)
281a8f5ee9fSmillert 				goto done;
282a8f5ee9fSmillert 			cp = ep + 4;
283a8f5ee9fSmillert 			*ngid = strtonum(cp, 0, GID_MAX - 1, &errstr);
284a8f5ee9fSmillert 			if (errstr != NULL)
285a8f5ee9fSmillert 				goto done;
286a8f5ee9fSmillert 			break;
287a8f5ee9fSmillert 		case 3:
288a8f5ee9fSmillert 			/* Pull out mailto user (and always_mail flag) */
289a8f5ee9fSmillert 			if (strncmp(line, "# mail ", 7) != 0)
290a8f5ee9fSmillert 				goto done;
291a8f5ee9fSmillert 			for (cp = line + 7; *cp == ' '; cp++)
292a8f5ee9fSmillert 				continue;
293a8f5ee9fSmillert 			if (*cp == '\0')
294a8f5ee9fSmillert 				goto done;
295a8f5ee9fSmillert 			for (ep = cp; *ep != ' ' && *ep != '\0'; ep++)
296a8f5ee9fSmillert 				continue;
297a8f5ee9fSmillert 			if (*ep != ' ')
298a8f5ee9fSmillert 				goto done;
299a8f5ee9fSmillert 			*ep++ = '\0';
300a8f5ee9fSmillert 			if (strlcpy(mailto, cp, MAX_UNAME) >= MAX_UNAME)
301a8f5ee9fSmillert 				goto done;
302a8f5ee9fSmillert 			*always_mail = *ep == '1';
303a8f5ee9fSmillert 
304a8f5ee9fSmillert 			/* success */
305a8f5ee9fSmillert 			ret = 0;
306a8f5ee9fSmillert 			goto done;
307a8f5ee9fSmillert 		default:
308a8f5ee9fSmillert 			/* can't happen */
309a8f5ee9fSmillert 			goto done;
310a8f5ee9fSmillert 		}
311a8f5ee9fSmillert 	}
312a8f5ee9fSmillert done:
313a8f5ee9fSmillert 	free(line);
314a8f5ee9fSmillert 	return ret;
315a8f5ee9fSmillert }
316a8f5ee9fSmillert 
317a8f5ee9fSmillert /*
3182b139a93Smillert  * Run the specified job contained in atfile.
3192b139a93Smillert  */
3202b139a93Smillert static void
run_job(const atjob * job,int dfd,const char * atfile)321f64f78fdSmillert run_job(const atjob *job, int dfd, const char *atfile)
3222b139a93Smillert {
323a642adccSmillert 	struct stat sb;
3242b139a93Smillert 	struct passwd *pw;
325b8c5c73dSmillert 	login_cap_t *lc;
326b8c5c73dSmillert 	auth_session_t *as;
3272b139a93Smillert 	pid_t pid;
328a8f5ee9fSmillert 	uid_t nuid;
329a8f5ee9fSmillert 	gid_t ngid;
3302b139a93Smillert 	FILE *fp;
331afc409ecSmillert 	int waiter;
33272574193Smillert 	size_t nread;
333a8f5ee9fSmillert 	char mailto[MAX_UNAME], buf[BUFSIZ];
3342b139a93Smillert 	int fd, always_mail;
3352b139a93Smillert 	int output_pipe[2];
3362b139a93Smillert 	char *nargv[2], *nenvp[1];
3372b139a93Smillert 
3382b139a93Smillert 	/* Open the file and unlink it so we don't try running it again. */
339b7041c07Sderaadt 	if ((fd = openat(dfd, atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW)) == -1) {
340a38a3395Smillert 		syslog(LOG_ERR, "(CRON) CAN'T OPEN (%s)", atfile);
3412b139a93Smillert 		return;
3422b139a93Smillert 	}
343f64f78fdSmillert 	unlinkat(dfd, atfile, 0);
3440aec96c6Smillert 
3452b139a93Smillert 	/* Fork so other pending jobs don't have to wait for us to finish. */
3462b139a93Smillert 	switch (fork()) {
3472b139a93Smillert 	case 0:
3482b139a93Smillert 		/* child */
3492b139a93Smillert 		break;
3502b139a93Smillert 	case -1:
3512b139a93Smillert 		/* error */
352a38a3395Smillert 		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
3532b139a93Smillert 		/* FALLTHROUGH */
3542b139a93Smillert 	default:
3552b139a93Smillert 		/* parent */
3562b139a93Smillert 		close(fd);
3572b139a93Smillert 		return;
3582b139a93Smillert 	}
3592b139a93Smillert 
3600a66a1feSjca 	/* Close fds opened by the parent. */
3610a66a1feSjca 	close(cronSock);
3620a66a1feSjca 	close(dfd);
3630a66a1feSjca 
3642b139a93Smillert 	/*
3652b139a93Smillert 	 * We don't want the main cron daemon to wait for our children--
3662b139a93Smillert 	 * we will do it ourselves via waitpid().
3672b139a93Smillert 	 */
3682b139a93Smillert 	(void) signal(SIGCHLD, SIG_DFL);
3692b139a93Smillert 
3702b139a93Smillert 	/*
3712b139a93Smillert 	 * Verify the user still exists and their account has not expired.
3722b139a93Smillert 	 */
3732b139a93Smillert 	pw = getpwuid(job->uid);
3742b139a93Smillert 	if (pw == NULL) {
375a38a3395Smillert 		syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile);
37610ee6820Smillert 		_exit(EXIT_FAILURE);
3772b139a93Smillert 	}
3782b139a93Smillert 	if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
379a38a3395Smillert 		syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)",
380a38a3395Smillert 		    pw->pw_name, atfile);
38110ee6820Smillert 		_exit(EXIT_FAILURE);
3822b139a93Smillert 	}
3832b139a93Smillert 
3842b139a93Smillert 	/* Sanity checks */
385df69c215Sderaadt 	if (fstat(fd, &sb) == -1) {
386a38a3395Smillert 		syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", pw->pw_name, atfile);
38710ee6820Smillert 		_exit(EXIT_FAILURE);
3882b139a93Smillert 	}
389a642adccSmillert 	if (!S_ISREG(sb.st_mode)) {
390a38a3395Smillert 		syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", pw->pw_name,
391a38a3395Smillert 		    atfile);
39210ee6820Smillert 		_exit(EXIT_FAILURE);
3932b139a93Smillert 	}
394a642adccSmillert 	if ((sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
395a38a3395Smillert 		syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", pw->pw_name,
396a38a3395Smillert 		    atfile);
39710ee6820Smillert 		_exit(EXIT_FAILURE);
3982b139a93Smillert 	}
399a642adccSmillert 	if (sb.st_uid != 0 && sb.st_uid != job->uid) {
400a38a3395Smillert 		syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", pw->pw_name,
401a38a3395Smillert 		    atfile);
40210ee6820Smillert 		_exit(EXIT_FAILURE);
4032b139a93Smillert 	}
404a9e807deSmillert 	if (sb.st_gid != cron_gid) {
405a9e807deSmillert 		syslog(LOG_WARNING, "(%s) WRONG FILE GROUP (%s)", pw->pw_name,
406a9e807deSmillert 		    atfile);
407a9e807deSmillert 		_exit(EXIT_FAILURE);
408a9e807deSmillert 	}
409a642adccSmillert 	if (sb.st_nlink > 1) {
410a38a3395Smillert 		syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", pw->pw_name,
411a38a3395Smillert 		    atfile);
41210ee6820Smillert 		_exit(EXIT_FAILURE);
4132b139a93Smillert 	}
4142b139a93Smillert 	if ((fp = fdopen(dup(fd), "r")) == NULL) {
415a38a3395Smillert 		syslog(LOG_ERR, "(CRON) DUP FAILED (%m)");
41610ee6820Smillert 		_exit(EXIT_FAILURE);
4172b139a93Smillert 	}
418a8f5ee9fSmillert 	if (parse_header(fp, &nuid, &ngid, mailto, &always_mail) == -1) {
419a8f5ee9fSmillert 		syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name,
420a8f5ee9fSmillert 		    atfile);
421a8f5ee9fSmillert 		_exit(EXIT_FAILURE);
422a8f5ee9fSmillert 	}
4232b139a93Smillert 	(void)fclose(fp);
4242b139a93Smillert 	if (!safe_p(pw->pw_name, mailto))
42510ee6820Smillert 		_exit(EXIT_FAILURE);
4262b139a93Smillert 	if ((uid_t)nuid != job->uid) {
427a38a3395Smillert 		syslog(LOG_WARNING, "(%s) UID MISMATCH (%s)", pw->pw_name,
428a38a3395Smillert 		    atfile);
42910ee6820Smillert 		_exit(EXIT_FAILURE);
4302b139a93Smillert 	}
4312b139a93Smillert 	if ((gid_t)ngid != job->gid) {
432a38a3395Smillert 		syslog(LOG_WARNING, "(%s) GID MISMATCH (%s)", pw->pw_name,
433a38a3395Smillert 		    atfile);
43410ee6820Smillert 		_exit(EXIT_FAILURE);
4352b139a93Smillert 	}
4362b139a93Smillert 
4370aec96c6Smillert 	/* mark ourselves as different to PS command watchers */
4380aec96c6Smillert 	setproctitle("atrun %s", atfile);
4392b139a93Smillert 
4403b9f8275Smillert 	if (pipe(output_pipe) != 0) {	/* child's stdout/stderr */
4413b9f8275Smillert 		syslog(LOG_ERR, "(CRON) PIPE (%m)");
4423b9f8275Smillert 		_exit(EXIT_FAILURE);
4433b9f8275Smillert 	}
4442b139a93Smillert 
4452b139a93Smillert 	/* Fork again, child will run the job, parent will catch output. */
4462b139a93Smillert 	switch ((pid = fork())) {
4472b139a93Smillert 	case -1:
448a38a3395Smillert 		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
44910ee6820Smillert 		_exit(EXIT_FAILURE);
4502b139a93Smillert 		/*NOTREACHED*/
4512b139a93Smillert 	case 0:
4522b139a93Smillert 		/* Write log message now that we have our real pid. */
453a38a3395Smillert 		syslog(LOG_INFO, "(%s) ATJOB (%s)", pw->pw_name, atfile);
4542b139a93Smillert 
4552b139a93Smillert 		/* Connect grandchild's stdin to the at job file. */
456df69c215Sderaadt 		if (lseek(fd, 0, SEEK_SET) == -1) {
457b8c5c73dSmillert 			syslog(LOG_ERR, "(CRON) LSEEK (%m)");
45810ee6820Smillert 			_exit(EXIT_FAILURE);
4592b139a93Smillert 		}
46010ee6820Smillert 		if (fd != STDIN_FILENO) {
46110ee6820Smillert 			dup2(fd, STDIN_FILENO);
4622b139a93Smillert 			close(fd);
4632b139a93Smillert 		}
4642b139a93Smillert 
4652b139a93Smillert 		/* Connect stdout/stderr to the pipe from our parent. */
46610ee6820Smillert 		if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) {
46710ee6820Smillert 			dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO);
4682b139a93Smillert 			close(output_pipe[WRITE_PIPE]);
4692b139a93Smillert 		}
47010ee6820Smillert 		dup2(STDOUT_FILENO, STDERR_FILENO);
4712b139a93Smillert 		close(output_pipe[READ_PIPE]);
4722b139a93Smillert 
4732b139a93Smillert 		(void) setsid();
4742b139a93Smillert 
475b8c5c73dSmillert 		/*
476b8c5c73dSmillert 		 * From this point on, anything written to stderr will be
477b8c5c73dSmillert 		 * mailed to the user as output.
478b8c5c73dSmillert 		 */
479b8c5c73dSmillert 
480b8c5c73dSmillert 		/* Setup execution environment as per login.conf */
4812b139a93Smillert 		if ((lc = login_getclass(pw->pw_class)) == NULL) {
482b8c5c73dSmillert 			warnx("unable to get login class for %s",
483b8c5c73dSmillert 			    pw->pw_name);
484b8c5c73dSmillert 			syslog(LOG_ERR, "(CRON) CAN'T GET LOGIN CLASS (%s)",
4852b139a93Smillert 			    pw->pw_name);
48610ee6820Smillert 			_exit(EXIT_FAILURE);
4872b139a93Smillert 
4882b139a93Smillert 		}
4892b139a93Smillert 		if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
490b8c5c73dSmillert 			warn("setusercontext failed for %s", pw->pw_name);
491b8c5c73dSmillert 			syslog(LOG_ERR, "(%s) SETUSERCONTEXT FAILED (%m)",
4922b139a93Smillert 			    pw->pw_name);
49310ee6820Smillert 			_exit(EXIT_FAILURE);
4942b139a93Smillert 		}
495b8c5c73dSmillert 
496b8c5c73dSmillert 		/* Run any approval scripts. */
4972b139a93Smillert 		as = auth_open();
4982b139a93Smillert 		if (as == NULL || auth_setpwd(as, pw) != 0) {
499b8c5c73dSmillert 			warn("auth_setpwd");
500b8c5c73dSmillert 			syslog(LOG_ERR, "(%s) AUTH_SETPWD FAILED (%m)",
501b8c5c73dSmillert 			    pw->pw_name);
50210ee6820Smillert 			_exit(EXIT_FAILURE);
5032b139a93Smillert 		}
5042b139a93Smillert 		if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
505b8c5c73dSmillert 			warnx("approval failed for %s", pw->pw_name);
506b8c5c73dSmillert 			syslog(LOG_ERR, "(%s) APPROVAL FAILED (cron)",
5072b139a93Smillert 			    pw->pw_name);
50810ee6820Smillert 			_exit(EXIT_FAILURE);
5092b139a93Smillert 		}
5102b139a93Smillert 		auth_close(as);
51163705530Smillert 		login_close(lc);
5122b139a93Smillert 
5132b139a93Smillert 		/* If this is a low priority job, nice ourself. */
514b8c5c73dSmillert 		if (job->queue > 'b') {
515b8c5c73dSmillert 			if (setpriority(PRIO_PROCESS, 0, job->queue - 'b') != 0)
516b8c5c73dSmillert 				syslog(LOG_ERR, "(%s) CAN'T NICE (%m)",
517b8c5c73dSmillert 				    pw->pw_name);
518b8c5c73dSmillert 		}
5192b139a93Smillert 
5208b18fb30Smillert 		(void) signal(SIGPIPE, SIG_DFL);
5218b18fb30Smillert 
5222b139a93Smillert 		/*
5232b139a93Smillert 		 * Exec /bin/sh with stdin connected to the at job file
5242b139a93Smillert 		 * and stdout/stderr hooked up to our parent.
5252b139a93Smillert 		 * The at file will set the environment up for us.
5262b139a93Smillert 		 */
5272b139a93Smillert 		nargv[0] = "sh";
5282b139a93Smillert 		nargv[1] = NULL;
5292b139a93Smillert 		nenvp[0] = NULL;
5302b139a93Smillert 		if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
531b8c5c73dSmillert 			warn("unable to execute %s", _PATH_BSHELL);
532b8c5c73dSmillert 			syslog(LOG_ERR, "(%s) CAN'T EXEC (%s: %m)", pw->pw_name,
533b8c5c73dSmillert 			    _PATH_BSHELL);
53410ee6820Smillert 			_exit(EXIT_FAILURE);
5352b139a93Smillert 		}
5362b139a93Smillert 		break;
5372b139a93Smillert 	default:
5382b139a93Smillert 		/* parent */
5392b139a93Smillert 		break;
5402b139a93Smillert 	}
5412b139a93Smillert 
5422b139a93Smillert 	/* Close the atfile's fd and the end of the pipe we don't use. */
5432b139a93Smillert 	close(fd);
5442b139a93Smillert 	close(output_pipe[WRITE_PIPE]);
5452b139a93Smillert 
5462b139a93Smillert 	/* Read piped output (if any) from the at job. */
54772574193Smillert 	if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
548b8c5c73dSmillert 		syslog(LOG_ERR, "(%s) FDOPEN (%m)", pw->pw_name);
54910ee6820Smillert 		(void) _exit(EXIT_FAILURE);
55072574193Smillert 	}
55172574193Smillert 	nread = fread(buf, 1, sizeof(buf), fp);
55272574193Smillert 	if (nread != 0 || always_mail) {
5532b139a93Smillert 		FILE	*mail;
55494b4a649Stedu 		pid_t	mailpid;
55572574193Smillert 		size_t	bytes = 0;
5562b139a93Smillert 		int	status = 0;
5572b139a93Smillert 		char	mailcmd[MAX_COMMAND];
55875e8a723Smillert 		char	hostname[HOST_NAME_MAX + 1];
5592b139a93Smillert 
5602b139a93Smillert 		if (gethostname(hostname, sizeof(hostname)) != 0)
561f56315c7Smillert 			strlcpy(hostname, "unknown", sizeof(hostname));
5622b139a93Smillert 		if (snprintf(mailcmd, sizeof mailcmd, MAILFMT,
5632b139a93Smillert 		    MAILARG) >= sizeof mailcmd) {
564b8c5c73dSmillert 			syslog(LOG_ERR, "(%s) ERROR (mailcmd too long)",
565b8c5c73dSmillert 			    pw->pw_name);
56610ee6820Smillert 			(void) _exit(EXIT_FAILURE);
5672b139a93Smillert 		}
56894b4a649Stedu 		if (!(mail = cron_popen(mailcmd, "w", pw, &mailpid))) {
569b8c5c73dSmillert 			syslog(LOG_ERR, "(%s) POPEN (%s)", pw->pw_name, mailcmd);
57010ee6820Smillert 			(void) _exit(EXIT_FAILURE);
5712b139a93Smillert 		}
5722b139a93Smillert 		fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
5732b139a93Smillert 		fprintf(mail, "To: %s\n", mailto);
5742b139a93Smillert 		fprintf(mail, "Subject: Output from \"at\" job\n");
57513c10301Smillert 		fprintf(mail, "Auto-Submitted: auto-generated\n");
57688959323Smillert 		fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s\"\n",
57788959323Smillert 		    hostname, _PATH_AT_SPOOL, atfile);
5782b139a93Smillert 		fprintf(mail, "\nproduced the following output:\n\n");
5792b139a93Smillert 
5802b139a93Smillert 		/* Pipe the job's output to sendmail. */
58172574193Smillert 		do {
5822b139a93Smillert 			bytes += nread;
5832b139a93Smillert 			fwrite(buf, nread, 1, mail);
58472574193Smillert 		} while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
5852b139a93Smillert 
5862b139a93Smillert 		/*
5872b139a93Smillert 		 * If the mailer exits with non-zero exit status, log
5882b139a93Smillert 		 * this fact so the problem can (hopefully) be debugged.
5892b139a93Smillert 		 */
59094b4a649Stedu 		if ((status = cron_pclose(mail, mailpid)) != 0) {
591a38a3395Smillert 			syslog(LOG_NOTICE, "(%s) MAIL (mailed %zu byte%s of "
592a38a3395Smillert 			    "output but got status 0x%04x)", pw->pw_name,
593a38a3395Smillert 			    bytes, (bytes == 1) ? "" : "s", status);
5942b139a93Smillert 		}
5952b139a93Smillert 	}
5962b139a93Smillert 
5972b139a93Smillert 	fclose(fp);	/* also closes output_pipe[READ_PIPE] */
5982b139a93Smillert 
5992b139a93Smillert 	/* Wait for grandchild to die.  */
6002b139a93Smillert 	for (;;) {
6012b139a93Smillert 		if (waitpid(pid, &waiter, 0) == -1) {
6022b139a93Smillert 			if (errno == EINTR)
6032b139a93Smillert 				continue;
6042b139a93Smillert 			break;
6052b139a93Smillert 		} else {
606585d9be6Stedu 			/*
6072b139a93Smillert 			if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
6082b139a93Smillert 				Debug(DPROC, (", dumped core"))
609585d9be6Stedu 			*/
6102b139a93Smillert 			break;
6112b139a93Smillert 		}
6122b139a93Smillert 	}
61310ee6820Smillert 	_exit(EXIT_SUCCESS);
6142b139a93Smillert }
615