1 /* $OpenBSD: atrun.c,v 1.43 2016/01/11 14:23:50 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 <sys/types.h> 24 #include <sys/resource.h> 25 #include <sys/stat.h> 26 #include <sys/time.h> 27 #include <sys/wait.h> 28 29 #include <bitstring.h> /* for structs.h */ 30 #include <bsd_auth.h> 31 #include <ctype.h> 32 #include <dirent.h> 33 #include <err.h> 34 #include <errno.h> 35 #include <fcntl.h> 36 #include <limits.h> 37 #include <login_cap.h> 38 #include <pwd.h> 39 #include <signal.h> 40 #include <stdio.h> 41 #include <stdlib.h> 42 #include <string.h> 43 #include <syslog.h> 44 #include <time.h> 45 #include <unistd.h> 46 47 #include "config.h" 48 #include "pathnames.h" 49 #include "macros.h" 50 #include "structs.h" 51 #include "funcs.h" 52 #include "globals.h" 53 54 static void run_job(atjob *, char *); 55 56 static int 57 strtot(const char *nptr, char **endptr, time_t *tp) 58 { 59 long long ll; 60 61 errno = 0; 62 ll = strtoll(nptr, endptr, 10); 63 if (*endptr == nptr) 64 return (-1); 65 if (ll < 0 || (errno == ERANGE && ll == LLONG_MAX) || (time_t)ll != ll) 66 return (-1); 67 *tp = (time_t)ll; 68 return (0); 69 } 70 71 /* 72 * Scan the at jobs dir and build up a list of jobs found. 73 */ 74 int 75 scan_atjobs(at_db **db, struct timespec *ts) 76 { 77 DIR *atdir = NULL; 78 int dfd, queue, pending; 79 time_t run_time; 80 char *ep; 81 at_db *new_db, *old_db = *db; 82 atjob *job; 83 struct dirent *file; 84 struct stat sb; 85 86 if ((dfd = open(_PATH_AT_SPOOL, O_RDONLY|O_DIRECTORY)) == -1) { 87 syslog(LOG_ERR, "(CRON) OPEN FAILED (%s)", _PATH_AT_SPOOL); 88 return (0); 89 } 90 if (fstat(dfd, &sb) != 0) { 91 syslog(LOG_ERR, "(CRON) FSTAT FAILED (%s)", _PATH_AT_SPOOL); 92 close(dfd); 93 return (0); 94 } 95 if (old_db != NULL && timespeccmp(&old_db->mtime, &sb.st_mtim, ==)) { 96 close(dfd); 97 return (0); 98 } 99 100 if ((atdir = fdopendir(dfd)) == NULL) { 101 syslog(LOG_ERR, "(CRON) OPENDIR FAILED (%s)", _PATH_AT_SPOOL); 102 close(dfd); 103 return (0); 104 } 105 106 if ((new_db = malloc(sizeof(*new_db))) == NULL) { 107 closedir(atdir); 108 return (0); 109 } 110 new_db->mtime = sb.st_mtim; /* stash at dir mtime */ 111 TAILQ_INIT(&new_db->jobs); 112 113 pending = 0; 114 while ((file = readdir(atdir)) != NULL) { 115 if (fstatat(dfd, file->d_name, &sb, AT_SYMLINK_NOFOLLOW) != 0 || 116 !S_ISREG(sb.st_mode)) 117 continue; 118 119 /* 120 * at jobs are named as RUNTIME.QUEUE 121 * RUNTIME is the time to run in seconds since the epoch 122 * QUEUE is a letter that designates the job's queue 123 */ 124 if (strtot(file->d_name, &ep, &run_time) == -1) 125 continue; 126 if (ep[0] != '.' || !isalpha((unsigned char)ep[1])) 127 continue; 128 queue = (unsigned char)ep[1]; 129 130 job = malloc(sizeof(*job)); 131 if (job == NULL) { 132 while ((job = TAILQ_FIRST(&new_db->jobs))) { 133 TAILQ_REMOVE(&new_db->jobs, job, entries); 134 free(job); 135 } 136 free(new_db); 137 closedir(atdir); 138 return (0); 139 } 140 job->uid = sb.st_uid; 141 job->gid = sb.st_gid; 142 job->queue = queue; 143 job->run_time = run_time; 144 TAILQ_INSERT_TAIL(&new_db->jobs, job, entries); 145 if (ts != NULL && run_time <= ts->tv_sec) 146 pending = 1; 147 } 148 closedir(atdir); 149 150 /* Free up old at db and install new one */ 151 if (old_db != NULL) { 152 while ((job = TAILQ_FIRST(&old_db->jobs))) { 153 TAILQ_REMOVE(&old_db->jobs, job, entries); 154 free(job); 155 } 156 free(old_db); 157 } 158 *db = new_db; 159 160 return (pending); 161 } 162 163 /* 164 * Loop through the at job database and run jobs whose time have come. 165 */ 166 void 167 atrun(at_db *db, double batch_maxload, time_t now) 168 { 169 char atfile[PATH_MAX]; 170 struct stat sb; 171 double la; 172 atjob *job, *tjob, *batch = NULL; 173 174 if (db == NULL) 175 return; 176 177 TAILQ_FOREACH_SAFE(job, &db->jobs, entries, tjob) { 178 /* Skip jobs in the future */ 179 if (job->run_time > now) 180 continue; 181 182 snprintf(atfile, sizeof(atfile), "%s/%lld.%c", _PATH_AT_SPOOL, 183 (long long)job->run_time, job->queue); 184 185 if (lstat(atfile, &sb) != 0 || !S_ISREG(sb.st_mode)) { 186 TAILQ_REMOVE(&db->jobs, job, entries); 187 free(job); 188 continue; /* disapeared or not a file */ 189 } 190 191 /* 192 * Pending jobs have the user execute bit set. 193 */ 194 if (sb.st_mode & S_IXUSR) { 195 /* new job to run */ 196 if (isupper(job->queue)) { 197 /* we run one batch job per atrun() call */ 198 if (batch == NULL || 199 job->run_time < batch->run_time) 200 batch = job; 201 } else { 202 /* normal at job */ 203 run_job(job, atfile); 204 TAILQ_REMOVE(&db->jobs, job, entries); 205 free(job); 206 } 207 } 208 } 209 210 /* Run a single batch job if there is one pending. */ 211 if (batch != NULL 212 && (batch_maxload == 0.0 || 213 ((getloadavg(&la, 1) == 1) && la <= batch_maxload)) 214 ) { 215 snprintf(atfile, sizeof(atfile), "%s/%lld.%c", _PATH_AT_SPOOL, 216 (long long)batch->run_time, batch->queue); 217 run_job(batch, atfile); 218 TAILQ_REMOVE(&db->jobs, batch, entries); 219 free(job); 220 } 221 } 222 223 /* 224 * Run the specified job contained in atfile. 225 */ 226 static void 227 run_job(atjob *job, char *atfile) 228 { 229 struct stat sb; 230 struct passwd *pw; 231 login_cap_t *lc; 232 auth_session_t *as; 233 pid_t pid; 234 long nuid, ngid; 235 FILE *fp; 236 int 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 /* Open the file and unlink it so we don't try running it again. */ 244 if ((fd = open(atfile, O_RDONLY|O_NONBLOCK|O_NOFOLLOW, 0)) < 0) { 245 syslog(LOG_ERR, "(CRON) CAN'T OPEN (%s)", atfile); 246 return; 247 } 248 unlink(atfile); 249 250 /* We don't want the atjobs dir in the log messages. */ 251 if ((cp = strrchr(atfile, '/')) != NULL) 252 atfile = cp + 1; 253 254 /* Fork so other pending jobs don't have to wait for us to finish. */ 255 switch (fork()) { 256 case 0: 257 /* child */ 258 break; 259 case -1: 260 /* error */ 261 syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)"); 262 /* FALLTHROUGH */ 263 default: 264 /* parent */ 265 close(fd); 266 return; 267 } 268 269 /* 270 * We don't want the main cron daemon to wait for our children-- 271 * we will do it ourselves via waitpid(). 272 */ 273 (void) signal(SIGCHLD, SIG_DFL); 274 275 /* 276 * Verify the user still exists and their account has not expired. 277 */ 278 pw = getpwuid(job->uid); 279 if (pw == NULL) { 280 syslog(LOG_WARNING, "(CRON) ORPHANED JOB (%s)", atfile); 281 _exit(EXIT_FAILURE); 282 } 283 if (pw->pw_expire && time(NULL) >= pw->pw_expire) { 284 syslog(LOG_NOTICE, "(%s) ACCOUNT EXPIRED, JOB ABORTED (%s)", 285 pw->pw_name, atfile); 286 _exit(EXIT_FAILURE); 287 } 288 289 /* Sanity checks */ 290 if (fstat(fd, &sb) < 0) { 291 syslog(LOG_ERR, "(%s) FSTAT FAILED (%s)", pw->pw_name, atfile); 292 _exit(EXIT_FAILURE); 293 } 294 if (!S_ISREG(sb.st_mode)) { 295 syslog(LOG_WARNING, "(%s) NOT REGULAR (%s)", pw->pw_name, 296 atfile); 297 _exit(EXIT_FAILURE); 298 } 299 if ((sb.st_mode & ALLPERMS) != (S_IRUSR | S_IWUSR | S_IXUSR)) { 300 syslog(LOG_WARNING, "(%s) BAD FILE MODE (%s)", pw->pw_name, 301 atfile); 302 _exit(EXIT_FAILURE); 303 } 304 if (sb.st_uid != 0 && sb.st_uid != job->uid) { 305 syslog(LOG_WARNING, "(%s) WRONG FILE OWNER (%s)", pw->pw_name, 306 atfile); 307 _exit(EXIT_FAILURE); 308 } 309 if (sb.st_nlink > 1) { 310 syslog(LOG_WARNING, "(%s) BAD LINK COUNT (%s)", pw->pw_name, 311 atfile); 312 _exit(EXIT_FAILURE); 313 } 314 315 if ((fp = fdopen(dup(fd), "r")) == NULL) { 316 syslog(LOG_ERR, "(CRON) DUP FAILED (%m)"); 317 _exit(EXIT_FAILURE); 318 } 319 320 /* 321 * Check the at job header for sanity and extract the 322 * uid, gid, mailto user and always_mail flag. 323 * 324 * The header should look like this: 325 * #!/bin/sh 326 * # atrun uid=123 gid=123 327 * # mail joeuser 0 328 */ 329 if (fgets(buf, sizeof(buf), fp) == NULL || 330 strcmp(buf, "#!/bin/sh\n") != 0 || 331 fgets(buf, sizeof(buf), fp) == NULL || 332 strncmp(buf, "# atrun uid=", 12) != 0) 333 goto bad_file; 334 335 /* Pull out uid */ 336 cp = buf + 12; 337 errno = 0; 338 nuid = strtol(cp, &ep, 10); 339 if (errno == ERANGE || (uid_t)nuid > UID_MAX || cp == ep || 340 strncmp(ep, " gid=", 5) != 0) 341 goto bad_file; 342 343 /* Pull out gid */ 344 cp = ep + 5; 345 errno = 0; 346 ngid = strtol(cp, &ep, 10); 347 if (errno == ERANGE || (gid_t)ngid > GID_MAX || cp == ep || *ep != '\n') 348 goto bad_file; 349 350 /* Pull out mailto user (and always_mail flag) */ 351 if (fgets(buf, sizeof(buf), fp) == NULL || 352 strncmp(buf, "# mail ", 7) != 0) 353 goto bad_file; 354 cp = buf + 7; 355 while (isspace((unsigned char)*cp)) 356 cp++; 357 ep = cp; 358 while (!isspace((unsigned char)*ep) && *ep != '\0') 359 ep++; 360 if (*ep == '\0' || *ep != ' ' || ep - cp >= sizeof(mailto)) 361 goto bad_file; 362 memcpy(mailto, cp, ep - cp); 363 mailto[ep - cp] = '\0'; 364 always_mail = ep[1] == '1'; 365 366 (void)fclose(fp); 367 if (!safe_p(pw->pw_name, mailto)) 368 _exit(EXIT_FAILURE); 369 if ((uid_t)nuid != job->uid) { 370 syslog(LOG_WARNING, "(%s) UID MISMATCH (%s)", pw->pw_name, 371 atfile); 372 _exit(EXIT_FAILURE); 373 } 374 if ((gid_t)ngid != job->gid) { 375 syslog(LOG_WARNING, "(%s) GID MISMATCH (%s)", pw->pw_name, 376 atfile); 377 _exit(EXIT_FAILURE); 378 } 379 380 /* mark ourselves as different to PS command watchers */ 381 setproctitle("atrun %s", atfile); 382 383 if (pipe(output_pipe) != 0) { /* child's stdout/stderr */ 384 syslog(LOG_ERR, "(CRON) PIPE (%m)"); 385 _exit(EXIT_FAILURE); 386 } 387 388 /* Fork again, child will run the job, parent will catch output. */ 389 switch ((pid = fork())) { 390 case -1: 391 syslog(LOG_ERR, "(CRON) CAN'T FORK (%m)"); 392 _exit(EXIT_FAILURE); 393 /*NOTREACHED*/ 394 case 0: 395 /* Write log message now that we have our real pid. */ 396 syslog(LOG_INFO, "(%s) ATJOB (%s)", pw->pw_name, atfile); 397 398 /* Connect grandchild's stdin to the at job file. */ 399 if (lseek(fd, 0, SEEK_SET) < 0) { 400 syslog(LOG_ERR, "(CRON) LSEEK (%m)"); 401 _exit(EXIT_FAILURE); 402 } 403 if (fd != STDIN_FILENO) { 404 dup2(fd, STDIN_FILENO); 405 close(fd); 406 } 407 408 /* Connect stdout/stderr to the pipe from our parent. */ 409 if (output_pipe[WRITE_PIPE] != STDOUT_FILENO) { 410 dup2(output_pipe[WRITE_PIPE], STDOUT_FILENO); 411 close(output_pipe[WRITE_PIPE]); 412 } 413 dup2(STDOUT_FILENO, STDERR_FILENO); 414 close(output_pipe[READ_PIPE]); 415 416 (void) setsid(); 417 418 /* 419 * From this point on, anything written to stderr will be 420 * mailed to the user as output. 421 */ 422 423 /* Setup execution environment as per login.conf */ 424 if ((lc = login_getclass(pw->pw_class)) == NULL) { 425 warnx("unable to get login class for %s", 426 pw->pw_name); 427 syslog(LOG_ERR, "(CRON) CAN'T GET LOGIN CLASS (%s)", 428 pw->pw_name); 429 _exit(EXIT_FAILURE); 430 431 } 432 if (setusercontext(lc, pw, pw->pw_uid, LOGIN_SETALL)) { 433 warn("setusercontext failed for %s", pw->pw_name); 434 syslog(LOG_ERR, "(%s) SETUSERCONTEXT FAILED (%m)", 435 pw->pw_name); 436 _exit(EXIT_FAILURE); 437 } 438 439 /* Run any approval scripts. */ 440 as = auth_open(); 441 if (as == NULL || auth_setpwd(as, pw) != 0) { 442 warn("auth_setpwd"); 443 syslog(LOG_ERR, "(%s) AUTH_SETPWD FAILED (%m)", 444 pw->pw_name); 445 _exit(EXIT_FAILURE); 446 } 447 if (auth_approval(as, lc, pw->pw_name, "cron") <= 0) { 448 warnx("approval failed for %s", pw->pw_name); 449 syslog(LOG_ERR, "(%s) APPROVAL FAILED (cron)", 450 pw->pw_name); 451 _exit(EXIT_FAILURE); 452 } 453 auth_close(as); 454 login_close(lc); 455 456 /* If this is a low priority job, nice ourself. */ 457 if (job->queue > 'b') { 458 if (setpriority(PRIO_PROCESS, 0, job->queue - 'b') != 0) 459 syslog(LOG_ERR, "(%s) CAN'T NICE (%m)", 460 pw->pw_name); 461 } 462 463 (void) signal(SIGPIPE, SIG_DFL); 464 465 /* 466 * Exec /bin/sh with stdin connected to the at job file 467 * and stdout/stderr hooked up to our parent. 468 * The at file will set the environment up for us. 469 */ 470 nargv[0] = "sh"; 471 nargv[1] = NULL; 472 nenvp[0] = NULL; 473 if (execve(_PATH_BSHELL, nargv, nenvp) != 0) { 474 warn("unable to execute %s", _PATH_BSHELL); 475 syslog(LOG_ERR, "(%s) CAN'T EXEC (%s: %m)", pw->pw_name, 476 _PATH_BSHELL); 477 _exit(EXIT_FAILURE); 478 } 479 break; 480 default: 481 /* parent */ 482 break; 483 } 484 485 /* Close the atfile's fd and the end of the pipe we don't use. */ 486 close(fd); 487 close(output_pipe[WRITE_PIPE]); 488 489 /* Read piped output (if any) from the at job. */ 490 if ((fp = fdopen(output_pipe[READ_PIPE], "r")) == NULL) { 491 syslog(LOG_ERR, "(%s) FDOPEN (%m)", pw->pw_name); 492 (void) _exit(EXIT_FAILURE); 493 } 494 nread = fread(buf, 1, sizeof(buf), fp); 495 if (nread != 0 || always_mail) { 496 FILE *mail; 497 pid_t mailpid; 498 size_t bytes = 0; 499 int status = 0; 500 char mailcmd[MAX_COMMAND]; 501 char hostname[HOST_NAME_MAX + 1]; 502 503 if (gethostname(hostname, sizeof(hostname)) != 0) 504 strlcpy(hostname, "unknown", sizeof(hostname)); 505 if (snprintf(mailcmd, sizeof mailcmd, MAILFMT, 506 MAILARG) >= sizeof mailcmd) { 507 syslog(LOG_ERR, "(%s) ERROR (mailcmd too long)", 508 pw->pw_name); 509 (void) _exit(EXIT_FAILURE); 510 } 511 if (!(mail = cron_popen(mailcmd, "w", pw, &mailpid))) { 512 syslog(LOG_ERR, "(%s) POPEN (%s)", pw->pw_name, mailcmd); 513 (void) _exit(EXIT_FAILURE); 514 } 515 fprintf(mail, "From: %s (Atrun Service)\n", pw->pw_name); 516 fprintf(mail, "To: %s\n", mailto); 517 fprintf(mail, "Subject: Output from \"at\" job\n"); 518 fprintf(mail, "Auto-Submitted: auto-generated\n"); 519 fprintf(mail, "\nYour \"at\" job on %s\n\"%s/%s\"\n", 520 hostname, _PATH_AT_SPOOL, atfile); 521 fprintf(mail, "\nproduced the following output:\n\n"); 522 523 /* Pipe the job's output to sendmail. */ 524 do { 525 bytes += nread; 526 fwrite(buf, nread, 1, mail); 527 } while ((nread = fread(buf, 1, sizeof(buf), fp)) != 0); 528 529 /* 530 * If the mailer exits with non-zero exit status, log 531 * this fact so the problem can (hopefully) be debugged. 532 */ 533 if ((status = cron_pclose(mail, mailpid)) != 0) { 534 syslog(LOG_NOTICE, "(%s) MAIL (mailed %zu byte%s of " 535 "output but got status 0x%04x)", pw->pw_name, 536 bytes, (bytes == 1) ? "" : "s", status); 537 } 538 } 539 540 fclose(fp); /* also closes output_pipe[READ_PIPE] */ 541 542 /* Wait for grandchild to die. */ 543 for (;;) { 544 if (waitpid(pid, &waiter, 0) == -1) { 545 if (errno == EINTR) 546 continue; 547 break; 548 } else { 549 /* 550 if (WIFSIGNALED(waiter) && WCOREDUMP(waiter)) 551 Debug(DPROC, (", dumped core")) 552 */ 553 break; 554 } 555 } 556 _exit(EXIT_SUCCESS); 557 558 bad_file: 559 syslog(LOG_ERR, "(%s) BAD FILE FORMAT (%s)", pw->pw_name, atfile); 560 _exit(EXIT_FAILURE); 561 } 562