xref: /openbsd-src/usr.sbin/cron/crontab.c (revision 91f110e064cd7c194e59e019b83bb7496c1c84d4)
1 /*	$OpenBSD: crontab.c,v 1.64 2011/08/22 19:32:42 millert Exp $	*/
2 
3 /* Copyright 1988,1990,1993,1994 by Paul Vixie
4  * All rights reserved
5  */
6 
7 /*
8  * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
9  * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
10  *
11  * Permission to use, copy, modify, and distribute this software for any
12  * purpose with or without fee is hereby granted, provided that the above
13  * copyright notice and this permission notice appear in all copies.
14  *
15  * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
16  * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
17  * MERCHANTABILITY AND FITNESS.  IN NO EVENT SHALL ISC BE LIABLE FOR
18  * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
19  * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
20  * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
21  * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
22  */
23 
24 /* crontab - install and manage per-user crontab files
25  * vix 02may87 [RCS has the rest of the log]
26  * vix 26jan87 [original]
27  */
28 
29 #include <err.h>
30 
31 #define	MAIN_PROGRAM
32 
33 #include "cron.h"
34 
35 #define NHEADER_LINES 3
36 
37 enum opt_t	{ opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
38 
39 #if DEBUGGING
40 static char	*Options[] = { "???", "list", "delete", "edit", "replace" };
41 static char	*getoptargs = "u:lerx:";
42 #else
43 static char	*getoptargs = "u:ler";
44 #endif
45 
46 static	PID_T		Pid;
47 static	char		User[MAX_UNAME], RealUser[MAX_UNAME];
48 static	char		Filename[MAX_FNAME], TempFilename[MAX_FNAME];
49 static	FILE		*NewCrontab;
50 static	int		CheckErrorCount;
51 static	enum opt_t	Option;
52 static	struct passwd	*pw;
53 int			editit(const char *);
54 static	void		list_cmd(void),
55 			delete_cmd(void),
56 			edit_cmd(void),
57 			check_error(const char *),
58 			parse_args(int c, char *v[]),
59 			copy_crontab(FILE *, FILE *),
60 			die(int);
61 static	int		replace_cmd(void);
62 
63 static void
64 usage(const char *msg) {
65 	fprintf(stderr, "%s: usage error: %s\n", ProgramName, msg);
66 	fprintf(stderr, "usage: %s [-u user] file\n", ProgramName);
67 	fprintf(stderr, "       %s [-e | -l | -r] [-u user]\n", ProgramName);
68 	fprintf(stderr,
69 	    "\t\t(default operation is replace, per 1003.2)\n"
70 	    "\t-e\t(edit user's crontab)\n"
71 	    "\t-l\t(list user's crontab)\n"
72 	    "\t-r\t(delete user's crontab)\n");
73 	exit(EXIT_FAILURE);
74 }
75 
76 int
77 main(int argc, char *argv[]) {
78 	int exitstatus;
79 
80 	Pid = getpid();
81 	ProgramName = argv[0];
82 
83 	setlocale(LC_ALL, "");
84 
85 #if defined(BSD)
86 	setlinebuf(stderr);
87 #endif
88 	parse_args(argc, argv);		/* sets many globals, opens a file */
89 	set_cron_cwd();
90 	if (!allowed(RealUser, CRON_ALLOW, CRON_DENY)) {
91 		fprintf(stderr,
92 			"You (%s) are not allowed to use this program (%s)\n",
93 			User, ProgramName);
94 		fprintf(stderr, "See crontab(1) for more information\n");
95 		log_it(RealUser, Pid, "AUTH", "crontab command not allowed");
96 		exit(EXIT_FAILURE);
97 	}
98 	exitstatus = EXIT_SUCCESS;
99 	switch (Option) {
100 	case opt_list:
101 		list_cmd();
102 		break;
103 	case opt_delete:
104 		delete_cmd();
105 		break;
106 	case opt_edit:
107 		edit_cmd();
108 		break;
109 	case opt_replace:
110 		if (replace_cmd() < 0)
111 			exitstatus = EXIT_FAILURE;
112 		break;
113 	default:
114 		exitstatus = EXIT_FAILURE;
115 		break;
116 	}
117 	exit(exitstatus);
118 	/*NOTREACHED*/
119 }
120 
121 static void
122 parse_args(int argc, char *argv[]) {
123 	int argch;
124 
125 	if (!(pw = getpwuid(getuid()))) {
126 		fprintf(stderr, "%s: your UID isn't in the passwd file.\n",
127 			ProgramName);
128 		fprintf(stderr, "bailing out.\n");
129 		exit(EXIT_FAILURE);
130 	}
131 	if (strlen(pw->pw_name) >= sizeof User) {
132 		fprintf(stderr, "username too long\n");
133 		exit(EXIT_FAILURE);
134 	}
135 	strlcpy(User, pw->pw_name, sizeof(User));
136 	strlcpy(RealUser, User, sizeof(RealUser));
137 	Filename[0] = '\0';
138 	Option = opt_unknown;
139 	while (-1 != (argch = getopt(argc, argv, getoptargs))) {
140 		switch (argch) {
141 #if DEBUGGING
142 		case 'x':
143 			if (!set_debug_flags(optarg))
144 				usage("bad debug option");
145 			break;
146 #endif
147 		case 'u':
148 			if (MY_UID(pw) != ROOT_UID) {
149 				fprintf(stderr,
150 					"must be privileged to use -u\n");
151 				exit(EXIT_FAILURE);
152 			}
153 			if (!(pw = getpwnam(optarg))) {
154 				fprintf(stderr, "%s:  user `%s' unknown\n",
155 					ProgramName, optarg);
156 				exit(EXIT_FAILURE);
157 			}
158 			if (strlcpy(User, optarg, sizeof User) >= sizeof User)
159 				usage("username too long");
160 			break;
161 		case 'l':
162 			if (Option != opt_unknown)
163 				usage("only one operation permitted");
164 			Option = opt_list;
165 			break;
166 		case 'r':
167 			if (Option != opt_unknown)
168 				usage("only one operation permitted");
169 			Option = opt_delete;
170 			break;
171 		case 'e':
172 			if (Option != opt_unknown)
173 				usage("only one operation permitted");
174 			Option = opt_edit;
175 			break;
176 		default:
177 			usage("unrecognized option");
178 		}
179 	}
180 
181 	endpwent();
182 
183 	if (Option != opt_unknown) {
184 		if (argv[optind] != NULL)
185 			usage("no arguments permitted after this option");
186 	} else {
187 		if (argv[optind] != NULL) {
188 			Option = opt_replace;
189 			if (strlcpy(Filename, argv[optind], sizeof Filename)
190 			    >= sizeof Filename)
191 				usage("filename too long");
192 		} else
193 			usage("file name must be specified for replace");
194 	}
195 
196 	if (Option == opt_replace) {
197 		/* we have to open the file here because we're going to
198 		 * chdir(2) into /var/cron before we get around to
199 		 * reading the file.
200 		 */
201 		if (!strcmp(Filename, "-"))
202 			NewCrontab = stdin;
203 		else {
204 			/* relinquish the setgid status of the binary during
205 			 * the open, lest nonroot users read files they should
206 			 * not be able to read.  we can't use access() here
207 			 * since there's a race condition.  thanks go out to
208 			 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
209 			 * the race.
210 			 */
211 
212 			if (swap_gids() < OK) {
213 				perror("swapping gids");
214 				exit(EXIT_FAILURE);
215 			}
216 			if (!(NewCrontab = fopen(Filename, "r"))) {
217 				perror(Filename);
218 				exit(EXIT_FAILURE);
219 			}
220 			if (swap_gids_back() < OK) {
221 				perror("swapping gids back");
222 				exit(EXIT_FAILURE);
223 			}
224 		}
225 	}
226 
227 	Debug(DMISC, ("user=%s, file=%s, option=%s\n",
228 		      User, Filename, Options[(int)Option]))
229 }
230 
231 static void
232 list_cmd(void) {
233 	char n[MAX_FNAME];
234 	FILE *f;
235 
236 	log_it(RealUser, Pid, "LIST", User);
237 	if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
238 		fprintf(stderr, "path too long\n");
239 		exit(EXIT_FAILURE);
240 	}
241 	if (!(f = fopen(n, "r"))) {
242 		if (errno == ENOENT)
243 			fprintf(stderr, "no crontab for %s\n", User);
244 		else
245 			perror(n);
246 		exit(EXIT_FAILURE);
247 	}
248 
249 	/* file is open. copy to stdout, close.
250 	 */
251 	Set_LineNum(1)
252 
253 	copy_crontab(f, stdout);
254 	fclose(f);
255 }
256 
257 static void
258 delete_cmd(void) {
259 	char n[MAX_FNAME];
260 
261 	log_it(RealUser, Pid, "DELETE", User);
262 	if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
263 		fprintf(stderr, "path too long\n");
264 		exit(EXIT_FAILURE);
265 	}
266 	if (unlink(n) != 0) {
267 		if (errno == ENOENT)
268 			fprintf(stderr, "no crontab for %s\n", User);
269 		else
270 			perror(n);
271 		exit(EXIT_FAILURE);
272 	}
273 	poke_daemon(SPOOL_DIR, RELOAD_CRON);
274 }
275 
276 static void
277 check_error(const char *msg) {
278 	CheckErrorCount++;
279 	fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
280 }
281 
282 static void
283 edit_cmd(void) {
284 	char n[MAX_FNAME], q[MAX_TEMPSTR];
285 	const char *tmpdir;
286 	FILE *f;
287 	int t;
288 	struct stat statbuf, xstatbuf;
289 	struct timespec ts[2];
290 
291 	log_it(RealUser, Pid, "BEGIN EDIT", User);
292 	if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
293 		fprintf(stderr, "path too long\n");
294 		exit(EXIT_FAILURE);
295 	}
296 	if (!(f = fopen(n, "r"))) {
297 		if (errno != ENOENT) {
298 			perror(n);
299 			exit(EXIT_FAILURE);
300 		}
301 		fprintf(stderr, "no crontab for %s - using an empty one\n",
302 			User);
303 		if (!(f = fopen(_PATH_DEVNULL, "r"))) {
304 			perror(_PATH_DEVNULL);
305 			exit(EXIT_FAILURE);
306 		}
307 	}
308 
309 	if (fstat(fileno(f), &statbuf) < 0) {
310 		perror("fstat");
311 		goto fatal;
312 	}
313 	ts[0] = statbuf.st_atim;
314 	ts[1] = statbuf.st_mtim;
315 
316 	/* Turn off signals. */
317 	(void)signal(SIGHUP, SIG_IGN);
318 	(void)signal(SIGINT, SIG_IGN);
319 	(void)signal(SIGQUIT, SIG_IGN);
320 
321 	tmpdir = getenv("TMPDIR");
322 	if (tmpdir == NULL || tmpdir[0] == '\0')
323 		tmpdir = _PATH_TMP;
324 	for (t = strlen(tmpdir); t != 0 && tmpdir[t - 1] == '/'; t--)
325 		continue;
326 	if (snprintf(Filename, sizeof Filename, "%.*s/crontab.XXXXXXXXXX",
327 	    t, tmpdir) >= sizeof(Filename)) {
328 		fprintf(stderr, "path too long\n");
329 		goto fatal;
330 	}
331 	if (swap_gids() < OK) {
332 		perror("swapping gids");
333 		exit(EXIT_FAILURE);
334 	}
335 	t = mkstemp(Filename);
336 	if (swap_gids_back() < OK) {
337 		perror("swapping gids back");
338 		exit(EXIT_FAILURE);
339 	}
340 	if (t == -1) {
341 		perror(Filename);
342 		goto fatal;
343 	}
344 	if (!(NewCrontab = fdopen(t, "r+"))) {
345 		perror("fdopen");
346 		goto fatal;
347 	}
348 
349 	Set_LineNum(1)
350 
351 	copy_crontab(f, NewCrontab);
352 	fclose(f);
353 	if (fflush(NewCrontab) < OK) {
354 		perror(Filename);
355 		exit(EXIT_FAILURE);
356 	}
357 	(void)futimens(t, ts);
358  again:
359 	rewind(NewCrontab);
360 	if (ferror(NewCrontab)) {
361 		fprintf(stderr, "%s: error while writing new crontab to %s\n",
362 			ProgramName, Filename);
363  fatal:
364 		if (swap_gids() < OK) {
365 			perror("swapping gids");
366 			exit(EXIT_FAILURE);
367 		}
368 		unlink(Filename);
369 		if (swap_gids_back() < OK) {
370 			perror("swapping gids back");
371 			exit(EXIT_FAILURE);
372 		}
373 		exit(EXIT_FAILURE);
374 	}
375 
376 	/* we still have the file open.  editors will generally rewrite the
377 	 * original file rather than renaming/unlinking it and starting a
378 	 * new one; even backup files are supposed to be made by copying
379 	 * rather than by renaming.  if some editor does not support this,
380 	 * then don't use it.  the security problems are more severe if we
381 	 * close and reopen the file around the edit.
382 	 */
383 	if (editit(Filename) == -1) {
384 		warn("error starting editor");
385 		goto fatal;
386 	}
387 
388 	if (fstat(t, &statbuf) < 0) {
389 		perror("fstat");
390 		goto fatal;
391 	}
392 	if (timespeccmp(&ts[1], &statbuf.st_mtim, ==)) {
393 		if (swap_gids() < OK) {
394 			perror("swapping gids");
395 			exit(EXIT_FAILURE);
396 		}
397 		if (lstat(Filename, &xstatbuf) == 0 &&
398 		    statbuf.st_ino != xstatbuf.st_ino) {
399 			fprintf(stderr, "%s: crontab temp file moved, editor "
400 			   "may create backup files improperly\n", ProgramName);
401 		}
402 		if (swap_gids_back() < OK) {
403 			perror("swapping gids back");
404 			exit(EXIT_FAILURE);
405 		}
406 		fprintf(stderr, "%s: no changes made to crontab\n",
407 			ProgramName);
408 		goto remove;
409 	}
410 	fprintf(stderr, "%s: installing new crontab\n", ProgramName);
411 	switch (replace_cmd()) {
412 	case 0:
413 		break;
414 	case -1:
415 		for (;;) {
416 			printf("Do you want to retry the same edit? ");
417 			fflush(stdout);
418 			q[0] = '\0';
419 			if (fgets(q, sizeof q, stdin) == NULL) {
420 				putchar('\n');
421 				goto abandon;
422 			}
423 			switch (q[0]) {
424 			case 'y':
425 			case 'Y':
426 				goto again;
427 			case 'n':
428 			case 'N':
429 				goto abandon;
430 			default:
431 				fprintf(stderr, "Enter Y or N\n");
432 			}
433 		}
434 		/*NOTREACHED*/
435 	case -2:
436 	abandon:
437 		fprintf(stderr, "%s: edits left in %s\n",
438 			ProgramName, Filename);
439 		goto done;
440 	default:
441 		fprintf(stderr, "%s: panic: bad switch() in replace_cmd()\n",
442 			ProgramName);
443 		goto fatal;
444 	}
445  remove:
446 	if (swap_gids() < OK) {
447 		perror("swapping gids");
448 		exit(EXIT_FAILURE);
449 	}
450 	unlink(Filename);
451 	if (swap_gids_back() < OK) {
452 		perror("swapping gids back");
453 		exit(EXIT_FAILURE);
454 	}
455  done:
456 	log_it(RealUser, Pid, "END EDIT", User);
457 }
458 
459 /* returns	0	on success
460  *		-1	on syntax error
461  *		-2	on install error
462  */
463 static int
464 replace_cmd(void) {
465 	char n[MAX_FNAME], envstr[MAX_ENVSTR];
466 	FILE *tmp;
467 	int ch, eof, fd;
468 	int error = 0;
469 	entry *e;
470 	time_t now = time(NULL);
471 	char **envp = env_init();
472 
473 	if (envp == NULL) {
474 		fprintf(stderr, "%s: Cannot allocate memory.\n", ProgramName);
475 		return (-2);
476 	}
477 	if (snprintf(TempFilename, sizeof TempFilename, "%s/tmp.XXXXXXXXX",
478 	    SPOOL_DIR) >= sizeof(TempFilename)) {
479 		TempFilename[0] = '\0';
480 		fprintf(stderr, "path too long\n");
481 		return (-2);
482 	}
483 	if ((fd = mkstemp(TempFilename)) == -1 || !(tmp = fdopen(fd, "w+"))) {
484 		perror(TempFilename);
485 		if (fd != -1) {
486 			close(fd);
487 			unlink(TempFilename);
488 		}
489 		TempFilename[0] = '\0';
490 		return (-2);
491 	}
492 
493 	(void) signal(SIGHUP, die);
494 	(void) signal(SIGINT, die);
495 	(void) signal(SIGQUIT, die);
496 
497 	/* write a signature at the top of the file.
498 	 *
499 	 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
500 	 */
501 	fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
502 	fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
503 	fprintf(tmp, "# (Cron version %s)\n", CRON_VERSION);
504 
505 	/* copy the crontab to the tmp
506 	 */
507 	rewind(NewCrontab);
508 	Set_LineNum(1)
509 	while (EOF != (ch = get_char(NewCrontab)))
510 		putc(ch, tmp);
511 	ftruncate(fileno(tmp), ftello(tmp));	/* XXX redundant with "w+"? */
512 	fflush(tmp);  rewind(tmp);
513 
514 	if (ferror(tmp)) {
515 		fprintf(stderr, "%s: error while writing new crontab to %s\n",
516 			ProgramName, TempFilename);
517 		fclose(tmp);
518 		error = -2;
519 		goto done;
520 	}
521 
522 	/* check the syntax of the file being installed.
523 	 */
524 
525 	/* BUG: was reporting errors after the EOF if there were any errors
526 	 * in the file proper -- kludged it by stopping after first error.
527 	 *		vix 31mar87
528 	 */
529 	Set_LineNum(1 - NHEADER_LINES)
530 	CheckErrorCount = 0;  eof = FALSE;
531 	while (!CheckErrorCount && !eof) {
532 		switch (load_env(envstr, tmp)) {
533 		case ERR:
534 			/* check for data before the EOF */
535 			if (envstr[0] != '\0') {
536 				Set_LineNum(LineNumber + 1);
537 				check_error("premature EOF");
538 			}
539 			eof = TRUE;
540 			break;
541 		case FALSE:
542 			e = load_entry(tmp, check_error, pw, envp);
543 			if (e)
544 				free_entry(e);
545 			break;
546 		case TRUE:
547 			break;
548 		}
549 	}
550 
551 	if (CheckErrorCount != 0) {
552 		fprintf(stderr, "errors in crontab file, can't install.\n");
553 		fclose(tmp);
554 		error = -1;
555 		goto done;
556 	}
557 
558 #ifdef HAVE_FCHOWN
559 	if (fchown(fileno(tmp), pw->pw_uid, -1) < OK) {
560 		perror("fchown");
561 		fclose(tmp);
562 		error = -2;
563 		goto done;
564 	}
565 #else
566 	if (chown(TempFilename, pw->pw_uid, -1) < OK) {
567 		perror("chown");
568 		fclose(tmp);
569 		error = -2;
570 		goto done;
571 	}
572 #endif
573 
574 	if (fclose(tmp) == EOF) {
575 		perror("fclose");
576 		error = -2;
577 		goto done;
578 	}
579 
580 	if (snprintf(n, sizeof n, "%s/%s", SPOOL_DIR, User) >= sizeof(n)) {
581 		fprintf(stderr, "path too long\n");
582 		error = -2;
583 		goto done;
584 	}
585 	if (rename(TempFilename, n)) {
586 		fprintf(stderr, "%s: error renaming %s to %s\n",
587 			ProgramName, TempFilename, n);
588 		perror("rename");
589 		error = -2;
590 		goto done;
591 	}
592 	TempFilename[0] = '\0';
593 	log_it(RealUser, Pid, "REPLACE", User);
594 
595 	poke_daemon(SPOOL_DIR, RELOAD_CRON);
596 
597 done:
598 	(void) signal(SIGHUP, SIG_DFL);
599 	(void) signal(SIGINT, SIG_DFL);
600 	(void) signal(SIGQUIT, SIG_DFL);
601 	if (TempFilename[0]) {
602 		(void) unlink(TempFilename);
603 		TempFilename[0] = '\0';
604 	}
605 	return (error);
606 }
607 
608 /*
609  * Execute an editor on the specified pathname, which is interpreted
610  * from the shell.  This means flags may be included.
611  *
612  * Returns -1 on error, or the exit value on success.
613  */
614 int
615 editit(const char *pathname)
616 {
617 	char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p;
618 	sig_t sighup, sigint, sigquit, sigchld;
619 	pid_t pid;
620 	int saved_errno, st, ret = -1;
621 
622 	ed = getenv("VISUAL");
623 	if (ed == NULL || ed[0] == '\0')
624 		ed = getenv("EDITOR");
625 	if (ed == NULL || ed[0] == '\0')
626 		ed = _PATH_VI;
627 	if (asprintf(&p, "%s %s", ed, pathname) == -1)
628 		return (-1);
629 	argp[2] = p;
630 
631 	sighup = signal(SIGHUP, SIG_IGN);
632 	sigint = signal(SIGINT, SIG_IGN);
633 	sigquit = signal(SIGQUIT, SIG_IGN);
634 	sigchld = signal(SIGCHLD, SIG_DFL);
635 	if ((pid = fork()) == -1)
636 		goto fail;
637 	if (pid == 0) {
638 		setgid(getgid());
639 		setuid(getuid());
640 		execv(_PATH_BSHELL, argp);
641 		_exit(127);
642 	}
643 	while (waitpid(pid, &st, 0) == -1)
644 		if (errno != EINTR)
645 			goto fail;
646 	if (!WIFEXITED(st))
647 		errno = EINTR;
648 	else
649 		ret = WEXITSTATUS(st);
650 
651  fail:
652 	saved_errno = errno;
653 	(void)signal(SIGHUP, sighup);
654 	(void)signal(SIGINT, sigint);
655 	(void)signal(SIGQUIT, sigquit);
656 	(void)signal(SIGCHLD, sigchld);
657 	free(p);
658 	errno = saved_errno;
659 	return (ret);
660 }
661 
662 static void
663 die(int x) {
664 	if (TempFilename[0])
665 		(void) unlink(TempFilename);
666 	_exit(EXIT_FAILURE);
667 }
668 
669 static void
670 copy_crontab(FILE *f, FILE *out) {
671 	int ch, x;
672 
673 	/* ignore the top few comments since we probably put them there.
674 	 */
675 	x = 0;
676 	while (EOF != (ch = get_char(f))) {
677 		if ('#' != ch) {
678 			putc(ch, out);
679 			break;
680 		}
681 		while (EOF != (ch = get_char(f)))
682 			if (ch == '\n')
683 				break;
684 		if (++x >= NHEADER_LINES)
685 			break;
686 	}
687 
688 	/* copy out the rest of the crontab (if any)
689 	 */
690 	if (EOF != ch)
691 		while (EOF != (ch = get_char(f)))
692 			putc(ch, out);
693 }
694