### RT-Thread Version master, commit `a59e72fb5c2f272761a2813fc9bd9fe18eb29d7d`. ### Affected area libc, `components/libc/compilers/common/ctime.c`, `gmtime_r()`. ### Hardware / Architecture / Toolchain 不限定具体硬件;要求 `time_t` 为有符号类型。本报告使用源码级隔离复现,无需 RTC 硬件。该问题与 `time_t` 是 32 位还是 64 位无关。 ### 问题描述 / Describe the bug `gmtime_r()` 对可表示的负时间戳返回非 NULL,但生成非法的日历字段。例如 `time_t t = -1` 得到 `1970-01-01 00:00:-1`,而不是 `1969-12-31 23:59:59 UTC`。 [固定版本源码 L166–215](https://github.com/RT-Thread/rt-thread/blob/a59e72fb5c2f272761a2813fc9bd9fe18eb29d7d/components/libc/compilers/common/ctime.c#L166-L215) ### 最小复现 / Reproduction 在使用 RT-Thread 此实现的环境中运行(不要用宿主 libc 的同名函数代替): ```c #include <time.h> #include <stdio.h> void reproduce(void) { time_t t = (time_t)-1; struct tm r; if (gmtime_r(&t, &r) != NULL) { printf("%d-%02d-%02d %02d:%02d:%02d\n", r.tm_year + 1900, r.tm_mon + 1, r.tm_mday, r.tm_hour, r.tm_min, r.tm_sec); } } ``` 预期:`1969-12-31 23:59:59`。 实际:`1970-01-01 00:00:-1`。 另一个反例:`t = -86400`,应为 `1969-12-31 00:00:00`,却得到 `1970-01-00 00:00:00`,其中 `tm_yday=-1`。 验证方式:此前已提取 RT-Thread 原始 `gmtime_r()`、月份表和闰年辅助代码,在隔离的 signed time32/time64 模型中运行;仅替换 RT-Thread 的内存和 errno 接口,得到上述结果。未进行目标板验证。 ### Cause / Suggested direction ```c work = *timep % (24*60*60); r->tm_sec = work % 60; /* ... */ work = (int)(*timep / (24*60*60)); ``` C 的负数除法向零截断、负余数没有规范化;后续年份循环又只从 1970 向未来遍历,无法处理负天数。 建议正确规范化负的秒内余数和天数,并支持对应的 Epoch 前日期,而不是成功返回包含负秒、零日期等非法字段的 `struct tm`。这不是要求直接拒绝所有负时间,也不依赖 Y2038 溢出才能触发。 ### 最早可追溯实现和版本 / History - 现行组件实现的算法可追溯至 [3bf68f2f42](https://github.com/RT-Thread/rt-thread/commit/3bf68f2f42),2018-10-26,`[Libc][dlib][armlibc] add gmtime_r api`;首个包含该提交的正式 release 为 **v3.1.1**。后来搬至 common 层,不能将搬运提交误认为 bug 首次引入。 - 更早的 `libc/minilibc/time.c` 中已有同类负时间算法,可追溯至 [9a7ae65085](https://github.com/RT-Thread/rt-thread/commit/9a7ae65085),2009-10-20。历史 tag 中至少 **v1.2.0** 已包含该旧实现。这是“最早可追溯代码 / 最早已确认 tag”,不声称 v1.2.0 是整个项目历史上首次受影响的发布版;旧 minilibc 后来已删除。 ### 其他实现参考 / Comparison - [glibc 2.42 time/offtime.c](https://github.com/bminor/glibc/blob/glibc-2.42/time/offtime.c):对负的日内余数补上一天并减少天数,再处理 Epoch 前后的年份。 - [musl 1.2.5 __secs_to_tm.c](https://git.musl-libc.org/cgit/musl/tree/src/time/__secs_to_tm.c?h=v1.2.5):显式处理 `remsecs < 0`,并对负的历法周期余数进行规范化。 - [newlib gmtime_r.c](https://github.com/mirror/newlib-cygwin/blob/9087163804df8af6dc2ec1f675a2341c25f7795f/newlib/libc/time/gmtime_r.c):同样先规范化负余数,并采用 civil-from-days 类算法。 - [Zephyr gmtime_r.c](https://github.com/zephyrproject-rtos/zephyr/blob/3a15e37b5a9a0d17c111eecb3a1803c2ab20cd5d/lib/libc/common/source/time/gmtime_r.c):使用能处理负日期的日历换算算法。 这些链接用于说明处理思路和预期行为,不是建议未经许可证及范围检查就直接复制实现。
Activity
github-actions commentedon Oct 23, 2026
如需认领并处理此 Issue,请评论
/claim。To claim and work on this issue, comment
/claim.Yunlk commentedon Oct 24, 2026
在 QEMU 上按 issue 的例子实测了,现象完全一致;顺手多扫了几个更早的时间点,结果比报告举的两个例子更糟,补丁也验证过了。
环境
bsp/qemu-vexpress-a9,QEMU 11.0.0,arm-none-eabi-gcc 15.2.1,master1c801efa,sizeof(time_t)=8。用的是 RT-Thread 自己的gmtime_r()(不是宿主 libc)。实测(基线 vs 修复)
前两行负值与你报的现象一致。后面两个是我额外扫的:
tm_mday会变成-364、-25566这种明显非法的值,tm_wday也成了负数,说明不只是"差一点",而是字段整体失效。非负时间戳行为完全没变。
补丁思路
按你建议的方向做,具体三步:
先做 floor 除法——余数为负时借一天:
这样
tm_sec/tm_min/tm_hour都是非负的。tm_wday用规范化后的天数算,并保证结果非负:原来
(4 + work) % 7在work为负时会得到负的 wday(实测 -4、-6)。年份遍历改成可后退:
__isleap()本身对 1970 之前的年份是有效的,直接复用。补丁 +35/-9,只动
gmtime_r()一个函数。两点说明
int work的类型我没有改。days现在是time_t,但落到年份循环时仍转成int。对 64 位time_t的极端值(超过 ±2^31 天,约 ±580 万年)依然会截断。这属于另一个层面(glibc 在那种情况下返回EOVERFLOW),与你这个 issue 无关,我没有混进来——如果你希望顺带处理,可以说明。utest 还没加。
components/libc/compilers/common/utest/time_tc.c里应该可以挂两个用例(-1 与 -86400),但我需要先确认它挂在哪个 Kconfig 下、以及哪些跑 utest 的 BSP 会执行到。如果维护者认可这个改法,我可以接着补用例再更新 PR——你之前也说过"改完把用例一起提 PR 更有说服力",这点我认同,只是不想在没确认改法方向前先把用例写进去。历史
按你给的线索核对了
ChangeLog:现行实现可追到 2018-10-26 的3bf68f2f42,与 issue 描述一致。我没有去追更早的minilibc版本,所以不对"最早引入"下结论。Yunlk commentedon Oct 24, 2026
/claim
github-actions commentedon Oct 24, 2026
@Yunlk has claimed this issue.