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