1 /* $OpenBSD: crontab.c,v 1.64 2011/08/22 19:32:42 millert Exp $ */ 2 3 /* Copyright 1988,1990,1993,1994 by Paul Vixie 4 * All rights reserved 5 */ 6 7 /* 8 * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") 9 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc. 10 * 11 * Permission to use, copy, modify, and distribute this software for any 12 * purpose with or without fee is hereby granted, provided that the above 13 * copyright notice and this permission notice appear in all copies. 14 * 15 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES 16 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 17 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR 18 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 19 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 20 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 21 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 22 */ 23 24 /* crontab - install and manage per-user crontab files 25 * vix 02may87 [RCS has the rest of the log] 26 * vix 26jan87 [original] 27 */ 28 29 #include <err.h> 30 31 #define MAIN_PROGRAM 32 33 #include "cron.h" 34 35 #define NHEADER_LINES 3 36 37 enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace }; 38 39 #if DEBUGGING 40 static char *Options[] = { "???", "list", "delete", "edit", "replace" }; 41 static char *getoptargs = "u:lerx:"; 42 #else 43 static char *getoptargs = "u:ler"; 44 #endif 45 46 static PID_T Pid; 47 static char User[MAX_UNAME], RealUser[MAX_UNAME]; 48 static char Filename[MAX_FNAME], TempFilename[MAX_FNAME]; 49 static FILE *NewCrontab; 50 static int CheckErrorCount; 51 static enum opt_t Option; 52 static struct passwd *pw; 53 int editit(const char *); 54 static void list_cmd(void), 55 delete_cmd(void), 56 edit_cmd(void), 57 check_error(const char *), 58 parse_args(int c, char *v[]), 59 copy_crontab(FILE *, FILE *), 60 die(int); 61 static int replace_cmd(void); 62 63 static void 64 usage(const char *msg) { 65 fprintf(stderr, "%s: usage error: %s\n", ProgramName, msg); 66 fprintf(stderr, "usage: %s [-u user] file\n", ProgramName); 67 fprintf(stderr, " %s [-e | -l | -r] [-u user]\n", ProgramName); 68 fprintf(stderr, 69 "\t\t(default operation is replace, per 1003.2)\n" 70 "\t-e\t(edit user's crontab)\n" 71 "\t-l\t(list user's crontab)\n" 72 "\t-r\t(delete user's crontab)\n"); 73 exit(EXIT_FAILURE); 74 } 75 76 int 77 main(int argc, char *argv[]) { 78 int exitstatus; 79 80 Pid = getpid(); 81 ProgramName = argv[0]; 82 83 setlocale(LC_ALL, ""); 84 85 #if defined(BSD) 86 setlinebuf(stderr); 87 #endif 88 parse_args(argc, argv); /* sets many globals, opens a file */ 89 set_cron_cwd(); 90 if (!allowed(RealUser, CRON_ALLOW, CRON_DENY)) { 91 fprintf(stderr, 92 "You (%s) are not allowed to use this program (%s)\n", 93 User, ProgramName); 94 fprintf(stderr, "See crontab(1) for more information\n"); 95 log_it(RealUser, Pid, "AUTH", "crontab command not allowed"); 96 exit(EXIT_FAILURE); 97 } 98 exitstatus = EXIT_SUCCESS; 99 switch (Option) { 100 case opt_list: 101 list_cmd(); 102 break; 103 case opt_delete: 104 delete_cmd(); 105 break; 106 case opt_edit: 107 edit_cmd(); 108 break; 109 case opt_replace: 110 if (replace_cmd() < 0) 111 exitstatus = EXIT_FAILURE; 112 break; 113 default: 114 exitstatus = EXIT_FAILURE; 115 break; 116 } 117 exit(exitstatus); 118 /*NOTREACHED*/ 119 } 120 121 static void 122 parse_args(int argc, char *argv[]) { 123 int argch; 124 125 if (!(pw = getpwuid(getuid()))) { 126 fprintf(stderr, "%s: your UID isn't in the passwd file.\n", 127 ProgramName); 128 fprintf(stderr, "bailing out.\n"); 129 exit(EXIT_FAILURE); 130 } 131 if (strlen(pw->pw_name) >= sizeof User) { 132 fprintf(stderr, "username too long\n"); 133 exit(EXIT_FAILURE); 134 } 135 strlcpy(User, pw->pw_name, sizeof(User)); 136 strlcpy(RealUser, User, sizeof(RealUser)); 137 Filename[0] = '\0'; 138 Option = opt_unknown; 139 while (-1 != (argch = getopt(argc, argv, getoptargs))) { 140 switch (argch) { 141 #if DEBUGGING 142 case 'x': 143 if (!set_debug_flags(optarg)) 144 usage("bad debug option"); 145 break; 146 #endif 147 case 'u': 148 if (MY_UID(pw) != ROOT_UID) { 149 fprintf(stderr, 150 "must be privileged to use -u\n"); 151 exit(EXIT_FAILURE); 152 } 153 if (!(pw = getpwnam(optarg))) { 154 fprintf(stderr, "%s: user `%s' unknown\n", 155 ProgramName, optarg); 156 exit(EXIT_FAILURE); 157 } 158 if (strlcpy(User, optarg, sizeof User) >= sizeof User) 159 usage("username too long"); 160 break; 161 case 'l': 162 if (Option != opt_unknown) 163 usage("only one operation permitted"); 164 Option = opt_list; 165 break; 166 case 'r': 167 if (Option != opt_unknown) 168 usage("only one operation permitted"); 169 Option = opt_delete; 170 break; 171 case 'e': 172 if (Option != opt_unknown) 173 usage("only one operation permitted"); 174 Option = opt_edit; 175 break; 176 default: 177 usage("unrecognized option"); 178 } 179 } 180 181 endpwent(); 182 183 if (Option != opt_unknown) { 184 if (argv[optind] != NULL) 185 usage("no arguments permitted after this option"); 186 } else { 187 if (argv[optind] != NULL) { 188 Option = opt_replace; 189 if (strlcpy(Filename, argv[optind], sizeof Filename) 190 >= sizeof Filename) 191 usage("filename too long"); 192 } else 193 usage("file name must be specified for replace"); 194 } 195 196 if (Option == opt_replace) { 197 /* we have to open the file here because we're going to 198 * chdir(2) into /var/cron before we get around to 199 * reading the file. 200 */ 201 if (!strcmp(Filename, "-")) 202 NewCrontab = stdin; 203 else { 204 /* relinquish the setgid status of the binary during 205 * the open, lest nonroot users read files they should 206 * not be able to read. we can't use access() here 207 * since there's a race condition. thanks go out to 208 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting 209 * the race. 210 */ 211 212 if (swap_gids() < OK) { 213 perror("swapping gids"); 214 exit(EXIT_FAILURE); 215 } 216 if (!(NewCrontab = fopen(Filename, "r"))) { 217 perror(Filename); 218 exit(EXIT_FAILURE); 219 } 220 if (swap_gids_back() < OK) { 221 perror("swapping gids back"); 222 exit(EXIT_FAILURE); 223 } 224 } 225 } 226 227 Debug(DMISC, ("user=%s, file=%s, option=%s\n", 228 User, Filename, Options[(int)Option])) 229 } 230 231 static void 232 list_cmd(void) { 233 char n[MAX_FNAME]; 234 FILE *f; 235 236 log_it(RealUser, Pid, "LIST", User); 237 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) { 238 fprintf(stderr, "path too long\n"); 239 exit(EXIT_FAILURE); 240 } 241 if (!(f = fopen(n, "r"))) { 242 if (errno == ENOENT) 243 fprintf(stderr, "no crontab for %s\n", User); 244 else 245 perror(n); 246 exit(EXIT_FAILURE); 247 } 248 249 /* file is open. copy to stdout, close. 250 */ 251 Set_LineNum(1) 252 253 copy_crontab(f, stdout); 254 fclose(f); 255 } 256 257 static void 258 delete_cmd(void) { 259 char n[MAX_FNAME]; 260 261 log_it(RealUser, Pid, "DELETE", User); 262 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) { 263 fprintf(stderr, "path too long\n"); 264 exit(EXIT_FAILURE); 265 } 266 if (unlink(n) != 0) { 267 if (errno == ENOENT) 268 fprintf(stderr, "no crontab for %s\n", User); 269 else 270 perror(n); 271 exit(EXIT_FAILURE); 272 } 273 poke_daemon(SPOOL_DIR, RELOAD_CRON); 274 } 275 276 static void 277 check_error(const char *msg) { 278 CheckErrorCount++; 279 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg); 280 } 281 282 static void 283 edit_cmd(void) { 284 char n[MAX_FNAME], q[MAX_TEMPSTR]; 285 const char *tmpdir; 286 FILE *f; 287 int t; 288 struct stat statbuf, xstatbuf; 289 struct timespec ts[2]; 290 291 log_it(RealUser, Pid, "BEGIN EDIT", User); 292 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) { 293 fprintf(stderr, "path too long\n"); 294 exit(EXIT_FAILURE); 295 } 296 if (!(f = fopen(n, "r"))) { 297 if (errno != ENOENT) { 298 perror(n); 299 exit(EXIT_FAILURE); 300 } 301 fprintf(stderr, "no crontab for %s - using an empty one\n", 302 User); 303 if (!(f = fopen(_PATH_DEVNULL, "r"))) { 304 perror(_PATH_DEVNULL); 305 exit(EXIT_FAILURE); 306 } 307 } 308 309 if (fstat(fileno(f), &statbuf) < 0) { 310 perror("fstat"); 311 goto fatal; 312 } 313 ts[0] = statbuf.st_atim; 314 ts[1] = statbuf.st_mtim; 315 316 /* Turn off signals. */ 317 (void)signal(SIGHUP, SIG_IGN); 318 (void)signal(SIGINT, SIG_IGN); 319 (void)signal(SIGQUIT, SIG_IGN); 320 321 tmpdir = getenv("TMPDIR"); 322 if (tmpdir == NULL || tmpdir[0] == '\0') 323 tmpdir = _PATH_TMP; 324 for (t = strlen(tmpdir); t != 0 && tmpdir[t - 1] == '/'; t--) 325 continue; 326 if (snprintf(Filename, sizeof Filename, "%.*s/crontab.XXXXXXXXXX", 327 t, tmpdir) >= sizeof(Filename)) { 328 fprintf(stderr, "path too long\n"); 329 goto fatal; 330 } 331 if (swap_gids() < OK) { 332 perror("swapping gids"); 333 exit(EXIT_FAILURE); 334 } 335 t = mkstemp(Filename); 336 if (swap_gids_back() < OK) { 337 perror("swapping gids back"); 338 exit(EXIT_FAILURE); 339 } 340 if (t == -1) { 341 perror(Filename); 342 goto fatal; 343 } 344 if (!(NewCrontab = fdopen(t, "r+"))) { 345 perror("fdopen"); 346 goto fatal; 347 } 348 349 Set_LineNum(1) 350 351 copy_crontab(f, NewCrontab); 352 fclose(f); 353 if (fflush(NewCrontab) < OK) { 354 perror(Filename); 355 exit(EXIT_FAILURE); 356 } 357 (void)futimens(t, ts); 358 again: 359 rewind(NewCrontab); 360 if (ferror(NewCrontab)) { 361 fprintf(stderr, "%s: error while writing new crontab to %s\n", 362 ProgramName, Filename); 363 fatal: 364 if (swap_gids() < OK) { 365 perror("swapping gids"); 366 exit(EXIT_FAILURE); 367 } 368 unlink(Filename); 369 if (swap_gids_back() < OK) { 370 perror("swapping gids back"); 371 exit(EXIT_FAILURE); 372 } 373 exit(EXIT_FAILURE); 374 } 375 376 /* we still have the file open. editors will generally rewrite the 377 * original file rather than renaming/unlinking it and starting a 378 * new one; even backup files are supposed to be made by copying 379 * rather than by renaming. if some editor does not support this, 380 * then don't use it. the security problems are more severe if we 381 * close and reopen the file around the edit. 382 */ 383 if (editit(Filename) == -1) { 384 warn("error starting editor"); 385 goto fatal; 386 } 387 388 if (fstat(t, &statbuf) < 0) { 389 perror("fstat"); 390 goto fatal; 391 } 392 if (timespeccmp(&ts[1], &statbuf.st_mtim, ==)) { 393 if (swap_gids() < OK) { 394 perror("swapping gids"); 395 exit(EXIT_FAILURE); 396 } 397 if (lstat(Filename, &xstatbuf) == 0 && 398 statbuf.st_ino != xstatbuf.st_ino) { 399 fprintf(stderr, "%s: crontab temp file moved, editor " 400 "may create backup files improperly\n", ProgramName); 401 } 402 if (swap_gids_back() < OK) { 403 perror("swapping gids back"); 404 exit(EXIT_FAILURE); 405 } 406 fprintf(stderr, "%s: no changes made to crontab\n", 407 ProgramName); 408 goto remove; 409 } 410 fprintf(stderr, "%s: installing new crontab\n", ProgramName); 411 switch (replace_cmd()) { 412 case 0: 413 break; 414 case -1: 415 for (;;) { 416 printf("Do you want to retry the same edit? "); 417 fflush(stdout); 418 q[0] = '\0'; 419 if (fgets(q, sizeof q, stdin) == NULL) { 420 putchar('\n'); 421 goto abandon; 422 } 423 switch (q[0]) { 424 case 'y': 425 case 'Y': 426 goto again; 427 case 'n': 428 case 'N': 429 goto abandon; 430 default: 431 fprintf(stderr, "Enter Y or N\n"); 432 } 433 } 434 /*NOTREACHED*/ 435 case -2: 436 abandon: 437 fprintf(stderr, "%s: edits left in %s\n", 438 ProgramName, Filename); 439 goto done; 440 default: 441 fprintf(stderr, "%s: panic: bad switch() in replace_cmd()\n", 442 ProgramName); 443 goto fatal; 444 } 445 remove: 446 if (swap_gids() < OK) { 447 perror("swapping gids"); 448 exit(EXIT_FAILURE); 449 } 450 unlink(Filename); 451 if (swap_gids_back() < OK) { 452 perror("swapping gids back"); 453 exit(EXIT_FAILURE); 454 } 455 done: 456 log_it(RealUser, Pid, "END EDIT", User); 457 } 458 459 /* returns 0 on success 460 * -1 on syntax error 461 * -2 on install error 462 */ 463 static int 464 replace_cmd(void) { 465 char n[MAX_FNAME], envstr[MAX_ENVSTR]; 466 FILE *tmp; 467 int ch, eof, fd; 468 int error = 0; 469 entry *e; 470 time_t now = time(NULL); 471 char **envp = env_init(); 472 473 if (envp == NULL) { 474 fprintf(stderr, "%s: Cannot allocate memory.\n", ProgramName); 475 return (-2); 476 } 477 if (snprintf(TempFilename, sizeof TempFilename, "%s/tmp.XXXXXXXXX", 478 SPOOL_DIR) >= sizeof(TempFilename)) { 479 TempFilename[0] = '\0'; 480 fprintf(stderr, "path too long\n"); 481 return (-2); 482 } 483 if ((fd = mkstemp(TempFilename)) == -1 || !(tmp = fdopen(fd, "w+"))) { 484 perror(TempFilename); 485 if (fd != -1) { 486 close(fd); 487 unlink(TempFilename); 488 } 489 TempFilename[0] = '\0'; 490 return (-2); 491 } 492 493 (void) signal(SIGHUP, die); 494 (void) signal(SIGINT, die); 495 (void) signal(SIGQUIT, die); 496 497 /* write a signature at the top of the file. 498 * 499 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code. 500 */ 501 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n"); 502 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now)); 503 fprintf(tmp, "# (Cron version %s)\n", CRON_VERSION); 504 505 /* copy the crontab to the tmp 506 */ 507 rewind(NewCrontab); 508 Set_LineNum(1) 509 while (EOF != (ch = get_char(NewCrontab))) 510 putc(ch, tmp); 511 ftruncate(fileno(tmp), ftello(tmp)); /* XXX redundant with "w+"? */ 512 fflush(tmp); rewind(tmp); 513 514 if (ferror(tmp)) { 515 fprintf(stderr, "%s: error while writing new crontab to %s\n", 516 ProgramName, TempFilename); 517 fclose(tmp); 518 error = -2; 519 goto done; 520 } 521 522 /* check the syntax of the file being installed. 523 */ 524 525 /* BUG: was reporting errors after the EOF if there were any errors 526 * in the file proper -- kludged it by stopping after first error. 527 * vix 31mar87 528 */ 529 Set_LineNum(1 - NHEADER_LINES) 530 CheckErrorCount = 0; eof = FALSE; 531 while (!CheckErrorCount && !eof) { 532 switch (load_env(envstr, tmp)) { 533 case ERR: 534 /* check for data before the EOF */ 535 if (envstr[0] != '\0') { 536 Set_LineNum(LineNumber + 1); 537 check_error("premature EOF"); 538 } 539 eof = TRUE; 540 break; 541 case FALSE: 542 e = load_entry(tmp, check_error, pw, envp); 543 if (e) 544 free_entry(e); 545 break; 546 case TRUE: 547 break; 548 } 549 } 550 551 if (CheckErrorCount != 0) { 552 fprintf(stderr, "errors in crontab file, can't install.\n"); 553 fclose(tmp); 554 error = -1; 555 goto done; 556 } 557 558 #ifdef HAVE_FCHOWN 559 if (fchown(fileno(tmp), pw->pw_uid, -1) < OK) { 560 perror("fchown"); 561 fclose(tmp); 562 error = -2; 563 goto done; 564 } 565 #else 566 if (chown(TempFilename, pw->pw_uid, -1) < OK) { 567 perror("chown"); 568 fclose(tmp); 569 error = -2; 570 goto done; 571 } 572 #endif 573 574 if (fclose(tmp) == EOF) { 575 perror("fclose"); 576 error = -2; 577 goto done; 578 } 579 580 if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) { 581 fprintf(stderr, "path too long\n"); 582 error = -2; 583 goto done; 584 } 585 if (rename(TempFilename, n)) { 586 fprintf(stderr, "%s: error renaming %s to %s\n", 587 ProgramName, TempFilename, n); 588 perror("rename"); 589 error = -2; 590 goto done; 591 } 592 TempFilename[0] = '\0'; 593 log_it(RealUser, Pid, "REPLACE", User); 594 595 poke_daemon(SPOOL_DIR, RELOAD_CRON); 596 597 done: 598 (void) signal(SIGHUP, SIG_DFL); 599 (void) signal(SIGINT, SIG_DFL); 600 (void) signal(SIGQUIT, SIG_DFL); 601 if (TempFilename[0]) { 602 (void) unlink(TempFilename); 603 TempFilename[0] = '\0'; 604 } 605 return (error); 606 } 607 608 /* 609 * Execute an editor on the specified pathname, which is interpreted 610 * from the shell. This means flags may be included. 611 * 612 * Returns -1 on error, or the exit value on success. 613 */ 614 int 615 editit(const char *pathname) 616 { 617 char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p; 618 sig_t sighup, sigint, sigquit, sigchld; 619 pid_t pid; 620 int saved_errno, st, ret = -1; 621 622 ed = getenv("VISUAL"); 623 if (ed == NULL || ed[0] == '\0') 624 ed = getenv("EDITOR"); 625 if (ed == NULL || ed[0] == '\0') 626 ed = _PATH_VI; 627 if (asprintf(&p, "%s %s", ed, pathname) == -1) 628 return (-1); 629 argp[2] = p; 630 631 sighup = signal(SIGHUP, SIG_IGN); 632 sigint = signal(SIGINT, SIG_IGN); 633 sigquit = signal(SIGQUIT, SIG_IGN); 634 sigchld = signal(SIGCHLD, SIG_DFL); 635 if ((pid = fork()) == -1) 636 goto fail; 637 if (pid == 0) { 638 setgid(getgid()); 639 setuid(getuid()); 640 execv(_PATH_BSHELL, argp); 641 _exit(127); 642 } 643 while (waitpid(pid, &st, 0) == -1) 644 if (errno != EINTR) 645 goto fail; 646 if (!WIFEXITED(st)) 647 errno = EINTR; 648 else 649 ret = WEXITSTATUS(st); 650 651 fail: 652 saved_errno = errno; 653 (void)signal(SIGHUP, sighup); 654 (void)signal(SIGINT, sigint); 655 (void)signal(SIGQUIT, sigquit); 656 (void)signal(SIGCHLD, sigchld); 657 free(p); 658 errno = saved_errno; 659 return (ret); 660 } 661 662 static void 663 die(int x) { 664 if (TempFilename[0]) 665 (void) unlink(TempFilename); 666 _exit(EXIT_FAILURE); 667 } 668 669 static void 670 copy_crontab(FILE *f, FILE *out) { 671 int ch, x; 672 673 /* ignore the top few comments since we probably put them there. 674 */ 675 x = 0; 676 while (EOF != (ch = get_char(f))) { 677 if ('#' != ch) { 678 putc(ch, out); 679 break; 680 } 681 while (EOF != (ch = get_char(f))) 682 if (ch == '\n') 683 break; 684 if (++x >= NHEADER_LINES) 685 break; 686 } 687 688 /* copy out the rest of the crontab (if any) 689 */ 690 if (EOF != ch) 691 while (EOF != (ch = get_char(f))) 692 putc(ch, out); 693 } 694