Pre-2.0 release, MFC 63 - HAMMER I/O error handling and catastrophic
[dragonfly.git] / usr.sbin / cron / crontab / crontab.c
blob2b67265dfc743eedbee55513115abb4b6487036a
1 /* Copyright 1988,1990,1993,1994 by Paul Vixie
2 * All rights reserved
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.
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
17 * From Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp
18 * $FreeBSD: src/usr.sbin/cron/crontab/crontab.c,v 1.12.2.4 2001/06/16 03:18:37 peter Exp $
19 * $DragonFly: src/usr.sbin/cron/crontab/crontab.c,v 1.5 2004/12/18 22:48:03 swildner Exp $
22 /* crontab - install and manage per-user crontab files
23 * vix 02may87 [RCS has the rest of the log]
24 * vix 26jan87 [original]
27 static char version[] = "$DragonFly: src/usr.sbin/cron/crontab/crontab.c,v 1.5 2004/12/18 22:48:03 swildner Exp $";
29 #define MAIN_PROGRAM
31 #include "cron.h"
32 #include <errno.h>
33 #include <fcntl.h>
34 #include <paths.h>
35 #include <sys/file.h>
36 #include <sys/stat.h>
37 #ifdef USE_UTIMES
38 # include <sys/time.h>
39 #else
40 # include <time.h>
41 # include <utime.h>
42 #endif
43 #if defined(POSIX)
44 # include <locale.h>
45 #endif
48 #define NHEADER_LINES 3
51 enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
53 #if DEBUGGING
54 static char *Options[] = { "???", "list", "delete", "edit", "replace" };
55 #endif
58 static PID_T Pid;
59 static char User[MAX_UNAME], RealUser[MAX_UNAME];
60 static char Filename[MAX_FNAME];
61 static FILE *NewCrontab;
62 static int CheckErrorCount;
63 static enum opt_t Option;
64 static struct passwd *pw;
65 static void list_cmd(void),
66 delete_cmd(void),
67 edit_cmd(void),
68 poke_daemon(void),
69 check_error(char *),
70 parse_args(int c, char *v[]);
71 static int replace_cmd(void);
74 static void
75 usage(char *msg)
77 fprintf(stderr, "crontab: usage error: %s\n", msg);
78 fprintf(stderr, "%s\n%s\n",
79 "usage: crontab [-u user] file",
80 " crontab [-u user] { -e | -l | -r }");
81 exit(ERROR_EXIT);
85 int
86 main(int argc, char **argv)
88 int exitstatus;
90 Pid = getpid();
91 ProgramName = argv[0];
93 #if defined(POSIX)
94 setlocale(LC_ALL, "");
95 #endif
97 #if defined(BSD)
98 setlinebuf(stderr);
99 #endif
100 parse_args(argc, argv); /* sets many globals, opens a file */
101 set_cron_uid();
102 set_cron_cwd();
103 if (!allowed(User)) {
104 warnx("you (%s) are not allowed to use this program", User);
105 log_it(RealUser, Pid, "AUTH", "crontab command not allowed");
106 exit(ERROR_EXIT);
108 exitstatus = OK_EXIT;
109 switch (Option) {
110 case opt_list: list_cmd();
111 break;
112 case opt_delete: delete_cmd();
113 break;
114 case opt_edit: edit_cmd();
115 break;
116 case opt_replace: if (replace_cmd() < 0)
117 exitstatus = ERROR_EXIT;
118 break;
119 case opt_unknown:
120 break;
122 exit(0);
123 /*NOTREACHED*/
127 static void
128 parse_args(int argc, char **argv)
130 int argch;
132 if (!(pw = getpwuid(getuid())))
133 errx(ERROR_EXIT, "your UID isn't in the passwd file, bailing out");
134 strncpy(User, pw->pw_name, (sizeof User)-1);
135 User[(sizeof User)-1] = '\0';
136 strcpy(RealUser, User);
137 Filename[0] = '\0';
138 Option = opt_unknown;
139 while ((argch = getopt(argc, argv, "u:lerx:")) != -1) {
140 switch (argch) {
141 case 'x':
142 if (!set_debug_flags(optarg))
143 usage("bad debug option");
144 break;
145 case 'u':
146 if (getuid() != ROOT_UID)
147 errx(ERROR_EXIT, "must be privileged to use -u");
148 if (!(pw = getpwnam(optarg)))
149 errx(ERROR_EXIT, "user `%s' unknown", optarg);
150 strncpy(User, pw->pw_name, (sizeof User)-1);
151 User[(sizeof User)-1] = '\0';
152 break;
153 case 'l':
154 if (Option != opt_unknown)
155 usage("only one operation permitted");
156 Option = opt_list;
157 break;
158 case 'r':
159 if (Option != opt_unknown)
160 usage("only one operation permitted");
161 Option = opt_delete;
162 break;
163 case 'e':
164 if (Option != opt_unknown)
165 usage("only one operation permitted");
166 Option = opt_edit;
167 break;
168 default:
169 usage("unrecognized option");
173 endpwent();
175 if (Option != opt_unknown) {
176 if (argv[optind] != NULL) {
177 usage("no arguments permitted after this option");
179 } else {
180 if (argv[optind] != NULL) {
181 Option = opt_replace;
182 strncpy (Filename, argv[optind], (sizeof Filename)-1);
183 Filename[(sizeof Filename)-1] = '\0';
185 } else {
186 usage("file name must be specified for replace");
190 if (Option == opt_replace) {
191 /* we have to open the file here because we're going to
192 * chdir(2) into /var/cron before we get around to
193 * reading the file.
195 if (!strcmp(Filename, "-")) {
196 NewCrontab = stdin;
197 } else {
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.
206 if (swap_uids() < OK)
207 err(ERROR_EXIT, "swapping uids");
208 if (!(NewCrontab = fopen(Filename, "r")))
209 err(ERROR_EXIT, "%s", Filename);
210 if (swap_uids() < OK)
211 err(ERROR_EXIT, "swapping uids back");
215 Debug(DMISC, ("user=%s, file=%s, option=%s\n",
216 User, Filename, Options[(int)Option]))
220 static void
221 list_cmd(void)
223 char n[MAX_FNAME];
224 FILE *f;
225 int ch, x;
227 log_it(RealUser, Pid, "LIST", User);
228 sprintf(n, CRON_TAB(User));
229 if (!(f = fopen(n, "r"))) {
230 if (errno == ENOENT)
231 errx(ERROR_EXIT, "no crontab for %s", User);
232 else
233 err(ERROR_EXIT, "%s", n);
236 /* file is open. copy to stdout, close.
238 Set_LineNum(1)
240 /* ignore the top few comments since we probably put them there.
242 for (x = 0; x < NHEADER_LINES; x++) {
243 ch = get_char(f);
244 if (EOF == ch)
245 break;
246 if ('#' != ch) {
247 putchar(ch);
248 break;
250 while (EOF != (ch = get_char(f)))
251 if (ch == '\n')
252 break;
253 if (EOF == ch)
254 break;
257 while (EOF != (ch = get_char(f)))
258 putchar(ch);
259 fclose(f);
263 static void
264 delete_cmd(void)
266 char n[MAX_FNAME];
267 int ch, first;
269 if (isatty(STDIN_FILENO)) {
270 fprintf(stderr, "remove crontab for %s? ", User);
271 first = ch = getchar();
272 while (ch != '\n' && ch != EOF)
273 ch = getchar();
274 if (first != 'y' && first != 'Y')
275 return;
278 log_it(RealUser, Pid, "DELETE", User);
279 sprintf(n, CRON_TAB(User));
280 if (unlink(n)) {
281 if (errno == ENOENT)
282 errx(ERROR_EXIT, "no crontab for %s", User);
283 else
284 err(ERROR_EXIT, "%s", n);
286 poke_daemon();
290 static void
291 check_error(char *msg)
293 CheckErrorCount++;
294 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
298 static void
299 edit_cmd(void)
301 char n[MAX_FNAME], q[MAX_TEMPSTR], *editor;
302 FILE *f;
303 int ch, t, x;
304 struct stat statbuf, fsbuf;
305 time_t mtime;
306 WAIT_T waiter;
307 PID_T pid, xpid;
308 mode_t um;
310 log_it(RealUser, Pid, "BEGIN EDIT", User);
311 sprintf(n, CRON_TAB(User));
312 if (!(f = fopen(n, "r"))) {
313 if (errno != ENOENT)
314 err(ERROR_EXIT, "%s", n);
315 warnx("no crontab for %s - using an empty one", User);
316 if (!(f = fopen(_PATH_DEVNULL, "r")))
317 err(ERROR_EXIT, _PATH_DEVNULL);
320 um = umask(077);
321 sprintf(Filename, "/tmp/crontab.XXXXXXXXXX");
322 if ((t = mkstemp(Filename)) == -1) {
323 warn("%s", Filename);
324 umask(um);
325 goto fatal;
327 umask(um);
328 #ifdef HAS_FCHOWN
329 if (fchown(t, getuid(), getgid()) < 0) {
330 #else
331 if (chown(Filename, getuid(), getgid()) < 0) {
332 #endif
333 warn("fchown");
334 goto fatal;
336 if (!(NewCrontab = fdopen(t, "r+"))) {
337 warn("fdopen");
338 goto fatal;
341 Set_LineNum(1)
343 /* ignore the top few comments since we probably put them there.
345 for (x = 0; x < NHEADER_LINES; x++) {
346 ch = get_char(f);
347 if (EOF == ch)
348 break;
349 if ('#' != ch) {
350 putc(ch, NewCrontab);
351 break;
353 while (EOF != (ch = get_char(f)))
354 if (ch == '\n')
355 break;
356 if (EOF == ch)
357 break;
360 /* copy the rest of the crontab (if any) to the temp file.
362 if (EOF != ch)
363 while (EOF != (ch = get_char(f)))
364 putc(ch, NewCrontab);
365 fclose(f);
366 if (fflush(NewCrontab))
367 err(ERROR_EXIT, "%s", Filename);
368 if (fstat(t, &fsbuf) < 0) {
369 warn("unable to fstat temp file");
370 goto fatal;
372 again:
373 if (stat(Filename, &statbuf) < 0) {
374 warn("stat");
375 fatal: unlink(Filename);
376 exit(ERROR_EXIT);
378 if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
379 errx(ERROR_EXIT, "temp file must be edited in place");
380 mtime = statbuf.st_mtime;
382 if ((!(editor = getenv("VISUAL")))
383 && (!(editor = getenv("EDITOR")))
385 editor = EDITOR;
388 /* we still have the file open. editors will generally rewrite the
389 * original file rather than renaming/unlinking it and starting a
390 * new one; even backup files are supposed to be made by copying
391 * rather than by renaming. if some editor does not support this,
392 * then don't use it. the security problems are more severe if we
393 * close and reopen the file around the edit.
396 switch (pid = fork()) {
397 case -1:
398 warn("fork");
399 goto fatal;
400 case 0:
401 /* child */
402 if (setuid(getuid()) < 0)
403 err(ERROR_EXIT, "setuid(getuid())");
404 if (chdir("/tmp") < 0)
405 err(ERROR_EXIT, "chdir(/tmp)");
406 if (strlen(editor) + strlen(Filename) + 2 >= MAX_TEMPSTR)
407 errx(ERROR_EXIT, "editor or filename too long");
408 execlp(editor, editor, Filename, NULL);
409 err(ERROR_EXIT, "%s", editor);
410 /*NOTREACHED*/
411 default:
412 /* parent */
413 break;
416 /* parent */
418 void (*f[4])();
419 f[0] = signal(SIGHUP, SIG_IGN);
420 f[1] = signal(SIGINT, SIG_IGN);
421 f[2] = signal(SIGTERM, SIG_IGN);
422 xpid = wait(&waiter);
423 signal(SIGHUP, f[0]);
424 signal(SIGINT, f[1]);
425 signal(SIGTERM, f[2]);
427 if (xpid != pid) {
428 warnx("wrong PID (%d != %d) from \"%s\"", xpid, pid, editor);
429 goto fatal;
431 if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) {
432 warnx("\"%s\" exited with status %d", editor, WEXITSTATUS(waiter));
433 goto fatal;
435 if (WIFSIGNALED(waiter)) {
436 warnx("\"%s\" killed; signal %d (%score dumped)",
437 editor, WTERMSIG(waiter), WCOREDUMP(waiter) ?"" :"no ");
438 goto fatal;
440 if (stat(Filename, &statbuf) < 0) {
441 warn("stat");
442 goto fatal;
444 if (statbuf.st_dev != fsbuf.st_dev || statbuf.st_ino != fsbuf.st_ino)
445 errx(ERROR_EXIT, "temp file must be edited in place");
446 if (mtime == statbuf.st_mtime) {
447 warnx("no changes made to crontab");
448 goto remove;
450 warnx("installing new crontab");
451 switch (replace_cmd()) {
452 case 0:
453 break;
454 case -1:
455 for (;;) {
456 printf("Do you want to retry the same edit? ");
457 fflush(stdout);
458 q[0] = '\0';
459 fgets(q, sizeof q, stdin);
460 switch (islower(q[0]) ? q[0] : tolower(q[0])) {
461 case 'y':
462 goto again;
463 case 'n':
464 goto abandon;
465 default:
466 fprintf(stderr, "Enter Y or N\n");
469 /*NOTREACHED*/
470 case -2:
471 abandon:
472 warnx("edits left in %s", Filename);
473 goto done;
474 default:
475 warnx("panic: bad switch() in replace_cmd()");
476 goto fatal;
478 remove:
479 unlink(Filename);
480 done:
481 log_it(RealUser, Pid, "END EDIT", User);
485 /* returns 0 on success
486 * -1 on syntax error
487 * -2 on install error
489 static int
490 replace_cmd(void)
492 char n[MAX_FNAME], envstr[MAX_ENVSTR], tn[MAX_FNAME];
493 FILE *tmp;
494 int ch, eof;
495 entry *e;
496 time_t now = time(NULL);
497 char **envp = env_init();
499 if (envp == NULL) {
500 warnx("cannot allocate memory");
501 return (-2);
504 sprintf(n, "tmp.%d", Pid);
505 sprintf(tn, CRON_TAB(n));
506 if (!(tmp = fopen(tn, "w+"))) {
507 warn("%s", tn);
508 return (-2);
511 /* write a signature at the top of the file.
513 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
515 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
516 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
517 fprintf(tmp, "# (Cron version -- %s)\n", version);
519 /* copy the crontab to the tmp
521 rewind(NewCrontab);
522 Set_LineNum(1)
523 while (EOF != (ch = get_char(NewCrontab)))
524 putc(ch, tmp);
525 ftruncate(fileno(tmp), ftell(tmp));
526 fflush(tmp); rewind(tmp);
528 if (ferror(tmp)) {
529 warnx("error while writing new crontab to %s", tn);
530 fclose(tmp); unlink(tn);
531 return (-2);
534 /* check the syntax of the file being installed.
537 /* BUG: was reporting errors after the EOF if there were any errors
538 * in the file proper -- kludged it by stopping after first error.
539 * vix 31mar87
541 Set_LineNum(1 - NHEADER_LINES)
542 CheckErrorCount = 0; eof = FALSE;
543 while (!CheckErrorCount && !eof) {
544 switch (load_env(envstr, tmp)) {
545 case ERR:
546 eof = TRUE;
547 break;
548 case FALSE:
549 e = load_entry(tmp, check_error, pw, envp);
550 if (e)
551 free(e);
552 break;
553 case TRUE:
554 break;
558 if (CheckErrorCount != 0) {
559 warnx("errors in crontab file, can't install");
560 fclose(tmp); unlink(tn);
561 return (-1);
564 #ifdef HAS_FCHOWN
565 if (fchown(fileno(tmp), ROOT_UID, -1) < OK)
566 #else
567 if (chown(tn, ROOT_UID, -1) < OK)
568 #endif
570 warn("chown");
571 fclose(tmp); unlink(tn);
572 return (-2);
575 #ifdef HAS_FCHMOD
576 if (fchmod(fileno(tmp), 0600) < OK)
577 #else
578 if (chmod(tn, 0600) < OK)
579 #endif
581 warn("chown");
582 fclose(tmp); unlink(tn);
583 return (-2);
586 if (fclose(tmp) == EOF) {
587 warn("fclose");
588 unlink(tn);
589 return (-2);
592 sprintf(n, CRON_TAB(User));
593 if (rename(tn, n)) {
594 warn("error renaming %s to %s", tn, n);
595 unlink(tn);
596 return (-2);
598 log_it(RealUser, Pid, "REPLACE", User);
600 poke_daemon();
602 return (0);
606 static void
607 poke_daemon(void)
609 #ifdef USE_UTIMES
610 struct timeval tvs[2];
611 struct timezone tz;
613 gettimeofday(&tvs[0], &tz);
614 tvs[1] = tvs[0];
615 if (utimes(SPOOL_DIR, tvs) < OK) {
616 warn("can't update mtime on spooldir %s", SPOOL_DIR);
617 return;
619 #else
620 if (utime(SPOOL_DIR, NULL) < OK) {
621 warn("can't update mtime on spooldir %s", SPOOL_DIR);
622 return;
624 #endif /*USE_UTIMES*/