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