xref: /openbsd-src/usr.sbin/cron/atrun.c (revision 91f110e064cd7c194e59e019b83bb7496c1c84d4)
1 /*	$OpenBSD: atrun.c,v 1.20 2013/11/23 19:18:52 deraadt 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 "cron.h"
24 #include <limits.h>
25 #include <sys/resource.h>
26 
27 static void unlink_job(at_db *, atjob *);
28 static void run_job(atjob *, char *);
29 
30 #ifndef	UID_MAX
31 #define	UID_MAX	INT_MAX
32 #endif
33 #ifndef	GID_MAX
34 #define	GID_MAX	INT_MAX
35 #endif
36 
37 /*
38  * Scan the at jobs dir and build up a list of jobs found.
39  */
40 int
41 scan_atjobs(at_db *old_db, struct timeval *tv)
42 {
43 	DIR *atdir = NULL;
44 	int cwd, queue, pending;
45 	time_t run_time;
46 	char *ep;
47 	at_db new_db;
48 	atjob *job, *tjob;
49 	struct dirent *file;
50 	struct stat statbuf;
51 
52 	Debug(DLOAD, ("[%ld] scan_atjobs()\n", (long)getpid()))
53 
54 	if (stat(AT_DIR, &statbuf) != 0) {
55 		log_it("CRON", getpid(), "CAN'T STAT", AT_DIR);
56 		return (0);
57 	}
58 
59 	if (old_db->mtime == statbuf.st_mtime) {
60 		Debug(DLOAD, ("[%ld] at jobs dir mtime unch, no load needed.\n",
61 		    (long)getpid()))
62 		return (0);
63 	}
64 
65 	/* XXX - would be nice to stash the crontab cwd */
66 	if ((cwd = open(".", O_RDONLY, 0)) < 0) {
67 		log_it("CRON", getpid(), "CAN'T OPEN", ".");
68 		return (0);
69 	}
70 
71 	if (chdir(AT_DIR) != 0 || (atdir = opendir(".")) == NULL) {
72 		if (atdir == NULL)
73 			log_it("CRON", getpid(), "OPENDIR FAILED", AT_DIR);
74 		else
75 			log_it("CRON", getpid(), "CHDIR FAILED", AT_DIR);
76 		fchdir(cwd);
77 		close(cwd);
78 		return (0);
79 	}
80 
81 	new_db.mtime = statbuf.st_mtime;	/* stash at dir mtime */
82 	new_db.head = new_db.tail = NULL;
83 
84 	pending = 0;
85 	while ((file = readdir(atdir)) != NULL) {
86 		if (stat(file->d_name, &statbuf) != 0 ||
87 		    !S_ISREG(statbuf.st_mode))
88 			continue;
89 
90 		/*
91 		 * at jobs are named as RUNTIME.QUEUE
92 		 * RUNTIME is the time to run in seconds since the epoch
93 		 * QUEUE is a letter that designates the job's queue
94 		 */
95 		if (strtot(file->d_name, &ep, &run_time) == -1)
96 			continue;
97 		if (ep[0] != '.' || !isalpha((unsigned char)ep[1]))
98 			continue;
99 		queue = (unsigned char)ep[1];
100 
101 		job = (atjob *)malloc(sizeof(*job));
102 		if (job == NULL) {
103 			for (job = new_db.head; job != NULL; ) {
104 				tjob = job;
105 				job = job->next;
106 				free(tjob);
107 			}
108 			closedir(atdir);
109 			fchdir(cwd);
110 			close(cwd);
111 			return (0);
112 		}
113 		job->uid = statbuf.st_uid;
114 		job->gid = statbuf.st_gid;
115 		job->queue = queue;
116 		job->run_time = run_time;
117 		job->prev = new_db.tail;
118 		job->next = NULL;
119 		if (new_db.head == NULL)
120 			new_db.head = job;
121 		if (new_db.tail != NULL)
122 			new_db.tail->next = job;
123 		new_db.tail = job;
124 		if (tv != NULL && run_time <= tv->tv_sec)
125 			pending = 1;
126 	}
127 	closedir(atdir);
128 
129 	/* Free up old at db */
130 	Debug(DLOAD, ("unlinking old at database:\n"))
131 	for (job = old_db->head; job != NULL; ) {
132 		Debug(DLOAD, ("\t%lld.%c\n", (long long)job->run_time, job->queue))
133 		tjob = job;
134 		job = job->next;
135 		free(tjob);
136 	}
137 
138 	/* Change back to the normal cron dir. */
139 	fchdir(cwd);
140 	close(cwd);
141 
142 	/* Install the new database */
143 	*old_db = new_db;
144 	Debug(DLOAD, ("scan_atjobs is done\n"))
145 
146 	return (pending);
147 }
148 
149 /*
150  * Loop through the at job database and run jobs whose time have come.
151  */
152 void
153 atrun(at_db *db, double batch_maxload, time_t now)
154 {
155 	char atfile[MAX_FNAME];
156 	struct stat statbuf;
157 	double la;
158 	atjob *job, *batch;
159 
160 	Debug(DPROC, ("[%ld] atrun()\n", (long)getpid()))
161 
162 	for (batch = NULL, job = db->head; job; job = job->next) {
163 		/* Skip jobs in the future */
164 		if (job->run_time > now)
165 			continue;
166 
167 		snprintf(atfile, sizeof(atfile), "%s/%lld.%c", AT_DIR,
168 		    (long long)job->run_time, job->queue);
169 
170 		if (stat(atfile, &statbuf) != 0)
171 			unlink_job(db, job);	/* disapeared */
172 
173 		if (!S_ISREG(statbuf.st_mode))
174 			continue;		/* should not happen */
175 
176 		/*
177 		 * Pending jobs have the user execute bit set.
178 		 */
179 		if (statbuf.st_mode & S_IXUSR) 	{
180 			/* new job to run */
181 			if (isupper(job->queue)) {
182 				/* we run one batch job per atrun() call */
183 				if (batch == NULL ||
184 				    job->run_time < batch->run_time)
185 					batch = job;
186 			} else {
187 				/* normal at job */
188 				run_job(job, atfile);
189 				unlink_job(db, job);
190 			}
191 		}
192 	}
193 
194 	/* Run a single batch job if there is one pending. */
195 	if (batch != NULL
196 #ifdef HAVE_GETLOADAVG
197 	    && (batch_maxload == 0.0 ||
198 	    ((getloadavg(&la, 1) == 1) && la <= batch_maxload))
199 #endif
200 	    ) {
201 		snprintf(atfile, sizeof(atfile), "%s/%lld.%c", AT_DIR,
202 		    (long long)batch->run_time, batch->queue);
203 		run_job(batch, atfile);
204 		unlink_job(db, batch);
205 	}
206 }
207 
208 /*
209  * Remove the specified at job from the database.
210  */
211 static void
212 unlink_job(at_db *db, atjob *job)
213 {
214 	if (job->prev == NULL)
215 		db->head = job->next;
216 	else
217 		job->prev->next = job->next;
218 
219 	if (job->next == NULL)
220 		db->tail = job->prev;
221 	else
222 		job->next->prev = job->prev;
223 }
224 
225 /*
226  * Run the specified job contained in atfile.
227  */
228 static void
229 run_job(atjob *job, char *atfile)
230 {
231 	struct stat statbuf;
232 	struct passwd *pw;
233 	pid_t pid;
234 	long nuid, ngid;
235 	FILE *fp;
236 	WAIT_T 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 	Debug(DPROC, ("[%ld] run_job('%s')\n", (long)getpid(), atfile))
244 
245 	/* Open the file and unlink it so we don't try running it again. */
246 	if ((fd = open(atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < OK) {
247 		log_it("CRON", getpid(), "CAN'T OPEN", atfile);
248 		return;
249 	}
250 	unlink(atfile);
251 
252 	/* We don't want the atjobs dir in the log messages. */
253 	if ((cp = strrchr(atfile, '/')) != NULL)
254 		atfile = cp + 1;
255 
256 	/* Fork so other pending jobs don't have to wait for us to finish. */
257 	switch (fork()) {
258 	case 0:
259 		/* child */
260 		break;
261 	case -1:
262 		/* error */
263 		log_it("CRON", getpid(), "error", "can't fork");
264 		/* FALLTHROUGH */
265 	default:
266 		/* parent */
267 		close(fd);
268 		return;
269 	}
270 
271 	acquire_daemonlock(1);			/* close lock fd */
272 
273 	/*
274 	 * We don't want the main cron daemon to wait for our children--
275 	 * we will do it ourselves via waitpid().
276 	 */
277 	(void) signal(SIGCHLD, SIG_DFL);
278 
279 	/*
280 	 * Verify the user still exists and their account has not expired.
281 	 */
282 	pw = getpwuid(job->uid);
283 	if (pw == NULL) {
284 		log_it("CRON", getpid(), "ORPHANED JOB", atfile);
285 		_exit(EXIT_FAILURE);
286 	}
287 #if (defined(BSD)) && (BSD >= 199103)
288 	if (pw->pw_expire && time(NULL) >= pw->pw_expire) {
289 		log_it(pw->pw_name, getpid(), "ACCOUNT EXPIRED, JOB ABORTED",
290 		    atfile);
291 		_exit(EXIT_FAILURE);
292 	}
293 #endif
294 
295 	/* Sanity checks */
296 	if (fstat(fd, &statbuf) < OK) {
297 		log_it(pw->pw_name, getpid(), "FSTAT FAILED", atfile);
298 		_exit(EXIT_FAILURE);
299 	}
300 	if (!S_ISREG(statbuf.st_mode)) {
301 		log_it(pw->pw_name, getpid(), "NOT REGULAR", atfile);
302 		_exit(EXIT_FAILURE);
303 	}
304 	if ((statbuf.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) {
305 		log_it(pw->pw_name, getpid(), "BAD FILE MODE", atfile);
306 		_exit(EXIT_FAILURE);
307 	}
308 	if (statbuf.st_uid != 0 && statbuf.st_uid != job->uid) {
309 		log_it(pw->pw_name, getpid(), "WRONG FILE OWNER", atfile);
310 		_exit(EXIT_FAILURE);
311 	}
312 	if (statbuf.st_nlink > 1) {
313 		log_it(pw->pw_name, getpid(), "BAD LINK COUNT", atfile);
314 		_exit(EXIT_FAILURE);
315 	}
316 
317 	if ((fp = fdopen(dup(fd), "r")) == NULL) {
318 		log_it("CRON", getpid(), "error", "dup(2) failed");
319 		_exit(EXIT_FAILURE);
320 	}
321 
322 	/*
323 	 * Check the at job header for sanity and extract the
324 	 * uid, gid, mailto user and always_mail flag.
325 	 *
326 	 * The header should look like this:
327 	 * #!/bin/sh
328 	 * # atrun uid=123 gid=123
329 	 * # mail                         joeuser 0
330 	 */
331 	if (fgets(buf, sizeof(buf), fp) == NULL ||
332 	    strcmp(buf, "#!/bin/sh\n") != 0 ||
333 	    fgets(buf, sizeof(buf), fp) == NULL ||
334 	    strncmp(buf, "# atrun uid=", 12) != 0)
335 		goto bad_file;
336 
337 	/* Pull out uid */
338 	cp = buf + 12;
339 	errno = 0;
340 	nuid = strtol(cp, &ep, 10);
341 	if (errno == ERANGE || (uid_t)nuid > UID_MAX || cp == ep ||
342 	    strncmp(ep, " gid=", 5) != 0)
343 		goto bad_file;
344 
345 	/* Pull out gid */
346 	cp = ep + 5;
347 	errno = 0;
348 	ngid = strtol(cp, &ep, 10);
349 	if (errno == ERANGE || (gid_t)ngid > GID_MAX || cp == ep || *ep != '\n')
350 		goto bad_file;
351 
352 	/* Pull out mailto user (and always_mail flag) */
353 	if (fgets(buf, sizeof(buf), fp) == NULL ||
354 	    strncmp(buf, "# mail ", 7) != 0)
355 		goto bad_file;
356 	cp = buf + 7;
357 	while (isspace((unsigned char)*cp))
358 		cp++;
359 	ep = cp;
360 	while (!isspace((unsigned char)*ep) && *ep != '\0')
361 		ep++;
362 	if (*ep == '\0' || *ep != ' ' || ep - cp >= sizeof(mailto))
363 		goto bad_file;
364 	memcpy(mailto, cp, ep - cp);
365 	mailto[ep - cp] = '\0';
366 	always_mail = ep[1] == '1';
367 
368 	(void)fclose(fp);
369 	if (!safe_p(pw->pw_name, mailto))
370 		_exit(EXIT_FAILURE);
371 	if ((uid_t)nuid != job->uid) {
372 		log_it(pw->pw_name, getpid(), "UID MISMATCH", atfile);
373 		_exit(EXIT_FAILURE);
374 	}
375 	if ((gid_t)ngid != job->gid) {
376 		log_it(pw->pw_name, getpid(), "GID MISMATCH", atfile);
377 		_exit(EXIT_FAILURE);
378 	}
379 
380 	/* mark ourselves as different to PS command watchers */
381 	setproctitle("atrun %s", atfile);
382 
383 	pipe(output_pipe);	/* child's stdout/stderr */
384 
385 	/* Fork again, child will run the job, parent will catch output. */
386 	switch ((pid = fork())) {
387 	case -1:
388 		log_it("CRON", getpid(), "error", "can't fork");
389 		_exit(EXIT_FAILURE);
390 		/*NOTREACHED*/
391 	case 0:
392 		Debug(DPROC, ("[%ld] grandchild process fork()'ed\n",
393 			      (long)getpid()))
394 
395 		/* Write log message now that we have our real pid. */
396 		log_it(pw->pw_name, getpid(), "ATJOB", atfile);
397 
398 		/* Close log file (or syslog) */
399 		log_close();
400 
401 		/* Connect grandchild's stdin to the at job file. */
402 		if (lseek(fd, (off_t) 0, SEEK_SET) < 0) {
403 			perror("lseek");
404 			_exit(EXIT_FAILURE);
405 		}
406 		if (fd != STDIN_FILENO) {
407 			dup2(fd, STDIN_FILENO);
408 			close(fd);
409 		}
410 
411 		/* Connect stdout/stderr to the pipe from our parent. */
412 		if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) {
413 			dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO);
414 			close(output_pipe[WRITE_PIPE]);
415 		}
416 		dup2(STDOUT_FILENO, STDERR_FILENO);
417 		close(output_pipe[READ_PIPE]);
418 
419 		(void) setsid();
420 
421 #ifdef LOGIN_CAP
422 		{
423 			login_cap_t *lc;
424 # ifdef BSD_AUTH
425 			auth_session_t *as;
426 # endif
427 			if ((lc = login_getclass(pw->pw_class)) == NULL) {
428 				fprintf(stderr,
429 				    "Cannot get login class for %s\n",
430 				    pw->pw_name);
431 				_exit(EXIT_FAILURE);
432 
433 			}
434 
435 			if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) {
436 				fprintf(stderr,
437 				    "setusercontext failed for %s\n",
438 				    pw->pw_name);
439 				_exit(EXIT_FAILURE);
440 			}
441 # ifdef BSD_AUTH
442 			as = auth_open();
443 			if (as == NULL || auth_setpwd(as, pw) != 0) {
444 				fprintf(stderr, "can't malloc\n");
445 				_exit(EXIT_FAILURE);
446 			}
447 			if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) {
448 				fprintf(stderr, "approval failed for %s\n",
449 				    pw->pw_name);
450 				_exit(EXIT_FAILURE);
451 			}
452 			auth_close(as);
453 # endif /* BSD_AUTH */
454 			login_close(lc);
455 		}
456 #else
457 		if (setgid(pw->pw_gid) || initgroups(pw->pw_name, pw->pw_gid)) {
458 			fprintf(stderr,
459 			    "unable to set groups for %s\n", pw->pw_name);
460 			_exit(EXIT_FAILURE);
461 		}
462 #if (defined(BSD)) && (BSD >= 199103)
463 		setlogin(pw->pw_name);
464 #endif
465 		if (setuid(pw->pw_uid)) {
466 			fprintf(stderr, "unable to set uid to %lu\n",
467 			    (unsigned long)pw->pw_uid);
468 			_exit(EXIT_FAILURE);
469 		}
470 
471 #endif /* LOGIN_CAP */
472 
473 		chdir("/");		/* at job will chdir to correct place */
474 
475 		/* If this is a low priority job, nice ourself. */
476 		if (job->queue > 'b')
477 			(void)setpriority(PRIO_PROCESS, 0, job->queue - 'b');
478 
479 #if DEBUGGING
480 		if (DebugFlags & DTEST) {
481 			fprintf(stderr,
482 			    "debug DTEST is on, not exec'ing at job %s\n",
483 			    atfile);
484 			_exit(EXIT_SUCCESS);
485 		}
486 #endif /*DEBUGGING*/
487 
488 		(void) signal(SIGPIPE, SIG_DFL);
489 
490 		/*
491 		 * Exec /bin/sh with stdin connected to the at job file
492 		 * and stdout/stderr hooked up to our parent.
493 		 * The at file will set the environment up for us.
494 		 */
495 		nargv[0] = "sh";
496 		nargv[1] = NULL;
497 		nenvp[0] = NULL;
498 		if (execve(_PATH_BSHELL, nargv, nenvp) != 0) {
499 			perror("execve: " _PATH_BSHELL);
500 			_exit(EXIT_FAILURE);
501 		}
502 		break;
503 	default:
504 		/* parent */
505 		break;
506 	}
507 
508 	Debug(DPROC, ("[%ld] child continues, closing output pipe\n",
509 	    (long)getpid()))
510 
511 	/* Close the atfile's fd and the end of the pipe we don't use. */
512 	close(fd);
513 	close(output_pipe[WRITE_PIPE]);
514 
515 	/* Read piped output (if any) from the at job. */
516 	Debug(DPROC, ("[%ld] child reading output from grandchild\n",
517 	    (long)getpid()))
518 
519 	if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) {
520 		perror("fdopen");
521 		(void) _exit(EXIT_FAILURE);
522 	}
523 	nread = fread(buf, 1, sizeof(buf), fp);
524 	if (nread != 0 || always_mail) {
525 		FILE	*mail;
526 		size_t	bytes = 0;
527 		int	status = 0;
528 		char	mailcmd[MAX_COMMAND];
529 		char	hostname[MAXHOSTNAMELEN];
530 
531 		Debug(DPROC|DEXT, ("[%ld] got data from grandchild\n",
532 		    (long)getpid()))
533 
534 		if (gethostname(hostname, sizeof(hostname)) != 0)
535 			strlcpy(hostname, "unknown", sizeof(hostname));
536 		if (snprintf(mailcmd, sizeof mailcmd,  MAILFMT,
537 		    MAILARG) >= sizeof mailcmd) {
538 			fprintf(stderr, "mailcmd too long\n");
539 			(void) _exit(EXIT_FAILURE);
540 		}
541 		if (!(mail = cron_popen(mailcmd, "w", pw))) {
542 			perror(mailcmd);
543 			(void) _exit(EXIT_FAILURE);
544 		}
545 		fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name);
546 		fprintf(mail, "To: %s\n", mailto);
547 		fprintf(mail, "Subject: Output from \"at\" job\n");
548 		fprintf(mail, "Auto-Submitted: auto-generated\n");
549 #ifdef MAIL_DATE
550 		fprintf(mail, "Date: %s\n", arpadate(&StartTime));
551 #endif /*MAIL_DATE*/
552 		fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s/%s\"\n",
553 		    hostname, CRONDIR, AT_DIR, atfile);
554 		fprintf(mail, "\nproduced the following output:\n\n");
555 
556 		/* Pipe the job's output to sendmail. */
557 		do {
558 			bytes += nread;
559 			fwrite(buf, nread, 1, mail);
560 		} while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
561 
562 		/*
563 		 * If the mailer exits with non-zero exit status, log
564 		 * this fact so the problem can (hopefully) be debugged.
565 		 */
566 		Debug(DPROC, ("[%ld] closing pipe to mail\n",
567 		    (long)getpid()))
568 		if ((status = cron_pclose(mail)) != 0) {
569 			snprintf(buf, sizeof(buf), "mailed %lu byte%s of output"
570 			    " but got status 0x%04x\n", (unsigned long)bytes,
571 			    (bytes == 1) ? "" : "s", status);
572 			log_it(pw->pw_name, getpid(), "MAIL", buf);
573 		}
574 	}
575 	Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long)getpid()))
576 
577 	fclose(fp);	/* also closes output_pipe[READ_PIPE] */
578 
579 	/* Wait for grandchild to die.  */
580 	Debug(DPROC, ("[%ld] waiting for grandchild (%ld) to finish\n",
581 		      (long)getpid(), (long)pid))
582 	for (;;) {
583 		if (waitpid(pid, &waiter, 0) == -1) {
584 			if (errno == EINTR)
585 				continue;
586 			Debug(DPROC,
587 			    ("[%ld] no grandchild process--mail written?\n",
588 			    (long)getpid()))
589 			break;
590 		} else {
591 			Debug(DPROC, ("[%ld] grandchild (%ld) finished, status=%04x",
592 			    (long)getpid(), (long)pid, WEXITSTATUS(waiter)))
593 			if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
594 				Debug(DPROC, (", dumped core"))
595 			Debug(DPROC, ("\n"))
596 			break;
597 		}
598 	}
599 	_exit(EXIT_SUCCESS);
600 
601 bad_file:
602 	log_it(pw->pw_name, getpid(), "BAD FILE FORMAT", atfile);
603 	_exit(EXIT_FAILURE);
604 }
605