Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 22 additions & 28 deletions src/syscall/fd.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include "debug/log.h"
#include <sys/event.h>

#include "proved/timespec.h"
#include "syscall/linux-wire.h"
#include "syscall/fd.h"
#include "syscall/internal.h"
Expand All @@ -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.
Expand Down Expand Up @@ -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;
Expand All @@ -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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

timespec_to_ns_sat saturates at sec > TIMESPEC_SEC_MAX; ktime_set saturates at sec >= KTIME_SEC_MAX, same 9223372036. For {9223372036, 0} Linux stores KTIME_MAX and timerfd_gettime reports 9223372036.854775807, this reports 9223372036.000000000. 0.85 s on a 292 year timer, so the behavior may not be worth changing, but the comment above claims the conversion matches timespec64_to_ktime and the thresholds differ. Either clamp sec >= TIMESPEC_SEC_MAX here or soften the claim.

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 "
Expand Down Expand Up @@ -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:
Expand All @@ -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) {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions tests/manifest.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions tests/test-matrix.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
161 changes: 161 additions & 0 deletions tests/test-timerfd-overflow.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
/*
* Copyright 2026 elfuse contributors
* SPDX-License-Identifier: Apache-2.0
*/

#include <stdbool.h>
#include <stdint.h>
#include <poll.h>
#include <unistd.h>
#include <time.h>
#include <sys/syscall.h>
#include <sys/timerfd.h>

#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, &current) < 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;
}
Loading