1*ea01913fShtodd /* $NetBSD: crontab.c,v 1.15 2018/03/06 21:21:27 htodd Exp $ */
2032a4398Schristos
30061c6a5Schristos /* Copyright 1988,1990,1993,1994 by Paul Vixie
40061c6a5Schristos * All rights reserved
50061c6a5Schristos */
60061c6a5Schristos
70061c6a5Schristos /*
80061c6a5Schristos * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC")
90061c6a5Schristos * Copyright (c) 1997,2000 by Internet Software Consortium, Inc.
100061c6a5Schristos *
110061c6a5Schristos * Permission to use, copy, modify, and distribute this software for any
120061c6a5Schristos * purpose with or without fee is hereby granted, provided that the above
130061c6a5Schristos * copyright notice and this permission notice appear in all copies.
140061c6a5Schristos *
150061c6a5Schristos * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES
160061c6a5Schristos * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
170061c6a5Schristos * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR
180061c6a5Schristos * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
190061c6a5Schristos * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
200061c6a5Schristos * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT
210061c6a5Schristos * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
220061c6a5Schristos */
23032a4398Schristos #include <sys/cdefs.h>
240061c6a5Schristos #if !defined(lint) && !defined(LINT)
25032a4398Schristos #if 0
260061c6a5Schristos static char rcsid[] = "Id: crontab.c,v 1.12 2004/01/23 18:56:42 vixie Exp";
27032a4398Schristos #else
28*ea01913fShtodd __RCSID("$NetBSD: crontab.c,v 1.15 2018/03/06 21:21:27 htodd Exp $");
29032a4398Schristos #endif
300061c6a5Schristos #endif
310061c6a5Schristos
320061c6a5Schristos /* crontab - install and manage per-user crontab files
330061c6a5Schristos * vix 02may87 [RCS has the rest of the log]
340061c6a5Schristos * vix 26jan87 [original]
350061c6a5Schristos */
360061c6a5Schristos
370061c6a5Schristos #define MAIN_PROGRAM
380061c6a5Schristos
390061c6a5Schristos #include "cron.h"
400061c6a5Schristos
410061c6a5Schristos #define NHEADER_LINES 3
420061c6a5Schristos
430061c6a5Schristos enum opt_t { opt_unknown, opt_list, opt_delete, opt_edit, opt_replace };
440061c6a5Schristos
450061c6a5Schristos #if DEBUGGING
46032a4398Schristos static const char *Options[] = {
47032a4398Schristos "???", "list", "delete", "edit", "replace" };
48032a4398Schristos static const char *getoptargs = "u:lerx:";
490061c6a5Schristos #else
50032a4398Schristos static const char *getoptargs = "u:ler";
510061c6a5Schristos #endif
520061c6a5Schristos
530061c6a5Schristos static PID_T Pid;
540061c6a5Schristos static char User[MAX_UNAME], RealUser[MAX_UNAME];
550061c6a5Schristos static char Filename[MAX_FNAME], TempFilename[MAX_FNAME];
560061c6a5Schristos static FILE *NewCrontab;
570061c6a5Schristos static int CheckErrorCount;
580061c6a5Schristos static enum opt_t Option;
590061c6a5Schristos static struct passwd *pw;
600061c6a5Schristos static void list_cmd(void),
610061c6a5Schristos delete_cmd(void),
620061c6a5Schristos edit_cmd(void),
630061c6a5Schristos poke_daemon(void),
640061c6a5Schristos check_error(const char *),
65032a4398Schristos parse_args(int c, char *v[]);
660061c6a5Schristos static int replace_cmd(void);
67032a4398Schristos static int allowed(const char *, const char *, const char *);
68032a4398Schristos static int in_file(const char *, FILE *, int);
69*ea01913fShtodd static int relinquish_priv(void);
702ed8e2f0Schristos static int regain_priv(void);
710061c6a5Schristos
725f34e14bSjoerg static __dead void
usage(const char * msg)730061c6a5Schristos usage(const char *msg) {
74032a4398Schristos (void)fprintf(stderr, "%s: usage error: %s\n", getprogname(), msg);
75032a4398Schristos (void)fprintf(stderr, "usage:\t%s [-u user] file\n", getprogname());
76032a4398Schristos (void)fprintf(stderr, "\t%s [-u user] [ -e | -l | -r ]\n", getprogname());
77032a4398Schristos (void)fprintf(stderr, "\t\t(default operation is replace, per 1003.2)\n");
78032a4398Schristos (void)fprintf(stderr, "\t-e\t(edit user's crontab)\n");
79032a4398Schristos (void)fprintf(stderr, "\t-l\t(list user's crontab)\n");
80032a4398Schristos (void)fprintf(stderr, "\t-r\t(delete user's crontab)\n");
810061c6a5Schristos exit(ERROR_EXIT);
820061c6a5Schristos }
830061c6a5Schristos
842ed8e2f0Schristos static uid_t euid, ruid;
852ed8e2f0Schristos static gid_t egid, rgid;
862ed8e2f0Schristos
870061c6a5Schristos int
main(int argc,char * argv[])880061c6a5Schristos main(int argc, char *argv[]) {
890061c6a5Schristos int exitstatus;
900061c6a5Schristos
91032a4398Schristos setprogname(argv[0]);
920061c6a5Schristos Pid = getpid();
93032a4398Schristos (void)setlocale(LC_ALL, "");
940061c6a5Schristos
952ed8e2f0Schristos euid = geteuid();
962ed8e2f0Schristos egid = getegid();
972ed8e2f0Schristos ruid = getuid();
982ed8e2f0Schristos rgid = getgid();
992ed8e2f0Schristos
100d2071ea3Stron if (euid == ruid && ruid != 0)
101d2071ea3Stron errx(ERROR_EXIT, "Not installed setuid root");
1022ed8e2f0Schristos
103032a4398Schristos (void)setvbuf(stderr, NULL, _IOLBF, 0);
1040061c6a5Schristos parse_args(argc, argv); /* sets many globals, opens a file */
1050061c6a5Schristos set_cron_cwd();
1060061c6a5Schristos if (!allowed(RealUser, CRON_ALLOW, CRON_DENY)) {
107032a4398Schristos (void)fprintf(stderr,
108032a4398Schristos "You `%s' are not allowed to use this program `%s'\n",
109032a4398Schristos User, getprogname());
110032a4398Schristos (void)fprintf(stderr, "See crontab(1) for more information\n");
1110061c6a5Schristos log_it(RealUser, Pid, "AUTH", "crontab command not allowed");
1120061c6a5Schristos exit(ERROR_EXIT);
1130061c6a5Schristos }
1140061c6a5Schristos exitstatus = OK_EXIT;
1150061c6a5Schristos switch (Option) {
1160061c6a5Schristos case opt_unknown:
117032a4398Schristos usage("unrecognized option");
1180061c6a5Schristos exitstatus = ERROR_EXIT;
1190061c6a5Schristos break;
1200061c6a5Schristos case opt_list:
1210061c6a5Schristos list_cmd();
1220061c6a5Schristos break;
1230061c6a5Schristos case opt_delete:
1240061c6a5Schristos delete_cmd();
1250061c6a5Schristos break;
1260061c6a5Schristos case opt_edit:
1270061c6a5Schristos edit_cmd();
1280061c6a5Schristos break;
1290061c6a5Schristos case opt_replace:
1300061c6a5Schristos if (replace_cmd() < 0)
1310061c6a5Schristos exitstatus = ERROR_EXIT;
1320061c6a5Schristos break;
1330061c6a5Schristos default:
1340061c6a5Schristos abort();
1350061c6a5Schristos }
1360061c6a5Schristos exit(exitstatus);
1370061c6a5Schristos /*NOTREACHED*/
1380061c6a5Schristos }
1390061c6a5Schristos
1400061c6a5Schristos static void
get_time(const struct stat * st,struct timespec * ts)1410a00da6dSchristos get_time(const struct stat *st, struct timespec *ts)
1420a00da6dSchristos {
1430a00da6dSchristos ts[0].tv_sec = st->st_atime;
1440a00da6dSchristos ts[0].tv_nsec = st->st_atimensec;
1450a00da6dSchristos ts[1].tv_sec = st->st_mtime;
1460a00da6dSchristos ts[1].tv_nsec = st->st_mtimensec;
1470a00da6dSchristos }
1480a00da6dSchristos
1490a00da6dSchristos static int
change_time(const char * name,const struct timespec * ts)1500a00da6dSchristos change_time(const char *name, const struct timespec *ts)
1510a00da6dSchristos {
1520a00da6dSchristos #if defined(HAVE_UTIMENSAT)
1530a00da6dSchristos return utimensat(AT_FDCWD, name, ts, 0);
1540a00da6dSchristos #elif defined(HAVE_UTIMES)
1550a00da6dSchristos struct timeval tv[2];
1560a00da6dSchristos TIMESPEC_TO_TIMEVAL(&tv[0], &ts[0]);
1570a00da6dSchristos TIMESPEC_TO_TIMEVAL(&tv[1], &ts[1]);
15898b021fdSchristos return utimes(name, tv);
1590a00da6dSchristos #else
16098b021fdSchristos struct utimbuf ut;
16198b021fdSchristos ut.actime = ts[0].tv_sec;
16298b021fdSchristos ut.modtime = ts[1].tv_sec;
1630a00da6dSchristos return utime(name, &ut);
1640a00da6dSchristos #endif
1650a00da6dSchristos }
1660a00da6dSchristos
1670a00da6dSchristos static int
compare_time(const struct stat * st,const struct timespec * ts2)1680a00da6dSchristos compare_time(const struct stat *st, const struct timespec *ts2)
1690a00da6dSchristos {
1700a00da6dSchristos struct timespec ts1[2];
1710a00da6dSchristos get_time(st, ts1);
1720a00da6dSchristos
1730a00da6dSchristos return ts1[1].tv_sec == ts2[1].tv_sec
1740a00da6dSchristos #if defined(HAVE_UTIMENSAT)
1750a00da6dSchristos && ts1[1].tv_nsec == ts2[1].tv_nsec
1760a00da6dSchristos #elif defined(HAVE_UTIMES)
17798b021fdSchristos && ts1[1].tv_nsec / 1000 == ts2[1].tv_nsec / 1000
1780a00da6dSchristos #endif
1790a00da6dSchristos ;
1800a00da6dSchristos }
1810a00da6dSchristos
1820a00da6dSchristos static void
parse_args(int argc,char * argv[])1830061c6a5Schristos parse_args(int argc, char *argv[]) {
1840061c6a5Schristos int argch;
1850061c6a5Schristos
1860061c6a5Schristos if (!(pw = getpwuid(getuid()))) {
187032a4398Schristos errx(ERROR_EXIT,
188032a4398Schristos "your UID isn't in the passwd file. bailingo out");
1890061c6a5Schristos }
1900061c6a5Schristos if (strlen(pw->pw_name) >= sizeof User) {
191032a4398Schristos errx(ERROR_EXIT, "username too long");
1920061c6a5Schristos }
193032a4398Schristos (void)strlcpy(User, pw->pw_name, sizeof(User));
194032a4398Schristos (void)strlcpy(RealUser, User, sizeof(RealUser));
1950061c6a5Schristos Filename[0] = '\0';
1960061c6a5Schristos Option = opt_unknown;
1970061c6a5Schristos while (-1 != (argch = getopt(argc, argv, getoptargs))) {
1980061c6a5Schristos switch (argch) {
1990061c6a5Schristos #if DEBUGGING
2000061c6a5Schristos case 'x':
2010061c6a5Schristos if (!set_debug_flags(optarg))
2020061c6a5Schristos usage("bad debug option");
2030061c6a5Schristos break;
2040061c6a5Schristos #endif
2050061c6a5Schristos case 'u':
2060061c6a5Schristos if (MY_UID(pw) != ROOT_UID) {
207032a4398Schristos errx(ERROR_EXIT,
208032a4398Schristos "must be privileged to use -u");
2090061c6a5Schristos }
2100061c6a5Schristos if (!(pw = getpwnam(optarg))) {
211032a4398Schristos errx(ERROR_EXIT, "user `%s' unknown", optarg);
2120061c6a5Schristos }
2130061c6a5Schristos if (strlen(optarg) >= sizeof User)
2140061c6a5Schristos usage("username too long");
215032a4398Schristos (void) strlcpy(User, optarg, sizeof(User));
2160061c6a5Schristos break;
2170061c6a5Schristos case 'l':
2180061c6a5Schristos if (Option != opt_unknown)
2190061c6a5Schristos usage("only one operation permitted");
2200061c6a5Schristos Option = opt_list;
2210061c6a5Schristos break;
2220061c6a5Schristos case 'r':
2230061c6a5Schristos if (Option != opt_unknown)
2240061c6a5Schristos usage("only one operation permitted");
2250061c6a5Schristos Option = opt_delete;
2260061c6a5Schristos break;
2270061c6a5Schristos case 'e':
2280061c6a5Schristos if (Option != opt_unknown)
2290061c6a5Schristos usage("only one operation permitted");
2300061c6a5Schristos Option = opt_edit;
2310061c6a5Schristos break;
2320061c6a5Schristos default:
2330061c6a5Schristos usage("unrecognized option");
2340061c6a5Schristos }
2350061c6a5Schristos }
2360061c6a5Schristos
2370061c6a5Schristos endpwent();
2380061c6a5Schristos
2390061c6a5Schristos if (Option != opt_unknown) {
2400061c6a5Schristos if (argv[optind] != NULL)
2410061c6a5Schristos usage("no arguments permitted after this option");
2420061c6a5Schristos } else {
2430061c6a5Schristos if (argv[optind] != NULL) {
2440061c6a5Schristos Option = opt_replace;
2450061c6a5Schristos if (strlen(argv[optind]) >= sizeof Filename)
2460061c6a5Schristos usage("filename too long");
247032a4398Schristos (void)strlcpy(Filename, argv[optind], sizeof(Filename));
2480061c6a5Schristos } else
2490061c6a5Schristos usage("file name must be specified for replace");
2500061c6a5Schristos }
2510061c6a5Schristos
2520061c6a5Schristos if (Option == opt_replace) {
2530061c6a5Schristos /* we have to open the file here because we're going to
2540061c6a5Schristos * chdir(2) into /var/cron before we get around to
2550061c6a5Schristos * reading the file.
2560061c6a5Schristos */
2570061c6a5Schristos if (!strcmp(Filename, "-"))
2580061c6a5Schristos NewCrontab = stdin;
2590061c6a5Schristos else {
2600061c6a5Schristos /* relinquish the setuid status of the binary during
2610061c6a5Schristos * the open, lest nonroot users read files they should
2620061c6a5Schristos * not be able to read. we can't use access() here
2630061c6a5Schristos * since there's a race condition. thanks go out to
2640061c6a5Schristos * Arnt Gulbrandsen <agulbra@pvv.unit.no> for spotting
2650061c6a5Schristos * the race.
2660061c6a5Schristos */
2670061c6a5Schristos
268*ea01913fShtodd if (relinquish_priv() < OK) {
269032a4398Schristos err(ERROR_EXIT, "swapping uids");
2700061c6a5Schristos }
2710061c6a5Schristos if (!(NewCrontab = fopen(Filename, "r"))) {
272032a4398Schristos err(ERROR_EXIT, "cannot open `%s'", Filename);
2730061c6a5Schristos }
2742ed8e2f0Schristos if (regain_priv() < OK) {
275032a4398Schristos err(ERROR_EXIT, "swapping uids back");
2760061c6a5Schristos }
2770061c6a5Schristos }
2780061c6a5Schristos }
2790061c6a5Schristos
2800061c6a5Schristos Debug(DMISC, ("user=%s, file=%s, option=%s\n",
281032a4398Schristos User, Filename, Options[(int)Option]));
282032a4398Schristos }
283032a4398Schristos
284032a4398Schristos static void
skip_header(int * pch,FILE * f)285032a4398Schristos skip_header(int *pch, FILE *f)
286032a4398Schristos {
287032a4398Schristos int ch;
288032a4398Schristos int x;
289032a4398Schristos
290032a4398Schristos /* ignore the top few comments since we probably put them there.
291032a4398Schristos */
292032a4398Schristos for (x = 0; x < NHEADER_LINES; x++) {
293032a4398Schristos ch = get_char(f);
294032a4398Schristos if (EOF == ch)
295032a4398Schristos break;
296032a4398Schristos if ('#' != ch)
297032a4398Schristos break;
298032a4398Schristos while (EOF != (ch = get_char(f)))
299032a4398Schristos if (ch == '\n')
300032a4398Schristos break;
301032a4398Schristos if (EOF == ch)
302032a4398Schristos break;
303032a4398Schristos }
304032a4398Schristos if (ch == '\n')
305032a4398Schristos ch = get_char(f);
306032a4398Schristos
307032a4398Schristos *pch = ch;
3080061c6a5Schristos }
3090061c6a5Schristos
3100061c6a5Schristos static void
list_cmd(void)3110061c6a5Schristos list_cmd(void) {
3120061c6a5Schristos char n[MAX_FNAME];
3130061c6a5Schristos FILE *f;
3140061c6a5Schristos int ch;
3150061c6a5Schristos
3160061c6a5Schristos log_it(RealUser, Pid, "LIST", User);
3170061c6a5Schristos if (!glue_strings(n, sizeof n, SPOOL_DIR, User, '/')) {
318032a4398Schristos errx(ERROR_EXIT, "path too long");
3190061c6a5Schristos }
3200061c6a5Schristos if (!(f = fopen(n, "r"))) {
3210061c6a5Schristos if (errno == ENOENT)
322032a4398Schristos errx(ERROR_EXIT, "no crontab for `%s'", User);
3230061c6a5Schristos else
324032a4398Schristos err(ERROR_EXIT, "Cannot open `%s'", n);
3250061c6a5Schristos }
3260061c6a5Schristos
3270061c6a5Schristos /* file is open. copy to stdout, close.
3280061c6a5Schristos */
329032a4398Schristos Set_LineNum(1);
330032a4398Schristos skip_header(&ch, f);
331032a4398Schristos for (; EOF != ch; ch = get_char(f))
332032a4398Schristos (void)putchar(ch);
333032a4398Schristos (void)fclose(f);
3340061c6a5Schristos }
3350061c6a5Schristos
3360061c6a5Schristos static void
delete_cmd(void)3370061c6a5Schristos delete_cmd(void) {
3380061c6a5Schristos char n[MAX_FNAME];
3390061c6a5Schristos
3400061c6a5Schristos log_it(RealUser, Pid, "DELETE", User);
3410061c6a5Schristos if (!glue_strings(n, sizeof n, SPOOL_DIR, User, '/')) {
342032a4398Schristos errx(ERROR_EXIT, "path too long");
3430061c6a5Schristos }
3440061c6a5Schristos if (unlink(n) != 0) {
3450061c6a5Schristos if (errno == ENOENT)
346032a4398Schristos errx(ERROR_EXIT, "no crontab for `%s'", User);
3470061c6a5Schristos else
348032a4398Schristos err(ERROR_EXIT, "cannot unlink `%s'", n);
3490061c6a5Schristos }
3500061c6a5Schristos poke_daemon();
3510061c6a5Schristos }
3520061c6a5Schristos
3530061c6a5Schristos static void
check_error(const char * msg)3540061c6a5Schristos check_error(const char *msg) {
3550061c6a5Schristos CheckErrorCount++;
356032a4398Schristos (void)fprintf(stderr, "\"%s\":%d: %s\n", Filename, LineNumber-1, msg);
3570061c6a5Schristos }
3580061c6a5Schristos
3590061c6a5Schristos static void
edit_cmd(void)3600061c6a5Schristos edit_cmd(void) {
361032a4398Schristos char n[MAX_FNAME], q[MAX_TEMPSTR];
362032a4398Schristos const char *editor;
3630061c6a5Schristos FILE *f;
3640061c6a5Schristos int ch, t, x;
365032a4398Schristos sig_t oint, oabrt, oquit, ohup;
3660061c6a5Schristos struct stat statbuf;
3670061c6a5Schristos WAIT_T waiter;
3680061c6a5Schristos PID_T pid, xpid;
3690a00da6dSchristos struct timespec ts[2];
3700061c6a5Schristos
3710061c6a5Schristos log_it(RealUser, Pid, "BEGIN EDIT", User);
3720061c6a5Schristos if (!glue_strings(n, sizeof n, SPOOL_DIR, User, '/')) {
373032a4398Schristos errx(ERROR_EXIT, "path too long");
3740061c6a5Schristos }
3750061c6a5Schristos if (!(f = fopen(n, "r"))) {
3760061c6a5Schristos if (errno != ENOENT) {
377032a4398Schristos err(ERROR_EXIT, "cannot open `%s'", n);
3780061c6a5Schristos }
379032a4398Schristos warnx("no crontab for `%s' - using an empty one", User);
3800061c6a5Schristos if (!(f = fopen(_PATH_DEVNULL, "r"))) {
381032a4398Schristos err(ERROR_EXIT, "cannot open `%s'", _PATH_DEVNULL);
3820061c6a5Schristos }
3830061c6a5Schristos }
3840061c6a5Schristos
3850061c6a5Schristos if (fstat(fileno(f), &statbuf) < 0) {
386032a4398Schristos warn("cannot stat crontab file");
3870061c6a5Schristos goto fatal;
3880061c6a5Schristos }
3890a00da6dSchristos get_time(&statbuf, ts);
3900061c6a5Schristos
3910061c6a5Schristos /* Turn off signals. */
392032a4398Schristos ohup = signal(SIGHUP, SIG_IGN);
393032a4398Schristos oint = signal(SIGINT, SIG_IGN);
394032a4398Schristos oquit = signal(SIGQUIT, SIG_IGN);
395032a4398Schristos oabrt = signal(SIGABRT, SIG_IGN);
3960061c6a5Schristos
3970061c6a5Schristos if (!glue_strings(Filename, sizeof Filename, _PATH_TMP,
3980061c6a5Schristos "crontab.XXXXXXXXXX", '/')) {
399032a4398Schristos warnx("path too long");
4000061c6a5Schristos goto fatal;
4010061c6a5Schristos }
4020061c6a5Schristos if (-1 == (t = mkstemp(Filename))) {
403032a4398Schristos warn("cannot create `%s'", Filename);
4040061c6a5Schristos goto fatal;
4050061c6a5Schristos }
4060061c6a5Schristos #ifdef HAS_FCHOWN
407032a4398Schristos x = fchown(t, MY_UID(pw), MY_GID(pw));
4080061c6a5Schristos #else
409032a4398Schristos x = chown(Filename, MY_UID(pw), MY_GID(pw));
4100061c6a5Schristos #endif
411032a4398Schristos if (x < 0) {
412032a4398Schristos warn("cannot chown `%s'", Filename);
413032a4398Schristos goto fatal;
414032a4398Schristos }
4150061c6a5Schristos if (!(NewCrontab = fdopen(t, "r+"))) {
416032a4398Schristos warn("cannot open fd");
4170061c6a5Schristos goto fatal;
4180061c6a5Schristos }
4190061c6a5Schristos
420032a4398Schristos Set_LineNum(1);
4210061c6a5Schristos
422032a4398Schristos skip_header(&ch, f);
4230061c6a5Schristos
4240061c6a5Schristos /* copy the rest of the crontab (if any) to the temp file.
4250061c6a5Schristos */
426032a4398Schristos for (; EOF != ch; ch = get_char(f))
427032a4398Schristos (void)putc(ch, NewCrontab);
428032a4398Schristos (void)fclose(f);
4290061c6a5Schristos if (fflush(NewCrontab) < OK) {
430032a4398Schristos err(ERROR_EXIT, "cannot flush output for `%s'", Filename);
4310061c6a5Schristos }
432065057e6Schristos #ifdef HAVE_FUTIMENS
433065057e6Schristos if (futimens(t, ts) == -1)
434065057e6Schristos #else
4350a00da6dSchristos if (change_time(Filename, ts) == -1)
436065057e6Schristos #endif
4370a00da6dSchristos err(ERROR_EXIT, "cannot set time info for `%s'", Filename);
4380061c6a5Schristos again:
4390061c6a5Schristos rewind(NewCrontab);
4400061c6a5Schristos if (ferror(NewCrontab)) {
441032a4398Schristos warn("error while writing new crontab to `%s'", Filename);
4420061c6a5Schristos fatal:
443032a4398Schristos (void)unlink(Filename);
4440061c6a5Schristos exit(ERROR_EXIT);
4450061c6a5Schristos }
4460061c6a5Schristos
4470061c6a5Schristos if (((editor = getenv("VISUAL")) == NULL || *editor == '\0') &&
4480061c6a5Schristos ((editor = getenv("EDITOR")) == NULL || *editor == '\0')) {
4490061c6a5Schristos editor = EDITOR;
4500061c6a5Schristos }
4510061c6a5Schristos
4520061c6a5Schristos /* we still have the file open. editors will generally rewrite the
4530061c6a5Schristos * original file rather than renaming/unlinking it and starting a
4540061c6a5Schristos * new one; even backup files are supposed to be made by copying
4550061c6a5Schristos * rather than by renaming. if some editor does not support this,
4560061c6a5Schristos * then don't use it. the security problems are more severe if we
4570061c6a5Schristos * close and reopen the file around the edit.
4580061c6a5Schristos */
4590061c6a5Schristos
4600061c6a5Schristos switch (pid = fork()) {
4610061c6a5Schristos case -1:
462032a4398Schristos warn("cannot fork");
4630061c6a5Schristos goto fatal;
4640061c6a5Schristos case 0:
4650061c6a5Schristos /* child */
4660061c6a5Schristos if (setgid(MY_GID(pw)) < 0) {
467032a4398Schristos err(ERROR_EXIT, "cannot setgid(getgid())");
4680061c6a5Schristos }
4690061c6a5Schristos if (setuid(MY_UID(pw)) < 0) {
470032a4398Schristos err(ERROR_EXIT, "cannot setuid(getuid())");
4710061c6a5Schristos }
472065057e6Schristos if (close_all(3)) {
473065057e6Schristos err(ERROR_EXIT, "cannot close files");
474065057e6Schristos }
4750061c6a5Schristos if (chdir(_PATH_TMP) < 0) {
476032a4398Schristos err(ERROR_EXIT, "cannot chdir to `%s'", _PATH_TMP);
4770061c6a5Schristos }
4780061c6a5Schristos if (!glue_strings(q, sizeof q, editor, Filename, ' ')) {
479032a4398Schristos errx(ERROR_EXIT, "editor command line too long");
4800061c6a5Schristos }
481032a4398Schristos (void)execlp(_PATH_BSHELL, _PATH_BSHELL, "-c", q, (char *)0);
482032a4398Schristos err(ERROR_EXIT, "cannot start `%s'", editor);
4830061c6a5Schristos /*NOTREACHED*/
4840061c6a5Schristos default:
4850061c6a5Schristos /* parent */
4860061c6a5Schristos break;
4870061c6a5Schristos }
4880061c6a5Schristos
4890061c6a5Schristos /* parent */
4900061c6a5Schristos for (;;) {
4910061c6a5Schristos xpid = waitpid(pid, &waiter, WUNTRACED);
4920061c6a5Schristos if (xpid == -1) {
4930061c6a5Schristos if (errno != EINTR)
494032a4398Schristos warn("waitpid() failed waiting for PID %ld "
495032a4398Schristos "from `%s'", (long)pid, editor);
4960061c6a5Schristos } else if (xpid != pid) {
497032a4398Schristos warnx("wrong PID (%ld != %ld) from `%s'",
498032a4398Schristos (long)xpid, (long)pid, editor);
4990061c6a5Schristos goto fatal;
5000061c6a5Schristos } else if (WIFSTOPPED(waiter)) {
501032a4398Schristos (void)kill(getpid(), WSTOPSIG(waiter));
5020061c6a5Schristos } else if (WIFEXITED(waiter) && WEXITSTATUS(waiter)) {
503032a4398Schristos warnx("`%s' exited with status %d\n",
504032a4398Schristos editor, WEXITSTATUS(waiter));
5050061c6a5Schristos goto fatal;
5060061c6a5Schristos } else if (WIFSIGNALED(waiter)) {
507032a4398Schristos warnx("`%s' killed; signal %d (%score dumped)",
508032a4398Schristos editor, WTERMSIG(waiter),
5090061c6a5Schristos WCOREDUMP(waiter) ? "" : "no ");
5100061c6a5Schristos goto fatal;
5110061c6a5Schristos } else
5120061c6a5Schristos break;
5130061c6a5Schristos }
514032a4398Schristos (void)signal(SIGHUP, ohup);
515032a4398Schristos (void)signal(SIGINT, oint);
516032a4398Schristos (void)signal(SIGQUIT, oquit);
517032a4398Schristos (void)signal(SIGABRT, oabrt);
518032a4398Schristos
5190061c6a5Schristos if (fstat(t, &statbuf) < 0) {
520032a4398Schristos warn("cannot stat `%s'", Filename);
5210061c6a5Schristos goto fatal;
5220061c6a5Schristos }
5230a00da6dSchristos if (compare_time(&statbuf, ts)) {
524032a4398Schristos warnx("no changes made to crontab");
5250061c6a5Schristos goto remove;
5260061c6a5Schristos }
527032a4398Schristos warnx("installing new crontab");
5280061c6a5Schristos switch (replace_cmd()) {
5290061c6a5Schristos case 0:
5300061c6a5Schristos break;
5310061c6a5Schristos case -1:
5320061c6a5Schristos for (;;) {
533032a4398Schristos (void)fpurge(stdin);
534032a4398Schristos (void)printf("Do you want to retry the same edit? ");
535032a4398Schristos (void)fflush(stdout);
5360061c6a5Schristos q[0] = '\0';
537032a4398Schristos (void) fgets(q, (int)sizeof(q), stdin);
5380061c6a5Schristos switch (q[0]) {
5390061c6a5Schristos case 'y':
5400061c6a5Schristos case 'Y':
5410061c6a5Schristos goto again;
5420061c6a5Schristos case 'n':
5430061c6a5Schristos case 'N':
5440061c6a5Schristos goto abandon;
5450061c6a5Schristos default:
546032a4398Schristos (void)printf("Enter Y or N\n");
5470061c6a5Schristos }
5480061c6a5Schristos }
5490061c6a5Schristos /*NOTREACHED*/
5500061c6a5Schristos case -2:
5510061c6a5Schristos abandon:
552032a4398Schristos warnx("edits left in `%s'", Filename);
5530061c6a5Schristos goto done;
5540061c6a5Schristos default:
555032a4398Schristos warnx("panic: bad switch() in replace_cmd()");
5560061c6a5Schristos goto fatal;
5570061c6a5Schristos }
5580061c6a5Schristos remove:
559032a4398Schristos (void)unlink(Filename);
5600061c6a5Schristos done:
5610061c6a5Schristos log_it(RealUser, Pid, "END EDIT", User);
5620061c6a5Schristos }
5630061c6a5Schristos
5642ef06139Schristos static size_t
getmaxtabsize(void)5652ef06139Schristos getmaxtabsize(void)
5662ef06139Schristos {
5672ef06139Schristos char n2[MAX_FNAME];
5682ef06139Schristos FILE *fmaxtabsize;
569032a4398Schristos size_t maxtabsize;
570032a4398Schristos
571032a4398Schristos /* Make sure that the crontab is not an unreasonable size.
572032a4398Schristos *
573032a4398Schristos * XXX This is subject to a race condition--the user could
574032a4398Schristos * add stuff to the file after we've checked the size but
575032a4398Schristos * before we slurp it in and write it out. We can't just move
576032a4398Schristos * the test to test the temp file we later create, because by
577032a4398Schristos * that time we've already filled up the crontab disk. Probably
578032a4398Schristos * the right thing to do is to do a bytecount in the copy loop
579032a4398Schristos * rather than stating the file we're about to read.
580032a4398Schristos */
581032a4398Schristos (void)snprintf(n2, sizeof(n2), "%s/%s", CRONDIR, MAXTABSIZE_FILE);
582032a4398Schristos if ((fmaxtabsize = fopen(n2, "r")) != NULL) {
583032a4398Schristos if (fgets(n2, (int)sizeof(n2), fmaxtabsize) == NULL) {
584032a4398Schristos maxtabsize = 0;
585032a4398Schristos } else {
586ae5dea0aSchristos maxtabsize = (size_t)atoi(n2);
587032a4398Schristos }
588032a4398Schristos (void)fclose(fmaxtabsize);
589032a4398Schristos } else {
590032a4398Schristos maxtabsize = MAXTABSIZE_DEFAULT;
591032a4398Schristos }
5922ef06139Schristos return maxtabsize;
5932ef06139Schristos }
594032a4398Schristos
5952ef06139Schristos static int
checkmaxtabsize(FILE * fp,size_t maxtabsize)5962ef06139Schristos checkmaxtabsize(FILE *fp, size_t maxtabsize)
5972ef06139Schristos {
5982ef06139Schristos struct stat statbuf;
5992ef06139Schristos
6002ef06139Schristos if (fstat(fileno(fp), &statbuf)) {
601032a4398Schristos warn("error stat'ing crontab input");
6022ef06139Schristos return 0;
6032ef06139Schristos }
6042ef06139Schristos if ((uintmax_t)statbuf.st_size > (uintmax_t)maxtabsize) {
6052ef06139Schristos warnx("%ju bytes is larger than the maximum size of %ju bytes",
6062ef06139Schristos (uintmax_t)statbuf.st_size, (uintmax_t)maxtabsize);
6072ef06139Schristos return 0;
6082ef06139Schristos }
6092ef06139Schristos return 1;
6102ef06139Schristos }
6112ef06139Schristos
6122ef06139Schristos static void
cleanTempFile(void)6132ef06139Schristos cleanTempFile(void)
6142ef06139Schristos {
6152ef06139Schristos
6162ef06139Schristos if (TempFilename[0]) {
6172ef06139Schristos (void) unlink(TempFilename);
6182ef06139Schristos TempFilename[0] = '\0';
6192ef06139Schristos }
6202ef06139Schristos }
6212ef06139Schristos
6220b40943bSjoerg static __dead void
bail(int signo)6232ef06139Schristos bail(int signo)
6242ef06139Schristos {
6252ef06139Schristos
6262ef06139Schristos cleanTempFile();
6272ef06139Schristos errx(ERROR_EXIT, "Exiting on signal %d", signo);
6282ef06139Schristos }
6292ef06139Schristos
6302ef06139Schristos /* returns 0 on success
6312ef06139Schristos * -1 on syntax error
6322ef06139Schristos * -2 on install error
6332ef06139Schristos */
6342ef06139Schristos static int
replace_cmd(void)6352ef06139Schristos replace_cmd(void) {
6362ef06139Schristos char n[MAX_FNAME], envstr[MAX_ENVSTR];
6372ef06139Schristos int lastch;
6382ef06139Schristos FILE *tmp = NULL;
6392ef06139Schristos int ch, eof, fd;
6402ef06139Schristos int error = 0;
6412ef06139Schristos entry *e;
6422ef06139Schristos sig_t oint, oabrt, oquit, ohup;
6432ef06139Schristos uid_t file_owner;
6442ef06139Schristos time_t now = time(NULL);
6452ef06139Schristos char **envp = env_init();
6462ef06139Schristos size_t i, maxtabsize;
6472ef06139Schristos
6482ef06139Schristos if (envp == NULL) {
6492ef06139Schristos warn("Cannot allocate memory.");
6502ef06139Schristos return (-2);
6512ef06139Schristos }
6522ef06139Schristos
6532ef06139Schristos if (!glue_strings(TempFilename, sizeof TempFilename, SPOOL_DIR,
6542ef06139Schristos "tmp.XXXXXXXXXX", '/')) {
6552ef06139Schristos TempFilename[0] = '\0';
6562ef06139Schristos warnx("path too long");
6572ef06139Schristos return (-2);
6582ef06139Schristos }
6592ef06139Schristos
6602ef06139Schristos /* Interruptible while doing I/O */
6612ef06139Schristos ohup = signal(SIGHUP, bail);
6622ef06139Schristos oint = signal(SIGINT, bail);
6632ef06139Schristos oquit = signal(SIGQUIT, bail);
6642ef06139Schristos oabrt = signal(SIGABRT, bail);
6652ef06139Schristos
6662ef06139Schristos if ((fd = mkstemp(TempFilename)) == -1 || !(tmp = fdopen(fd, "w+"))) {
6672ef06139Schristos warn("cannot create `%s'", TempFilename);
6682ef06139Schristos if (fd != -1)
6692ef06139Schristos (void)close(fd);
670032a4398Schristos error = -2;
671032a4398Schristos goto done;
672032a4398Schristos }
6732ef06139Schristos
6742ef06139Schristos maxtabsize = getmaxtabsize();
6752ef06139Schristos
6762ef06139Schristos /* This does not work for stdin, so we'll also check later */
6772ef06139Schristos if (!checkmaxtabsize(NewCrontab, maxtabsize)) {
678032a4398Schristos error = -2;
679032a4398Schristos goto done;
680032a4398Schristos }
6810061c6a5Schristos
6820061c6a5Schristos /* write a signature at the top of the file.
6830061c6a5Schristos *
6840061c6a5Schristos * VERY IMPORTANT: make sure NHEADER_LINES agrees with this code.
6850061c6a5Schristos */
6862ef06139Schristos (void)fprintf(tmp,
6872ef06139Schristos "# DO NOT EDIT THIS FILE - edit the master and reinstall.\n");
6882ef06139Schristos (void)fprintf(tmp,
6892ef06139Schristos "# (%s installed on %-24.24s)\n", Filename, ctime(&now));
6902ef06139Schristos (void)fprintf(tmp,
6912ef06139Schristos "# (Cron version %s -- %s)\n", CRON_VERSION,
692*ea01913fShtodd "$NetBSD: crontab.c,v 1.15 2018/03/06 21:21:27 htodd Exp $");
6930061c6a5Schristos
6940061c6a5Schristos /* copy the crontab to the tmp
6950061c6a5Schristos */
696032a4398Schristos (void)rewind(NewCrontab);
697032a4398Schristos Set_LineNum(1);
698032a4398Schristos lastch = EOF;
6992ef06139Schristos for (i = 0; i < maxtabsize && EOF != (ch = get_char(NewCrontab)); i++)
700ae5dea0aSchristos (void)putc(lastch = (char)ch, tmp);
701032a4398Schristos
7022ef06139Schristos /* Non-Interruptible while installing */
7032ef06139Schristos (void)signal(SIGHUP, SIG_IGN);
7042ef06139Schristos (void)signal(SIGINT, SIG_IGN);
7052ef06139Schristos (void)signal(SIGQUIT, SIG_IGN);
7062ef06139Schristos (void)signal(SIGABRT, SIG_IGN);
7072ef06139Schristos
7082ef06139Schristos if (i == maxtabsize) {
7092ef06139Schristos warnx("is larger than the maximum size of %ju bytes",
7102ef06139Schristos (uintmax_t)maxtabsize);
7112ef06139Schristos error = -2;
7122ef06139Schristos goto done;
7132ef06139Schristos }
7142ef06139Schristos
715734041fcSchristos if (lastch != EOF && lastch != '\n') {
716032a4398Schristos warnx("missing trailing newline in `%s'", Filename);
717032a4398Schristos error = -1;
718032a4398Schristos goto done;
719032a4398Schristos }
720032a4398Schristos
721032a4398Schristos if (ferror(NewCrontab)) {
722032a4398Schristos warn("error while reading `%s'", Filename);
723032a4398Schristos error = -2;
724032a4398Schristos goto done;
725032a4398Schristos }
726032a4398Schristos
727032a4398Schristos (void)ftruncate(fileno(tmp), ftell(tmp));
728032a4398Schristos /* XXX this should be a NOOP - is */
729032a4398Schristos (void)fflush(tmp);
7300061c6a5Schristos
7310061c6a5Schristos if (ferror(tmp)) {
732032a4398Schristos warn("error while writing new crontab to `%s'", TempFilename);
7330061c6a5Schristos error = -2;
7340061c6a5Schristos goto done;
7350061c6a5Schristos }
7360061c6a5Schristos
7370061c6a5Schristos /* check the syntax of the file being installed.
7380061c6a5Schristos */
7390061c6a5Schristos
7400061c6a5Schristos /* BUG: was reporting errors after the EOF if there were any errors
7410061c6a5Schristos * in the file proper -- kludged it by stopping after first error.
7420061c6a5Schristos * vix 31mar87
7430061c6a5Schristos */
744032a4398Schristos Set_LineNum(1 - NHEADER_LINES);
7450061c6a5Schristos CheckErrorCount = 0; eof = FALSE;
746f8739668Schristos rewind(tmp);
7470061c6a5Schristos while (!CheckErrorCount && !eof) {
7480061c6a5Schristos switch (load_env(envstr, tmp)) {
7490061c6a5Schristos case ERR:
7500061c6a5Schristos /* check for data before the EOF */
7510061c6a5Schristos if (envstr[0] != '\0') {
7520061c6a5Schristos Set_LineNum(LineNumber + 1);
7530061c6a5Schristos check_error("premature EOF");
7540061c6a5Schristos }
7550061c6a5Schristos eof = TRUE;
7560061c6a5Schristos break;
7570061c6a5Schristos case FALSE:
7580061c6a5Schristos e = load_entry(tmp, check_error, pw, envp);
7590061c6a5Schristos if (e)
7600061c6a5Schristos free(e);
7610061c6a5Schristos break;
7620061c6a5Schristos case TRUE:
7630061c6a5Schristos break;
7640061c6a5Schristos }
7650061c6a5Schristos }
7660061c6a5Schristos
7670061c6a5Schristos if (CheckErrorCount != 0) {
768032a4398Schristos warnx("errors in crontab file, can't install.");
7690061c6a5Schristos error = -1;
7700061c6a5Schristos goto done;
7710061c6a5Schristos }
7720061c6a5Schristos
7730061c6a5Schristos file_owner = (getgid() == getegid()) ? ROOT_UID : pw->pw_uid;
7740061c6a5Schristos
775032a4398Schristos #ifdef HAVE_FCHOWN
776032a4398Schristos error = fchown(fileno(tmp), file_owner, (uid_t)-1);
7770061c6a5Schristos #else
778032a4398Schristos error = chown(TempFilename, file_owner, (gid_t)-1);
779032a4398Schristos #endif
7800061c6a5Schristos
7810061c6a5Schristos if (fclose(tmp) == EOF) {
782032a4398Schristos warn("error closing file");
7830061c6a5Schristos error = -2;
7842ef06139Schristos tmp = NULL;
7852ef06139Schristos goto done;
7862ef06139Schristos }
7872ef06139Schristos tmp = NULL;
7882ef06139Schristos
7892ef06139Schristos if (error < OK) {
7902ef06139Schristos warn("cannot chown `%s'", TempFilename);
7912ef06139Schristos error = -2;
7920061c6a5Schristos goto done;
7930061c6a5Schristos }
7940061c6a5Schristos
7950061c6a5Schristos if (!glue_strings(n, sizeof n, SPOOL_DIR, User, '/')) {
796032a4398Schristos warnx("path too long");
7970061c6a5Schristos error = -2;
7980061c6a5Schristos goto done;
7990061c6a5Schristos }
8000061c6a5Schristos if (rename(TempFilename, n)) {
801032a4398Schristos warn("error renaming `%s' to `%s'", TempFilename, n);
8020061c6a5Schristos error = -2;
8030061c6a5Schristos goto done;
8040061c6a5Schristos }
8050061c6a5Schristos TempFilename[0] = '\0';
8060061c6a5Schristos log_it(RealUser, Pid, "REPLACE", User);
8070061c6a5Schristos
8080061c6a5Schristos poke_daemon();
8090061c6a5Schristos
8100061c6a5Schristos done:
811032a4398Schristos (void)signal(SIGHUP, ohup);
812032a4398Schristos (void)signal(SIGINT, oint);
813032a4398Schristos (void)signal(SIGQUIT, oquit);
814032a4398Schristos (void)signal(SIGABRT, oabrt);
8152ef06139Schristos if (tmp != NULL)
8162ef06139Schristos (void)fclose(tmp);
8172ef06139Schristos cleanTempFile();
8180061c6a5Schristos return (error);
8190061c6a5Schristos }
8200061c6a5Schristos
8210061c6a5Schristos static void
poke_daemon(void)8220061c6a5Schristos poke_daemon(void) {
8230a00da6dSchristos struct timespec ts[2];
8240a00da6dSchristos (void) clock_gettime(CLOCK_REALTIME, ts);
8250a00da6dSchristos ts[1] = ts[0];
8260a00da6dSchristos if (change_time(SPOOL_DIR, ts) == -1)
8270a00da6dSchristos warn("can't update times on spooldir %s", SPOOL_DIR);
8280061c6a5Schristos }
8290a00da6dSchristos
830032a4398Schristos /* int allowed(const char *username, const char *allow_file, const char *deny_file)
831032a4398Schristos * returns TRUE if (allow_file exists and user is listed)
832032a4398Schristos * or (deny_file exists and user is NOT listed).
833032a4398Schristos * root is always allowed.
834032a4398Schristos */
835032a4398Schristos static int
allowed(const char * username,const char * allow_file,const char * deny_file)836032a4398Schristos allowed(const char *username, const char *allow_file, const char *deny_file) {
837032a4398Schristos FILE *fp;
838032a4398Schristos int isallowed;
839032a4398Schristos
840032a4398Schristos if (strcmp(username, ROOT_USER) == 0)
841032a4398Schristos return (TRUE);
842e8894454Schristos #ifdef ALLOW_ONLY_ROOT
843032a4398Schristos isallowed = FALSE;
844e8894454Schristos #else
845e8894454Schristos isallowed = TRUE;
846e8894454Schristos #endif
847032a4398Schristos if ((fp = fopen(allow_file, "r")) != NULL) {
848032a4398Schristos isallowed = in_file(username, fp, FALSE);
849032a4398Schristos (void)fclose(fp);
850032a4398Schristos } else if ((fp = fopen(deny_file, "r")) != NULL) {
851032a4398Schristos isallowed = !in_file(username, fp, FALSE);
852032a4398Schristos (void)fclose(fp);
853032a4398Schristos }
854032a4398Schristos return (isallowed);
855032a4398Schristos }
856032a4398Schristos /* int in_file(const char *string, FILE *file, int error)
857032a4398Schristos * return TRUE if one of the lines in file matches string exactly,
858032a4398Schristos * FALSE if no lines match, and error on error.
859032a4398Schristos */
860032a4398Schristos static int
in_file(const char * string,FILE * file,int error)861032a4398Schristos in_file(const char *string, FILE *file, int error)
862032a4398Schristos {
863032a4398Schristos char line[MAX_TEMPSTR];
864032a4398Schristos char *endp;
865032a4398Schristos
866032a4398Schristos if (fseek(file, 0L, SEEK_SET))
867032a4398Schristos return (error);
868032a4398Schristos while (fgets(line, MAX_TEMPSTR, file)) {
869032a4398Schristos if (line[0] != '\0') {
870032a4398Schristos endp = &line[strlen(line) - 1];
871032a4398Schristos if (*endp != '\n')
872032a4398Schristos return (error);
873032a4398Schristos *endp = '\0';
874032a4398Schristos if (0 == strcmp(line, string))
875032a4398Schristos return (TRUE);
876032a4398Schristos }
877032a4398Schristos }
878032a4398Schristos if (ferror(file))
879032a4398Schristos return (error);
880032a4398Schristos return (FALSE);
8810061c6a5Schristos }
8820061c6a5Schristos
883032a4398Schristos #ifdef HAVE_SAVED_UIDS
884032a4398Schristos
relinquish_priv(void)885*ea01913fShtodd static int relinquish_priv(void) {
8862ed8e2f0Schristos return (setegid(rgid) || seteuid(ruid)) ? -1 : 0;
8870061c6a5Schristos }
8882ed8e2f0Schristos
regain_priv(void)8892ed8e2f0Schristos static int regain_priv(void) {
8902ed8e2f0Schristos return (setegid(egid) || seteuid(euid)) ? -1 : 0;
891032a4398Schristos }
892032a4398Schristos
893032a4398Schristos #else /*HAVE_SAVED_UIDS*/
894032a4398Schristos
relinquish_priv(void)895*ea01913fShtodd static int relinquish_priv(void) {
8962ed8e2f0Schristos return (setregid(egid, rgid) || setreuid(euid, ruid)) ? -1 : 0;
897032a4398Schristos }
898032a4398Schristos
regain_priv(void)8992ed8e2f0Schristos static int regain_priv(void) {
9002ed8e2f0Schristos return (setregid(rgid, egid) || setreuid(ruid, euid)) ? -1 : 0;
901032a4398Schristos }
902032a4398Schristos #endif /*HAVE_SAVED_UIDS*/
903