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