1/* Copyright 1988,1990,1993,1994 by Paul Vixie 2 * All rights reserved 3 * 4 * Distribute freely, except: don't remove my name from the source or 5 * documentation (don't take credit for my work), mark your changes (don't 6 * get me blamed for your possible bugs), don't alter or remove this 7 * notice. May be sold if buildable source is provided to buyer. No 8 * warrantee of any kind, express or implied, is included with this 9 * software; use at your own risk, responsibility for damages (if any) to 10 * anyone resulting from the use of this software rests entirely with the 11 * user. 12 * 13 * Send bug reports, bug fixes, enhancements, requests, flames, etc., and 14 * I'll try to keep a version up to date. I can be reached as follows: 15 * Paul Vixie <paul@vix.com> uunet!decwrl!vixie!paul 16 * From Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp 17 */ 18 19#if !defined(lint) && !defined(LINT) 20static const char rcsid[] = 21 "$FreeBSD: src/usr.sbin/cron/crontab/crontab.c,v 1.24 2006/09/03 17:52:19 ru Exp $"; 22#endif 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#ifdef __APPLE__ 30#include <get_compat.h> 31#else /* !__APPLE__ */ 32#define COMPAT_MODE(a,b) (1) 33#endif /* __APPLE__ */ 34#define MAIN_PROGRAM 35 36#include "cron.h" 37#include <errno.h> 38#include <fcntl.h> 39#ifdef __APPLE__ 40#include <CommonCrypto/CommonDigest.h> 41#include <sys/mman.h> 42#else 43#include <md5.h> 44#endif 45#include <paths.h> 46#include <sys/file.h> 47#include <sys/stat.h> 48#ifdef USE_UTIMES 49# include <sys/time.h> 50#else 51# include <time.h> 52# include <utime.h> 53#endif 54#if defined(POSIX) 55# include <locale.h> 56#endif 57 58#ifndef __APPLE__ 59#define MD5_SIZE 33 60#endif 61#define NHEADER_LINES 3 62 63 64enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace }; 65 66#if DEBUGGING 67static char *Options[] = { "???", "list", "delete", "edit", "replace" }; 68#endif 69 70 71static PID_T Pid; 72static char User[MAX_UNAME], RealUser[MAX_UNAME]; 73static char Filename[MAX_FNAME]; 74static FILE *NewCrontab; 75static int CheckErrorCount; 76static enum opt_t Option; 77static struct passwd *pw; 78#ifdef __APPLE__ 79static int posixly_correct; 80#endif 81static void list_cmd __P((void)), 82 delete_cmd __P((void)), 83 edit_cmd __P((void)), 84 poke_daemon __P((void)), 85 check_error __P((char *)), 86 parse_args __P((int c, char *v[])); 87static int replace_cmd __P((void)); 88 89 90static void 91usage(msg) 92 char *msg; 93{ 94 fprintf(stderr, "crontab: usage error: %s\n", msg); 95 fprintf(stderr, "%s\n%s\n", 96 "usage: crontab [-u user] file", 97 " crontab [-u user] { -e | -l | -r }"); 98 exit(ERROR_EXIT); 99} 100 101 102int 103main(argc, argv) 104 int argc; 105 char *argv[]; 106{ 107 int exitstatus; 108#ifdef __APPLE__ 109 posixly_correct = COMPAT_MODE("bin/crontab", "Unix2003"); 110#endif /* __APPLE__ */ 111 Pid = getpid(); 112 ProgramName = argv[0]; 113 114#if defined(POSIX) 115 setlocale(LC_ALL, ""); 116#endif 117 118#if defined(BSD) 119 setlinebuf(stderr); 120#endif 121 parse_args(argc, argv); /* sets many globals, opens a file */ 122 set_cron_uid(); 123 set_cron_cwd(); 124#ifdef __APPLE__ 125 // ALLOW_ONLY_ROOT should be defined for any option other than 126 // list when posixly_correct. 127 if (!allowed(User, (posixly_correct && Option != opt_list))) { 128#else /* __APPLE__ */ 129 if (!allowed(User)) { 130#endif /* __APPLE__ */ 131 warnx("you (%s) are not allowed to use this program", User); 132 log_it(RealUser, Pid, "AUTH", "crontab command not allowed"); 133 exit(ERROR_EXIT); 134 } 135 exitstatus = OK_EXIT; 136 switch (Option) { 137 case opt_list: list_cmd(); 138 break; 139 case opt_delete: delete_cmd(); 140 break; 141 case opt_edit: edit_cmd(); 142 break; 143 case opt_replace: if (replace_cmd() < 0) 144 exitstatus = ERROR_EXIT; 145 break; 146 case opt_unknown: 147 break; 148 } 149 exit(exitstatus); 150 /*NOTREACHED*/ 151} 152 153 154static void 155parse_args(argc, argv) 156 int argc; 157 char *argv[]; 158{ 159 int argch; 160#ifndef __APPLE__ 161 char resolved_path[PATH_MAX]; 162#endif 163 164 if (!(pw = getpwuid(getuid()))) 165 errx(ERROR_EXIT, "your UID isn't in the passwd file, bailing out"); 166 (void) strncpy(User, pw->pw_name, (sizeof User)-1); 167 User[(sizeof User)-1] = '\0'; 168 strcpy(RealUser, User); 169 Filename[0] = '\0'; 170 Option = opt_unknown; 171 while ((argch = getopt(argc, argv, "u:lerx:")) != -1) { 172 switch (argch) { 173 case 'x': 174 if (!set_debug_flags(optarg)) 175 usage("bad debug option"); 176 break; 177 case 'u': 178 if (getuid() != ROOT_UID) 179 errx(ERROR_EXIT, "must be privileged to use -u"); 180 if (!(pw = getpwnam(optarg))) 181 errx(ERROR_EXIT, "user `%s' unknown", optarg); 182 (void) strncpy(User, pw->pw_name, (sizeof User)-1); 183 User[(sizeof User)-1] = '\0'; 184 break; 185 case 'l': 186 if (Option != opt_unknown) 187 usage("only one operation permitted"); 188 Option = opt_list; 189 break; 190 case 'r': 191 if (Option != opt_unknown) 192 usage("only one operation permitted"); 193 Option = opt_delete; 194 break; 195 case 'e': 196 if (Option != opt_unknown) 197 usage("only one operation permitted"); 198 Option = opt_edit; 199 break; 200 default: 201 usage("unrecognized option"); 202 } 203 } 204 205 endpwent(); 206 207 if (Option != opt_unknown) { 208 if (argv[optind] != NULL) { 209 usage("no arguments permitted after this option"); 210 } 211 } else { 212 if (argv[optind] != NULL) { 213 Option = opt_replace; 214 (void) strncpy (Filename, argv[optind], (sizeof Filename)-1); 215 Filename[(sizeof Filename)-1] = '\0'; 216 217 } else { 218#ifdef __APPLE__ 219 if (posixly_correct) { 220 Option = opt_replace; 221 strcpy(Filename, "-"); 222 } else 223#endif /* __APPLE__ */ 224 usage("file name must be specified for replace"); 225 } 226 } 227 228 if (Option == opt_replace) { 229 /* we have to open the file here because we're going to 230 * chdir(2) into /var/cron before we get around to 231 * reading the file. 232 */ 233 if (!strcmp(Filename, "-")) { 234 NewCrontab = stdin; 235#ifndef __APPLE__ 236 } else if (realpath(Filename, resolved_path) != NULL && 237 !strcmp(resolved_path, SYSCRONTAB)) { 238 err(ERROR_EXIT, SYSCRONTAB " must be edited manually"); 239#endif 240 } else { 241 /* relinquish the setuid status of the binary during 242 * the open, lest nonroot users read files they should 243 * not be able to read. we can't use access() here 244 * since there's a race condition. thanks go out to 245 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting 246 * the race. 247 */ 248 249 if (swap_uids() < OK) 250 err(ERROR_EXIT, "swapping uids"); 251 if (!(NewCrontab = fopen(Filename, "r"))) 252 err(ERROR_EXIT, "%s", Filename); 253 if (swap_uids() < OK) 254 err(ERROR_EXIT, "swapping uids back"); 255 } 256 } 257 258 Debug(DMISC, ("user=%s, file=%s, option=%s\n", 259 User, Filename, Options[(int)Option])) 260} 261 262static void 263copy_file(FILE *in, FILE *out) { 264 int x, ch; 265 266 Set_LineNum(1) 267 /* ignore the top few comments since we probably put them there. 268 */ 269 for (x = 0; x < NHEADER_LINES; x++) { 270 ch = get_char(in); 271 if (EOF == ch) 272 break; 273 if ('#' != ch) { 274 putc(ch, out); 275 break; 276 } 277 while (EOF != (ch = get_char(in))) 278 if (ch == '\n') 279 break; 280 if (EOF == ch) 281 break; 282 } 283 284 /* copy the rest of the crontab (if any) to the output file. 285 */ 286 if (EOF != ch) 287 while (EOF != (ch = get_char(in))) 288 putc(ch, out); 289} 290 291static void 292list_cmd() { 293 char n[MAX_FNAME]; 294 FILE *f; 295 296 log_it(RealUser, Pid, "LIST", User); 297 (void) sprintf(n, CRON_TAB(User)); 298 if (!(f = fopen(n, "r"))) { 299 if (errno == ENOENT) 300 errx(ERROR_EXIT, "no crontab for %s", User); 301 else 302 err(ERROR_EXIT, "%s", n); 303 } 304 305 /* file is open. copy to stdout, close. 306 */ 307 copy_file(f, stdout); 308 fclose(f); 309} 310 311 312static void 313delete_cmd() { 314 char n[MAX_FNAME]; 315 int ch, first; 316 317#ifdef __APPLE__ 318 if (!posixly_correct && isatty(STDIN_FILENO)) { 319#else 320 if (isatty(STDIN_FILENO)) { 321#endif 322 (void)fprintf(stderr, "remove crontab for %s? ", User); 323 first = ch = getchar(); 324 while (ch != '\n' && ch != EOF) 325 ch = getchar(); 326 if (first != 'y' && first != 'Y') 327 return; 328 } 329 330 log_it(RealUser, Pid, "DELETE", User); 331 (void) sprintf(n, CRON_TAB(User)); 332 if (unlink(n)) { 333 if (errno == ENOENT) 334 errx(ERROR_EXIT, "no crontab for %s", User); 335 else 336 err(ERROR_EXIT, "%s", n); 337 } 338 poke_daemon(); 339} 340 341 342static void 343check_error(msg) 344 char *msg; 345{ 346 CheckErrorCount++; 347 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg); 348} 349 350 351#ifdef __APPLE__ 352/* Different semantics from libmd's MD5File: does not convert output to hex. */ 353static unsigned char * 354MD5File(const char *filename, unsigned char *output) 355{ 356 int fd; 357 struct stat sb; 358 void *ptr; 359 unsigned char *result = NULL; 360 361 fd = open(filename, O_RDONLY); 362 if (fd >= 0) { 363 if (fstat(fd, &sb) == 0) { 364 if (sb.st_size == 0) { 365 result = CC_MD5(NULL, 0, output); 366 } else { 367 ptr = mmap(NULL, sb.st_size, PROT_READ, MAP_FILE | MAP_PRIVATE, fd, 0); 368 if (ptr != MAP_FAILED) { 369 result = CC_MD5(ptr, sb.st_size, output); 370 (void)munmap(ptr, sb.st_size); 371 } 372 } 373 } 374 (void)close(fd); 375 } 376 377 return result; 378} 379#endif /* __APPLE__ */ 380 381static void 382edit_cmd() { 383 char n[MAX_FNAME], q[MAX_TEMPSTR], *editor; 384 FILE *f; 385 int t; 386 struct stat statbuf, fsbuf; 387 WAIT_T waiter; 388 PID_T pid, xpid; 389 mode_t um; 390 int syntax_error = 0; 391#ifdef __APPLE__ 392 unsigned char orig_md5[CC_MD5_DIGEST_LENGTH]; 393 unsigned char new_md5[CC_MD5_DIGEST_LENGTH]; 394#else 395 char orig_md5[MD5_SIZE]; 396 char new_md5[MD5_SIZE]; 397#endif 398 399 log_it(RealUser, Pid, "BEGIN EDIT", User); 400 (void) sprintf(n, CRON_TAB(User)); 401 if (!(f = fopen(n, "r"))) { 402 if (errno != ENOENT) 403 err(ERROR_EXIT, "%s", n); 404 warnx("no crontab for %s - using an empty one", User); 405 if (!(f = fopen(_PATH_DEVNULL, "r"))) 406 err(ERROR_EXIT, _PATH_DEVNULL); 407 } 408 409 um = umask(077); 410 (void) sprintf(Filename, "/tmp/crontab.XXXXXXXXXX"); 411 if ((t = mkstemp(Filename)) == -1) { 412 warn("%s", Filename); 413 (void) umask(um); 414 goto fatal; 415 } 416 (void) umask(um); 417#ifdef HAS_FCHOWN 418 if (fchown(t, getuid(), getgid()) < 0) { 419#else 420 if (chown(Filename, getuid(), getgid()) < 0) { 421#endif 422 warn("fchown"); 423 goto fatal; 424 } 425 if (!(NewCrontab = fdopen(t, "r+"))) { 426 warn("fdopen"); 427 goto fatal; 428 } 429 430 copy_file(f, NewCrontab); 431 fclose(f); 432 if (fflush(NewCrontab)) 433 err(ERROR_EXIT, "%s", Filename); 434 if (fstat(t, &fsbuf) < 0) { 435 warn("unable to fstat temp file"); 436 goto fatal; 437 } 438 again: 439 if (stat(Filename, &statbuf) < 0) { 440 warn("stat"); 441 fatal: unlink(Filename); 442 exit(ERROR_EXIT); 443 } 444 if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino) 445 errx(ERROR_EXIT, "temp file must be edited in place"); 446 if (MD5File(Filename, orig_md5) == NULL) { 447 warn("MD5"); 448 goto fatal; 449 } 450 451 if ((!(editor = getenv("VISUAL"))) 452 && (!(editor = getenv("EDITOR"))) 453 ) { 454 editor = EDITOR; 455 } 456 457 /* we still have the file open. editors will generally rewrite the 458 * original file rather than renaming/unlinking it and starting a 459 * new one; even backup files are supposed to be made by copying 460 * rather than by renaming. if some editor does not support this, 461 * then don't use it. the security problems are more severe if we 462 * close and reopen the file around the edit. 463 */ 464 465 switch (pid = fork()) { 466 case -1: 467 warn("fork"); 468 goto fatal; 469 case 0: 470 /* child */ 471 if (setuid(getuid()) < 0) 472 err(ERROR_EXIT, "setuid(getuid())"); 473 if (chdir("/tmp") < 0) 474 err(ERROR_EXIT, "chdir(/tmp)"); 475 if (strlen(editor) + strlen(Filename) + 2 >= MAX_TEMPSTR) 476 errx(ERROR_EXIT, "editor or filename too long"); 477 execlp(editor, editor, Filename, (char *)NULL); 478 err(ERROR_EXIT, "%s", editor); 479 /*NOTREACHED*/ 480 default: 481 /* parent */ 482 break; 483 } 484 485 /* parent */ 486 { 487 void (*f[4])(); 488 f[0] = signal(SIGHUP, SIG_IGN); 489 f[1] = signal(SIGINT, SIG_IGN); 490 f[2] = signal(SIGTERM, SIG_IGN); 491 xpid = wait(&waiter); 492 signal(SIGHUP, f[0]); 493 signal(SIGINT, f[1]); 494 signal(SIGTERM, f[2]); 495 } 496 if (xpid != pid) { 497 warnx("wrong PID (%d != %d) from \"%s\"", xpid, pid, editor); 498 goto fatal; 499 } 500 if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) { 501 warnx("\"%s\" exited with status %d", editor, WEXITSTATUS(waiter)); 502 goto fatal; 503 } 504 if (WIFSIGNALED(waiter)) { 505 warnx("\"%s\" killed; signal %d (%score dumped)", 506 editor, WTERMSIG(waiter), WCOREDUMP(waiter) ?"" :"no "); 507 goto fatal; 508 } 509 if (stat(Filename, &statbuf) < 0) { 510 warn("stat"); 511 goto fatal; 512 } 513 if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino) 514 errx(ERROR_EXIT, "temp file must be edited in place"); 515 if (MD5File(Filename, new_md5) == NULL) { 516 warn("MD5"); 517 goto fatal; 518 } 519#ifdef __APPLE__ 520 if (memcmp(orig_md5, new_md5, CC_MD5_DIGEST_LENGTH) == 0 && !syntax_error) { 521#else 522 if (strcmp(orig_md5, new_md5) == 0 && !syntax_error) { 523#endif 524 warnx("no changes made to crontab"); 525 goto remove; 526 } 527 warnx("installing new crontab"); 528 switch (replace_cmd()) { 529 case 0: /* Success */ 530 break; 531 case -1: /* Syntax error */ 532 for (;;) { 533 printf("Do you want to retry the same edit? "); 534 fflush(stdout); 535 q[0] = '\0'; 536 (void) fgets(q, sizeof q, stdin); 537 switch (islower(q[0]) ? q[0] : tolower(q[0])) { 538 case 'y': 539 syntax_error = 1; 540 goto again; 541 case 'n': 542 goto abandon; 543 default: 544 fprintf(stderr, "Enter Y or N\n"); 545 } 546 } 547 /*NOTREACHED*/ 548 case -2: /* Install error */ 549 abandon: 550 warnx("edits left in %s", Filename); 551 goto done; 552 default: 553 warnx("panic: bad switch() in replace_cmd()"); 554 goto fatal; 555 } 556 remove: 557 unlink(Filename); 558 done: 559 log_it(RealUser, Pid, "END EDIT", User); 560} 561 562 563/* returns 0 on success 564 * -1 on syntax error 565 * -2 on install error 566 */ 567static int 568replace_cmd() { 569 char n[MAX_FNAME], envstr[MAX_ENVSTR], tn[MAX_FNAME]; 570 FILE *tmp; 571 int ch, eof; 572 entry *e; 573 time_t now = time(NULL); 574 char **envp = env_init(); 575 576 if (envp == NULL) { 577 warnx("cannot allocate memory"); 578 return (-2); 579 } 580 581 (void) sprintf(n, "tmp.%d", Pid); 582 (void) sprintf(tn, TMP_CRON_TAB(n)); 583 if (!(tmp = fopen(tn, "w+"))) { 584 warn("%s", tn); 585 return (-2); 586 } 587 588 /* write a signature at the top of the file. 589 * 590 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code. 591 */ 592 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n"); 593 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now)); 594 fprintf(tmp, "# (Cron version -- %s)\n", rcsid); 595 596 /* copy the crontab to the tmp 597 */ 598 rewind(NewCrontab); 599 Set_LineNum(1) 600 while (EOF != (ch = get_char(NewCrontab))) 601 putc(ch, tmp); 602 ftruncate(fileno(tmp), ftell(tmp)); 603 fflush(tmp); rewind(tmp); 604 605 if (ferror(tmp)) { 606 warnx("error while writing new crontab to %s", tn); 607 fclose(tmp); unlink(tn); 608 return (-2); 609 } 610 611 /* check the syntax of the file being installed. 612 */ 613 614 /* BUG: was reporting errors after the EOF if there were any errors 615 * in the file proper -- kludged it by stopping after first error. 616 * vix 31mar87 617 */ 618 Set_LineNum(1 - NHEADER_LINES) 619 CheckErrorCount = 0; eof = FALSE; 620 while (!CheckErrorCount && !eof) { 621 switch (load_env(envstr, tmp)) { 622 case ERR: 623 eof = TRUE; 624 break; 625 case FALSE: 626#ifdef __APPLE__ 627 e = load_entry(tmp, check_error, pw->pw_name, envp); 628#else 629 e = load_entry(tmp, check_error, pw, envp); 630#endif 631 if (e) 632 free(e); 633 break; 634 case TRUE: 635 break; 636 } 637 } 638 639 if (CheckErrorCount != 0) { 640 warnx("errors in crontab file, can't install"); 641 fclose(tmp); unlink(tn); 642 return (-1); 643 } 644 645#ifdef HAS_FCHOWN 646 if (fchown(fileno(tmp), ROOT_UID, -1) < OK) 647#else 648 if (chown(tn, ROOT_UID, -1) < OK) 649#endif 650 { 651 warn("chown"); 652 fclose(tmp); unlink(tn); 653 return (-2); 654 } 655 656#ifdef HAS_FCHMOD 657 if (fchmod(fileno(tmp), 0600) < OK) 658#else 659 if (chmod(tn, 0600) < OK) 660#endif 661 { 662 warn("chown"); 663 fclose(tmp); unlink(tn); 664 return (-2); 665 } 666 667 if (fclose(tmp) == EOF) { 668 warn("fclose"); 669 unlink(tn); 670 return (-2); 671 } 672 673 (void) sprintf(n, CRON_TAB(User)); 674 if (rename(tn, n)) { 675 warn("error renaming %s to %s", tn, n); 676 unlink(tn); 677 return (-2); 678 } 679 log_it(RealUser, Pid, "REPLACE", User); 680 681 poke_daemon(); 682 683 return (0); 684} 685 686 687static void 688poke_daemon() { 689#ifdef USE_UTIMES 690 struct timeval tvs[2]; 691 struct timezone tz; 692 693 (void) gettimeofday(&tvs[0], &tz); 694 tvs[1] = tvs[0]; 695 if (utimes(SPOOL_DIR, tvs) < OK) { 696 warn("can't update mtime on spooldir %s", SPOOL_DIR); 697 return; 698 } 699#else 700 if (utime(SPOOL_DIR, NULL) < OK) { 701 warn("can't update mtime on spooldir %s", SPOOL_DIR); 702 return; 703 } 704#endif /*USE_UTIMES*/ 705} 706