git: 36c6d5275677 - stable/15 - tzcode: Update to 2026d

From: Dag-Erling Smørgrav <des_at_FreeBSD.org>
Date: Sun, 20 Sep 2026 17:07:38 UTC
The branch stable/15 has been updated by des:

URL: https://cgit.FreeBSD.org/src/commit/?id=36c6d5275677cd54e46880509fcc5a5dcebaa743

commit 36c6d5275677cd54e46880509fcc5a5dcebaa743
Author:     Dag-Erling Smørgrav <des@FreeBSD.org>
AuthorDate: 2026-09-13 13:29:59 +0000
Commit:     Dag-Erling Smørgrav <des@FreeBSD.org>
CommitDate: 2026-09-20 16:51:12 +0000

    tzcode: Update to 2026d
    
    MFC after:      1 week
    
    (cherry picked from commit 212e3524943222650688bd1f49d5eb4c9326de7f)
    
    libc/stdtime: Catch up with tzcode 2026d
    
    Fixes:          212e35249432 ("tzcode: Update to 2026c")
    (cherry picked from commit 1689b875b4b80c152cf7d3652c72335a2149173e)
---
 contrib/tzcode/CONTRIBUTING   |   2 +-
 contrib/tzcode/Makefile       |  19 +-
 contrib/tzcode/NEWS           | 115 +++++++++++-
 contrib/tzcode/README         |   2 +-
 contrib/tzcode/localtime.c    | 399 ++++++++++++++++++------------------------
 contrib/tzcode/private.h      |  15 +-
 contrib/tzcode/strftime.c     |   9 +-
 contrib/tzcode/theory.html    |  63 +++----
 contrib/tzcode/tz-art.html    |  14 +-
 contrib/tzcode/tz-how-to.html |  18 +-
 contrib/tzcode/tz-link.html   | 118 +++++++------
 contrib/tzcode/version        |   2 +-
 contrib/tzcode/zdump.c        |   4 +-
 contrib/tzcode/zic.8          |  45 +++--
 contrib/tzcode/zic.c          |  89 +++++-----
 lib/libc/stdtime/strftime.c   |   8 +-
 16 files changed, 507 insertions(+), 415 deletions(-)

diff --git a/contrib/tzcode/CONTRIBUTING b/contrib/tzcode/CONTRIBUTING
index c5fa803f7275..213ae67e962a 100644
--- a/contrib/tzcode/CONTRIBUTING
+++ b/contrib/tzcode/CONTRIBUTING
@@ -22,7 +22,7 @@ To email small changes, please run a POSIX shell command like
 For more-elaborate or possibly controversial changes,
 such as renaming, adding or removing zones, please read
 “Theory and pragmatics of the tz code and data”
-<https://www.iana.org/time-zones/repository/theory.html>.
+<https://data.iana.org/time-zones/theory.html>
 It is also good to browse the mailing list archives
 <https://lists.iana.org/hyperkitty/list/tz@iana.org/>
 for examples of patches that tend to work well.
diff --git a/contrib/tzcode/Makefile b/contrib/tzcode/Makefile
index f5a22f1896a4..96a7462e9a28 100644
--- a/contrib/tzcode/Makefile
+++ b/contrib/tzcode/Makefile
@@ -172,9 +172,9 @@ EXPIRES_LINE=	0
 
 TZDATA_TEXT=	leapseconds tzdata.zi
 
-# For backward-compatibility links for old zone names, use
+# For backward-compatibility links and zones for old names, use
 #	BACKWARD=	backward
-# To omit these links, use
+# To omit these links and zones, use
 #	BACKWARD=
 
 BACKWARD=	backward
@@ -574,7 +574,7 @@ SAFE_CHAR=	'[]'$(SAFE_CHARSET)'-]'
 # These non-alphabetic, non-ASCII printable characters are
 # used in commentary or in generated *.txt files
 # and are not likely to cause confusion.
-UNUSUAL_OK_CHARSET= §«°±»½¾×–‘’“”•→−≤★⟨⟩⯪
+UNUSUAL_OK_CHARSET= §«°±»½¾¿×–‘’“”•→−≤★⟨⟩⯪
 
 # Put this in a bracket expression to match spaces.
 s = [:space:]
@@ -922,7 +922,7 @@ check_mild: check_web check_zishrink \
 UTF8_LOCALE_MISSING = \
   { test ! '$(UTF8_LOCALE)' \
     || ! printf 'A\304\200B\n' \
-         | LC_ALL='$(UTF8_LOCALE)' grep -q '^A.B$$' >/dev/null 2>&1 \
+         | LC_ALL='$(UTF8_LOCALE)' grep -q '^A[[:alpha:]]B$$' >/dev/null 2>&1 \
     || { export LC_ALL='$(UTF8_LOCALE)'; false; }; }
 
 character-set.ck: $(ENCHILADA)
@@ -1006,11 +1006,11 @@ now.ck: checknow.awk date tzdata.zi zdump zic zone1970.tab zonenow.tab
 		now=$(CHECK_NOW_TIMESTAMP) && \
 		  future=$$(($(CHECK_NOW_FUTURE_SECS) + $$now)) && \
 		  ./zdump -i -t $$now,$$future \
-		     $$(find "$$PWD/$@d"/????*/ -type f) \
+		     $$(find "$$PWD/$@d"/????*/ -type f -o -type l) \
 		     >$@d/zdump-now.tab && \
 		  ./zdump -i -t 0,$$future \
 		     $$(find "$$PWD/$@d" -name Etc -prune \
-			  -o -type f ! -name '*.tab' -print) \
+			  -o \( -type f -o -type l \) ! -name '*.tab' -print) \
 		     >$@d/zdump-1970.tab && \
 		$(AWK) \
 		  -v now=$$now \
@@ -1018,7 +1018,12 @@ now.ck: checknow.awk date tzdata.zi zdump zic zone1970.tab zonenow.tab
 		  -v zdump_table=$@d/zdump-now.tab \
 		  -f checknow.awk zonenow.tab
 		$(AWK) \
-		  'BEGIN {print "-\t-\tUTC"} /^Zone/ {print "-\t-\t" $$2}' \
+		  'BEGIN { \
+		     SysVzone["EST5EDT"] = 1; SysVzone["CST6CDT"] = 1; \
+		     SysVzone["MST7MDT"] = 1; SysVzone["PST8PDT"] = 1; \
+		     print "-\t-\tUTC" \
+		   } \
+		   $$1 == "Zone" && !SysVzone[$$2] {print "-\t-\t" $$2}' \
 		  $(PRIMARY_YDATA) backward factory | \
 		 $(AWK) \
 		   -v zdump_table=$@d/zdump-1970.tab \
diff --git a/contrib/tzcode/NEWS b/contrib/tzcode/NEWS
index dd962b54d529..ae240d31b84e 100644
--- a/contrib/tzcode/NEWS
+++ b/contrib/tzcode/NEWS
@@ -1,5 +1,87 @@
 News for the tz database
 
+Release 2026d - 2026-09-11 15:21:07 -0700
+
+  Briefly:
+    Canada’s Northwest Territories moved to permanent -06 on 2026-08-21.
+    Obsolescent settings like TZ="EST5EDT" now conform better to POSIX.
+    Fix security, performance and porting bugs in zic and localtime.
+
+  Changes to future timestamps
+
+    Canada’s Northwest Territories will not fall back on 2026-11-01
+    and will stay on -06 year-round, matching Alberta’s recent change.
+    Model this with its traditional abbreviation CST.  Although the
+    change to permanent -06 legally took place on 2026-08-21,
+    temporarily model the change to occur on 2026-11-01 at 02:00
+    for the same reason as other recent temporary hacks.  (Caution:
+    see “NOTE FOR 2026b TEMPORARY HACK FOR CLDR AND CANADA” below.)
+    This affects only America/Inuvik as the rest of the territory is
+    covered by America/Edmonton, for which the equivalent change was
+    released in 2026c.
+
+  Changes to past timestamps
+
+    Colombia’s 1992-05-02 spring forward was at 00:00, not 24:00.
+
+    Iran’s 1979-05-26 spring forward was at 00:00, not 24:00.
+    (Thanks to N.F. Hase.)
+
+    The backward-compatibility names EST5EDT, CST6CDT, MST7MDT, and
+    PST8PDT now conform better to POSIX.  For example, EST5EDT now
+    always uses the abbreviation "EST" for standard time (now always 5
+    hours behind UT) and "EDT" for daylight saving time, whereas it
+    formerly had different UT offsets before standard time was
+    introduced and sometimes used abbreviations like "LMT", "EWT" and
+    "EPT", all contrary to POSIX.  Also, though not required by POSIX
+    these names now use US federal rules rather than rules of places
+    like New York, reverting to 2024a behavior.  This change affects
+    only timestamps before 1966-10-30 at 01:00 standard time.
+
+  Other data changes
+
+    The temporary hacks used for North American timekeeping changes
+    now work around a libstdc++ std::chrono bug in GCC 14.1-14.4,
+    15.1-15.2, and 16.1; see GCC bug 124851.  This data change does
+    not affect TZif files or timestamps.  The change does not work
+    around the related but less serious GCC bugs 116110 and 124513.
+    These GCC bugs are all fixed in GCC 16.2.
+
+  Changes to code
+
+    zic now rejects Link targets that would have invalid names, and
+    more efficiently processes Expires, Leap and Rule lines with years
+    far in the past or future.  (Thanks to Darren Carreras.)
+
+    zic now ports to systems that report lack of link support via
+    EINVAL, ENOSYS or EPERM errno values. (Thanks to Tom Lane.)
+
+    When tzset and related functions encounter a TZif file that is too
+    large for them, they now consistently fail instead of sometimes
+    silently ignoring excess parts of the file.
+
+    localtime-related functions no longer mishandle extreme timestamps
+    when given TZif files holding some unlikely timezone histories.
+    (Problem reported by David Sarkisyan.)
+
+    localtime-related functions no longer check the values of TZif
+    files’ standard/wall and UT/local indicators, which these
+    functions have not used since 2026a’s removal of the old
+    posixrules feature.
+
+    tzcode has been ported to Haiku.
+
+    localtime.c now works again by default on AIX and DragonFly BSD.
+
+    zic now rejects ‘:’ and ‘\’ in Zone and Link names when running on
+    Microsoft Windows.  (Problem reported by David Diaz.)
+
+  Changes to documentation
+
+    URLs for release tarballs in tz-link.html have been updated to
+    reflect their new canonical URLs on data.iana.org.
+
+
 Release 2026c - 2026-07-08 10:23:58 -0700
 
   Briefly:
@@ -15,7 +97,13 @@ Release 2026c - 2026-07-08 10:23:58 -0700
     CST.  Although the change to permanent -06 legally took place on
     2026-06-18, temporarily model the change to occur on 2026-11-01 at
     02:00 instead, for the same reason we introduced a similarly
-    temporary hack for British Columbia in 2026b.
+    temporary hack for British Columbia in 2026b.  (Caution: see
+    “NOTE FOR 2026b TEMPORARY HACK FOR CLDR AND CANADA” below.)
+
+      As the change affects both America/Edmonton and its backward
+      compatibility link, the obsolescent setting TZ="Canada/Mountain"
+      will now use the abbreviation CST for affected timestamps,
+      akin to TZ="Canada/Pacific" behavior introduced in 2026b.
 
       Although another TZDB release will likely be needed soon because
       Northwest Territories will likely follow Alberta, the legal
@@ -67,6 +155,7 @@ Release 2026b - 2026-04-22 23:06:43 -0700
 
   Briefly:
     British Columbia moved to permanent -07 on 2026-03-09.
+    zic no longer mishandles a last transition to a new time type.
     Some more overflow bugs have been fixed in zic.
 
   Changes to future timestamps
@@ -79,9 +168,33 @@ Release 2026b - 2026-04-22 23:06:43 -0700
     limitation in CLDR 48.1 (2026-01-08).  This temporary hack is
     planned to be removed after CLDR is fixed.
 
+      To match longstanding practice for -07 standard time in North
+      America, such as for America/Dawson, America/Dawson_Creek,
+      America/Fort_Nelson and America/Whitehorse, affected BC
+      timestamps use the abbreviation MST.  As the change affects both
+      America/Vancouver and its backward compatibility link, the
+      obsolescent setting TZ="Canada/Pacific" will now use the
+      abbreviation MST.  Future TZDB releases may change the
+      abbreviation if something else becomes popular across areas
+      observing -07, and may change it to "-07" if no alphabetic
+      consensus emerges.
+
   Changes to code
 
     zic no longer mishandles a last transition to a new time type.
+    NOTE FOR 2026b TEMPORARY HACK FOR CLDR AND CANADA:
+    This zic fix is needed for the temporary hack (mentioned above)
+    that works around the Canadian timekeeping bug in Unicode CLDR.
+    Without the fix, the temporary hack causes zic versions 2023d
+    through 2026a, in their default mode that generates slim output,
+    to generate a TZif file that violates Internet RFC 9636 §3.3.
+    The buggy file in turn causes some TZif readers, including tzcode
+    itself, to ignore America/Vancouver’s 2026-11-01 02:00 transition
+    from PDT (tm_isdst=1) to MST (tm_isdst=0).  Although the buggy
+    file does not cause any known TZif reader to mishandle UT offsets,
+    caution is advised when using zic 2023d through 2026a to compile
+    data from more-recent tz releases.  To work around this problem
+    when using these older zic versions, use ‘zic -b fat’.
 
     zic no longer overflows a buffer when generating a TZ string like
     "PST-167:59:58PDT-167:59:59,M11.5.6/-167:59:59,M12.5.6/-167:59:59",
diff --git a/contrib/tzcode/README b/contrib/tzcode/README
index f22ec5492d09..0b69d8ea4a4d 100644
--- a/contrib/tzcode/README
+++ b/contrib/tzcode/README
@@ -10,7 +10,7 @@ locations around the globe.  It is updated periodically to reflect
 changes made by political bodies to time zone boundaries, UTC offsets,
 and daylight-saving rules.
 
-See <https://www.iana.org/time-zones/repository/tz-link.html> or the
+See <https://data.iana.org/time-zones/tz-link.html> or the
 file tz-link.html for how to acquire the code and data.
 
 Once acquired, read the leading comments in the file ‘Makefile’
diff --git a/contrib/tzcode/localtime.c b/contrib/tzcode/localtime.c
index cd1bc8bd7523..67eba0fb7459 100644
--- a/contrib/tzcode/localtime.c
+++ b/contrib/tzcode/localtime.c
@@ -64,7 +64,7 @@ struct stat { char st_ctime, st_dev, st_ino; };
 # endif
 # if THREAD_PREFER_SINGLE
 #  ifndef HAVE___ISTHREADED
-#   if defined __FreeBSD__ || defined __OpenBSD__
+#   if defined __FreeBSD__ || defined __OpenBSD__ || defined __DragonFly__
 #    define HAVE___ISTHREADED 1
 #   else
 #    define HAVE___ISTHREADED 0
@@ -348,6 +348,10 @@ static int fstatat(int dd, char const *path, struct stat *st, int flags)
 #  include <sys/auxv.h>
 # endif
 
+/* Avoid clash if headers declare but libraries do not define issetugid.  */
+# undef issetugid
+# define issetugid localtime_issetugid
+
 /* Return 1 if the process is privileged, 0 otherwise.  */
 static int
 issetugid(void)
@@ -475,8 +479,6 @@ struct ttinfo {				/* time type information */
 					   -2**31 + 1 .. 2**31 - 1  */
 	desigidx_type	tt_desigidx;	/* abbreviation list index */
 	bool		tt_isdst;	/* used to set tm_isdst */
-	bool		tt_ttisstd;	/* transition is std time */
-	bool		tt_ttisut;	/* transition is UT */
 };
 
 struct lsinfo {				/* leap second information */
@@ -505,7 +507,6 @@ struct state {
 	int		timecnt;
 	int		typecnt;
 	int		charcnt;
-	bool		goback;
 	bool		goahead;
 	time_t		ats[TZ_MAX_TIMES];
 	unsigned char	types[TZ_MAX_TIMES];
@@ -569,12 +570,11 @@ struct rule {
 };
 
 static struct tm *gmtsub(struct state const *, time_t const *, int_fast32_t,
-			 struct tm *);
-static bool increment_overflow(int *, int);
+			 struct tm *, time_t *);
 static bool increment_overflow_time(time_t *, int_fast32_2s);
 static int_fast32_2s leapcorr(struct state const *, time_t);
 static struct tm *timesub(time_t const *, int_fast32_t, struct state const *,
-			  struct tm *);
+			  struct tm *, time_t *);
 static bool tzparse(char const *, struct state *, struct state const *);
 
 #ifndef ALL_STATE
@@ -641,8 +641,6 @@ init_ttinfo(struct ttinfo *s, int_fast32_t utoff, bool isdst,
   s->tt_utoff = utoff;
   s->tt_isdst = isdst;
   s->tt_desigidx = desigidx;
-  s->tt_ttisstd = false;
-  s->tt_ttisut = false;
 }
 
 /* Return true if SP's time type I does not specify local time.  */
@@ -838,10 +836,11 @@ tzfile_changed(int fd, struct stat *st)
        resolution if available, as this can help distinguish files on
        non-POSIX platforms where st_dev and st_ino are unreliable.  */
     struct timespec ctim;
+    /* Copy via members, as AIX 7.3 defaults to an incompatible st_ctim.  */
+    ctim.tv_sec = st->st_ctime;
 #if HAVE_STRUCT_STAT_ST_CTIM
-    ctim = st->st_ctim;
+    ctim.tv_nsec = st->st_ctim.tv_nsec;
 #else
-    ctim.tv_sec = st->st_ctime;
     ctim.tv_nsec = 0;
 #endif
 
@@ -896,9 +895,19 @@ union local_storage {
 };
 
 /* These tzload flags can be ORed together, and fit into 'char'.  */
-enum { TZLOAD_FROMENV = 1 }; /* The TZ string came from the environment.  */
-enum { TZLOAD_TZSTRING = 2 }; /* Read any newline-surrounded TZ string.  */
-enum { TZLOAD_TZDIR_SUB = 4 }; /* TZ should be a file under TZDIR.  */
+
+/* TZLOAD_FROMENV means the TZ string is a process-wide setting either
+   taken from the TZ environment variable or inferred from TZ being unset.
+   When 0 <= tz_change_interval, TZLOAD_FROMENV lets us skip reanalysis
+   of a TZif file that did not change during the interval.  */
+enum { TZLOAD_FROMENV = 1 };
+
+/* Read any newline-surrounded TZ string.  */
+enum { TZLOAD_TZSTRING = 2 };
+
+/* TZ should be a file under TZDIR.  */
+enum { TZLOAD_TZDIR_SUB = 4 };
+
 
 /* Load tz data from the file named NAME into *SP.  Respect TZLOADFLAGS.
    Use **LSPP for temporary storage.  Return 0 on
@@ -923,7 +932,7 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags,
 	struct stat st;
 	st.st_ctime = 0;
 
-	sp->goback = sp->goahead = false;
+	sp->goahead = false;
 
 	if (! name) {
 		name = TZDEFAULT;
@@ -1201,30 +1210,9 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags,
 		}
 		set_leapcount(sp, leapcnt);
 
-		for (i = 0; i < sp->typecnt; ++i) {
-			register struct ttinfo *	ttisp;
-
-			ttisp = &sp->ttis[i];
-			if (ttisstdcnt == 0)
-				ttisp->tt_ttisstd = false;
-			else {
-				if (*p != true && *p != false)
-				  return EFTYPE;
-				ttisp->tt_ttisstd = *p++;
-			}
-		}
-		for (i = 0; i < sp->typecnt; ++i) {
-			register struct ttinfo *	ttisp;
-
-			ttisp = &sp->ttis[i];
-			if (ttisutcnt == 0)
-				ttisp->tt_ttisut = false;
-			else {
-				if (*p != true && *p != false)
-						return EFTYPE;
-				ttisp->tt_ttisut = *p++;
-			}
-		}
+		/* Do not bother to validate standard/wall and UT/local
+		   indicators, as they are no longer used here.  */
+		p += ttisstdcnt + ttisutcnt;
 	    }
 
 	    nread -= p - up->buf;
@@ -1235,13 +1223,13 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags,
 	      break;
 	}
 	if ((tzloadflags & TZLOAD_TZSTRING) && nread > 2 &&
-		up->buf[0] == '\n' && up->buf[nread - 1] == '\n' &&
-		sp->typecnt + 2 <= TZ_MAX_TYPES) {
+		up->buf[0] == '\n' && up->buf[nread - 1] == '\n') {
 			struct state	*ts = &lsp->u.st;
 
 			up->buf[nread - 1] = '\0';
-			if (tzparse(&up->buf[1], ts, sp)) {
-
+			if (!tzparse(&up->buf[1], ts, sp))
+			  return EFTYPE;
+			else {
 			  /* Attempt to reuse existing abbreviations.
 			     Without this, America/Anchorage would
 			     consume 50 bytes for abbreviations, as
@@ -1249,7 +1237,6 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags,
 			     AHDT YST AKDT AKST) and ts->charcnt equals 10
 			     (for AKST AKDT).  Reusing means sp->charcnt can
 			     stay 40 in this example.  */
-			  int gotabbr = 0;
 			  int charcnt = sp->charcnt;
 			  for (i = 0; i < ts->typecnt; i++) {
 			    char *tsabbr = ts->chars + ts->ttis[i].tt_desigidx;
@@ -1257,22 +1244,25 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags,
 			    for (j = 0; j < charcnt; j++)
 			      if (strcmp(sp->chars + j, tsabbr) == 0) {
 				ts->ttis[i].tt_desigidx = j;
-				gotabbr++;
 				break;
 			      }
 			    if (! (j < charcnt)) {
 			      int tsabbrlen = strnlen(tsabbr, TZ_MAX_CHARS - j);
-			      if (j + tsabbrlen < TZ_MAX_CHARS) {
+			      if (TZ_MAX_CHARS <= j + tsabbrlen)
+				return EOVERFLOW;
+			      else {
 				char *cp = sp->chars + j;
 				cp = mempcpy(cp, tsabbr, tsabbrlen);
 				*cp = '\0';
 				charcnt = j + tsabbrlen + 1;
 				ts->ttis[i].tt_desigidx = j;
-				gotabbr++;
 			      }
 			    }
 			  }
-			  if (gotabbr == ts->typecnt) {
+
+			  if (TZ_MAX_TYPES - sp->typecnt < ts->typecnt)
+			    return EOVERFLOW;
+			  else {
 			    sp->charcnt = charcnt;
 
 			    /* Ignore any trailing, no-op transitions generated
@@ -1291,10 +1281,8 @@ tzloadbody(char const *name, struct state *sp, char tzloadflags,
 				  || (0 < sp->timecnt
 				      && t <= sp->ats[sp->timecnt - 1]))
 				continue;
-			      if (TZ_MAX_TIMES <= sp->timecnt) {
-				sp->goahead = false;
-				break;
-			      }
+			      if (TZ_MAX_TIMES <= sp->timecnt)
+				return EOVERFLOW;
 			      sp->ats[sp->timecnt] = t;
 			      sp->types[sp->timecnt] = (sp->typecnt
 							+ ts->types[i]);
@@ -1337,10 +1325,6 @@ static const int	mon_lengths[2][MONSPERYEAR] = {
 	{ 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }
 };
 
-static const int	year_lengths[2] = {
-	DAYSPERNYEAR, DAYSPERLYEAR
-};
-
 /* Is C an ASCII digit?  */
 static bool
 is_digit(char c)
@@ -1540,40 +1524,26 @@ getrule(const char *strp, register struct rule *const rulep)
 */
 
 static int_fast32_t
-transtime(const int year, register const struct rule *const rulep,
+transtime(time_t year, register const struct rule *const rulep,
 	  const int_fast32_t offset)
 {
+	int d;  /* Day of year (zero-origin).  */
 	register bool	leapyear;
-	register int_fast32_t value;
-	register int	i;
-	int		d, m1, yy0, yy1, yy2, dow;
 
 	leapyear = isleap(year);
-	switch (rulep->r_type) {
 
-	case JULIAN_DAY:
+	if (rulep->r_type <= DAY_OF_YEAR) {
 		/*
 		** Jn - Julian day, 1 == January 1, 60 == March 1 even in leap
 		** years.
 		** In non-leap years, or if the day number is 59 or less, just
-		** add SECSPERDAY times the day number-1 to the time of
-		** January 1, midnight, to get the day.
-		*/
-		value = (rulep->r_day - 1) * SECSPERDAY;
-		if (leapyear && rulep->r_day >= 60)
-			value += SECSPERDAY;
-		break;
-
-	case DAY_OF_YEAR:
-		/*
+		** use the day number - 1.
+		**
 		** n - day of year.
-		** Just add SECSPERDAY times the day number to the time of
-		** January 1, midnight, to get the day.
 		*/
-		value = rulep->r_day * SECSPERDAY;
-		break;
-
-	case MONTH_NTH_DAY_OF_WEEK:
+		d = rulep->r_day - ((rulep->r_type < DAY_OF_YEAR)
+				    & (!leapyear | (rulep->r_day <= 59)));
+	} else {
 		/*
 		** Mm.n.d - nth "dth day" of month m.
 		*/
@@ -1582,14 +1552,16 @@ transtime(const int year, register const struct rule *const rulep,
 		** Use Zeller's Congruence to get day-of-week of first day of
 		** month.
 		*/
-		m1 = (rulep->r_mon + 9) % 12 + 1;
-		yy0 = (rulep->r_mon <= 2) ? (year - 1) : year;
-		yy1 = yy0 / 100;
-		yy2 = yy0 % 100;
-		dow = ((26 * m1 - 2) / 10 +
-			1 + yy2 + yy2 / 4 + yy1 / 4 - 2 * yy1) % 7;
-		if (dow < 0)
-			dow += DAYSPERWEEK;
+		int i;
+		bool janfeb = rulep->r_mon <= 2;
+		int month = (rulep->r_mon
+			     + (janfeb ? MONSPERYEAR : 0)); /* 3..14  */
+		int adjustment = (TYPE_SIGNED(time_t) ? 0 : 400) - janfeb;
+		int ay_rem = (year + adjustment) % YEARSPERREPEAT;
+		int y = ay_rem + (ay_rem < 0 ? YEARSPERREPEAT : 0);
+		int dow = (((13 * (month + 1)) / 5
+			    + y + y / 4 - y / 100 + y / 400)
+			   % DAYSPERWEEK);
 
 		/*
 		** "dow" is the day-of-week of the first day of the month. Get
@@ -1597,33 +1569,24 @@ transtime(const int year, register const struct rule *const rulep,
 		** month.
 		*/
 		d = rulep->r_day - dow;
-		if (d < 0)
-			d += DAYSPERWEEK;
-		for (i = 1; i < rulep->r_week; ++i) {
-			if (d + DAYSPERWEEK >=
-				mon_lengths[leapyear][rulep->r_mon - 1])
-					break;
-			d += DAYSPERWEEK;
-		}
+		d += (rulep->r_week - (0 <= d)) * DAYSPERWEEK;
+		if (mon_lengths[leapyear][rulep->r_mon - 1] <= d)
+		  d -= DAYSPERWEEK;
 
 		/*
 		** "d" is the day-of-month (zero-origin) of the day we want.
 		*/
-		value = d * SECSPERDAY;
 		for (i = 0; i < rulep->r_mon - 1; ++i)
-			value += mon_lengths[leapyear][i] * SECSPERDAY;
-		break;
-
-	default: unreachable();
+		  d += mon_lengths[leapyear][i];
 	}
 
 	/*
-	** "value" is the year-relative time of 00:00:00 UT on the day in
+	** d is the origin-0 year-relative day in
 	** question. To get the year-relative time of the specified local
 	** time on that day, add the transition time and the current offset
-	** from UT.
+	** from UT to d * SECSPERDAY.
 	*/
-	return value + rulep->r_time + offset;
+	return d * SECSPERDAY + rulep->r_time + offset;
 }
 
 /*
@@ -1673,11 +1636,11 @@ tzparse(const char *name, struct state *sp, struct state const *basep)
 	  }
 	} else
 	  set_leapcount(sp, 0);	/* So, we're off a little.  */
-	sp->goback = sp->goahead = false;
+	sp->goahead = false;
 	if (*name != '\0') {
 		struct rule start, end;
-		int year, yearbeg, yearlim, timecnt;
-		time_t janfirst;
+		int timecnt;
+		time_t janfirst, repeatbeg, year, yearbeg, yearlim;
 		int_fast32_t janoffset = 0;
 
 		if (*name == '<') {
@@ -1715,18 +1678,20 @@ tzparse(const char *name, struct state *sp, struct state const *basep)
 		if (!name || *name)
 		  return false;
 		sp->typecnt = 2;	/* standard time and DST */
-		/*
-		** Two transitions per year, from EPOCH_YEAR forward.
-		*/
+
+		/* Two transitions per year, from atlo forward, and going on
+		   for years_of_observations past max(atlo, leaplo).  */
 		init_ttinfo(&sp->ttis[0], -stdoffset, false, 0);
 		init_ttinfo(&sp->ttis[1], -dstoffset, true, stdlen + 1);
 		timecnt = 0;
-		janfirst = 0;
-		yearbeg = EPOCH_YEAR;
+		repeatbeg = (atlo / SECSPERREPEAT
+			     + (atlo < 0 && 0 < atlo % SECSPERREPEAT));
+		yearbeg = repeatbeg * YEARSPERREPEAT + EPOCH_YEAR;
+		janfirst = repeatbeg * SECSPERREPEAT;
 
 		do {
 		  int_fast32_t yearsecs
-		    = year_lengths[isleap(yearbeg - 1)] * SECSPERDAY;
+		    = year_days(yearbeg - 1) * SECSPERDAY;
 		  time_t janfirst1 = janfirst;
 		  yearbeg--;
 		  if (increment_overflow_time(&janfirst1, -yearsecs)) {
@@ -1734,30 +1699,25 @@ tzparse(const char *name, struct state *sp, struct state const *basep)
 		    break;
 		  }
 		  janfirst = janfirst1;
-		} while (atlo < janfirst
-			 && EPOCH_YEAR - YEARSPERREPEAT / 2 < yearbeg);
+		} while (atlo < janfirst);
 
 		while (true) {
 		  int_fast32_t yearsecs
-		    = year_lengths[isleap(yearbeg)] * SECSPERDAY;
-		  int yearbeg1 = yearbeg;
+		    = year_days(yearbeg) * SECSPERDAY;
 		  time_t janfirst1 = janfirst;
 		  if (increment_overflow_time(&janfirst1, yearsecs)
-		      || increment_overflow(&yearbeg1, 1)
 		      || atlo <= janfirst1)
 		    break;
-		  yearbeg = yearbeg1;
+		  yearbeg++;
 		  janfirst = janfirst1;
 		}
 
-		yearlim = yearbeg;
-		if (increment_overflow(&yearlim, years_of_observations))
-		  yearlim = INT_MAX;
+		yearlim = yearbeg + years_of_observations;
 		for (year = yearbeg; year < yearlim; year++) {
 		  int_fast32_t
 		    starttime = transtime(year, &start, stdoffset),
 		    endtime = transtime(year, &end, dstoffset),
-		    yearsecs = year_lengths[isleap(year)] * SECSPERDAY;
+		    yearsecs = year_days(year) * SECSPERDAY;
 		  bool reversed = endtime < starttime;
 		  if (reversed) {
 		    int_fast32_t swap = starttime;
@@ -1767,24 +1727,25 @@ tzparse(const char *name, struct state *sp, struct state const *basep)
 		  if (reversed
 		      || (starttime < endtime
 			  && endtime - starttime < yearsecs)) {
-		    if (TZ_MAX_TIMES - 2 < timecnt)
-		      break;
-		    sp->ats[timecnt] = janfirst;
-		    if (! increment_overflow_time(&sp->ats[timecnt],
-						  janoffset + starttime)
-			&& atlo <= sp->ats[timecnt])
+		    time_t at_added = TIME_T_MAX;
+		    time_t at = janfirst;
+		    if (! increment_overflow_time(&at, janoffset + starttime)
+			&& atlo <= at) {
+		      if (TZ_MAX_TIMES <= timecnt)
+			return false;
+		      sp->ats[timecnt] = at_added = at;
 		      sp->types[timecnt++] = !reversed;
-		    sp->ats[timecnt] = janfirst;
-		    if (! increment_overflow_time(&sp->ats[timecnt],
-						  janoffset + endtime)
-			&& atlo <= sp->ats[timecnt]) {
+		    }
+		    at = janfirst;
+		    if (! increment_overflow_time(&at, janoffset + endtime)
+			&& atlo <= at) {
+		      if (TZ_MAX_TIMES <= timecnt)
+			return false;
+		      sp->ats[timecnt] = at_added = at;
 		      sp->types[timecnt++] = reversed;
 		    }
-		  }
-		  if (endtime < leaplo) {
-		    yearlim = year;
-		    if (increment_overflow(&yearlim, years_of_observations))
-		      yearlim = INT_MAX;
+		    if (at_added < leaplo)
+		      yearlim = year + years_of_observations;
 		  }
 		  if (increment_overflow_time(&janfirst, janoffset + yearsecs))
 		    break;
@@ -1795,7 +1756,7 @@ tzparse(const char *name, struct state *sp, struct state const *basep)
 		  sp->ttis[0] = sp->ttis[1];
 		  sp->typecnt = 1;	/* Perpetual DST.  */
 		} else if (years_of_observations <= year - yearbeg)
-		  sp->goback = sp->goahead = true;
+		  sp->goahead = true;
 	} else {
 		dstlen = 0;
 		sp->typecnt = 1;		/* only standard time */
@@ -1852,7 +1813,7 @@ zoneinit(struct state *sp, char const *name, char tzloadflags)
     sp->timecnt = 0;
     sp->typecnt = 0;
     sp->charcnt = 0;
-    sp->goback = sp->goahead = false;
+    sp->goahead = false;
     init_ttinfo(&sp->ttis[0], 0, false, 0);
     strcpy(sp->chars, utc);
     return 0;
@@ -2089,12 +2050,15 @@ tzfree(timezone_t sp)
 ** since in that case tzset should have already done this step correctly.
 ** SETNAME's type is int_fast32_t for compatibility with gmtsub,
 ** but it is actually a boolean and its value should be 0 or 1.
+**
+** If LTM_YEAR, store the resulting year-1900 into *LTM_YEAR rather
+** than into the default TMP->tm_year; this prevents year overflow.
 */
 
 /*ARGSUSED*/
 static struct tm *
 localsub(struct state const *sp, time_t const *timep, int_fast32_t setname,
-	 struct tm *const tmp)
+	 struct tm *tmp, time_t *ltm_year)
 {
 	register const struct ttinfo *	ttisp;
 	register int			i;
@@ -2103,52 +2067,39 @@ localsub(struct state const *sp, time_t const *timep, int_fast32_t setname,
 
 	if (sp == NULL) {
 	  /* Don't bother to set tzname etc.; tzset has already done it.  */
-	  return gmtsub(gmtptr, timep, 0, tmp);
+	  return gmtsub(gmtptr, timep, 0, tmp, ltm_year);
 	}
-	if ((sp->goback && t < sp->ats[0]) ||
-		(sp->goahead && t > sp->ats[sp->timecnt - 1])) {
-			time_t newt;
-			register time_t		seconds;
-			register time_t		years;
-
-			if (t < sp->ats[0])
-				seconds = sp->ats[0] - t;
-			else	seconds = t - sp->ats[sp->timecnt - 1];
-			--seconds;
-
-			/* Beware integer overflow, as SECONDS might
-			   be close to the maximum time_t.  */
-			years = seconds / SECSPERREPEAT * YEARSPERREPEAT;
-			seconds = years * AVGSECSPERYEAR;
-			years += YEARSPERREPEAT;
-			if (t < sp->ats[0])
-			  newt = t + seconds + SECSPERREPEAT;
-			else
-			  newt = t - seconds - SECSPERREPEAT;
-
-			if (newt < sp->ats[0] ||
-				newt > sp->ats[sp->timecnt - 1])
-					return NULL;	/* "cannot happen" */
-			result = localsub(sp, &newt, setname, tmp);
+	if (sp->goahead && sp->ats[sp->timecnt - 1] < t) {
+			/* Avoid integer overflow when time_t is signed, by
+			   using secs_div_2 twice; the full value would
+			   always be even, so halving does not round.  */
+			time_t
+			  tlo = sp->ats[sp->timecnt - 1],
+			  diffyears = ((t / 2 - tlo / 2
+					+ ((t % 2 - tlo % 2 + 2) / 2 - 1))
+				       / (SECSPERREPEAT / 2)
+				       * YEARSPERREPEAT),
+			  years = diffyears + YEARSPERREPEAT,
+			  secs_div_2 = (diffyears * (AVGSECSPERYEAR / 2)
+					+ SECSPERREPEAT / 2),
+			  newt = t - secs_div_2 - secs_div_2,
+			  ryear;
+
+			result = localsub(sp, &newt, setname, tmp, &ryear);
 			if (result) {
-# if defined ckd_add && defined ckd_sub
-				if (t < sp->ats[0]
-				    ? ckd_sub(&result->tm_year,
-					      result->tm_year, years)
-				    : ckd_add(&result->tm_year,
-					      result->tm_year, years))
+			  if (ltm_year)
+			    *ltm_year = ryear + years;
+			  else {
+# ifdef ckd_add
+				if (ckd_add(&result->tm_year, ryear, years))
 				  return NULL;
 # else
-				register int_fast64_t newy;
-
-				newy = result->tm_year;
-				if (t < sp->ats[0])
-					newy -= years;
-				else	newy += years;
+				time_t newy = ryear + years;
 				if (! (INT_MIN <= newy && newy <= INT_MAX))
 					return NULL;
 				result->tm_year = newy;
 # endif
+			  }
 			}
 			return result;
 	}
@@ -2172,9 +2123,9 @@ localsub(struct state const *sp, time_t const *timep, int_fast32_t setname,
 	** To get (wrong) behavior that's compatible with System V Release 2.0
 	** you'd replace the statement below with
 	**	t += ttisp->tt_utoff;
-	**	timesub(&t, 0, sp, tmp);
+	**	timesub(&t, 0, sp, tmp, ltm_year);
 	*/
-	result = timesub(&t, ttisp->tt_utoff, sp, tmp);
+	result = timesub(&t, ttisp->tt_utoff, sp, tmp, ltm_year);
 	if (result) {
 	  result->tm_isdst = ttisp->tt_isdst;
 # ifdef TM_ZONE
@@ -2224,7 +2175,7 @@ struct tm *
 localtime_rz(struct state *restrict sp, time_t const *restrict timep,
 	     struct tm *restrict tmp)
 {
-  return localsub(sp, timep, 0, tmp);
+  return localsub(sp, timep, 0, tmp, NULL);
 }
 # endif
 
@@ -2239,7 +2190,7 @@ localtime_tzset(time_t const *timep, struct tm *tmp, bool setname)
   }
   if (0 <= tz_change_interval || setname || !lcl_is_set)
     tzset_unlocked(!err, false, now);
-  tmp = localsub(lclptr, timep, setname, tmp);
+  tmp = localsub(lclptr, timep, setname, tmp, NULL);
   unlock(!err);
   return tmp;
 }
@@ -2266,11 +2217,11 @@ localtime_r(const time_t *restrict timep, struct tm *restrict tmp)
 
 static struct tm *
 gmtsub(ATTRIBUTE_MAYBE_UNUSED struct state const *sp, time_t const *timep,
-       int_fast32_t offset, struct tm *tmp)
+       int_fast32_t offset, struct tm *tmp, time_t *ltm_year)
 {
 	register struct tm *	result;
 
-	result = timesub(timep, offset, gmtptr, tmp);
+	result = timesub(timep, offset, gmtptr, tmp, ltm_year);
 #ifdef TM_ZONE
 	/*
 	** Could get fancy here and deliver something such as
@@ -2293,7 +2244,7 @@ struct tm *
 gmtime_r(time_t const *restrict timep, struct tm *restrict tmp)
 {
   gmtcheck();
-  return gmtsub(gmtptr, timep, 0, tmp);
+  return gmtsub(gmtptr, timep, 0, tmp, NULL);
 }
 
 struct tm *
@@ -2314,7 +2265,7 @@ struct tm *
 offtime_r(time_t const *restrict timep, long offset, struct tm *restrict tmp)
 {
   gmtcheck();
-  return gmtsub(gmtptr, timep, offset, tmp);
+  return gmtsub(gmtptr, timep, offset, tmp, NULL);
 }
 
 struct tm *
@@ -2350,7 +2301,7 @@ leaps_thru_end_of(time_t y)
 
 static struct tm *
 timesub(const time_t *timep, int_fast32_t offset,
-	const struct state *sp, struct tm *tmp)
+	const struct state *sp, struct tm *tmp, time_t *ltm_year)
 {
 	register time_t			tdays;
 	register const int *		ip;
@@ -2400,7 +2351,7 @@ timesub(const time_t *timep, int_fast32_t offset,
 	idays += dayoff % DAYSPERREPEAT + 2 * DAYSPERREPEAT;
 	idays %= DAYSPERREPEAT;
 	/* Increase Y and decrease IDAYS until IDAYS is in range for Y.  */
-	while (year_lengths[isleap(y)] <= idays) {
+	while (year_days(y) <= idays) {
 		int tdelta = idays / DAYSPERLYEAR;
 		int_fast32_t ydelta = tdelta + !tdelta;
 		time_t newy = y + ydelta;
@@ -2412,29 +2363,34 @@ timesub(const time_t *timep, int_fast32_t offset,
 		y = newy;
 	}
 
+	if (ltm_year) {
+	  *ltm_year = y - TM_YEAR_BASE;
+	} else {
 #ifdef ckd_add
-	if (ckd_add(&tmp->tm_year, y, -TM_YEAR_BASE)) {
-	  errno = EOVERFLOW;
-	  return NULL;
-	}
+	  if (ckd_add(&tmp->tm_year, y, -TM_YEAR_BASE)) {
+	    errno = EOVERFLOW;
+	    return NULL;
+	  }
 #else
-	if (!TYPE_SIGNED(time_t) && y < TM_YEAR_BASE) {
-	  int signed_y = y;
-	  tmp->tm_year = signed_y - TM_YEAR_BASE;
-	} else if ((!TYPE_SIGNED(time_t) || INT_MIN + TM_YEAR_BASE <= y)
-		   && y - TM_YEAR_BASE <= INT_MAX)
-	  tmp->tm_year = y - TM_YEAR_BASE;
-	else {
-	  errno = EOVERFLOW;
-	  return NULL;
-	}
+	  if (!TYPE_SIGNED(time_t) && y < TM_YEAR_BASE) {
+	    int signed_y = y;
+	    tmp->tm_year = signed_y - TM_YEAR_BASE;
+	  } else if ((!TYPE_SIGNED(time_t) || INT_MIN + TM_YEAR_BASE <= y)
+		     && y - TM_YEAR_BASE <= INT_MAX)
+	    tmp->tm_year = y - TM_YEAR_BASE;
+	  else {
+	    errno = EOVERFLOW;
+	    return NULL;
+	  }
 #endif
+	}
 	tmp->tm_yday = idays;
 	/*
 	** The "extra" mods below avoid overflow problems.
 	*/
 	tmp->tm_wday = (TM_WDAY_BASE
-			+ ((tmp->tm_year % DAYSPERWEEK)
+			+ ((y % DAYSPERWEEK - TM_YEAR_BASE % DAYSPERWEEK)
+			   % DAYSPERWEEK
 			   * (DAYSPERNYEAR % DAYSPERWEEK))
 			+ leaps_thru_end_of(y - 1)
 			- leaps_thru_end_of(TM_YEAR_BASE - 1)
@@ -2479,27 +2435,6 @@ timesub(const time_t *timep, int_fast32_t offset,
 ** Normalize logic courtesy Paul Eggert.
 */
 
-static bool
-increment_overflow(int *ip, int j)
-{
-#ifdef ckd_add
-	return ckd_add(ip, *ip, j);
*** 1226 LINES SKIPPED ***