Skip to content

[libc] Fix overflow check for 32-bit mktime. #101993

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged

Conversation

statham-arm
Copy link
Collaborator

The 32-bit time_t rolls over to a value with its high bit set at 2038-01-19 03:14:07. The overflow check for this in mktime() was checking each individual component of the time: reject if year>2038, or if month>1, or if day>19, etc. As a result it would reject valid times before the overflow point, because a low-order component was out of range even though a higher-order one makes the time as a whole safe. The earliest failing value is 2145916808 == 2038-01-01 00:00:08, in which only the seconds field 'overflows'.

Fixed so that if any component is less than its threshold value, we don't check the lower-order components.

The 32-bit time_t rolls over to a value with its high bit set at
2038-01-19 03:14:07. The overflow check for this in mktime() was
checking each individual component of the time: reject if year>2038,
or if month>1, or if day>19, etc. As a result it would reject valid
times before the overflow point, because a low-order component was out
of range even though a higher-order one makes the time as a whole
safe. The earliest failing value is 2145916808 == 2038-01-01 00:00:08,
in which only the seconds field 'overflows'.

Fixed so that if any component is _less_ than its threshold value, we
don't check the lower-order components.
@llvmbot
Copy link
Member

llvmbot commented Aug 5, 2024

@llvm/pr-subscribers-libc

Author: Simon Tatham (statham-arm)

Changes

The 32-bit time_t rolls over to a value with its high bit set at 2038-01-19 03:14:07. The overflow check for this in mktime() was checking each individual component of the time: reject if year>2038, or if month>1, or if day>19, etc. As a result it would reject valid times before the overflow point, because a low-order component was out of range even though a higher-order one makes the time as a whole safe. The earliest failing value is 2145916808 == 2038-01-01 00:00:08, in which only the seconds field 'overflows'.

Fixed so that if any component is less than its threshold value, we don't check the lower-order components.


Full diff: https://github.com/llvm/llvm-project/pull/101993.diff

2 Files Affected:

  • (modified) libc/src/time/mktime.cpp (+12-6)
  • (modified) libc/test/src/time/mktime_test.cpp (+109-16)
diff --git a/libc/src/time/mktime.cpp b/libc/src/time/mktime.cpp
index 9ea316d504f5b..72cd229120538 100644
--- a/libc/src/time/mktime.cpp
+++ b/libc/src/time/mktime.cpp
@@ -42,12 +42,18 @@ LLVM_LIBC_FUNCTION(time_t, mktime, (struct tm * tm_out)) {
       return time_utils::out_of_range();
     if (tm_out->tm_mday > 19)
       return time_utils::out_of_range();
-    if (tm_out->tm_hour > 3)
-      return time_utils::out_of_range();
-    if (tm_out->tm_min > 14)
-      return time_utils::out_of_range();
-    if (tm_out->tm_sec > 7)
-      return time_utils::out_of_range();
+    else if (tm_out->tm_mday == 19) {
+      if (tm_out->tm_hour > 3)
+        return time_utils::out_of_range();
+      else if (tm_out->tm_hour == 3) {
+        if (tm_out->tm_min > 14)
+          return time_utils::out_of_range();
+        else if (tm_out->tm_min == 14) {
+          if (tm_out->tm_sec > 7)
+            return time_utils::out_of_range();
+        }
+      }
+    }
   }
 
   // Years are ints.  A 32-bit year will fit into a 64-bit time_t.
diff --git a/libc/test/src/time/mktime_test.cpp b/libc/test/src/time/mktime_test.cpp
index 3142a4f3d06dd..84e6c7eb2c42e 100644
--- a/libc/test/src/time/mktime_test.cpp
+++ b/libc/test/src/time/mktime_test.cpp
@@ -380,22 +380,115 @@ TEST(LlvmLibcMkTime, InvalidDays) {
 TEST(LlvmLibcMkTime, EndOf32BitEpochYear) {
   // Test for maximum value of a signed 32-bit integer.
   // Test implementation can encode time for Tue 19 January 2038 03:14:07 UTC.
-  struct tm tm_data {
-    .tm_sec = 7, .tm_min = 14, .tm_hour = 3, .tm_mday = 19,
-    .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0,
-    .tm_yday = 0, .tm_isdst = 0
-  };
-  EXPECT_THAT(LIBC_NAMESPACE::mktime(&tm_data), Succeeds(0x7FFFFFFF));
-  EXPECT_TM_EQ((tm{.tm_sec = 7,
-                   .tm_min = 14,
-                   .tm_hour = 3,
-                   .tm_mday = 19,
-                   .tm_mon = Month::JANUARY,
-                   .tm_year = tm_year(2038),
-                   .tm_wday = 2,
-                   .tm_yday = 7,
-                   .tm_isdst = 0}),
-               tm_data);
+  {
+    struct tm tm_data {
+      .tm_sec = 7, .tm_min = 14, .tm_hour = 3, .tm_mday = 19,
+      .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0,
+      .tm_yday = 0, .tm_isdst = 0
+    };
+    EXPECT_THAT(LIBC_NAMESPACE::mktime(&tm_data), Succeeds(0x7FFFFFFF));
+    EXPECT_TM_EQ((tm{.tm_sec = 7,
+                     .tm_min = 14,
+                     .tm_hour = 3,
+                     .tm_mday = 19,
+                     .tm_mon = Month::JANUARY,
+                     .tm_year = tm_year(2038),
+                     .tm_wday = 2,
+                     .tm_yday = 7,
+                     .tm_isdst = 0}),
+                 tm_data);
+  }
+
+  // Now test some times before that, to ensure they are not rejected.
+  {
+    // 2038-01-19 03:13:59 tests that even a large seconds field is
+    // accepted if the minutes field is smaller.
+    struct tm tm_data {
+      .tm_sec = 59, .tm_min = 13, .tm_hour = 3, .tm_mday = 19,
+      .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0,
+      .tm_yday = 0, .tm_isdst = 0
+    };
+    EXPECT_THAT(LIBC_NAMESPACE::mktime(&tm_data), Succeeds(0x7FFFFFFF - 8));
+    EXPECT_TM_EQ((tm{.tm_sec = 59,
+                     .tm_min = 13,
+                     .tm_hour = 3,
+                     .tm_mday = 19,
+                     .tm_mon = Month::JANUARY,
+                     .tm_year = tm_year(2038),
+                     .tm_wday = 2,
+                     .tm_yday = 7,
+                     .tm_isdst = 0}),
+                 tm_data);
+  }
+
+  {
+    // 2038-01-19 02:59:59 tests that large seconds and minutes are
+    // accepted if the hours field is smaller.
+    struct tm tm_data {
+      .tm_sec = 59, .tm_min = 59, .tm_hour = 2, .tm_mday = 19,
+      .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0,
+      .tm_yday = 0, .tm_isdst = 0
+    };
+    EXPECT_THAT(LIBC_NAMESPACE::mktime(&tm_data),
+                Succeeds(0x7FFFFFFF - 8 - 14 * TimeConstants::SECONDS_PER_MIN));
+    EXPECT_TM_EQ((tm{.tm_sec = 59,
+                     .tm_min = 59,
+                     .tm_hour = 2,
+                     .tm_mday = 19,
+                     .tm_mon = Month::JANUARY,
+                     .tm_year = tm_year(2038),
+                     .tm_wday = 2,
+                     .tm_yday = 7,
+                     .tm_isdst = 0}),
+                 tm_data);
+  }
+
+  {
+    // 2038-01-18 23:59:59 tests that large seconds, minutes and hours
+    // are accepted if the days field is smaller.
+    struct tm tm_data {
+      .tm_sec = 59, .tm_min = 59, .tm_hour = 23, .tm_mday = 18,
+      .tm_mon = Month::JANUARY, .tm_year = tm_year(2038), .tm_wday = 0,
+      .tm_yday = 0, .tm_isdst = 0
+    };
+    EXPECT_THAT(LIBC_NAMESPACE::mktime(&tm_data),
+                Succeeds(0x7FFFFFFF - 8 - 14 * TimeConstants::SECONDS_PER_MIN -
+                         3 * TimeConstants::SECONDS_PER_HOUR));
+    EXPECT_TM_EQ((tm{.tm_sec = 59,
+                     .tm_min = 59,
+                     .tm_hour = 23,
+                     .tm_mday = 18,
+                     .tm_mon = Month::JANUARY,
+                     .tm_year = tm_year(2038),
+                     .tm_wday = 2,
+                     .tm_yday = 7,
+                     .tm_isdst = 0}),
+                 tm_data);
+  }
+
+  {
+    // 2038-01-18 23:59:59 tests that the final second of 2037 is
+    // accepted.
+    struct tm tm_data {
+      .tm_sec = 59, .tm_min = 59, .tm_hour = 23, .tm_mday = 31,
+      .tm_mon = Month::DECEMBER, .tm_year = tm_year(2037), .tm_wday = 0,
+      .tm_yday = 0, .tm_isdst = 0
+    };
+    EXPECT_THAT(LIBC_NAMESPACE::mktime(&tm_data),
+                Succeeds(0x7FFFFFFF - 8 - 14 * TimeConstants::SECONDS_PER_MIN -
+                         3 * TimeConstants::SECONDS_PER_HOUR -
+                         18 * TimeConstants::SECONDS_PER_DAY));
+    EXPECT_TM_EQ((tm{.tm_sec = 59,
+                     .tm_min = 59,
+                     .tm_hour = 23,
+                     .tm_mday = 31,
+                     .tm_mon = Month::DECEMBER,
+                     .tm_year = tm_year(2037),
+                     .tm_wday = 2,
+                     .tm_yday = 7,
+                     .tm_isdst = 0}),
+                 tm_data);
+  }
 }
 
 TEST(LlvmLibcMkTime, Max64BitYear) {

@SchrodingerZhu SchrodingerZhu merged commit 81d8273 into llvm:main Aug 7, 2024
8 checks passed
@statham-arm statham-arm deleted the mktime-32bit-false-positive-overflow branch August 7, 2024 08:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants