[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
Fri May 29 07:49:28 PDT 2026
https://github.com/kaladron created https://github.com/llvm/llvm-project/pull/200425
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.
>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] [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;
More information about the libc-commits
mailing list