xref: /openbsd-src/usr.sbin/cron/atrun.c (revision f2da64fbbbf1b03f09f390ab01267c93dfd77c4c)
1 /*	$OpenBSD: atrun.c,v 1.43 2016/01/11 14:23:50 millert Exp $	*/
2 
3 /*
4  * Copyright (c) 2002-2003 Todd C. Miller <Todd.Miller@courtesan.com>
5  *
6  * Permission to use, copy, modify, and distribute this software for any
7  * purpose with or without fee is hereby granted, provided that the above
8  * copyright notice and this permission notice appear in all copies.
9  *
10  * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
11  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
12  * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
13  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
14  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
15  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
16  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
17  *
18  * Sponsored in part by the Defense Advanced Research Projects
19  * Agency (DARPA) and Air Force Research Laboratory, Air Force
20  * Materiel Command, USAF, under agreement number F39502-99-1-0512.
21  */
22 
23 #include <sys/types.h>
24 #include <sys/resource.h>
25 #include <sys/stat.h>
26 #include <sys/time.h>
27 #include <sys/wait.h>
28 
29 #include <bitstring.h>		/* for structs.h */
30 #include <bsd_auth.h>
31 #include <ctype.h>
32 #include <dirent.h>
33 #include <err.h>
34 #include <errno.h>
35 #include <fcntl.h>
36 #include <limits.h>
37 #include <login_cap.h>
38 #include <pwd.h>
39 #include <signal.h>
40 #include <stdio.h>
41 #include <stdlib.h>
42 #include <string.h>
43 #include <syslog.h>
44 #include <time.h>
45 #include <unistd.h>
46 
47 #include "config.h"
48 #include "pathnames.h"
49 #include "macros.h"
50 #include "structs.h"
51 #include "funcs.h"
52 #include "globals.h"
53 
54 static void run_job(atjob *, char *);
55 
56 static int
57 strtot(const char *nptr, char **endptr, time_t *tp)
58 {
59 	long long ll;
60 
61 	errno = 0;
62 	ll = strtoll(nptr, endptr, 10);
63 	if (*endptr == nptr)
64 		return (-1);
65 	if (ll < 0 || (errno == ERANGE && ll == LLONG_MAX) || (time_t)ll != ll)
66 		return (-1);
67 	*tp = (time_t)ll;
68 	return (0);
69 }
70 
71 /*
72  * Scan the at jobs dir and build up a list of jobs found.
73  */
74 int
75 scan_atjobs(at_db **db, struct timespec *ts)
76 {
77 	DIR *atdir = NULL;
78 	int dfd, queue, pending;
79 	time_t run_time;
80 	char *ep;
81 	at_db *new_db, *old_db = *db;
82 	atjob *job;
83 	struct dirent *file;
84 	struct stat sb;
85 
86 	if ((dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY)) == -1) {
87 		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
88 		return (0);
89 	}
90 	if (fstat(dfd, &sb) != 0) {
91 		syslog(LOG_ERR, "(CRON) FSTAT FAILED (%s)", _PATH_AT_SPOOL);
92 		close(dfd);
93 		return (0);
94 	}
95 	if (old_db != NULL && timespeccmp(&old_db->mtime, &sb.st_mtim, ==)) {
96 		close(dfd);
97 		return (0);
98 	}
99 
100 	if ((atdir = fdopendir(dfd)) == NULL) {
101 		syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_AT_SPOOL);
102 		close(dfd);
103 		return (0);
104 	}
105 
106 	if ((new_db = malloc(sizeof(*new_db))) == NULL) {
107 		closedir(atdir);
108 		return (0);
109 	}
110 	new_db->mtime = sb.st_mtim;	/* stash at dir mtime */
111 	TAILQ_INIT(&new_db->jobs);
112 
113 	pending = 0;
114 	while ((file = readdir(atdir)) != NULL) {
115 		if (fstatat(dfd, file->d_name, &sb, AT_SYMLINK_NOFOLLOW) != 0 ||
116 		    !S_ISREG(sb.st_mode))
117 			continue;
118 
119 		/*
120 		 * at jobs are named as RUNTIME.QUEUE
121 		 * RUNTIME is the time to run in seconds since the epoch
122 		 * QUEUE is a letter that designates the job's queue
123 		 */
124 		if (strtot(file->d_name, &ep, &run_time) == -1)
125 			continue;
126 		if (ep[0] != '.' || !isalpha((unsigned char)ep[1]))
127 			continue;
128 		queue = (unsigned char)ep[1];
129 
130 		job = malloc(sizeof(*job));
131 		if (job == NULL) {
132 			while ((job = TAILQ_FIRST(&new_db->jobs))) {
133 				TAILQ_REMOVE(&new_db->jobs, job, entries);
134 				free(job);
135 			}
136 			free(new_db);
137 			closedir(atdir);
138 			return (0);
139 		}
140 		job->uid = sb.st_uid;
141 		job->gid = sb.st_gid;
142 		job->queue = queue;
143 		job->run_time = run_time;
144 		TAILQ_INSERT_TAIL(&new_db->jobs, job, entries);
145 		if (ts != NULL && run_time <= ts->tv_sec)
146 			pending = 1;
147 	}
148 	closedir(atdir);
149 
150 	/* Free up old at db and install new one */
151 	if (old_db != NULL) {
152 		while ((job = TAILQ_FIRST(&old_db->jobs))) {
153 			TAILQ_REMOVE(&old_db->jobs, job, entries);
154 			free(job);
155 		}
156 		free(old_db);
157 	}
158 	*db = new_db;
159 
160 	return (pending);
161 }
162 
163 /*
164  * Loop through the at job database and run jobs whose time have come.
165  */
166 void
167 atrun(at_db *db, double batch_maxload, time_t now)
168 {
169 	char atfile[PATH_MAX];
170 	struct stat sb;
171 	double la;
172 	atjob *job, *tjob, *batch = NULL;
173 
174 	if (db == NULL)
175 		return;
176 
177 	TAILQ_FOREACH_SAFE(job, &db->jobs, entries, tjob) {
178 		/* Skip jobs in the future */
179 		if (job->run_time > now)
180 			continue;
181 
182 		snprintf(atfile, sizeof(atfile), "%s/%lld.%c", _PATH_AT_SPOOL,
183 		    (long long)job->run_time, job->queue);
184 
185 		if (lstat(atfile, &sb) != 0 || !S_ISREG(sb.st_mode)) {
186 			TAILQ_REMOVE(&db->jobs, job, entries);
187 			free(job);
188 			continue;		/* disapeared or not a file */
189 		}
190 
191 		/*
192 		 * Pending jobs have the user execute bit set.
193 		 */
194 		if (sb.st_mode & S_IXUSR) {
195 			/* new job to run */
196 			if (isupper(job->queue)) {
197 				/* we run one batch job per atrun() call */
198 				if (batch == NULL ||
199 				    job->run_time < batch->run_time)
200 					batch = job;
201 			} else {
202 				/* normal at job */
203 				run_job(job, atfile);
204 				TAILQ_REMOVE(&db->jobs, job, entries);
205 				free(job);
206 			}
207 		}
208 	}
209 
210 	/* Run a single batch job if there is one pending. */
211 	if (batch != NULL
212 	    && (batch_maxload == 0.0 ||
213 	    ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
214 	    ) {
215 		snprintf(atfile, sizeof(atfile), "%s/%lld.%c", _PATH_AT_SPOOL,
216 		    (long long)batch->run_time, batch->queue);
217 		run_job(batch, atfile);
218 		TAILQ_REMOVE(&db->jobs, batch, entries);
219 		free(job);
220 	}
221 }
222 
223 /*
224  * Run the specified job contained in atfile.
225  */
226 static void
227 run_job(atjob *job, char *atfile)
228 {
229 	struct stat sb;
230 	struct passwd *pw;
231 	login_cap_t *lc;
232 	auth_session_t *as;
233 	pid_t pid;
234 	long nuid, ngid;
235 	FILE *fp;
236 	int waiter;
237 	size_t nread;
238 	char *cp, *ep, mailto[MAX_UNAME], buf[BUFSIZ];
239 	int fd, always_mail;
240 	int output_pipe[2];
241 	char *nargv[2], *nenvp[1];
242 
243 	/* Open the file and unlink it so we don't try running it again. */
244 	if ((fd = open(atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < 0) {
245 		syslog(LOG_ERR, "(CRON) CAN'T OPEN (%s)", atfile);
246 		return;
247 	}
248 	unlink(atfile);
249 
250 	/* We don't want the atjobs dir in the log messages. */
251 	if ((cp = strrchr(atfile, '/')) != NULL)
252 		atfile = cp + 1;
253 
254 	/* Fork so other pending jobs don't have to wait for us to finish. */
255 	switch (fork()) {
256 	case 0:
257 		/* child */
258 		break;
259 	case -1:
260 		/* error */
261 		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
262 		/* FALLTHROUGH */
263 	default:
264 		/* parent */
265 		close(fd);
266 		return;
267 	}
268 
269 	/*
270 	 * We don't want the main cron daemon to wait for our children--
271 	 * we will do it ourselves via waitpid().
272 	 */
273 	(void) signal(SIGCHLD, SIG_DFL);
274 
275 	/*
276 	 * Verify the user still exists and their account has not expired.
277 	 */
278 	pw = getpwuid(job->uid);
279 	if (pw == NULL) {
280 		syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile);
281 		_exit(EXIT_FAILURE);
282 	}
283 	if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
284 		syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)",
285 		    pw->pw_name, atfile);
286 		_exit(EXIT_FAILURE);
287 	}
288 
289 	/* Sanity checks */
290 	if (fstat(fd, &sb) < 0) {
291 		syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", pw->pw_name, atfile);
292 		_exit(EXIT_FAILURE);
293 	}
294 	if (!S_ISREG(sb.st_mode)) {
295 		syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", pw->pw_name,
296 		    atfile);
297 		_exit(EXIT_FAILURE);
298 	}
299 	if ((sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
300 		syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", pw->pw_name,
301 		    atfile);
302 		_exit(EXIT_FAILURE);
303 	}
304 	if (sb.st_uid != 0 && sb.st_uid != job->uid) {
305 		syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", pw->pw_name,
306 		    atfile);
307 		_exit(EXIT_FAILURE);
308 	}
309 	if (sb.st_nlink > 1) {
310 		syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", pw->pw_name,
311 		    atfile);
312 		_exit(EXIT_FAILURE);
313 	}
314 
315 	if ((fp = fdopen(dup(fd), "r")) == NULL) {
316 		syslog(LOG_ERR, "(CRON) DUP FAILED (%m)");
317 		_exit(EXIT_FAILURE);
318 	}
319 
320 	/*
321 	 * Check the at job header for sanity and extract the
322 	 * uid, gid, mailto user and always_mail flag.
323 	 *
324 	 * The header should look like this:
325 	 * #!/bin/sh
326 	 * # atrun uid=123 gid=123
327 	 * # mail                         joeuser 0
328 	 */
329 	if (fgets(buf, sizeof(buf), fp) == NULL ||
330 	    strcmp(buf, "#!/bin/sh\n") != 0 ||
331 	    fgets(buf, sizeof(buf), fp) == NULL ||
332 	    strncmp(buf, "# atrun uid=", 12) != 0)
333 		goto bad_file;
334 
335 	/* Pull out uid */
336 	cp = buf + 12;
337 	errno = 0;
338 	nuid = strtol(cp, &ep, 10);
339 	if (errno == ERANGE || (uid_t)nuid > UID_MAX || cp == ep ||
340 	    strncmp(ep, " gid=", 5) != 0)
341 		goto bad_file;
342 
343 	/* Pull out gid */
344 	cp = ep + 5;
345 	errno = 0;
346 	ngid = strtol(cp, &ep, 10);
347 	if (errno == ERANGE || (gid_t)ngid > GID_MAX || cp == ep || *ep != '\n')
348 		goto bad_file;
349 
350 	/* Pull out mailto user (and always_mail flag) */
351 	if (fgets(buf, sizeof(buf), fp) == NULL ||
352 	    strncmp(buf, "# mail ", 7) != 0)
353 		goto bad_file;
354 	cp = buf + 7;
355 	while (isspace((unsigned char)*cp))
356 		cp++;
357 	ep = cp;
358 	while (!isspace((unsigned char)*ep) && *ep != '\0')
359 		ep++;
360 	if (*ep == '\0' || *ep != ' ' || ep - cp >= sizeof(mailto))
361 		goto bad_file;
362 	memcpy(mailto, cp, ep - cp);
363 	mailto[ep - cp] = '\0';
364 	always_mail = ep[1] == '1';
365 
366 	(void)fclose(fp);
367 	if (!safe_p(pw->pw_name, mailto))
368 		_exit(EXIT_FAILURE);
369 	if ((uid_t)nuid != job->uid) {
370 		syslog(LOG_WARNING, "(%s) UID MISMATCH (%s)", pw->pw_name,
371 		    atfile);
372 		_exit(EXIT_FAILURE);
373 	}
374 	if ((gid_t)ngid != job->gid) {
375 		syslog(LOG_WARNING, "(%s) GID MISMATCH (%s)", pw->pw_name,
376 		    atfile);
377 		_exit(EXIT_FAILURE);
378 	}
379 
380 	/* mark ourselves as different to PS command watchers */
381 	setproctitle("atrun %s", atfile);
382 
383 	if (pipe(output_pipe) != 0) {	/* child's stdout/stderr */
384 		syslog(LOG_ERR, "(CRON) PIPE (%m)");
385 		_exit(EXIT_FAILURE);
386 	}
387 
388 	/* Fork again, child will run the job, parent will catch output. */
389 	switch ((pid = fork())) {
390 	case -1:
391 		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
392 		_exit(EXIT_FAILURE);
393 		/*NOTREACHED*/
394 	case 0:
395 		/* Write log message now that we have our real pid. */
396 		syslog(LOG_INFO, "(%s) ATJOB (%s)", pw->pw_name, atfile);
397 
398 		/* Connect grandchild's stdin to the at job file. */
399 		if (lseek(fd, 0, SEEK_SET) < 0) {
400 			syslog(LOG_ERR, "(CRON) LSEEK (%m)");
401 			_exit(EXIT_FAILURE);
402 		}
403 		if (fd != STDIN_FILENO) {
404 			dup2(fd, STDIN_FILENO);
405 			close(fd);
406 		}
407 
408 		/* Connect stdout/stderr to the pipe from our parent. */
409 		if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) {
410 			dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO);
411 			close(output_pipe[WRITE_PIPE]);
412 		}
413 		dup2(STDOUT_FILENO, STDERR_FILENO);
414 		close(output_pipe[READ_PIPE]);
415 
416 		(void) setsid();
417 
418 		/*
419 		 * From this point on, anything written to stderr will be
420 		 * mailed to the user as output.
421 		 */
422 
423 		/* Setup execution environment as per login.conf */
424 		if ((lc = login_getclass(pw->pw_class)) == NULL) {
425 			warnx("unable to get login class for %s",
426 			    pw->pw_name);
427 			syslog(LOG_ERR, "(CRON) CAN'T GET LOGIN CLASS (%s)",
428 			    pw->pw_name);
429 			_exit(EXIT_FAILURE);
430 
431 		}
432 		if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
433 			warn("setusercontext failed for %s", pw->pw_name);
434 			syslog(LOG_ERR, "(%s) SETUSERCONTEXT FAILED (%m)",
435 			    pw->pw_name);
436 			_exit(EXIT_FAILURE);
437 		}
438 
439 		/* Run any approval scripts. */
440 		as = auth_open();
441 		if (as == NULL || auth_setpwd(as, pw) != 0) {
442 			warn("auth_setpwd");
443 			syslog(LOG_ERR, "(%s) AUTH_SETPWD FAILED (%m)",
444 			    pw->pw_name);
445 			_exit(EXIT_FAILURE);
446 		}
447 		if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
448 			warnx("approval failed for %s", pw->pw_name);
449 			syslog(LOG_ERR, "(%s) APPROVAL FAILED (cron)",
450 			    pw->pw_name);
451 			_exit(EXIT_FAILURE);
452 		}
453 		auth_close(as);
454 		login_close(lc);
455 
456 		/* If this is a low priority job, nice ourself. */
457 		if (job->queue > 'b') {
458 			if (setpriority(PRIO_PROCESS, 0, job->queue - 'b') != 0)
459 				syslog(LOG_ERR, "(%s) CAN'T NICE (%m)",
460 				    pw->pw_name);
461 		}
462 
463 		(void) signal(SIGPIPE, SIG_DFL);
464 
465 		/*
466 		 * Exec /bin/sh with stdin connected to the at job file
467 		 * and stdout/stderr hooked up to our parent.
468 		 * The at file will set the environment up for us.
469 		 */
470 		nargv[0] = "sh";
471 		nargv[1] = NULL;
472 		nenvp[0] = NULL;
473 		if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
474 			warn("unable to execute %s", _PATH_BSHELL);
475 			syslog(LOG_ERR, "(%s) CAN'T EXEC (%s: %m)", pw->pw_name,
476 			    _PATH_BSHELL);
477 			_exit(EXIT_FAILURE);
478 		}
479 		break;
480 	default:
481 		/* parent */
482 		break;
483 	}
484 
485 	/* Close the atfile's fd and the end of the pipe we don't use. */
486 	close(fd);
487 	close(output_pipe[WRITE_PIPE]);
488 
489 	/* Read piped output (if any) from the at job. */
490 	if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
491 		syslog(LOG_ERR, "(%s) FDOPEN (%m)", pw->pw_name);
492 		(void) _exit(EXIT_FAILURE);
493 	}
494 	nread = fread(buf, 1, sizeof(buf), fp);
495 	if (nread != 0 || always_mail) {
496 		FILE	*mail;
497 		pid_t	mailpid;
498 		size_t	bytes = 0;
499 		int	status = 0;
500 		char	mailcmd[MAX_COMMAND];
501 		char	hostname[HOST_NAME_MAX + 1];
502 
503 		if (gethostname(hostname, sizeof(hostname)) != 0)
504 			strlcpy(hostname, "unknown", sizeof(hostname));
505 		if (snprintf(mailcmd, sizeof mailcmd, MAILFMT,
506 		    MAILARG) >= sizeof mailcmd) {
507 			syslog(LOG_ERR, "(%s) ERROR (mailcmd too long)",
508 			    pw->pw_name);
509 			(void) _exit(EXIT_FAILURE);
510 		}
511 		if (!(mail = cron_popen(mailcmd, "w", pw, &mailpid))) {
512 			syslog(LOG_ERR, "(%s) POPEN (%s)", pw->pw_name, mailcmd);
513 			(void) _exit(EXIT_FAILURE);
514 		}
515 		fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
516 		fprintf(mail, "To: %s\n", mailto);
517 		fprintf(mail, "Subject: Output from \"at\" job\n");
518 		fprintf(mail, "Auto-Submitted: auto-generated\n");
519 		fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s\"\n",
520 		    hostname, _PATH_AT_SPOOL, atfile);
521 		fprintf(mail, "\nproduced the following output:\n\n");
522 
523 		/* Pipe the job's output to sendmail. */
524 		do {
525 			bytes += nread;
526 			fwrite(buf, nread, 1, mail);
527 		} while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
528 
529 		/*
530 		 * If the mailer exits with non-zero exit status, log
531 		 * this fact so the problem can (hopefully) be debugged.
532 		 */
533 		if ((status = cron_pclose(mail, mailpid)) != 0) {
534 			syslog(LOG_NOTICE, "(%s) MAIL (mailed %zu byte%s of "
535 			    "output but got status 0x%04x)", pw->pw_name,
536 			    bytes, (bytes == 1) ? "" : "s", status);
537 		}
538 	}
539 
540 	fclose(fp);	/* also closes output_pipe[READ_PIPE] */
541 
542 	/* Wait for grandchild to die.  */
543 	for (;;) {
544 		if (waitpid(pid, &waiter, 0) == -1) {
545 			if (errno == EINTR)
546 				continue;
547 			break;
548 		} else {
549 			/*
550 			if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
551 				Debug(DPROC, (", dumped core"))
552 			*/
553 			break;
554 		}
555 	}
556 	_exit(EXIT_SUCCESS);
557 
558 bad_file:
559 	syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name, atfile);
560 	_exit(EXIT_FAILURE);
561 }
562