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