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