[libc-commits] [libc] [libc] Fix select tv_nsec conversion and tm_year overflow (PR #200425)

Jeff Bailey via libc-commits libc-commits at lists.llvm.org
Thu Jun 11 05:52:11 PDT 2026


https://github.com/kaladron updated https://github.com/llvm/llvm-project/pull/200425

>From 5eda27891097b0b16e6d2aea2cb3fb587dd7b08a Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Fri, 29 May 2026 15:26:40 +0100
Subject: [PATCH 1/2] [libc] Fix select tv_nsec conversion and tm_year overflow

Fixed two pre-existing bugs in time-related functions:

* sys/select: Fix tv_nsec calculation in select.cpp during timeout
  normalization. Previously, when timeout->tv_usec was >= 1000000, the
  overflow seconds were added to tv_sec but the entire tv_usec value
  was still converted to tv_nsec. This double-counted the overflow,
  resulting in an incorrect timeout, and caused tv_nsec to exceed 10^9
  which made the kernel fail with EINVAL. Fixed by modulo-ing
  tv_usec by 1000000 before converting to nanoseconds.

* time: Tighten the years bounds check in time_utils.cpp to account
  for the 100-year offset before casting to 32-bit signed int tm_year.
  This prevents an integer wrap-around/overflow bug for years close
  to INT_MAX.

Added unit tests to demonstrate and verify both fixes.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/sys/select/linux/select.cpp          |  2 +-
 libc/src/time/time_utils.cpp                  |  3 ++-
 .../src/sys/select/select_failure_test.cpp    | 25 +++++++++++++++++++
 libc/test/src/time/gmtime_test.cpp            | 16 ++++++++++++
 4 files changed, 44 insertions(+), 2 deletions(-)

diff --git a/libc/src/sys/select/linux/select.cpp b/libc/src/sys/select/linux/select.cpp
index 6c434eb584596..97264287434c9 100644
--- a/libc/src/sys/select/linux/select.cpp
+++ b/libc/src/sys/select/linux/select.cpp
@@ -50,7 +50,7 @@ LLVM_LIBC_FUNCTION(int, select,
       ts.tv_nsec = 999999999;
     } else {
       ts.tv_sec = timeout->tv_sec + timeout->tv_usec / 1000000;
-      ts.tv_nsec = timeout->tv_usec * 1000;
+      ts.tv_nsec = (timeout->tv_usec % 1000000) * 1000;
     }
   }
   pselect6_sigset_t pss{nullptr, sizeof(sigset_t)};
diff --git a/libc/src/time/time_utils.cpp b/libc/src/time/time_utils.cpp
index 1d0daea6b321e..45ba0e8ce2793 100644
--- a/libc/src/time/time_utils.cpp
+++ b/libc/src/time/time_utils.cpp
@@ -217,7 +217,8 @@ int64_t update_from_seconds(time_t total_seconds, tm *tm) {
     years++;
   }
 
-  if (years > INT_MAX || years < INT_MIN)
+  constexpr int64_t YEAR_OFFSET = 2000 - time_constants::TIME_YEAR_BASE; // 100
+  if (years > INT_MAX - YEAR_OFFSET || years < INT_MIN - YEAR_OFFSET)
     return time_utils::out_of_range();
 
   // All the data (years, month and remaining days) was calculated from
diff --git a/libc/test/src/sys/select/select_failure_test.cpp b/libc/test/src/sys/select/select_failure_test.cpp
index c5a7ad7a11a35..0b26aa3508837 100644
--- a/libc/test/src/sys/select/select_failure_test.cpp
+++ b/libc/test/src/sys/select/select_failure_test.cpp
@@ -27,3 +27,28 @@ TEST_F(LlvmLibcSelectTest, SelectInvalidFD) {
   ASSERT_THAT(LIBC_NAMESPACE::select(-1, &set, nullptr, nullptr, &timeout),
               Fails(EINVAL));
 }
+
+TEST_F(LlvmLibcSelectTest, SelectAcceptsLargeMicroseconds) {
+  int pipe_fds[2];
+  ASSERT_EQ(0, ::pipe(pipe_fds));
+
+  fd_set read_set;
+  FD_ZERO(&read_set);
+  FD_SET(pipe_fds[0], &read_set);
+
+  struct timeval timeout{
+      0, 1000000 // 1 second (will be normalized)
+  };
+
+  // Write to pipe so select returns immediately.
+  ASSERT_EQ(1, static_cast<int>(::write(pipe_fds[1], "a", 1)));
+
+  // select should return 1 (ready) immediately, not fail with EINVAL.
+  int ret = LIBC_NAMESPACE::select(pipe_fds[0] + 1, &read_set, nullptr, nullptr,
+                                   &timeout);
+  ASSERT_EQ(1, ret);
+
+  // Cleanup
+  ::close(pipe_fds[0]);
+  ::close(pipe_fds[1]);
+}
diff --git a/libc/test/src/time/gmtime_test.cpp b/libc/test/src/time/gmtime_test.cpp
index 1df768ac721fd..fbc6112a1582e 100644
--- a/libc/test/src/time/gmtime_test.cpp
+++ b/libc/test/src/time/gmtime_test.cpp
@@ -39,6 +39,22 @@ TEST_F(LlvmLibcGmTime, OutOfRange) {
   ASSERT_ERRNO_EQ(EOVERFLOW);
 }
 
+TEST_F(LlvmLibcGmTime, OverflowYear) {
+  if (sizeof(time_t) < sizeof(int64_t))
+    return;
+
+  // Test for year close to INT_MAX that would overflow tm_year (int)
+  // after adding the 100-year offset.
+  constexpr int64_t SECONDS_PER_AVERAGE_YEAR = 31556952;
+  time_t seconds =
+      LIBC_NAMESPACE::time_constants::SECONDS_UNTIL2000_MARCH_FIRST +
+      (static_cast<int64_t>(INT_MAX) - 50) * SECONDS_PER_AVERAGE_YEAR;
+
+  struct tm *tm_data = LIBC_NAMESPACE::gmtime(&seconds);
+  EXPECT_TRUE(tm_data == nullptr);
+  ASSERT_ERRNO_EQ(EOVERFLOW);
+}
+
 TEST_F(LlvmLibcGmTime, InvalidSeconds) {
   time_t seconds = 0;
   struct tm *tm_data = nullptr;

>From 9de5b9ffaa52ae606865b8ef858a748efcef27fb Mon Sep 17 00:00:00 2001
From: Jeff Bailey <jbailey at raspberryginger.com>
Date: Thu, 11 Jun 2026 13:51:11 +0100
Subject: [PATCH 2/2] [libc] Address review feedback for select and time
 (#200425)

Addressed review comments and fixed a bug found during review:

* sys/select: Fixed select to block indefinitely when timeout is null
  by passing nullptr to the pselect6 syscalls instead of a zeroed
  timespec.
* sys/select: Used scope_exit in select_failure_test.cpp to ensure
  pipe file descriptors are closed on failure.
* time: Reused YEAR_OFFSET in time_utils.cpp for tm_year assignment.
* time: Adjusted comment placement in time_utils.cpp for better
  context.

Assisted-by: Automated tooling, human reviewed.
---
 libc/src/sys/select/linux/select.cpp             | 10 +++++-----
 libc/src/time/time_utils.cpp                     |  6 +++---
 libc/test/src/sys/select/select_failure_test.cpp |  9 +++++----
 3 files changed, 13 insertions(+), 12 deletions(-)

diff --git a/libc/src/sys/select/linux/select.cpp b/libc/src/sys/select/linux/select.cpp
index 97264287434c9..5b025c34fc5f9 100644
--- a/libc/src/sys/select/linux/select.cpp
+++ b/libc/src/sys/select/linux/select.cpp
@@ -36,9 +36,8 @@ LLVM_LIBC_FUNCTION(int, select,
   // instead of a struct timeval argument. Also, it takes an additional
   // argument which is a pointer to an object of a type defined above as
   // "pselect6_sigset_t".
-  struct timespec ts {
-    0, 0
-  };
+  struct timespec ts;
+  struct timespec *pts = nullptr;
   if (timeout != nullptr) {
     // In general, if the tv_sec and tv_usec in |timeout| are correctly set,
     // then converting tv_usec to nanoseconds will not be a problem. However,
@@ -52,14 +51,15 @@ LLVM_LIBC_FUNCTION(int, select,
       ts.tv_sec = timeout->tv_sec + timeout->tv_usec / 1000000;
       ts.tv_nsec = (timeout->tv_usec % 1000000) * 1000;
     }
+    pts = &ts;
   }
   pselect6_sigset_t pss{nullptr, sizeof(sigset_t)};
 #if SYS_pselect6
   int ret = LIBC_NAMESPACE::syscall_impl<int>(SYS_pselect6, nfds, read_set,
-                                              write_set, error_set, &ts, &pss);
+                                              write_set, error_set, pts, &pss);
 #elif defined(SYS_pselect6_time64)
   int ret = LIBC_NAMESPACE::syscall_impl<int>(
-      SYS_pselect6_time64, nfds, read_set, write_set, error_set, &ts, &pss);
+      SYS_pselect6_time64, nfds, read_set, write_set, error_set, pts, &pss);
 #else
 #error "SYS_pselect6 and SYS_pselect6_time64 syscalls not available."
 #endif
diff --git a/libc/src/time/time_utils.cpp b/libc/src/time/time_utils.cpp
index 45ba0e8ce2793..0b02f5f84c363 100644
--- a/libc/src/time/time_utils.cpp
+++ b/libc/src/time/time_utils.cpp
@@ -217,13 +217,13 @@ int64_t update_from_seconds(time_t total_seconds, tm *tm) {
     years++;
   }
 
+  // All the data (years, month and remaining days) was calculated from
+  // March, 2000. Thus adjust the data to be from January, 1900.
   constexpr int64_t YEAR_OFFSET = 2000 - time_constants::TIME_YEAR_BASE; // 100
   if (years > INT_MAX - YEAR_OFFSET || years < INT_MIN - YEAR_OFFSET)
     return time_utils::out_of_range();
 
-  // All the data (years, month and remaining days) was calculated from
-  // March, 2000. Thus adjust the data to be from January, 1900.
-  tm->tm_year = static_cast<int>(years + 2000 - time_constants::TIME_YEAR_BASE);
+  tm->tm_year = static_cast<int>(years + YEAR_OFFSET);
   tm->tm_mon = static_cast<int>(months + 2);
   tm->tm_mday = static_cast<int>(remainingDays + 1);
   tm->tm_wday = static_cast<int>(wday);
diff --git a/libc/test/src/sys/select/select_failure_test.cpp b/libc/test/src/sys/select/select_failure_test.cpp
index 0b26aa3508837..282e308add0c6 100644
--- a/libc/test/src/sys/select/select_failure_test.cpp
+++ b/libc/test/src/sys/select/select_failure_test.cpp
@@ -6,6 +6,7 @@
 //
 //===----------------------------------------------------------------------===//
 
+#include "src/__support/CPP/scope.h"
 #include "src/sys/select/select.h"
 #include "src/unistd/read.h"
 #include "test/UnitTest/ErrnoCheckingTest.h"
@@ -31,6 +32,10 @@ TEST_F(LlvmLibcSelectTest, SelectInvalidFD) {
 TEST_F(LlvmLibcSelectTest, SelectAcceptsLargeMicroseconds) {
   int pipe_fds[2];
   ASSERT_EQ(0, ::pipe(pipe_fds));
+  LIBC_NAMESPACE::cpp::scope_exit close_pipes([&] {
+    ::close(pipe_fds[0]);
+    ::close(pipe_fds[1]);
+  });
 
   fd_set read_set;
   FD_ZERO(&read_set);
@@ -47,8 +52,4 @@ TEST_F(LlvmLibcSelectTest, SelectAcceptsLargeMicroseconds) {
   int ret = LIBC_NAMESPACE::select(pipe_fds[0] + 1, &read_set, nullptr, nullptr,
                                    &timeout);
   ASSERT_EQ(1, ret);
-
-  // Cleanup
-  ::close(pipe_fds[0]);
-  ::close(pipe_fds[1]);
 }



More information about the libc-commits mailing list