1/*
2 * CDDL HEADER START
3 *
4 * The contents of this file are subject to the terms of the
5 * Common Development and Distribution License (the "License").
6 * You may not use this file except in compliance with the License.
7 *
8 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
9 * or http://www.opensolaris.org/os/licensing.
10 * See the License for the specific language governing permissions
11 * and limitations under the License.
12 *
13 * When distributing Covered Code, include this CDDL HEADER in each
14 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
15 * If applicable, add the following below this CDDL HEADER, with the
16 * fields enclosed by brackets "[]" replaced with your own identifying
17 * information: Portions Copyright [yyyy] [name of copyright owner]
18 *
19 * CDDL HEADER END
20 */
21/*
22 * Copyright 2009 Sun Microsystems, Inc.  All rights reserved.
23 * Use is subject to license terms.
24 */
25/*	Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T	*/
26/*	  All Rights Reserved	*/
27
28/*
29 * Copyright 2019 OmniOS Community Edition (OmniOSce) Association.
30 */
31
32#include <sys/types.h>
33#include <sys/stat.h>
34#include <sys/types.h>
35#include <sys/wait.h>
36#include <errno.h>
37#include <signal.h>
38#include <stdio.h>
39#include <stdlib.h>
40#include <string.h>
41#include <fcntl.h>
42#include <ctype.h>
43#include <pwd.h>
44#include <unistd.h>
45#include <locale.h>
46#include <nl_types.h>
47#include <langinfo.h>
48#include <libintl.h>
49#include <security/pam_appl.h>
50#include <limits.h>
51#include <libzoneinfo.h>
52#include "cron.h"
53#include "getresponse.h"
54
55#if defined(XPG4)
56#define	VIPATH	"/usr/xpg4/bin/vi"
57#elif defined(XPG6)
58#define	VIPATH	"/usr/xpg6/bin/vi"
59#else
60#define	_XPG_NOTDEFINED
61#define	VIPATH	"vi"
62#endif
63
64#define	TMPFILE		"_cron"		/* prefix for tmp file */
65#define	CRMODE		0600	/* mode for creating crontabs */
66
67#define	BADCREATE	\
68	"can't create your crontab file in the crontab directory."
69#define	BADOPEN		"can't open your crontab file."
70#define	BADSHELL	\
71	"because your login shell isn't /usr/bin/sh, you can't use cron."
72#define	WARNSHELL	"warning: commands will be executed using /usr/bin/sh\n"
73#define	BADUSAGE	\
74	"usage:\n"			\
75	"\tcrontab [file]\n"		\
76	"\tcrontab -e [username]\n"	\
77	"\tcrontab -l [username]\n"	\
78	"\tcrontab -r [username]"
79#define	INVALIDUSER	"you are not a valid user (no entry in /etc/passwd)."
80#define	NOTALLOWED	"you are not authorized to use cron.  Sorry."
81#define	NOTROOT		\
82	"you must be super-user to access another user's crontab file"
83#define	AUDITREJECT	"The audit context for your shell has not been set."
84#define	EOLN		"unexpected end of line."
85#define	UNEXPECT	"unexpected character found in line."
86#define	OUTOFBOUND	"number out of bounds."
87#define	OVERFLOW	"too many elements."
88#define	ERRSFND		"errors detected in input, no crontab file generated."
89#define	ED_ERROR	\
90	"     The editor indicates that an error occurred while you were\n"\
91	"     editing the crontab data - usually a minor typing error.\n\n"
92#define	BADREAD		"error reading your crontab file"
93#define	ED_PROMPT	\
94	"     Edit again, to ensure crontab information is intact (%s/%s)?\n"\
95	"     ('%s' will discard edits.)"
96#define	NAMETOOLONG	"login name too long"
97#define	BAD_TZ	"Timezone unrecognized in: %s"
98#define	BAD_SHELL	"Invalid shell specified: %s"
99#define	BAD_HOME	"Unable to access directory: %s\t%s\n"
100
101extern int	per_errno;
102
103extern int	audit_crontab_modify(char *, char *, int);
104extern int	audit_crontab_delete(char *, int);
105extern int	audit_crontab_not_allowed(uid_t, char *);
106
107int		err;
108int		cursor;
109char		*cf;
110char		*tnam;
111char		edtemp[5+13+1];
112char		line[CTLINESIZE];
113static		char	login[UNAMESIZE];
114
115static void	catch(int);
116static void	crabort(char *);
117static void	cerror(char *);
118static void	copycron(FILE *);
119
120int
121main(int argc, char **argv)
122{
123	int	c, r;
124	int	rflag	= 0;
125	int	lflag	= 0;
126	int	eflag	= 0;
127	int	errflg	= 0;
128	char *pp;
129	FILE *fp, *tmpfp;
130	struct stat stbuf;
131	struct passwd *pwp;
132	time_t omodtime;
133	char *editor;
134	uid_t ruid;
135	pid_t pid;
136	int stat_loc;
137	int ret;
138	char real_login[UNAMESIZE];
139	int tmpfd = -1;
140	pam_handle_t *pamh;
141	int pam_error;
142	char *buf;
143	size_t buflen;
144
145	(void) setlocale(LC_ALL, "");
146#if !defined(TEXT_DOMAIN)	/* Should be defined by cc -D */
147#define	TEXT_DOMAIN "SYS_TEST"	/* Use this only if it weren't */
148#endif
149	(void) textdomain(TEXT_DOMAIN);
150
151	if (init_yes() < 0) {
152		(void) fprintf(stderr, gettext(ERR_MSG_INIT_YES),
153		    strerror(errno));
154		exit(1);
155	}
156
157	while ((c = getopt(argc, argv, "elr")) != EOF)
158		switch (c) {
159			case 'e':
160				eflag++;
161				break;
162			case 'l':
163				lflag++;
164				break;
165			case 'r':
166				rflag++;
167				break;
168			case '?':
169				errflg++;
170				break;
171		}
172
173	if (eflag + lflag + rflag > 1)
174		errflg++;
175
176	argc -= optind;
177	argv += optind;
178	if (errflg || argc > 1)
179		crabort(BADUSAGE);
180
181	ruid = getuid();
182	if ((pwp = getpwuid(ruid)) == NULL)
183		crabort(INVALIDUSER);
184
185	if (strlcpy(real_login, pwp->pw_name, sizeof (real_login))
186	    >= sizeof (real_login))
187		crabort(NAMETOOLONG);
188
189	if ((eflag || lflag || rflag) && argc == 1) {
190		if ((pwp = getpwnam(*argv)) == NULL)
191			crabort(INVALIDUSER);
192
193		if (!cron_admin(real_login)) {
194			if (pwp->pw_uid != ruid)
195				crabort(NOTROOT);
196			else
197				pp = getuser(ruid);
198		} else
199			pp = *argv++;
200	} else {
201		pp = getuser(ruid);
202	}
203
204	if (pp == NULL) {
205		if (per_errno == 2)
206			crabort(BADSHELL);
207		else
208			crabort(INVALIDUSER);
209	}
210	if (strlcpy(login, pp, sizeof (login)) >= sizeof (login))
211		crabort(NAMETOOLONG);
212	if (!allowed(login, CRONALLOW, CRONDENY))
213		crabort(NOTALLOWED);
214
215	/* Do account validation check */
216	pam_error = pam_start("cron", pp, NULL, &pamh);
217	if (pam_error != PAM_SUCCESS) {
218		crabort((char *)pam_strerror(pamh, pam_error));
219	}
220	pam_error = pam_acct_mgmt(pamh, PAM_SILENT);
221	if (pam_error != PAM_SUCCESS) {
222		(void) fprintf(stderr, gettext("Warning - Invalid account: "
223		    "'%s' not allowed to execute cronjobs\n"), pp);
224	}
225	(void) pam_end(pamh, PAM_SUCCESS);
226
227
228	/* check for unaudited shell */
229	if (audit_crontab_not_allowed(ruid, pp))
230		crabort(AUDITREJECT);
231
232	cf = xmalloc(strlen(CRONDIR)+strlen(login)+2);
233	strcat(strcat(strcpy(cf, CRONDIR), "/"), login);
234
235	if (rflag) {
236		r = unlink(cf);
237		cron_sendmsg(DELETE, login, login, CRON);
238		audit_crontab_delete(cf, r);
239		exit(0);
240	}
241	if (lflag) {
242		if ((fp = fopen(cf, "r")) == NULL)
243			crabort(BADOPEN);
244		while (fgets(line, CTLINESIZE, fp) != NULL)
245			fputs(line, stdout);
246		fclose(fp);
247		exit(0);
248	}
249	if (eflag) {
250		if ((fp = fopen(cf, "r")) == NULL) {
251			if (errno != ENOENT)
252				crabort(BADOPEN);
253		}
254		(void) strcpy(edtemp, "/tmp/crontabXXXXXX");
255		tmpfd = mkstemp(edtemp);
256		if (fchown(tmpfd, ruid, -1) == -1) {
257			(void) close(tmpfd);
258			crabort("fchown of temporary file failed");
259		}
260		(void) close(tmpfd);
261		/*
262		 * Fork off a child with user's permissions,
263		 * to edit the crontab file
264		 */
265		if ((pid = fork()) == (pid_t)-1)
266			crabort("fork failed");
267		if (pid == 0) {		/* child process */
268			/* give up super-user privileges. */
269			setuid(ruid);
270			if ((tmpfp = fopen(edtemp, "w")) == NULL)
271				crabort("can't create temporary file");
272			if (fp != NULL) {
273				/*
274				 * Copy user's crontab file to temporary file.
275				 */
276				while (fgets(line, CTLINESIZE, fp) != NULL) {
277					fputs(line, tmpfp);
278					if (ferror(tmpfp)) {
279						fclose(fp);
280						fclose(tmpfp);
281						crabort("write error on"
282						    "temporary file");
283					}
284				}
285				if (ferror(fp)) {
286					fclose(fp);
287					fclose(tmpfp);
288					crabort(BADREAD);
289				}
290				fclose(fp);
291			}
292			if (fclose(tmpfp) == EOF)
293				crabort("write error on temporary file");
294			if (stat(edtemp, &stbuf) < 0)
295				crabort("can't stat temporary file");
296			omodtime = stbuf.st_mtime;
297#ifdef _XPG_NOTDEFINED
298			editor = getenv("VISUAL");
299			if (editor == NULL) {
300#endif
301				editor = getenv("EDITOR");
302				if (editor == NULL)
303					editor = VIPATH;
304#ifdef _XPG_NOTDEFINED
305			}
306#endif
307			buflen = strlen(editor) + strlen(edtemp) + 2;
308			buf = xmalloc(buflen);
309			(void) snprintf(buf, buflen, "%s %s", editor, edtemp);
310
311			sleep(1);
312
313			while (1) {
314				ret = system(buf);
315
316				/* sanity checks */
317				if ((tmpfp = fopen(edtemp, "r")) == NULL)
318					crabort("can't open temporary file");
319				if (fstat(fileno(tmpfp), &stbuf) < 0)
320					crabort("can't stat temporary file");
321				if (stbuf.st_size == 0)
322					crabort("temporary file empty");
323				if (omodtime == stbuf.st_mtime) {
324					(void) unlink(edtemp);
325					fprintf(stderr, gettext(
326					    "The crontab file was not"
327					    " changed.\n"));
328					exit(1);
329				}
330				if ((ret) && (errno != EINTR)) {
331					/*
332					 * Some editors (like 'vi') can return
333					 * a non-zero exit status even though
334					 * everything is okay. Need to check.
335					 */
336					fprintf(stderr, gettext(ED_ERROR));
337					fflush(stderr);
338					if (isatty(fileno(stdin))) {
339						/* Interactive */
340						fprintf(stdout,
341						    gettext(ED_PROMPT),
342						    yesstr, nostr, nostr);
343						fflush(stdout);
344
345						if (yes()) {
346							/* Edit again */
347							continue;
348						} else {
349							/* Dump changes */
350							(void) unlink(edtemp);
351							exit(1);
352						}
353					} else {
354						/*
355						 * Non-interactive, dump changes
356						 */
357						(void) unlink(edtemp);
358						exit(1);
359					}
360				}
361				exit(0);
362			} /* while (1) */
363		}
364
365		/* fix for 1125555 - ignore common signals while waiting */
366		(void) signal(SIGINT, SIG_IGN);
367		(void) signal(SIGHUP, SIG_IGN);
368		(void) signal(SIGQUIT, SIG_IGN);
369		(void) signal(SIGTERM, SIG_IGN);
370		wait(&stat_loc);
371		if ((stat_loc & 0xFF00) != 0)
372			exit(1);
373
374		/*
375		 * unlink edtemp as 'ruid'. The file contents will be held
376		 * since we open the file descriptor 'tmpfp' before calling
377		 * unlink.
378		 */
379		if (((ret = seteuid(ruid)) < 0) ||
380		    ((tmpfp = fopen(edtemp, "r")) == NULL) ||
381		    (unlink(edtemp) == -1)) {
382			fprintf(stderr, "crontab: %s: %s\n",
383			    edtemp, errmsg(errno));
384			if ((ret < 0) || (tmpfp == NULL))
385				(void) unlink(edtemp);
386			exit(1);
387		} else
388			seteuid(0);
389
390		copycron(tmpfp);
391	} else {
392		if (argc == 0)
393			copycron(stdin);
394		else if (seteuid(getuid()) != 0 || (fp = fopen(argv[0], "r"))
395		    == NULL)
396			crabort(BADOPEN);
397		else {
398			seteuid(0);
399			copycron(fp);
400		}
401	}
402	cron_sendmsg(ADD, login, login, CRON);
403/*
404 *	if (per_errno == 2)
405 *		fprintf(stderr, gettext(WARNSHELL));
406 */
407	return (0);
408}
409
410static void
411copycron(FILE *fp)
412{
413	FILE *tfp;
414	char pid[6], *tnam_end;
415	int t;
416	char buf[LINE_MAX];
417	cferror_t cferr;
418
419	sprintf(pid, "%-5d", getpid());
420	tnam = xmalloc(strlen(CRONDIR)+strlen(TMPFILE)+7);
421	strcat(strcat(strcat(strcpy(tnam, CRONDIR), "/"), TMPFILE), pid);
422	/* cut trailing blanks */
423	tnam_end = strchr(tnam, ' ');
424	if (tnam_end != NULL)
425		*tnam_end = 0;
426	/* catch SIGINT, SIGHUP, SIGQUIT signals */
427	if (signal(SIGINT, catch) == SIG_IGN)
428		signal(SIGINT, SIG_IGN);
429	if (signal(SIGHUP, catch) == SIG_IGN) signal(SIGHUP, SIG_IGN);
430	if (signal(SIGQUIT, catch) == SIG_IGN) signal(SIGQUIT, SIG_IGN);
431	if (signal(SIGTERM, catch) == SIG_IGN) signal(SIGTERM, SIG_IGN);
432	if ((t = creat(tnam, CRMODE)) == -1) crabort(BADCREATE);
433	if ((tfp = fdopen(t, "w")) == NULL) {
434		unlink(tnam);
435		crabort(BADCREATE);
436	}
437	err = 0;	/* if errors found, err set to 1 */
438	while (fgets(line, CTLINESIZE, fp) != NULL) {
439		cursor = 0;
440		while (line[cursor] == ' ' || line[cursor] == '\t')
441			cursor++;
442		/* fix for 1039689 - treat blank line like a comment */
443		if (line[cursor] == '#' || line[cursor] == '\n')
444			goto cont;
445
446		if (strncmp(&line[cursor], ENV_TZ, strlen(ENV_TZ)) == 0) {
447			char *x;
448
449			strncpy(buf, &line[cursor + strlen(ENV_TZ)],
450			    sizeof (buf));
451			if ((x = strchr(buf, '\n')) != NULL)
452				*x = '\0';
453
454			if (isvalid_tz(buf, NULL, _VTZ_ALL)) {
455				goto cont;
456			} else {
457				err = 1;
458				fprintf(stderr, BAD_TZ, &line[cursor]);
459				continue;
460			}
461		} else if (strncmp(&line[cursor], ENV_SHELL,
462		    strlen(ENV_SHELL)) == 0) {
463			char *x;
464
465			strncpy(buf, &line[cursor + strlen(ENV_SHELL)],
466			    sizeof (buf));
467			if ((x = strchr(buf, '\n')) != NULL)
468				*x = '\0';
469
470			if (isvalid_shell(buf)) {
471				goto cont;
472			} else {
473				err = 1;
474				fprintf(stderr, BAD_SHELL, &line[cursor]);
475				continue;
476			}
477		} else if (strncmp(&line[cursor], ENV_HOME,
478		    strlen(ENV_HOME)) == 0) {
479			char *x;
480
481			strncpy(buf, &line[cursor + strlen(ENV_HOME)],
482			    sizeof (buf));
483			if ((x = strchr(buf, '\n')) != NULL)
484				*x = '\0';
485			if (chdir(buf) == 0) {
486				goto cont;
487			} else {
488				err = 1;
489				fprintf(stderr, BAD_HOME, &line[cursor],
490				    strerror(errno));
491				continue;
492			}
493		}
494
495		if ((cferr = next_field(0, 59, line, &cursor, NULL)) != CFOK ||
496		    (cferr = next_field(0, 23, line, &cursor, NULL)) != CFOK ||
497		    (cferr = next_field(1, 31, line, &cursor, NULL)) != CFOK ||
498		    (cferr = next_field(1, 12, line, &cursor, NULL)) != CFOK ||
499		    (cferr = next_field(0, 6, line, &cursor, NULL)) != CFOK) {
500			switch (cferr) {
501			case CFEOLN:
502				cerror(EOLN);
503				break;
504			case CFUNEXPECT:
505				cerror(UNEXPECT);
506				break;
507			case CFOUTOFBOUND:
508				cerror(OUTOFBOUND);
509				break;
510			case CFEOVERFLOW:
511				cerror(OVERFLOW);
512				break;
513			case CFENOMEM:
514				(void) fprintf(stderr, "Out of memory\n");
515				exit(55);
516				break;
517			default:
518				break;
519			}
520			continue;
521		}
522
523		if (line[++cursor] == '\0') {
524			cerror(EOLN);
525			continue;
526		}
527cont:
528		if (fputs(line, tfp) == EOF) {
529			unlink(tnam);
530			crabort(BADCREATE);
531		}
532	}
533	fclose(fp);
534	fclose(tfp);
535
536	/* audit differences between old and new crontabs */
537	audit_crontab_modify(cf, tnam, err);
538
539	if (!err) {
540		/* make file tfp the new crontab */
541		unlink(cf);
542		if (link(tnam, cf) == -1) {
543			unlink(tnam);
544			crabort(BADCREATE);
545		}
546	} else {
547		crabort(ERRSFND);
548	}
549	unlink(tnam);
550}
551
552static void
553cerror(char *msg)
554{
555	fprintf(stderr, gettext("%scrontab: error on previous line; %s\n"),
556	    line, msg);
557	err = 1;
558}
559
560
561static void
562catch(int x)
563{
564	unlink(tnam);
565	exit(1);
566}
567
568static void
569crabort(char *msg)
570{
571	int sverrno;
572
573	if (strcmp(edtemp, "") != 0) {
574		sverrno = errno;
575		(void) unlink(edtemp);
576		errno = sverrno;
577	}
578	if (tnam != NULL) {
579		sverrno = errno;
580		(void) unlink(tnam);
581		errno = sverrno;
582	}
583	fprintf(stderr, "crontab: %s\n", gettext(msg));
584	exit(1);
585}
586