提高网站的访问速度,怎么制作图片二维码,西安黄页网,设计海报网站在软件开发的世界里#xff0c;每一次技术的变革和尝试都伴随着未知的挑战。EdgeDB 团队在将部分网络 I/O 代码从 Python 迁移到 Rust 的过程中#xff0c;就遭遇了一场棘手的问题#xff0c;这个问题不仅暴露了 C 标准库的线程安全隐患#xff0c;也让我们对 Rust 的 “安…在软件开发的世界里每一次技术的变革和尝试都伴随着未知的挑战。EdgeDB 团队在将部分网络 I/O 代码从 Python 迁移到 Rust 的过程中就遭遇了一场棘手的问题这个问题不仅暴露了 C 标准库的线程安全隐患也让我们对 Rust 的 “安全特性” 有了新的认识。
EdgeDB 正在为产品开发一个新的 HTTP 获取功能选用reqwest作为 HTTP 客户端库。起初一切进展顺利。在本地开发环境中功能正常运行在 x86_64 架构的 CI持续集成运行器上各项测试也顺利通过看起来十分稳定。然而当测试在 ARM64 架构的 CI 运行器上进行时奇怪的事情发生了。
测试开始间歇性失败测试运行器启动后会无限期地挂起然后 CI 任务超时。从日志中看不到任何错误信息只显示某个测试一直在运行。几个小时后任务最终以超时错误结束。最初团队以为是死锁问题毕竟测试进程毫无响应的表现很符合死锁的特征。但深入调查后发现事情远没有这么简单。
为了找出问题所在团队成员决定直接连接到 ARM64 运行器一探究竟。由于 CI 机器运行在亚马逊 AWS 上这使得他们可以获得真实的、非容器化的 root 用户权限方便查看系统日志。一番查找后发现测试进程并非死锁而是直接崩溃了。只不过测试运行器没有检测到这一情况这也成为了后续需要解决的另一个问题。
通过journalctl命令团队找到了进程的核心转储文件将其加载到gdb调试工具中。但一开始由于缺少相关文件调试过程遇到了诸多错误。经过一番操作将相关库文件从容器中复制出来并配置好gdb后终于得到了有用的回溯信息。令人意外的是崩溃并非发生在新开发的 HTTP 代码中而是出现在getenv函数里。
getenv函数用于获取环境变量的值从回溯信息来看它在扫描感兴趣的环境变量时尝试从一个无效的内存位置加载数据从而导致崩溃。但这就引发了新的疑问为什么会出现这种情况呢毕竟环境变量看起来是完全有效的。
在深入调查过程中团队成员 Yury 提供了关键线索。他指出可能是文件 I/O 相关操作出错Python 试图根据errno构建异常这一过程调用了gettext进而触发了getenv。而getenv在多线程环境中并非安全函数这很可能就是问题的根源。
为了验证这一猜测团队开始检查环境块。environ是 POSIX 标准定义的一个char **类型的变量本质上是一个指向环境字符串的指针列表列表末尾用NULL指针标记。通过gdb查看环境块看起来并没有明显异常。但进一步分析发现getenv函数中用于遍历environ数组的指针x20其值与environ实际地址相差近 60MB。对比两个内存区域的指针值发现它们在SSL_CERT_FILE和SSL_CERT_DIR这两个环境变量处开始出现差异。这强烈暗示了存在竞态条件另一个线程在调用setenv时修改了environ。
setenv用于设置环境变量在多线程环境中调用它存在风险。当环境块的内存空间不足时setenv可能会调用realloc重新分配内存而此时如果另一个线程正在调用getenv就可能导致数据不一致引发崩溃。
那么是哪段代码在调用setenv呢经过一番谷歌搜索团队发现问题可能出在openssl-probe上。openssl-probe会设置SSL_CERT_FILE和SSL_CERT_DIR这两个环境变量而 EdgeDB 在 Linux 上使用的rust-native-tls的openssl后端会调用这些函数。查看openssl-probe库的代码可以发现它在设置环境变量时没有考虑到多线程环境下的安全性。
问题找到了那如何解决呢EdgeDB 团队最终决定在 Linux 上放弃使用reqwest的rust-native-tls/openssl后端转而采用rustls。虽然最初选择rust-native-tls是为了避免在将 Python 代码移植到 Rust 时同时引入两个 TLS 引擎但考虑到当前的线程安全问题短期内使用两个引擎也成为了无奈之举。
此外还有另一种解决思路即在调用try_init_ssl_cert_env_vars时持有 Python 的全局解释器锁GIL。Rust 本身有内部锁来防止 Rust 代码在读写环境变量时出现竞态条件但无法阻止其他语言的代码直接使用libc。持有 GIL 可以避免与 Python 线程产生竞争。
值得一提的是Rust 项目已经意识到了这一问题并计划在 2024 版中将环境设置函数标记为不安全。而 glibc 项目也在近期对getenv函数进行了改进通过避免realloc和泄漏旧环境增加了其线程安全性。
这次事件为开发者们敲响了警钟。在多线程编程中即使使用了像 Rust 这样强调安全性的语言也不能忽视底层 C 标准库带来的风险。C 标准库中的一些函数如setenv和getenv在多线程环境下的不安全性可能会引发难以排查的问题。
对于 Rust 开发者来说虽然 Rust 提供了强大的安全机制但在与其他语言交互或使用底层库时仍需谨慎对待。特别是在涉及到多线程操作时要充分考虑不同语言和库之间的兼容性和线程安全性。
在实际开发中我们往往会依赖各种库来实现功能但这些库可能隐藏着潜在的风险。就像这次 EdgeDB 遇到的问题openssl-probe看似无害的代码却在多线程环境下引发了严重的崩溃。因此在选择和使用库时开发者需要深入了解其内部实现评估潜在风险尤其是在关键业务场景中更要确保代码的稳定性和安全性。
同时这也反映出跨语言开发的复杂性。不同语言有不同的特性和规范在混合使用时需要特别注意边界情况和交互细节。在 EdgeDB 的案例中Rust 代码与 Python 代码以及底层 C 标准库之间的交互出现了问题导致了崩溃。这提醒我们在跨语言开发项目中要建立完善的测试机制覆盖各种可能的情况及时发现并解决潜在问题。
从更广泛的角度看这次事件也为整个软件开发社区提供了宝贵的经验教训。无论是语言开发者还是库开发者都应该更加重视多线程环境下的安全性问题。语言标准的制定者可以考虑进一步完善标准库的设计提供更安全的接口库开发者在编写代码时要充分考虑多线程场景确保库的线程安全性减少类似问题的发生。
在软件开发的道路上每一次遇到的问题都是一次成长的机会。EdgeDB 团队通过这次经历不仅解决了当前的技术难题也为未来的开发积累了宝贵的经验。希望其他开发者能够从这个案例中汲取教训在开发过程中更加注重细节避免陷入类似的困境。你在开发中遇到过哪些因库的不安全性导致的问题呢欢迎在评论区分享你的经验和看法。 科技脉搏每日跳动。
与敖行客 Allthinker一起创造属于开发者的多彩世界。 - 智慧链接 思想协作 -