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