xref: /openbsd/usr.sbin/cron/entry.c (revision 988a3bda)
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