From 1f4140ac22303d4871077120b181e11c57e6ad6e Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 04:02:33 +0800 Subject: [PATCH 1/4] Saturate timerfd timeout conversions Clamping only seconds leaves the nanosecond addition able to overflow. Large valid timerfd requests then expire immediately or return negative intervals. Use the existing proved timespec conversion for the deadline, initial value and interval, and derive the host timeout from the same bounded value. Add six boundary cases to the unit and matrix suites. They fail under the original host, pass after the fix and pass on the QEMU Linux reference. --- src/syscall/fd.c | 37 ++++++--------- tests/manifest.txt | 1 + tests/test-matrix.sh | 2 + tests/test-timerfd-overflow.c | 89 +++++++++++++++++++++++++++++++++++ 4 files changed, 107 insertions(+), 22 deletions(-) create mode 100644 tests/test-timerfd-overflow.c diff --git a/src/syscall/fd.c b/src/syscall/fd.c index 26972590..c097eb77 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. @@ -293,26 +297,22 @@ 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 target_ns = + timespec_to_ns_sat(its.it_value_sec, its.it_value_nsec); int64_t now_ns = now.tv_sec * NS_PER_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. + /* Linux timerfd_setup converts both fields through timespec64_to_ktime, + * which saturates at KTIME_MAX. Derive the host timeout from the same + * bounded value reported by timerfd_gettime. */ - 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,14 +352,7 @@ 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 */ 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..1e347bb1 --- /dev/null +++ b/tests/test-timerfd-overflow.c @@ -0,0 +1,89 @@ +/* + * Copyright 2026 elfuse contributors + * SPDX-License-Identifier: Apache-2.0 + */ + +#include +#include +#include +#include +#include + +#include "test-harness.h" + +int main(void) +{ + int passes = 0, fails = 0; + const struct { + const char *name; + int clockid, flags; + int64_t seconds; + bool interval; + } cases[] = { + {"relative nanosecond overflow", CLOCK_MONOTONIC, 0, + INT64_MAX / 1000000000, false}, + {"relative microsecond overflow", CLOCK_MONOTONIC, 0, + INT64_MAX / 1000000, false}, + {"relative maximum seconds", CLOCK_MONOTONIC, 0, INT64_MAX, false}, + {"interval nanosecond overflow", CLOCK_MONOTONIC, 0, + INT64_MAX / 1000000000, true}, + {"absolute monotonic overflow", CLOCK_MONOTONIC, TFD_TIMER_ABSTIME, + INT64_MAX / 1000000000, false}, + {"absolute realtime overflow", CLOCK_REALTIME, TFD_TIMER_ABSTIME, + INT64_MAX / 1000000000, false}, + }; + + 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) { + requested.it_value.tv_sec = 60; + requested.it_interval = large; + } else { + requested.it_value = large; + } + if (timerfd_settime(fd, cases[i].flags, &requested, NULL) < 0) { + FAIL("timerfd_settime rejected a valid large timeout"); + close(fd); + continue; + } + + struct itimerspec current; + if (timerfd_gettime(fd, ¤t) < 0) { + FAIL("timerfd_gettime"); + close(fd); + continue; + } + 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 (current.it_value.tv_sec > 30 && interval_ok && got == -1 && + read_errno == EAGAIN) { + PASS(); + } else { + FAIL("large timer expired or returned an invalid 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); + } + close(fd); + } + + SUMMARY("test-timerfd-overflow"); + return fails > 0 ? 1 : 0; +} From 3b2bd5d4b18835a6cc4c654bd8a05b4827c55ba8 Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 06:27:01 +0800 Subject: [PATCH 2/4] Saturate host timerfd clocks and tighten tests Host clock conversions still multiply seconds without saturation. Reuse the existing helper for timer setup, reads and fdinfo snapshots, and describe the shared conversion without implying Linux's exact ktime saturation threshold. Require over a billion seconds remaining in the large-timeout cases, while keeping the 30-second floor for the 60-second initial expiry. This rejects large timers accidentally shortened to an hour. --- src/syscall/fd.c | 19 ++++++++++--------- tests/test-timerfd-overflow.c | 21 +++++++++++---------- 2 files changed, 21 insertions(+), 19 deletions(-) diff --git a/src/syscall/fd.c b/src/syscall/fd.c index c097eb77..b905da4d 100644 --- a/src/syscall/fd.c +++ b/src/syscall/fd.c @@ -275,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; @@ -299,15 +299,14 @@ int64_t sys_timerfd_settime(guest_t *g, clock_gettime(host_clock, &now); int64_t target_ns = timespec_to_ns_sat(its.it_value_sec, its.it_value_nsec); - 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 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; } - /* Linux timerfd_setup converts both fields through timespec64_to_ktime, - * which saturates at KTIME_MAX. Derive the host timeout from the same - * bounded value reported by timerfd_gettime. + /* Derive the host timeout and stored timer state from the same saturating + * nanosecond conversion. */ int64_t value_ns = timespec_to_ns_sat(its.it_value_sec, its.it_value_nsec); int64_t value_us = value_ns / 1000; @@ -358,7 +357,8 @@ int64_t sys_timerfd_settime(guest_t *g, /* 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: @@ -382,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) { @@ -503,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); @@ -1451,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/test-timerfd-overflow.c b/tests/test-timerfd-overflow.c index 1e347bb1..ef17659d 100644 --- a/tests/test-timerfd-overflow.c +++ b/tests/test-timerfd-overflow.c @@ -17,20 +17,21 @@ int main(void) const struct { const char *name; int clockid, flags; - int64_t seconds; + int64_t seconds, min_remaining_sec; bool interval; } cases[] = { {"relative nanosecond overflow", CLOCK_MONOTONIC, 0, - INT64_MAX / 1000000000, false}, + INT64_MAX / 1000000000, 1000000000, false}, {"relative microsecond overflow", CLOCK_MONOTONIC, 0, - INT64_MAX / 1000000, false}, - {"relative maximum seconds", CLOCK_MONOTONIC, 0, INT64_MAX, false}, + INT64_MAX / 1000000, 1000000000, false}, + {"relative maximum seconds", CLOCK_MONOTONIC, 0, INT64_MAX, 1000000000, + false}, {"interval nanosecond overflow", CLOCK_MONOTONIC, 0, - INT64_MAX / 1000000000, true}, + INT64_MAX / 1000000000, 30, true}, {"absolute monotonic overflow", CLOCK_MONOTONIC, TFD_TIMER_ABSTIME, - INT64_MAX / 1000000000, false}, + INT64_MAX / 1000000000, 1000000000, false}, {"absolute realtime overflow", CLOCK_REALTIME, TFD_TIMER_ABSTIME, - INT64_MAX / 1000000000, false}, + INT64_MAX / 1000000000, 1000000000, false}, }; for (unsigned i = 0; i < sizeof(cases) / sizeof(cases[0]); i++) { @@ -70,11 +71,11 @@ int main(void) !cases[i].interval || (current.it_interval.tv_sec == INT64_MAX / 1000000000 && current.it_interval.tv_nsec == INT64_MAX % 1000000000); - if (current.it_value.tv_sec > 30 && interval_ok && got == -1 && - read_errno == EAGAIN) { + if (current.it_value.tv_sec > cases[i].min_remaining_sec && + interval_ok && got == -1 && read_errno == EAGAIN) { PASS(); } else { - FAIL("large timer expired or returned an invalid interval"); + 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, From 987d85786e57daa0defeeb204a5996878694ac88 Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 06:40:55 +0800 Subject: [PATCH 3/4] Test timerfd state after a large rearm The overflow cases query timers before their first expiration, so they do not check the state recorded when a large interval is rearmed. Read a short initial expiration before checking the remaining time and repeat interval. Check the old value returned when disarming that timer. Bound the readiness wait to keep a missed expiration from hanging tests. --- tests/test-timerfd-overflow.c | 48 +++++++++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 8 deletions(-) diff --git a/tests/test-timerfd-overflow.c b/tests/test-timerfd-overflow.c index ef17659d..a21b62b2 100644 --- a/tests/test-timerfd-overflow.c +++ b/tests/test-timerfd-overflow.c @@ -5,6 +5,7 @@ #include #include +#include #include #include #include @@ -18,20 +19,22 @@ int main(void) const char *name; int clockid, flags; int64_t seconds, min_remaining_sec; - bool interval; + bool interval, rearm; } cases[] = { {"relative nanosecond overflow", CLOCK_MONOTONIC, 0, - INT64_MAX / 1000000000, 1000000000, false}, + INT64_MAX / 1000000000, 1000000000, false, false}, {"relative microsecond overflow", CLOCK_MONOTONIC, 0, - INT64_MAX / 1000000, 1000000000, false}, + INT64_MAX / 1000000, 1000000000, false, false}, {"relative maximum seconds", CLOCK_MONOTONIC, 0, INT64_MAX, 1000000000, - false}, + false, false}, {"interval nanosecond overflow", CLOCK_MONOTONIC, 0, - INT64_MAX / 1000000000, 30, true}, + INT64_MAX / 1000000000, 30, true, false}, {"absolute monotonic overflow", CLOCK_MONOTONIC, TFD_TIMER_ABSTIME, - INT64_MAX / 1000000000, 1000000000, false}, + INT64_MAX / 1000000000, 1000000000, false, false}, {"absolute realtime overflow", CLOCK_REALTIME, TFD_TIMER_ABSTIME, - INT64_MAX / 1000000000, 1000000000, false}, + 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++) { @@ -46,7 +49,10 @@ int main(void) .tv_nsec = 999999999}; struct itimerspec requested = {0}; if (cases[i].interval) { - requested.it_value.tv_sec = 60; + 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; @@ -57,6 +63,23 @@ int main(void) 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"); @@ -82,6 +105,15 @@ int main(void) (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 && + 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); } From 28388eda9355125f7b68c145316a42d36eb08a1c Mon Sep 17 00:00:00 2001 From: Suzu Date: Sat, 5 Sep 2026 08:00:18 +0800 Subject: [PATCH 4/4] Check saturated timerfd remaining values A lower bound on seconds accepts malformed nanoseconds and incorrect absolute remaining times. Reject invalid or unrepresentable timer values and check absolute timers against clock samples bracketing settime and gettime. Raw clock syscalls keep the bounds on the timer backend clock. Relative and rearmed timers retain their existing lower bounds. The case whose initial expiration is 60 seconds keeps its separate threshold. The regression passes all eight checks on elfuse release, the existing UBSAN host build and QEMU Linux. Six synthetic readback faults pass the original assertions and fail the strengthened assertions; make check-format also passes. --- tests/test-timerfd-overflow.c | 43 +++++++++++++++++++++++++++++++++-- 1 file changed, 41 insertions(+), 2 deletions(-) diff --git a/tests/test-timerfd-overflow.c b/tests/test-timerfd-overflow.c index a21b62b2..b2623af6 100644 --- a/tests/test-timerfd-overflow.c +++ b/tests/test-timerfd-overflow.c @@ -8,10 +8,23 @@ #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; @@ -57,6 +70,17 @@ int main(void) } 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); @@ -86,6 +110,21 @@ int main(void) 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)); @@ -94,8 +133,7 @@ int main(void) !cases[i].interval || (current.it_interval.tv_sec == INT64_MAX / 1000000000 && current.it_interval.tv_nsec == INT64_MAX % 1000000000); - if (current.it_value.tv_sec > cases[i].min_remaining_sec && - interval_ok && got == -1 && read_errno == EAGAIN) { + if (value_ok && interval_ok && got == -1 && read_errno == EAGAIN) { PASS(); } else { FAIL("large timer returned an invalid remaining time or interval"); @@ -109,6 +147,7 @@ int main(void) 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,