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