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