This implements my suggestion in:
https://lists.iana.org/hyperkitty/list/[email protected]/thread/Y4WLBYNBTFFG2XEQHXWH7EO35ERVSS4B/
to add a compile-time option to disable localtime.c's support for
leap seconds, and thereby shrink the attack surface on tzcode.
* Makefile, NEWS, date.1, etcetera, zic.8: Mention this.
* localtime.c (struct state): Define leapcnt and lsis
members only if TZ_RUNTIME_LEAPS.
All uses removed and replaced with the following:
(leapcount, set_leapcount, lsinfo, set_lsinfo):
New static getter and setter functions for leap second data.
(tzloadbody) [!TZ_RUNTIME_LEAPS]: Reject TZif files with leap seconds.
(gmtload) [!TZ_RUNTIME_LEAPS]: Do not load from TZLOAD_TZSTRING
since we are not doing leap seconds.
* private.h (TZ_RUNTIME_LEAPS, ATTRIBUTE_POSIX2TIME): New macros.
(posix2time_z, time2posix_z): Declare with ATTRIBUTE_POSIX2TIME
not ATTRIBUTE_PURE, because these functions are const (not merely
pure) when leap seconds are disabled.
---
Makefile | 4 ++
NEWS | 6 +++
date.1 | 2 +-
etcetera | 3 +-
localtime.c | 125 ++++++++++++++++++++++++++++++++++------------------
private.h | 13 +++++-
zic.8 | 3 +-
7 files changed, 108 insertions(+), 48 deletions(-)
diff --git a/Makefile b/Makefile
index 021186fc..1e0a5903 100644
--- a/Makefile
+++ b/Makefile
@@ -328,6 +328,10 @@ LDLIBS=
# -DTZ_DOMAIN=\"foo\" to use "foo" for gettext domain name; default is "tz"
# -DTZ_DOMAINDIR=\"/path\" to use "/path" for gettext directory;
# the default is system-supplied, typically "/usr/lib/locale"
+# -DTZ_RUNTIME_LEAPS=0 to disable runtime support for leap seconds.
+# This conforms to POSIX, shrinks tzcode's attack surface,
+# and is more efficient. However, it fails to support Internet
+# RFC 9636's leap seconds.
# -DTZDEFRULESTRING=\",date/time,date/time\" to default to the specified
# DST transitions for proleptic format TZ strings lacking them.
# If not specified, it defaults to US rules for future DST transitions.
diff --git a/NEWS b/NEWS
index 995507f0..95b1851c 100644
--- a/NEWS
+++ b/NEWS
@@ -4,6 +4,7 @@ Unreleased, experimental changes
Briefly:
The "right" TZif files are no longer installed by default.
+ -DTZ_RUNTIME_LEAPS=0 disables runtime support for leap seconds.
Several integer overflow bugs have been fixed.
Changes to build procedure
@@ -23,6 +24,11 @@ Unreleased, experimental changes
Changes to code
+ Compiling with the new option -DTZ_RUNTIME_LEAPS=0 disables
+ runtime support for leap seconds. Although this conforms to
+ POSIX, shrinks tzcode's attack surface, and is more efficient,
+ it fails to support Internet RFC 9636's leap seconds.
+
localtime.c no longer accesses the posixrules file generated by
zic -p. Hence for obsolete and nonconforming settings like
TZ="AST4ADT" it now typically falls back on US DST rules, rather
diff --git a/date.1 b/date.1
index 5f334358..8f2c212b 100644
--- a/date.1
+++ b/date.1
@@ -77,6 +77,6 @@ hexadecimal (leading 0x), preceded by an optional sign.
.br
/usr/share/zoneinfo timezone directory
.br
-/usr/share/zoneinfo/Etc/UTC for UTC leap seconds
+/usr/share/zoneinfo/Etc/UTC for UTC leap seconds, if supported
.SH SEE ALSO
.BR strftime (3).
diff --git a/etcetera b/etcetera
index 948531c8..d78f0413 100644
--- a/etcetera
+++ b/etcetera
@@ -20,7 +20,8 @@
# which load the "UTC" file to handle seconds properly.
Zone Etc/UTC 0 - UTC
-# Functions like gmtime load the "GMT" file to handle leap seconds properly.
+# If leap second support is enabled, functions like gmtime
+# load the "GMT" file to handle leap seconds properly.
# Vanguard section, which works with most .zi parsers.
#Zone GMT 0 - GMT
# Rearguard section, for TZUpdater 2.3.2 and earlier.
diff --git a/localtime.c b/localtime.c
index 40a4d7ee..ed7a32f2 100644
--- a/localtime.c
+++ b/localtime.c
@@ -498,7 +498,9 @@ enum { CHARS_EXTRA = max(sizeof UNSPEC, 2) - 1 };
are put on the stack and stacks are relatively small on some platforms.
See tzfile.h for more about the sizes. */
struct state {
+#if TZ_RUNTIME_LEAPS
int leapcnt;
+#endif
int timecnt;
int typecnt;
int charcnt;
@@ -509,9 +511,48 @@ struct state {
struct ttinfo ttis[TZ_MAX_TYPES];
char chars[max(max(TZ_MAX_CHARS + CHARS_EXTRA, sizeof "UTC"),
2 * (TZNAME_MAXIMUM + 1))];
+#if TZ_RUNTIME_LEAPS
struct lsinfo lsis[TZ_MAX_LEAPS];
+#endif
};
+static int
+leapcount(ATTRIBUTE_MAYBE_UNUSED struct state const *sp)
+{
+#if TZ_RUNTIME_LEAPS
+ return sp->leapcnt;
+#else
+ return 0;
+#endif
+}
+static void
+set_leapcount(ATTRIBUTE_MAYBE_UNUSED struct state *sp,
+ ATTRIBUTE_MAYBE_UNUSED int leapcnt)
+{
+#if TZ_RUNTIME_LEAPS
+ sp->leapcnt = leapcnt;
+#endif
+}
+static struct lsinfo
+lsinfo(ATTRIBUTE_MAYBE_UNUSED struct state const *sp,
+ ATTRIBUTE_MAYBE_UNUSED int i)
+{
+#if TZ_RUNTIME_LEAPS
+ return sp->lsis[i];
+#else
+ unreachable();
+#endif
+}
+static void
+set_lsinfo(ATTRIBUTE_MAYBE_UNUSED struct state *sp,
+ ATTRIBUTE_MAYBE_UNUSED int i,
+ ATTRIBUTE_MAYBE_UNUSED struct lsinfo lsinfo)
+{
+#if TZ_RUNTIME_LEAPS
+ sp->lsis[i] = lsinfo;
+#endif
+}
+
enum r_type {
JULIAN_DAY, /* Jn = Julian day */
DAY_OF_YEAR, /* n = day of year */
@@ -1005,8 +1046,6 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
char version = up->tzhead.tzh_version[0];
bool skip_datablock = stored == 4 && version;
int_fast32_t datablock_size;
- int_fast64_t prevtr = -1;
- int_fast32_2s prevcorr;
int_fast32_2s
ttisstdcnt = detzcode(up->tzhead.tzh_ttisstdcnt),
ttisutcnt = detzcode(up->tzhead.tzh_ttisutcnt),
@@ -1018,7 +1057,8 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
/* Although tzfile(5) currently requires typecnt to be nonzero,
support future formats that may allow zero typecnt
in files that have a TZ string and no transitions. */
- if (! (0 <= leapcnt && leapcnt <= TZ_MAX_LEAPS
+ if (! (0 <= leapcnt
+ && leapcnt <= (TZ_RUNTIME_LEAPS ? TZ_MAX_LEAPS : 0)
&& 0 <= typecnt && typecnt <= TZ_MAX_TYPES
&& 0 <= timecnt && timecnt <= TZ_MAX_TIMES
&& 0 <= charcnt && charcnt <= TZ_MAX_CHARS
@@ -1037,12 +1077,13 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
return EINVAL;
if (skip_datablock)
p += datablock_size;
+ else if (! ((ttisstdcnt == typecnt || ttisstdcnt == 0)
+ && (ttisutcnt == typecnt || ttisutcnt == 0)))
+ return EINVAL;
else {
- if (! ((ttisstdcnt == typecnt || ttisstdcnt == 0)
- && (ttisutcnt == typecnt || ttisutcnt == 0)))
- return EINVAL;
-
- sp->leapcnt = leapcnt;
+ int_fast64_t prevtr = -1;
+ int_fast32_2s prevcorr;
+ set_leapcount(sp, leapcnt);
sp->timecnt = timecnt;
sp->typecnt = typecnt;
sp->charcnt = charcnt;
@@ -1110,7 +1151,7 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
/* Read leap seconds, discarding those out of time_t range. */
leapcnt = 0;
- for (i = 0; i < sp->leapcnt; ++i) {
+ for (i = 0; i < leapcount(sp); i++) {
int_fast64_t tr = stored == 4 ? detzcode(p) : detzcode64(p);
int_fast32_2s corr = detzcode(p + stored);
p += stored + 4;
@@ -1135,12 +1176,14 @@ tzloadbody(char const *name, struct state *sp, char
tzloadflags,
prevcorr = corr;
if (tr <= TIME_T_MAX) {
- sp->lsis[leapcnt].ls_trans = tr;
- sp->lsis[leapcnt].ls_corr = corr;
+ struct lsinfo ls;
+ ls.ls_trans = tr;
+ ls.ls_corr = corr;
+ set_lsinfo(sp, leapcnt, ls);
leapcnt++;
}
}
- sp->leapcnt = leapcnt;
+ set_leapcount(sp, leapcnt);
for (i = 0; i < sp->typecnt; ++i) {
register struct ttinfo * ttisp;
@@ -1605,13 +1648,15 @@ tzparse(const char *name, struct state *sp, struct
state const *basep)
if (basep) {
if (0 < basep->timecnt)
atlo = basep->ats[basep->timecnt - 1];
- sp->leapcnt = basep->leapcnt;
- if (0 < sp->leapcnt) {
- memcpy(sp->lsis, basep->lsis, sp->leapcnt * sizeof *sp->lsis);
- leaplo = sp->lsis[sp->leapcnt - 1].ls_trans;
+ set_leapcount(sp, leapcount(basep));
+ if (0 < leapcount(sp)) {
+ int i;
+ for (i = 0; i < leapcount(sp); i++)
+ set_lsinfo(sp, i, lsinfo(basep, i));
+ leaplo = lsinfo(sp, leapcount(sp) - 1).ls_trans;
}
} else
- sp->leapcnt = 0; /* So, we're off a little. */
+ set_leapcount(sp, 0); /* So, we're off a little. */
sp->goback = sp->goahead = false;
if (*name != '\0') {
struct rule start, end;
@@ -1755,7 +1800,7 @@ tzparse(const char *name, struct state *sp, struct state
const *basep)
static void
gmtload(struct state *const sp)
{
- if (tzload(etc_utc, sp, TZLOAD_TZSTRING) != 0)
+ if (!TZ_RUNTIME_LEAPS || tzload(etc_utc, sp, TZLOAD_TZSTRING) != 0)
tzparse("UTC0", sp, NULL);
}
@@ -1787,7 +1832,7 @@ zoneinit(struct state *sp, char const *name, char
tzloadflags)
/*
** User wants it fast rather than right.
*/
- sp->leapcnt = 0; /* so, we're off a little */
+ set_leapcount(sp, 0); /* so, we're off a little */
sp->timecnt = 0;
sp->typecnt = 0;
sp->charcnt = 0;
@@ -2274,7 +2319,6 @@ static struct tm *
timesub(const time_t *timep, int_fast32_t offset,
const struct state *sp, struct tm *tmp)
{
- register const struct lsinfo * lp;
register time_t tdays;
register const int * ip;
int_fast32_2s corr;
@@ -2288,13 +2332,13 @@ timesub(const time_t *timep, int_fast32_t offset,
time_t secs_since_posleap = SECSPERMIN;
corr = 0;
- i = (sp == NULL) ? 0 : sp->leapcnt;
+ i = sp ? leapcount(sp) : 0;
while (--i >= 0) {
- lp = &sp->lsis[i];
- if (*timep >= lp->ls_trans) {
- corr = lp->ls_corr;
- if ((i == 0 ? 0 : lp[-1].ls_corr) < corr)
- secs_since_posleap = *timep - lp->ls_trans;
+ struct lsinfo ls = lsinfo(sp, i);
+ if (ls.ls_trans <= *timep) {
+ corr = ls.ls_corr;
+ if ((i == 0 ? 0 : lsinfo(sp, i - 1).ls_corr) < corr)
+ secs_since_posleap = *timep - ls.ls_trans;
break;
}
}
@@ -2948,14 +2992,13 @@ timegm(struct tm *tmp)
static int_fast32_2s
leapcorr(struct state const *sp, time_t t)
{
- register struct lsinfo const * lp;
register int i;
- i = sp->leapcnt;
+ i = leapcount(sp);
while (--i >= 0) {
- lp = &sp->lsis[i];
- if (t >= lp->ls_trans)
- return lp->ls_corr;
+ struct lsinfo ls = lsinfo(sp, i);
+ if (ls.ls_trans <= t)
+ return ls.ls_corr;
}
return 0;
}
@@ -3031,13 +3074,12 @@ NETBSD_INSPIRED_EXTERN time_t
posix2time_z(struct state *sp, time_t t)
{
int i;
- for (i = sp->leapcnt; 0 <= --i; ) {
- struct lsinfo *lp = &sp->lsis[i];
- int_fast32_2s corr = lp->ls_corr;
+ for (i = leapcount(sp); 0 <= --i; ) {
+ struct lsinfo ls = lsinfo(sp, i);
time_t t_corr = t;
- if (increment_overflow_time(&t_corr, corr)) {
- if (0 <= corr) {
+ if (increment_overflow_time(&t_corr, ls.ls_corr)) {
+ if (0 <= ls.ls_corr) {
/* Overflow near maximum time_t value with positive correction.
This can happen with ordinary TZif files with leap seconds. */
errno = EOVERFLOW;
@@ -3046,13 +3088,10 @@ posix2time_z(struct state *sp, time_t t)
/* A negative correction overflowed, so keep going.
This can happen with unrealistic-but-valid TZif files. */
}
- } else {
- time_t trans = lp->ls_trans;
- if (trans <= t_corr)
- return (t_corr
- - (trans == t_corr
- && (i == 0 ? 0 : sp->lsis[i - 1].ls_corr) < corr));
- }
+ } else if (ls.ls_trans <= t_corr)
+ return (t_corr
+ - (ls.ls_trans == t_corr
+ && (i == 0 ? 0 : lsinfo(sp, i - 1).ls_corr) < ls.ls_corr));
}
return t;
}
diff --git a/private.h b/private.h
index e8cf9c22..6a58b073 100644
--- a/private.h
+++ b/private.h
@@ -193,6 +193,10 @@
# define ctime_r _incompatible_ctime_r
#endif /* HAVE_INCOMPATIBLE_CTIME_R */
+#ifndef TZ_RUNTIME_LEAPS
+# define TZ_RUNTIME_LEAPS 1
+#endif
+
/*
** Nested includes
*/
@@ -922,11 +926,16 @@ time_t mktime_z(timezone_t restrict, struct tm *restrict);
timezone_t tzalloc(char const *);
void tzfree(timezone_t);
# if STD_INSPIRED
+# if TZ_RUNTIME_LEAPS
+# define ATTRIBUTE_POSIX2TIME ATTRIBUTE_PURE
+# else
+# define ATTRIBUTE_POSIX2TIME ATTRIBUTE_CONST
+# endif
# if TZ_TIME_T || !defined posix2time_z
-ATTRIBUTE_PURE time_t posix2time_z(timezone_t, time_t);
+ATTRIBUTE_POSIX2TIME time_t posix2time_z(timezone_t, time_t);
# endif
# if TZ_TIME_T || !defined time2posix_z
-ATTRIBUTE_PURE time_t time2posix_z(timezone_t, time_t);
+ATTRIBUTE_POSIX2TIME time_t time2posix_z(timezone_t, time_t);
# endif
# endif
#endif
diff --git a/zic.8 b/zic.8
index 73b19ca9..05eea105 100644
--- a/zic.8
+++ b/zic.8
@@ -107,7 +107,8 @@ any already-existing link is removed.
.BI "\-L " leapsecondfilename
Read leap second information from the file with the given name.
If this option is not used,
-no leap second information appears in output files.
+no leap second information appears in output files;
+this is required by some TZif readers.
.TP
.BI "\-p " timezone
Act as if the input contained a link line of the form
--
2.52.0