ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

.NET 9 Linux arm32 的 Y2038 支持:64 位 time_t 迁移与 OpenSSL ABI 兼容方案

.NET 9 Linux arm32 的 Y2038 支持:64 位 time_t 迁移与 OpenSSL ABI 兼容方案 .NET 9 Linux arm32 的 Y2038 支持64 位 time_t 迁移与 OpenSSL ABI 兼容方案【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime导读本篇基于 .NET runtime 仓库的设计文档 深入讲解 .NET 9 如何在 Linux arm32ARMv7/ArmHF架构上解决 Y2038 问题一方面将构建基线迁移到支持 64 位time_t的 glibc 2.35对齐 Ubuntu 24.04另一方面通过巧妙的 ABI 探测技巧检测系统 OpenSSL 是否仍使用 32 位time_t从而在证书链验证、OCSP 响应过期检查等关键路径上避免时间溢出带来的安全风险。读完本文你将掌握 .NET 官方构建的 libc 兼容策略、_TIME_BITS64的来龙去脉以及运行时如何预测性失败地处理 32 位与 64 位time_t的混用场景。背景Y2038 问题与 Linux 生态的三层解决路径Y2038 问题的根源在于经典 Unix 时间戳用 32 位有符号整数表示自 1970-01-01 起的秒数其最大值 2147483647 对应 2038-01-19 03:14:07 UTC。Linux 生态从内核、C 库到发行版三个层面逐级推进了 64 位time_t的改造。内核层新增 64 位时间 syscall内核层面的 Y2038 支持体现在新增一组使用 64 位time_t的系统调用例如clock_gettime64、clock_settime64等。关键是内核保持了向后兼容仍在使用 32 位time_t的用户态程序可以继续在这类内核上运行改造并不强制。glibc 层__TIME_BITS编译期开关glibc 在 2.34 版本引入 Y2038 支持同样采取向后兼容策略对于每一个原来使用 32 位时间的 API 符号新增一个 64 位等价符号如mktime对应__mktime64。glibc 头文件提供宏__TIME_BITS编译期决定引用哪一组符号——当使用__TIME_BITS64构建时源码中的mktime调用实际会被编译为对__mktime64的调用。发行版层ABI 断裂与全量重编译glibc 的兼容止步于 C 库自身。真正引入断裂的是发行版Ubuntu 24.04Noble Numbat成为首个默认对 Linux arm32armhf 架构启用 64 位time_t的 Ubuntu 版本Debian 13 则计划成为首个启用 64 位时间的 Debian 版本。发行版需要把所有公开接口引用过time_t的库和应用用__TIME_BITS64重新编译这就在公开面涉及time_t的库边界上造成了 ABI 不兼容。通常情况下这类 ABI 断裂对 Linux 生态不是问题因为发行版会统一用新 ABI 重建所有包。但 .NET 的官方发行版有其特殊性一套二进制要兼容一大片发行版只要架构与 libc 口味匹配即可这与 Python manylinux wheel 的思路类似。因此 .NET 必须在跟随最新发行版与兼容旧发行版之间做出取舍。musl 侧1.2.0 起支持musl libc 自 1.2.0 版本起支持 64 位time_t并被 Alpine 3.13 采纳。.NET 官方构建体系rootfs 决定 libc 兼容基线要理解 .NET 的 Y2038 决策必须先理解 .NET 官方构建产物是如何产生的。.NET 官方构建使用一套专用构建镜像镜像运行 Azure Linux 3.0 并提供最新的交叉编译工具链同时内置一个旧发行版的 rootfs——正是这个旧 rootfs 的 glibc或 musl libc版本决定了 .NET 构建产物的 libc 兼容下限。以 .NET 8 为例通过分别使用 Ubuntu 16.04 与 Alpine 3.13 的 rootfs 构建.NET 8 支持 glibc 2.23 与 musl 1.2.2。这意味着在 .NET 8 时代glibc 构建的 arm32 版本默认仍使用 32 位time_t。.NET 9 的 Y2038 支持策略先对齐最新发行版再向后兼容.NET 官方对新版本的默认姿态是先与最新 Linux 发行版对齐再倒推确定对旧发行版的合理兼容故事。据此.NET 9 Linux arm32 构建的默认姿态是要求 glibc 2.35来自 Ubuntu 22.04默认使用 64 位time_t构建。这一变更对齐了 Ubuntu 24.04 的 armhf 默认行为。由于 Ubuntu 22.04 的 glibc2.35比 Ubuntu 24.04 的 glibc2.39更老选择 22.04 作为 rootfs 在满足 64 位time_t需求的同时把兼容面最大化地覆盖到了更多旧发行版。.NET 8 已覆盖的部分实际上 .NET 8 就已经支持了 musl Linux 上以及除 32 位 arm 之外其他架构上的 64 位时间。因此 .NET 9 真正要补的短板只剩基于 glibc 的 arm32 Linux 构建这一个组合。glibc 侧改造rootfs 升级 _TIME_BITS64glibc 侧的改动相对直接由于 .NET 构建此前已经定义了_TIME_BITS64主要工作就是把 arm32 构建的 rootfs 从旧版 Ubuntu 升级到一个 glibc 支持_TIME_BITS的 Ubuntu 版本即 Ubuntu 22.04。由此 .NET 以 64 位time_t构建DateTime.UtcNow在 2038 年之后也能正确工作。从仓库源码看这个构建必须使用 64 位time_t的约束被以编译期断言的形式固化在代码中。opensslshim.c 的初始化函数 中有#if defined(TARGET_ARM) defined(TARGET_LINUX) !defined(TARGET_ANDROID) c_static_assert_msg(sizeof(time_t) 8, Build requires 64-bit time_t.);也就是说在 arm32 Linux非 Android的官方构建中time_t的大小被硬性断言为 8 字节从编译层面杜绝了误用 32 位time_t构建产物的可能。OpenSSL 侧改造核心难点与 ABI 探测技巧问题本质64 位值传入 32 位函数要求 64 位time_t兼容的 glibc并不意味着用户空间其余部分也全部完成了 64 位时间迁移——事实上在 Ubuntu 24.04 / Debian 13 之前发行版自带的 OpenSSL 很可能仍是 32 位time_t构建。这就产生了错位.NET 以 64 位time_t调用 OpenSSL 中期望 32 位time_t的函数。最初的故障分析源自 Ubuntu 24.04 上 Linux arm32 构建开始出现 OpenSSL 相关错误的 issue定位到两个具体调用点X509_VERIFY_PARAM_set_time向 OpenSSL 传入时间用于证书链验证X509_cmp_time将当前时间与 OCSP 响应返回的时间比较用于获取证书过期时间。在这两处.NET 传入的 64 位值会被 32 位time_t的 OpenSSL 误解读。为什么不能直接 clamp 到 32 位一个直觉的修法是把超出 32 位的时间钳制clamp到INT_MAX/INT_MIN。但 .NET 团队明确否决了这一方案因为它可能打开安全漏洞例如在 Y2038 回绕rollover期间OpenSSL 内部会拿一个没有 Y2038 问题的ASN1_GENERALIZEDTIME证书有效期字段8 字节时间字符串与一个被钳制的 32 位时间比较这可能导致 OpenSSL 错误地接受一张本应在 2038 年之后过期的证书链。因此正确的思路是检测当前运行环境中的 OpenSSL 是否使用 32 位time_t若使用则在遇到无法用 32 位表示的时间时可预测地失败而非静默产生错误结果。ABI 探测OPENSSL_gmtime的妙用检测方案基于 OpenSSL 的一个简单函数OPENSSL_gmtime它接收一个time_t*指针返回分解后的struct tm。关键技巧如下.NET 侧64 位time_t构建传入一个指向 64 位值的指针若 OpenSSL 是 32 位time_t构建由于小端序little-endianness它会把该指针解释为指向这个 64 位值的低 32 位的指针通过观察分解出的tm结构代表的是完整的 64 位时间还是被截断的低 32 位时间即可判定 OpenSSL 的time_t宽度。仓库中的实际实现位于 opensslshim.c// This value will represent a time in year 2038 if 64-bit time is used, // or 1901 if the lower 32 bits are interpreted as a 32-bit time_t value. time_t timeVal (time_t)0x80000000U; struct tm tmVal { 0 }; // Detect whether openssl is using 32-bit or 64-bit time_t. // If it uses 32-bit time_t, little-endianness means that the pointer // will be interpreted as a pointer to the lower 32 bits of timeVal. // tm_year is the number of years since 1900. if (!OPENSSL_gmtime(timeVal, tmVal) || (tmVal.tm_year ! 138 tmVal.tm_year ! 1)) { fprintf(stderr, Cannot determine the time_t size used by libssl\n); abort(); } g_libSslUses32BitTime (tmVal.tm_year 1);这里的哨兵值是0x80000000U即INT_MAX 1若 OpenSSL 使用 64 位time_t该值对应 2038 年tm_year 138即 1900 138若使用 32 位time_t低 32 位被解释为负的 32 位时间对应 1901 年tm_year 1。两种结果之外的任何情况都视为无法判定直接向 stderr 输出诊断信息并abort()——宁可启动失败也不在未知的 ABI 状态下带病运行。探测结果的传递与固化探测结果g_libSslUses32BitTime是全局布尔变量声明与符号加载逻辑分布在 shim 相关文件中变量定义在 opensslshim.cbool g_libSslUses32BitTime false;变量在 opensslshim.h 中以extern对外暴露OPENSSL_gmtime通过dlsym(libssl, OPENSSL_gmtime)动态加载见 opensslshim.c并经由 opensslshim.h 的#define OPENSSL_gmtime OPENSSL_gmtime_ptr宏间接调用这是 shim 层统一处理 OpenSSL 符号动态解析的常规模式。调用点一证书链验证时间X509_VERIFY_PARAM_set_time在设置证书链验证时间时代码先检查探测结果若 OpenSSL 使用 32 位time_t且 .NET 侧时间超出 32 位有符号整数范围则直接失败返回 0表示无法设置验证时间否则通过函数指针强转以 32 位参数签名的 ABI 调用X509_VERIFY_PARAM_set_time。实现在 openssl.c#if defined(FEATURE_DISTRO_AGNOSTIC_SSL) defined(TARGET_ARM) defined(TARGET_LINUX) !defined(TARGET_ANDROID) if (g_libSslUses32BitTime) { if (verifyTime INT_MAX || verifyTime INT_MIN) { return 0; } // Cast to a signature that takes a 32-bit value for the time. ((void (*)(X509_VERIFY_PARAM*, int32_t))(void*)(X509_VERIFY_PARAM_set_time))(verifyParams, (int32_t)verifyTime); return 1; } #endif X509_VERIFY_PARAM_set_time(verifyParams, verifyTime); return 1;这段代码体现了两个要点其一预编译宏FEATURE_DISTRO_AGNOSTIC_SSL发行版无关 SSL 特性即官方跨发行版构建配合架构/平台宏确保该分支只在需要发行版无关二进制且目标为 arm32 Linux 时生效其二函数指针强转的目的正是确保调用使用 OpenSSL 所期望的 32 位time_t的 ABI 约定。调用点二OCSP 响应过期检查X509_cmp_time在 OCSP 响应处理路径中.NET 需要比较当前时间与 OCSP 响应中的nextUpdate字段以判断响应是否过期。相关代码位于 pal_x509.c#if defined(FEATURE_DISTRO_AGNOSTIC_SSL) defined(TARGET_ARM) defined(TARGET_LINUX) !defined(TARGET_ANDROID) // If openssl uses 32-bit time_t and the current time doesnt fit in 32 bits, // skip checking the status/nextupd, and fall through to return PAL_X509_V_ERR_UNABLE_TO_GET_CRL. if (!g_libSslUses32BitTime || (currentTime INT_MIN currentTime INT_MAX)) #endif { // X509_cmp_current_time uses 0 for error already, so we can use it when theres a null value. // 1 means the nextupd value is in the future, -1 means it is now-or-in-the-past. nextUpdComparison nextupd NULL ? 0 : X509_cmp_time(nextupd, currentTime); // Un-revoking is rare, so reporting revoked on an expired response has a low chance // of a false-positive. if (status V_OCSP_CERTSTATUS_REVOKED) { ret PAL_X509_V_ERR_CERT_REVOKED; } else { if (nextupd ! NULL nextUpdComparison 0) { ret PAL_X509_V_ERR_CRL_HAS_EXPIRED; } else if (status V_OCSP_CERTSTATUS_GOOD) { ret PAL_X509_V_OK; } } }这里的处理策略是跳过而非报错当 OpenSSL 使用 32 位time_t且当前时间已超出 32 位范围时跳过X509_cmp_time的过期性比较让流程回退到PAL_X509_V_ERR_UNABLE_TO_GET_CRL无法获取 CRL这一可预测的失败结果避免把溢出后的错误时间交给 32 位 OpenSSL 做比较。当当前时间仍可被 32 位表示时则正常走X509_cmp_time路径。代码注释同样指出了对revoked状态宁可误报也不放过的保守考量因为撤销后再次撤销un-revoking极为罕见。兼容性总结能跑什么什么时候会失败完成上述改造后.NET 9 Linux arm32 的支持矩阵为组件要求/行为glibc至少 2.35Ubuntu 22.04 起构建时使用 64 位time_tOpenSSL64 位time_t构建完全正常工作无时间上限限制OpenSSL32 位time_t构建正常工作但一旦 .NET 侧处理的时间超出 32 位有符号范围会在证书链验证 / OCSP 过期检查等路径上可预测地失败32 位time_t的系统可运行但无法表示超过 32 位的时间换句话说只要底层 glibc 支持 64 位时间.NET 9 arm32 就能同时兼容 32 位与 64 位time_t的 OpenSSL直到遇到一个 32 位装不下的时间为止。而到那时运行时会给出确定的失败行为而不是在证书验证或 OCSP 检查中产生可能被利用的错误判定——这正是这份设计的核心价值所在。延伸阅读设计文档原文docs/design/features/y2038.md探测逻辑与全局状态src/native/libs/System.Security.Cryptography.Native/opensslshim.c、opensslshim.h证书链验证时间调用点src/native/libs/System.Security.Cryptography.Native/openssl.cOCSP 过期检查调用点src/native/libs/System.Security.Cryptography.Native/pal_x509.c官方构建镜像与 rootfs 机制docs/workflow/using-docker.md【免费下载链接】runtime.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.项目地址: https://gitcode.com/GitHub_Trending/runtime6/runtime创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表