[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