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