1 /* $OpenBSD: crontab.c,v 1.96 2023/05/05 13:50:40 millert Exp $ */
2
3 /* Copyright 1988,1990,1993,1994 by Paul Vixie
4 * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
5 * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
6 *
7 * Permission to use, copy, modify, and distribute this software for any
8 * purpose with or without fee is hereby granted, provided that the above
9 * copyright notice and this permission notice appear in all copies.
10 *
11 * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
12 * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
13 * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR
14 * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
15 * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
16 * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
17 * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
18 */
19
20 #include <sys/types.h>
21 #include <sys/stat.h>
22 #include <sys/time.h>
23 #include <sys/wait.h>
24
25 #include <bitstring.h> /* for structs.h */
26 #include <err.h>
27 #include <errno.h>
28 #include <limits.h>
29 #include <pwd.h>
30 #include <signal.h>
31 #include <stdio.h>
32 #include <stdlib.h>
33 #include <string.h>
34 #include <syslog.h>
35 #include <time.h>
36 #include <unistd.h>
37
38 #include "pathnames.h"
39 #include "macros.h"
40 #include "structs.h"
41 #include "funcs.h"
42 #include "globals.h"
43
44 #define NHEADER_LINES 3
45
46 enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
47
48 static gid_t crontab_gid;
49 static gid_t user_gid;
50 static char User[MAX_UNAME], RealUser[MAX_UNAME];
51 static char Filename[PATH_MAX], TempFilename[PATH_MAX];
52 static FILE *NewCrontab;
53 static int CheckErrorCount;
54 static enum opt_t Option;
55 static struct passwd *pw;
56 int editit(const char *);
57 static void list_cmd(void),
58 delete_cmd(void),
59 edit_cmd(void),
60 check_error(const char *),
61 parse_args(int c, char *v[]),
62 copy_crontab(FILE *, FILE *),
63 die(int);
64 static int replace_cmd(void);
65
66 static void
usage(const char * msg)67 usage(const char *msg)
68 {
69 if (msg != NULL)
70 warnx("usage error: %s", msg);
71 fprintf(stderr, "usage: %s [-u user] file\n", __progname);
72 fprintf(stderr, " %s [-e | -l | -r] [-u user]\n", __progname);
73
74 exit(EXIT_FAILURE);
75 }
76
77 int
main(int argc,char * argv[])78 main(int argc, char *argv[])
79 {
80 int exitstatus;
81
82 if (pledge("stdio rpath wpath cpath fattr getpw unix id proc exec",
83 NULL) == -1) {
84 err(EXIT_FAILURE, "pledge");
85 }
86
87 user_gid = getgid();
88 crontab_gid = getegid();
89
90 openlog(__progname, LOG_PID, LOG_CRON);
91
92 setvbuf(stderr, NULL, _IOLBF, 0);
93 parse_args(argc, argv); /* sets many globals, opens a file */
94 if (!allowed(RealUser, _PATH_CRON_ALLOW, _PATH_CRON_DENY)) {
95 fprintf(stderr, "You do not have permission to use crontab\n");
96 fprintf(stderr, "See crontab(1) for more information\n");
97 syslog(LOG_WARNING, "(%s) AUTH (crontab command not allowed)",
98 RealUser);
99 exit(EXIT_FAILURE);
100 }
101 exitstatus = EXIT_SUCCESS;
102 switch (Option) {
103 case opt_list:
104 list_cmd();
105 break;
106 case opt_delete:
107 delete_cmd();
108 break;
109 case opt_edit:
110 edit_cmd();
111 break;
112 case opt_replace:
113 if (replace_cmd() < 0)
114 exitstatus = EXIT_FAILURE;
115 break;
116 default:
117 exitstatus = EXIT_FAILURE;
118 break;
119 }
120 exit(exitstatus);
121 /*NOTREACHED*/
122 }
123
124 static void
parse_args(int argc,char * argv[])125 parse_args(int argc, char *argv[])
126 {
127 int argch;
128
129 if (!(pw = getpwuid(getuid())))
130 errx(EXIT_FAILURE, "your UID isn't in the password database");
131 if (strlen(pw->pw_name) >= sizeof User)
132 errx(EXIT_FAILURE, "username too long");
133 strlcpy(User, pw->pw_name, sizeof(User));
134 strlcpy(RealUser, User, sizeof(RealUser));
135 Filename[0] = '\0';
136 Option = opt_unknown;
137 while ((argch = getopt(argc, argv, "u:ler")) != -1) {
138 switch (argch) {
139 case 'u':
140 if (getuid() != 0)
141 errx(EXIT_FAILURE,
142 "only the super user may use -u");
143 if (!(pw = getpwnam(optarg)))
144 errx(EXIT_FAILURE, "unknown user %s", optarg);
145 if (strlcpy(User, optarg, sizeof User) >= sizeof User)
146 usage("username too long");
147 break;
148 case 'l':
149 if (Option != opt_unknown)
150 usage("only one operation permitted");
151 Option = opt_list;
152 break;
153 case 'r':
154 if (Option != opt_unknown)
155 usage("only one operation permitted");
156 Option = opt_delete;
157 break;
158 case 'e':
159 if (Option != opt_unknown)
160 usage("only one operation permitted");
161 Option = opt_edit;
162 break;
163 default:
164 usage(NULL);
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 } else {
174 if (argv[optind] != NULL) {
175 Option = opt_replace;
176 if (strlcpy(Filename, argv[optind], sizeof Filename)
177 >= sizeof Filename)
178 usage("filename too long");
179 } else
180 usage("file name must be specified for replace");
181 }
182
183 if (Option == opt_replace) {
184 /* XXX - no longer need to open the file early, move this. */
185 if (!strcmp(Filename, "-"))
186 NewCrontab = stdin;
187 else {
188 /* relinquish the setgid status of the binary during
189 * the open, lest nonroot users read files they should
190 * not be able to read. we can't use access() here
191 * since there's a race condition. thanks go out to
192 * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
193 * the race.
194 */
195
196 if (setegid(user_gid) == -1)
197 err(EXIT_FAILURE, "setegid(user_gid)");
198 if (!(NewCrontab = fopen(Filename, "r")))
199 err(EXIT_FAILURE, "%s", Filename);
200 if (setegid(crontab_gid) == -1)
201 err(EXIT_FAILURE, "setegid(crontab_gid)");
202 }
203 }
204 }
205
206 static void
list_cmd(void)207 list_cmd(void)
208 {
209 char n[PATH_MAX];
210 FILE *f;
211
212 syslog(LOG_INFO, "(%s) LIST (%s)", RealUser, User);
213 if (snprintf(n, sizeof n, "%s/%s", _PATH_CRON_SPOOL, User) >= sizeof(n))
214 errc(EXIT_FAILURE, ENAMETOOLONG, "%s/%s", _PATH_CRON_SPOOL, User);
215 if (!(f = fopen(n, "r"))) {
216 if (errno == ENOENT)
217 warnx("no crontab for %s", User);
218 else
219 warn("%s", n);
220 exit(EXIT_FAILURE);
221 }
222
223 /* file is open. copy to stdout, close.
224 */
225 Set_LineNum(1)
226
227 copy_crontab(f, stdout);
228 fclose(f);
229 }
230
231 static void
delete_cmd(void)232 delete_cmd(void)
233 {
234 char n[PATH_MAX];
235
236 syslog(LOG_INFO, "(%s) DELETE (%s)", RealUser, User);
237 if (snprintf(n, sizeof n, "%s/%s", _PATH_CRON_SPOOL, User) >= sizeof(n))
238 errc(EXIT_FAILURE, ENAMETOOLONG, "%s/%s", _PATH_CRON_SPOOL, User);
239 if (unlink(n) != 0) {
240 if (errno == ENOENT)
241 warnx("no crontab for %s", User);
242 else
243 warn("%s", n);
244 exit(EXIT_FAILURE);
245 }
246 poke_daemon(RELOAD_CRON);
247 }
248
249 static void
check_error(const char * msg)250 check_error(const char *msg)
251 {
252 CheckErrorCount++;
253 fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
254 }
255
256 static void
edit_cmd(void)257 edit_cmd(void)
258 {
259 char n[PATH_MAX], q[MAX_TEMPSTR];
260 FILE *f;
261 int t;
262 struct stat statbuf, xstatbuf;
263 struct timespec ts[2];
264
265 syslog(LOG_INFO, "(%s) BEGIN EDIT (%s)", RealUser, User);
266 if (snprintf(n, sizeof n, "%s/%s", _PATH_CRON_SPOOL, User) >= sizeof(n))
267 errc(EXIT_FAILURE, ENAMETOOLONG, "%s/%s", _PATH_CRON_SPOOL, User);
268 if (!(f = fopen(n, "r"))) {
269 if (errno != ENOENT)
270 err(EXIT_FAILURE, "%s", n);
271 warnx("creating new crontab for %s", User);
272 if (!(f = fopen(_PATH_DEVNULL, "r")))
273 err(EXIT_FAILURE, _PATH_DEVNULL);
274 }
275
276 if (fstat(fileno(f), &statbuf) == -1) {
277 warn("fstat");
278 goto fatal;
279 }
280 ts[0] = statbuf.st_atim;
281 ts[1] = statbuf.st_mtim;
282
283 /* Turn off signals. */
284 (void)signal(SIGHUP, SIG_IGN);
285 (void)signal(SIGINT, SIG_IGN);
286 (void)signal(SIGQUIT, SIG_IGN);
287
288 if (snprintf(Filename, sizeof Filename, "%scrontab.XXXXXXXXXX",
289 _PATH_TMP) >= sizeof(Filename)) {
290 warnc(ENAMETOOLONG, "%scrontab.XXXXXXXXXX", _PATH_TMP);
291 goto fatal;
292 }
293 t = mkstemp(Filename);
294 if (t == -1) {
295 warn("%s", Filename);
296 goto fatal;
297 }
298 if (!(NewCrontab = fdopen(t, "r+"))) {
299 warn("fdopen");
300 goto fatal;
301 }
302
303 Set_LineNum(1)
304
305 copy_crontab(f, NewCrontab);
306 fclose(f);
307 if (fflush(NewCrontab) == EOF)
308 err(EXIT_FAILURE, "%s", Filename);
309 if (futimens(t, ts) == -1)
310 warn("unable to set times on %s", Filename);
311 again:
312 rewind(NewCrontab);
313 if (ferror(NewCrontab)) {
314 warnx("error writing new crontab to %s", Filename);
315 fatal:
316 unlink(Filename);
317 exit(EXIT_FAILURE);
318 }
319
320 /* we still have the file open. editors will generally rewrite the
321 * original file rather than renaming/unlinking it and starting a
322 * new one; even backup files are supposed to be made by copying
323 * rather than by renaming. if some editor does not support this,
324 * then don't use it. the security problems are more severe if we
325 * close and reopen the file around the edit.
326 */
327 if (editit(Filename) == -1) {
328 warn("error starting editor");
329 goto fatal;
330 }
331
332 if (fstat(t, &statbuf) == -1) {
333 warn("fstat");
334 goto fatal;
335 }
336 if (timespeccmp(&ts[1], &statbuf.st_mtim, ==)) {
337 if (lstat(Filename, &xstatbuf) == 0 &&
338 statbuf.st_ino != xstatbuf.st_ino) {
339 warnx("crontab temp file moved, editor "
340 "may create backup files improperly");
341 }
342 warnx("no changes made to crontab");
343 goto remove;
344 }
345 warnx("installing new crontab");
346 switch (replace_cmd()) {
347 case 0:
348 break;
349 case -1:
350 for (;;) {
351 printf("Do you want to retry the same edit? ");
352 fflush(stdout);
353 q[0] = '\0';
354 if (fgets(q, sizeof q, stdin) == NULL) {
355 putchar('\n');
356 goto abandon;
357 }
358 switch (q[0]) {
359 case 'y':
360 case 'Y':
361 goto again;
362 case 'n':
363 case 'N':
364 goto abandon;
365 default:
366 fprintf(stderr, "Enter Y or N\n");
367 }
368 }
369 /*NOTREACHED*/
370 case -2:
371 abandon:
372 warnx("edits left in %s", Filename);
373 goto done;
374 default:
375 warnx("panic: bad switch() in replace_cmd()");
376 goto fatal;
377 }
378 remove:
379 unlink(Filename);
380 done:
381 syslog(LOG_INFO, "(%s) END EDIT (%s)", RealUser, User);
382 }
383
384 /* Create a temporary file in the spool dir owned by "pw". */
385 static FILE *
spool_mkstemp(char * template)386 spool_mkstemp(char *template)
387 {
388 uid_t euid = geteuid();
389 int fd = -1;
390 FILE *fp;
391
392 if (euid != pw->pw_uid) {
393 if (seteuid(pw->pw_uid) == -1) {
394 warn("unable to change uid to %u", pw->pw_uid);
395 goto bad;
396 }
397 }
398 fd = mkstemp(template);
399 if (euid != pw->pw_uid) {
400 if (seteuid(euid) == -1) {
401 warn("unable to change uid to %u", euid);
402 goto bad;
403 }
404 }
405 if (fd == -1 || !(fp = fdopen(fd, "w+"))) {
406 warn("%s", template);
407 goto bad;
408 }
409 return (fp);
410
411 bad:
412 if (fd != -1) {
413 close(fd);
414 unlink(template);
415 }
416 return (NULL);
417 }
418
419 /* returns 0 on success
420 * -1 on syntax error
421 * -2 on install error
422 */
423 static int
replace_cmd(void)424 replace_cmd(void)
425 {
426 char n[PATH_MAX], envstr[MAX_ENVSTR];
427 FILE *tmp;
428 int ch, eof;
429 int error = 0;
430 entry *e;
431 time_t now = time(NULL);
432 char **envp = env_init();
433
434 if (envp == NULL) {
435 warn(NULL); /* ENOMEM */
436 return (-2);
437 }
438 if (snprintf(TempFilename, sizeof TempFilename, "%s/tmp.XXXXXXXXX",
439 _PATH_CRON_SPOOL) >= sizeof(TempFilename)) {
440 TempFilename[0] = '\0';
441 warnc(ENAMETOOLONG, "%s/tmp.XXXXXXXXX", _PATH_CRON_SPOOL);
442 return (-2);
443 }
444 tmp = spool_mkstemp(TempFilename);
445 if (tmp == NULL) {
446 TempFilename[0] = '\0';
447 return (-2);
448 }
449
450 (void) signal(SIGHUP, die);
451 (void) signal(SIGINT, die);
452 (void) signal(SIGQUIT, die);
453
454 /* write a signature at the top of the file.
455 *
456 * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
457 */
458 fprintf(tmp, "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
459 fprintf(tmp, "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
460 fprintf(tmp, "# (Cron version %s)\n", CRON_VERSION);
461
462 /* copy the crontab to the tmp
463 */
464 rewind(NewCrontab);
465 Set_LineNum(1)
466 while (EOF != (ch = get_char(NewCrontab)))
467 putc(ch, tmp);
468 ftruncate(fileno(tmp), ftello(tmp)); /* XXX redundant with "w+"? */
469 fflush(tmp); rewind(tmp);
470
471 if (ferror(tmp)) {
472 warnx("error while writing new crontab to %s", TempFilename);
473 fclose(tmp);
474 error = -2;
475 goto done;
476 }
477
478 /* check the syntax of the file being installed.
479 */
480
481 /* BUG: was reporting errors after the EOF if there were any errors
482 * in the file proper -- kludged it by stopping after first error.
483 * vix 31mar87
484 */
485 Set_LineNum(1 - NHEADER_LINES)
486 CheckErrorCount = 0; eof = FALSE;
487 while (!CheckErrorCount && !eof) {
488 switch (load_env(envstr, tmp)) {
489 case -1:
490 /* check for data before the EOF */
491 if (envstr[0] != '\0') {
492 Set_LineNum(LineNumber + 1);
493 check_error("premature EOF");
494 }
495 eof = TRUE;
496 break;
497 case FALSE:
498 e = load_entry(tmp, check_error, pw, envp);
499 if (e)
500 free_entry(e);
501 break;
502 case TRUE:
503 break;
504 }
505 }
506
507 if (CheckErrorCount != 0) {
508 warnx("errors in crontab file, unable to install");
509 fclose(tmp);
510 error = -1;
511 goto done;
512 }
513
514 if (fclose(tmp) == EOF) {
515 warn("fclose");
516 error = -2;
517 goto done;
518 }
519
520 if (snprintf(n, sizeof n, "%s/%s", _PATH_CRON_SPOOL, User) >= sizeof(n)) {
521 warnc(ENAMETOOLONG, "%s/%s", _PATH_CRON_SPOOL, User);
522 error = -2;
523 goto done;
524 }
525 if (rename(TempFilename, n)) {
526 warn("unable to rename %s to %s", TempFilename, n);
527 error = -2;
528 goto done;
529 }
530 TempFilename[0] = '\0';
531 syslog(LOG_INFO, "(%s) REPLACE (%s)", RealUser, User);
532
533 poke_daemon(RELOAD_CRON);
534
535 done:
536 (void) signal(SIGHUP, SIG_DFL);
537 (void) signal(SIGINT, SIG_DFL);
538 (void) signal(SIGQUIT, SIG_DFL);
539 if (TempFilename[0]) {
540 (void) unlink(TempFilename);
541 TempFilename[0] = '\0';
542 }
543 return (error);
544 }
545
546 /*
547 * Execute an editor on the specified pathname, which is interpreted
548 * from the shell. This means flags may be included.
549 *
550 * Returns -1 on error, or the exit value on success.
551 */
552 int
editit(const char * pathname)553 editit(const char *pathname)
554 {
555 char *argp[] = {"sh", "-c", NULL, NULL}, *ed, *p;
556 sig_t sighup, sigint, sigquit, sigchld;
557 pid_t pid;
558 int saved_errno, st, ret = -1;
559
560 ed = getenv("VISUAL");
561 if (ed == NULL || ed[0] == '\0')
562 ed = getenv("EDITOR");
563 if (ed == NULL || ed[0] == '\0')
564 ed = _PATH_VI;
565 if (asprintf(&p, "%s %s", ed, pathname) == -1)
566 return (-1);
567 argp[2] = p;
568
569 sighup = signal(SIGHUP, SIG_IGN);
570 sigint = signal(SIGINT, SIG_IGN);
571 sigquit = signal(SIGQUIT, SIG_IGN);
572 sigchld = signal(SIGCHLD, SIG_DFL);
573 if ((pid = fork()) == -1)
574 goto fail;
575 if (pid == 0) {
576 /* Drop setgid and exec the command. */
577 if (setgid(user_gid) == -1) {
578 warn("unable to set gid to %u", user_gid);
579 } else {
580 execv(_PATH_BSHELL, argp);
581 warn("unable to execute %s", _PATH_BSHELL);
582 }
583 _exit(127);
584 }
585 while (waitpid(pid, &st, 0) == -1)
586 if (errno != EINTR)
587 goto fail;
588 if (!WIFEXITED(st))
589 errno = EINTR;
590 else
591 ret = WEXITSTATUS(st);
592
593 fail:
594 saved_errno = errno;
595 (void)signal(SIGHUP, sighup);
596 (void)signal(SIGINT, sigint);
597 (void)signal(SIGQUIT, sigquit);
598 (void)signal(SIGCHLD, sigchld);
599 free(p);
600 errno = saved_errno;
601 return (ret);
602 }
603
604 static void
die(int x)605 die(int x)
606 {
607 if (TempFilename[0])
608 (void) unlink(TempFilename);
609 _exit(EXIT_FAILURE);
610 }
611
612 static void
copy_crontab(FILE * f,FILE * out)613 copy_crontab(FILE *f, FILE *out)
614 {
615 int ch, x;
616
617 /* ignore the top few comments since we probably put them there.
618 */
619 x = 0;
620 while (EOF != (ch = get_char(f))) {
621 if ('#' != ch) {
622 putc(ch, out);
623 break;
624 }
625 while (EOF != (ch = get_char(f)))
626 if (ch == '\n')
627 break;
628 if (++x >= NHEADER_LINES)
629 break;
630 }
631
632 /* copy out the rest of the crontab (if any)
633 */
634 if (EOF != ch)
635 while (EOF != (ch = get_char(f)))
636 putc(ch, out);
637 }
638