[tz] Timezone change detection

Edward Tomasz Napierała trasz at freebsd.org
Fri Sep 3 16:53:40 UTC 2021


Hello.  I'm working on upstreaming a patch, which originally came
from NetApp, to modify localtime.c to automatically detect changes
to the time zone.  The use case is to make the applications handle
moving between timezones, or changing the default system timezone.
The idea is pretty simple: just stat(2) the timezone file inode
to make sure it still refers to the same file it previously did,
and make sure to do this at most once per minute.

The patch (with the FreeBSD-specific build glue stripped) follows;
previous discussion can be found at https://reviews.freebsd.org/D30183.
The patch was written for FreeBSD version of localtime.c, which differs
a bit from upstream, but as there seems to be an effort underway
to sync FreeBSD with upstream, I believe it also makes sense to ask for
comments here before commiting it to FreeBSD.  Any feedback is welcome.
Thanks :-)


diff --git a/contrib/tzcode/stdtime/localtime.c b/contrib/tzcode/stdtime/localtime.c
index e221c1fa396..926b24470e1 100644
--- a/contrib/tzcode/stdtime/localtime.c
+++ b/contrib/tzcode/stdtime/localtime.c
@@ -354,6 +354,45 @@ settzname(void)
 	}
 }
 
+#ifdef DETECT_TZ_CHANGES
+/*
+ * Determine if there's a change in the timezone since the last time we checked.
+ * Returns: -1 on error
+ * 	     0 if the timezone has not changed
+ *	     1 if the timezone has changed
+ */
+static int
+change_in_tz(const char *name)
+{
+	static char old_name[PATH_MAX];
+	static struct stat old_sb;
+	struct stat sb;
+	int error;
+
+	error = stat(name, &sb);
+	if (error != 0)
+		return -1;
+
+	if (strcmp(name, old_name) != 0) {
+		strlcpy(old_name, name, sizeof(old_name));
+		old_sb = sb;
+		return 1;
+	}
+
+	if (sb.st_dev != old_sb.st_dev ||
+	    sb.st_ino != old_sb.st_ino ||
+	    sb.st_ctime != old_sb.st_ctime ||
+	    sb.st_mtime != old_sb.st_mtime) {
+		old_sb = sb;
+		return 1;
+	}
+
+	return 0;
+}
+#else /* !DETECT_TZ_CHANGES */
+#define	change_in_tz(X)	0
+#endif /* !DETECT_TZ_CHANGES */
+
 static int
 differ_by_repeat(const time_t t1, const time_t t0)
 {
@@ -379,6 +418,7 @@ register const int	doextend;
 	int		stored;
 	int		nread;
 	int		res;
+	int		ret;
 	union {
 		struct tzhead	tzhead;
 		char		buf[2 * sizeof(struct tzhead) +
@@ -427,6 +467,22 @@ register const int	doextend;
 			(void) strcat(fullname, name);
 			name = fullname;
 		}
+		if (doextend == TRUE) {
+			/*
+			 * Detect if the timezone file has changed.  Check
+			 * 'doextend' to ignore TZDEFRULES; the change_in_tz()
+			 * function can only keep state for a single file.
+			 */
+			ret = change_in_tz(name);
+			if (ret <= 0) {
+				/*
+				 * Returns -1 if there was an error,
+				 * and 0 if the timezone had not changed.
+				 */
+				free(fullname);
+				return ret;
+			}
+		}
 		if ((fid = _open(name, OPEN_MODE)) == -1) {
 			free(fullname);
 			return -1;
@@ -1209,12 +1265,43 @@ gmtload(struct state *const sp)
 		(void) tzparse(gmt, sp, TRUE);
 }
 
+#ifdef DETECT_TZ_CHANGES
+static int
+recheck_tzdata()
+{
+	static time_t last_checked;
+	struct timespec now;
+	time_t current_time;
+	int error;
+
+	/*
+	 * We want to recheck the timezone file every 61 sec.
+	 */
+	error = clock_gettime(CLOCK_MONOTONIC, &now);
+	if (error <= 0) {
+		/* XXX: Can we somehow report this? */
+		return 0;
+	}
+
+	current_time = now.tv_sec;
+	if ((current_time - last_checked > 61) ||
+	    (last_checked > current_time)) {
+		last_checked = current_time;
+		return 1;
+	}
+
+	return 0;
+}
+#else /* !DETECT_TZ_CHANGES */
+#define	recheck_tzdata()	0
+#endif /* !DETECT_TZ_CHANGES */
+
 static void
 tzsetwall_basic(int rdlocked)
 {
 	if (!rdlocked)
 		_RWLOCK_RDLOCK(&lcl_rwlock);
-	if (lcl_is_set < 0) {
+	if (lcl_is_set < 0 && recheck_tzdata() == 0) {
 		if (!rdlocked)
 			_RWLOCK_UNLOCK(&lcl_rwlock);
 		return;



More information about the tz mailing list