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