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