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