xref: /freebsd/usr.sbin/cron/crontab/crontab.c (revision 273c26a3c3bea87a241d6879abd4f991db180bf0)
1 /* Copyright 1988,1990,1993,1994 by Paul Vixie
2  * All rights reserved
3  *
4  * Distribute freely, except: don't remove my name from the source or
5  * documentation (don't take credit for my work), mark your changes (don't
6  * get me blamed for your possible bugs), don't alter or remove this
7  * notice.  May be sold if buildable source is provided to buyer.  No
8  * warrantee of any kind, express or implied, is included with this
9  * software; use at your own risk, responsibility for damages (if any) to
10  * anyone resulting from the use of this software rests entirely with the
11  * user.
12  *
13  * Send bug reports, bug fixes, enhancements, requests, flames, etc., and
14  * I'll try to keep a version up to date.  I can be reached as follows:
15  * Paul Vixie          <paul@vix.com>          uunet!decwrl!vixie!paul
16  * From Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp
17  */
18 
19 #if !defined(lint) && !defined(LINT)
20 static const char rcsid[] =
21   "$FreeBSD$";
22 #endif
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 #define	MAIN_PROGRAM
30 
31 #include <sys/param.h>
32 #include "cron.h"
33 #include <errno.h>
34 #include <fcntl.h>
35 #include <md5.h>
36 #include <paths.h>
37 #include <sys/file.h>
38 #include <sys/stat.h>
39 #ifdef USE_UTIMES
40 # include <sys/time.h>
41 #else
42 # include <time.h>
43 # include <utime.h>
44 #endif
45 #if defined(POSIX)
46 # include <locale.h>
47 #endif
48 
49 #define MD5_SIZE 33
50 #define NHEADER_LINES 3
51 
52 
53 enum opt_t	{ opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
54 
55 #if DEBUGGING
56 static char	*Options[] = { "???", "list", "delete", "edit", "replace" };
57 #endif
58 
59 
60 static	PID_T		Pid;
61 static	char		User[MAXLOGNAME], RealUser[MAXLOGNAME];
62 static	char		Filename[MAX_FNAME];
63 static	FILE		*NewCrontab;
64 static	int		CheckErrorCount;
65 static	enum opt_t	Option;
66 static	struct passwd	*pw;
67 static	void		list_cmd(void),
68 			delete_cmd(void),
69 			edit_cmd(void),
70 			poke_daemon(void),
71 			check_error(char *),
72 			parse_args(int c, char *v[]);
73 static	int		replace_cmd(void);
74 
75 
76 static void
77 usage(char *msg)
78 {
79 	fprintf(stderr, "crontab: usage error: %s\n", msg);
80 	fprintf(stderr, "%s\n%s\n",
81 		"usage: crontab [-u user] file",
82 		"       crontab [-u user] { -e | -l | -r }");
83 	exit(ERROR_EXIT);
84 }
85 
86 
87 int
88 main(int argc, char *argv[])
89 {
90 	int	exitstatus;
91 
92 	Pid = getpid();
93 	ProgramName = argv[0];
94 
95 #if defined(POSIX)
96 	setlocale(LC_ALL, "");
97 #endif
98 
99 #if defined(BSD)
100 	setlinebuf(stderr);
101 #endif
102 	parse_args(argc, argv);		/* sets many globals, opens a file */
103 	set_cron_uid();
104 	set_cron_cwd();
105 	if (!allowed(User)) {
106 		warnx("you (%s) are not allowed to use this program", User);
107 		log_it(RealUser, Pid, "AUTH", "crontab command not allowed");
108 		exit(ERROR_EXIT);
109 	}
110 	exitstatus = OK_EXIT;
111 	switch (Option) {
112 	case opt_list:		list_cmd();
113 				break;
114 	case opt_delete:	delete_cmd();
115 				break;
116 	case opt_edit:		edit_cmd();
117 				break;
118 	case opt_replace:	if (replace_cmd() < 0)
119 					exitstatus = ERROR_EXIT;
120 				break;
121 	case opt_unknown:
122 				break;
123 	}
124 	exit(exitstatus);
125 	/*NOTREACHED*/
126 }
127 
128 
129 static void
130 parse_args(argc, argv)
131 	int	argc;
132 	char	*argv[];
133 {
134 	int		argch;
135 	char		resolved_path[PATH_MAX];
136 
137 	if (!(pw = getpwuid(getuid())))
138 		errx(ERROR_EXIT, "your UID isn't in the passwd file, bailing out");
139 	bzero(pw->pw_passwd, strlen(pw->pw_passwd));
140 	(void) strncpy(User, pw->pw_name, (sizeof User)-1);
141 	User[(sizeof User)-1] = '\0';
142 	strcpy(RealUser, User);
143 	Filename[0] = '\0';
144 	Option = opt_unknown;
145 	while ((argch = getopt(argc, argv, "u:lerx:")) != -1) {
146 		switch (argch) {
147 		case 'x':
148 			if (!set_debug_flags(optarg))
149 				usage("bad debug option");
150 			break;
151 		case 'u':
152 			if (getuid() != ROOT_UID)
153 				errx(ERROR_EXIT, "must be privileged to use -u");
154 			if (!(pw = getpwnam(optarg)))
155 				errx(ERROR_EXIT, "user `%s' unknown", optarg);
156 			bzero(pw->pw_passwd, strlen(pw->pw_passwd));
157 			(void) strncpy(User, pw->pw_name, (sizeof User)-1);
158 			User[(sizeof User)-1] = '\0';
159 			break;
160 		case 'l':
161 			if (Option != opt_unknown)
162 				usage("only one operation permitted");
163 			Option = opt_list;
164 			break;
165 		case 'r':
166 			if (Option != opt_unknown)
167 				usage("only one operation permitted");
168 			Option = opt_delete;
169 			break;
170 		case 'e':
171 			if (Option != opt_unknown)
172 				usage("only one operation permitted");
173 			Option = opt_edit;
174 			break;
175 		default:
176 			usage("unrecognized option");
177 		}
178 	}
179 
180 	endpwent();
181 
182 	if (Option != opt_unknown) {
183 		if (argv[optind] != NULL) {
184 			usage("no arguments permitted after this option");
185 		}
186 	} else {
187 		if (argv[optind] != NULL) {
188 			Option = opt_replace;
189 			(void) strncpy (Filename, argv[optind], (sizeof Filename)-1);
190 			Filename[(sizeof Filename)-1] = '\0';
191 
192 		} else {
193 			usage("file name must be specified for replace");
194 		}
195 	}
196 
197 	if (Option == opt_replace) {
198 		/* relinquish the setuid status of the binary during
199 		 * the open, lest nonroot users read files they should
200 		 * not be able to read.  we can't use access() here
201 		 * since there's a race condition.  thanks go out to
202 		 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
203 		 * the race.
204 		 */
205 
206 		if (swap_uids() < OK)
207 			err(ERROR_EXIT, "swapping uids");
208 
209 		/* we have to open the file here because we're going to
210 		 * chdir(2) into /var/cron before we get around to
211 		 * reading the file.
212 		 */
213 		if (!strcmp(Filename, "-")) {
214 			NewCrontab = stdin;
215 		} else if (realpath(Filename, resolved_path) != NULL &&
216 		    !strcmp(resolved_path, SYSCRONTAB)) {
217 			err(ERROR_EXIT, SYSCRONTAB " must be edited manually");
218 		} else {
219 			if (!(NewCrontab = fopen(Filename, "r")))
220 				err(ERROR_EXIT, "%s", Filename);
221 		}
222 		if (swap_uids_back() < OK)
223 			err(ERROR_EXIT, "swapping uids back");
224 	}
225 
226 	Debug(DMISC, ("user=%s, file=%s, option=%s\n",
227 		      User, Filename, Options[(int)Option]))
228 }
229 
230 static void
231 copy_file(FILE *in, FILE *out) {
232 	int	x, ch;
233 
234 	Set_LineNum(1)
235 	/* ignore the top few comments since we probably put them there.
236 	 */
237 	for (x = 0;  x < NHEADER_LINES;  x++) {
238 		ch = get_char(in);
239 		if (EOF == ch)
240 			break;
241 		if ('#' != ch) {
242 			putc(ch, out);
243 			break;
244 		}
245 		while (EOF != (ch = get_char(in)))
246 			if (ch == '\n')
247 				break;
248 		if (EOF == ch)
249 			break;
250 	}
251 
252 	/* copy the rest of the crontab (if any) to the output file.
253 	 */
254 	if (EOF != ch)
255 		while (EOF != (ch = get_char(in)))
256 			putc(ch, out);
257 }
258 
259 static void
260 list_cmd() {
261 	char	n[MAX_FNAME];
262 	FILE	*f;
263 
264 	log_it(RealUser, Pid, "LIST", User);
265 	(void) snprintf(n, sizeof(n), CRON_TAB(User));
266 	if (!(f = fopen(n, "r"))) {
267 		if (errno == ENOENT)
268 			errx(ERROR_EXIT, "no crontab for %s", User);
269 		else
270 			err(ERROR_EXIT, "%s", n);
271 	}
272 
273 	/* file is open. copy to stdout, close.
274 	 */
275 	copy_file(f, stdout);
276 	fclose(f);
277 }
278 
279 
280 static void
281 delete_cmd() {
282 	char	n[MAX_FNAME];
283 	int ch, first;
284 
285 	if (isatty(STDIN_FILENO)) {
286 		(void)fprintf(stderr, "remove crontab for %s? ", User);
287 		first = ch = getchar();
288 		while (ch != '\n' && ch != EOF)
289 			ch = getchar();
290 		if (first != 'y' && first != 'Y')
291 			return;
292 	}
293 
294 	log_it(RealUser, Pid, "DELETE", User);
295 	(void) snprintf(n, sizeof(n), CRON_TAB(User));
296 	if (unlink(n)) {
297 		if (errno == ENOENT)
298 			errx(ERROR_EXIT, "no crontab for %s", User);
299 		else
300 			err(ERROR_EXIT, "%s", n);
301 	}
302 	poke_daemon();
303 }
304 
305 
306 static void
307 check_error(msg)
308 	char	*msg;
309 {
310 	CheckErrorCount++;
311 	fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
312 }
313 
314 
315 static void
316 edit_cmd() {
317 	char		n[MAX_FNAME], q[MAX_TEMPSTR], *editor;
318 	FILE		*f;
319 	int		t;
320 	struct stat	statbuf, fsbuf;
321 	WAIT_T		waiter;
322 	PID_T		pid, xpid;
323 	mode_t		um;
324 	int		syntax_error = 0;
325 	char		orig_md5[MD5_SIZE];
326 	char		new_md5[MD5_SIZE];
327 
328 	log_it(RealUser, Pid, "BEGIN EDIT", User);
329 	(void) snprintf(n, sizeof(n), CRON_TAB(User));
330 	if (!(f = fopen(n, "r"))) {
331 		if (errno != ENOENT)
332 			err(ERROR_EXIT, "%s", n);
333 		warnx("no crontab for %s - using an empty one", User);
334 		if (!(f = fopen(_PATH_DEVNULL, "r")))
335 			err(ERROR_EXIT, _PATH_DEVNULL);
336 	}
337 
338 	um = umask(077);
339 	(void) snprintf(Filename, sizeof(Filename), "/tmp/crontab.XXXXXXXXXX");
340 	if ((t = mkstemp(Filename)) == -1) {
341 		warn("%s", Filename);
342 		(void) umask(um);
343 		goto fatal;
344 	}
345 	(void) umask(um);
346 #ifdef HAS_FCHOWN
347 	if (fchown(t, getuid(), getgid()) < 0) {
348 #else
349 	if (chown(Filename, getuid(), getgid()) < 0) {
350 #endif
351 		warn("fchown");
352 		goto fatal;
353 	}
354 	if (!(NewCrontab = fdopen(t, "r+"))) {
355 		warn("fdopen");
356 		goto fatal;
357 	}
358 
359 	copy_file(f, NewCrontab);
360 	fclose(f);
361 	if (fflush(NewCrontab))
362 		err(ERROR_EXIT, "%s", Filename);
363 	if (fstat(t, &fsbuf) < 0) {
364 		warn("unable to fstat temp file");
365 		goto fatal;
366 	}
367  again:
368 	if (swap_uids() < OK)
369 		err(ERROR_EXIT, "swapping uids");
370 	if (stat(Filename, &statbuf) < 0) {
371 		warn("stat");
372  fatal:		unlink(Filename);
373 		exit(ERROR_EXIT);
374 	}
375 	if (swap_uids_back() < OK)
376 		err(ERROR_EXIT, "swapping uids back");
377 	if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
378 		errx(ERROR_EXIT, "temp file must be edited in place");
379 	if (MD5File(Filename, orig_md5) == NULL) {
380 		warn("MD5");
381 		goto fatal;
382 	}
383 
384 	if ((!(editor = getenv("VISUAL")))
385 	 && (!(editor = getenv("EDITOR")))
386 	    ) {
387 		editor = EDITOR;
388 	}
389 
390 	/* we still have the file open.  editors will generally rewrite the
391 	 * original file rather than renaming/unlinking it and starting a
392 	 * new one; even backup files are supposed to be made by copying
393 	 * rather than by renaming.  if some editor does not support this,
394 	 * then don't use it.  the security problems are more severe if we
395 	 * close and reopen the file around the edit.
396 	 */
397 
398 	switch (pid = fork()) {
399 	case -1:
400 		warn("fork");
401 		goto fatal;
402 	case 0:
403 		/* child */
404 		if (setuid(getuid()) < 0)
405 			err(ERROR_EXIT, "setuid(getuid())");
406 		if (chdir("/tmp") < 0)
407 			err(ERROR_EXIT, "chdir(/tmp)");
408 		if (strlen(editor) + strlen(Filename) + 2 >= MAX_TEMPSTR)
409 			errx(ERROR_EXIT, "editor or filename too long");
410 		execlp(editor, editor, Filename, (char *)NULL);
411 		err(ERROR_EXIT, "%s", editor);
412 		/*NOTREACHED*/
413 	default:
414 		/* parent */
415 		break;
416 	}
417 
418 	/* parent */
419 	{
420 	void (*sig[3])(int signal);
421 	sig[0] = signal(SIGHUP, SIG_IGN);
422 	sig[1] = signal(SIGINT, SIG_IGN);
423 	sig[2] = signal(SIGTERM, SIG_IGN);
424 	xpid = wait(&waiter);
425 	signal(SIGHUP, sig[0]);
426 	signal(SIGINT, sig[1]);
427 	signal(SIGTERM, sig[2]);
428 	}
429 	if (xpid != pid) {
430 		warnx("wrong PID (%d != %d) from \"%s\"", xpid, pid, editor);
431 		goto fatal;
432 	}
433 	if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) {
434 		warnx("\"%s\" exited with status %d", editor, WEXITSTATUS(waiter));
435 		goto fatal;
436 	}
437 	if (WIFSIGNALED(waiter)) {
438 		warnx("\"%s\" killed; signal %d (%score dumped)",
439 			editor, WTERMSIG(waiter), WCOREDUMP(waiter) ?"" :"no ");
440 		goto fatal;
441 	}
442 	if (swap_uids() < OK)
443 		err(ERROR_EXIT, "swapping uids");
444 	if (stat(Filename, &statbuf) < 0) {
445 		warn("stat");
446 		goto fatal;
447 	}
448 	if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
449 		errx(ERROR_EXIT, "temp file must be edited in place");
450 	if (MD5File(Filename, new_md5) == NULL) {
451 		warn("MD5");
452 		goto fatal;
453 	}
454 	if (swap_uids_back() < OK)
455 		err(ERROR_EXIT, "swapping uids back");
456 	if (strcmp(orig_md5, new_md5) == 0 && !syntax_error) {
457 		warnx("no changes made to crontab");
458 		goto remove;
459 	}
460 	warnx("installing new crontab");
461 	switch (replace_cmd()) {
462 	case 0:			/* Success */
463 		break;
464 	case -1:		/* Syntax error */
465 		for (;;) {
466 			printf("Do you want to retry the same edit? ");
467 			fflush(stdout);
468 			q[0] = '\0';
469 			(void) fgets(q, sizeof q, stdin);
470 			switch (islower(q[0]) ? q[0] : tolower(q[0])) {
471 			case 'y':
472 				syntax_error = 1;
473 				goto again;
474 			case 'n':
475 				goto abandon;
476 			default:
477 				fprintf(stderr, "Enter Y or N\n");
478 			}
479 		}
480 		/*NOTREACHED*/
481 	case -2:		/* Install error */
482 	abandon:
483 		warnx("edits left in %s", Filename);
484 		goto done;
485 	default:
486 		warnx("panic: bad switch() in replace_cmd()");
487 		goto fatal;
488 	}
489  remove:
490 	unlink(Filename);
491  done:
492 	log_it(RealUser, Pid, "END EDIT", User);
493 }
494 
495 
496 /* returns	0	on success
497  *		-1	on syntax error
498  *		-2	on install error
499  */
500 static int
501 replace_cmd() {
502 	char	n[MAX_FNAME], envstr[MAX_ENVSTR], tn[MAX_FNAME];
503 	FILE	*tmp;
504 	int	ch, eof;
505 	entry	*e;
506 	time_t	now = time(NULL);
507 	char	**envp = env_init();
508 
509 	if (envp == NULL) {
510 		warnx("cannot allocate memory");
511 		return (-2);
512 	}
513 
514 	(void) snprintf(n, sizeof(n), "tmp.%d", Pid);
515 	(void) snprintf(tn, sizeof(tn), CRON_TAB(n));
516 
517 	if (!(tmp = fopen(tn, "w+"))) {
518 		warn("%s", tn);
519 		return (-2);
520 	}
521 
522 	/* write a signature at the top of the file.
523 	 *
524 	 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
525 	 */
526 	fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
527 	fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
528 	fprintf(tmp, "# (Cron version -- %s)\n", rcsid);
529 
530 	/* copy the crontab to the tmp
531 	 */
532 	rewind(NewCrontab);
533 	Set_LineNum(1)
534 	while (EOF != (ch = get_char(NewCrontab)))
535 		putc(ch, tmp);
536 	ftruncate(fileno(tmp), ftello(tmp));
537 	fflush(tmp);  rewind(tmp);
538 
539 	if (ferror(tmp)) {
540 		warnx("error while writing new crontab to %s", tn);
541 		fclose(tmp);  unlink(tn);
542 		return (-2);
543 	}
544 
545 	/* check the syntax of the file being installed.
546 	 */
547 
548 	/* BUG: was reporting errors after the EOF if there were any errors
549 	 * in the file proper -- kludged it by stopping after first error.
550 	 *		vix 31mar87
551 	 */
552 	Set_LineNum(1 - NHEADER_LINES)
553 	CheckErrorCount = 0;  eof = FALSE;
554 	while (!CheckErrorCount && !eof) {
555 		switch (load_env(envstr, tmp)) {
556 		case ERR:
557 			eof = TRUE;
558 			break;
559 		case FALSE:
560 			e = load_entry(tmp, check_error, pw, envp);
561 			if (e)
562 				free_entry(e);
563 			break;
564 		case TRUE:
565 			break;
566 		}
567 	}
568 
569 	if (CheckErrorCount != 0) {
570 		warnx("errors in crontab file, can't install");
571 		fclose(tmp);  unlink(tn);
572 		return (-1);
573 	}
574 
575 #ifdef HAS_FCHOWN
576 	if (fchown(fileno(tmp), ROOT_UID, -1) < OK)
577 #else
578 	if (chown(tn, ROOT_UID, -1) < OK)
579 #endif
580 	{
581 		warn("chown");
582 		fclose(tmp);  unlink(tn);
583 		return (-2);
584 	}
585 
586 #ifdef HAS_FCHMOD
587 	if (fchmod(fileno(tmp), 0600) < OK)
588 #else
589 	if (chmod(tn, 0600) < OK)
590 #endif
591 	{
592 		warn("chown");
593 		fclose(tmp);  unlink(tn);
594 		return (-2);
595 	}
596 
597 	if (fclose(tmp) == EOF) {
598 		warn("fclose");
599 		unlink(tn);
600 		return (-2);
601 	}
602 
603 	(void) snprintf(n, sizeof(n), CRON_TAB(User));
604 	if (rename(tn, n)) {
605 		warn("error renaming %s to %s", tn, n);
606 		unlink(tn);
607 		return (-2);
608 	}
609 
610 	log_it(RealUser, Pid, "REPLACE", User);
611 
612 	/*
613 	 * Creating the 'tn' temp file has already updated the
614 	 * modification time of the spool directory.  Sleep for a
615 	 * second to ensure that poke_daemon() sets a later
616 	 * modification time.  Otherwise, this can race with the cron
617 	 * daemon scanning for updated crontabs.
618 	 */
619 	sleep(1);
620 
621 	poke_daemon();
622 
623 	return (0);
624 }
625 
626 
627 static void
628 poke_daemon() {
629 #ifdef USE_UTIMES
630 	struct timeval tvs[2];
631 
632 	(void)gettimeofday(&tvs[0], NULL);
633 	tvs[1] = tvs[0];
634 	if (utimes(SPOOL_DIR, tvs) < OK) {
635 		warn("can't update mtime on spooldir %s", SPOOL_DIR);
636 		return;
637 	}
638 #else
639 	if (utime(SPOOL_DIR, NULL) < OK) {
640 		warn("can't update mtime on spooldir %s", SPOOL_DIR);
641 		return;
642 	}
643 #endif /*USE_UTIMES*/
644 }
645