1 /* $OpenBSD: atrun.c,v 1.18 2011/08/22 19:32:42 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 #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(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 303 } 304 if (!S_ISREG(statbuf.st_mode)) { 305 log_it(pw->pw_name, getpid(), "NOT REGULAR", atfile); 306 _exit(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 315 } 316 if (statbuf.st_nlink > 1) { 317 log_it(pw->pw_name, getpid(), "BAD LINK COUNT", atfile); 318 _exit(EXIT_FAILURE); 319 } 320 321 if ((fp = fdopen(dup(fd), "r")) == NULL) { 322 log_it("CRON", getpid(), "error", "dup(2) failed"); 323 _exit(EXIT_FAILURE); 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(EXIT_FAILURE); 375 if ((uid_t)nuid != job->uid) { 376 log_it(pw->pw_name, getpid(), "UID MISMATCH", atfile); 377 _exit(EXIT_FAILURE); 378 } 379 if ((gid_t)ngid != job->gid) { 380 log_it(pw->pw_name, getpid(), "GID MISMATCH", atfile); 381 _exit(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 409 } 410 if (fd != STDIN_FILENO) { 411 dup2(fd, STDIN_FILENO); 412 close(fd); 413 } 414 415 /* Connect stdout/stderr to the pipe from our parent. */ 416 if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) { 417 dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO); 418 close(output_pipe[WRITE_PIPE]); 419 } 420 dup2(STDOUT_FILENO, STDERR_FILENO); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_FAILURE); 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(EXIT_SUCCESS); 489 } 490 #endif /*DEBUGGING*/ 491 492 (void) signal(SIGPIPE, SIG_DFL); 493 494 /* 495 * Exec /bin/sh with stdin connected to the at job file 496 * and stdout/stderr hooked up to our parent. 497 * The at file will set the environment up for us. 498 */ 499 nargv[0] = "sh"; 500 nargv[1] = NULL; 501 nenvp[0] = NULL; 502 if (execve(_PATH_BSHELL, nargv, nenvp) != 0) { 503 perror("execve: " _PATH_BSHELL); 504 _exit(EXIT_FAILURE); 505 } 506 break; 507 default: 508 /* parent */ 509 break; 510 } 511 512 Debug(DPROC, ("[%ld] child continues, closing output pipe\n", 513 (long)getpid())) 514 515 /* Close the atfile's fd and the end of the pipe we don't use. */ 516 close(fd); 517 close(output_pipe[WRITE_PIPE]); 518 519 /* Read piped output (if any) from the at job. */ 520 Debug(DPROC, ("[%ld] child reading output from grandchild\n", 521 (long)getpid())) 522 523 if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) { 524 perror("fdopen"); 525 (void) _exit(EXIT_FAILURE); 526 } 527 nread = fread(buf, 1, sizeof(buf), fp); 528 if (nread != 0 || always_mail) { 529 FILE *mail; 530 size_t bytes = 0; 531 int status = 0; 532 char mailcmd[MAX_COMMAND]; 533 char hostname[MAXHOSTNAMELEN]; 534 535 Debug(DPROC|DEXT, ("[%ld] got data from grandchild\n", 536 (long)getpid())) 537 538 if (gethostname(hostname, sizeof(hostname)) != 0) 539 strlcpy(hostname, "unknown", sizeof(hostname)); 540 if (snprintf(mailcmd, sizeof mailcmd, MAILFMT, 541 MAILARG) >= sizeof mailcmd) { 542 fprintf(stderr, "mailcmd too long\n"); 543 (void) _exit(EXIT_FAILURE); 544 } 545 if (!(mail = cron_popen(mailcmd, "w", pw))) { 546 perror(mailcmd); 547 (void) _exit(EXIT_FAILURE); 548 } 549 fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name); 550 fprintf(mail, "To: %s\n", mailto); 551 fprintf(mail, "Subject: Output from \"at\" job\n"); 552 fprintf(mail, "Auto-Submitted: auto-generated\n"); 553 #ifdef MAIL_DATE 554 fprintf(mail, "Date: %s\n", arpadate(&StartTime)); 555 #endif /*MAIL_DATE*/ 556 fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s/%s\"\n", 557 hostname, CRONDIR, AT_DIR, atfile); 558 fprintf(mail, "\nproduced the following output:\n\n"); 559 560 /* Pipe the job's output to sendmail. */ 561 do { 562 bytes += nread; 563 fwrite(buf, nread, 1, mail); 564 } while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0); 565 566 /* 567 * If the mailer exits with non-zero exit status, log 568 * this fact so the problem can (hopefully) be debugged. 569 */ 570 Debug(DPROC, ("[%ld] closing pipe to mail\n", 571 (long)getpid())) 572 if ((status = cron_pclose(mail)) != 0) { 573 snprintf(buf, sizeof(buf), "mailed %lu byte%s of output" 574 " but got status 0x%04x\n", (unsigned long)bytes, 575 (bytes == 1) ? "" : "s", status); 576 log_it(pw->pw_name, getpid(), "MAIL", buf); 577 } 578 } 579 Debug(DPROC, ("[%ld] got EOF from grandchild\n", (long)getpid())) 580 581 fclose(fp); /* also closes output_pipe[READ_PIPE] */ 582 583 /* Wait for grandchild to die. */ 584 Debug(DPROC, ("[%ld] waiting for grandchild (%ld) to finish\n", 585 (long)getpid(), (long)pid)) 586 for (;;) { 587 if (waitpid(pid, &waiter, 0) == -1) { 588 if (errno == EINTR) 589 continue; 590 Debug(DPROC, 591 ("[%ld] no grandchild process--mail written?\n", 592 (long)getpid())) 593 break; 594 } else { 595 Debug(DPROC, ("[%ld] grandchild (%ld) finished, status=%04x", 596 (long)getpid(), (long)pid, WEXITSTATUS(waiter))) 597 if (WIFSIGNALED(waiter) && WCOREDUMP(waiter)) 598 Debug(DPROC, (", dumped core")) 599 Debug(DPROC, ("\n")) 600 break; 601 } 602 } 603 _exit(EXIT_SUCCESS); 604 605 bad_file: 606 log_it(pw->pw_name, getpid(), "BAD FILE FORMAT", atfile); 607 _exit(EXIT_FAILURE); 608 } 609