xref: /openbsd-src/usr.sbin/cron/atrun.c (revision ae3cb403620ab940fbaabb3055fac045a63d56b7)
1 /*	$OpenBSD: atrun.c,v 1.48 2017/10/25 17:08:58 jca 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(const atjob *, int, const 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 	dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
87 	if (dfd == -1) {
88 		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
89 		return (0);
90 	}
91 	if (fstat(dfd, &sb) != 0) {
92 		syslog(LOG_ERR, "(CRON) FSTAT FAILED (%s)", _PATH_AT_SPOOL);
93 		close(dfd);
94 		return (0);
95 	}
96 	if (old_db != NULL && timespeccmp(&old_db->mtime, &sb.st_mtim, ==)) {
97 		close(dfd);
98 		return (0);
99 	}
100 
101 	if ((atdir = fdopendir(dfd)) == NULL) {
102 		syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_AT_SPOOL);
103 		close(dfd);
104 		return (0);
105 	}
106 
107 	if ((new_db = malloc(sizeof(*new_db))) == NULL) {
108 		closedir(atdir);
109 		return (0);
110 	}
111 	new_db->mtime = sb.st_mtim;	/* stash at dir mtime */
112 	TAILQ_INIT(&new_db->jobs);
113 
114 	pending = 0;
115 	while ((file = readdir(atdir)) != NULL) {
116 		if (fstatat(dfd, file->d_name, &sb, AT_SYMLINK_NOFOLLOW) != 0 ||
117 		    !S_ISREG(sb.st_mode))
118 			continue;
119 
120 		/*
121 		 * at jobs are named as RUNTIME.QUEUE
122 		 * RUNTIME is the time to run in seconds since the epoch
123 		 * QUEUE is a letter that designates the job's queue
124 		 */
125 		if (strtot(file->d_name, &ep, &run_time) == -1)
126 			continue;
127 		if (ep[0] != '.' || !isalpha((unsigned char)ep[1]))
128 			continue;
129 		queue = (unsigned char)ep[1];
130 
131 		job = malloc(sizeof(*job));
132 		if (job == NULL) {
133 			while ((job = TAILQ_FIRST(&new_db->jobs))) {
134 				TAILQ_REMOVE(&new_db->jobs, job, entries);
135 				free(job);
136 			}
137 			free(new_db);
138 			closedir(atdir);
139 			return (0);
140 		}
141 		job->uid = sb.st_uid;
142 		job->gid = sb.st_gid;
143 		job->queue = queue;
144 		job->run_time = run_time;
145 		TAILQ_INSERT_TAIL(&new_db->jobs, job, entries);
146 		if (ts != NULL && run_time <= ts->tv_sec)
147 			pending = 1;
148 	}
149 	closedir(atdir);
150 
151 	/* Free up old at db and install new one */
152 	if (old_db != NULL) {
153 		while ((job = TAILQ_FIRST(&old_db->jobs))) {
154 			TAILQ_REMOVE(&old_db->jobs, job, entries);
155 			free(job);
156 		}
157 		free(old_db);
158 	}
159 	*db = new_db;
160 
161 	return (pending);
162 }
163 
164 /*
165  * Loop through the at job database and run jobs whose time have come.
166  */
167 void
168 atrun(at_db *db, double batch_maxload, time_t now)
169 {
170 	char atfile[PATH_MAX];
171 	struct stat sb;
172 	double la;
173 	int dfd, len;
174 	atjob *job, *tjob, *batch = NULL;
175 
176 	if (db == NULL)
177 		return;
178 
179 	dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
180 	if (dfd == -1) {
181 		syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL);
182 		return;
183 	}
184 
185 	TAILQ_FOREACH_SAFE(job, &db->jobs, entries, tjob) {
186 		/* Skip jobs in the future */
187 		if (job->run_time > now)
188 			continue;
189 
190 		len = snprintf(atfile, sizeof(atfile), "%lld.%c",
191 		    (long long)job->run_time, job->queue);
192 		if (len >= sizeof(atfile)) {
193 			TAILQ_REMOVE(&db->jobs, job, entries);
194 			free(job);
195 			continue;
196 		}
197 
198 		if (fstatat(dfd, atfile, &sb, AT_SYMLINK_NOFOLLOW) != 0) {
199 			TAILQ_REMOVE(&db->jobs, job, entries);
200 			free(job);
201 			continue;		/* disapeared from queue */
202 		}
203 		if (!S_ISREG(sb.st_mode)) {
204 			syslog(LOG_WARNING, "(CRON) NOT REGULAR (%s)",
205 			    atfile);
206 			TAILQ_REMOVE(&db->jobs, job, entries);
207 			free(job);
208 			continue;		/* was a file, no longer is */
209 		}
210 
211 		/*
212 		 * Pending jobs have the user execute bit set.
213 		 */
214 		if (sb.st_mode & S_IXUSR) {
215 			/* new job to run */
216 			if (isupper(job->queue)) {
217 				/* we run one batch job per atrun() call */
218 				if (batch == NULL ||
219 				    job->run_time < batch->run_time)
220 					batch = job;
221 			} else {
222 				/* normal at job */
223 				run_job(job, dfd, atfile);
224 				TAILQ_REMOVE(&db->jobs, job, entries);
225 				free(job);
226 			}
227 		}
228 	}
229 
230 	/* Run a single batch job if there is one pending. */
231 	if (batch != NULL
232 	    && (batch_maxload == 0.0 ||
233 	    ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
234 	    ) {
235 		len = snprintf(atfile, sizeof(atfile), "%lld.%c",
236 		    (long long)batch->run_time, batch->queue);
237 		if (len < sizeof(atfile))
238 			run_job(batch, dfd, atfile);
239 		TAILQ_REMOVE(&db->jobs, batch, entries);
240 		free(job);
241 	}
242 
243 	close(dfd);
244 }
245 
246 /*
247  * Run the specified job contained in atfile.
248  */
249 static void
250 run_job(const atjob *job, int dfd, const char *atfile)
251 {
252 	struct stat sb;
253 	struct passwd *pw;
254 	login_cap_t *lc;
255 	auth_session_t *as;
256 	pid_t pid;
257 	long nuid, ngid;
258 	FILE *fp;
259 	int waiter;
260 	size_t nread;
261 	char *cp, *ep, mailto[MAX_UNAME], buf[BUFSIZ];
262 	int fd, always_mail;
263 	int output_pipe[2];
264 	char *nargv[2], *nenvp[1];
265 
266 	/* Open the file and unlink it so we don't try running it again. */
267 	if ((fd = openat(dfd, atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < 0) {
268 		syslog(LOG_ERR, "(CRON) CAN'T OPEN (%s)", atfile);
269 		return;
270 	}
271 	unlinkat(dfd, atfile, 0);
272 
273 	/* Fork so other pending jobs don't have to wait for us to finish. */
274 	switch (fork()) {
275 	case 0:
276 		/* child */
277 		break;
278 	case -1:
279 		/* error */
280 		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
281 		/* FALLTHROUGH */
282 	default:
283 		/* parent */
284 		close(fd);
285 		return;
286 	}
287 
288 	/* Close fds opened by the parent. */
289 	close(cronSock);
290 	close(dfd);
291 
292 	/*
293 	 * We don't want the main cron daemon to wait for our children--
294 	 * we will do it ourselves via waitpid().
295 	 */
296 	(void) signal(SIGCHLD, SIG_DFL);
297 
298 	/*
299 	 * Verify the user still exists and their account has not expired.
300 	 */
301 	pw = getpwuid(job->uid);
302 	if (pw == NULL) {
303 		syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile);
304 		_exit(EXIT_FAILURE);
305 	}
306 	if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
307 		syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)",
308 		    pw->pw_name, atfile);
309 		_exit(EXIT_FAILURE);
310 	}
311 
312 	/* Sanity checks */
313 	if (fstat(fd, &sb) < 0) {
314 		syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", pw->pw_name, atfile);
315 		_exit(EXIT_FAILURE);
316 	}
317 	if (!S_ISREG(sb.st_mode)) {
318 		syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", pw->pw_name,
319 		    atfile);
320 		_exit(EXIT_FAILURE);
321 	}
322 	if ((sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
323 		syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", pw->pw_name,
324 		    atfile);
325 		_exit(EXIT_FAILURE);
326 	}
327 	if (sb.st_uid != 0 && sb.st_uid != job->uid) {
328 		syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", pw->pw_name,
329 		    atfile);
330 		_exit(EXIT_FAILURE);
331 	}
332 	if (sb.st_gid != cron_gid) {
333 		syslog(LOG_WARNING, "(%s) WRONG FILE GROUP (%s)", pw->pw_name,
334 		    atfile);
335 		_exit(EXIT_FAILURE);
336 	}
337 	if (sb.st_nlink > 1) {
338 		syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", pw->pw_name,
339 		    atfile);
340 		_exit(EXIT_FAILURE);
341 	}
342 
343 	if ((fp = fdopen(dup(fd), "r")) == NULL) {
344 		syslog(LOG_ERR, "(CRON) DUP FAILED (%m)");
345 		_exit(EXIT_FAILURE);
346 	}
347 
348 	/*
349 	 * Check the at job header for sanity and extract the
350 	 * uid, gid, mailto user and always_mail flag.
351 	 *
352 	 * The header should look like this:
353 	 * #!/bin/sh
354 	 * # atrun uid=123 gid=123
355 	 * # mail                         joeuser 0
356 	 */
357 	if (fgets(buf, sizeof(buf), fp) == NULL ||
358 	    strcmp(buf, "#!/bin/sh\n") != 0 ||
359 	    fgets(buf, sizeof(buf), fp) == NULL ||
360 	    strncmp(buf, "# atrun uid=", 12) != 0)
361 		goto bad_file;
362 
363 	/* Pull out uid */
364 	cp = buf + 12;
365 	errno = 0;
366 	nuid = strtol(cp, &ep, 10);
367 	if (errno == ERANGE || (uid_t)nuid > UID_MAX || cp == ep ||
368 	    strncmp(ep, " gid=", 5) != 0)
369 		goto bad_file;
370 
371 	/* Pull out gid */
372 	cp = ep + 5;
373 	errno = 0;
374 	ngid = strtol(cp, &ep, 10);
375 	if (errno == ERANGE || (gid_t)ngid > GID_MAX || cp == ep || *ep != '\n')
376 		goto bad_file;
377 
378 	/* Pull out mailto user (and always_mail flag) */
379 	if (fgets(buf, sizeof(buf), fp) == NULL ||
380 	    strncmp(buf, "# mail ", 7) != 0)
381 		goto bad_file;
382 	cp = buf + 7;
383 	while (isspace((unsigned char)*cp))
384 		cp++;
385 	ep = cp;
386 	while (!isspace((unsigned char)*ep) && *ep != '\0')
387 		ep++;
388 	if (*ep == '\0' || *ep != ' ' || ep - cp >= sizeof(mailto))
389 		goto bad_file;
390 	memcpy(mailto, cp, ep - cp);
391 	mailto[ep - cp] = '\0';
392 	always_mail = ep[1] == '1';
393 
394 	(void)fclose(fp);
395 	if (!safe_p(pw->pw_name, mailto))
396 		_exit(EXIT_FAILURE);
397 	if ((uid_t)nuid != job->uid) {
398 		syslog(LOG_WARNING, "(%s) UID MISMATCH (%s)", pw->pw_name,
399 		    atfile);
400 		_exit(EXIT_FAILURE);
401 	}
402 	if ((gid_t)ngid != job->gid) {
403 		syslog(LOG_WARNING, "(%s) GID MISMATCH (%s)", pw->pw_name,
404 		    atfile);
405 		_exit(EXIT_FAILURE);
406 	}
407 
408 	/* mark ourselves as different to PS command watchers */
409 	setproctitle("atrun %s", atfile);
410 
411 	if (pipe(output_pipe) != 0) {	/* child's stdout/stderr */
412 		syslog(LOG_ERR, "(CRON) PIPE (%m)");
413 		_exit(EXIT_FAILURE);
414 	}
415 
416 	/* Fork again, child will run the job, parent will catch output. */
417 	switch ((pid = fork())) {
418 	case -1:
419 		syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)");
420 		_exit(EXIT_FAILURE);
421 		/*NOTREACHED*/
422 	case 0:
423 		/* Write log message now that we have our real pid. */
424 		syslog(LOG_INFO, "(%s) ATJOB (%s)", pw->pw_name, atfile);
425 
426 		/* Connect grandchild's stdin to the at job file. */
427 		if (lseek(fd, 0, SEEK_SET) < 0) {
428 			syslog(LOG_ERR, "(CRON) LSEEK (%m)");
429 			_exit(EXIT_FAILURE);
430 		}
431 		if (fd != STDIN_FILENO) {
432 			dup2(fd, STDIN_FILENO);
433 			close(fd);
434 		}
435 
436 		/* Connect stdout/stderr to the pipe from our parent. */
437 		if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) {
438 			dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO);
439 			close(output_pipe[WRITE_PIPE]);
440 		}
441 		dup2(STDOUT_FILENO, STDERR_FILENO);
442 		close(output_pipe[READ_PIPE]);
443 
444 		(void) setsid();
445 
446 		/*
447 		 * From this point on, anything written to stderr will be
448 		 * mailed to the user as output.
449 		 */
450 
451 		/* Setup execution environment as per login.conf */
452 		if ((lc = login_getclass(pw->pw_class)) == NULL) {
453 			warnx("unable to get login class for %s",
454 			    pw->pw_name);
455 			syslog(LOG_ERR, "(CRON) CAN'T GET LOGIN CLASS (%s)",
456 			    pw->pw_name);
457 			_exit(EXIT_FAILURE);
458 
459 		}
460 		if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
461 			warn("setusercontext failed for %s", pw->pw_name);
462 			syslog(LOG_ERR, "(%s) SETUSERCONTEXT FAILED (%m)",
463 			    pw->pw_name);
464 			_exit(EXIT_FAILURE);
465 		}
466 
467 		/* Run any approval scripts. */
468 		as = auth_open();
469 		if (as == NULL || auth_setpwd(as, pw) != 0) {
470 			warn("auth_setpwd");
471 			syslog(LOG_ERR, "(%s) AUTH_SETPWD FAILED (%m)",
472 			    pw->pw_name);
473 			_exit(EXIT_FAILURE);
474 		}
475 		if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
476 			warnx("approval failed for %s", pw->pw_name);
477 			syslog(LOG_ERR, "(%s) APPROVAL FAILED (cron)",
478 			    pw->pw_name);
479 			_exit(EXIT_FAILURE);
480 		}
481 		auth_close(as);
482 		login_close(lc);
483 
484 		/* If this is a low priority job, nice ourself. */
485 		if (job->queue > 'b') {
486 			if (setpriority(PRIO_PROCESS, 0, job->queue - 'b') != 0)
487 				syslog(LOG_ERR, "(%s) CAN'T NICE (%m)",
488 				    pw->pw_name);
489 		}
490 
491 		(void) signal(SIGPIPE, SIG_DFL);
492 
493 		/*
494 		 * Exec /bin/sh with stdin connected to the at job file
495 		 * and stdout/stderr hooked up to our parent.
496 		 * The at file will set the environment up for us.
497 		 */
498 		nargv[0] = "sh";
499 		nargv[1] = NULL;
500 		nenvp[0] = NULL;
501 		if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
502 			warn("unable to execute %s", _PATH_BSHELL);
503 			syslog(LOG_ERR, "(%s) CAN'T EXEC (%s: %m)", pw->pw_name,
504 			    _PATH_BSHELL);
505 			_exit(EXIT_FAILURE);
506 		}
507 		break;
508 	default:
509 		/* parent */
510 		break;
511 	}
512 
513 	/* Close the atfile's fd and the end of the pipe we don't use. */
514 	close(fd);
515 	close(output_pipe[WRITE_PIPE]);
516 
517 	/* Read piped output (if any) from the at job. */
518 	if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
519 		syslog(LOG_ERR, "(%s) FDOPEN (%m)", pw->pw_name);
520 		(void) _exit(EXIT_FAILURE);
521 	}
522 	nread = fread(buf, 1, sizeof(buf), fp);
523 	if (nread != 0 || always_mail) {
524 		FILE	*mail;
525 		pid_t	mailpid;
526 		size_t	bytes = 0;
527 		int	status = 0;
528 		char	mailcmd[MAX_COMMAND];
529 		char	hostname[HOST_NAME_MAX + 1];
530 
531 		if (gethostname(hostname, sizeof(hostname)) != 0)
532 			strlcpy(hostname, "unknown", sizeof(hostname));
533 		if (snprintf(mailcmd, sizeof mailcmd, MAILFMT,
534 		    MAILARG) >= sizeof mailcmd) {
535 			syslog(LOG_ERR, "(%s) ERROR (mailcmd too long)",
536 			    pw->pw_name);
537 			(void) _exit(EXIT_FAILURE);
538 		}
539 		if (!(mail = cron_popen(mailcmd, "w", pw, &mailpid))) {
540 			syslog(LOG_ERR, "(%s) POPEN (%s)", pw->pw_name, mailcmd);
541 			(void) _exit(EXIT_FAILURE);
542 		}
543 		fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
544 		fprintf(mail, "To: %s\n", mailto);
545 		fprintf(mail, "Subject: Output from \"at\" job\n");
546 		fprintf(mail, "Auto-Submitted: auto-generated\n");
547 		fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s\"\n",
548 		    hostname, _PATH_AT_SPOOL, atfile);
549 		fprintf(mail, "\nproduced the following output:\n\n");
550 
551 		/* Pipe the job's output to sendmail. */
552 		do {
553 			bytes += nread;
554 			fwrite(buf, nread, 1, mail);
555 		} while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
556 
557 		/*
558 		 * If the mailer exits with non-zero exit status, log
559 		 * this fact so the problem can (hopefully) be debugged.
560 		 */
561 		if ((status = cron_pclose(mail, mailpid)) != 0) {
562 			syslog(LOG_NOTICE, "(%s) MAIL (mailed %zu byte%s of "
563 			    "output but got status 0x%04x)", pw->pw_name,
564 			    bytes, (bytes == 1) ? "" : "s", status);
565 		}
566 	}
567 
568 	fclose(fp);	/* also closes output_pipe[READ_PIPE] */
569 
570 	/* Wait for grandchild to die.  */
571 	for (;;) {
572 		if (waitpid(pid, &waiter, 0) == -1) {
573 			if (errno == EINTR)
574 				continue;
575 			break;
576 		} else {
577 			/*
578 			if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
579 				Debug(DPROC, (", dumped core"))
580 			*/
581 			break;
582 		}
583 	}
584 	_exit(EXIT_SUCCESS);
585 
586 bad_file:
587 	syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name, atfile);
588 	_exit(EXIT_FAILURE);
589 }
590