1 /* $OpenBSD: entry.c,v 1.51 2020/04/16 17:51:56 millert Exp $ */ 2 3 /* 4 * Copyright 1988,1990,1993,1994 by Paul Vixie 5 * All rights reserved 6 */ 7 8 /* 9 * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") 10 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc. 11 * 12 * Permission to use, copy, modify, and distribute this software for any 13 * purpose with or without fee is hereby granted, provided that the above 14 * copyright notice and this permission notice appear in all copies. 15 * 16 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES 17 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF 18 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR 19 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES 20 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN 21 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT 22 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. 23 */ 24 #include <sys/cdefs.h> 25 #if !defined(lint) && !defined(LINT) 26 #if 0 27 static char rcsid[] = "Id: entry.c,v 1.17 2004/01/23 18:56:42 vixie Exp"; 28 #else 29 __RCSID("$NetBSD: entry.c,v 1.13 2024/08/19 23:50:23 christos Exp $"); 30 #endif 31 #endif 32 33 /* vix 26jan87 [RCS'd; rest of log is in RCS file] 34 * vix 01jan87 [added line-level error recovery] 35 * vix 31dec86 [added /step to the from-to range, per bob@acornrc] 36 * vix 30dec86 [written] 37 */ 38 39 #include "cron.h" 40 41 typedef enum ecode { 42 e_none, e_minute, e_hour, e_dom, e_month, e_dow, 43 e_cmd, e_timespec, e_username, e_option, e_memory 44 } ecode_e; 45 46 static const char * const ecodes[] = 47 { 48 "no error", 49 "bad minute", 50 "bad hour", 51 "bad day-of-month", 52 "bad month", 53 "bad day-of-week", 54 "bad command", 55 "bad time specifier", 56 "bad username", 57 "bad option", 58 "out of memory" 59 }; 60 61 static int get_list(bitstr_t *, int, int, const char * const [], int, FILE *), 62 get_range(bitstr_t *, int, int, const char * const [], int, FILE *), 63 get_number(int *, int, const char * const [], int, FILE *, const char *), 64 set_element(bitstr_t *, int, int, int); 65 66 void 67 free_entry(entry *e) { 68 free(e->cmd); 69 free(e->pwd); 70 env_free(e->envp); 71 free(e); 72 } 73 74 /* return NULL if eof or syntax error occurs; 75 * otherwise return a pointer to a new entry. 76 */ 77 entry * 78 load_entry(FILE *file, void (*error_func)(const char *), struct passwd *pw, 79 char **envp) { 80 /* this function reads one crontab entry -- the next -- from a file. 81 * it skips any leading blank lines, ignores comments, and returns 82 * NULL if for any reason the entry can't be read and parsed. 83 * 84 * the entry is also parsed here. 85 * 86 * syntax: 87 * user crontab: 88 * minutes hours doms months dows cmd\n 89 * system crontab (/etc/crontab): 90 * minutes hours doms months dows USERNAME cmd\n 91 */ 92 93 ecode_e ecode = e_none; 94 entry *e; 95 int ch; 96 char cmd[MAX_COMMAND]; 97 char envstr[MAX_ENVSTR]; 98 char **tenvp; 99 100 Debug(DPARS, ("load_entry()...about to eat comments\n")); 101 102 skip_comments(file); 103 104 ch = get_char(file); 105 if (ch == EOF) 106 return (NULL); 107 108 /* ch is now the first useful character of a useful line. 109 * it may be an @special or it may be the first character 110 * of a list of minutes. 111 */ 112 113 e = calloc(sizeof(*e), sizeof(char)); 114 115 if (ch == '@') { 116 /* all of these should be flagged and load-limited; i.e., 117 * instead of @hourly meaning "0 * * * *" it should mean 118 * "close to the front of every hour but not 'til the 119 * system load is low". Problems are: how do you know 120 * what "low" means? (save me from /etc/cron.conf!) and: 121 * how to guarantee low variance (how low is low?), which 122 * means how to we run roughly every hour -- seems like 123 * we need to keep a history or let the first hour set 124 * the schedule, which means we aren't load-limited 125 * anymore. too much for my overloaded brain. (vix, jan90) 126 * HINT 127 */ 128 ch = get_string(cmd, MAX_COMMAND, file, " \t\n"); 129 if (!strcmp("reboot", cmd)) { 130 e->flags |= WHEN_REBOOT; 131 } else if (!strcmp("yearly", cmd) || !strcmp("annually", cmd)){ 132 bit_set(e->minute, 0); 133 bit_set(e->hour, 0); 134 bit_set(e->dom, 0); 135 bit_set(e->month, 0); 136 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 137 e->flags |= DOW_STAR; 138 } else if (!strcmp("monthly", cmd)) { 139 bit_set(e->minute, 0); 140 bit_set(e->hour, 0); 141 bit_set(e->dom, 0); 142 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 143 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 144 e->flags |= DOW_STAR; 145 } else if (!strcmp("weekly", cmd)) { 146 bit_set(e->minute, 0); 147 bit_set(e->hour, 0); 148 bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); 149 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 150 bit_set(e->dow, 0); 151 e->flags |= DOM_STAR; 152 } else if (!strcmp("daily", cmd) || !strcmp("midnight", cmd)) { 153 bit_set(e->minute, 0); 154 bit_set(e->hour, 0); 155 bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); 156 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 157 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 158 e->flags |= DOM_STAR | DOW_STAR; 159 } else if (!strcmp("hourly", cmd)) { 160 bit_set(e->minute, 0); 161 bit_nset(e->hour, 0, (LAST_HOUR-FIRST_HOUR+1)); 162 bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1)); 163 bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1)); 164 bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1)); 165 e->flags |= DOM_STAR | DOW_STAR; 166 } else { 167 ecode = e_timespec; 168 goto eof; 169 } 170 /* Advance past whitespace between shortcut and 171 * username/command. 172 */ 173 Skip_Blanks(ch, file); 174 if (ch == EOF || ch == '\n') { 175 ecode = e_cmd; 176 goto eof; 177 } 178 } else { 179 Debug(DPARS, ("load_entry()...about to parse numerics\n")); 180 181 if (ch == '*') 182 e->flags |= MIN_STAR; 183 ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE, 184 PPC_NULL, ch, file); 185 if (ch == EOF) { 186 ecode = e_minute; 187 goto eof; 188 } 189 190 /* hours 191 */ 192 193 if (ch == '*') 194 e->flags |= HR_STAR; 195 ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR, 196 PPC_NULL, ch, file); 197 if (ch == EOF) { 198 ecode = e_hour; 199 goto eof; 200 } 201 202 /* DOM (days of month) 203 */ 204 205 if (ch == '*') 206 e->flags |= DOM_STAR; 207 ch = get_list(e->dom, FIRST_DOM, LAST_DOM, 208 PPC_NULL, ch, file); 209 if (ch == EOF) { 210 ecode = e_dom; 211 goto eof; 212 } 213 214 /* month 215 */ 216 217 ch = get_list(e->month, FIRST_MONTH, LAST_MONTH, 218 MonthNames, ch, file); 219 if (ch == EOF) { 220 ecode = e_month; 221 goto eof; 222 } 223 224 /* DOW (days of week) 225 */ 226 227 if (ch == '*') 228 e->flags |= DOW_STAR; 229 ch = get_list(e->dow, FIRST_DOW, LAST_DOW, 230 DowNames, ch, file); 231 if (ch == EOF) { 232 ecode = e_dow; 233 goto eof; 234 } 235 } 236 237 /* make sundays equivalent */ 238 if (bit_test(e->dow, 0) || bit_test(e->dow, 7)) { 239 bit_set(e->dow, 0); 240 bit_set(e->dow, 7); 241 } 242 243 /* check for permature EOL and catch a common typo */ 244 if (ch == '\n' || ch == '*') { 245 ecode = e_cmd; 246 goto eof; 247 } 248 249 /* ch is the first character of a command, or a username */ 250 unget_char(ch, file); 251 252 if (!pw) { 253 char *username = cmd; /* temp buffer */ 254 255 Debug(DPARS, ("load_entry()...about to parse username\n")); 256 ch = get_string(username, MAX_COMMAND, file, " \t\n"); 257 258 Debug(DPARS, ("load_entry()...got %s\n",username)); 259 if (ch == EOF || ch == '\n' || ch == '*') { 260 ecode = e_cmd; 261 goto eof; 262 } 263 264 pw = getpwnam(username); 265 if (pw == NULL) { 266 ecode = e_username; 267 goto eof; 268 } 269 Debug(DPARS, ("load_entry()...uid %ld, gid %ld\n", 270 (long)pw->pw_uid, (long)pw->pw_gid)); 271 } 272 273 if ((e->pwd = pw_dup(pw)) == NULL) { 274 ecode = e_memory; 275 goto eof; 276 } 277 (void)memset(e->pwd->pw_passwd, 0, strlen(e->pwd->pw_passwd)); 278 279 /* copy and fix up environment. some variables are just defaults and 280 * others are overrides. 281 */ 282 if ((e->envp = env_copy(envp)) == NULL) { 283 ecode = e_memory; 284 goto eof; 285 } 286 if (!env_get("SHELL", e->envp)) { 287 if (glue_strings(envstr, sizeof envstr, "SHELL", 288 _PATH_BSHELL, '=')) { 289 if ((tenvp = env_set(e->envp, envstr)) == NULL) { 290 ecode = e_memory; 291 goto eof; 292 } 293 e->envp = tenvp; 294 } else 295 log_it("CRON", getpid(), "error", "can't set SHELL"); 296 } 297 if (!env_get("HOME", e->envp)) { 298 if (glue_strings(envstr, sizeof envstr, "HOME", 299 pw->pw_dir, '=')) { 300 if ((tenvp = env_set(e->envp, envstr)) == NULL) { 301 ecode = e_memory; 302 goto eof; 303 } 304 e->envp = tenvp; 305 } else 306 log_it("CRON", getpid(), "error", "can't set HOME"); 307 } 308 /* If login.conf is in used we will get the default PATH later. */ 309 if (!env_get("PATH", e->envp)) { 310 if (glue_strings(envstr, sizeof envstr, "PATH", 311 _PATH_DEFPATH, '=')) { 312 if ((tenvp = env_set(e->envp, envstr)) == NULL) { 313 ecode = e_memory; 314 goto eof; 315 } 316 e->envp = tenvp; 317 } else 318 log_it("CRON", getpid(), "error", "can't set PATH"); 319 } 320 if (glue_strings(envstr, sizeof envstr, "LOGNAME", 321 pw->pw_name, '=')) { 322 if ((tenvp = env_set(e->envp, envstr)) == NULL) { 323 ecode = e_memory; 324 goto eof; 325 } 326 e->envp = tenvp; 327 } else 328 log_it("CRON", getpid(), "error", "can't set LOGNAME"); 329 #if defined(BSD) || defined(__linux) 330 if (glue_strings(envstr, sizeof envstr, "USER", 331 pw->pw_name, '=')) { 332 if ((tenvp = env_set(e->envp, envstr)) == NULL) { 333 ecode = e_memory; 334 goto eof; 335 } 336 e->envp = tenvp; 337 } else 338 log_it("CRON", getpid(), "error", "can't set USER"); 339 #endif 340 341 Debug(DPARS, ("load_entry()...about to parse command\n")); 342 343 /* If the first character of the command is '-' it is a cron option. 344 */ 345 ch = get_char(file); 346 while (ch == '-') { 347 switch (ch = get_char(file)) { 348 case 'n': 349 /* only allow the user to set the option once */ 350 if ((e->flags & MAIL_WHEN_ERR) == MAIL_WHEN_ERR) { 351 ecode = e_option; 352 goto eof; 353 } 354 e->flags |= MAIL_WHEN_ERR; 355 break; 356 case 'q': 357 /* only allow the user to set the option once */ 358 if ((e->flags & DONT_LOG) == DONT_LOG) { 359 ecode = e_option; 360 goto eof; 361 } 362 e->flags |= DONT_LOG; 363 break; 364 case 's': 365 /* only allow the user to set the option once */ 366 if ((e->flags & SINGLE_JOB) == SINGLE_JOB) { 367 ecode = e_option; 368 goto eof; 369 } 370 e->flags |= SINGLE_JOB; 371 break; 372 default: 373 ecode = e_option; 374 goto eof; 375 } 376 ch = get_char(file); 377 if (ch != '\t' && ch != ' ') { 378 ecode = e_option; 379 goto eof; 380 } 381 Skip_Blanks(ch, file); 382 if (ch == EOF || ch == '\n') { 383 ecode = e_cmd; 384 goto eof; 385 } 386 } 387 unget_char(ch, file); 388 389 /* Everything up to the next \n or EOF is part of the command... 390 * too bad we don't know in advance how long it will be, since we 391 * need to malloc a string for it... so, we limit it to MAX_COMMAND. 392 */ 393 ch = get_string(cmd, MAX_COMMAND, file, "\n"); 394 395 /* a file without a \n before the EOF is rude, so we'll complain... 396 */ 397 if (ch == EOF) { 398 ecode = e_cmd; 399 goto eof; 400 } 401 402 /* got the command in the 'cmd' string; save it in *e. 403 */ 404 if ((e->cmd = strdup(cmd)) == NULL) { 405 ecode = e_memory; 406 goto eof; 407 } 408 409 Debug(DPARS, ("load_entry()...returning successfully\n")); 410 411 /* success, fini, return pointer to the entry we just created... 412 */ 413 return (e); 414 415 eof: 416 if (e->envp) 417 env_free(e->envp); 418 if (e->pwd) 419 free(e->pwd); 420 if (e->cmd) 421 free(e->cmd); 422 free(e); 423 while (ch != '\n' && !feof(file)) 424 ch = get_char(file); 425 if (ecode != e_none && error_func) 426 (*error_func)(ecodes[(int)ecode]); 427 return (NULL); 428 } 429 430 static int 431 get_list(bitstr_t *bits, int low, int high, const char * const names[], 432 int ch, FILE *file) 433 { 434 int done; 435 436 /* we know that we point to a non-blank character here; 437 * must do a Skip_Blanks before we exit, so that the 438 * next call (or the code that picks up the cmd) can 439 * assume the same thing. 440 */ 441 442 Debug(DPARS|DEXT, ("get_list()...entered\n")); 443 444 /* list = range {"," range} 445 */ 446 447 /* clear the bit string, since the default is 'off'. 448 */ 449 bit_nclear(bits, 0, (size_t)(high-low+1)); 450 451 /* process all ranges 452 */ 453 done = FALSE; 454 while (!done) { 455 if (EOF == (ch = get_range(bits, low, high, names, ch, file))) 456 return (EOF); 457 if (ch == ',') 458 ch = get_char(file); 459 else 460 done = TRUE; 461 } 462 463 /* exiting. skip to some blanks, then skip over the blanks. 464 */ 465 Skip_Nonblanks(ch, file); 466 Skip_Blanks(ch, file); 467 468 Debug(DPARS|DEXT, ("get_list()...exiting w/ %02x\n", ch)); 469 470 return (ch); 471 } 472 473 474 static int 475 random_with_range(int low, int high) 476 { 477 /* Kind of crappy error detection, but... 478 */ 479 if (low < 0 || low >= high) 480 return low; 481 else 482 return (int)(arc4random() % (unsigned)((high - low + 1) + low)); 483 } 484 485 static int 486 get_range(bitstr_t *bits, int low, int high, const char * const names[], 487 int ch, FILE *file) 488 { 489 /* range = number | number "-" number [ "/" number ] 490 */ 491 492 int i, num1, num2, num3; 493 int qmark, star; 494 495 qmark = star = FALSE; 496 Debug(DPARS|DEXT, ("get_range()...entering, exit won't show\n")); 497 498 if (ch == '*') { 499 /* '*' means "first-last" but can still be modified by /step 500 */ 501 star = TRUE; 502 num1 = low; 503 num2 = high; 504 ch = get_char(file); 505 if (ch == EOF) 506 return (EOF); 507 } else if (ch == '?') { 508 qmark = TRUE; 509 ch = get_char(file); 510 if (ch == EOF) 511 return EOF; 512 if (!isdigit(ch)) { 513 num1 = random_with_range(low, high); 514 if (EOF == set_element(bits, low, high, num1)) 515 return EOF; 516 return ch; 517 } 518 } 519 520 if (!star) { 521 ch = get_number(&num1, low, names, ch, file, ",- \t\n"); 522 if (ch == EOF) 523 return (EOF); 524 525 if (ch != '-') { 526 /* not a range, it's a single number. 527 * a single number after '?' is bogus. 528 */ 529 if (qmark) 530 return EOF; 531 if (EOF == set_element(bits, low, high, num1)) { 532 unget_char(ch, file); 533 return (EOF); 534 } 535 return (ch); 536 } else { 537 /* eat the dash 538 */ 539 ch = get_char(file); 540 if (ch == EOF) 541 return (EOF); 542 543 /* get the number following the dash 544 */ 545 ch = get_number(&num2, low, names, ch, file, "/, \t\n"); 546 if (ch == EOF || num1 > num2) 547 return (EOF); 548 549 /* if we have a random range, it is really 550 * like having a single number. 551 */ 552 if (qmark) { 553 if (num1 > num2) 554 return EOF; 555 num1 = random_with_range(num1, num2); 556 if (EOF == set_element(bits, low, high, num1)) 557 return EOF; 558 return ch; 559 } 560 } 561 } 562 563 /* check for step size 564 */ 565 if (ch == '/') { 566 /* '?' is incompatible with '/' 567 */ 568 if (qmark) 569 return EOF; 570 /* eat the slash 571 */ 572 ch = get_char(file); 573 if (ch == EOF) 574 return (EOF); 575 576 /* get the step size -- note: we don't pass the 577 * names here, because the number is not an 578 * element id, it's a step size. 'low' is 579 * sent as a 0 since there is no offset either. 580 */ 581 ch = get_number(&num3, 0, PPC_NULL, ch, file, ", \t\n"); 582 if (ch == EOF || num3 == 0) 583 return (EOF); 584 } else { 585 /* no step. default==1. 586 */ 587 num3 = 1; 588 } 589 590 /* range. set all elements from num1 to num2, stepping 591 * by num3. (the step is a downward-compatible extension 592 * proposed conceptually by bob@acornrc, syntactically 593 * designed then implemented by paul vixie). 594 */ 595 for (i = num1; i <= num2; i += num3) 596 if (EOF == set_element(bits, low, high, i)) { 597 unget_char(ch, file); 598 return (EOF); 599 } 600 601 return (ch); 602 } 603 604 static int 605 get_number(int *numptr, int low, const char * const names[], int ch, FILE *file, 606 const char *terms) { 607 char temp[MAX_TEMPSTR], *pc; 608 int len, i; 609 610 pc = temp; 611 len = 0; 612 613 /* first look for a number */ 614 while (isdigit((unsigned char)ch)) { 615 if (++len >= MAX_TEMPSTR) 616 goto bad; 617 *pc++ = (char)ch; 618 ch = get_char(file); 619 } 620 *pc = '\0'; 621 if (len != 0) { 622 /* got a number, check for valid terminator */ 623 if (!strchr(terms, ch)) 624 goto bad; 625 *numptr = atoi(temp); 626 return (ch); 627 } 628 629 /* no numbers, look for a string if we have any */ 630 if (names) { 631 while (isalpha((unsigned char)ch)) { 632 if (++len >= MAX_TEMPSTR) 633 goto bad; 634 *pc++ = (char)ch; 635 ch = get_char(file); 636 } 637 *pc = '\0'; 638 if (len != 0 && strchr(terms, ch)) { 639 for (i = 0; names[i] != NULL; i++) { 640 Debug(DPARS|DEXT, 641 ("get_num, compare(%s,%s)\n", names[i], 642 temp)); 643 if (!strcasecmp(names[i], temp)) { 644 *numptr = i+low; 645 return (ch); 646 } 647 } 648 } 649 } 650 651 bad: 652 unget_char(ch, file); 653 return (EOF); 654 } 655 656 static int 657 set_element(bitstr_t *bits, int low, int high, int number) { 658 Debug(DPARS|DEXT, ("set_element(?,%d,%d,%d)\n", low, high, number)); 659 660 if (number < low || number > high) 661 return (EOF); 662 663 bit_set(bits, (number-low)); 664 return (OK); 665 } 666