/* $OpenBSD: cron.c,v 1.82 2022/07/08 20:47:24 millert Exp $ */ /* Copyright 1988,1990,1993,1994 by Paul Vixie * Copyright (c) 2004 by Internet Systems Consortium, Inc. ("ISC") * Copyright (c) 1997,2000 by Internet Software Consortium, Inc. * * Permission to use, copy, modify, and distribute this software for any * purpose with or without fee is hereby granted, provided that the above * copyright notice and this permission notice appear in all copies. * * THE SOFTWARE IS PROVIDED "AS IS" AND ISC DISCLAIMS ALL WARRANTIES * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL ISC BE LIABLE FOR * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT * OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #include "pathnames.h" #include "macros.h" #include "structs.h" #include "funcs.h" #include "globals.h" enum timejump { negative, small, medium, large }; static void usage(void), run_reboot_jobs(cron_db *), find_jobs(time_t, cron_db *, int, int), set_time(int), cron_sleep(time_t, sigset_t *), sigchld_handler(int), sigchld_reaper(void), parse_args(int c, char *v[]); static int open_socket(void); static volatile sig_atomic_t got_sigchld; static time_t timeRunning, virtualTime, clockTime; static long GMToff; static cron_db *database; static at_db *at_database; static double batch_maxload = BATCH_MAXLOAD; static int NoFork; static time_t StartTime; gid_t cron_gid; int cronSock; static void usage(void) { fprintf(stderr, "usage: %s [-n] [-l load_avg]\n", __progname); exit(EXIT_FAILURE); } int main(int argc, char *argv[]) { struct sigaction sact; sigset_t blocked, omask; struct group *grp; setvbuf(stdout, NULL, _IOLBF, 0); setvbuf(stderr, NULL, _IOLBF, 0); parse_args(argc, argv); bzero((char *)&sact, sizeof sact); sigemptyset(&sact.sa_mask); sact.sa_flags = SA_RESTART; sact.sa_handler = sigchld_handler; (void) sigaction(SIGCHLD, &sact, NULL); sact.sa_handler = SIG_IGN; (void) sigaction(SIGHUP, &sact, NULL); (void) sigaction(SIGPIPE, &sact, NULL); openlog(__progname, LOG_PID, LOG_CRON); if (pledge("stdio rpath wpath cpath fattr getpw unix id dns proc exec", NULL) == -1) { warn("pledge"); syslog(LOG_ERR, "(CRON) PLEDGE (%m)"); exit(EXIT_FAILURE); } if ((grp = getgrnam(CRON_GROUP)) == NULL) { warnx("can't find cron group %s", CRON_GROUP); syslog(LOG_ERR, "(CRON) DEATH (can't find cron group)"); exit(EXIT_FAILURE); } cron_gid = grp->gr_gid; cronSock = open_socket(); if (putenv("PATH="_PATH_DEFPATH) < 0) { warn("putenv"); syslog(LOG_ERR, "(CRON) DEATH (%m)"); exit(EXIT_FAILURE); } if (NoFork == 0) { if (daemon(0, 0) == -1) { syslog(LOG_ERR, "(CRON) DEATH (%m)"); exit(EXIT_FAILURE); } syslog(LOG_INFO, "(CRON) STARTUP (%s)", CRON_VERSION); } load_database(&database); scan_atjobs(&at_database, NULL); set_time(TRUE); run_reboot_jobs(database); timeRunning = virtualTime = clockTime; /* * We block SIGHUP and SIGCHLD while running jobs and receive them * only while sleeping in ppoll(). This ensures no signal is lost. */ sigemptyset(&blocked); sigaddset(&blocked, SIGCHLD); sigaddset(&blocked, SIGHUP); sigprocmask(SIG_BLOCK, &blocked, &omask); /* * Too many clocks, not enough time (Al. Einstein) * These clocks are in minutes since the epoch, adjusted for timezone. * virtualTime: is the time it *would* be if we woke up * promptly and nobody ever changed the clock. It is * monotonically increasing... unless a timejump happens. * At the top of the loop, all jobs for 'virtualTime' have run. * timeRunning: is the time we last awakened. * clockTime: is the time when set_time was last called. */ while (TRUE) { int timeDiff; enum timejump wakeupKind; /* ... wait for the time (in minutes) to change ... */ do { cron_sleep(timeRunning + 1, &omask); set_time(FALSE); } while (clockTime == timeRunning); timeRunning = clockTime; /* * Calculate how the current time differs from our virtual * clock. Classify the change into one of 4 cases. */ timeDiff = timeRunning - virtualTime; /* shortcut for the most common case */ if (timeDiff == 1) { virtualTime = timeRunning; find_jobs(virtualTime, database, TRUE, TRUE); } else { if (timeDiff > (3*MINUTE_COUNT) || timeDiff < -(3*MINUTE_COUNT)) wakeupKind = large; else if (timeDiff > 5) wakeupKind = medium; else if (timeDiff > 0) wakeupKind = small; else wakeupKind = negative; switch (wakeupKind) { case small: /* * case 1: timeDiff is a small positive number * (wokeup late) run jobs for each virtual * minute until caught up. */ do { if (job_runqueue()) sleep(10); virtualTime++; find_jobs(virtualTime, database, TRUE, TRUE); } while (virtualTime < timeRunning); break; case medium: /* * case 2: timeDiff is a medium-sized positive * number, for example because we went to DST * run wildcard jobs once, then run any * fixed-time jobs that would otherwise be * skipped if we use up our minute (possible, * if there are a lot of jobs to run) go * around the loop again so that wildcard jobs * have a chance to run, and we do our * housekeeping. */ /* run wildcard jobs for current minute */ find_jobs(timeRunning, database, TRUE, FALSE); /* run fixed-time jobs for each minute missed */ do { if (job_runqueue()) sleep(10); virtualTime++; find_jobs(virtualTime, database, FALSE, TRUE); set_time(FALSE); } while (virtualTime< timeRunning && clockTime == timeRunning); break; case negative: /* * case 3: timeDiff is a small or medium-sized * negative num, eg. because of DST ending. * Just run the wildcard jobs. The fixed-time * jobs probably have already run, and should * not be repeated. Virtual time does not * change until we are caught up. */ find_jobs(timeRunning, database, TRUE, FALSE); break; default: /* * other: time has changed a *lot*, * jump virtual time, and run everything */ virtualTime = timeRunning; find_jobs(timeRunning, database, TRUE, TRUE); } } /* Jobs to be run (if any) are loaded; clear the queue. */ job_runqueue(); /* Run any jobs in the at queue. */ atrun(at_database, batch_maxload, timeRunning * SECONDS_PER_MINUTE - GMToff); /* Reload jobs as needed. */ load_database(&database); scan_atjobs(&at_database, NULL); } } static void run_reboot_jobs(cron_db *db) { user *u; entry *e; TAILQ_FOREACH(u, &db->users, entries) { SLIST_FOREACH(e, &u->crontab, entries) { if (e->flags & WHEN_REBOOT) job_add(e, u); } } (void) job_runqueue(); } static void find_jobs(time_t vtime, cron_db *db, int doWild, int doNonWild) { time_t virtualSecond = vtime * SECONDS_PER_MINUTE; struct tm *tm = gmtime(&virtualSecond); int minute, hour, dom, month, dow; user *u; entry *e; /* make 0-based values out of these so we can use them as indices */ minute = tm->tm_min -FIRST_MINUTE; hour = tm->tm_hour -FIRST_HOUR; dom = tm->tm_mday -FIRST_DOM; month = tm->tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH; dow = tm->tm_wday -FIRST_DOW; /* the dom/dow situation is odd. '* * 1,15 * Sun' will run on the * first and fifteenth AND every Sunday; '* * * * Sun' will run *only* * on Sundays; '* * 1,15 * *' will run *only* the 1st and 15th. this * is why we keep 'e->dow_star' and 'e->dom_star'. yes, it's bizarre. * like many bizarre things, it's the standard. */ TAILQ_FOREACH(u, &db->users, entries) { SLIST_FOREACH(e, &u->crontab, entries) { if (bit_test(e->minute, minute) && bit_test(e->hour, hour) && bit_test(e->month, month) && ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR)) ? (bit_test(e->dow,dow) && bit_test(e->dom,dom)) : (bit_test(e->dow,dow) || bit_test(e->dom,dom)) ) ) { if ((doNonWild && !(e->flags & (MIN_STAR|HR_STAR))) || (doWild && (e->flags & (MIN_STAR|HR_STAR)))) job_add(e, u); } } } } /* * Set StartTime and clockTime to the current time. * These are used for computing what time it really is right now. * Note that clockTime is a unix wallclock time converted to minutes. */ static void set_time(int initialize) { struct tm tm; static int isdst; StartTime = time(NULL); /* We adjust the time to GMT so we can catch DST changes. */ tm = *localtime(&StartTime); if (initialize || tm.tm_isdst != isdst) { isdst = tm.tm_isdst; GMToff = get_gmtoff(&StartTime, &tm); } clockTime = (StartTime + GMToff) / (time_t)SECONDS_PER_MINUTE; } /* * Try to just hit the next minute. */ static void cron_sleep(time_t target, sigset_t *mask) { int fd, nfds; unsigned char poke; struct timespec t1, t2, timeout; struct sockaddr_un s_un; socklen_t sunlen; static struct pollfd pfd[1]; clock_gettime(CLOCK_REALTIME, &t1); t1.tv_sec += GMToff; timeout.tv_sec = (target * SECONDS_PER_MINUTE - t1.tv_sec) + 1; if (timeout.tv_sec < 0) timeout.tv_sec = 0; timeout.tv_nsec = 0; pfd[0].fd = cronSock; pfd[0].events = POLLIN; while (timespecisset(&timeout) && timeout.tv_sec < 65) { poke = RELOAD_CRON | RELOAD_AT; /* Sleep until we time out, get a poke, or get a signal. */ nfds = ppoll(pfd, 1, &timeout, mask); switch (nfds) { case -1: if (errno != EINTR && errno != EAGAIN) { syslog(LOG_ERR, "(CRON) DEATH (ppoll failure: %m)"); exit(EXIT_FAILURE); } if (errno == EINTR) { if (got_sigchld) { got_sigchld = 0; sigchld_reaper(); } } break; case 0: /* done sleeping */ return; default: sunlen = sizeof(s_un); fd = accept4(cronSock, (struct sockaddr *)&s_un, &sunlen, SOCK_NONBLOCK); if (fd >= 0) { (void) read(fd, &poke, 1); close(fd); if (poke & RELOAD_CRON) { timespecclear(&database->mtime); load_database(&database); } if (poke & RELOAD_AT) { /* * We run any pending at jobs right * away so that "at now" really runs * jobs immediately. */ clock_gettime(CLOCK_REALTIME, &t2); timespecclear(&at_database->mtime); if (scan_atjobs(&at_database, &t2)) atrun(at_database, batch_maxload, t2.tv_sec); } } } /* Adjust tv and continue where we left off. */ clock_gettime(CLOCK_REALTIME, &t2); t2.tv_sec += GMToff; timespecsub(&t2, &t1, &t1); timespecsub(&timeout, &t1, &timeout); memcpy(&t1, &t2, sizeof(t1)); if (timeout.tv_sec < 0) timespecclear(&timeout); } } /* int open_socket(void) * opens a UNIX domain socket that crontab uses to poke cron. * If the socket is already in use, return an error. */ static int open_socket(void) { int sock, rc; mode_t omask; struct sockaddr_un s_un; sock = socket(AF_UNIX, SOCK_STREAM|SOCK_CLOEXEC|SOCK_NONBLOCK, 0); if (sock == -1) { warn("socket"); syslog(LOG_ERR, "(CRON) DEATH (can't create socket)"); exit(EXIT_FAILURE); } bzero(&s_un, sizeof(s_un)); if (strlcpy(s_un.sun_path, _PATH_CRON_SOCK, sizeof(s_un.sun_path)) >= sizeof(s_un.sun_path)) { warnc(ENAMETOOLONG, _PATH_CRON_SOCK); syslog(LOG_ERR, "(CRON) DEATH (socket path too long)"); exit(EXIT_FAILURE); } s_un.sun_family = AF_UNIX; if (connect(sock, (struct sockaddr *)&s_un, sizeof(s_un)) == 0) { warnx("already running"); syslog(LOG_ERR, "(CRON) DEATH (already running)"); exit(EXIT_FAILURE); } if (errno != ENOENT) unlink(s_un.sun_path); omask = umask(007); rc = bind(sock, (struct sockaddr *)&s_un, sizeof(s_un)); umask(omask); if (rc != 0) { warn("bind"); syslog(LOG_ERR, "(CRON) DEATH (can't bind socket)"); exit(EXIT_FAILURE); } if (listen(sock, SOMAXCONN)) { warn("listen"); syslog(LOG_ERR, "(CRON) DEATH (can't listen on socket)"); exit(EXIT_FAILURE); } /* pledge won't let us change files to a foreign group. */ if (setegid(cron_gid) == 0) { chown(s_un.sun_path, -1, cron_gid); (void)setegid(getgid()); } chmod(s_un.sun_path, 0660); return(sock); } static void sigchld_handler(int x) { got_sigchld = 1; } static void sigchld_reaper(void) { int waiter; pid_t pid; do { pid = waitpid(-1, &waiter, WNOHANG); switch (pid) { case -1: if (errno == EINTR) continue; break; case 0: break; default: job_exit(pid); break; } } while (pid > 0); } static void parse_args(int argc, char *argv[]) { int argch; char *ep; while (-1 != (argch = getopt(argc, argv, "l:n"))) { switch (argch) { case 'l': errno = 0; batch_maxload = strtod(optarg, &ep); if (*ep != '\0' || ep == optarg || errno == ERANGE || batch_maxload < 0) { warnx("illegal load average: %s", optarg); usage(); } break; case 'n': NoFork = 1; break; default: usage(); } } }