Skip to content

[libc] fix: gmtime_r mishandles timestamps before the epoch - #11929

Open
Yunlk wants to merge 3 commits into
RT-Thread:masterfrom
Yunlk:fix/gmtime-r-negative
Open

Yunlk wants to merge 3 commits into
RT-Thread:masterfrom
Yunlk:fix/gmtime-r-negative

Conversation

@Yunlk

@Yunlk Yunlk commented Oct 24, 2026

Copy link
Copy Markdown
Contributor

拉取/合并请求描述:(PR description)

[

为什么提交这份PR (why to submit this PR)

修复 #11923。

gmtime_r() 用 C 的除法拆时间戳,而 C 的除法向零截断:

work = *timep % (24*60*60);          /* *timep < 0 时余数为负 */
work = (int)(*timep / (24*60*60));   /* 天数也是负的 */
for (i = 1970;; ++i) { ... }         /* 只能向未来遍历 */

于是负时间戳会返回一个非 NULL、但字段非法的结构体:

t master 实际 期望
-1 1970-01-01 00:00:-1(wday=4 yday=0) 1969-12-31 23:59:59
-86400 1970-01-00 00:00:00(yday=-1) 1969-12-31 00:00:00
-31536000 1970-01--364 00:00:00(wday=-4 yday=-365) 1969-01-01 00:00:00
-2208988800 1970-01--25566 00:00:00(wday=-6 yday=-25567) 1900-01-01 00:00:00

不只是秒为负,tm_mday 和 tm_wday 也会变成明显的非法值。

你的解决方案是什么 (what is your solution)

只改 gmtime_r() 一个函数,三步:

  1. 改 floor 除法:余数为负时借一天,保证 tm_sec/tm_min/tm_hour 非负
  2. tm_wday 用规范化后的天数计算,并保证结果落在 0~6(原来 (4 + work) % 7 在天数为负时返回负值)
  3. 年份遍历支持后退:while (work < 0) { --i; work += 闰年?366:365; },让早于 1970 的时间戳也能定位到正确年份

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

请提供验证的bsp和config (provide the config and bsp)

  • BSP: bsp/qemu-vexpress-a9(sizeof(time_t)=8)
  • .config: BSP 默认配置,未改动
  • action: 未使用 GitHub Actions;本地 QEMU 11.0.0 + arm-none-eabi-gcc 15.2.1,master 1c801efa

同一份固件、同一个探针,改前/改后各跑一次:

t master 本 PR
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(周一)

非负时间戳行为完全未变;1900-01-01 的 wday=1(周一)也对。

两点未包含的内容

  1. int work 的类型没改。天数在进入年份循环时仍转 int,对 64 位 time_t 的极端值(超过约 ±580 万年)依然会截断。glibc 在那种情况下返回 EOVERFLOW,但这与 [Bug] [libc] gmtime_r 对负时间戳返回非法日历字段 #11923 是两件事,没有混进本次改动。

  2. utest 暂未加入。可以在 components/libc/compilers/common/utest/time_tc.c 里挂 -1 与 -86400 两个用例,但需要先确认它归属哪个 Kconfig、以及哪些跑 utest 的 BSP 会执行到。若维护者认可当前改法,我可以补上用例后更新本 PR。

]

当前拉取/合并请求的状态 Intent for your PR

必须选择一项 Choose one (Mandatory):

  • 本拉取/合并请求是一个草稿版本 This PR is for a code-review and is intended to get feedback
  • 本拉取/合并请求是一个成熟版本 This PR is mature, and ready to be integrated into the repo

代码质量 Code Quality:

我在这个拉取/合并请求中已经考虑了 As part of this pull request, I've considered the following:

  • 已经仔细查看过代码改动的对比 Already check the difference between PR and old code
  • 代码风格正确,包括缩进空格,命名及其他风格 Style guide is adhered to, including spacing, naming and other styles
  • 没有垃圾代码,代码尽量精简,不包含#if 0代码,不包含已经被注释了的代码 All redundant code is removed and cleaned up
  • 所有变更均有原因及合理的,并且不会影响到其他软件组件代码或BSP All modifications are justified and not affect other components or BSP
  • 对难懂代码均提供对应的注释 I've commented appropriately where code is tricky
  • 代码是高质量的 Code in this PR is of high quality
  • 已经使用clang-format 源码格式化工具确保格式符合RT-Thread代码规范 This PR has been formatted with clang-format and complies with RT-Thread code specification
  • 如果是新增bsp, 已经添加ci检查到.github/ALL_BSP_COMPILE.json 详细请参考链接BSP自查

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 RT-Thread#11923
@Yunlk
Yunlk requested a review from mysterywolf as a code owner October 24, 2026 17:23
@github-actions

Copy link
Copy Markdown

👋 感谢您对 RT-Thread 的贡献!Thank you for your contribution to RT-Thread!

为确保代码符合 RT-Thread 的编码规范,请在你的仓库中执行以下步骤运行代码格式化工作流(如果格式化CI运行失败)。
To ensure your code complies with RT-Thread's coding style, please run the code formatting workflow by following the steps below (If the formatting of CI fails to run).


🛠 操作步骤 | Steps

  1. 前往 Actions 页面 | Go to the Actions page
    点击进入工作流 → | Click to open workflow →

  2. 点击 Run workflow | Click Run workflow

  • Use workflow from 保持默认分支(通常为 master)
    Keep the default branch (usually master) in Use workflow from
  • 在 branch 输入框填写 PR 分支 fix/gmtime-r-negative
    Enter PR branch fix/gmtime-r-negative in the branch field
  • 设置需排除的文件/目录(目录请以"/"结尾)
    Set files/directories to exclude (directories should end with "/")
  1. 等待工作流完成 | Wait for the workflow to complete
    格式化后的代码将作为独立提交推送至你的分支。
    The formatting changes will be pushed to your branch as a separate commit.

完成后,提交将自动更新至 fix/gmtime-r-negative 分支,关联的 Pull Request 也会同步更新。
Once completed, commits will be pushed to the fix/gmtime-r-negative branch automatically, and the related Pull Request will be updated.

如有问题欢迎联系我们,再次感谢您的贡献!💐
If you have any questions, feel free to reach out. Thanks again for your contribution!

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

📌 Code Review Assignment

🏷️ Tag: components

Reviewers: @Maihuanyi

Changed Files (Click to expand)
  • components/libc/compilers/common/ctime.c
  • components/libc/compilers/common/utest/time_tc.c

🏷️ Tag: components_libc

Reviewers: @GorrayLi @mysterywolf

Changed Files (Click to expand)
  • components/libc/compilers/common/ctime.c
  • components/libc/compilers/common/utest/time_tc.c

📊 Current Review Status (Last Updated: 2026-10-26 14:38 CST)


📝 Review Instructions

  1. 维护者可以通过单击此处来刷新审查状态: 🔄 刷新状态
    Maintainers can refresh the review status by clicking here: 🔄 Refresh Status

  2. 确认审核通过后评论 LGTM/lgtm
    Comment LGTM/lgtm after confirming approval

  3. PR合并前需至少一位维护者确认
    PR must be confirmed by at least one maintainer before merging

ℹ️ 刷新CI状态操作需要具备仓库写入权限。
ℹ️ Refresh CI status operation requires repository Write permission.

@github-actions

github-actions Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

MemBrowse Memory Report · Explore what changed →

k230

  • SRAM: .bss +4,096 B, .data +8 B, .eh_frame +13,752 B, .sbss +120 B, .srodata.cst8 +16 B, .stack -8 B, .text +62,648 B (+7.1%, 1,209,180 B / 268,300,288 B, total: 0% used)

loongson-ls1cdev

  • Code: .text +264 B (+0.1%, 363,624 B)

nordic-nrf51822

  • FLASH: .text -8 B (-0.0%, 52,028 B / 262,144 B, total: 20% used)

nuvoton-m487

  • CODE: .text +216 B (+0.1%, 356,392 B / 524,288 B, total: 68% used)

qemu-virt64-aarch64

  • Code: .eh_frame -15,064 B, .text -52,624 B (-6.3%, 1,010,124 B)
  • Data: .bss +597,200 B, .data -47,560 B (+106.2%, 1,067,160 B)

raspberry-pico-rp2040

  • FLASH: .text -8 B (-0.0%, 114,160 B / 2,097,152 B, total: 5% used)

simulator

  • Code: .eh_frame +4 B, .rela.dyn +384 B, .rodata +73 B, .text +557 B (+0.0%, 2,674,038 B)
  • Data: .data +384 B (+0.0%, 1,609,668 B)

wch-ch32v208w-r0

  • FLASH: .text +280 B (+0.2%, 159,880 B / 491,520 B, total: 33% used)

x86

  • Code: .text +128 B (+0.1%, 199,053 B)

No memory changes detected for:


Catch memory regressions in CI with MemBrowse. Free for open source.

@CYFS3

CYFS3 commented Oct 26, 2026

Copy link
Copy Markdown
Contributor

可以增加一下utest的测试。
增加 -1、-86400、-86401、1968 年闰日和纪元零点,检查全部日期字段及 tm_wday/tm_yday

Requested in the review of RT-Thread#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 RT-Thread#11923
Refs RT-Thread#11929
@Yunlk

Yunlk commented Oct 26, 2026

Copy link
Copy Markdown
Contributor Author

已按你的要求补上 utest,提交 3b77a8ad9 已推送到本分支。

加在哪

components/libc/compilers/common/utest/time_tc.c,挂在已有的 components.libc.time 测试用例下(新增一个 UTEST_UNIT_RUN(gmtime_r_before_epoch_test)),Kconfig 沿用 RT_UTEST_LIBC_TIME。

覆盖的五个点

按你说的清单逐条对应:

timestamp 期望
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 闰日 2/29) 1968-02-29 00:00:00,wday=4,yday=59

全部日期字段都断言,不只看年月日:tm_year / tm_mon / tm_mday / tm_hour / tm_min / tm_sec / tm_wday / tm_yday 八个逐一比对。之所以连 wday/yday 一起断言,是因为原实现的坏值正好出在这两个字段上(wday 会变成 -4、-6,yday 会变成 -365、-25567),只查日期的话抓不到。

-58060800 是 1968-02-29 到纪元的距离:1968 是闰年,该年剩 307 天 + 1969 的 365 天 = 672 天,672×86400 = 58060800。

验证:用例确实有鉴别力

这是我觉得更需要说明的一点——新用例在未修复的 master 上会失败,不是恒真断言:

带修复:

[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  ] [ result   ] testcase (components.libc.time)

不带修复(仅把 ctime.c 回退到 master,utest 不变):

[I/utest] t = -1: 1970-01-01 00:00:-1 wday=4 yday=0
[E/utest] [  ASSERT  ] msg: ((r.tm_year) not == (c->year))
[E/utest] [  ASSERT  ] msg: ((r.tm_mon) not == (c->mon))
[E/utest] [  ASSERT  ] msg: ((r.tm_mday) not == (c->mday))
[E/utest] [  ASSERT  ] msg: ((r.tm_hour) not == (c->hour))
[E/utest] [  ASSERT  ] msg: ((r.tm_min) not == (c->min))
[E/utest] [  ASSERT  ] msg: ((r.tm_sec) not == (c->sec))
[E/utest] [  ASSERT  ] msg: ((r.tm_wday) not == (c->wday))
[E/utest] [  ASSERT  ] msg: ((r.tm_yday) not == (c->yday))

[I/utest] t = -86400: 1970-01-00 00:00:00 wday=3 yday=-1
...(同样八条全失败)

[I/utest] t = -86401: 1970-01-00 00:00:-1 wday=3 yday=-1

五个用例每个都是八条断言全失败,说明用例确实锁住了这个缺陷。

环境:bsp/qemu-vexpress-a9,RT_UTEST_LIBC_TIME=y,QEMU 11.0.0,arm-none-eabi-gcc 15.2.1。

另外

之前 issue 里提到的 int work 类型问题(64 位 time_t 算出的天数仍会截断到 int)本次没有处理,也没有加对应用例——那属于 EOVERFLOW 那种另一个层面的处理,不适合混在这个补丁里。如果希望一并覆盖,可以说明,我另开一条处理。

@CYFS3

CYFS3 commented Oct 29, 2026

Copy link
Copy Markdown
Contributor

gcc和arm的工具链,定义不一样,一个是有符号一个是无符号的,在ac6的工具链下utest的测试会有问题。

utest_run components.libc.time
[I/utest] [==========] [ utest    ] loop 1/1
[I/utest] [==========] [ utest    ] started
[I/utest] [----------] [ testcase ] (components.libc.time) started
[I/utest] [==========] utest unit name: (timegm_all_test)
[I/utest] large day offset: timestamp=1822204800, normalized=2027-09-29 08:00:00
[I/utest] zero day: timestamp=1672444800, normalized=2022-12-31 00:00:00
[I/utest] negative day: timestamp=1709078400, normalized=2024-02-28 00:00:00
[I/utest] positive overflow: timestamp=1704160921, normalized=2024-01-02 02:02:01
[I/utest] combined negative overflow: timestamp=1638223079, normalized=2021-11-29 21:57:59
[I/utest] [==========] utest unit name: (gmtime_r_before_epoch_test)
[I/utest] epoch zero: 1970-01-01 00:00:00 wday=4 yday=0
[I/utest] t = -1: 2106-02-07 06:28:15 wday=0 yday=37
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:329); msg: ((r.tm_year) not == (c->year))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:330); msg: ((r.tm_mon) not == (c->mon))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:331); msg: ((r.tm_mday) not == (c->mday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:332); msg: ((r.tm_hour) not == (c->hour))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:333); msg: ((r.tm_min) not == (c->min))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:334); msg: ((r.tm_sec) not == (c->sec))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:335); msg: ((r.tm_wday) not == (c->wday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:336); msg: ((r.tm_yday) not == (c->yday))
[I/utest] t = -86400: 2106-02-06 06:28:16 wday=6 yday=36
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:329); msg: ((r.tm_year) not == (c->year))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:330); msg: ((r.tm_mon) not == (c->mon))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:331); msg: ((r.tm_mday) not == (c->mday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:332); msg: ((r.tm_hour) not == (c->hour))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:333); msg: ((r.tm_min) not == (c->min))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:334); msg: ((r.tm_sec) not == (c->sec))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:335); msg: ((r.tm_wday) not == (c->wday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:336); msg: ((r.tm_yday) not == (c->yday))
[I/utest] t = -86401: 2106-02-06 06:28:15 wday=6 yday=36
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:329); msg: ((r.tm_year) not == (c->year))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:330); msg: ((r.tm_mon) not == (c->mon))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:331); msg: ((r.tm_mday) not == (c->mday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:332); msg: ((r.tm_hour) not == (c->hour))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:333); msg: ((r.tm_min) not == (c->min))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:334); msg: ((r.tm_sec) not == (c->sec))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:335); msg: ((r.tm_wday) not == (c->wday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:336); msg: ((r.tm_yday) not == (c->yday))
[I/utest] 1968 leap day: 2104-04-06 06:28:16 wday=0 yday=96
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:329); msg: ((r.tm_year) not == (c->year))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:330); msg: ((r.tm_mon) not == (c->mon))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:331); msg: ((r.tm_mday) not == (c->mday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:332); msg: ((r.tm_hour) not == (c->hour))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:333); msg: ((r.tm_min) not == (c->min))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:334); msg: ((r.tm_sec) not == (c->sec))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:335); msg: ((r.tm_wday) not == (c->wday))
[E/utest] [  ASSERT  ] [ unit     ] at (time_tc.c); func: (gmtime_r_before_epoch_test:336); msg: ((r.tm_yday) not == (c->yday))
[E/utest] [  FAILED  ] [ result   ] testcase (components.libc.time)
[I/utest] [----------] [ testcase ] (components.libc.time) finished
[I/utest] [==========] [ utest    ] 1 tests from 2 testcase ran.
[I/utest] [  PASSED  ] [ result   ] 0 tests.
[E/utest] [  FAILED  ] [ result   ] 1 tests, listed below:
[E/utest] [  FAILED  ] [ result   ] components.libc.time
[I/utest] [==========] [ utest    ] finished

…igned

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 RT-Thread#11923
Refs RT-Thread#11929
@Yunlk

Yunlk commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

确认了,这是我在用例里引入的缺陷,不是环境差异。已修复,提交 669c95fd9。

你贴的日志指向的原因

t = -1 在 AC6 下算出 2106-02-07 06:28:15。这个值不是随机的——4294967295 秒正好就是 2106-02-07 06:28:15。也就是说在 AC6 上,(time_t)-1 就是 4294967295,time_t 是无符号的。

components/libc/compilers/common/include/sys/time.h:19-21 也只在自己定义 time_t 时才生效:

#ifdef _WIN32
typedef __time64_t time_t;
#endif

其他工具链用的是各自 libc 的 time_t,armclang 给的是无符号 32 位。

验证方式

我用一个 time_t = uint32_t 的参考模型跑了同样的四个负值,输出与你 AC6 日志逐字段一致:

用例 我的无符号模型 你的 AC6
t = -1 2106-02-07 06:28:15 wday=0 yday=37 2106-02-07 06:28:15 wday=0 yday=37
t = -86400 2106-02-06 06:28:16 wday=6 yday=36 2106-02-06 06:28:16 wday=6 yday=36
t = -86401 2106-02-06 06:28:15 wday=6 yday=36 2106-02-06 06:28:15 wday=6 yday=36
1968 leap 2104-04-06 06:28:16 wday=0 yday=96 2104-04-06 06:28:16 wday=0 yday=96

所以这四行不是 gmtime_r() 算错了,而是用例的期望值本身错了——无符号下"纪元前一秒"这个概念不存在。gmtime_r() 对 4294967295 返回 2106-02-07 是正确的。

修法

在用例里检测 time_t 的符号性,无符号时只跑纪元零点这一条,并打日志说明:

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

循环里跳过 i > 0 的负数用例。

没有改 gmtime_r() 实现本身:它用 time_t 类型的变量做 < 0 判断,在无符号下该分支永不成立,行为本来就是自洽的。改实现反而会让无符号构建多出一段死代码。

我这边重跑的结果(gcc,time_t 有符号)

[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)

无符号那侧我只能在参考模型上验证,没有真的 AC6 工具链跑过。如果方便的话,麻烦你在 AC6 上再跑一次确认——预期会看到 time_t is unsigned, only checking the epoch itself,然后纪元零点那条通过。

顺带一个教训

我提这个用例时只在 gcc(有符号 time_t)上验证,把"负数时间戳"当成了跨工具链成立的契约。实际上 time_t 的符号性在嵌入式里并不统一。这条应该在写用例时就检查,而不是等 AC6 报错。

This branch has not been deployed

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants