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