1 /* $OpenBSD: entry.c,v 1.56 2023/05/08 15:18:31 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 | number* "~" number* | number "-" number ["/" number] 457 */ 458 459 int i, num1, num2, num3, rndstep; 460 461 num1 = low; 462 num2 = high; 463 rndstep = 0; 464 465 if (ch == '*') { 466 /* '*' means [low, high] but can still be modified by /step 467 */ 468 ch = get_char(file); 469 if (ch == EOF) 470 return (EOF); 471 } else { 472 if (ch != '~') { 473 ch = get_number(&num1, low, names, ch, file, ",-~ \t\n"); 474 if (ch == EOF) 475 return (EOF); 476 } 477 478 switch (ch) { 479 case '-': 480 /* eat the dash 481 */ 482 ch = get_char(file); 483 if (ch == EOF) 484 return (EOF); 485 486 /* get the number following the dash 487 */ 488 ch = get_number(&num2, low, names, ch, file, "/, \t\n"); 489 if (ch == EOF || num1 > num2) 490 return (EOF); 491 break; 492 case '~': 493 /* eat the tilde 494 */ 495 ch = get_char(file); 496 if (ch == EOF) 497 return (EOF); 498 499 /* get the (optional) number following the tilde 500 */ 501 ch = get_number(&num2, low, names, ch, file, "/, \t\n"); 502 if (ch == EOF) { 503 /* no second number, check for valid terminator 504 */ 505 ch = get_char(file); 506 if (!strchr("/, \t\n", ch)) { 507 unget_char(ch, file); 508 return (EOF); 509 } 510 } 511 if (ch == EOF || num1 > num2) { 512 unget_char(ch, file); 513 return (EOF); 514 } 515 516 /* we must perform the bounds checking ourselves 517 */ 518 if (num1 < low || num2 > high) 519 return (EOF); 520 521 if (ch == '/') { 522 /* randomize the step value instead of num1 523 */ 524 rndstep = 1; 525 break; 526 } 527 528 /* get a random number in the interval [num1, num2] 529 */ 530 num3 = num1; 531 num1 = arc4random_uniform(num2 - num3 + 1) + num3; 532 /* FALLTHROUGH */ 533 default: 534 /* not a range, it's a single number. 535 */ 536 if (set_element(bits, low, high, num1) == EOF) { 537 unget_char(ch, file); 538 return (EOF); 539 } 540 return (ch); 541 } 542 } 543 544 /* check for step size 545 */ 546 if (ch == '/') { 547 /* eat the slash 548 */ 549 ch = get_char(file); 550 if (ch == EOF) 551 return (EOF); 552 553 /* get the step size -- note: we don't pass the 554 * names here, because the number is not an 555 * element id, it's a step size. 'low' is 556 * sent as a 0 since there is no offset either. 557 */ 558 ch = get_number(&num3, 0, NULL, ch, file, ", \t\n"); 559 if (ch == EOF || num3 == 0) 560 return (EOF); 561 if (rndstep) { 562 /* 563 * use a random offset smaller than the step size 564 * and the difference between high and low values. 565 */ 566 num1 += arc4random_uniform(MINIMUM(num3, num2 - num1)); 567 } 568 } else { 569 /* no step. default==1. 570 */ 571 num3 = 1; 572 } 573 574 /* range. set all elements from num1 to num2, stepping 575 * by num3. (the step is a downward-compatible extension 576 * proposed conceptually by bob@acornrc, syntactically 577 * designed then implemented by paul vixie). 578 */ 579 for (i = num1; i <= num2; i += num3) 580 if (set_element(bits, low, high, i) == EOF) { 581 unget_char(ch, file); 582 return (EOF); 583 } 584 585 return (ch); 586 } 587 588 static int 589 get_number(int *numptr, int low, const char *names[], int ch, FILE *file, 590 const char *terms) 591 { 592 char temp[MAX_TEMPSTR], *pc; 593 int len, i; 594 595 pc = temp; 596 len = 0; 597 598 /* first look for a number */ 599 while (isdigit((unsigned char)ch)) { 600 if (++len >= MAX_TEMPSTR) 601 goto bad; 602 *pc++ = ch; 603 ch = get_char(file); 604 } 605 *pc = '\0'; 606 if (len != 0) { 607 /* got a number, check for valid terminator */ 608 if (!strchr(terms, ch)) 609 goto bad; 610 *numptr = atoi(temp); 611 return (ch); 612 } 613 614 /* no numbers, look for a string if we have any */ 615 if (names) { 616 while (isalpha((unsigned char)ch)) { 617 if (++len >= MAX_TEMPSTR) 618 goto bad; 619 *pc++ = ch; 620 ch = get_char(file); 621 } 622 *pc = '\0'; 623 if (len != 0 && strchr(terms, ch)) { 624 for (i = 0; names[i] != NULL; i++) { 625 if (!strcasecmp(names[i], temp)) { 626 *numptr = i+low; 627 return (ch); 628 } 629 } 630 } 631 } 632 633 bad: 634 unget_char(ch, file); 635 return (EOF); 636 } 637 638 static int 639 set_element(bitstr_t *bits, int low, int high, int number) 640 { 641 642 if (number < low || number > high) 643 return (EOF); 644 645 bit_set(bits, (number-low)); 646 return (0); 647 } 648