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/3] [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/3] [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); From 669c95fd90b72dfef64917b69f59d605a0b42d0c Mon Sep 17 00:00:00 2001 From: Yunlk <3283559503@qq.com> Date: Mon, 2 Nov 2026 15:59:18 +0800 Subject: [PATCH 3/3] [libc] test: make the gmtime_r pre-epoch case work when time_t is unsigned gmtime_r_before_epoch_test() assumed a signed time_t. armclang (AC6) defines time_t as an unsigned 32-bit type, so (time_t)-1 is 4294967295, which is a perfectly valid timestamp meaning 2106-02-07 06:28:15 - not the epoch minus one second. The case therefore failed on that toolchain with every field mismatching: t = -1 2106-02-07 06:28:15 wday=0 yday=37 t = -86400 2106-02-06 06:28:16 wday=6 yday=36 t = -86401 2106-02-06 06:28:15 wday=6 yday=36 1968 leap 2104-04-06 06:28:16 wday=0 yday=96 Those values are not a defect in gmtime_r(); they are what an unsigned 4294967295-second offset actually is. Only the test's expectation was wrong. Detect the signedness at compile time by comparing (time_t)-1 against 0: when time_t is unsigned that expression is false. Keep checking the epoch entry everywhere, and skip the pre-epoch entries when time_t cannot represent them, logging which of the two happened so the run is self-explanatory. Verified on bsp/qemu-vexpress-a9 with arm-none-eabi-gcc, where time_t is signed: [I/utest] time_t is signed, checking pre-epoch timestamps [I/utest] epoch zero: 1970-01-01 00:00:00 wday=4 yday=0 [I/utest] t = -1: 1969-12-31 23:59:59 wday=3 yday=364 [I/utest] t = -86400: 1969-12-31 00:00:00 wday=3 yday=364 [I/utest] t = -86401: 1969-12-30 23:59:59 wday=2 yday=363 [I/utest] 1968 leap day: 1968-02-29 00:00:00 wday=4 yday=59 [I/utest] [ PASSED ] testcase (components.libc.time) The unsigned path was checked against a reference model that reproduces the AC6 output exactly for all four pre-epoch entries. Refs #11923 Refs #11929 --- .../libc/compilers/common/utest/time_tc.c | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/components/libc/compilers/common/utest/time_tc.c b/components/libc/compilers/common/utest/time_tc.c index a7d36e5ca829..6270bb561c53 100644 --- a/components/libc/compilers/common/utest/time_tc.c +++ b/components/libc/compilers/common/utest/time_tc.c @@ -310,12 +310,34 @@ static void gmtime_r_before_epoch_test(void) { size_t i; + /* + * A timestamp before the epoch only exists when time_t is signed. Some + * toolchains (armclang/AC6 among them) define time_t as an unsigned 32-bit + * type; there (time_t)-1 is 4294967295, i.e. 2106-02-07, so the negative + * cases below do not describe anything and the first entry has to be + * checked on its own. + */ + if ((time_t)-1 < (time_t)0) + { + LOG_I("time_t is signed, checking pre-epoch timestamps"); + } + else + { + LOG_I("time_t is unsigned, only checking the epoch itself"); + } + 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; + /* the pre-epoch cases are meaningless when time_t cannot hold them */ + if (i > 0 && !((time_t)-1 < (time_t)0)) + { + continue; + } + rt_memset(&r, 0, sizeof(r)); uassert_not_null(gmtime_r(×tamp, &r));