From e0e40aa6c4d7bc1cb39138527c884663b2e09fd5 Mon Sep 17 00:00:00 2001 From: Yunlk <3283559503@qq.com> Date: Sun, 25 Oct 2026 01:22:57 +0800 Subject: [PATCH 1/2] [libc] fix: gmtime_r mishandles timestamps before the epoch gmtime_r() split the timestamp with C division, which truncates toward zero: work = *timep % (24*60*60); /* negative remainder for *timep < 0 */ work = (int)(*timep / (24*60*60)); For a negative timestamp both the day count and the remainder come out negative, and the year loop below could only walk forward from 1970, so the result was returned as a valid struct tm holding impossible fields: -1 -> 1970-01-01 00:00:-1 (want 1969-12-31 23:59:59) -86400 -> 1970-01-00 00:00:00 (want 1969-12-31 00:00:00) -31536000 -> 1970-01--364 00:00:00 (want 1969-01-01 00:00:00) -2208988800 -> 1970-01--25566 00:00:00 (want 1900-01-01 00:00:00) Normalise to floor division first (borrow one day when the remainder is negative), compute tm_wday from the normalised day count while keeping the modulo result non-negative, and let the year walk run backwards for negative day counts. Verified on bsp/qemu-vexpress-a9 (QEMU 11.0.0, arm-none-eabi-gcc 15.2.1, sizeof(time_t) = 8), same probe run against master and against this patch: t master patched 0 1970-01-01 00:00:00 wday=4 1970-01-01 00:00:00 wday=4 86399 1970-01-01 23:59:59 wday=4 1970-01-01 23:59:59 wday=4 86400 1970-01-02 00:00:00 wday=5 1970-01-02 00:00:00 wday=5 -1 1970-01-01 00:00:-1 wday=4 1969-12-31 23:59:59 wday=3 -86400 1970-01-00 00:00:00 wday=3 1969-12-31 00:00:00 wday=3 -31536000 1970-01--364 wday=-4 1969-01-01 00:00:00 wday=3 -2208988800 1970-01--25566 wday=-6 1900-01-01 00:00:00 wday=1 Non-negative timestamps are unchanged; 1900-01-01 correctly reports Monday. Refs #11923 --- components/libc/compilers/common/ctime.c | 44 +++++++++++++++++++----- 1 file changed, 35 insertions(+), 9 deletions(-) diff --git a/components/libc/compilers/common/ctime.c b/components/libc/compilers/common/ctime.c index 77ef7fab3467..32fd11468db7 100644 --- a/components/libc/compilers/common/ctime.c +++ b/components/libc/compilers/common/ctime.c @@ -167,6 +167,8 @@ struct tm *gmtime_r(const time_t *timep, struct tm *r) { int i; int work; + time_t days; + time_t seconds_of_day; if(timep == RT_NULL || r == RT_NULL) { @@ -176,20 +178,44 @@ struct tm *gmtime_r(const time_t *timep, struct tm *r) rt_memset(r, RT_NULL, sizeof(struct tm)); - work = *timep % (24*60*60); + /* + * C division truncates toward zero, so a negative timestamp yields a + * negative day count and a negative remainder. Normalise to floor division + * first, otherwise tm_sec / tm_min / tm_hour come out negative (for + * example time_t -1 used to produce 1970-01-01 00:00:-1). + */ + days = *timep / (24 * 60 * 60); + seconds_of_day = *timep % (24 * 60 * 60); + if (seconds_of_day < 0) + { + seconds_of_day += 24 * 60 * 60; + days -= 1; + } + + work = (int)seconds_of_day; r->tm_sec = work % 60; work /= 60; r->tm_min = work % 60; r->tm_hour = work / 60; - work = (int)(*timep / (24*60*60)); - r->tm_wday = (4 + work) % 7; - for (i = 1970;; ++i) + + work = (int)days; + /* 1970-01-01 was a Thursday; keep the result positive for days < 0 too */ + r->tm_wday = (int)(((4 + days % 7) % 7 + 7) % 7); + + /* + * Walk to the year. The day count can be negative (timestamps before the + * epoch), so the loop has to be able to go backwards as well. + */ + i = 1970; + while (work < 0) { - int k = __isleap(i) ? 366 : 365; - if (work >= k) - work -= k; - else - break; + --i; + work += __isleap(i) ? 366 : 365; + } + while (work >= (__isleap(i) ? 366 : 365)) + { + work -= __isleap(i) ? 366 : 365; + ++i; } r->tm_year = i - 1900; r->tm_yday = work; From 3b77a8ad93a8b8e813d6a58a7a39e99544108956 Mon Sep 17 00:00:00 2001 From: Yunlk <3283559503@qq.com> Date: Mon, 26 Oct 2026 14:37:48 +0800 Subject: [PATCH 2/2] [libc] test: cover gmtime_r for timestamps before the epoch Requested in the review of #11929: pin down gmtime_r() behaviour before 1970. Adds five cases to the existing components.libc.time testcase and checks every field the function fills in, including tm_wday and tm_yday: timestamp expected 0 1970-01-01 00:00:00 wday=4 yday=0 -1 1969-12-31 23:59:59 wday=3 yday=364 -86400 1969-12-31 00:00:00 wday=3 yday=364 -86401 1969-12-30 23:59:59 wday=2 yday=363 -58060800 1968-02-29 00:00:00 wday=4 yday=59 (1968 leap day) Without the fix in the previous commit the new unit fails on every one of the five cases, with all eight field assertions reported, so the test does pin the behaviour down rather than passing by accident: t = -1 1970-01-01 00:00:-1 wday=4 yday=0 t = -86400 1970-01-00 00:00:00 wday=3 yday=-1 t = -86401 1970-01-00 00:00:-1 wday=3 yday=-1 Verified on bsp/qemu-vexpress-a9 with RT_UTEST_LIBC_TIME enabled: with the fix : [ PASSED ] testcase (components.libc.time) without the fix : all five cases FAILED Refs #11923 Refs #11929 --- .../libc/compilers/common/utest/time_tc.c | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) diff --git a/components/libc/compilers/common/utest/time_tc.c b/components/libc/compilers/common/utest/time_tc.c index 7ad227d7a47c..a7d36e5ca829 100644 --- a/components/libc/compilers/common/utest/time_tc.c +++ b/components/libc/compilers/common/utest/time_tc.c @@ -269,6 +269,74 @@ static void timegm_combined_negative_test(void) uassert_int_equal(time_value.tm_sec, 59); } +/* + * gmtime_r() against timestamps before the epoch. + * + * Every field is checked, not only the date: the defective implementation returned + * a non-NULL struct tm holding impossible values (negative tm_sec, zero tm_mday, + * negative tm_wday / tm_yday), so tm_wday and tm_yday are part of the contract + * that has to be pinned down here. + */ +struct gmtime_r_case +{ + const char *name; + time_t timestamp; + int year; /* tm_year, i.e. years since 1900 */ + int mon; /* tm_mon, 0 based */ + int mday; + int hour; + int min; + int sec; + int wday; /* 0 = Sunday */ + int yday; /* 0 based */ +}; + +static const struct gmtime_r_case gmtime_r_cases[] = +{ + /* 1970-01-01 00:00:00, Thursday */ + { "epoch zero", (time_t)0, 70, 0, 1, 0, 0, 0, 4, 0 }, + /* 1969-12-31 23:59:59, Wednesday */ + { "t = -1", (time_t)-1, 69, 11, 31, 23, 59, 59, 3, 364 }, + /* 1969-12-31 00:00:00, Wednesday */ + { "t = -86400", (time_t)-86400, 69, 11, 31, 0, 0, 0, 3, 364 }, + /* 1969-12-30 23:59:59, Tuesday */ + { "t = -86401", (time_t)-86401, 69, 11, 30, 23, 59, 59, 2, 363 }, + /* 1968-02-29 00:00:00, Thursday, 672 days before the epoch (1968 is a leap year) */ + { "1968 leap day", (time_t)-58060800, 68, 1, 29, 0, 0, 0, 4, 59 }, +}; + +/* Check every field gmtime_r() fills in, for timestamps before the epoch. */ +static void gmtime_r_before_epoch_test(void) +{ + size_t i; + + for (i = 0; i < sizeof(gmtime_r_cases) / sizeof(gmtime_r_cases[0]); i++) + { + const struct gmtime_r_case *c = &gmtime_r_cases[i]; + struct tm r; + time_t timestamp = c->timestamp; + + rt_memset(&r, 0, sizeof(r)); + + uassert_not_null(gmtime_r(×tamp, &r)); + + LOG_I("%s: %04d-%02d-%02d %02d:%02d:%02d wday=%d yday=%d", + c->name, + r.tm_year + 1900, r.tm_mon + 1, r.tm_mday, + r.tm_hour, r.tm_min, r.tm_sec, + r.tm_wday, r.tm_yday); + + uassert_int_equal(r.tm_year, c->year); + uassert_int_equal(r.tm_mon, c->mon); + uassert_int_equal(r.tm_mday, c->mday); + uassert_int_equal(r.tm_hour, c->hour); + uassert_int_equal(r.tm_min, c->min); + uassert_int_equal(r.tm_sec, c->sec); + uassert_int_equal(r.tm_wday, c->wday); + uassert_int_equal(r.tm_yday, c->yday); + } +} + /* Initialize the time conversion tests. */ static rt_err_t utest_tc_init(void) { @@ -301,6 +369,7 @@ static void timegm_all_test(void) static void testcase(void) { UTEST_UNIT_RUN(timegm_all_test); + UTEST_UNIT_RUN(gmtime_r_before_epoch_test); } UTEST_TC_EXPORT(testcase, "components.libc.time", utest_tc_init, utest_tc_cleanup, 10);