* Makefile (strftime.o): Depend on localtime.c. * NEWS: Mention this. * localtime.c (USE_TIMEX_T): Provide default for new macro. (timex_t) [USE_TIMEX_T]: New type. (time_t, TIME_T_MIN, TIME_T_MAX) [USE_TIMEX_T]: (timeoff) [USE_TIMEX_T && TM_GMTOFF]: #define to timex_timeoff. (mktime) [USE_TIMEX_T && !TM_GMTOFF]: #define to timex_mktime. (EXTERN_TIMEOFF) [USE_TIMEX_T && TM_GMTOFF]: (utc, lcl_TZname, lcl_is_set, tm, tzname, timezone, daylight) (altzone, update_tzname_etc, may_update_tzname_etc, settzname) (scrub_abbrs, zoneinit, tzset_unlocked, tzset, tzalloc, tzfree) (localsub, localtime_rz, localtime_tzset, localtime, localtime_r) (gmtime_r, gmtime, offtime, mktime_tzname, mktime_z, timelocal) (timeoff, timegm, time2posix_z, time2posix, posix2time_z) (posix2time, time): Define only if still used even when USE_TIMEX_T. (mktime, timeoff) [USE_TIMEX_T]: Now static, because these are now actually timex_mktime and timex_timeoff and are local to strftime.c. * private.h (defined_time_t, MKTIME_FITS_IN, MKTIME_MIGHT_OVERFLOW): New macros. (timeoff) [USE_TIMEX_T]: Do not #define to tz_private_timeoff as it should be defined to timex_timeoff. * strftime.c [!MKTIME_MIGHT_OVERFLOW]: #define USE_TIMEX_T and include localtime.c. * newstrftime.3: Document the improved behavior. --- Makefile | 3 +- NEWS | 7 ++ localtime.c | 208 ++++++++++++++++++++++++++++++++++---------------- newstrftime.3 | 15 ++-- private.h | 57 +++++++++++++- strftime.c | 15 +++- 6 files changed, 226 insertions(+), 79 deletions(-) diff --git a/Makefile b/Makefile index 6e623eb0..01db50d6 100644 --- a/Makefile +++ b/Makefile @@ -255,6 +255,7 @@ LDLIBS= # -DHAVE_UNISTD_H=0 if <unistd.h> does not work* # -DHAVE_UTMPX_H=0 if <utmpx.h> does not work* # -Dlocale_t=XXX if your system uses XXX instead of locale_t +# -DMKTIME_MIGHT_OVERFLOW if mktime might fail due to time_t overflow # -DPORT_TO_C89 if tzcode should also run on mostly-C89 platforms+ # Typically it is better to use a later standard. For example, # with GCC 4.9.4 (2016), prefer '-std=gnu11' to '-DPORT_TO_C89'. @@ -1358,7 +1359,7 @@ asctime.o: private.h tzfile.h date.o: private.h difftime.o: private.h localtime.o: private.h tzfile.h tzdir.h -strftime.o: private.h tzfile.h +strftime.o: localtime.c private.h tzfile.h zdump.o: version.h zic.o: private.h tzfile.h tzdir.h version.h diff --git a/NEWS b/NEWS index d1fc28f6..5ed2f9f5 100644 --- a/NEWS +++ b/NEWS @@ -26,6 +26,13 @@ Unreleased, experimental changes Changes to code + strftime %s now generates the correct numeric string even when the + represented number does not fit into time_t. This is better than + generating the numeric equivalent of (time_t) -1, as strftime did + in TZDB releases 96a (when %s was introduced) through 2020a and in + releases 2022b through 2024b. It is also better than failing and + returning 0, as strftime did in releases 2020b through 2022a. + strftime now outputs an invalid conversion specifier as-is, instead of eliding the leading '%', which confused debugging. diff --git a/localtime.c b/localtime.c index f14ee80c..07ce41b8 100644 --- a/localtime.c +++ b/localtime.c @@ -29,6 +29,46 @@ static int lock(void) { return 0; } static void unlock(void) { } #endif +/* On platforms where offtime or mktime might overflow, + strftime.c defines USE_TIMEX_T to be true and includes us. + This tells us to #define time_t to an internal type timex_t that is + wide enough so that strftime %s never suffers from integer overflow, + and to #define offtime (if TM_GMTOFF is defined) or mktime (otherwise) + to a static function that returns the redefined time_t. + It also tells us to define only data and code needed + to support the offtime or mktime variant. */ +#ifndef USE_TIMEX_T +# define USE_TIMEX_T false +#endif +#if USE_TIMEX_T +# undef TIME_T_MIN +# undef TIME_T_MAX +# undef time_t +# define time_t timex_t +# if MKTIME_FITS_IN(LONG_MIN, LONG_MAX) +typedef long timex_t; +# define TIME_T_MIN LONG_MIN +# define TIME_T_MAX LONG_MAX +# elif MKTIME_FITS_IN(LLONG_MIN, LLONG_MAX) +typedef long long timex_t; +# define TIME_T_MIN LLONG_MIN +# define TIME_T_MAX LLONG_MAX +# else +typedef intmax_t timex_t; +# define TIME_T_MIN INTMAX_MIN +# define TIME_T_MAX INTMAX_MAX +# endif + +# ifdef TM_GMTOFF +# undef timeoff +# define timeoff timex_timeoff +# undef EXTERN_TIMEOFF +# else +# undef mktime +# define mktime timex_mktime +# endif +#endif + #ifndef TZ_ABBR_CHAR_SET # define TZ_ABBR_CHAR_SET \ "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789 :+-._" @@ -72,7 +112,10 @@ static void unlock(void) { } static const char wildabbr[] = WILDABBR; static char const etc_utc[] = "Etc/UTC"; + +#if defined TM_ZONE || ((!USE_TIMEX_T || !defined TM_GMTOFF) && defined TZ_NAME) static char const *utc = etc_utc + sizeof "Etc/" - 1; +#endif /* ** The DST rules to use if TZ has no rules and we can't load TZDEFRULES. @@ -172,8 +215,10 @@ static struct state *const gmtptr = &gmtmem; # define TZ_STRLEN_MAX 255 #endif /* !defined TZ_STRLEN_MAX */ +#if !USE_TIMEX_T || !defined TM_GMTOFF static char lcl_TZname[TZ_STRLEN_MAX + 1]; static int lcl_is_set; +#endif /* ** Section 4.12.3 of X3.159-1989 requires that @@ -187,22 +232,26 @@ static int lcl_is_set; ** trigger latent bugs in programs. */ -#if SUPPORT_C89 +#if !USE_TIMEX_T + +# if SUPPORT_C89 static struct tm tm; #endif -#if 2 <= HAVE_TZNAME + TZ_TIME_T +# if 2 <= HAVE_TZNAME + TZ_TIME_T char * tzname[2] = { (char *) wildabbr, (char *) wildabbr }; -#endif -#if 2 <= USG_COMPAT + TZ_TIME_T +# endif +# if 2 <= USG_COMPAT + TZ_TIME_T long timezone; int daylight; -#endif -#if 2 <= ALTZONE + TZ_TIME_T +# endif +# if 2 <= ALTZONE + TZ_TIME_T long altzone; +# endif + #endif /* Initialize *S to a value based on UTOFF, ISDST, and DESIGIDX. */ @@ -271,20 +320,22 @@ detzcode64(const char *const codep) return result; } +#if !USE_TIMEX_T || !defined TM_GMTOFF + static void update_tzname_etc(struct state const *sp, struct ttinfo const *ttisp) { -#if HAVE_TZNAME +# if HAVE_TZNAME tzname[ttisp->tt_isdst] = (char *) &sp->chars[ttisp->tt_desigidx]; -#endif -#if USG_COMPAT +# endif +# if USG_COMPAT if (!ttisp->tt_isdst) timezone = - ttisp->tt_utoff; -#endif -#if ALTZONE +# endif +# if ALTZONE if (ttisp->tt_isdst) altzone = - ttisp->tt_utoff; -#endif +# endif } /* If STDDST_MASK indicates that SP's TYPE provides useful info, @@ -315,18 +366,18 @@ settzname(void) When STDDST_MASK becomes zero we can stop looking. */ int stddst_mask = 0; -#if HAVE_TZNAME +# if HAVE_TZNAME tzname[0] = tzname[1] = (char *) (sp ? wildabbr : utc); stddst_mask = 3; -#endif -#if USG_COMPAT +# endif +# if USG_COMPAT timezone = 0; stddst_mask = 3; -#endif -#if ALTZONE +# endif +# if ALTZONE altzone = 0; stddst_mask |= 2; -#endif +# endif /* ** And to get the latest time zone abbreviations into tzname. . . */ @@ -336,9 +387,9 @@ settzname(void) for (i = sp->typecnt - 1; stddst_mask && 0 <= i; i--) stddst_mask = may_update_tzname_etc(stddst_mask, sp, i); } -#if USG_COMPAT +# if USG_COMPAT daylight = stddst_mask >> 1 ^ 1; -#endif +# endif } /* Replace bogus characters in time zone abbreviations. @@ -365,6 +416,8 @@ scrub_abbrs(struct state *sp) return 0; } +#endif + /* Input buffer for data read from a compiled tz file. */ union input_buffer { /* The first part of the buffer, interpreted as a header. */ @@ -1307,6 +1360,8 @@ gmtload(struct state *const sp) tzparse("UTC0", sp, NULL); } +#if !USE_TIMEX_T || !defined TM_GMTOFF + /* Initialize *SP to a value appropriate for the TZ setting NAME. Return 0 on success, an errno value on failure. */ static int @@ -1344,10 +1399,10 @@ tzset_unlocked(void) ? lcl_is_set < 0 : 0 < lcl_is_set && strcmp(lcl_TZname, name) == 0) return; -#ifdef ALL_STATE +# ifdef ALL_STATE if (! sp) lclptr = sp = malloc(sizeof *lclptr); -#endif /* defined ALL_STATE */ +# endif if (sp) { if (zoneinit(sp, name) != 0) zoneinit(sp, ""); @@ -1358,6 +1413,9 @@ tzset_unlocked(void) lcl_is_set = lcl; } +#endif + +#if !USE_TIMEX_T void tzset(void) { @@ -1366,6 +1424,7 @@ tzset(void) tzset_unlocked(); unlock(); } +#endif static void gmtcheck(void) @@ -1384,7 +1443,7 @@ gmtcheck(void) unlock(); } -#if NETBSD_INSPIRED +#if NETBSD_INSPIRED && !USE_TIMEX_T timezone_t tzalloc(char const *name) @@ -1420,6 +1479,8 @@ tzfree(timezone_t sp) #endif +#if !USE_TIMEX_T || !defined TM_GMTOFF + /* ** The easy way to behave "as if no library function calls" localtime ** is to not call it, so we drop its guts into "localsub", which can be @@ -1474,14 +1535,14 @@ localsub(struct state const *sp, time_t const *timep, int_fast32_t setname, return NULL; /* "cannot happen" */ result = localsub(sp, &newt, setname, tmp); if (result) { -#if defined ckd_add && defined ckd_sub +# 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)) return NULL; -#else +# else register int_fast64_t newy; newy = result->tm_year; @@ -1491,7 +1552,7 @@ localsub(struct state const *sp, time_t const *timep, int_fast32_t setname, if (! (INT_MIN <= newy && newy <= INT_MAX)) return NULL; result->tm_year = newy; -#endif +# endif } return result; } @@ -1520,25 +1581,26 @@ localsub(struct state const *sp, time_t const *timep, int_fast32_t setname, result = timesub(&t, ttisp->tt_utoff, sp, tmp); if (result) { result->tm_isdst = ttisp->tt_isdst; -#ifdef TM_ZONE +# ifdef TM_ZONE result->TM_ZONE = (char *) &sp->chars[ttisp->tt_desigidx]; -#endif /* defined TM_ZONE */ +# endif if (setname) update_tzname_etc(sp, ttisp); } return result; } +#endif -#if NETBSD_INSPIRED +#if !USE_TIMEX_T +# if NETBSD_INSPIRED struct tm * localtime_rz(struct state *restrict sp, time_t const *restrict timep, struct tm *restrict tmp) { return localsub(sp, timep, 0, tmp); } - -#endif +# endif static struct tm * localtime_tzset(time_t const *timep, struct tm *tmp, bool setname) @@ -1558,9 +1620,9 @@ localtime_tzset(time_t const *timep, struct tm *tmp, bool setname) struct tm * localtime(const time_t *timep) { -#if !SUPPORT_C89 +# if !SUPPORT_C89 static struct tm tm; -#endif +# endif return localtime_tzset(timep, &tm, true); } @@ -1569,6 +1631,7 @@ localtime_r(const time_t *restrict timep, struct tm *restrict tmp) { return localtime_tzset(timep, tmp, false); } +#endif /* ** gmtsub is to gmtime as localsub is to localtime. @@ -1593,6 +1656,8 @@ gmtsub(ATTRIBUTE_MAYBE_UNUSED struct state const *sp, time_t const *timep, return result; } +#if !USE_TIMEX_T + /* * Re-entrant version of gmtime. */ @@ -1607,13 +1672,13 @@ gmtime_r(time_t const *restrict timep, struct tm *restrict tmp) struct tm * gmtime(const time_t *timep) { -#if !SUPPORT_C89 +# if !SUPPORT_C89 static struct tm tm; -#endif +# endif return gmtime_r(timep, &tm); } -#if STD_INSPIRED +# if STD_INSPIRED /* This function is obsolescent and may disappear in future releases. Callers can instead use localtime_rz with a fixed-offset zone. */ @@ -1623,12 +1688,13 @@ offtime(const time_t *timep, long offset) { gmtcheck(); -#if !SUPPORT_C89 +# if !SUPPORT_C89 static struct tm tm; -#endif +# endif return gmtsub(gmtptr, timep, offset, &tm); } +# endif #endif /* @@ -2189,6 +2255,8 @@ time1(struct tm *const tmp, return WRONG; } +#if !defined TM_GMTOFF || !USE_TIMEX_T + static time_t mktime_tzname(struct state *sp, struct tm *tmp, bool setname) { @@ -2200,16 +2268,9 @@ mktime_tzname(struct state *sp, struct tm *tmp, bool setname) } } -#if NETBSD_INSPIRED - -time_t -mktime_z(struct state *restrict sp, struct tm *restrict tmp) -{ - return mktime_tzname(sp, tmp, false); -} - -#endif - +# if USE_TIMEX_T +static +# endif time_t mktime(struct tm *tmp) { @@ -2225,7 +2286,17 @@ mktime(struct tm *tmp) return t; } -#if STD_INSPIRED +#endif + +#if NETBSD_INSPIRED && !USE_TIMEX_T +time_t +mktime_z(struct state *restrict sp, struct tm *restrict tmp) +{ + return mktime_tzname(sp, tmp, false); +} +#endif + +#if STD_INSPIRED && !USE_TIMEX_T /* This function is obsolescent and may disappear in future releases. Callers can instead use mktime. */ time_t @@ -2237,12 +2308,14 @@ timelocal(struct tm *tmp) } #endif -#ifndef EXTERN_TIMEOFF -# ifndef timeoff -# define timeoff my_timeoff /* Don't collide with OpenBSD 7.4 <time.h>. */ +#if defined TM_GMTOFF || !USE_TIMEX_T + +# ifndef EXTERN_TIMEOFF +# ifndef timeoff +# define timeoff my_timeoff /* Don't collide with OpenBSD 7.4 <time.h>. */ +# endif +# define EXTERN_TIMEOFF static # endif -# define EXTERN_TIMEOFF static -#endif /* This function is obsolescent and may disappear in future releases. Callers can instead use mktime_z with a fixed-offset zone. */ @@ -2254,7 +2327,9 @@ timeoff(struct tm *tmp, long offset) gmtcheck(); return time1(tmp, gmtsub, gmtptr, offset); } +#endif +#if !USE_TIMEX_T time_t timegm(struct tm *tmp) { @@ -2267,6 +2342,7 @@ timegm(struct tm *tmp) *tmp = tmcpy; return t; } +#endif static int_fast32_t leapcorr(struct state const *sp, time_t t) @@ -2287,15 +2363,16 @@ leapcorr(struct state const *sp, time_t t) ** XXX--is the below the right way to conditionalize?? */ -#if STD_INSPIRED +#if !USE_TIMEX_T +# if STD_INSPIRED /* NETBSD_INSPIRED_EXTERN functions are exported to callers if NETBSD_INSPIRED is defined, and are private otherwise. */ -# if NETBSD_INSPIRED -# define NETBSD_INSPIRED_EXTERN -# else -# define NETBSD_INSPIRED_EXTERN static -# endif +# if NETBSD_INSPIRED +# define NETBSD_INSPIRED_EXTERN +# else +# define NETBSD_INSPIRED_EXTERN static +# endif /* ** IEEE Std 1003.1 (POSIX) says that 536457599 @@ -2372,13 +2449,13 @@ posix2time(time_t t) return t; } -#endif /* STD_INSPIRED */ +# endif /* STD_INSPIRED */ -#if TZ_TIME_T +# if TZ_TIME_T -# if !USG_COMPAT -# define timezone 0 -# endif +# if !USG_COMPAT +# define timezone 0 +# endif /* Convert from the underlying system's time_t to the ersatz time_tz, which is called 'time_t' in this file. Typically, this merely @@ -2409,4 +2486,5 @@ time(time_t *p) return r; } +# endif #endif diff --git a/newstrftime.3 b/newstrftime.3 index 6fd01114..21aef9d0 100644 --- a/newstrftime.3 +++ b/newstrftime.3 @@ -247,15 +247,14 @@ of leap seconds. .TP %s is replaced by the number of seconds since the Epoch (see -.BR ctime (3)); -on the mostly-obsolescent platforms where this number can be out of range for -.BR time_t , -any out-of-range number is treated as if it were -.BR "((time_t) \*-1)" . +.BR ctime (3)). Although %s is reliable in this implementation, -it can have glitches on other platforms (notably platforms lacking -.IR tm_gmtoff ), -and POSIX also allows +it can have glitches on other platforms +(notably obsolescent platforms lacking +.I tm_gmtoff +or where +.B time_t +is no wider than int), and POSIX allows .B strftime to set .B errno diff --git a/private.h b/private.h index 730786dc..3a699f0f 100644 --- a/private.h +++ b/private.h @@ -626,6 +626,12 @@ static_assert(IINNTT_MIN < INT_MIN && INT_MAX < IINNTT_MAX); # define RESERVE_STD_EXT_IDS 0 #endif +#ifdef time_tz +# define defined_time_tz true +#else +# define defined_time_tz false +#endif + /* If standard C identifiers with external linkage (e.g., localtime) are reserved and are not already being renamed anyway, rename them as if compiling with '-Dtime_tz=time_t'. */ @@ -925,6 +931,55 @@ enum { SIGNED_PADDING_CHECK_NEEDED = true }; static_assert(! TYPE_SIGNED(time_t) || ! SIGNED_PADDING_CHECK_NEEDED || TIME_T_MAX >> (TYPE_BIT(time_t) - 2) == 1); +/* If true, the value returned by an idealized unlimited-range mktime + always fits into an integer type with bounds MIN and MAX. + If false, the value might not fit. + This macro is usable in #if if its arguments are. + Add or subtract 2**31 - 1 for the maximum UT offset allowed in a TZif file, + divide by the maximum number of non-leap seconds in a year, + divide again by two just to be safe, + and account for the tm_year origin (1900) and time_t origin (1970). */ +#define MKTIME_FITS_IN(min, max) \ + ((min) < 0 \ + && ((min) + 0x7fffffff) / 366 / 24 / 60 / 60 / 2 + 1970 - 1900 < INT_MIN \ + && INT_MAX < ((max) - 0x7fffffff) / 366 / 24 / 60 / 60 / 2 + 1970 - 1900) + +/* MKTIME_MIGHT_OVERFLOW is true if mktime can fail due to time_t overflow + or if it is not known whether mktime can fail, + and is false if mktime definitely cannot fail. + This macro is usable in #if, and so does not use TIME_T_MAX or sizeof. + If the builder has not configured this macro, guess based on what + known platforms do. When in doubt, guess true. */ +#ifndef MKTIME_MIGHT_OVERFLOW +# if defined __FreeBSD__ || defined __NetBSD__ || defined __OpenBSD__ +# include <sys/param.h> +# endif +# if ((/* The following heuristics assume native time_t. */ \ + defined_time_tz) \ + || ((/* Traditional time_t is 'long', so if 'long' is not wide enough \ + assume overflow unless we're on a known-safe host. */ \ + !MKTIME_FITS_IN(LONG_MIN, LONG_MAX)) \ + && (/* GNU C Library 2.29 (2019-02-01) and later has 64-bit time_t \ + if __TIMESIZE is 64. */ \ + !defined __TIMESIZE || __TIMESIZE < 64) \ + && (/* FreeBSD 12 r320347 (__FreeBSD_version 1200036; 2017-06-26), \ + and later has 64-bit time_t on all platforms but i386 which \ + is currently scheduled for end-of-life on 2028-11-30. */ \ + !defined __FreeBSD_version || __FreeBSD_version < 1200036 \ + || defined __i386) \ + && (/* NetBSD 6.0 (2012-10-17) and later has 64-bit time_t. */ \ + !defined __NetBSD_Version__ || __NetBSD_Version__ < 600000000) \ + && (/* OpenBSD 5.5 (2014-05-01) and later has 64-bit time_t. */ \ + !defined OpenBSD || OpenBSD < 201405))) +# define MKTIME_MIGHT_OVERFLOW true +# else +# define MKTIME_MIGHT_OVERFLOW false +# endif +#endif +/* Check that MKTIME_MIGHT_OVERFLOW is consistent with time_t's range. */ +static_assert(MKTIME_MIGHT_OVERFLOW + || MKTIME_FITS_IN(TIME_T_MIN, TIME_T_MAX)); + /* ** 302 / 1000 is log10(2.0) rounded up. ** Subtract one for the sign bit if the type is signed; @@ -957,7 +1012,7 @@ static_assert(! TYPE_SIGNED(time_t) || ! SIGNED_PADDING_CHECK_NEEDED /* strftime.c sometimes needs access to timeoff if it is not already public. tz_private_timeoff should be used only by localtime.c and strftime.c. */ -#if (!defined EXTERN_TIMEOFF \ +#if (!defined EXTERN_TIMEOFF && ! (defined USE_TIMEX_T && USE_TIMEX_T) \ && defined TM_GMTOFF && (200809 < _POSIX_VERSION || ! UNINIT_TRAP)) # ifndef timeoff # define timeoff tz_private_timeoff diff --git a/strftime.c b/strftime.c index a50ed90d..7be94de3 100644 --- a/strftime.c +++ b/strftime.c @@ -39,6 +39,12 @@ #include <locale.h> #include <stdio.h> +#if MKTIME_MIGHT_OVERFLOW +/* Do this after system includes as it redefines time_t, mktime, timeoff. */ +# define USE_TIMEX_T true +# include "localtime.c" +#endif + #ifndef DEPRECATE_TWO_DIGIT_YEARS # define DEPRECATE_TWO_DIGIT_YEARS false #endif @@ -328,16 +334,17 @@ label: tm.tm_mday = t->tm_mday; tm.tm_mon = t->tm_mon; tm.tm_year = t->tm_year; + + /* Get the time_t value for TM. + Native time_t, or its redefinition + by localtime.c above, is wide enough + so that this cannot overflow. */ #ifdef TM_GMTOFF mkt = timeoff(&tm, t->TM_GMTOFF); #else tm.tm_isdst = t->tm_isdst; mkt = mktime(&tm); #endif - /* If mktime fails, %s expands to the - value of (time_t) -1 as a failure - marker; this is better in practice - than strftime failing. */ if (TYPE_SIGNED(time_t)) { intmax_t n = mkt; sprintf(buf, "%"PRIdMAX, n); -- 2.47.0