From: Christian Kastner <ckk@kvr.at>
Date: Sat, 09 Jan 2016 18:21:39 +0100
Subject: Better timeskip handling

Better handling of time skips (when the clock jumps either forwards or
backwards), in particular with regards to/from daylight savings time.

Originally from OpenBSD patches supplied by Topi Miettinen.

Bug-Debian: https://bugs.debian.org/8499
Bug-Debian: https://bugs.debian.org/458123
Bug-Debian: https://bugs.debian.org/474157
Bug-Ubuntu: https://bugs.launchpad.net/bugs/36690
Forwarded: no
Last-Update: 2016-01-09
Index: cron/cron.h
===================================================================
--- cron.orig/cron.h
+++ cron/cron.h
@@ -40,6 +40,13 @@
 #include "config.h"
 #include "externs.h"
 
+#if SYS_TIME_H
+# include <sys/time.h>
+#else
+# include <time.h>
+#endif
+
+
 #ifdef WITH_SELINUX
 #include <selinux/selinux.h>
 #endif
@@ -129,6 +136,10 @@
 			 LineNumber = ln; \
 			}
 
+typedef int time_min;
+
+#define SECONDS_PER_MINUTE 60
+
 #define	FIRST_MINUTE	0
 #define	LAST_MINUTE	59
 #define	MINUTE_COUNT	(LAST_MINUTE - FIRST_MINUTE + 1)
@@ -171,6 +182,8 @@ typedef	struct _entry {
 #define	DOM_STAR	0x01
 #define	DOW_STAR	0x02
 #define	WHEN_REBOOT	0x04
+#define	MIN_STAR	0x08
+#define	HR_STAR		0x10
 } entry;
 
 			/* the crontab database will be a list of the
@@ -228,6 +241,8 @@ int		job_runqueue __P((void)),
 		allowed __P((char *)),
 		strdtb __P((char *));
 
+long		get_gmtoff(time_t *, struct tm *);
+
 char		*env_get __P((char *, char **)),
 		*arpadate __P((time_t *)),
 		*mkprints __P((unsigned char *, unsigned int)),
@@ -271,7 +286,11 @@ char	*DowNames[] = {
 
 char	*ProgramName;
 int	LineNumber;
-time_t	TargetTime;
+time_t	StartTime;
+time_min timeRunning;
+time_min virtualTime;
+time_min clockTime;
+static long GMToff;
 
 int	lsbsysinit_mode;
 
@@ -289,7 +308,10 @@ extern	char	*copyright[],
 		*ProgramName;
 extern	int	lsbsysinit_mode;
 extern	int	LineNumber;
-extern	time_t	TargetTime;
+extern	time_t	StartTime;
+extern  time_min timeRunning;
+extern  time_min virtualTime;
+extern  time_min clockTime;
 # if DEBUGGING
 extern	int	DebugFlags;
 extern	char	*DebugFlagNames[];
Index: cron/entry.c
===================================================================
--- cron.orig/entry.c
+++ cron/entry.c
@@ -162,6 +162,7 @@ load_entry(file, error_func, pw, envp)
 			bit_nset(e->dom, 0, (LAST_DOM-FIRST_DOM+1));
 			bit_nset(e->month, 0, (LAST_MONTH-FIRST_MONTH+1));
 			bit_nset(e->dow, 0, (LAST_DOW-FIRST_DOW+1));
+			e->flags |= HR_STAR;
 		} else {
 			ecode = e_timespec;
 			goto eof;
@@ -169,6 +170,8 @@ load_entry(file, error_func, pw, envp)
 	} else {
 		Debug(DPARS, ("load_entry()...about to parse numerics\n"))
 
+		if (ch == '*')
+			e->flags |= MIN_STAR;
 		ch = get_list(e->minute, FIRST_MINUTE, LAST_MINUTE,
 			      PPC_NULL, ch, file);
 		if (ch == EOF) {
@@ -179,6 +182,8 @@ load_entry(file, error_func, pw, envp)
 		/* hours
 		 */
 
+		if (ch == '*')
+			e->flags |= HR_STAR;
 		ch = get_list(e->hour, FIRST_HOUR, LAST_HOUR,
 			      PPC_NULL, ch, file);
 		if (ch == EOF) {
@@ -251,6 +256,9 @@ load_entry(file, error_func, pw, envp)
 			goto eof;
 		}
 		Debug(DPARS, ("load_entry()...uid %d, gid %d\n",e->uid,e->gid))
+	} else if (ch == '*') {
+		ecode = e_cmd;
+		goto eof;
 	}
 
 	e->uid = pw->pw_uid;
Index: cron/do_command.c
===================================================================
--- cron.orig/do_command.c
+++ cron/do_command.c
@@ -487,7 +487,7 @@ child_process(e, u)
 					e->cmd);
 # if defined(MAIL_DATE)
 				fprintf(mail, "Date: %s\n",
-					arpadate(&TargetTime));
+					arpadate(&StartTime));
 # endif /* MAIL_DATE */
 				fprintf(mail, "MIME-Version: 1.0\n");
 				fprintf(mail, "Content-Transfer-Encoding: 8bit\n");
Index: cron/misc.c
===================================================================
--- cron.orig/misc.c
+++ cron/misc.c
@@ -683,8 +683,9 @@ arpadate(clock)
 	struct tm *tm = localtime(&t);
 	char *qmark;
 	size_t len;
-	int hours = tm->tm_gmtoff / 3600;
-	int minutes = (tm->tm_gmtoff - (hours * 3600)) / 60;
+	long gmtoff = get_gmtoff(&t, tm);
+	int hours = gmtoff / 3600;
+	int minutes = (gmtoff - (hours * 3600)) / 60;
 
 	if (minutes < 0)
 		minutes = -minutes;
@@ -725,3 +726,38 @@ int swap_uids()
 }
 int swap_uids_back() { return swap_uids(); }
 #endif /*HAVE_SAVED_UIDS*/
+
+
+/* Return the offset from GMT in seconds (algorithm taken from sendmail).
+ *
+ * warning:
+ *	clobbers the static storage space used by localtime() and gmtime().
+ *	If the local pointer is non-NULL it *must* point to a local copy.
+ */
+#ifndef HAVE_TM_GMTOFF
+long get_gmtoff(time_t *clock, struct tm *local)
+{
+	struct tm gmt;
+	long offset;
+
+	gmt = *gmtime(clock);
+	if (local == NULL)
+		local = localtime(clock);
+
+	offset = (local->tm_sec - gmt.tm_sec) +
+		((local->tm_min - gmt.tm_min) * 60) +
+		((local->tm_hour - gmt.tm_hour) * 3600);
+
+	/* Timezone may cause year rollover to happen on a different day. */
+	if (local->tm_year < gmt.tm_year)
+		offset -= 24 * 3600;
+	else if (local->tm_year > gmt.tm_year)
+		offset += 24 * 3600;
+	else if (local->tm_yday < gmt.tm_yday)
+		offset -= 24 * 3600;
+	else if (local->tm_yday > gmt.tm_yday)
+		offset += 24 * 3600;
+
+	return (offset);
+}
+#endif /* HAVE_TM_GMTOFF */
Index: cron/cron.c
===================================================================
--- cron.orig/cron.c
+++ cron/cron.c
@@ -25,11 +25,6 @@ static char rcsid[] = "$Id: cron.c,v 2.1
 
 #include "cron.h"
 #include <signal.h>
-#if SYS_TIME_H
-# include <sys/time.h>
-#else
-# include <time.h>
-#endif
 
 #include <sys/stat.h>
 #include <sys/types.h>
@@ -38,9 +33,9 @@ static char rcsid[] = "$Id: cron.c,v 2.1
 
 static	void	usage __P((void)),
 		run_reboot_jobs __P((cron_db *)),
-		cron_tick __P((cron_db *)),
-		cron_sync __P((void)),
-		cron_sleep __P((void)),
+		find_jobs __P((time_min, cron_db *, int, int)),
+		set_time __P((int)),
+		cron_sleep __P((time_min)),
 #ifdef USE_SIGCHLD
 		sigchld_handler __P((int)),
 #endif
@@ -135,23 +130,125 @@ main(argc, argv)
 	database.user_mtime = (time_t) 0;
 	database.sysd_mtime = (time_t) 0;
 	load_database(&database);
+	set_time(TRUE);
 	run_reboot_jobs(&database);
-	cron_sync();
+	timeRunning = virtualTime = clockTime;
+
+	/*
+	 * too many clocks, not enough time (Al. Einstein)
+	 * These clocks are in minutes since the epoch (time()/60).
+	 * 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) {
-# if DEBUGGING
-		if (!(DebugFlags & DTEST))
-# endif /*DEBUGGING*/
-			cron_sleep();
+		time_min timeDiff;
+		int wakeupKind;
+
+		/* ... wait for the time (in minutes) to change ... */
+		do {
+			cron_sleep(timeRunning + 1);
+			set_time(FALSE);
+		} while (clockTime == timeRunning);
+		timeRunning = clockTime;
 
 		load_database(&database);
 
-		/* do this iteration
+		/*
+		 * ... calculate how the current time differs from
+		 * our virtual clock. Classify the change into one
+		 * of 4 cases
 		 */
-		cron_tick(&database);
+		timeDiff = timeRunning - virtualTime;
 
-		/* sleep 1 minute
-		 */
-		TargetTime += 60;
+		Debug(DSCH, ("[%d] pulse: %d = %d - %d\n",
+			getpid(), timeDiff, timeRunning, virtualTime));
+
+		/* shortcut for the most common case */
+		if (timeDiff == 1) {
+			virtualTime = timeRunning;
+			find_jobs(virtualTime, &database, TRUE, TRUE);
+		} else {
+			wakeupKind = -1;
+			if (timeDiff > -(3*MINUTE_COUNT))
+				wakeupKind = 0;
+			if (timeDiff > 0)
+				wakeupKind = 1;
+			if (timeDiff > 5)
+				wakeupKind = 2;
+			if (timeDiff > (3*MINUTE_COUNT))
+				wakeupKind = 3;
+
+			switch (wakeupKind) {
+			case 1:
+				/*
+				 * case 1: timeDiff is a small positive number
+				 * (wokeup late) run jobs for each virtual minute
+				 * until caught up.
+				 */
+				Debug(DSCH, ("[%d], normal case %d minutes to go\n",
+					getpid(), timeRunning - virtualTime))
+				do {
+					if (job_runqueue())
+						sleep(10);
+					virtualTime++;
+					find_jobs(virtualTime, &database, TRUE, TRUE);
+				} while (virtualTime< timeRunning);
+				break;
+
+			case 2:
+				/*
+				 * 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
+				 */
+				Debug(DSCH, ("[%d], DST begins %d minutes to go\n",
+					getpid(), timeRunning - virtualTime))
+				/* 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 0:
+				/*
+				 * 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
+				 */
+				Debug(DSCH, ("[%d], DST ends %d minutes to go\n",
+					getpid(), virtualTime - timeRunning))
+				find_jobs(timeRunning, &database, TRUE, FALSE);
+				break;
+			default:
+				/*
+				 * other: time has changed a *lot*,
+				 * jump virtual time, and run everything
+				 */
+				Debug(DSCH, ("[%d], clock jumped\n", getpid()))
+				virtualTime = timeRunning;
+				find_jobs(timeRunning, &database, TRUE, TRUE);
+			}
+		}
+		/* jobs to be run (if any) are loaded. clear the queue */
+		job_runqueue();
 	}
 }
 
@@ -193,10 +290,14 @@ run_reboot_jobs(db)
 
 
 static void
-cron_tick(db)
+find_jobs(vtime, db, doWild, doNonWild)
+	time_min vtime;
 	cron_db	*db;
+	int doWild;
+	int doNonWild;
 {
- 	register struct tm	*tm = localtime(&TargetTime);
+	time_t   virtualSecond  = vtime * SECONDS_PER_MINUTE;
+	register struct tm	*tm = gmtime(&virtualSecond);
 	register int		minute, hour, dom, month, dow;
 	register user		*u;
 	register entry		*e;
@@ -209,8 +310,9 @@ cron_tick(db)
 	month = tm->tm_mon +1 /* 0..11 -> 1..12 */ -FIRST_MONTH;
 	dow = tm->tm_wday -FIRST_DOW;
 
-	Debug(DSCH, ("[%d] tick(%d,%d,%d,%d,%d)\n",
-		getpid(), minute, hour, dom, month, dow))
+	Debug(DSCH, ("[%d] tick(%d,%d,%d,%d,%d) %s %s\n",
+		getpid(), minute, hour, dom, month, dow,
+		doWild?" ":"No wildcard",doNonWild?" ":"Wildcard only"))
 
 	/* the dom/dow situation is odd.  '* * 1,15 * Sun' will run on the
 	 * first and fifteenth AND every Sunday;  '* * * * Sun' will run *only*
@@ -221,67 +323,65 @@ cron_tick(db)
 	for (u = db->head;  u != NULL;  u = u->next) {
 		for (e = u->crontab;  e != NULL;  e = e->next) {
 			Debug(DSCH|DEXT, ("user [%s:%d:%d:...] cmd=\"%s\"\n",
-					  env_get("LOGNAME", e->envp),
-					  e->uid, e->gid, e->cmd))
-			if (bit_test(e->minute, minute)
-			 && bit_test(e->hour, hour)
-			 && bit_test(e->month, month)
-			 && ( ((e->flags & DOM_STAR) || (e->flags & DOW_STAR))
+			    env_get("LOGNAME", e->envp),
+			    e->uid, e->gid, e->cmd))
+			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))
-			    )
-			   ) {
-				job_add(e, u);
+			      : (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);
 			}
 		}
 	}
 }
 
 
-/* the task here is to figure out how long it's going to be until :00 of the
- * following minute and initialize TargetTime to this value.  TargetTime
- * will subsequently slide 60 seconds at a time, with correction applied
- * implicitly in cron_sleep().  it would be nice to let cron execute in
- * the "current minute" before going to sleep, but by restarting cron you
- * could then get it to execute a given minute's jobs more than once.
- * instead we have the chance of missing a minute's jobs completely, but
- * that's something sysadmin's know to expect what with crashing computers..
+/*
+ * 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
-cron_sync() {
- 	register struct tm	*tm;
+set_time(int initialize)
+{
+    struct tm tm;
+    static int isdst;
 
-	TargetTime = time((time_t*)0);
-	tm = localtime(&TargetTime);
-	TargetTime += (60 - tm->tm_sec);
-}
+    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);
+       Debug(DSCH, ("[%d] GMToff=%ld\n",
+           getpid(), (long)GMToff))
+    }
+    clockTime = (StartTime + GMToff) / (time_t)SECONDS_PER_MINUTE;
+}
 
+/*
+ * try to just hit the next minute
+ */
 static void
-cron_sleep() {
-	register int	seconds_to_wait;
+cron_sleep(target)
+	time_min target;
+{
+	time_t t;
+	int seconds_to_wait;
 
-	do {
-		seconds_to_wait = (int) (TargetTime - time((time_t*)0));
-		Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n",
-			getpid(), TargetTime, seconds_to_wait))
-
-		/* if we intend to sleep, this means that it's finally
-		 * time to empty the job queue (execute it).
-		 *
-		 * if we run any jobs, we'll probably screw up our timing,
-		 * so go recompute.
-		 *
-		 * note that we depend here on the left-to-right nature
-		 * of &&, and the short-circuiting.
-		 */
-	} while (seconds_to_wait > 0 && job_runqueue());
+	t = time(NULL) + GMToff;
 
-	while (seconds_to_wait > 0) {
-		Debug(DSCH, ("[%d] sleeping for %d seconds\n",
-			getpid(), seconds_to_wait))
-		seconds_to_wait = (int) sleep((unsigned int) seconds_to_wait);
-	}
+	seconds_to_wait = (int)(target * SECONDS_PER_MINUTE - t) + 1;
+	Debug(DSCH, ("[%d] TargetTime=%ld, sec-to-wait=%d\n",
+	    getpid(), (long)target*SECONDS_PER_MINUTE, seconds_to_wait))
+
+        if (seconds_to_wait > 0 && seconds_to_wait < 65)
+            sleep((unsigned int) seconds_to_wait);
 }
 
 
Index: cron/cron.8
===================================================================
--- cron.orig/cron.8
+++ cron/cron.8
@@ -111,6 +111,22 @@ need not be restarted whenever a crontab
 .IR crontab (1)
 command updates the modtime of the spool directory whenever it changes a
 crontab.
+.PP
+Special considerations exist when the clock is changed by less than 3
+hours, for example at the beginning and end of daylight savings
+time.  If the time has moved forwards, those jobs which would have
+run in the time that was skipped will be run soon after the change.
+Conversely, if the time has moved backwards by less than 3 hours,
+those jobs that fall into the repeated time will not be re-run.
+.PP
+Only jobs that run at a particular time (not specified as
+@hourly, nor with '*' in the hour or minute specifier) are
+affected.  Jobs which are specified with wildcards are run based on the
+new time immediately.
+.PP
+Clock changes of more than 3 hours are considered to be corrections to
+the clock, and the new time is used immediately.
+.PP
 .I cron
 logs its action to the syslog facility 'cron', and logging may be
 controlled using the standard
@@ -162,6 +178,8 @@ SELinux support,
 .IP \(em
 auditlog support,
 .IP \(em
+DST and other time-related changes/fixes,
+.IP \(em
 Debian-specific file locations and commands,
 .IP \(em
 Debian-specific configuration (/etc/default/cron),
