1 /* $OpenBSD: atrun.c,v 1.14 2005/01/30 20:45:58 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.14 2005/01/30 20:45:58 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
scan_atjobs(at_db * old_db,struct timeval * tv)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
atrun(at_db * db,double batch_maxload,TIME_T now)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
unlink_job(at_db * db,atjob * job)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
run_job(atjob * job,char * atfile)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 #ifdef MAIL_DATE
555 fprintf(mail, "Date: %s\n", arpadate(&StartTime));
556 #endif /*MAIL_DATE*/
557 fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s/%s\"\n",
558 hostname, CRONDIR, AT_DIR, atfile);
559 fprintf(mail, "\nproduced the following output:\n\n");
560
561 /* Pipe the job's output to sendmail. */
562 do {
563 bytes += nread;
564 fwrite(buf, nread, 1, mail);
565 } while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0);
566
567 /*
568 * If the mailer exits with non-zero exit status, log
569 * this fact so the problem can (hopefully) be debugged.
570 */
571 Debug(DPROC, ("[%ld] closing pipe to mail\n",
572 (long)getpid()))
573 if ((status = cron_pclose(mail)) != 0) {
574 snprintf(buf, sizeof(buf), "mailed %lu byte%s of output"
575 " but got status 0x%04x\n", (unsigned long)bytes,
576 (bytes == 1) ? "" : "s", status);
577 log_it(pw->pw_name, getpid(), "MAIL", buf);
578 }
579 }
580 Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long)getpid()))
581
582 fclose(fp); /* also closes output_pipe[READ_PIPE] */
583
584 /* Wait for grandchild to die. */
585 Debug(DPROC, ("[%ld] waiting for grandchild (%ld) to finish\n",
586 (long)getpid(), (long)pid))
587 for (;;) {
588 if (waitpid(pid, &waiter, 0) == -1) {
589 if (errno == EINTR)
590 continue;
591 Debug(DPROC,
592 ("[%ld] no grandchild process--mail written?\n",
593 (long)getpid()))
594 break;
595 } else {
596 Debug(DPROC, ("[%ld] grandchild (%ld) finished, status=%04x",
597 (long)getpid(), (long)pid, WEXITSTATUS(waiter)))
598 if (WIFSIGNALED(waiter) && WCOREDUMP(waiter))
599 Debug(DPROC, (", dumped core"))
600 Debug(DPROC, ("\n"))
601 break;
602 }
603 }
604 _exit(OK_EXIT);
605
606 bad_file:
607 log_it(pw->pw_name, getpid(), "BAD FILE FORMAT", atfile);
608 _exit(ERROR_EXIT);
609 }
610