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