Qsc
Qsc
发布于 2026-08-26 / 12 阅读
0
0

关于Y2K38的碎碎念

2026年8月23日,系统突然登录不上了。

突然其实不太准确,是过了一天多才发现的。说登录不上其实也不太准确,准确来说是登录上了但其他接口全部401,因为生产客户端没受影响,主要影响我调试,最近比较忙,也没什么业务需求,直到昨天才管这件事。当时百思不得其解:系统好好的,最近完全没动过代码,怎么突然鉴权就失败了?

第一反应当然是查 git 提交记录。翻了翻,JWT 那块代码一行都没改过。又看了看配置,里面有一个键值对:"expired": 99999——同事设的,C/S 架构内网环境,不需要频繁登录,所以搞了个超长过期时间。虽然有点好笑但也没什么问题。

那为什么前一天还好端端的,过了一天就不行了?然后记忆突然抽了一下。

一个"跨日"的概念毫无征兆地闯了进来。紧接着,死去的 Y2038 漏洞突然从记忆深处敲了一下脑袋:99999 小时换算成时间戳,再加上当下的时间——等等,这不就是一个很眼熟的数字吗?int32 的最大值,2,147,483,647,不就和这个量级差不多?

让 AI 帮忙算了一下。好家伙,果然溢出了。刚好溢出那么一点点。

因为这个过期时间的计算是框架内部自行完成的,大概就能猜出它的代码逻辑:

// 伪代码示意
if ((int32)expired_timestamp < (int32)now_timestamp)
    return 401;

溢出之后,一个本应远在未来的过期时间戳,变成了一个负数或者一个极小的值。于是框架一比较:过期了,401。干净利落,毫不留情。

到这里其实也还好,毕竟是个很明显的漏洞应该升级一下版本就行。但接下来的事情就变得有意思了。

因为生产环境不宜升级框架,和破坏性升级组件,我开始一个一个小版本地升级验证组件,想看看哪个版本修了这个问题。结果发现,整个微软官方的 System.IdentityModel.Tokens.Jwt v6 全系列——跨越了几十个 minor 版本——统统都有这个问题。一个 int32 溢出,从 v6.0 一路躺到 v6.x,纹丝不动。直到 7.0.0 才修。

几十个小版本。无数次 CI/CD。无数行 changelog。无数个 PR review。

一个 int32 边界问题,安安静静地躺在那里,等着某一天某个倒霉蛋的过期时间戳刚好跨过那条线。

这让我想到一件事:我们对"大厂的代码"有一种天然的信任感。微软、谷歌、苹果——这些名字本身就代表着工程能力的天花板。我们会下意识地认为,这种底层框架一定是经过严密测试和反复审查的。但现实是,一个本科生在操作系统课上就会学到的整数溢出问题,可以在微软的官方认证框架里存活几十个小版本而不被发现。

这不是个例。1999 年的 Y2K,2038 年问题,Therac-25 放射治疗仪的溢出事故,阿丽亚娜 5 号火箭因为一个 int16 转 int64 的溢出炸了自己——人类在整数溢出这件事上栽的跟头,够写一本厚厚的合集了。

但每次还是会栽。因为问题的本质从来不是"程序员不知道 int32 会溢出",而是"在一个足够庞大的系统里,没有人会去检查每一行代码是否考虑了每一个边界条件"。代码审查看的是逻辑对不对,单元测试跑的是 happy path,集成测试验证的是业务流——而一个 99999 小时的过期时间戳在 2026 年 8 月的某一天刚好溢出 int32 边界,这种场景,大概不会出现在任何一份测试用例里。

所以世界果然是个草台班子。不是因为它搭得不好,而是因为它太大了,大到没有人能确保每一颗螺丝都拧到位。而那些没拧紧的螺丝,往往要等到某一天、某个角度、某阵风刚好吹到的时候,才会松脱。

只不过这次,微风刚好吹在了 2026 年 8 月 23 日。更大的风暴还在2038年1月19日等着全世界。


评论