Skip to content

[Bug] [libc] gmtime_r 对负时间戳返回非法日历字段 #11923

Open

Description

@dongly

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

最小复现 / Reproduction

在使用 RT-Thread 此实现的环境中运行(不要用宿主 libc 的同名函数代替):

#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

work = *timep % (24*60*60);
r->tm_sec = work % 60;
/* ... */
work = (int)(*timep / (24*60*60));

C 的负数除法向零截断、负余数没有规范化;后续年份循环又只从 1970 向未来遍历,无法处理负天数。

建议正确规范化负的秒内余数和天数,并支持对应的 Epoch 前日期,而不是成功返回包含负秒、零日期等非法字段的 struct tm。这不是要求直接拒绝所有负时间,也不依赖 Y2038 溢出才能触发。

最早可追溯实现和版本 / History

  • 现行组件实现的算法可追溯至 3bf68f2f42,2018-10-26,[Libc][dlib][armlibc] add gmtime_r api;首个包含该提交的正式 release 为 v3.1.1。后来搬至 common 层,不能将搬运提交误认为 bug 首次引入。
  • 更早的 libc/minilibc/time.c 中已有同类负时间算法,可追溯至 9a7ae65085,2009-10-20。历史 tag 中至少 v1.2.0 已包含该旧实现。这是“最早可追溯代码 / 最早已确认 tag”,不声称 v1.2.0 是整个项目历史上首次受影响的发布版;旧 minilibc 后来已删除。

其他实现参考 / Comparison

这些链接用于说明处理思路和预期行为,不是建议未经许可证及范围检查就直接复制实现。

Activity

  1. github-actions commented on Oct 23, 2026

    @github-actions

    如需认领并处理此 Issue,请评论 /claim。

    To claim and work on this issue, comment /claim.

  2. Yunlk commented on Oct 24, 2026

    @Yunlk
    Contributor

    在 QEMU 上按 issue 的例子实测了,现象完全一致;顺手多扫了几个更早的时间点,结果比报告举的两个例子更糟,补丁也验证过了。

    环境

    bsp/qemu-vexpress-a9,QEMU 11.0.0,arm-none-eabi-gcc 15.2.1,master 1c801efa,sizeof(time_t)=8。用的是 RT-Thread 自己的 gmtime_r()(不是宿主 libc)。

    实测(基线 vs 修复)

    t master 修复后 期望
    0 1970-01-01 00:00:00 1970-01-01 00:00:00 ✅
    86399 1970-01-01 23:59:59 1970-01-01 23:59:59 ✅
    86400 1970-01-02 00:00:00 1970-01-02 00:00:00 ✅
    -1 1970-01-01 00:00:-1,wday=4 yday=0 1969-12-31 23:59:59,wday=3 yday=364 1969-12-31 23:59:59
    -86400 1970-01-00 00:00:00,yday=-1 1969-12-31 00:00:00,yday=364 1969-12-31 00:00:00
    -31536000 1970-01--364 00:00:00,wday=-4 yday=-365 1969-01-01 00:00:00,wday=3 1969-01-01
    -2208988800 1970-01--25566 00:00:00,wday=-6 yday=-25567 1900-01-01 00:00:00,wday=1(周一) 1900-01-01

    前两行负值与你报的现象一致。后面两个是我额外扫的:tm_mday 会变成 -364、-25566 这种明显非法的值,tm_wday 也成了负数,说明不只是"差一点",而是字段整体失效。

    非负时间戳行为完全没变。

    补丁思路

    按你建议的方向做,具体三步:

    1. 先做 floor 除法——余数为负时借一天:

      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;
      }

      这样 tm_sec/tm_min/tm_hour 都是非负的。

    2. tm_wday 用规范化后的天数算,并保证结果非负:

      r->tm_wday = (int)(((4 + days % 7) % 7 + 7) % 7);

      原来 (4 + work) % 7 在 work 为负时会得到负的 wday(实测 -4、-6)。

    3. 年份遍历改成可后退:

      i = 1970;
      while (work < 0)              /* 早于 1970 的时间戳,天数为负 */
      {
          --i;
          work += __isleap(i) ? 366 : 365;
      }
      while (work >= (__isleap(i) ? 366 : 365))
      {
          work -= __isleap(i) ? 366 : 365;
          ++i;
      }

      __isleap() 本身对 1970 之前的年份是有效的,直接复用。

    补丁 +35/-9,只动 gmtime_r() 一个函数。

    两点说明

    1. int work 的类型我没有改。days 现在是 time_t,但落到年份循环时仍转成 int。对 64 位 time_t 的极端值(超过 ±2^31 天,约 ±580 万年)依然会截断。这属于另一个层面(glibc 在那种情况下返回 EOVERFLOW),与你这个 issue 无关,我没有混进来——如果你希望顺带处理,可以说明。

    2. utest 还没加。components/libc/compilers/common/utest/time_tc.c 里应该可以挂两个用例(-1 与 -86400),但我需要先确认它挂在哪个 Kconfig 下、以及哪些跑 utest 的 BSP 会执行到。如果维护者认可这个改法,我可以接着补用例再更新 PR——你之前也说过"改完把用例一起提 PR 更有说服力",这点我认同,只是不想在没确认改法方向前先把用例写进去。

    历史

    按你给的线索核对了 ChangeLog:现行实现可追到 2018-10-26 的 3bf68f2f42,与 issue 描述一致。我没有去追更早的 minilibc 版本,所以不对"最早引入"下结论。

  3. Yunlk commented on Oct 24, 2026

    @Yunlk
    Contributor

    /claim

  4. github-actions commented on Oct 24, 2026

    @github-actions

    @Yunlk has claimed this issue.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions