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