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