1/* $OpenBSD: crontab.c,v 1.96 2023/05/05 13:50:40 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 <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 46enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace }; 47 48static gid_t crontab_gid; 49static gid_t user_gid; 50static char User[MAX_UNAME], RealUser[MAX_UNAME]; 51static char Filename[PATH_MAX], TempFilename[PATH_MAX]; 52static FILE *NewCrontab; 53static int CheckErrorCount; 54static enum opt_t Option; 55static struct passwd *pw; 56int editit(const char *); 57static 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); 64static int replace_cmd(void); 65 66static void 67usage(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 77int 78main(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 124static void 125parse_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 206static void 207list_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 231static void 232delete_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 249static void 250check_error(const char *msg) 251{ 252 CheckErrorCount++; 253 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg); 254} 255 256static void 257edit_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/* Create a temporary file in the spool dir owned by "pw". */ 385static FILE * 386spool_mkstemp(char *template) 387{ 388 uid_t euid = geteuid(); 389 int fd = -1; 390 FILE *fp; 391 392 if (euid != pw->pw_uid) { 393 if (seteuid(pw->pw_uid) == -1) { 394 warn("unable to change uid to %u", pw->pw_uid); 395 goto bad; 396 } 397 } 398 fd = mkstemp(template); 399 if (euid != pw->pw_uid) { 400 if (seteuid(euid) == -1) { 401 warn("unable to change uid to %u", euid); 402 goto bad; 403 } 404 } 405 if (fd == -1 || !(fp = fdopen(fd, "w+"))) { 406 warn("%s", template); 407 goto bad; 408 } 409 return (fp); 410 411bad: 412 if (fd != -1) { 413 close(fd); 414 unlink(template); 415 } 416 return (NULL); 417} 418 419/* returns 0 on success 420 * -1 on syntax error 421 * -2 on install error 422 */ 423static int 424replace_cmd(void) 425{ 426 char n[PATH_MAX], envstr[MAX_ENVSTR]; 427 FILE *tmp; 428 int ch, eof; 429 int error = 0; 430 entry *e; 431 time_t now = time(NULL); 432 char **envp = env_init(); 433 434 if (envp == NULL) { 435 warn(NULL); /* ENOMEM */ 436 return (-2); 437 } 438 if (snprintf(TempFilename, sizeof TempFilename, "%s/tmp.XXXXXXXXX", 439 _PATH_CRON_SPOOL) >= sizeof(TempFilename)) { 440 TempFilename[0] = '\0'; 441 warnc(ENAMETOOLONG, "%s/tmp.XXXXXXXXX", _PATH_CRON_SPOOL); 442 return (-2); 443 } 444 tmp = spool_mkstemp(TempFilename); 445 if (tmp == NULL) { 446 TempFilename[0] = '\0'; 447 return (-2); 448 } 449 450 (void) signal(SIGHUP, die); 451 (void) signal(SIGINT, die); 452 (void) signal(SIGQUIT, die); 453 454 /* write a signature at the top of the file. 455 * 456 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code. 457 */ 458 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n"); 459 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now)); 460 fprintf(tmp, "# (Cron version %s)\n", CRON_VERSION); 461 462 /* copy the crontab to the tmp 463 */ 464 rewind(NewCrontab); 465 Set_LineNum(1) 466 while (EOF != (ch = get_char(NewCrontab))) 467 putc(ch, tmp); 468 ftruncate(fileno(tmp), ftello(tmp)); /* XXX redundant with "w+"? */ 469 fflush(tmp); rewind(tmp); 470 471 if (ferror(tmp)) { 472 warnx("error while writing new crontab to %s", TempFilename); 473 fclose(tmp); 474 error = -2; 475 goto done; 476 } 477 478 /* check the syntax of the file being installed. 479 */ 480 481 /* BUG: was reporting errors after the EOF if there were any errors 482 * in the file proper -- kludged it by stopping after first error. 483 * vix 31mar87 484 */ 485 Set_LineNum(1 - NHEADER_LINES) 486 CheckErrorCount = 0; eof = FALSE; 487 while (!CheckErrorCount && !eof) { 488 switch (load_env(envstr, tmp)) { 489 case -1: 490 /* check for data before the EOF */ 491 if (envstr[0] != '\0') { 492 Set_LineNum(LineNumber + 1); 493 check_error("premature EOF"); 494 } 495 eof = TRUE; 496 break; 497 case FALSE: 498 e = load_entry(tmp, check_error, pw, envp); 499 if (e) 500 free_entry(e); 501 break; 502 case TRUE: 503 break; 504 } 505 } 506 507 if (CheckErrorCount != 0) { 508 warnx("errors in crontab file, unable to install"); 509 fclose(tmp); 510 error = -1; 511 goto done; 512 } 513 514 if (fclose(tmp) == EOF) { 515 warn("fclose"); 516 error = -2; 517 goto done; 518 } 519 520 if (snprintf(n, sizeof n, "%s/%s", _PATH_CRON_SPOOL, User) >= sizeof(n)) { 521 warnc(ENAMETOOLONG, "%s/%s", _PATH_CRON_SPOOL, User); 522 error = -2; 523 goto done; 524 } 525 if (rename(TempFilename, n)) { 526 warn("unable to rename %s to %s", TempFilename, n); 527 error = -2; 528 goto done; 529 } 530 TempFilename[0] = '\0'; 531 syslog(LOG_INFO, "(%s) REPLACE (%s)", RealUser, User); 532 533 poke_daemon(RELOAD_CRON); 534 535done: 536 (void) signal(SIGHUP, SIG_DFL); 537 (void) signal(SIGINT, SIG_DFL); 538 (void) signal(SIGQUIT, SIG_DFL); 539 if (TempFilename[0]) { 540 (void) unlink(TempFilename); 541 TempFilename[0] = '\0'; 542 } 543 return (error); 544} 545 546/* 547 * Execute an editor on the specified pathname, which is interpreted 548 * from the shell. This means flags may be included. 549 * 550 * Returns -1 on error, or the exit value on success. 551 */ 552int 553editit(const char *pathname) 554{ 555 char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p; 556 sig_t sighup, sigint, sigquit, sigchld; 557 pid_t pid; 558 int saved_errno, st, ret = -1; 559 560 ed = getenv("VISUAL"); 561 if (ed == NULL || ed[0] == '\0') 562 ed = getenv("EDITOR"); 563 if (ed == NULL || ed[0] == '\0') 564 ed = _PATH_VI; 565 if (asprintf(&p, "%s %s", ed, pathname) == -1) 566 return (-1); 567 argp[2] = p; 568 569 sighup = signal(SIGHUP, SIG_IGN); 570 sigint = signal(SIGINT, SIG_IGN); 571 sigquit = signal(SIGQUIT, SIG_IGN); 572 sigchld = signal(SIGCHLD, SIG_DFL); 573 if ((pid = fork()) == -1) 574 goto fail; 575 if (pid == 0) { 576 /* Drop setgid and exec the command. */ 577 if (setgid(user_gid) == -1) { 578 warn("unable to set gid to %u", user_gid); 579 } else { 580 execv(_PATH_BSHELL, argp); 581 warn("unable to execute %s", _PATH_BSHELL); 582 } 583 _exit(127); 584 } 585 while (waitpid(pid, &st, 0) == -1) 586 if (errno != EINTR) 587 goto fail; 588 if (!WIFEXITED(st)) 589 errno = EINTR; 590 else 591 ret = WEXITSTATUS(st); 592 593 fail: 594 saved_errno = errno; 595 (void)signal(SIGHUP, sighup); 596 (void)signal(SIGINT, sigint); 597 (void)signal(SIGQUIT, sigquit); 598 (void)signal(SIGCHLD, sigchld); 599 free(p); 600 errno = saved_errno; 601 return (ret); 602} 603 604static void 605die(int x) 606{ 607 if (TempFilename[0]) 608 (void) unlink(TempFilename); 609 _exit(EXIT_FAILURE); 610} 611 612static void 613copy_crontab(FILE *f, FILE *out) 614{ 615 int ch, x; 616 617 /* ignore the top few comments since we probably put them there. 618 */ 619 x = 0; 620 while (EOF != (ch = get_char(f))) { 621 if ('#' != ch) { 622 putc(ch, out); 623 break; 624 } 625 while (EOF != (ch = get_char(f))) 626 if (ch == '\n') 627 break; 628 if (++x >= NHEADER_LINES) 629 break; 630 } 631 632 /* copy out the rest of the crontab (if any) 633 */ 634 if (EOF != ch) 635 while (EOF != (ch = get_char(f))) 636 putc(ch, out); 637} 638