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
usage(const char * msg)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
main(int argc,char * argv[])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
parse_args(int argc,char * argv[])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
copy_file(FILE * in,FILE * out)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
list_cmd(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
delete_cmd(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
check_error(const char * msg)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
edit_cmd(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