Просмотр исходного кода

libc: time: keep the zone offset when localtime avoids an overflow

__time_localtime_tzi() hands _time_t2tm() the time one week ahead
together with a -7 day correction, so that a 32-bit time_t stays
non-negative while the zone offset is applied.  Close to the top of the
range that added week would overflow, and the code then flipped the
whole sum -- including the zone offset -- instead of only the week.  The
returned clock was off by twice the offset.

Worse, the bound it compared against was LONG_MAX.  On a 32-bit target
with UCLIBC_USE_TIME64 that is 2^31-1 while time_t is 64 bits wide, so
every timestamp past 2038-01-11 took that path.  For 2040-06-01 12:00
UTC on i686:

  TZ=UTC0    12:00   correct, an offset of zero hides the sign
  TZ=JST-9   03:00   should be 21:00
  TZ=EST5    17:00   should be 07:00

x86_64, where long is 64 bits, was unaffected, and so was every date
before 2038.  It went unnoticed because the test guest runs in UTC.

Add TIME_T_MAX so the bound follows time_t rather than long.

Checked with fixed-offset zones on both sides of that boundary.

Signed-off-by: Ramin Moussavi <lordrasmus@gmail.com>
ramin 3 дней назад
Родитель
Сommit
8f71aa69bb
1 измененных файлов с 21 добавлено и 2 удалено
  1. 21 2
      libc/misc/time/time.c

+ 21 - 2
libc/misc/time/time.c

@@ -146,6 +146,14 @@
 #include <bits/uClibc_uintmaxtostr.h>
 #include <bits/uClibc_uintmaxtostr.h>
 #include <bits/uClibc_mutex.h>
 #include <bits/uClibc_mutex.h>
 
 
+/* Largest value a time_t can hold, named without assuming its width -- the
+ * places below that need it used to reach for LONG_MAX, which is not the same
+ * thing once UCLIBC_USE_TIME64 gives a 32-bit target a 64-bit time_t.  If
+ * time_t is unsigned this yields half its range, which only makes the tests
+ * that use it trigger earlier, never later. */
+#define TIME_T_MAX \
+	((time_t)(~(uintmax_t)0 >> (8 * (sizeof(uintmax_t) - sizeof(time_t)) + 1)))
+
 #if defined __UCLIBC_HAS_WCHAR__ && (defined L_wcsftime || defined L_wcsftime_l)
 #if defined __UCLIBC_HAS_WCHAR__ && (defined L_wcsftime || defined L_wcsftime_l)
 #include <wchar.h>
 #include <wchar.h>
 # define CHAR_T wchar_t
 # define CHAR_T wchar_t
@@ -748,11 +756,22 @@ struct tm attribute_hidden *__time_localtime_tzi(register const time_t *__restri
 
 
 	dst = 0;
 	dst = 0;
 	do {
 	do {
+		/* _time_t2tm() is handed the time a week ahead and a -7 day
+		 * correction, so that a 32-bit time_t stays non-negative while
+		 * the zone offset is applied.  Near the top of the range that
+		 * week would overflow, so it is subtracted instead -- but only
+		 * the week changes sign, never the zone offset.  Negating the
+		 * whole sum moved the clock by twice the offset.
+		 *
+		 * The bound follows time_t.  It used to be LONG_MAX, which is
+		 * 2^31-1 on the 32-bit targets that set UCLIBC_USE_TIME64:
+		 * every date past January 2038 took the fallback path there,
+		 * so localtime() was off by 2*gmtoff in every zone but UTC. */
 		days = -7;
 		days = -7;
 		offset = 604800L - tzi[dst].gmt_offset;
 		offset = 604800L - tzi[dst].gmt_offset;
-		if (*timer > (LONG_MAX - 604800L)) {
+		if (*timer > (TIME_T_MAX - 604800L)) {
 			days = -days;
 			days = -days;
-			offset = -offset;
+			offset = -604800L - tzi[dst].gmt_offset;
 		}
 		}
 		*x = *timer + offset;
 		*x = *timer + offset;