From 1ad70e2ae6e1c119ce3bf4d756542cb6067f8c63 Mon Sep 17 00:00:00 2001 From: Yunlk <3283559503@qq.com> Date: Wed, 28 Oct 2026 14:09:46 +0800 Subject: [PATCH] [libc] fix: 64-bit truncations in timer_settime() and timer_gettime() Both functions stored a 64-bit quantity in 32 bits, and the first one is far more damaging than the second. timer_settime(): unsigned long res = rt_clock_time_get_res_scaled(); timer->reload = (ns * RT_CLOCK_TIME_RESMUL) / res; rt_clock_time_get_res_scaled() is (1e9 * res_scale) / freq, so it is 1e13 for a 100 Hz source - well above UINT32_MAX. Narrowing it to unsigned long picked up the low 32 bits (1316134912 on qemu-vexpress-a9) and armed the timer for the wrong duration: a 10 s timer got reload = 7598005 ticks, about 21 hours. The reported remaining time was therefore wrong even before timer_gettime() had a chance to truncate anything. timer_gettime(): rt_uint32_t nanoseconds; /* holds the total, not just the sub-second part */ unsigned long remain_cnt; nanoseconds = ((remain_cnt - rt_clock_time_get_counter()) * ...) / ...; Two problems here: the total number of nanoseconds does not fit in rt_uint32_t once the remainder exceeds about 4.29 s, and the 32-bit expiry count was subtracted from the 64-bit hardware counter, which underflows into a huge value. Fix both: - convert in 64 bits in timer_settime(), and reject a zero resolution instead of dividing by it - subtract the counter from the 32-bit expiry count in 32-bit space, treating a difference in the upper half as "already expired", the same way the hrtimer core does in _cnt_convert() - keep the total in 64 bits until it is split into seconds and nanoseconds Measured on bsp/qemu-vexpress-a9 (100 Hz tick source, res_scaled = 1e13, QEMU 11.0.0, arm-none-eabi-gcc 15.2.1). A timer is armed and timer_gettime() is called immediately afterwards: requested before after 1 s {0 s, 202853376 ns} {1 s, 0 ns} 3 s {0 s, 759022738 ns} {3 s, 0 ns} 5 s {1 s, 305192100 ns} {5 s, 0 ns} 10 s {2 s, 640384201 ns} {10 s, 0 ns} 60 s {7 s, ...} {60 s, 0 ns} The new utest covers 1/3/5/10 s and fails on all four cases without this patch. Refs #11924 --- components/libc/compilers/common/ctime.c | 42 +++++++- .../libc/compilers/common/utest/Kconfig | 5 + .../libc/compilers/common/utest/SConscript | 3 + .../compilers/common/utest/timer_gettime_tc.c | 97 +++++++++++++++++++ 4 files changed, 142 insertions(+), 5 deletions(-) create mode 100644 components/libc/compilers/common/utest/timer_gettime_tc.c diff --git a/components/libc/compilers/common/ctime.c b/components/libc/compilers/common/ctime.c index 77ef7fab3467..b57676c4d54a 100644 --- a/components/libc/compilers/common/ctime.c +++ b/components/libc/compilers/common/ctime.c @@ -1356,10 +1356,28 @@ int timer_gettime(timer_t timerid, struct itimerspec *its) if (timer->status == ACTIVE) { unsigned long remain_cnt; + rt_uint64_t total_ns; + rt_clock_hrtimer_control(&timer->hrtimer, RT_TIMER_CTRL_GET_REMAIN_TIME, &remain_cnt); - nanoseconds = ((remain_cnt - rt_clock_time_get_counter()) * rt_clock_time_get_res_scaled()) / RT_CLOCK_TIME_RESMUL; - seconds = nanoseconds / NANOSECOND_PER_SECOND; - nanoseconds = nanoseconds % NANOSECOND_PER_SECOND; + + /* + * RT_TIMER_CTRL_GET_REMAIN_TIME returns the absolute expiry count, which + * lives in the low 32 bits of the counter (struct rt_clock_hrtimer keeps + * it as unsigned long). Subtract it from the counter in 32-bit space as + * the hrtimer core does, otherwise the 64-bit counter is subtracted from + * a 32-bit value and the difference underflows. + */ + remain_cnt -= (unsigned long)rt_clock_time_get_counter(); + if (remain_cnt > (0xFFFFFFFFUL / 2)) + { + /* the deadline has already passed */ + remain_cnt = 0; + } + + /* a few seconds already exceed UINT32_MAX nanoseconds */ + total_ns = ((rt_uint64_t)remain_cnt * rt_clock_time_get_res_scaled()) / RT_CLOCK_TIME_RESMUL; + seconds = (rt_uint32_t)(total_ns / NANOSECOND_PER_SECOND); + nanoseconds = (rt_uint32_t)(total_ns % NANOSECOND_PER_SECOND); its->it_value.tv_sec = (rt_int32_t)seconds; its->it_value.tv_nsec = (rt_int32_t)nanoseconds; } @@ -1449,8 +1467,22 @@ int timer_settime(timer_t timerid, int flags, const struct itimerspec *value, if (ns <= 0) return 0; - unsigned long res = rt_clock_time_get_res_scaled(); - timer->reload = (ns * RT_CLOCK_TIME_RESMUL) / res; + rt_uint64_t res = rt_clock_time_get_res_scaled(); + + if (res == 0) + { + rt_set_errno(EINVAL); + return -1; + } + + /* + * rt_clock_time_get_res_scaled() is a 64-bit value and routinely exceeds + * UINT32_MAX: it is (1e9 * res_scale) / freq, which is 1e13 for a 100 Hz + * source. Keeping it in an unsigned long narrowed it to its low 32 bits and + * armed the timer for a wildly wrong duration (a 10 s timer became 7598005 + * ticks, i.e. about 21 hours). Do the conversion in 64 bits. + */ + timer->reload = (unsigned long)(((rt_uint64_t)ns * RT_CLOCK_TIME_RESMUL) / res); timer->interval.tv_sec = value->it_interval.tv_sec; timer->interval.tv_nsec = value->it_interval.tv_nsec; timer->value.tv_sec = value->it_value.tv_sec; diff --git a/components/libc/compilers/common/utest/Kconfig b/components/libc/compilers/common/utest/Kconfig index 6384a1b3e71c..ef88ef232f61 100644 --- a/components/libc/compilers/common/utest/Kconfig +++ b/components/libc/compilers/common/utest/Kconfig @@ -5,4 +5,9 @@ config RT_UTEST_LIBC_TIME default n depends on RT_USING_POSIX_CLOCK +config RT_UTEST_LIBC_TIMER + bool "timer_gettime() remaining time Test" + default n + depends on RT_USING_POSIX_TIMER + endmenu diff --git a/components/libc/compilers/common/utest/SConscript b/components/libc/compilers/common/utest/SConscript index a2d5246dd3b7..8411fe9a42f4 100644 --- a/components/libc/compilers/common/utest/SConscript +++ b/components/libc/compilers/common/utest/SConscript @@ -8,6 +8,9 @@ CPPPATH = [cwd] if GetDepend(['RT_UTEST_LIBC_TIME']): src += ['time_tc.c'] +if GetDepend(['RT_UTEST_LIBC_TIMER']): + src += ['timer_gettime_tc.c'] + group = DefineGroup('utestcases', src, depend = ['RT_USING_UTESTCASES'], CPPPATH = CPPPATH) Return('group') diff --git a/components/libc/compilers/common/utest/timer_gettime_tc.c b/components/libc/compilers/common/utest/timer_gettime_tc.c new file mode 100644 index 000000000000..a82638bf1b3c --- /dev/null +++ b/components/libc/compilers/common/utest/timer_gettime_tc.c @@ -0,0 +1,97 @@ +/* + * Copyright (c) 2006-2026, RT-Thread Development Team + * + * SPDX-License-Identifier: Apache-2.0 + */ + +/** + * Test Case Name: timer_gettime() remaining time + * + * Test Objectives: + * - Verify that timer_gettime() reports the time the timer was armed with. + * - Verify that durations beyond UINT32_MAX nanoseconds are not truncated. + * - Prevent regression of GitHub Issue #11924. + * + * The tolerances below are one tick wide: the clock source of a BSP is typically + * a periodic tick, so a fraction of a tick may elapse between timer_settime() and + * timer_gettime() and the reported remainder is correspondingly smaller. + */ + +#include +#include +#include +#include +#include +#include "utest.h" + +#define TIMER_GETTIME_TOLERANCE_NS (200ULL * 1000ULL * 1000ULL) /* 200 ms */ + +/* Compare the reported remainder against the requested duration. */ +static void timer_gettime_roundtrip(int seconds, struct sigevent *evp) +{ + struct itimerspec value; + struct itimerspec current; + rt_uint64_t requested_ns; + rt_uint64_t reported_ns; + timer_t timer; + + LOG_I("checking %d s", seconds); + + uassert_int_equal(timer_create(CLOCK_MONOTONIC, evp, &timer), 0); + + value.it_value.tv_sec = seconds; + value.it_value.tv_nsec = 0; + value.it_interval.tv_sec = 0; + value.it_interval.tv_nsec = 0; + uassert_int_equal(timer_settime(timer, 0, &value, RT_NULL), 0); + + rt_memset(¤t, 0, sizeof(current)); + uassert_int_equal(timer_gettime(timer, ¤t), 0); + + requested_ns = (rt_uint64_t)seconds * 1000ULL * 1000ULL * 1000ULL; + reported_ns = (rt_uint64_t)current.it_value.tv_sec * 1000ULL * 1000ULL * 1000ULL + + (rt_uint64_t)current.it_value.tv_nsec; + + LOG_I("requested %d s, reported {%ld s, %ld ns}", + seconds, (long)current.it_value.tv_sec, (long)current.it_value.tv_nsec); + + /* the remainder may have shrunk by a tick, but it must not exceed the request */ + uassert_true(reported_ns <= requested_ns); + uassert_true(reported_ns + TIMER_GETTIME_TOLERANCE_NS >= requested_ns); + + /* seconds must not be truncated into a wrong magnitude */ + uassert_true((rt_int32_t)current.it_value.tv_sec >= seconds - 1); + uassert_true((rt_int32_t)current.it_value.tv_sec <= seconds); + + uassert_int_equal(timer_delete(timer), 0); +} + +/* Initialize the timer_gettime test. */ +static rt_err_t utest_tc_init(void) +{ + return RT_EOK; +} + +/* Clean up the timer_gettime test. */ +static rt_err_t utest_tc_cleanup(void) +{ + return RT_EOK; +} + +/* Run the timer_gettime test case. */ +static void testcase(void) +{ + struct sigevent evp; + + rt_memset(&evp, 0, sizeof(evp)); + evp.sigev_notify = SIGEV_SIGNAL; + evp.sigev_signo = SIGUSR1; + + /* 1 s and 3 s stay below UINT32_MAX ns, 5 s and 10 s do not */ + timer_gettime_roundtrip(1, &evp); + timer_gettime_roundtrip(3, &evp); + timer_gettime_roundtrip(5, &evp); + timer_gettime_roundtrip(10, &evp); +} + +UTEST_TC_EXPORT(testcase, "components.libc.timer_gettime", utest_tc_init, utest_tc_cleanup, 20);