diff --git a/src/syscall/fd.c b/src/syscall/fd.c index 26972590..b905da4d 100644 --- a/src/syscall/fd.c +++ b/src/syscall/fd.c @@ -28,6 +28,7 @@ #include "debug/log.h" #include +#include "proved/timespec.h" #include "syscall/linux-wire.h" #include "syscall/fd.h" #include "syscall/internal.h" @@ -47,7 +48,10 @@ static void eventfd_close(int guest_fd); static void signalfd_close(int guest_fd); #define NS_PER_SEC 1000000000LL -#define US_PER_SEC 1000000LL + +_Static_assert( + NS_PER_SEC == TIMESPEC_NSEC_PER_SEC, + "timerfd and proved timespec conversions must use the same scale"); /* All special-FD state arrays store guest_fd as their first field. Keep the * common slot walk in one place so timerfd/eventfd/signalfd stay consistent. @@ -271,7 +275,7 @@ int64_t sys_timerfd_settime(guest_t *g, struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); - int64_t now_ns = now.tv_sec * NS_PER_SEC + now.tv_nsec; + int64_t now_ns = timespec_to_ns_sat(now.tv_sec, now.tv_nsec); int64_t remaining = timerfd_remaining_ns_locked(slot, now_ns); if (remaining > 0) { old.it_value_sec = remaining / NS_PER_SEC; @@ -293,26 +297,21 @@ int64_t sys_timerfd_settime(guest_t *g, ? CLOCK_REALTIME : CLOCK_MONOTONIC; clock_gettime(host_clock, &now); - int64_t target_sec = its.it_value_sec; - if (target_sec > INT64_MAX / NS_PER_SEC) - target_sec = INT64_MAX / NS_PER_SEC; - int64_t target_ns = target_sec * NS_PER_SEC + its.it_value_nsec; - int64_t now_ns = now.tv_sec * NS_PER_SEC + now.tv_nsec; + int64_t target_ns = + timespec_to_ns_sat(its.it_value_sec, its.it_value_nsec); + int64_t now_ns = timespec_to_ns_sat(now.tv_sec, now.tv_nsec); int64_t relative_ns = target_ns > now_ns ? target_ns - now_ns : 1; its.it_value_sec = relative_ns / NS_PER_SEC; its.it_value_nsec = relative_ns % NS_PER_SEC; } - /* Clamp large seconds values to prevent signed integer overflow. INT64_MAX - * / 1e6 is about 9.2e12 seconds; INT64_MAX / 1e9 is about 9.2e9 seconds. + /* Derive the host timeout and stored timer state from the same saturating + * nanosecond conversion. */ - int64_t val_sec = its.it_value_sec, int_sec = its.it_interval_sec; - if (val_sec > INT64_MAX / US_PER_SEC) - val_sec = INT64_MAX / US_PER_SEC; - if (int_sec > INT64_MAX / NS_PER_SEC) - int_sec = INT64_MAX / NS_PER_SEC; - int64_t value_us = val_sec * US_PER_SEC + its.it_value_nsec / 1000; - int64_t interval_ns = int_sec * NS_PER_SEC + its.it_interval_nsec; + int64_t value_ns = timespec_to_ns_sat(its.it_value_sec, its.it_value_nsec); + int64_t value_us = value_ns / 1000; + int64_t interval_ns = + timespec_to_ns_sat(its.it_interval_sec, its.it_interval_nsec); log_debug( "timerfd_settime: gfd=%d value=%lld.%09lld " @@ -352,20 +351,14 @@ int64_t sys_timerfd_settime(guest_t *g, timerfd_state[slot].armed = true; timerfd_state[slot].interval_ns = interval_ns; - /* Use separate clamping for nanosecond computation: val_sec is clamped - * for microsecond use (INT64_MAX / 1e6), which overflows when * 1e9. - */ - int64_t init_sec = its.it_value_sec; - if (init_sec > INT64_MAX / NS_PER_SEC) - init_sec = INT64_MAX / NS_PER_SEC; - timerfd_state[slot].initial_ns = - init_sec * NS_PER_SEC + its.it_value_nsec; + timerfd_state[slot].initial_ns = value_ns; timerfd_state[slot].expirations = 0; /* Record arm time for gettime remaining-time calculation */ struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); - timerfd_state[slot].arm_time_ns = now.tv_sec * NS_PER_SEC + now.tv_nsec; + timerfd_state[slot].arm_time_ns = + timespec_to_ns_sat(now.tv_sec, now.tv_nsec); } unlock: @@ -389,7 +382,7 @@ int64_t sys_timerfd_gettime(guest_t *g, int fd, uint64_t curr_value_gva) struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); - int64_t now_ns = now.tv_sec * NS_PER_SEC + now.tv_nsec; + int64_t now_ns = timespec_to_ns_sat(now.tv_sec, now.tv_nsec); int64_t remaining = timerfd_remaining_ns_locked(slot, now_ns); if (remaining <= 0) { @@ -510,7 +503,8 @@ int64_t timerfd_read(int guest_fd, guest_t *g, uint64_t buf_gva, uint64_t count) /* Update arm time for gettime remaining-time calculation */ struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); - timerfd_state[slot].arm_time_ns = now.tv_sec * NS_PER_SEC + now.tv_nsec; + timerfd_state[slot].arm_time_ns = + timespec_to_ns_sat(now.tv_sec, now.tv_nsec); timerfd_state[slot].initial_ns = intv; /* Next fire is one interval */ } pthread_mutex_unlock(&sfd_lock); @@ -1458,7 +1452,7 @@ bool timerfd_fdinfo_snapshot(int guest_fd, if (timerfd_state[slot].armed) { struct timespec now; clock_gettime(CLOCK_MONOTONIC, &now); - int64_t now_ns = (int64_t) now.tv_sec * NS_PER_SEC + now.tv_nsec; + int64_t now_ns = timespec_to_ns_sat(now.tv_sec, now.tv_nsec); value_ns = timerfd_remaining_ns_locked(slot, now_ns); } *value_ns_out = value_ns; diff --git a/tests/manifest.txt b/tests/manifest.txt index d0c48743..8ad6cce6 100644 --- a/tests/manifest.txt +++ b/tests/manifest.txt @@ -83,6 +83,7 @@ test-epoll-dup test-epoll-refcount test-epoll-del-leak test-timerfd +test-timerfd-overflow test-large-io-boundary test-ioctl-cloexec test-pty diff --git a/tests/test-matrix.sh b/tests/test-matrix.sh index 840033a5..e594dc50 100755 --- a/tests/test-matrix.sh +++ b/tests/test-matrix.sh @@ -766,6 +766,8 @@ run_unit_tests() test_check "$runner" "test-epoll-unsupported" "0 failed" \ "$bindir/test-epoll-unsupported" test_check "$runner" "test-timerfd" "0 failed" "$bindir/test-timerfd" + test_check "$runner" "test-timerfd-overflow" "0 failed" \ + "$bindir/test-timerfd-overflow" test_rc "$runner" "test-eventfd-dup" 0 "$bindir/test-eventfd-dup" test_rc "$runner" "test-epoll-mt" 0 "$bindir/test-epoll-mt" test_rc "$runner" "test-epoll-aba" 0 "$bindir/test-epoll-aba" diff --git a/tests/test-timerfd-overflow.c b/tests/test-timerfd-overflow.c new file mode 100644 index 00000000..b2623af6 --- /dev/null +++ b/tests/test-timerfd-overflow.c @@ -0,0 +1,161 @@ +/* + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include +#include +#include + +#include "test-harness.h" + +static int64_t timespec_ns(struct timespec value) +{ + if (value.tv_sec < 0 || value.tv_sec > INT64_MAX / 1000000000 || + value.tv_nsec < 0 || value.tv_nsec >= 1000000000) + return -1; + + int64_t whole = value.tv_sec * 1000000000; + if (value.tv_nsec > INT64_MAX - whole) + return -1; + return whole + value.tv_nsec; +} + +int main(void) +{ + int passes = 0, fails = 0; + const struct { + const char *name; + int clockid, flags; + int64_t seconds, min_remaining_sec; + bool interval, rearm; + } cases[] = { + {"relative nanosecond overflow", CLOCK_MONOTONIC, 0, + INT64_MAX / 1000000000, 1000000000, false, false}, + {"relative microsecond overflow", CLOCK_MONOTONIC, 0, + INT64_MAX / 1000000, 1000000000, false, false}, + {"relative maximum seconds", CLOCK_MONOTONIC, 0, INT64_MAX, 1000000000, + false, false}, + {"interval nanosecond overflow", CLOCK_MONOTONIC, 0, + INT64_MAX / 1000000000, 30, true, false}, + {"absolute monotonic overflow", CLOCK_MONOTONIC, TFD_TIMER_ABSTIME, + INT64_MAX / 1000000000, 1000000000, false, false}, + {"absolute realtime overflow", CLOCK_REALTIME, TFD_TIMER_ABSTIME, + INT64_MAX / 1000000000, 1000000000, false, false}, + {"large interval rearm", CLOCK_MONOTONIC, 0, INT64_MAX / 1000000000, + 1000000000, true, true}, + }; + + for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { + TEST(cases[i].name); + int fd = timerfd_create(cases[i].clockid, TFD_NONBLOCK | TFD_CLOEXEC); + if (fd < 0) { + FAIL("timerfd_create"); + continue; + } + + struct timespec large = {.tv_sec = cases[i].seconds, + .tv_nsec = 999999999}; + struct itimerspec requested = {0}; + if (cases[i].interval) { + if (cases[i].rearm) + requested.it_value.tv_nsec = 20000000; + else + requested.it_value.tv_sec = 60; + requested.it_interval = large; + } else { + requested.it_value = large; + } + int64_t before_ns = 0; + if (cases[i].flags & TFD_TIMER_ABSTIME) { + /* Bypass the vDSO so bounds use the timer backend's clock. */ + struct timespec before; + if (syscall(SYS_clock_gettime, cases[i].clockid, &before) < 0 || + (before_ns = timespec_ns(before)) < 0) { + FAIL("clock_gettime before settime"); + close(fd); + continue; + } + } + if (timerfd_settime(fd, cases[i].flags, &requested, NULL) < 0) { + FAIL("timerfd_settime rejected a valid large timeout"); + close(fd); + continue; + } + + if (cases[i].rearm) { + struct pollfd pfd = {.fd = fd, .events = POLLIN}; + if (poll(&pfd, 1, 5000) != 1 || !(pfd.revents & POLLIN)) { + FAIL("first timer expiration not readable"); + close(fd); + continue; + } + uint64_t expirations = 0; + if (read(fd, &expirations, sizeof(expirations)) != + (ssize_t) sizeof(expirations) || + expirations == 0) { + FAIL("read first timer expiration"); + close(fd); + continue; + } + } + + struct itimerspec current; + if (timerfd_gettime(fd, ¤t) < 0) { + FAIL("timerfd_gettime"); + close(fd); + continue; + } + int64_t remaining_ns = timespec_ns(current.it_value); + bool value_ok = remaining_ns >= 0 && + current.it_value.tv_sec > cases[i].min_remaining_sec; + if (cases[i].flags & TFD_TIMER_ABSTIME) { + struct timespec after; + if (syscall(SYS_clock_gettime, cases[i].clockid, &after) < 0) { + FAIL("clock_gettime after gettime"); + close(fd); + continue; + } + int64_t after_ns = timespec_ns(after); + value_ok = value_ok && after_ns >= before_ns && + remaining_ns >= INT64_MAX - after_ns && + remaining_ns <= INT64_MAX - before_ns; + } + uint64_t count; + errno = 0; + ssize_t got = read(fd, &count, sizeof(count)); + int read_errno = errno; + bool interval_ok = + !cases[i].interval || + (current.it_interval.tv_sec == INT64_MAX / 1000000000 && + current.it_interval.tv_nsec == INT64_MAX % 1000000000); + if (value_ok && interval_ok && got == -1 && read_errno == EAGAIN) { + PASS(); + } else { + FAIL("large timer returned an invalid remaining time or interval"); + printf(" remaining=%lld.%09ld interval=%lld.%09ld read=%ld\n", + (long long) current.it_value.tv_sec, + current.it_value.tv_nsec, + (long long) current.it_interval.tv_sec, + current.it_interval.tv_nsec, (long) got); + } + if (cases[i].rearm) { + TEST("rearmed timer old value"); + struct itimerspec disarmed = {0}, old = {0}; + EXPECT_TRUE(timerfd_settime(fd, 0, &disarmed, &old) == 0 && + timespec_ns(old.it_value) >= 0 && + old.it_value.tv_sec > cases[i].min_remaining_sec && + old.it_interval.tv_sec == INT64_MAX / 1000000000 && + old.it_interval.tv_nsec == INT64_MAX % 1000000000, + "disarm returned invalid old timer state"); + } + close(fd); + } + + SUMMARY("test-timerfd-overflow"); + return fails > 0 ? 1 : 0; +}