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