/* * CDDL HEADER START * * The contents of this file are subject to the terms of the * Common Development and Distribution License, Version 1.0 only * (the "License"). You may not use this file except in compliance * with the License. * * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE * or http://www.opensolaris.org/os/licensing. * See the License for the specific language governing permissions * and limitations under the License. * * When distributing Covered Code, include this CDDL HEADER in each * file and include the License file at usr/src/OPENSOLARIS.LICENSE. * If applicable, add the following below this CDDL HEADER, with the * fields enclosed by brackets "[]" replaced with your own identifying * information: Portions Copyright [yyyy] [name of copyright owner] * * CDDL HEADER END */ /* * Copyright 2005 Sun Microsystems, Inc. All rights reserved. * Use is subject to license terms. */ /* Copyright (c) 1984, 1986, 1987, 1988, 1989 AT&T */ /* All Rights Reserved */ /* Copyright (c) 1987, 1988 Microsoft Corporation */ /* All Rights Reserved */ #pragma ident "%Z%%M% %I% %E% SMI" #ifdef lint /* make lint happy */ #define __EXTENSIONS__ #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "cron.h" /* * #define DEBUG */ #define MAIL "/usr/bin/mail" /* mail program to use */ #define CONSOLE "/dev/console" /* where messages go when cron dies */ #define TMPINFILE "/tmp/crinXXXXXX" /* file to put stdin in for cmd */ #define TMPDIR "/tmp" #define PFX "crout" #define TMPOUTFILE "/tmp/croutXXXXXX" /* file to place stdout, stderr */ #define INMODE 00400 /* mode for stdin file */ #define OUTMODE 00600 /* mode for stdout file */ #define ISUID S_ISUID /* mode for verifing at jobs */ #define INFINITY 2147483647L /* upper bound on time */ #define CUSHION 180L #define MAXRUN 100 /* max total jobs allowed in system */ #define ZOMB 100 /* proc slot used for mailing output */ #define JOBF 'j' #define NICEF 'n' #define USERF 'u' #define WAITF 'w' #define BCHAR '>' #define ECHAR '<' #define DEFAULT 0 #define LOAD 1 #define QBUFSIZ 80 /* Defined actions for crabort() routine */ #define NO_ACTION 000 #define REMOVE_FIFO 001 #define CONSOLE_MSG 002 #define BADCD "can't change directory to the crontab directory." #define NOREADDIR "can't read the crontab directory." #define BADJOBOPEN "unable to read your at job." #define BADSHELL "because your login shell \ isn't /usr/bin/sh, you can't use cron." #define BADSTAT "can't access your crontab file. Resubmit it." #define BADPROJID "can't set project id for your job." #define CANTCDHOME "can't change directory to your home directory.\ \nYour commands will not be executed." #define CANTEXECSH "unable to exec the shell for one of your commands." #define NOREAD "can't read your crontab file. Resubmit it." #define BADTYPE "crontab is not a regular file.\n" #define NOSTDIN "unable to create a standard input file for \ one of your crontab commands. \ \nThat command was not executed." #define NOTALLOWED "you are not authorized to use cron. Sorry." #define STDERRMSG "\n\n********************************************\ *****\nCron: The previous message is the \ standard output and standard error \ \nof one of your cron commands.\n" #define STDOUTERR "one of your commands generated output or errors, \ but cron was unable to mail you this output.\ \nRemember to redirect standard output and standard \ error for each of your commands." #define CLOCK_DRIFT "clock time drifted backwards after event!\n" #define PIDERR "unexpected pid returned %d (ignored)" #define CRONTABERR "Subject: Your crontab file has an error in it\n\n" #define CRONOUT "Subject: Output from \"cron\" command\n\n" #define MALLOCERR "out of space, cannot create new string\n" #define DIDFORK didfork #define NOFORK !didfork #define MAILBUFLEN (8*1024) #define LINELIMIT 80 #define MAILBINITFREE (MAILBUFLEN - (sizeof (cte_intro) - 1) \ - (sizeof (cte_trail1) - 1) - (sizeof (cte_trail2) - 1) - 1) #define ERR_CRONTABENT 0 /* error in crontab file entry */ #define ERR_UNIXERR 1 /* error in some system call */ #define ERR_CANTEXECCRON 2 /* error setting up "cron" job environment */ #define ERR_CANTEXECAT 3 /* error setting up "at" job environment */ #define PROJECT "project=" #define MAX_LOST_CONTRACTS 2048 /* reset if this many failed abandons */ #define FORMAT "%a %b %e %H:%M:%S %Y" static char timebuf[80]; struct event { time_t time; /* time of the event */ short etype; /* what type of event; 0=cron, 1=at */ char *cmd; /* command for cron, job name for at */ struct usr *u; /* ptr to the owner (usr) of this event */ struct event *link; /* ptr to another event for this user */ union { struct { /* for crontab events */ char *minute; /* (these */ char *hour; /* fields */ char *daymon; /* are */ char *month; /* from */ char *dayweek; /* crontab) */ char *input; /* ptr to stdin */ } ct; struct { /* for at events */ short exists; /* for revising at events */ int eventid; /* for el_remove-ing at events */ } at; } of; }; struct usr { char *name; /* name of user (e.g. "root") */ char *home; /* home directory for user */ uid_t uid; /* user id */ gid_t gid; /* group id */ int aruncnt; /* counter for running jobs per uid */ int cruncnt; /* counter for running cron jobs per uid */ int ctid; /* for el_remove-ing crontab events */ short ctexists; /* for revising crontab events */ struct event *ctevents; /* list of this usr's crontab events */ struct event *atevents; /* list of this usr's at events */ struct usr *nextusr; }; /* ptr to next user */ static struct queue { int njob; /* limit */ int nice; /* nice for execution */ int nwait; /* wait time to next execution attempt */ int nrun; /* number running */ } qd = {100, 2, 60}, /* default values for queue defs */ qt[NQUEUE]; static struct queue qq; struct runinfo { pid_t pid; short que; struct usr *rusr; /* pointer to usr struct */ char *outfile; /* file where stdout & stderr are trapped */ short jobtype; /* what type of event: 0=cron, 1=at */ char *jobname; /* command for "cron", jobname for "at" */ int mailwhendone; /* 1 = send mail even if no ouptut */ } rt[MAXRUN]; static struct miscpid { pid_t pid; struct miscpid *next; } *miscpid_head; static pid_t cron_pid; /* own pid */ static char didfork = 0; /* flag to see if I'm process group leader */ static int msgfd; /* file descriptor for fifo queue */ static int ecid = 1; /* event class id for el_remove(); MUST be set to 1 */ static int delayed; /* is job being rescheduled or did it run first time */ static int cwd; /* current working directory */ static struct event *next_event; /* the next event to execute */ static struct usr *uhead; /* ptr to the list of users */ /* Variables for error handling at reading crontabs. */ static char cte_intro[] = "Line(s) with errors:\n\n"; static char cte_trail1[] = "\nMax number of errors encountered."; static char cte_trail2[] = " Evaluation of crontab aborted.\n"; static int cte_free = MAILBINITFREE; /* Free buffer space */ static char *cte_text = NULL; /* Text buffer pointer */ static char *cte_lp; /* Next free line in cte_text */ static int cte_nvalid; /* Valid lines found */ /* user's default environment for the shell */ #define ROOTPATH "PATH=/usr/sbin:/usr/bin" #define NONROOTPATH "PATH=/usr/bin:" static char *Def_supath = NULL; static char *Def_path = NULL; static char path[LINE_MAX] = "PATH="; static char supath[LINE_MAX] = "PATH="; static char homedir[LINE_MAX] = "HOME="; static char logname[LINE_MAX] = "LOGNAME="; static char tzone[LINE_MAX] = "TZ="; static char *envinit[] = { homedir, logname, ROOTPATH, "SHELL=/usr/bin/sh", tzone, NULL }; extern char **environ; #define DEFTZ "GMT" static int log = 0; static char hzname[10]; static void cronend(int); static void thaw_handler(int); static void child_handler(int); static void child_sigreset(void); static void dscan(DIR *dir, void (*fp)(char *, time_t)); static void mod_ctab(char *, time_t); static void mod_atjob(char *, time_t); static void add_atevent(struct usr *, char *, time_t, int); static void rm_ctevents(struct usr *); static void cleanup(struct runinfo *rn, int r); static void crabort(char *, int); static void msg(char *fmt, ...); static void logit(int, struct runinfo *, int); static void parsqdef(char *); static void defaults(); static void initialize(int); static void quedefs(int); static int idle(long); static struct usr *find_usr(char *); static int ex(struct event *e); static void read_dirs(void); static void mail(char *, char *, int); static char *next_field(int, int); static void readcron(struct usr *, time_t); static int next_ge(int, char *); static void free_if_unused(struct usr *); static void del_atjob(char *, char *); static void del_ctab(char *); static void resched(int); static int msg_wait(long); static struct runinfo *rinfo_get(pid_t); static void rinfo_free(struct runinfo *rp); static void mail_result(struct usr *p, struct runinfo *pr, size_t filesize); static time_t next_time(struct event *, time_t); static time_t get_switching_time(int, time_t); static time_t xmktime(struct tm *); static void process_msg(struct message *, time_t); static void reap_child(void); static void miscpid_insert(pid_t); static int miscpid_delete(pid_t); static int contract_set_template(void); static int contract_clear_template(void); static void contract_abandon_latest(pid_t); static void cte_init(void); static void cte_add(int, char *); static void cte_valid(void); static int cte_istoomany(void); static void cte_sendmail(char *); static int set_user_cred(const struct usr *, struct project *); /* * last_time is set immediately prior to exection of an event (via ex()) * to indicate the last time an event was executed. This was (surely) * it's original intended use. */ static time_t last_time, init_time, t_old; static int accept_sigcld, notifypipe[2]; static sigset_t defmask, childmask; /* * BSM hooks */ extern int audit_cron_session(char *, char *, uid_t, gid_t, char *); extern void audit_cron_new_job(char *, int, void *); extern void audit_cron_bad_user(char *); extern void audit_cron_user_acct_expired(char *); extern int audit_cron_create_anc_file(char *, char *, char *, uid_t); extern int audit_cron_delete_anc_file(char *, char *); extern int audit_cron_is_anc_name(char *); extern int audit_cron_mode(); static int cron_conv(int, struct pam_message **, struct pam_response **, void *); static struct pam_conv pam_conv = {cron_conv, NULL}; static pam_handle_t *pamh; /* Authentication handle */ /* * Function to help check a user's credentials. */ static int verify_user_cred(const struct usr *u); /* * Values returned by verify_user_cred and set_user_cred: */ #define VUC_OK 0 #define VUC_BADUSER 1 #define VUC_NOTINGROUP 2 #define VUC_EXPIRED 3 #define VUC_NEW_AUTH 4 /* * Modes of process_anc_files function */ #define CRON_ANC_DELETE 1 #define CRON_ANC_CREATE 0 /* * Functions to remove a user or job completely from the running database. */ static void clean_out_atjobs(struct usr *u); static void clean_out_ctab(struct usr *u); static void clean_out_user(struct usr *u); static void cron_unlink(char *name); static void process_anc_files(int); /* * functions in elm.c */ extern void el_init(int, time_t, time_t, int); extern void el_add(void *, time_t, int); extern void el_remove(int, int); extern int el_empty(void); extern void *el_first(void); extern void el_delete(void); int main(int argc, char *argv[]) { time_t t; time_t ne_time; /* amt of time until next event execution */ time_t newtime, lastmtime = 0L; struct usr *u; struct event *e, *e2, *eprev; struct stat buf; pid_t rfork; struct sigaction act; /* * reset is set to 1 via the return from ex() should ex() find * that the event to be executed is being run at the wrong time. * We immediately return to the top of the while (TRUE) loop in * main() where the event list is cleared and rebuilt, and reset * is set back to 0. */ int reset = 0; /* * Only the privileged user can run this command. */ if (getuid() != 0) crabort(NOTALLOWED, 0); begin: (void) setlocale(LC_ALL, ""); /* fork unless 'nofork' is specified */ if ((argc <= 1) || (strcmp(argv[1], "nofork"))) { if (rfork = fork()) { if (rfork == (pid_t)-1) { (void) sleep(30); goto begin; } return (0); } didfork++; (void) setpgrp(); /* detach cron from console */ } (void) umask(022); (void) signal(SIGHUP, SIG_IGN); (void) signal(SIGINT, SIG_IGN); (void) signal(SIGQUIT, SIG_IGN); (void) signal(SIGTERM, cronend); defaults(); initialize(1); quedefs(DEFAULT); /* load default queue definitions */ cron_pid = getpid(); msg("*** cron started *** pid = %d", cron_pid); (void) sigset(SIGTHAW, thaw_handler); /* * setup SIGCLD handler/mask */ act.sa_handler = child_handler; act.sa_flags = 0; (void) sigemptyset(&act.sa_mask); (void) sigaddset(&act.sa_mask, SIGCLD); (void) sigaction(SIGCLD, &act, NULL); (void) sigemptyset(&childmask); (void) sigaddset(&childmask, SIGCLD); (void) sigprocmask(SIG_BLOCK, &childmask, &defmask); if (pipe(notifypipe) != 0) { crabort("cannot create pipe", REMOVE_FIFO|CONSOLE_MSG); } /* * will set O_NONBLOCK, so that the write() from child_handler * never be blocked. */ (void) fcntl(notifypipe[0], F_SETFL, O_WRONLY|O_NONBLOCK); t_old = time(NULL); last_time = t_old; for (;;) { /* MAIN LOOP */ t = time(NULL); if ((t_old > t) || (t-last_time > CUSHION) || reset) { reset = 0; /* the time was set backwards or forward */ el_delete(); u = uhead; while (u != NULL) { rm_ctevents(u); e = u->atevents; while (e != NULL) { free(e->cmd); e2 = e->link; free(e); e = e2; } u->atevents = NULL; u = u->nextusr; } (void) close(msgfd); initialize(0); t = time(NULL); last_time = t; } t_old = t; if (next_event == NULL && !el_empty()) { next_event = (struct event *)el_first(); } if (next_event == NULL) { ne_time = INFINITY; } else { ne_time = next_event->time - t; #ifdef DEBUG cftime(timebuf, "%C", &next_event->time); (void) fprintf(stderr, "next_time=%ld %s\n", next_event->time, timebuf); #endif } if (ne_time > 0) { if ((reset = idle(ne_time)) != 0) continue; } if (stat(QUEDEFS, &buf)) { msg("cannot stat QUEDEFS file"); } else if (lastmtime != buf.st_mtime) { quedefs(LOAD); lastmtime = buf.st_mtime; } last_time = next_event->time; /* save execution time */ if (reset = ex(next_event)) continue; switch (next_event->etype) { case CRONEVENT: /* add cronevent back into the main event list */ if (delayed) { delayed = 0; break; } /* * check if time(0)< last_time. if so, then the * system clock has gone backwards. to prevent this * job from being started twice, we reschedule this * job for the >>next time after last_time<<, and * then set next_event->time to this. note that * crontab's resolution is 1 minute. */ if (last_time > time(NULL)) { msg(CLOCK_DRIFT); /* * bump up to next 30 second * increment * 1 <= newtime <= 30 */ newtime = 30 - (last_time % 30); newtime += last_time; /* * get the next scheduled event, * not the one that we just * kicked off! */ next_event->time = next_time(next_event, newtime); t_old = time(NULL); } else { next_event->time = next_time(next_event, (time_t)0); } #ifdef DEBUG cftime(timebuf, "%C", &next_event->time); (void) fprintf(stderr, "pushing back cron event %s at %ld (%s)\n", next_event->cmd, next_event->time, timebuf); #endif el_add(next_event, next_event->time, (next_event->u)->ctid); break; default: /* remove at or batch job from system */ if (delayed) { delayed = 0; break; } eprev = NULL; e = (next_event->u)->atevents; while (e != NULL) { if (e == next_event) { if (eprev == NULL) (e->u)->atevents = e->link; else eprev->link = e->link; free(e->cmd); free(e); break; } else { eprev = e; e = e->link; } } break; } next_event = NULL; } /*NOTREACHED*/ } static void initialize(int firstpass) { #ifdef DEBUG (void) fprintf(stderr, "in initialize\n"); #endif init_time = time(NULL); el_init(8, init_time, (time_t)(60*60*24), 10); if (firstpass) { /* for mail(1), make sure messages come from root */ if (putenv("LOGNAME=root") != 0) { crabort("cannot expand env variable", REMOVE_FIFO|CONSOLE_MSG); } if (access(FIFO, R_OK) == -1) { if (errno == ENOENT) { if (mknod(FIFO, S_IFIFO|0600, 0) != 0) crabort("cannot create fifo queue", REMOVE_FIFO|CONSOLE_MSG); } else { if (NOFORK) { /* didn't fork... init(1M) is waiting */ (void) sleep(60); } perror("FIFO"); crabort("cannot access fifo queue", REMOVE_FIFO|CONSOLE_MSG); } } else { if (NOFORK) { /* didn't fork... init(1M) is waiting */ (void) sleep(60); /* * the wait is painful, but we don't want * init respawning this quickly */ } crabort("cannot start cron; FIFO exists", CONSOLE_MSG); } } if ((msgfd = open(FIFO, O_RDWR)) < 0) { perror("! open"); crabort("cannot open fifo queue", REMOVE_FIFO|CONSOLE_MSG); } /* * read directories, create users list, and add events to the * main event list. Only zero user list on firstpass. */ if (firstpass) uhead = NULL; read_dirs(); next_event = NULL; if (!firstpass) return; /* stdout is log file */ if (freopen(ACCTFILE, "a", stdout) == NULL) (void) fprintf(stderr, "cannot open %s\n", ACCTFILE); /* log should be root-only */ (void) fchmod(1, S_IRUSR|S_IWUSR); /* stderr also goes to ACCTFILE */ (void) close(fileno(stderr)); (void) dup(1); /* null for stdin */ (void) freopen("/dev/null", "r", stdin); contract_set_template(); } static void read_dirs() { DIR *dir; if (chdir(CRONDIR) == -1) crabort(BADCD, REMOVE_FIFO|CONSOLE_MSG); cwd = CRON; if ((dir = opendir(".")) == NULL) crabort(NOREADDIR, REMOVE_FIFO|CONSOLE_MSG); dscan(dir, mod_ctab); (void) closedir(dir); if (chdir(ATDIR) == -1) { msg("cannot chdir to at directory"); return; } cwd = AT; if ((dir = opendir(".")) == NULL) { msg("cannot read at at directory"); return; } dscan(dir, mod_atjob); (void) closedir(dir); } static void dscan(DIR *df, void (*fp)(char *, time_t)) { struct dirent *dp; while ((dp = readdir(df)) != NULL) { if (strcmp(dp->d_name, ".") == 0 || strcmp(dp->d_name, "..") == 0) { continue; } (*fp)(dp->d_name, 0); } } static void mod_ctab(char *name, time_t reftime) { struct passwd *pw; struct stat buf; struct usr *u; char namebuf[PATH_MAX]; char *pname; /* skip over ancillary file names */ if (audit_cron_is_anc_name(name)) return; if ((pw = getpwnam(name)) == NULL) { msg("No such user as %s - cron entries not created", name); return; } if (cwd != CRON) { if (snprintf(namebuf, sizeof (namebuf), "%s/%s", CRONDIR, name) >= sizeof (namebuf)) { msg("Too long path name %s - cron entries not created", namebuf); return; } pname = namebuf; } else { pname = name; } /* * a warning message is given by the crontab command so there is * no need to give one here...... use this code if you only want * users with a login shell of /usr/bin/sh to use cron */ #ifdef BOURNESHELLONLY if ((strcmp(pw->pw_shell, "") != 0) && (strcmp(pw->pw_shell, SHELL) != 0)) { mail(name, BADSHELL, ERR_CANTEXECCRON); cron_unlink(pname); return; } #endif if (stat(pname, &buf)) { mail(name, BADSTAT, ERR_UNIXERR); cron_unlink(pname); return; } if (!S_ISREG(buf.st_mode)) { mail(name, BADTYPE, ERR_CRONTABENT); return; } if ((u = find_usr(name)) == NULL) { #ifdef DEBUG (void) fprintf(stderr, "new user (%s) with a crontab\n", name); #endif u = xmalloc(sizeof (struct usr)); u->name = xmalloc(strlen(name)+1); (void) strcpy(u->name, name); u->home = xmalloc(strlen(pw->pw_dir)+1); (void) strcpy(u->home, pw->pw_dir); u->uid = pw->pw_uid; u->gid = pw->pw_gid; u->ctexists = TRUE; u->ctid = ecid++; u->ctevents = NULL; u->atevents = NULL; u->aruncnt = 0; u->cruncnt = 0; u->nextusr = uhead; uhead = u; readcron(u, reftime); } else { u->uid = pw->pw_uid; u->gid = pw->pw_gid; if (strcmp(u->home, pw->pw_dir) != 0) { free(u->home); u->home = xmalloc(strlen(pw->pw_dir)+1); (void) strcpy(u->home, pw->pw_dir); } u->ctexists = TRUE; if (u->ctid == 0) { #ifdef DEBUG (void) fprintf(stderr, "%s now has a crontab\n", u->name); #endif /* user didnt have a crontab last time */ u->ctid = ecid++; u->ctevents = NULL; readcron(u, reftime); return; } #ifdef DEBUG (void) fprintf(stderr, "%s has revised his crontab\n", u->name); #endif rm_ctevents(u); el_remove(u->ctid, 0); readcron(u, reftime); } } /* ARGSUSED */ static void mod_atjob(char *name, time_t reftime) { char *ptr; time_t tim; struct passwd *pw; struct stat buf; struct usr *u; struct event *e; char namebuf[PATH_MAX]; char *pname; int jobtype; ptr = name; if (((tim = num(&ptr)) == 0) || (*ptr != '.')) return; ptr++; if (!isalpha(*ptr)) return; jobtype = *ptr - 'a'; /* check for audit ancillary file */ if (audit_cron_is_anc_name(name)) return; if (cwd != AT) { if (snprintf(namebuf, sizeof (namebuf), "%s/%s", ATDIR, name) >= sizeof (namebuf)) { return; } pname = namebuf; } else { pname = name; } if (stat(pname, &buf) || jobtype >= NQUEUE) { cron_unlink(pname); return; } if (!(buf.st_mode & ISUID) || !S_ISREG(buf.st_mode)) { cron_unlink(pname); return; } if ((pw = getpwuid(buf.st_uid)) == NULL) { cron_unlink(pname); return; } /* * a warning message is given by the at command so there is no * need to give one here......use this code if you only want * users with a login shell of /usr/bin/sh to use cron */ #ifdef BOURNESHELLONLY if ((strcmp(pw->pw_shell, "") != 0) && (strcmp(pw->pw_shell, SHELL) != 0)) { mail(pw->pw_name, BADSHELL, ERR_CANTEXECAT); cron_unlink(pname); return; } #endif if ((u = find_usr(pw->pw_name)) == NULL) { #ifdef DEBUG (void) fprintf(stderr, "new user (%s) with an at job = %s\n", pw->pw_name, name); #endif u = xmalloc(sizeof (struct usr)); u->name = xmalloc(strlen(pw->pw_name)+1); (void) strcpy(u->name, pw->pw_name); u->home = xmalloc(strlen(pw->pw_dir)+1); (void) strcpy(u->home, pw->pw_dir); u->uid = pw->pw_uid; u->gid = pw->pw_gid; u->ctexists = FALSE; u->ctid = 0; u->ctevents = NULL; u->atevents = NULL; u->aruncnt = 0; u->cruncnt = 0; u->nextusr = uhead; uhead = u; add_atevent(u, name, tim, jobtype); } else { u->uid = pw->pw_uid; u->gid = pw->pw_gid; if (strcmp(u->home, pw->pw_dir) != 0) { free(u->home); u->home = xmalloc(strlen(pw->pw_dir)+1); (void) strcpy(u->home, pw->pw_dir); } e = u->atevents; while (e != NULL) { if (strcmp(e->cmd, name) == 0) { e->of.at.exists = TRUE; break; } else { e = e->link; } } if (e == NULL) { #ifdef DEBUG (void) fprintf(stderr, "%s has a new at job = %s\n", u->name, name); #endif add_atevent(u, name, tim, jobtype); } } } static void add_atevent(struct usr *u, char *job, time_t tim, int jobtype) { struct event *e; e = xmalloc(sizeof (struct event)); e->etype = jobtype; e->cmd = xmalloc(strlen(job)+1); (void) strcpy(e->cmd, job); e->u = u; e->link = u->atevents; u->atevents = e; e->of.at.exists = TRUE; e->of.at.eventid = ecid++; if (tim < init_time) /* old job */ e->time = init_time; else e->time = tim; #ifdef DEBUG (void) fprintf(stderr, "add_atevent: user=%s, job=%s, time=%ld\n", u->name, e->cmd, e->time); #endif el_add(e, e->time, e->of.at.eventid); } static char line[CTLINESIZE]; /* holds a line from a crontab file */ static int cursor; /* cursor for the above line */ static void readcron(struct usr *u, time_t reftime) { /* * readcron reads in a crontab file for a user (u). The list of * events for user u is built, and u->events is made to point to * this list. Each event is also entered into the main event * list. */ FILE *cf; /* cf will be a user's crontab file */ struct event *e; int start; unsigned int i; char namebuf[PATH_MAX]; char *pname; int lineno = 0; /* read the crontab file */ cte_init(); /* Init error handling */ if (cwd != CRON) { if (snprintf(namebuf, sizeof (namebuf), "%s/%s", CRONDIR, u->name) >= sizeof (namebuf)) { return; } pname = namebuf; } else { pname = u->name; } if ((cf = fopen(pname, "r")) == NULL) { mail(u->name, NOREAD, ERR_UNIXERR); return; } while (fgets(line, CTLINESIZE, cf) != NULL) { /* process a line of a crontab file */ lineno++; if (cte_istoomany()) break; cursor = 0; while (line[cursor] == ' ' || line[cursor] == '\t') cursor++; if (line[cursor] == '#' || line[cursor] == '\n') continue; e = xmalloc(sizeof (struct event)); e->etype = CRONEVENT; if (!(((e->of.ct.minute = next_field(0, 59)) != NULL) && ((e->of.ct.hour = next_field(0, 23)) != NULL) && ((e->of.ct.daymon = next_field(1, 31)) != NULL) && ((e->of.ct.month = next_field(1, 12)) != NULL) && ((e->of.ct.dayweek = next_field(0, 6)) != NULL))) { free(e); cte_add(lineno, line); continue; } while (line[cursor] == ' ' || line[cursor] == '\t') cursor++; if (line[cursor] == '\n' || line[cursor] == '\0') continue; /* get the command to execute */ start = cursor; again: while ((line[cursor] != '%') && (line[cursor] != '\n') && (line[cursor] != '\0') && (line[cursor] != '\\')) cursor++; if (line[cursor] == '\\') { cursor += 2; goto again; } e->cmd = xmalloc(cursor-start+1); (void) strncpy(e->cmd, line+start, cursor-start); e->cmd[cursor-start] = '\0'; /* see if there is any standard input */ if (line[cursor] == '%') { e->of.ct.input = xmalloc(strlen(line)-cursor+1); (void) strcpy(e->of.ct.input, line+cursor+1); for (i = 0; i < strlen(e->of.ct.input); i++) { if (e->of.ct.input[i] == '%') e->of.ct.input[i] = '\n'; } } else { e->of.ct.input = NULL; } /* have the event point to it's owner */ e->u = u; /* insert this event at the front of this user's event list */ e->link = u->ctevents; u->ctevents = e; /* set the time for the first occurance of this event */ e->time = next_time(e, reftime); /* finally, add this event to the main event list */ el_add(e, e->time, u->ctid); cte_valid(); #ifdef DEBUG cftime(timebuf, "%C", &e->time); (void) fprintf(stderr, "inserting cron event %s at %ld (%s)\n", e->cmd, e->time, timebuf); #endif } cte_sendmail(u->name); /* mail errors if any to user */ (void) fclose(cf); } /* * Below are the functions for handling of errors in crontabs. Concept is to * collect faulty lines and send one email at the end of the crontab * evaluation. If there are erroneous lines only ((cte_nvalid == 0), evaluation * of crontab is aborted. Otherwise reading of crontab is continued to the end * of the file but no further error logging appears. */ static void cte_init() { if (cte_text == NULL) cte_text = xmalloc(MAILBUFLEN); (void) strlcpy(cte_text, cte_intro, MAILBUFLEN); cte_lp = cte_text + sizeof (cte_intro) - 1; cte_free = MAILBINITFREE; cte_nvalid = 0; } static void cte_add(int lineno, char *ctline) { int len; char *p; if (cte_free >= LINELIMIT) { (void) sprintf(cte_lp, "%4d: ", lineno); (void) strlcat(cte_lp, ctline, LINELIMIT - 1); len = strlen(cte_lp); if (cte_lp[len - 1] != '\n') { cte_lp[len++] = '\n'; cte_lp[len] = '\0'; } for (p = cte_lp; *p; p++) { if (isprint(*p) || *p == '\n' || *p == '\t') continue; *p = '.'; } cte_lp += len; cte_free -= len; if (cte_free < LINELIMIT) { size_t buflen = MAILBUFLEN - (cte_lp - cte_text); (void) strlcpy(cte_lp, cte_trail1, buflen); if (cte_nvalid == 0) (void) strlcat(cte_lp, cte_trail2, buflen); } } } static void cte_valid() { cte_nvalid++; } static int cte_istoomany() { /* * Return TRUE only if all lines are faulty. So evaluation of * a crontab is not aborted if at least one valid line was found. */ return (cte_nvalid == 0 && cte_free < LINELIMIT); } static void cte_sendmail(char *username) { if (cte_free < MAILBINITFREE) mail(username, cte_text, ERR_CRONTABENT); } /* * Send mail with error message to a user */ static void mail(char *usrname, char *mesg, int format) { /* mail mails a user a message. */ FILE *pipe; char *temp; struct passwd *ruser_ids; pid_t fork_val; int saveerrno = errno; struct utsname name; #ifdef TESTING return; #endif (void) uname(&name); if ((fork_val = fork()) == (pid_t)-1) { msg("cron cannot fork\n"); return; } if (fork_val == 0) { child_sigreset(); contract_clear_template(); if ((ruser_ids = getpwnam(usrname)) == NULL) exit(0); (void) setuid(ruser_ids->pw_uid); temp = xmalloc(strlen(MAIL)+strlen(usrname)+2); (void) sprintf(temp, "%s %s", MAIL, usrname); pipe = popen(temp, "w"); if (pipe != NULL) { (void) fprintf(pipe, "To: %s\n", usrname); switch (format) { case ERR_CRONTABENT: (void) fprintf(pipe, CRONTABERR); (void) fprintf(pipe, "Your \"crontab\" on %s\n", name.nodename); (void) fprintf(pipe, mesg); (void) fprintf(pipe, "\nEntries or crontab have been ignored\n"); break; case ERR_UNIXERR: (void) fprintf(pipe, "Subject: %s\n\n", mesg); (void) fprintf(pipe, "The error on %s was \"%s\"\n", name.nodename, errmsg(saveerrno)); break; case ERR_CANTEXECCRON: (void) fprintf(pipe, "Subject: Couldn't run your \"cron\" job\n\n"); (void) fprintf(pipe, "Your \"cron\" job on %s ", name.nodename); (void) fprintf(pipe, "couldn't be run\n"); (void) fprintf(pipe, "%s\n", mesg); (void) fprintf(pipe, "The error was \"%s\"\n", errmsg(saveerrno)); break; case ERR_CANTEXECAT: (void) fprintf(pipe, "Subject: Couldn't run your \"at\" job\n\n"); (void) fprintf(pipe, "Your \"at\" job on %s ", name.nodename); (void) fprintf(pipe, "couldn't be run\n"); (void) fprintf(pipe, "%s\n", mesg); (void) fprintf(pipe, "The error was \"%s\"\n", errmsg(saveerrno)); break; default: break; } (void) pclose(pipe); } free(temp); exit(0); } contract_abandon_latest(fork_val); if (cron_pid == getpid()) { miscpid_insert(fork_val); } } static char * next_field(int lower, int upper) { /* * next_field returns a pointer to a string which holds the next * field of a line of a crontab file. * if (numbers in this field are out of range (lower..upper), * or there is a syntax error) then * NULL is returned, and a mail message is sent to the * user telling him which line the error was in. */ char *s; int num, num2, start; while ((line[cursor] == ' ') || (line[cursor] == '\t')) cursor++; start = cursor; if (line[cursor] == '\0') { return (NULL); } if (line[cursor] == '*') { cursor++; if ((line[cursor] != ' ') && (line[cursor] != '\t')) return (NULL); s = xmalloc(2); (void) strcpy(s, "*"); return (s); } for (;;) { if (!isdigit(line[cursor])) return (NULL); num = 0; do { num = num*10 + (line[cursor]-'0'); } while (isdigit(line[++cursor])); if ((num < lower) || (num > upper)) return (NULL); if (line[cursor] == '-') { if (!isdigit(line[++cursor])) return (NULL); num2 = 0; do { num2 = num2*10 + (line[cursor]-'0'); } while (isdigit(line[++cursor])); if ((num2 < lower) || (num2 > upper)) return (NULL); } if ((line[cursor] == ' ') || (line[cursor] == '\t')) break; if (line[cursor] == '\0') return (NULL); if (line[cursor++] != ',') return (NULL); } s = xmalloc(cursor-start+1); (void) strncpy(s, line+start, cursor-start); s[cursor-start] = '\0'; return (s); } #define tm_cmp(t1, t2) (\ (t1)->tm_year == (t2)->tm_year && \ (t1)->tm_mon == (t2)->tm_mon && \ (t1)->tm_mday == (t2)->tm_mday && \ (t1)->tm_hour == (t2)->tm_hour && \ (t1)->tm_min == (t2)->tm_min) #define tm_setup(tp, yr, mon, dy, hr, min, dst) \ (tp)->tm_year = yr; \ (tp)->tm_mon = mon; \ (tp)->tm_mday = dy; \ (tp)->tm_hour = hr; \ (tp)->tm_min = min; \ (tp)->tm_isdst = dst; \ (tp)->tm_sec = 0; \ (tp)->tm_wday = 0; \ (tp)->tm_yday = 0; /* * modification for bugid 1104537. the second argument to next_time is * now the value of time(2) to be used. if this is 0, then use the * current time. otherwise, the second argument is the time from which to * calculate things. this is useful to correct situations where you've * gone backwards in time (I.e. the system's internal clock is correcting * itself backwards). */ static time_t next_time(struct event *e, time_t tflag) { /* * returns the integer time for the next occurance of event e. * the following fields have ranges as indicated: * PRGM | min hour day of month mon day of week * ------|------------------------------------------------------- * cron | 0-59 0-23 1-31 1-12 0-6 (0=sunday) * time | 0-59 0-23 1-31 0-11 0-6 (0=sunday) * NOTE: this routine is hard to understand. */ struct tm *tm, ref_tm, tmp, tmp1, tmp2; int tm_mon, tm_mday, tm_wday, wday, m, min, h, hr, carry, day, days, d1, day1, carry1, d2, day2, carry2, daysahead, mon, yr, db, wd, today; time_t t, ref_t, t1, t2, zone_start; int fallback; extern int days_btwn(int, int, int, int, int, int); if (tflag == 0) { t = time(NULL); /* original way of doing things */ } else { t = tflag; } tm = &ref_tm; /* use a local variable and call localtime_r() */ ref_t = t; /* keep a copy of the reference time */ recalc: fallback = 0; (void) localtime_r(&t, tm); if (daylight) { tmp = *tm; tmp.tm_isdst = (tm->tm_isdst > 0 ? 0 : 1); t1 = xmktime(&tmp); /* * see if we will have timezone switch over, and clock will * fall back. zone_start will hold the time when it happens * (ie time of PST -> PDT switch over). */ if (tm->tm_isdst != tmp.tm_isdst && (t1 - t) == (timezone - altzone) && tm_cmp(tm, &tmp)) { zone_start = get_switching_time(tmp.tm_isdst, t); fallback = 1; } } tm_mon = next_ge(tm->tm_mon+1, e->of.ct.month) - 1; /* 0-11 */ tm_mday = next_ge(tm->tm_mday, e->of.ct.daymon); /* 1-31 */ tm_wday = next_ge(tm->tm_wday, e->of.ct.dayweek); /* 0-6 */ today = TRUE; if ((strcmp(e->of.ct.daymon, "*") == 0 && tm->tm_wday != tm_wday) || (strcmp(e->of.ct.dayweek, "*") == 0 && tm->tm_mday != tm_mday) || (tm->tm_mday != tm_mday && tm->tm_wday != tm_wday) || (tm->tm_mon != tm_mon)) { today = FALSE; } m = tm->tm_min + (t == ref_t ? 1 : 0); if ((tm->tm_hour + 1) <= next_ge(tm->tm_hour, e->of.ct.hour)) { m = 0; } min = next_ge(m%60, e->of.ct.minute); carry = (min < m) ? 1 : 0; h = tm->tm_hour + carry; hr = next_ge(h%24, e->of.ct.hour); carry = (hr < h) ? 1 : 0; if (carry == 0 && today) { /* this event must occur today */ tm_setup(&tmp, tm->tm_year, tm->tm_mon, tm->tm_mday, hr, min, tm->tm_isdst); tmp1 = tmp; if ((t1 = xmktime(&tmp1)) == (time_t)-1) { return (0); } if (daylight && tmp.tm_isdst != tmp1.tm_isdst) { /* In case we are falling back */ if (fallback) { /* we may need to run the job once more. */ t = zone_start; goto recalc; } /* * In case we are not in falling back period, * calculate the time assuming the DST. If the * date/time is not altered by mktime, it is the * time to execute the job. */ tmp2 = tmp; tmp2.tm_isdst = tmp1.tm_isdst; if ((t1 = xmktime(&tmp2)) == (time_t)-1) { return (0); } if (tmp1.tm_isdst == tmp2.tm_isdst && tm_cmp(&tmp, &tmp2)) { /* * We got a valid time. */ return (t1); } else { /* * If the date does not match even if * we assume the alternate timezone, then * it must be the invalid time. eg * 2am while switching 1:59am to 3am. * t1 should point the time before the * switching over as we've calculate the * time with assuming alternate zone. */ if (tmp1.tm_isdst != tmp2.tm_isdst) { t = get_switching_time(tmp1.tm_isdst, t1); } else { /* does this really happen? */ t = get_switching_time(tmp1.tm_isdst, t1 - abs(timezone - altzone)); } if (t == (time_t)-1) return (0); } goto recalc; } if (tm_cmp(&tmp, &tmp1)) { /* got valid time */ return (t1); } else { /* * This should never happen, but just in * case, we fall back to the old code. */ if (tm->tm_min > min) { t += (time_t)(hr-tm->tm_hour-1) * HOUR + (time_t)(60-tm->tm_min+min) * MINUTE; } else { t += (time_t)(hr-tm->tm_hour) * HOUR + (time_t)(min-tm->tm_min) * MINUTE; } t1 = t; t -= (time_t)tm->tm_sec; (void) localtime_r(&t, &tmp); if ((tm->tm_isdst == 0) && (tmp.tm_isdst > 0)) t -= (timezone - altzone); return ((t <= ref_t) ? t1 : t); } } /* * Job won't run today, however if we have a switch over within * one hour and we will have one hour time drifting back in this * period, we may need to run the job one more time if the job was * set to run on this hour of clock. */ if (fallback) { t = zone_start; goto recalc; } min = next_ge(0, e->of.ct.minute); hr = next_ge(0, e->of.ct.hour); /* * calculate the date of the next occurance of this event, which * will be on a different day than the current */ /* check monthly day specification */ d1 = tm->tm_mday+1; day1 = next_ge((d1-1)%days_in_mon(tm->tm_mon, tm->tm_year)+1, e->of.ct.daymon); carry1 = (day1 < d1) ? 1 : 0; /* check weekly day specification */ d2 = tm->tm_wday+1; wday = next_ge(d2%7, e->of.ct.dayweek); if (wday < d2) daysahead = 7 - d2 + wday; else daysahead = wday - d2; day2 = (d1+daysahead-1)%days_in_mon(tm->tm_mon, tm->tm_year)+1; carry2 = (day2 < d1) ? 1 : 0; /* * based on their respective specifications, day1, and day2 give * the day of the month for the next occurance of this event. */ if ((strcmp(e->of.ct.daymon, "*") == 0) && (strcmp(e->of.ct.dayweek, "*") != 0)) { day1 = day2; carry1 = carry2; } if ((strcmp(e->of.ct.daymon, "*") != 0) && (strcmp(e->of.ct.dayweek, "*") == 0)) { day2 = day1; carry2 = carry1; } yr = tm->tm_year; if ((carry1 && carry2) || (tm->tm_mon != tm_mon)) { /* event does not occur in this month */ m = tm->tm_mon+1; mon = next_ge(m%12+1, e->of.ct.month) - 1; /* 0..11 */ carry = (mon < m) ? 1 : 0; yr += carry; /* recompute day1 and day2 */ day1 = next_ge(1, e->of.ct.daymon); db = days_btwn(tm->tm_mon, tm->tm_mday, tm->tm_year, mon, 1, yr) + 1; wd = (tm->tm_wday+db)%7; /* wd is the day of the week of the first of month mon */ wday = next_ge(wd, e->of.ct.dayweek); if (wday < wd) day2 = 1 + 7 - wd + wday; else day2 = 1 + wday - wd; if ((strcmp(e->of.ct.daymon, "*") != 0) && (strcmp(e->of.ct.dayweek, "*") == 0)) day2 = day1; if ((strcmp(e->of.ct.daymon, "*") == 0) && (strcmp(e->of.ct.dayweek, "*") != 0)) day1 = day2; day = (day1 < day2) ? day1 : day2; } else { /* event occurs in this month */ mon = tm->tm_mon; if (!carry1 && !carry2) day = (day1 < day2) ? day1 : day2; else if (!carry1) day = day1; else day = day2; } /* * now that we have the min, hr, day, mon, yr of the next event, * figure out what time that turns out to be. */ tm_setup(&tmp, yr, mon, day, hr, min, -1); tmp2 = tmp; if ((t1 = xmktime(&tmp2)) == (time_t)-1) { return (0); } if (tm_cmp(&tmp, &tmp2)) { /* * mktime returns clock for the current time zone. If the * target date was in fallback period, it needs to be adjusted * to the time comes first. * Suppose, we are at Jan and scheduling job at 1:30am10/26/03. * mktime returns the time in PST, but 1:30am in PDT comes * first. So reverse the tm_isdst, and see if we have such * time/date. */ if (daylight) { int dst = tmp2.tm_isdst; tmp2 = tmp; tmp2.tm_isdst = (dst > 0 ? 0 : 1); if ((t2 = xmktime(&tmp2)) == (time_t)-1) { return (0); } if (tm_cmp(&tmp, &tmp2)) { /* * same time/date found in the opposite zone. * check the clock to see which comes early. */ if (t2 > ref_t && t2 < t1) { t1 = t2; } } } return (t1); } else { /* * mktime has set different time/date for the given date. * This means that the next job is scheduled to be run on the * invalid time. There are three possible invalid date/time. * 1. Non existing day of the month. such as April 31th. * 2. Feb 29th in the non-leap year. * 3. Time gap during the DST switch over. */ d1 = days_in_mon(mon, yr); if ((mon != 1 && day > d1) || (mon == 1 && day > 29)) { /* * see if we have got a specific date which * is invalid. */ if (strcmp(e->of.ct.dayweek, "*") == 0 && mon == (next_ge((mon+1)%12+1, e->of.ct.month)-1) && day <= next_ge(1, e->of.ct.daymon)) { /* job never run */ return (0); } /* * Since the day has gone invalid, we need to go to * next month, and recalcuate the first occurrence. * eg the cron tab such as: * 0 0 1,15,31 1,2,3,4,5 * /usr/bin.... * 2/31 is invalid, so the next job is 3/1. */ tmp2 = tmp; tmp2.tm_min = 0; tmp2.tm_hour = 0; tmp2.tm_mday = 1; /* 1st day of the month */ if (mon == 11) { tmp2.tm_mon = 0; tmp2.tm_year = yr + 1; } else { tmp2.tm_mon = mon + 1; } if ((t = xmktime(&tmp2)) == (time_t)-1) { return (0); } } else if (mon == 1 && day > d1) { /* * ie 29th in the non-leap year. Forwarding the * clock to Feb 29th 00:00 (March 1st), and recalculate * the next time. */ tmp2 = tmp; tmp2.tm_min = 0; tmp2.tm_hour = 0; if ((t = xmktime(&tmp2)) == (time_t)-1) { return (0); } } else if (daylight) { /* * Non existing time, eg 2am PST during summer time * switch. * We need to get the correct isdst which we are * swithing to, by adding time difference to make sure * that t2 is in the zone being switched. */ t2 = t1; t2 += abs(timezone - altzone); (void) localtime_r(&t2, &tmp2); zone_start = get_switching_time(tmp2.tm_isdst, t1 - abs(timezone - altzone)); if (zone_start == (time_t)-1) { return (0); } t = zone_start; } else { /* * This should never happen, but fall back to the * old code. */ days = days_btwn(tm->tm_mon, tm->tm_mday, tm->tm_year, mon, day, yr); t += (time_t)(23-tm->tm_hour)*HOUR + (time_t)(60-tm->tm_min)*MINUTE + (time_t)hr*HOUR + (time_t)min*MINUTE + (time_t)days*DAY; t1 = t; t -= (time_t)tm->tm_sec; (void) localtime_r(&t, &tmp); if ((tm->tm_isdst == 0) && (tmp.tm_isdst > 0)) t -= (timezone - altzone); return (t <= ref_t ? t1 : t); } goto recalc; } /*NOTREACHED*/ } /* * This returns TOD in time_t that zone switch will happen, and this * will be called when clock fallback is about to happen. * (ie 30minutes before the time of PST -> PDT switch. 2:00 AM PST * will fall back to 1:00 PDT. So this function will be called only * for the time between 1:00 AM PST and 2:00 PST(1:00 PST)). * First goes through the common time differences to see if zone * switch happens at those minutes later. If not, check every minutes * until 6 hours ahead see if it happens(We might have 45minutes * fallback). */ static time_t get_switching_time(int to_dst, time_t t_ref) { time_t t, t1; struct tm tmp, tmp1; int hints[] = { 60, 120, 30, 90, 0}; /* minutes */ int i; (void) localtime_r(&t_ref, &tmp); tmp1 = tmp; tmp1.tm_sec = 0; tmp1.tm_min = 0; if ((t = xmktime(&tmp1)) == (time_t)-1) return ((time_t)-1); /* fast path */ for (i = 0; hints[i] != 0; i++) { t1 = t + hints[i] * 60; (void) localtime_r(&t1, &tmp1); if (tmp1.tm_isdst == to_dst) { t1--; (void) localtime_r(&t1, &tmp1); if (tmp1.tm_isdst != to_dst) { return (t1 + 1); } } } /* ugly, but don't know other than this. */ tmp1 = tmp; tmp1.tm_sec = 0; if ((t = xmktime(&tmp1)) == (time_t)-1) return ((time_t)-1); while (t < (t_ref + 6*60*60)) { /* 6 hours should be enough */ t += 60; /* at least one minute, I assume */ (void) localtime_r(&t, &tmp); if (tmp.tm_isdst == to_dst) return (t); } return ((time_t)-1); } static time_t xmktime(struct tm *tmp) { time_t ret; if ((ret = mktime(tmp)) == (time_t)-1) { if (errno == EOVERFLOW) { return ((time_t)-1); } crabort("internal error: mktime failed", REMOVE_FIFO|CONSOLE_MSG); } return (ret); } #define DUMMY 100 static int next_ge(int current, char *list) { /* * list is a character field as in a crontab file; * for example: "40, 20, 50-10" * next_ge returns the next number in the list that is * greater than or equal to current. if no numbers of list * are >= current, the smallest element of list is returned. * NOTE: current must be in the appropriate range. */ char *ptr; int n, n2, min, min_gt; if (strcmp(list, "*") == 0) return (current); ptr = list; min = DUMMY; min_gt = DUMMY; for (;;) { if ((n = (int)num(&ptr)) == current) return (current); if (n < min) min = n; if ((n > current) && (n < min_gt)) min_gt = n; if (*ptr == '-') { ptr++; if ((n2 = (int)num(&ptr)) > n) { if ((current > n) && (current <= n2)) return (current); } else { /* range that wraps around */ if (current > n) return (current); if (current <= n2) return (current); } } if (*ptr == '\0') break; ptr += 1; } if (min_gt != DUMMY) return (min_gt); else return (min); } static void free_if_unused(struct usr *u) { struct usr *cur, *prev; /* * To make sure a usr structure is idle we must check that * there are no at jobs queued for the user; the user does * not have a crontab, and also that there are no running at * or cron jobs (since the runinfo structure also has a * pointer to the usr structure). */ if (!u->ctexists && u->atevents == NULL && u->cruncnt == 0 && u->aruncnt == 0) { #ifdef DEBUG (void) fprintf(stderr, "%s removed from usr list\n", u->name); #endif for (cur = uhead, prev = NULL; cur != u; prev = cur, cur = cur->nextusr) { if (cur == NULL) { return; } } if (prev == NULL) uhead = u->nextusr; else prev->nextusr = u->nextusr; free(u->name); free(u->home); free(u); } } static void del_atjob(char *name, char *usrname) { struct event *e, *eprev; struct usr *u; if ((u = find_usr(usrname)) == NULL) return; e = u->atevents; eprev = NULL; while (e != NULL) { if (strcmp(name, e->cmd) == 0) { if (next_event == e) next_event = NULL; if (eprev == NULL) u->atevents = e->link; else eprev->link = e->link; el_remove(e->of.at.eventid, 1); free(e->cmd); free(e); break; } else { eprev = e; e = e->link; } } free_if_unused(u); } static void del_ctab(char *name) { struct usr *u; if ((u = find_usr(name)) == NULL) return; rm_ctevents(u); el_remove(u->ctid, 0); u->ctid = 0; u->ctexists = 0; free_if_unused(u); } static void rm_ctevents(struct usr *u) { struct event *e2, *e3; /* * see if the next event (to be run by cron) is a cronevent * owned by this user. */ if ((next_event != NULL) && (next_event->etype == CRONEVENT) && (next_event->u == u)) { next_event = NULL; } e2 = u->ctevents; while (e2 != NULL) { free(e2->cmd); free(e2->of.ct.minute); free(e2->of.ct.hour); free(e2->of.ct.daymon); free(e2->of.ct.month); free(e2->of.ct.dayweek); if (e2->of.ct.input != NULL) free(e2->of.ct.input); e3 = e2->link; free(e2); e2 = e3; } u->ctevents = NULL; } static struct usr * find_usr(char *uname) { struct usr *u; u = uhead; while (u != NULL) { if (strcmp(u->name, uname) == 0) return (u); u = u->nextusr; } return (NULL); } /* * Execute cron command or at/batch job. * If ever a premature return is added to this function pay attention to * free at_cmdfile and outfile plus jobname buffers of the runinfo structure. */ static int ex(struct event *e) { int r; int fd; pid_t rfork; FILE *atcmdfp; char mailvar[4]; char *at_cmdfile = NULL; struct stat buf; struct queue *qp; struct runinfo *rp; struct project proj, *pproj = NULL; char mybuf[PROJECT_BUFSZ]; char mybuf2[PROJECT_BUFSZ]; char *tmpfile; FILE *fptr; time_t dhltime; projid_t projid; int projflag = 0; qp = &qt[e->etype]; /* set pointer to queue defs */ if (qp->nrun >= qp->njob) { msg("%c queue max run limit reached", e->etype+'a'); resched(qp->nwait); return (0); } if ((rp = rinfo_get(0)) == NULL) { msg("MAXRUN (%d) procs reached", MAXRUN); resched(qp->nwait); return (0); } #ifdef ATLIMIT if ((e->u)->uid != 0 && (e->u)->aruncnt >= ATLIMIT) { msg("ATLIMIT (%d) reached for uid %d", ATLIMIT, (e->u)->uid); rinfo_free(rp); resched(qp->nwait); return (0); } #endif #ifdef CRONLIMIT if ((e->u)->uid != 0 && (e->u)->cruncnt >= CRONLIMIT) { msg("CRONLIMIT (%d) reached for uid %d", CRONLIMIT, (e->u)->uid); rinfo_free(rp); resched(qp->nwait); return (0); } #endif if ((e->u)->uid == 0) { /* set default path */ /* path settable in defaults file */ envinit[2] = supath; } else { envinit[2] = path; } /* * the tempnam() function uses malloc(3C) to allocate space for the * constructed file name, and returns a pointer to this area, which * is assigned to rp->outfile. Here rp->outfile is not overwritten. */ rp->outfile = tempnam(TMPDIR, PFX); rp->jobtype = e->etype; if (e->etype == CRONEVENT) { rp->jobname = xmalloc(strlen(e->cmd)+1); (void) strcpy(rp->jobname, e->cmd); /* "cron" jobs only produce mail if there's output */ rp->mailwhendone = 0; } else { at_cmdfile = xmalloc(strlen(ATDIR)+strlen(e->cmd)+2); (void) sprintf(at_cmdfile, "%s/%s", ATDIR, e->cmd); if ((atcmdfp = fopen(at_cmdfile, "r")) == NULL) { if (errno == ENAMETOOLONG) { if (chdir(ATDIR) == 0) cron_unlink(e->cmd); } else { cron_unlink(at_cmdfile); } mail((e->u)->name, BADJOBOPEN, ERR_CANTEXECAT); free(at_cmdfile); rinfo_free(rp); return (0); } rp->jobname = xmalloc(strlen(at_cmdfile)+1); (void) strcpy(rp->jobname, at_cmdfile); /* * Skip over the first two lines. */ (void) fscanf(atcmdfp, "%*[^\n]\n"); (void) fscanf(atcmdfp, "%*[^\n]\n"); if (fscanf(atcmdfp, ": notify by mail: %3s%*[^\n]\n", mailvar) == 1) { /* * Check to see if we should always send mail * to the owner. */ rp->mailwhendone = (strcmp(mailvar, "yes") == 0); } else { rp->mailwhendone = 0; } if (fscanf(atcmdfp, "\n: project: %d\n", &projid) == 1) { projflag = 1; } (void) fclose(atcmdfp); } /* * we make sure that the system time * hasn't drifted backwards. if it has, el_add() is now * called, to make sure that the event queue is back in order, * and we set the delayed flag. cron will pick up the request * later on at the proper time. */ dhltime = time(NULL); if ((dhltime - e->time) < 0) { msg("clock time drifted backwards!\n"); if (next_event->etype == CRONEVENT) { msg("correcting cron event\n"); next_event->time = next_time(next_event, dhltime); el_add(next_event, next_event->time, (next_event->u)->ctid); } else { /* etype == ATEVENT */ msg("correcting batch event\n"); el_add(next_event, next_event->time, next_event->of.at.eventid); } delayed++; t_old = time(NULL); free(at_cmdfile); rinfo_free(rp); return (0); } if ((rfork = fork()) == (pid_t)-1) { reap_child(); if ((rfork = fork()) == (pid_t)-1) { msg("cannot fork"); free(at_cmdfile); rinfo_free(rp); resched(60); (void) sleep(30); return (0); } } if (rfork) { /* parent process */ contract_abandon_latest(rfork); ++qp->nrun; rp->pid = rfork; rp->que = e->etype; if (e->etype != CRONEVENT) (e->u)->aruncnt++; else (e->u)->cruncnt++; rp->rusr = (e->u); logit(BCHAR, rp, 0); free(at_cmdfile); return (0); } child_sigreset(); contract_clear_template(); if (e->etype != CRONEVENT) { /* open jobfile as stdin to shell */ if (stat(at_cmdfile, &buf)) { if (errno == ENAMETOOLONG) { if (chdir(ATDIR) == 0) cron_unlink(e->cmd); } else cron_unlink(at_cmdfile); mail((e->u)->name, BADJOBOPEN, ERR_CANTEXECCRON); exit(1); } if (!(buf.st_mode&ISUID)) { /* * if setuid bit off, original owner has * given this file to someone else */ cron_unlink(at_cmdfile); exit(1); } if ((fd = open(at_cmdfile, O_RDONLY)) == -1) { mail((e->u)->name, BADJOBOPEN, ERR_CANTEXECCRON); cron_unlink(at_cmdfile); exit(1); } if (fd != 0) { (void) dup2(fd, 0); (void) close(fd); } /* * retrieve the project id of the at job and convert it * to a project name. fail if it's not a valid project * or if the user isn't a member of the project. */ if (projflag == 1) { if ((pproj = getprojbyid(projid, &proj, (void *)&mybuf, sizeof (mybuf))) == NULL || !inproj(e->u->name, pproj->pj_name, mybuf2, sizeof (mybuf2))) { cron_unlink(at_cmdfile); mail((e->u)->name, BADPROJID, ERR_CANTEXECAT); exit(1); } } } /* * Put process in a new session, and create a new task. */ if (setsid() < 0) { msg("setsid failed with errno = %d. job failed (%s)" " for user %s", errno, e->cmd, e->u->name); if (e->etype != CRONEVENT) cron_unlink(at_cmdfile); exit(1); } /* * set correct user identification and check his account */ r = set_user_cred(e->u, pproj); if (r == VUC_EXPIRED) { msg("user (%s) account is expired", e->u->name); audit_cron_user_acct_expired(e->u->name); clean_out_user(e->u); exit(1); } if (r == VUC_NEW_AUTH) { msg("user (%s) password has expired", e->u->name); audit_cron_user_acct_expired(e->u->name); clean_out_user(e->u); exit(1); } if (r != VUC_OK) { msg("bad user (%s)", e->u->name); audit_cron_bad_user(e->u->name); clean_out_user(e->u); exit(1); } /* * check user and initialize the supplementary group access list. * bugid 1230784: deleted from parent to avoid cron hang. Now * only child handles the call. */ if (verify_user_cred(e->u) != VUC_OK || setgid(e->u->gid) == -1 || initgroups(e->u->name, e->u->gid) == -1) { msg("bad user (%s) or setgid failed (%s)", e->u->name, e->u->name); audit_cron_bad_user(e->u->name); clean_out_user(e->u); exit(1); } if (e->etype != CRONEVENT) { r = audit_cron_session(e->u->name, NULL, e->u->uid, e->u->gid, at_cmdfile); cron_unlink(at_cmdfile); } else { r = audit_cron_session(e->u->name, CRONDIR, e->u->uid, e->u->gid, NULL); } if (r != 0) { msg("cron audit problem. job failed (%s) for user %s", e->cmd, e->u->name); exit(1); } audit_cron_new_job(e->cmd, e->etype, (void *)e); if (setuid(e->u->uid) == -1) { msg("setuid failed (%s)", e->u->name); clean_out_user(e->u); exit(1); } if (e->etype == CRONEVENT) { /* check for standard input to command */ if (e->of.ct.input != NULL) { if ((tmpfile = strdup(TMPINFILE)) == NULL) { mail((e->u)->name, MALLOCERR, ERR_CANTEXECCRON); exit(1); } if ((fd = mkstemp(tmpfile)) == -1 || (fptr = fdopen(fd, "w")) == NULL) { mail((e->u)->name, NOSTDIN, ERR_CANTEXECCRON); cron_unlink(tmpfile); free(tmpfile); exit(1); } if ((fwrite(e->of.ct.input, sizeof (char), strlen(e->of.ct.input), fptr)) != strlen(e->of.ct.input)) { mail((e->u)->name, NOSTDIN, ERR_CANTEXECCRON); cron_unlink(tmpfile); free(tmpfile); (void) close(fd); (void) fclose(fptr); exit(1); } if (fseek(fptr, (off_t)0, SEEK_SET) != -1) { if (fd != 0) { (void) dup2(fd, 0); (void) close(fd); } } cron_unlink(tmpfile); free(tmpfile); (void) fclose(fptr); } else if ((fd = open("/dev/null", O_RDONLY)) > 0) { (void) dup2(fd, 0); (void) close(fd); } } /* redirect stdout and stderr for the shell */ if ((fd = open(rp->outfile, O_WRONLY|O_CREAT|O_EXCL, OUTMODE)) == 1) fd = open("/dev/null", O_WRONLY); if (fd >= 0 && fd != 1) (void) dup2(fd, 1); if (fd >= 0 && fd != 2) { (void) dup2(fd, 2); if (fd != 1) (void) close(fd); } (void) strlcat(homedir, (e->u)->home, sizeof (homedir)); (void) strlcat(logname, (e->u)->name, sizeof (logname)); environ = envinit; if (chdir((e->u)->home) == -1) { mail((e->u)->name, CANTCDHOME, e->etype == CRONEVENT ? ERR_CANTEXECCRON : ERR_CANTEXECAT); exit(1); } #ifdef TESTING exit(1); #endif /* * make sure that all file descriptors EXCEPT 0, 1 and 2 * will be closed. */ closefrom(3); if ((e->u)->uid != 0) (void) nice(qp->nice); if (e->etype == CRONEVENT) (void) execl(SHELL, "sh", "-c", e->cmd, 0); else /* type == ATEVENT */ (void) execl(SHELL, "sh", 0); mail((e->u)->name, CANTEXECSH, e->etype == CRONEVENT ? ERR_CANTEXECCRON : ERR_CANTEXECAT); exit(1); /*NOTREACHED*/ } static int idle(long t) { time_t now; while (t > 0L) { if (msg_wait(t) != 0) { /* we need to run next job immediately */ return (0); } reap_child(); now = time(NULL); if (last_time > now) { /* clock has been reset */ return (1); } if (next_event == NULL && !el_empty()) { next_event = (struct event *)el_first(); } if (next_event == NULL) t = INFINITY; else t = (long)next_event->time - now; } return (0); } /* * This used to be in the idle(), but moved to the separate function. * This called from various place when cron needs to reap the * child. It includes the situation that cron hit maxrun, and needs * to reschedule the job. */ static void reap_child() { pid_t pid; int prc; struct runinfo *rp; for (;;) { pid = waitpid((pid_t)-1, &prc, WNOHANG); if (pid <= 0) break; #ifdef DEBUG fprintf(stderr, "wait returned %x for process %d\n", prc, pid); #endif if ((rp = rinfo_get(pid)) == NULL) { if (miscpid_delete(pid) == 0) { /* not found in anywhere */ msg(PIDERR, pid); } } else if (rp->que == ZOMB) { (void) unlink(rp->outfile); rinfo_free(rp); } else { cleanup(rp, prc); } } } static void cleanup(struct runinfo *pr, int rc) { int nextfork = 1; struct usr *p; struct stat buf; logit(ECHAR, pr, rc); --qt[pr->que].nrun; p = pr->rusr; if (pr->que != CRONEVENT) --p->aruncnt; else --p->cruncnt; if (!lstat(pr->outfile, &buf)) { if ((buf.st_mode != S_IFLNK) && (buf.st_size > 0 || pr->mailwhendone)) { /* mail user stdout and stderr */ for (;;) { if ((pr->pid = fork()) < 0) { /* * if fork fails try forever in doubling * retry times, up to 16 seconds */ (void) sleep(nextfork); if (nextfork < 16) nextfork += nextfork; continue; } else if (pr->pid == 0) { child_sigreset(); contract_clear_template(); mail_result(p, pr, buf.st_size); /* NOTREACHED */ } else { contract_abandon_latest(pr->pid); pr->que = ZOMB; break; } } } else { (void) unlink(pr->outfile); rinfo_free(pr); } } else { rinfo_free(pr); } free_if_unused(p); } /* * Mail stdout and stderr of a job to user. Get uid for real user and become * that person. We do this so that mail won't come from root since this * could be a security hole. If failure, quit - don't send mail as root. */ static void mail_result(struct usr *p, struct runinfo *pr, size_t filesize) { struct passwd *ruser_ids; FILE *mailpipe; FILE *st; struct utsname name; int nbytes; char iobuf[BUFSIZ]; char *cmd; (void) uname(&name); if ((ruser_ids = getpwnam(p->name)) == NULL) exit(0); (void) setuid(ruser_ids->pw_uid); cmd = xmalloc(strlen(MAIL)+strlen(p->name)+2); (void) sprintf(cmd, "%s %s", MAIL, p->name); mailpipe = popen(cmd, "w"); contract_abandon_latest(0); free(cmd); if (mailpipe == NULL) exit(127); (void) fprintf(mailpipe, "To: %s\n", p->name); if (pr->jobtype == CRONEVENT) { (void) fprintf(mailpipe, CRONOUT); (void) fprintf(mailpipe, "Your \"cron\" job on %s\n", name.nodename); if (pr->jobname != NULL) { (void) fprintf(mailpipe, "%s\n\n", pr->jobname); } } else { (void) fprintf(mailpipe, "Subject: Output from \"at\" job\n\n"); (void) fprintf(mailpipe, "Your \"at\" job on %s\n", name.nodename); if (pr->jobname != NULL) { (void) fprintf(mailpipe, "\"%s\"\n\n", pr->jobname); } } /* Tmp. file is fopen'ed w/ "r", secure open */ if (filesize > 0 && (st = fopen(pr->outfile, "r")) != NULL) { (void) fprintf(mailpipe, "produced the following output:\n\n"); while ((nbytes = fread(iobuf, sizeof (char), BUFSIZ, st)) != 0) (void) fwrite(iobuf, sizeof (char), nbytes, mailpipe); (void) fclose(st); } else { (void) fprintf(mailpipe, "completed.\n"); } (void) pclose(mailpipe); exit(0); } static int msg_wait(long tim) { struct message msg; int cnt; time_t reftime; struct pollfd pfd[2]; int64_t tl; int timeout; static int pending_msg; static time_t pending_reftime; if (pending_msg) { process_msg(&msgbuf, pending_reftime); pending_msg = 0; return (0); } /* * We are opening the signal mask to receive SIGCLD. The notifypipe * is used to avoid race condition between SIGCLD and poll system * call. * If SIGCLD is delivered in poll(), poll will be interrupted, and * we will return to idle() to reap the dead children. * If SIGCLD is delivered between sigprocmask() below and poll(), * there is no way we can detect the SIGCLD because poll() won't * be interrupted. In such case, the dead children can't be wait'ed * until poll returns by timeout or a new job. To avoid this race * condition, child_handler write to the notifypipe, so that * poll() will be able to return with POLLIN which indicates that * we have received SIGCLD. * * Since the notifypipe is used to just let poll return from * system call, the data in the pipe won't be read. Therefore, * any data in the pipe needs to be flushed before opening signal * mask. * * Note that we can probably re-write this code with pselect() * which can handle this situation easily. */ (void) ioctl(notifypipe[0], I_FLUSH, FLUSHW); pfd[0].fd = msgfd; pfd[0].events = POLLIN; pfd[1].fd = notifypipe[1]; pfd[1].events = POLLIN; #ifdef CRON_MAXSLEEP /* * CRON_MAXSLEEP can be defined to have cron periodically wake * up, so that cron can detect a change of TOD and adjust the * sleep time accordingly. */ tim = (tim > CRON_MAXSLEEP) ? CRON_MAXSLEEP : tim; #endif tl = (tim == INFINITY) ? -1ll : (int64_t)tim * 1000; accept_sigcld = 1; (void) sigprocmask(SIG_UNBLOCK, &childmask, NULL); do { timeout = (tl > INT_MAX ? INT_MAX : (int)tl); tl -= timeout; cnt = poll(pfd, 2, timeout); if (cnt == -1 && errno != EINTR) { perror("! poll"); } } while (tl > 0 && cnt == 0); (void) sigprocmask(SIG_BLOCK, &childmask, NULL); accept_sigcld = 0; /* * poll timeout or interrupted. */ if (cnt <= 0) return (0); /* * Not the timeout or new job, but a SIGCLD has been delivered. */ if ((pfd[0].revents & POLLIN) == 0) return (0); errno = 0; if ((cnt = read(msgfd, &msg, sizeof (msg))) != sizeof (msg)) { if (cnt != -1 || errno != EAGAIN) perror("! read"); return (0); } reftime = time(NULL); if (next_event != NULL && reftime >= next_event->time) { /* * we need to run the job before reloading crontab. */ (void) memcpy(&msgbuf, &msg, sizeof (msg)); pending_msg = 1; pending_reftime = reftime; return (1); } process_msg(&msg, reftime); return (0); } /* * process the message supplied via pipe. This will be called either * immediately after cron read the message from pipe, or idle time * if the message was pending due to the job execution. */ static void process_msg(struct message *pmsg, time_t reftime) { if (pmsg->etype == NULL) return; switch (pmsg->etype) { case AT: if (pmsg->action == DELETE) del_atjob(pmsg->fname, pmsg->logname); else mod_atjob(pmsg->fname, (time_t)0); break; case CRON: if (pmsg->action == DELETE) del_ctab(pmsg->fname); else mod_ctab(pmsg->fname, reftime); break; default: msg("message received - bad format"); break; } if (next_event != NULL) { if (next_event->etype == CRONEVENT) el_add(next_event, next_event->time, (next_event->u)->ctid); else /* etype == ATEVENT */ el_add(next_event, next_event->time, next_event->of.at.eventid); next_event = NULL; } (void) fflush(stdout); pmsg->etype = NULL; } static struct runinfo * rinfo_get(pid_t pid) { struct runinfo *rp; for (rp = rt; rp < rt+MAXRUN; rp++) { if (rp->pid == pid) break; } if (rp >= rt+MAXRUN) return (NULL); else return (rp); } /* * Free memory used for output file name and job name in runinfo structure */ static void rinfo_free(struct runinfo *rp) { free(rp->outfile); free(rp->jobname); rp->outfile = rp->jobname = NULL; rp->pid = 0; } /* ARGSUSED */ static void thaw_handler(int sig) { ; } /* ARGSUSED */ static void cronend(int sig) { crabort("SIGTERM", REMOVE_FIFO); } /*ARGSUSED*/ static void child_handler(int sig) { /* * Just in case someone changes the signal mask. * we don't want to notify the SIGCLD. */ if (accept_sigcld) { (void) write(notifypipe[0], &sig, 1); } } static void child_sigreset(void) { (void) signal(SIGCLD, SIG_DFL); (void) sigprocmask(SIG_SETMASK, &defmask, NULL); } /* * crabort() - handle exits out of cron */ static void crabort(char *mssg, int action) { int c; if (action & REMOVE_FIFO) { /* FIFO vanishes when cron finishes */ if (unlink(FIFO) < 0) perror("cron could not unlink FIFO"); } if (action & CONSOLE_MSG) { /* write error msg to console */ if ((c = open(CONSOLE, O_WRONLY)) >= 0) { (void) write(c, "cron aborted: ", 14); (void) write(c, mssg, strlen(mssg)); (void) write(c, "\n", 1); (void) close(c); } } /* always log the message */ msg(mssg); msg("******* CRON ABORTED ********"); exit(1); } /* * msg() - time-stamped error reporting function */ /*PRINTFLIKE1*/ static void msg(char *fmt, ...) { va_list args; time_t t; t = time(NULL); (void) fflush(stdout); (void) fprintf(stderr, "! "); va_start(args, fmt); (void) vfprintf(stderr, fmt, args); va_end(args); (void) strftime(timebuf, sizeof (timebuf), FORMAT, localtime(&t)); (void) fprintf(stderr, " %s\n", timebuf); (void) fflush(stderr); } static void logit(int cc, struct runinfo *rp, int rc) { time_t t; int ret; if (!log) return; t = time(NULL); if (cc == BCHAR) (void) printf("%c CMD: %s\n", cc, next_event->cmd); (void) strftime(timebuf, sizeof (timebuf), FORMAT, localtime(&t)); (void) printf("%c %.8s %u %c %s", cc, (rp->rusr)->name, rp->pid, QUE(rp->que), timebuf); if ((ret = TSTAT(rc)) != 0) (void) printf(" ts=%d", ret); if ((ret = RCODE(rc)) != 0) (void) printf(" rc=%d", ret); (void) putchar('\n'); (void) fflush(stdout); } static void resched(int delay) { time_t nt; /* run job at a later time */ nt = next_event->time + delay; if (next_event->etype == CRONEVENT) { next_event->time = next_time(next_event, (time_t)0); if (nt < next_event->time) next_event->time = nt; el_add(next_event, next_event->time, (next_event->u)->ctid); delayed = 1; msg("rescheduling a cron job"); return; } add_atevent(next_event->u, next_event->cmd, nt, next_event->etype); msg("rescheduling at job"); } static void quedefs(int action) { int i; int j; char qbuf[QBUFSIZ]; FILE *fd; /* set up default queue definitions */ for (i = 0; i < NQUEUE; i++) { qt[i].njob = qd.njob; qt[i].nice = qd.nice; qt[i].nwait = qd.nwait; } if (action == DEFAULT) return; if ((fd = fopen(QUEDEFS, "r")) == NULL) { msg("cannot open quedefs file"); msg("using default queue definitions"); return; } while (fgets(qbuf, QBUFSIZ, fd) != NULL) { if ((j = qbuf[0]-'a') < 0 || j >= NQUEUE || qbuf[1] != '.') continue; parsqdef(&qbuf[2]); qt[j].njob = qq.njob; qt[j].nice = qq.nice; qt[j].nwait = qq.nwait; } (void) fclose(fd); } static void parsqdef(char *name) { int i; qq = qd; while (*name) { i = 0; while (isdigit(*name)) { i *= 10; i += *name++ - '0'; } switch (*name++) { case JOBF: qq.njob = i; break; case NICEF: qq.nice = i; break; case WAITF: qq.nwait = i; break; } } } /* * defaults - read defaults from /etc/default/cron */ static void defaults() { int flags; char *deflog; char *hz, *tz; /* * get HZ value for environment */ if ((hz = getenv("HZ")) == (char *)NULL) (void) sprintf(hzname, "HZ=%d", HZ); else (void) snprintf(hzname, sizeof (hzname), "HZ=%s", hz); /* * get TZ value for environment */ (void) snprintf(tzone, sizeof (tzone), "TZ=%s", ((tz = getenv("TZ")) != NULL) ? tz : DEFTZ); if (defopen(DEFFILE) == 0) { /* ignore case */ flags = defcntl(DC_GETFLAGS, 0); TURNOFF(flags, DC_CASE); (void) defcntl(DC_SETFLAGS, flags); if (((deflog = defread("CRONLOG=")) == NULL) || (*deflog == 'N') || (*deflog == 'n')) log = 0; else log = 1; /* fix for 1087611 - allow paths to be set in defaults file */ if ((Def_path = defread("PATH=")) != NULL) { (void) strlcat(path, Def_path, LINE_MAX); } else { (void) strlcpy(path, NONROOTPATH, LINE_MAX); } if ((Def_supath = defread("SUPATH=")) != NULL) { (void) strlcat(supath, Def_supath, LINE_MAX); } else { (void) strlcpy(supath, ROOTPATH, LINE_MAX); } (void) defopen(NULL); } } /* * Determine if a user entry for a job is still ok. The method used here * is a lot (about 75x) faster than using setgrent() / getgrent() * endgrent(). It should be safe because we use the sysconf to determine * the max, and it tolerates the max being 0. */ static int verify_user_cred(const struct usr *u) { struct passwd *pw; size_t numUsrGrps = 0; size_t numOrigGrps = 0; size_t i; int retval; /* * Maximum number of groups a user may be in concurrently. This * is a value which we obtain at runtime through a sysconf() * call. */ static size_t nGroupsMax = (size_t)-1; /* * Arrays for cron user's group list, constructed at startup to * be nGroupsMax elements long, used for verifying user * credentials prior to execution. */ static gid_t *UsrGrps; static gid_t *OrigGrps; if (((pw = getpwnam(u->name)) == NULL) || (pw->pw_uid != u->uid)) { return (VUC_BADUSER); } /* * Create the group id lists needed for job credential * verification. */ if (nGroupsMax == (size_t)-1) { if ((nGroupsMax = sysconf(_SC_NGROUPS_MAX)) > 0) { UsrGrps = xcalloc(nGroupsMax, sizeof (gid_t)); OrigGrps = xcalloc(nGroupsMax, sizeof (gid_t)); } #ifdef DEBUG (void) fprintf(stderr, "nGroupsMax = %ld\n", nGroupsMax); #endif } #ifdef DEBUG (void) fprintf(stderr, "verify_user_cred (%s-%d)\n", pw->pw_name, pw->pw_uid); (void) fprintf(stderr, "verify_user_cred: pw->pw_gid = %d, " "u->gid = %d\n", pw->pw_gid, u->gid); #endif retval = (u->gid == pw->pw_gid) ? VUC_OK : VUC_NOTINGROUP; if (nGroupsMax > 0) { numOrigGrps = getgroups(nGroupsMax, OrigGrps); (void) initgroups(pw->pw_name, pw->pw_gid); numUsrGrps = getgroups(nGroupsMax, UsrGrps); for (i = 0; i < numUsrGrps; i++) { if (UsrGrps[i] == u->gid) { retval = VUC_OK; break; } } if (OrigGrps) { (void) setgroups(numOrigGrps, OrigGrps); } } #ifdef DEBUG (void) fprintf(stderr, "verify_user_cred: VUC = %d\n", retval); #endif return (retval); } static int set_user_cred(const struct usr *u, struct project *pproj) { static char *progname = "cron"; int r = 0, rval = 0; if ((r = pam_start(progname, u->name, &pam_conv, &pamh)) != PAM_SUCCESS) { #ifdef DEBUG msg("pam_start returns %d\n", r); #endif rval = VUC_BADUSER; goto set_eser_cred_exit; } r = pam_acct_mgmt(pamh, 0); #ifdef DEBUG msg("pam_acc_mgmt returns %d\n", r); #endif if (r == PAM_ACCT_EXPIRED) { rval = VUC_EXPIRED; goto set_eser_cred_exit; } if (r == PAM_NEW_AUTHTOK_REQD) { rval = VUC_NEW_AUTH; goto set_eser_cred_exit; } if (r != PAM_SUCCESS) { rval = VUC_BADUSER; goto set_eser_cred_exit; } if (pproj != NULL) { size_t sz = sizeof (PROJECT) + strlen(pproj->pj_name); char *buf = alloca(sz); (void) snprintf(buf, sz, PROJECT "%s", pproj->pj_name); (void) pam_set_item(pamh, PAM_RESOURCE, buf); } r = pam_setcred(pamh, PAM_ESTABLISH_CRED); if (r != PAM_SUCCESS) rval = VUC_BADUSER; set_eser_cred_exit: (void) pam_end(pamh, r); return (rval); } static void clean_out_user(struct usr *u) { if (next_event->u == u) { next_event = NULL; } clean_out_ctab(u); clean_out_atjobs(u); free_if_unused(u); } static void clean_out_atjobs(struct usr *u) { struct event *ev, *pv; for (pv = NULL, ev = u->atevents; ev != NULL; pv = ev, ev = ev->link, free(pv)) { el_remove(ev->of.at.eventid, 1); if (cwd == AT) cron_unlink(ev->cmd); else { char buf[PATH_MAX]; if (strlen(ATDIR) + strlen(ev->cmd) + 2 < PATH_MAX) { (void) sprintf(buf, "%s/%s", ATDIR, ev->cmd); cron_unlink(buf); } } free(ev->cmd); } u->atevents = NULL; } static void clean_out_ctab(struct usr *u) { rm_ctevents(u); el_remove(u->ctid, 0); u->ctid = 0; u->ctexists = 0; } static void cron_unlink(char *name) { int r; r = unlink(name); if (r == 0 || (r == -1 && errno == ENOENT)) { (void) audit_cron_delete_anc_file(name, NULL); } } static void create_anc_ctab(struct event *e) { if (audit_cron_create_anc_file(e->u->name, (cwd == CRON) ? NULL:CRONDIR, e->u->name, e->u->uid) == -1) { process_anc_files(CRON_ANC_DELETE); crabort("cannot create ancillary files for crontabs", REMOVE_FIFO|CONSOLE_MSG); } } static void delete_anc_ctab(struct event *e) { (void) audit_cron_delete_anc_file(e->u->name, (cwd == CRON) ? NULL:CRONDIR); } static void create_anc_atjob(struct event *e) { if (!e->of.at.exists) return; if (audit_cron_create_anc_file(e->cmd, (cwd == AT) ? NULL:ATDIR, e->u->name, e->u->uid) == -1) { process_anc_files(CRON_ANC_DELETE); crabort("cannot create ancillary files for atjobs", REMOVE_FIFO|CONSOLE_MSG); } } static void delete_anc_atjob(struct event *e) { if (!e->of.at.exists) return; (void) audit_cron_delete_anc_file(e->cmd, (cwd == AT) ? NULL:ATDIR); } static void process_anc_files(int del) { struct usr *u = uhead; struct event *e; if (!audit_cron_mode()) return; for (;;) { if (u->ctexists && u->ctevents != NULL) { e = u->ctevents; for (;;) { if (del) delete_anc_ctab(e); else create_anc_ctab(e); if ((e = e->link) == NULL) break; } } if (u->atevents != NULL) { e = u->atevents; for (;;) { if (del) delete_anc_atjob(e); else create_anc_atjob(e); if ((e = e->link) == NULL) break; } } if ((u = u->nextusr) == NULL) break; } } /*ARGSUSED*/ static int cron_conv(int num_msg, struct pam_message **msgs, struct pam_response **response, void *appdata_ptr) { struct pam_message **m = msgs; int i; for (i = 0; i < num_msg; i++) { switch (m[i]->msg_style) { case PAM_ERROR_MSG: case PAM_TEXT_INFO: if (m[i]->msg != NULL) { (void) msg("%s\n", m[i]->msg); } break; default: break; } } return (0); } /* * Cron creates process for other than job. Mail process is the * one which rinfo does not cover. Therefore, miscpid will keep * track of the pids executed from cron. Otherwise, we will see * "unexpected pid returned.." messages appear in the log file. */ static void miscpid_insert(pid_t pid) { struct miscpid *mp; mp = xmalloc(sizeof (*mp)); mp->pid = pid; mp->next = miscpid_head; miscpid_head = mp; } static int miscpid_delete(pid_t pid) { struct miscpid *mp, *omp; int found = 0; omp = NULL; for (mp = miscpid_head; mp != NULL; mp = mp->next) { if (mp->pid == pid) { found = 1; break; } omp = mp; } if (found) { if (omp != NULL) omp->next = mp->next; else miscpid_head = NULL; free(mp); } return (found); } /* * Establish contract terms such that all children are in abandoned * process contracts. */ static int contract_set_template() { int fd; if ((fd = open64(CTFS_ROOT "/process/template", O_RDWR)) < 0) crabort("cannot open process contract template", REMOVE_FIFO | CONSOLE_MSG); if (ct_pr_tmpl_set_param(fd, 0) || ct_tmpl_set_informative(fd, 0) || ct_pr_tmpl_set_fatal(fd, CT_PR_EV_HWERR)) crabort("cannot establish contract template terms", REMOVE_FIFO | CONSOLE_MSG); if (ct_tmpl_activate(fd)) crabort("cannot activate contract template", REMOVE_FIFO | CONSOLE_MSG); (void) close(fd); } /* * Clear active process contract template. */ static int contract_clear_template() { int fd; if ((fd = open64(CTFS_ROOT "/process/template", O_RDWR)) < 0) crabort("cannot open process contract template", REMOVE_FIFO | CONSOLE_MSG); if (ct_tmpl_clear(fd)) crabort("cannot clear contract template", REMOVE_FIFO | CONSOLE_MSG); (void) close(fd); } /* * Abandon latest process contract unconditionally. If we have leaked [some * critical amount], exit such that the kernel reaps our contracts. */ static void contract_abandon_latest(pid_t pid) { int r; ctid_t id; static uint_t cts_lost; if (cts_lost > MAX_LOST_CONTRACTS) crabort("repeated failure to abandon contracts", REMOVE_FIFO | CONSOLE_MSG); if (r = contract_latest(&id)) { if (pid == 0) msg("could not obtain latest contract from " "popen(3C): %s", strerror(r)); else msg("could not obtain latest contract for " "PID %ld: %s", pid, strerror(r)); cts_lost++; return; } if (r = contract_abandon_id(id)) { msg("could not abandon latest contract %ld: %s", id, strerror(r)); cts_lost++; return; } }