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