diff options
author | stevel@tonic-gate <none@none> | 2005-06-14 00:00:00 -0700 |
---|---|---|
committer | stevel@tonic-gate <none@none> | 2005-06-14 00:00:00 -0700 |
commit | 7c478bd95313f5f23a4c958a745db2134aa03244 (patch) | |
tree | c871e58545497667cbb4b0a4f2daf204743e1fe7 /usr/src/cmd/cron/cron.c | |
download | illumos-joyent-7c478bd95313f5f23a4c958a745db2134aa03244.tar.gz |
OpenSolaris Launch
Diffstat (limited to 'usr/src/cmd/cron/cron.c')
-rw-r--r-- | usr/src/cmd/cron/cron.c | 3312 |
1 files changed, 3312 insertions, 0 deletions
diff --git a/usr/src/cmd/cron/cron.c b/usr/src/cmd/cron/cron.c new file mode 100644 index 0000000000..f29c1917ef --- /dev/null +++ b/usr/src/cmd/cron/cron.c @@ -0,0 +1,3312 @@ +/* + * 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 <sys/contract/process.h> +#include <sys/ctfs.h> +#include <sys/param.h> +#include <sys/resource.h> +#include <sys/stat.h> +#include <sys/task.h> +#include <sys/time.h> +#include <sys/types.h> +#include <sys/utsname.h> +#include <sys/wait.h> + +#include <security/pam_appl.h> + +#include <alloca.h> +#include <ctype.h> +#include <deflt.h> +#include <dirent.h> +#include <errno.h> +#include <fcntl.h> +#include <grp.h> +#include <libcontract.h> +#include <libcontract_priv.h> +#include <limits.h> +#include <locale.h> +#include <poll.h> +#include <project.h> +#include <pwd.h> +#include <signal.h> +#include <stdarg.h> +#include <stdio.h> +#include <stdlib.h> +#include <string.h> +#include <stropts.h> +#include <time.h> +#include <unistd.h> + +#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; + } +} |