1 /* $OpenBSD: entry.c,v 1.61 2024/08/23 00:58:04 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, int, const char *[], int, FILE *, const char *),
72 set_element(bitstr_t *, int, int, int),
73 set_range(bitstr_t *, int, int, int, int, int);
74
75 void
free_entry(entry * e)76 free_entry(entry *e)
77 {
78 free(e->cmd);
79 free(e->pwd);
80 if (e->envp)
81 env_free(e->envp);
82 free(e);
83 }
84
85 /* return NULL if eof or syntax error occurs;
86 * otherwise return a pointer to a new entry.
87 */
88 entry *
load_entry(FILE * file,void (* error_func)(const char *),struct passwd * pw,char ** envp)89 load_entry(FILE *file, void (*error_func)(const char *), struct passwd *pw,
90 char **envp)
91 {
92 /* this function reads one crontab entry -- the next -- from a file.
93 * it skips any leading blank lines, ignores comments, and returns
94 * NULL if for any reason the entry can't be read and parsed.
95 *
96 * the entry is also parsed here.
97 *
98 * syntax:
99 * user crontab:
100 * minutes hours doms months dows cmd\n
101 * system crontab (/etc/crontab):
102 * minutes hours doms months dows USERNAME cmd\n
103 */
104
105 ecode_e ecode = e_none;
106 entry *e;
107 int ch;
108 char cmd[MAX_COMMAND];
109 char envstr[MAX_ENVSTR];
110 char **tenvp;
111
112 skip_comments(file);
113
114 ch = get_char(file);
115 if (ch == EOF)
116 return (NULL);
117
118 /* ch is now the first useful character of a useful line.
119 * it may be an @special or it may be the first character
120 * of a list of minutes.
121 */
122
123 e = calloc(sizeof(entry), 1);
124 if (e == NULL) {
125 ecode = e_memory;
126 goto eof;
127 }
128
129 if (ch == '@') {
130 /* all of these should be flagged and load-limited; i.e.,
131 * instead of @hourly meaning "0 * * * *" it should mean
132 * "close to the front of every hour but not 'til the
133 * system load is low". Problems are: how do you know
134 * what "low" means? (save me from /etc/cron.conf!) and:
135 * how to guarantee low variance (how low is low?), which
136 * means how to we run roughly every hour -- seems like
137 * we need to keep a history or let the first hour set
138 * the schedule, which means we aren't load-limited
139 * anymore. too much for my overloaded brain. (vix, jan90)
140 * HINT
141 */
142 ch = get_string(cmd, MAX_COMMAND, file, " \t\n");
143 if (!strcmp("reboot", cmd)) {
144 e->flags |= WHEN_REBOOT;
145 } else if (!strcmp("yearly", cmd) || !strcmp("annually", cmd)){
146 set_element(e->minute, FIRST_MINUTE, LAST_MINUTE,
147 FIRST_MINUTE);
148 set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR);
149 set_element(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM);
150 set_element(e->month, FIRST_MONTH, LAST_MONTH,
151 FIRST_MONTH);
152 set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW,
153 LAST_DOW, 1);
154 e->flags |= DOW_STAR;
155 } else if (!strcmp("monthly", cmd)) {
156 set_element(e->minute, FIRST_MINUTE, LAST_MINUTE,
157 FIRST_MINUTE);
158 set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR);
159 set_element(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM);
160 set_range(e->month, FIRST_MONTH, LAST_MONTH,
161 FIRST_MONTH, LAST_MONTH, 1);
162 set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW,
163 LAST_DOW, 1);
164 e->flags |= DOW_STAR;
165 } else if (!strcmp("weekly", cmd)) {
166 set_element(e->minute, FIRST_MINUTE, LAST_MINUTE,
167 FIRST_MINUTE);
168 set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR);
169 set_range(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM,
170 LAST_DOM, 1);
171 set_range(e->month, FIRST_MONTH, LAST_MONTH,
172 FIRST_MONTH, LAST_MONTH, 1);
173 set_element(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW);
174 e->flags |= DOW_STAR;
175 } else if (!strcmp("daily", cmd) || !strcmp("midnight", cmd)) {
176 set_element(e->minute, FIRST_MINUTE, LAST_MINUTE,
177 FIRST_MINUTE);
178 set_element(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR);
179 set_range(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM,
180 LAST_DOM, 1);
181 set_range(e->month, FIRST_MONTH, LAST_MONTH,
182 FIRST_MONTH, LAST_MONTH, 1);
183 set_range(e->dow, FIRST_DOW, LAST_DOW, FIRST_DOW,
184 LAST_DOW, 1);
185 } else if (!strcmp("hourly", cmd)) {
186 set_element(e->minute, FIRST_MINUTE, LAST_MINUTE,
187 FIRST_MINUTE);
188 set_range(e->hour, FIRST_HOUR, LAST_HOUR, FIRST_HOUR,
189 LAST_HOUR, 1);
190 set_range(e->dom, FIRST_DOM, LAST_DOM, FIRST_DOM,
191 LAST_DOM, 1);
192 set_range(e->month, FIRST_MONTH, LAST_MONTH,
193 FIRST_MONTH, LAST_MONTH, 1);
194 set_range(e->dow, FIRST_DOW, LAST_DOW,
195 FIRST_DOW, LAST_DOW, 1);
196 e->flags |= HR_STAR;
197 } else {
198 ecode = e_timespec;
199 goto eof;
200 }
201 /* Advance past whitespace between shortcut and
202 * username/command.
203 */
204 Skip_Blanks(ch, file);
205 if (ch == EOF || ch == '\n') {
206 ecode = e_cmd;
207 goto eof;
208 }
209 } else {
210 if (ch == '*')
211 e->flags |= MIN_STAR;
212 ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE,
213 NULL, ch, file);
214 if (ch == EOF) {
215 ecode = e_minute;
216 goto eof;
217 }
218
219 /* hours
220 */
221
222 if (ch == '*')
223 e->flags |= HR_STAR;
224 ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR,
225 NULL, ch, file);
226 if (ch == EOF) {
227 ecode = e_hour;
228 goto eof;
229 }
230
231 /* DOM (days of month)
232 */
233
234 if (ch == '*')
235 e->flags |= DOM_STAR;
236 ch = get_list(e->dom, FIRST_DOM, LAST_DOM,
237 NULL, ch, file);
238 if (ch == EOF) {
239 ecode = e_dom;
240 goto eof;
241 }
242
243 /* month
244 */
245
246 ch = get_list(e->month, FIRST_MONTH, LAST_MONTH,
247 MonthNames, ch, file);
248 if (ch == EOF) {
249 ecode = e_month;
250 goto eof;
251 }
252
253 /* DOW (days of week)
254 */
255
256 if (ch == '*')
257 e->flags |= DOW_STAR;
258 ch = get_list(e->dow, FIRST_DOW, LAST_DOW,
259 DowNames, ch, file);
260 if (ch == EOF) {
261 ecode = e_dow;
262 goto eof;
263 }
264 }
265
266 /* make sundays equivalent */
267 if (bit_test(e->dow, 0) || bit_test(e->dow, 7)) {
268 bit_set(e->dow, 0);
269 bit_set(e->dow, 7);
270 }
271
272 /* check for permature EOL and catch a common typo */
273 if (ch == '\n' || ch == '*') {
274 ecode = e_cmd;
275 goto eof;
276 }
277
278 if (!pw) {
279 char *username = cmd; /* temp buffer */
280
281 unget_char(ch, file);
282 ch = get_string(username, MAX_COMMAND, file, " \t\n");
283
284 if (ch == EOF || ch == '\n' || ch == '*') {
285 ecode = e_cmd;
286 goto eof;
287 }
288 Skip_Blanks(ch, file)
289
290 pw = getpwnam(username);
291 if (pw == NULL) {
292 ecode = e_username;
293 goto eof;
294 }
295 }
296
297 if ((e->pwd = pw_dup(pw)) == NULL) {
298 ecode = e_memory;
299 goto eof;
300 }
301 explicit_bzero(e->pwd->pw_passwd, strlen(e->pwd->pw_passwd));
302
303 /* copy and fix up environment. some variables are just defaults and
304 * others are overrides.
305 */
306 if ((e->envp = env_copy(envp)) == NULL) {
307 ecode = e_memory;
308 goto eof;
309 }
310 if (!env_get("SHELL", e->envp)) {
311 if (snprintf(envstr, sizeof envstr, "SHELL=%s", _PATH_BSHELL) >=
312 sizeof(envstr))
313 syslog(LOG_ERR, "(CRON) ERROR (can't set SHELL)");
314 else {
315 if ((tenvp = env_set(e->envp, envstr)) == NULL) {
316 ecode = e_memory;
317 goto eof;
318 }
319 e->envp = tenvp;
320 }
321 }
322 if (!env_get("HOME", e->envp)) {
323 if (snprintf(envstr, sizeof envstr, "HOME=%s", pw->pw_dir) >=
324 sizeof(envstr))
325 syslog(LOG_ERR, "(CRON) ERROR (can't set HOME)");
326 else {
327 if ((tenvp = env_set(e->envp, envstr)) == NULL) {
328 ecode = e_memory;
329 goto eof;
330 }
331 e->envp = tenvp;
332 }
333 }
334 if (snprintf(envstr, sizeof envstr, "LOGNAME=%s", pw->pw_name) >=
335 sizeof(envstr))
336 syslog(LOG_ERR, "(CRON) ERROR (can't set LOGNAME)");
337 else {
338 if ((tenvp = env_set(e->envp, envstr)) == NULL) {
339 ecode = e_memory;
340 goto eof;
341 }
342 e->envp = tenvp;
343 }
344 if (snprintf(envstr, sizeof envstr, "USER=%s", pw->pw_name) >=
345 sizeof(envstr))
346 syslog(LOG_ERR, "(CRON) ERROR (can't set USER)");
347 else {
348 if ((tenvp = env_set(e->envp, envstr)) == NULL) {
349 ecode = e_memory;
350 goto eof;
351 }
352 e->envp = tenvp;
353 }
354
355 /* An optional series of '-'-prefixed flags in getopt style can
356 * occur before the command.
357 */
358 while (ch == '-') {
359 int flags = 0, loop = 1;
360
361 while (loop) {
362 switch (ch = get_char(file)) {
363 case 'n':
364 flags |= MAIL_WHEN_ERR;
365 break;
366 case 'q':
367 flags |= DONT_LOG;
368 break;
369 case 's':
370 flags |= SINGLE_JOB;
371 break;
372 case ' ':
373 case '\t':
374 Skip_Blanks(ch, file)
375 loop = 0;
376 break;
377 case EOF:
378 case '\n':
379 ecode = e_cmd;
380 goto eof;
381 default:
382 ecode = e_flags;
383 goto eof;
384 }
385 }
386
387 if (flags == 0) {
388 ecode = e_flags;
389 goto eof;
390 }
391 e->flags |= flags;
392 }
393 unget_char(ch, file);
394
395 /* Everything up to the next \n or EOF is part of the command...
396 * too bad we don't know in advance how long it will be, since we
397 * need to malloc a string for it... so, we limit it to MAX_COMMAND.
398 */
399 ch = get_string(cmd, MAX_COMMAND, file, "\n");
400
401 /* a file without a \n before the EOF is rude, so we'll complain...
402 */
403 if (ch == EOF) {
404 ecode = e_cmd;
405 goto eof;
406 }
407
408 /* got the command in the 'cmd' string; save it in *e.
409 */
410 if ((e->cmd = strdup(cmd)) == NULL) {
411 ecode = e_memory;
412 goto eof;
413 }
414
415 /* success, fini, return pointer to the entry we just created...
416 */
417 return (e);
418
419 eof:
420 if (e)
421 free_entry(e);
422 while (ch != '\n' && !feof(file))
423 ch = get_char(file);
424 if (ecode != e_none && error_func)
425 (*error_func)(ecodes[(int)ecode]);
426 return (NULL);
427 }
428
429 static int
get_list(bitstr_t * bits,int low,int high,const char * names[],int ch,FILE * file)430 get_list(bitstr_t *bits, int low, int high, const char *names[],
431 int ch, FILE *file)
432 {
433 int done;
434
435 /* we know that we point to a non-blank character here;
436 * must do a Skip_Blanks before we exit, so that the
437 * next call (or the code that picks up the cmd) can
438 * assume the same thing.
439 */
440
441 /* list = range {"," range}
442 */
443
444 /* clear the bit string, since the default is 'off'.
445 */
446 bit_nclear(bits, 0, high - low);
447
448 /* process all ranges
449 */
450 done = FALSE;
451 while (!done) {
452 if ((ch = get_range(bits, low, high, names, ch, file)) == EOF)
453 return (EOF);
454 if (ch == ',')
455 ch = get_char(file);
456 else
457 done = TRUE;
458 }
459
460 /* exiting. skip to some blanks, then skip over the blanks.
461 */
462 Skip_Nonblanks(ch, file)
463 Skip_Blanks(ch, file)
464
465 return (ch);
466 }
467
468
469 static int
get_range(bitstr_t * bits,int low,int high,const char * names[],int ch,FILE * file)470 get_range(bitstr_t *bits, int low, int high, const char *names[],
471 int ch, FILE *file)
472 {
473 /* range = number |
474 * [number] "~" [number] ["/" number] |
475 * number "-" number ["/" number]
476 */
477
478 int num1, num2, num3, rndstep;
479
480 num1 = low;
481 num2 = high;
482 rndstep = 0;
483
484 if (ch == '*') {
485 /* '*' means [low, high] but can still be modified by /step
486 */
487 ch = get_char(file);
488 if (ch == EOF)
489 return (EOF);
490 } else {
491 if (ch != '~') {
492 ch = get_number(&num1, low, high, names, ch, file, ",-~ \t\n");
493 if (ch == EOF)
494 return (EOF);
495 }
496
497 switch (ch) {
498 case '-':
499 /* eat the dash
500 */
501 ch = get_char(file);
502 if (ch == EOF)
503 return (EOF);
504
505 /* get the number following the dash
506 */
507 ch = get_number(&num2, low, high, names, ch, file, "/, \t\n");
508 if (ch == EOF || num1 > num2)
509 return (EOF);
510 break;
511 case '~':
512 /* eat the tilde
513 */
514 ch = get_char(file);
515 if (ch == EOF)
516 return (EOF);
517
518 /* get the (optional) number following the tilde
519 */
520 ch = get_number(&num2, low, high, names, ch, file, "/, \t\n");
521 if (ch == EOF) {
522 /* no second number, check for valid terminator
523 */
524 ch = get_char(file);
525 if (!strchr("/, \t\n", ch)) {
526 unget_char(ch, file);
527 return (EOF);
528 }
529 }
530 if (ch == EOF || num1 > num2) {
531 unget_char(ch, file);
532 return (EOF);
533 }
534
535 /* we must perform the bounds checking ourselves
536 */
537 if (num1 < low || num2 > high)
538 return (EOF);
539
540 if (ch == '/') {
541 /* randomize the step value instead of num1
542 */
543 rndstep = 1;
544 break;
545 }
546
547 /* get a random number in the interval [num1, num2]
548 */
549 num3 = num1;
550 num1 = arc4random_uniform(num2 - num3 + 1) + num3;
551 /* FALLTHROUGH */
552 default:
553 /* not a range, it's a single number.
554 */
555 if (set_element(bits, low, high, num1) == EOF) {
556 unget_char(ch, file);
557 return (EOF);
558 }
559 return (ch);
560 }
561 }
562
563 /* check for step size
564 */
565 if (ch == '/') {
566 const int max_step = high + 1 - low;
567
568 /* eat the slash
569 */
570 ch = get_char(file);
571 if (ch == EOF)
572 return (EOF);
573
574 /* get the step size -- note: we don't pass the
575 * names here, because the number is not an
576 * element id, it's a step size. 'low' is
577 * sent as a 0 since there is no offset either.
578 */
579 ch = get_number(&num3, 0, max_step, NULL, ch, file, ", \t\n");
580 if (ch == EOF || num3 == 0)
581 return (EOF);
582 if (rndstep) {
583 /*
584 * use a random offset smaller than the step size
585 * and the difference between high and low values.
586 */
587 num1 += arc4random_uniform(MINIMUM(num3, num2 - num1));
588 }
589 } else {
590 /* no step. default==1.
591 */
592 num3 = 1;
593 }
594
595 /* range. set all elements from num1 to num2, stepping
596 * by num3. (the step is a downward-compatible extension
597 * proposed conceptually by bob@acornrc, syntactically
598 * designed then implemented by paul vixie).
599 */
600 if (set_range(bits, low, high, num1, num2, num3) == EOF) {
601 unget_char(ch, file);
602 return (EOF);
603 }
604
605 return (ch);
606 }
607
608 static int
get_number(int * numptr,int low,int high,const char * names[],int ch,FILE * file,const char * terms)609 get_number(int *numptr, int low, int high, const char *names[], int ch,
610 FILE *file, const char *terms)
611 {
612 char temp[MAX_TEMPSTR], *pc;
613 int len, i;
614
615 pc = temp;
616 len = 0;
617
618 /* first look for a number */
619 while (isdigit((unsigned char)ch)) {
620 if (++len >= MAX_TEMPSTR)
621 goto bad;
622 *pc++ = ch;
623 ch = get_char(file);
624 }
625 *pc = '\0';
626 if (len != 0) {
627 const char *errstr;
628
629 /* got a number, check for valid terminator */
630 if (!strchr(terms, ch))
631 goto bad;
632 i = strtonum(temp, low, high, &errstr);
633 if (errstr != NULL)
634 goto bad;
635 *numptr = i;
636 return (ch);
637 }
638
639 /* no numbers, look for a string if we have any */
640 if (names) {
641 while (isalpha((unsigned char)ch)) {
642 if (++len >= MAX_TEMPSTR)
643 goto bad;
644 *pc++ = ch;
645 ch = get_char(file);
646 }
647 *pc = '\0';
648 if (len != 0 && strchr(terms, ch)) {
649 for (i = 0; names[i] != NULL; i++) {
650 if (!strcasecmp(names[i], temp)) {
651 *numptr = i+low;
652 return (ch);
653 }
654 }
655 }
656 }
657
658 bad:
659 unget_char(ch, file);
660 return (EOF);
661 }
662
663 static int
set_element(bitstr_t * bits,int low,int high,int number)664 set_element(bitstr_t *bits, int low, int high, int number)
665 {
666
667 if (number < low || number > high)
668 return (EOF);
669 number -= low;
670
671 bit_set(bits, number);
672 return (0);
673 }
674
675 static int
set_range(bitstr_t * bits,int low,int high,int start,int stop,int step)676 set_range(bitstr_t *bits, int low, int high, int start, int stop, int step)
677 {
678 int i;
679
680 if (start < low || stop > high)
681 return (EOF);
682 start -= low;
683 stop -= low;
684
685 if (step <= 1) {
686 bit_nset(bits, start, stop);
687 } else {
688 if (step > stop + 1)
689 return (EOF);
690 for (i = start; i <= stop; i += step)
691 bit_set(bits, i);
692 }
693 return (0);
694 }
695