Back to latest
OpenAI2026/6/30AI 中文译文 · 待人工复核

核心转储流行病学:修复一个 18 年的老 bug

英文标题:Core dump epidemiology: fixing an 18-year-old bug

OpenAI 工程师通过群体分析方法,在 Rockset 数据基础设施中识别并修复了两个看似相关的崩溃 bug:一个由硬件损坏引起,另一个是 GNU libunwind 中隐藏了 18 年的竞态条件。

跳到主要内容

登录试用 ChatGPT(在新窗口打开)

核心转储流行病学:修复一个 18 年的老 bug | OpenAI

2026 年 6 月 30 日

工程

核心转储流行病学:修复一个 18 年的老 bug

用群体分析来调试数据基础设施中棘手的崩溃问题。

听文章21:48

分享

第一次调试尝试:仔细检查几个核心转储

作者:Nathan Bronson,技术团队成员

OpenAI 的模型和智能体越来越依赖可扩展的数据基础设施,以便在推理时(也就是模型思考你的问题的时候)搜索相关数据。其中一些服务是用 C++ 编写的,这种语言对系统有底层控制能力,让我们能最大化性能并最小化内存使用。随着规模扩大,这些效率优势很重要,但 C++ 缺乏内存安全性,意味着 bug 可能会通过写入错误或不存在的内存地址导致崩溃。

几个月前,我们在 Rockset 服务中观察到一些崩溃。Rockset 是我们 ChatGPT 数据基础设施中一个定制化的部分,对许多数据插件和搜索对话历史至关重要。在每次崩溃中,一个正常的 C++ 函数似乎执行完毕,然后返回到了一个错误的地址,导致内核停止程序,因为指令指针不再指向代码。有时,栈帧中的返回地址槽是 NULL。有时,栈指针 CPU 寄存器本身似乎偏移了 8 个字节,就好像 %rsp 在正常执行过程中被莫名其妙地减少了。在这两种情况下,崩溃都发生在返回时。

这些不是应用程序代码的正常故障模式。一个只命中保存的返回地址的随机写入是可能的,但极不可能。一个不涉及内联汇编、setcontextlongjmp(我们都没用)的 bug 导致 %rsp 偏移 8 个字节,这就更奇怪了,因为编译后的代码只在函数序言和尾声里直接调整这个寄存器。我们(或 ChatGPT)能想到的每一个假设都有强有力的证据反对它,所以这个 bug 看起来不可能。

我们最初以为是一个问题,结果发现是两个不相关的 bug,碰巧同时被发现。首先,一个 Azure 主机上的静默硬件损坏,CPU 就是不能正确做数学运算。其次,GNU libunwind 中一个 18 年的竞态条件,这是一个广泛使用的开源库中未被注意到的 bug。

这篇文章讲述了我们如何通过像流行病学家一样思考,并构建一个关于所有崩溃的高质量数据集,来识别和修复看似无法解释的崩溃。

第一次调试尝试:仔细检查几个核心转储

首先,让我们深入了解 Rockset。它是一个用于搜索和实时分析的云原生数据系统,我们在 OpenAI 内部用于许多场景,比如同步连接器(Rockset 在 2024 年被 OpenAI 收购)。流式更新用于维护工作区知识库的最新索引,这样 ChatGPT 在回答问题或执行操作时可以搜索相关信息。

Rockset 的执行层是用 C++ 编写的。C++ 语言提供对 CPU 的底层访问,这对性能和效率有好处,但这意味着应用程序 bug 可能导致无效内存访问和段错误。为了帮助追踪这些问题,我们使用 folly 的致命信号处理程序在崩溃发生时记录堆栈跟踪,并将相应的核心转储(程序崩溃时状态的快照)上传到 Azure blob 存储以供后续分析。Rockset 的所有查询处理副本都是复制的,这最小化了崩溃对客户端的影响。然而,每个段错误都对应一个需要修复的 bug,以达到我们的可靠性和质量目标。

我们最初的方法是把这些核心转储当作传统的调试问题来处理:非常仔细地检查几个核心转储,形成假设,然后逐一排除。

大多数崩溃发生在一个叫 DocumentTree::updateDocument 的方法中。在这些崩溃中,看起来 updateDocument 调用了一个未知函数 X,堆栈在 X 活动时被损坏,然后 X 返回到了一个不是可执行代码的地址。在某些情况下,X 刚被弹出的帧看起来是有效的,只是它保存的返回地址是 NULL。在其他情况下,栈指针本身看起来不对,但下一个有效的帧似乎仍然是 updateDocument

我们不知道堆栈是什么时候被损坏的,这留下了巨大的搜索空间。updateDocument 是一个很大的方法,经历了大量的内联,所以 X 的候选者数量多得吓人。

这是我们的 C++ 代码中的 bug 吗?编译器或链接问题?运行时库的问题?关于信号传递或上下文切换的 Linux 内核 bug?还是更罕见的问题?如果这是一个随机写入,为什么我们的 ASAN 测试环境没有捕获到?

我们试图用应用程序级别的日志来识别所有出现的问题,但堆栈损坏的 bug 很难仅从日志中分类,因为记录的堆栈跟踪本身就被损坏或丢失了。我们无法构建一个既没有误报也没有漏报的日志查询。我们手动检查了更多的核心转储,找到了一些额外的例子,但这个过程太费力了,无法给我们一个可信的数据集。

在调查的这个阶段,我们(错误地)排除了硬件 bug,因为我们在多个区域和多种硬件类型上都看到了崩溃,所以我们仍然在寻找纯软件原因。有几天,我们深入研究了单个 %rsp 偏移的崩溃,使用堆栈和寄存器内容重建崩溃前的历史。这产生了一些可能的线索,但由于我们没有放弃最初的所有 bug 都有相同原因的结论,这并没有让我们摆脱困境。

堆栈中的线索

在进入我们调查的转折点之前,有必要解释一下我们从核心文件中提取了什么样的信息。

Rockset 是用 -fno-omit-frame-pointer 编译的,所以活动的栈帧总是可以通过 %rbp 访问,调用者形成一个帧指针的链表。

在 Linux x86_64 上,AMD64 System V ABI 还在 %rsp 下方保留了 128 个字节作为红区。这个区域可供用户空间代码使用,重要的是,作为 ABI 契约的一部分,内核承诺在传递信号时不会覆盖它。

红区对我们调试返回后崩溃至关重要,因为它保存了返回前的一些信息。当触发 SIGSEGV 时,folly 的致命信号处理程序在崩溃线程的堆栈上运行。不再活动的栈帧(因为它们的函数已经返回)会被信号处理程序覆盖,除了最后 128 个字节。这就是为什么我们可以说“X 刚被弹出的栈帧看起来是有效的,只是返回地址是 NULL”。红区保存了一些不活动的帧,或者有时只是一个不活动帧的尾部。

堆栈图显示损坏的栈帧可以覆盖返回地址并导致崩溃。

我们发现了一个堆栈偏移的崩溃,其中涉及的所有函数都非常小。这让我们看到,%rsp 在一个相对简单的函数执行过程中变得偏移了,并且之后更多的调用成功了。程序只有在活动函数最终试图返回时才崩溃。这些代码路径都没有使用异常、内联汇编、setcontextlongjmp,所以如果栈指针真的像核心转储显示的那样改变了,用户空间代码中没有合理的 bug 能解释这个问题。

这把我们推向了内核。

Rockset 比大多数程序更积极地使用信号。查询执行被分解成许多交换数据的轻量级任务。这对于高效处理高 QPS 工作负载很重要,但它使得每个查询的 CPU 记账变得棘手,因为许多查询的工作被多路复用到同一个线程池。

我们的解决方案是我们称之为 coarse_thread_cputime_clock 的东西,它足够廉价地近似 clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...),以便在每个任务边界进行采样。timer_create API 可用于基于几种时间流逝的概念(包括 CPU 时间的累积)来调度周期性信号传递。我们调度一个信号(SIGUSR2)每几毫秒 CPU 时间传递一次,此时信号处理程序更新一个线程局部值。即使许多任务在执行时没有看到粗略时钟前进,但将所有增量相加会产生查询实际 CPU 时间的无偏估计。

因为我们如此频繁地传递信号,一个关于上下文切换或信号传递的罕见内核 bug 似乎很合理。我们花时间阅读 bug 报告、内核源代码和 Azure 特定的内核补丁。我们尝试了压力测试。我们没能找到任何看起来相关的东西。

那时我们决定退一步,尝试不同的方法。

医生还是流行病学家?

调试这样的问题有两种广泛的方法。

一种是像医生一样:专注于一个病人,做很多测试,并尝试从详细的证据中诊断单个病例。

另一种是更像流行病学家:观察整个群体,并询问是否存在单个病例无法揭示的模式。这个 bug 是在特定版本开始的吗?它是否与一个硬件 SKU(特定的 CPU 和服务器型号)、一个区域或一个内核版本相关?在看起来像一个综合征的内部是否隐藏着多个不同的集群?

我们之前主要是医生模式。关键的转变是决定我们需要收集高质量的群体数据。

清理数据

我们之前自动找到所有问题实例的尝试失败了,因为我们试图使用日志的文本搜索。核心转储本身有更多信息,但手动查看它们无法扩展。我们决定投入精力构建一个可以自动分析核心转储的管道。

我们让 ChatGPT 写了一个脚本,下载每个核心文件的前缀,提取寄存器,使用日志过滤已知的误报,并自动将崩溃标记为返回空、堆栈偏移或其他。然后我们在过去一年中所有的生产 Rockset 核心转储上并行运行这个脚本。

这是转折点。

一旦我们有了干净的数据集,相关性立即显现。我们之前当作一个奇怪 bug 处理的东西,实际上是_两个_独立的崩溃群体。

返回空的核心转储分布在许多集群和地理区域。它们的频率最近增加了,但没有明确的开始日期和清晰的基础设施边界。

堆栈偏移的崩溃看起来完全不同。它们都来自一个区域,有明确的开始日期,并且从未在运行了很长时间的节点上发生。尽管它们涉及多个 Azure VM(云中托管的虚拟机),但模式看起来像是一台物理机器有坏硬件,导致落在它上面的任何 VM 出现问题。

按集群随时间变化的崩溃率点图,显示大多数崩溃集中在集群 2、3 和 6,在周期结束时集群 1 有一个峰值。

那一刻我们意识到,我们一直在心理上混淆了两个 bug。因为我们一直在混合来自两个 bug 的反例,我们无法找到一个单一的连贯解释。

Bug #1:坏主机

有了干净的 Kubernetes 节点和时间戳列表,我们能够将堆栈偏移的崩溃追溯到单个物理主机,这很容易加入黑名单。

我们无法在该主机上的受控环境中重现寄存器损坏,即使在几周的压力测试之后也是如此。然而,一旦有问题的主机被停止服务,堆栈偏移的崩溃就消失了。

移除坏主机不是一个永久性的解决方案,因为它不能防止同样问题的新发生。然而,我们可以更改软件,以便如果类似问题再次发生,可以轻松检测和处理。我们改进了致命信号处理程序,包含寄存器状态,这样我们只能从日志中检测复发(不需要核心转储)。我们更改了控制平面,以便 VM 通常被重用而不是回收,这使得在我们的基础设施堆栈层面更容易检测坏节点。我们还更新了我们的运行手册(和我们团队的思维模型),以包括这种可能性。

随着坏主机崩溃被分离出来,剩下的返回空核心转储变得更容易推理。之前我们排除了异常展开,因为我们以为有反例:在肯定不使用异常的代码路径中的崩溃。但那些反例都来自硬件损坏集群。

一旦我们带着这个想法重新审视剩下的核心转储,我们发现这个结论完全反了:崩溃都发生在异常展开期间。

异常处理是一种动态控制转移

当 C++ 抛出异常时,运行时必须发现哪个 catch 块应该接收它,以及哪些析构函数或清理处理程序应该沿途运行。编译器发出这些元数据,但实际的匹配在运行时动态发生。

异常展开实际上不是由调用 throw 的函数执行的,而是由生成的编译代码调用的辅助函数执行的。这些运行时例程检查堆栈,获取堆栈上函数的元数据,动态查找清理处理程序和 catch 块,然后将控制转移到其中一个位置。转移控制包括展开所有中间的栈帧(包括辅助函数的栈帧)。

操作上,这更接近于 longjmp 或协程切换,而不是普通的调用和返回。被调用者保存的寄存器必须恢复,以及栈帧寄存器 %rbp%rsp

我们的二进制文件链接了两个包含执行 C++ 异常展开的函数的库:libgcc 和 GNU libunwind。动态链接器选择了 GNU libunwind 的定义。这让我们很惊讶;我们原本期望 libgcc 的实现会因为符号版本控制规则而胜出;然而,检查正在运行的二进制文件显示情况并非如此。

推翻最后一个假设

此时,我们的工作假设发生了变化,因为我们放松了另一个假设,这个假设是在我们认为只有一个 bug 时做出的。

也许我们看到的不是普通函数返回 NULL。也许我们看到的是一个展开转移——实际上是一个 setcontext 风格的寄存器恢复——其中目标指令指针在控制转移之前变成了 NULL。换句话说,是展开库提供了错误的数据,而不是堆栈上错误的返回地址槽。

这极大地缩小了问题范围。要么是 GNU libunwind 计算了错误的目标状态,要么是它计算了正确的状态,但在应用之前被某些东西损坏了。

我们阅读了 GNU libunwind 的源代码,发现它在堆栈上合成一个 ucontext_t,填充清理处理程序帧所需的寄存器状态,然后将指向该结构的指针交给一个内部汇编例程:_Ux86_64_setcontext

此时我们有了所有的碎片。

合成的 ucontext_t 位于 _Ux86_64_setcontext 执行期间被展开的栈帧之一中。_Ux86_64_setcontext 在改变 %rsp 之后是否从该结构读取数据?此时该结构不再属于活动堆栈?那会使它容易受到信号传递的覆盖,比如我们频繁的 SIGUSR2

Bug #2:libunwind 的 bug

答案是肯定的。

以下是我们使用的 GNU libunwind 版本中 _Ux86_64_setcontext 的最后六条指令,主要由从内存加载到目标寄存器的 mov 指令组成:

纯文本

`1

74: mov UC_MCONTEXT_GREGS_RSP(%rdi),%rsp

2

75:

3

76: /* 将返回地址压入堆栈 */

4

77: mov UC_MCONTEXT_GREGS_RIP(%rdi),%rcx

5

78: push %rcx

6

79:

7

80: mov UC_MCONTEXT_GREGS_RCX(%rdi),%rcx

8

81: mov UC_MCONTEXT_GREGS_RDI(%rdi),%rdi

9

82: retq

`

%rdi 指向堆栈分配的 ucontext_tUC_MCONTEXT_* 宏只是展开为存储特定寄存器的固定偏移量。)

第一条指令是竞态窗口的开始。它更新 %rsp 以指向活动堆栈的新底部。一旦发生这种情况,由 %rdi 指向的结构就不再是活动堆栈(或红区)的一部分,并且不再对内核不可访问。

通常这不会引起问题,但如果信号恰好在正确(或错误?)的时刻到达,内核将在 %rsp-128 处构建信号帧。这可能会覆盖 %rdi 指向的内存。

如果这发生在下一条指令读取 UC_MCONTEXT_GREGS_RIP(%rdi) 之前,那么恢复的指令指针可能会被损坏。在我们的崩溃中,它变成了 NULL。

这就是 bug。

为什么核心转储伪装成普通的错误返回

这个汇编也解释了一个让我们困惑的观察:为什么函数 X 在前一个栈帧的返回地址槽中有一个 NULL。

setcontext 被编写为恢复所有寄存器,包括 %rdi,所以它不能在控制转移的最后时刻使用该寄存器来读取 UC_MCONTEXT_GREGS_RIP(%rdi)。相反,它更早地读取该值,将其保存到堆栈,恢复更多寄存器,然后使用 retq 读取保存的值并转移控制。

在核心转储中看起来像“一个函数返回到了 NULL”的东西,实际上是“展开器在堆栈上合成了一个目标返回地址,但该目标在转移完成之前被损坏了。”我们假设返回地址槽的损坏一定发生在原地,因为我们不知道有任何地方(可损坏的)数据被故意写入返回地址槽。

一个单指令的竞态窗口

使这个 bug 看起来荒谬的是这个竞态窗口有多窄。在这种竞态条件中,外部事件(信号)需要在另一个线程采取的两个步骤之间发生。这些步骤彼此越接近,竞态条件发生的可能性就越小。

在这种情况下,脆弱窗口实际上只有一个指令宽!信号必须在 %rsp 被改变之后,但在下一条指令加载 %rip 之前传递。在现代超标量乱序 CPU 上,几个这样简单的指令可以在一个周期内运行,所以竞态窗口大约是一百皮秒。

当我们发现这个竞态时,我们的第一反应是它一定太罕见了,无法解释观察到的崩溃率。我们在整个集群中每天看到超过十几个返回空崩溃。一个在异常清理期间的单指令竞态真的能解释这个吗?

我们转向费米估算。如果脆弱窗口大约是 10^{-10} 秒,而 SIGUSR2 每 10^{-2} 秒 CPU 时间到达一次,那么每个异常清理处理程序或 catch 块有大约 10^{-8} 的概率输掉竞态。

Rockset 使用异常作为其内部摄取背压机制的一部分。单个过载的主机每秒可以抛出大约 10^4 个异常。这意味着使用背压的主机的平均故障间隔时间是 10^4 秒,或者每几小时一次崩溃。在集群规模上,这足以解释观察到的崩溃频率。

为什么 libunwind 的 bug 现在才出现?

GNU libunwind 的 bug 很老——超过 18 年,存在于第一个支持 C++ 异常展开的 x86_64 版本中。

那么为什么它现在才出现?

崩溃率大致与抛出多少异常和传递多少信号成正比。它也取决于信号处理程序消耗多少堆栈。

Rockset 在所有三个轴上都不同寻常。我们作为正常过载控制的一部分以高速率抛出异常;由于 coarse_thread_cputime_clock,我们异常频繁地传递 SIGUSR2;今年早些时候,我们通过添加对 timer_getoverrun 的调用使 SIGUSR2 处理程序使用了更多堆栈,以便我们可以处理合并的信号。

最后一个变化似乎很重要。如果处理程序使用足够少的堆栈,它可能不会到达并覆盖过时的 ucontext_t 内存。在那个变化之前,我们根本没有观察到这些崩溃。变化之后,速率保持较低,直到我们为一些强调背压机制的用例增加了负载。

换句话说,libunwind 的 bug 一直存在,但我们的异常率、信号率和处理程序堆栈使用量的乘积直到最近才越过阈值,变得在操作上可见。

这个机制也解释了为什么硬件 bug 和 libunwind bug 都主要导致 DocumentTree::updateDocument 崩溃。libunwind 导致的崩溃特别集中在这个方法上,因为当我们抛出异常来施加数据摄入反压(ingest backpressure,即控制数据流入速度的机制)时,这个方法总是处于活跃状态。它也是 %rsp 未对齐崩溃的高发区,因为出问题的硬件节点属于我们用于批量数据摄入的 SKU(库存单位,这里指硬件型号),而这类节点的大部分 CPU 时间都花在这个方法里。

我们立即采取的缓解措施是把 GNU libunwind 换成 libgcc 的 unwinder(解卷器,用于在程序崩溃时回溯函数调用栈的工具)。这个替换本身就很值得:libgcc 的实现经过大量优化,减少了锁竞争(lock contention,多个线程争抢同一把锁导致的性能问题),这在扩展到大型虚拟机时非常重要。

我们还向上游的 GNU libunwind 提交了一个可独立运行的复现程序和一个修复⁠(在新窗口中打开),并验证了其他解卷器没有类似问题。

群体诊断的力量

这次调试之旅让我们学到了很多关于动态链接、DWARF 解卷元数据(DWARF unwind metadata,一种用于描述程序运行时栈帧信息的标准格式)、Linux 信号传递、System V ABI(应用程序二进制接口,定义了程序如何与操作系统交互)以及 C++ 异常机制的细节。但最重要的教训比这些都简单。

最关键的一步不是聪明的汇编代码阅读或对细节的深入了解,而是构建一个高质量的数据集。没有这个数据集,我们就把两种不同的现象混为一谈,试图在混乱中理出头绪。一旦我们有了准确、完整的群体数据,问题的结构就变得一目了然:一类崩溃属于有问题的硬件主机,另一类属于 libunwind 中的竞态条件(race condition,多个线程或进程因执行顺序不确定而导致的错误)。数据质量提高了,调试就变得容易了。

对于像 Rockset 这样的基础设施系统来说,这一点非常重要。这次调查进一步坚定了我们对深度监控、自动化调查和持续改进运维工具的承诺。可靠性不仅仅是事后修复 bug,更是要构建数据、工作流程和技能,把看似不可能解决的问题变成可诊断、可解决的问题。

作者

Nathan Bronson,技术团队成员

继续阅读

查看全部

Tax Agent > Art Card

用 Codex 构建能自我改进的税务代理工程2026年5月27日

codex windows > art card

构建安全有效的沙箱,让 Codex 在 Windows 上运行工程2026年5月13日

MRC 1_1

用超级计算机网络加速大规模 AI 训练工程2026年5月5日

研究

最新进展

安全

产品

API 平台

业务

开发者

公司

支持

更多

条款与政策

(在新窗口中打开)(在新窗口中打开)(在新窗口中打开)(在新窗口中打开)(在新窗口中打开)(在新窗口中打开)(在新窗口中打开)

OpenAI © 2015–2026您的隐私选择

英语美国

阅读英文原文

Markdown / MDX 原稿

包含 frontmatter、中文正文和英文来源链接,可直接复制或保存为 .md 分发。

查看原稿下载 .md
在页面中展开原始 Markdown
---
title: "核心转储流行病学:修复一个 18 年的老 bug"
title_en: "Core dump epidemiology: fixing an 18-year-old bug"
source: "OpenAI"
source_url: "https://openai.com/index/core-dump-epidemiology-data-infrastructure-bug"
published_at: "2026-06-30T00:00:00.000Z"
language: "zh-CN"
topics: ["openai"]
tags: ["OpenAI","Engineering"]
review_status: "unreviewed"
---

# 核心转储流行病学:修复一个 18 年的老 bug

> OpenAI 工程师通过群体分析方法,在 Rockset 数据基础设施中识别并修复了两个看似相关的崩溃 bug:一个由硬件损坏引起,另一个是 GNU libunwind 中隐藏了 18 年的竞态条件。

## 内容摘要

OpenAI 的 ChatGPT 数据基础设施组件 Rockset 出现频繁崩溃,表现为函数返回时跳转到错误地址。工程师最初试图通过仔细检查几个核心转储来调试,但未能找到原因。后来他们转变思路,像流行病学家一样收集并分析所有崩溃数据,发现实际上存在两个独立的 bug:一个是由 Azure 主机硬件损坏导致的栈指针偏移,另一个是 GNU libunwind 中一个存在 18 年的竞态条件,该竞态条件

## 为什么值得关注

这个案例展示了在复杂系统中,看似相同的故障现象可能由完全不同的原因引起。通过群体数据分析而非逐个排查,可以快速分离问题根源。同时,它揭示了底层库中隐藏多年的 bug 如何在高负载、高信号频率的特定条件下被触发,对构建高可靠性基础设施具有重要启示。

## 核心要点

- OpenAI 在 Rockset 服务中观察到两种崩溃:返回地址为空和栈指针偏移。
- 最初将所有崩溃视为同一问题,导致调试陷入僵局。
- 通过自动分析所有核心转储,发现崩溃分为两类:硬件损坏和 libunwind 竞态条件。
- 硬件 bug 由单个 Azure 主机 CPU 损坏引起,隔离后消失。
- libunwind 的竞态条件存在于 18 年,在异常展开时信号覆盖了栈上的上下文结构。
- 修复方法是用 libgcc 的 unwinder 替换 libunwind,并向上游提交了补丁。

[跳到主要内容](#main)

* [研究](https://openai.com/research/index/)
* 产品
* [企业](https://openai.com/business/)
* [开发者](https://openai.com/api/)
* [公司](https://openai.com/about/)
* [基金会(在新窗口打开)](https://openaifoundation.org/)

登录[试用 ChatGPT(在新窗口打开)](https://chatgpt.com/?openaicom-did=6b4e5ba4-f156-469a-86e7-c924dccbee69&openaicom%5Freferred=true)

* 研究
* 产品
* 企业
* 开发者
* 公司
* [基金会(在新窗口打开)](https://openaifoundation.org/)

核心转储流行病学:修复一个 18 年的老 bug | OpenAI

2026 年 6 月 30 日

[工程](https://openai.com/news/engineering/)

# 核心转储流行病学:修复一个 18 年的老 bug

用群体分析来调试数据基础设施中棘手的崩溃问题。

听文章21:48

分享

第一次调试尝试:仔细检查几个核心转储

* [第一次调试尝试:仔细检查几个核心转储](#first-debugging-attempt-carefully-examining-a-few-core-dumps)
* [堆栈中的线索](#clues-from-the-stack)
* [医生还是流行病学家?](#doctor-or-epidemiologist)
* [清理数据](#cleaning-the-data)
* [Bug #1:坏主机](#bug-1-the-bad-host)
* [异常处理是一种动态控制转移](#exception-handling-is-a-dynamic-control-transfer)
* [推翻最后一个假设](#undoing-one-last-assumption)
* [Bug #2:libunwind 的 bug](#bug-2-the-libunwind-bug)
* [为什么核心转储伪装成普通的错误返回](#why-the-cores-masked-as-ordinary-bad-returns)
* [一个单指令的竞态窗口](#a-single-instruction-race-window)
* [为什么 libunwind 的 bug 现在才出现?](#why-did-the-libunwind-bug-appear-now)
* [群体诊断的力量](#the-power-of-a-population-level-diagnosis)

* [第一次调试尝试:仔细检查几个核心转储](#first-debugging-attempt-carefully-examining-a-few-core-dumps)
* [堆栈中的线索](#clues-from-the-stack)
* [医生还是流行病学家?](#doctor-or-epidemiologist)
* [清理数据](#cleaning-the-data)
* [Bug #1:坏主机](#bug-1-the-bad-host)
* [异常处理是一种动态控制转移](#exception-handling-is-a-dynamic-control-transfer)
* [推翻最后一个假设](#undoing-one-last-assumption)
* [Bug #2:libunwind 的 bug](#bug-2-the-libunwind-bug)
* [为什么核心转储伪装成普通的错误返回](#why-the-cores-masked-as-ordinary-bad-returns)
* [一个单指令的竞态窗口](#a-single-instruction-race-window)
* [为什么 libunwind 的 bug 现在才出现?](#why-did-the-libunwind-bug-appear-now)
* [群体诊断的力量](#the-power-of-a-population-level-diagnosis)

_作者:Nathan Bronson,技术团队成员_

OpenAI 的模型和智能体越来越依赖可扩展的数据基础设施,以便在推理时(也就是模型思考你的问题的时候)搜索相关数据。其中一些服务是用 C++ 编写的,这种语言对系统有底层控制能力,让我们能最大化性能并最小化内存使用。随着规模扩大,这些效率优势很重要,但 C++ 缺乏内存安全性,意味着 bug 可能会通过写入错误或不存在的内存地址导致崩溃。

几个月前,我们在 Rockset 服务中观察到一些崩溃。Rockset 是我们 ChatGPT 数据基础设施中一个定制化的部分,对许多数据插件和搜索对话历史至关重要。在每次崩溃中,一个正常的 C++ 函数似乎执行完毕,然后返回到了一个错误的地址,导致内核停止程序,因为指令指针不再指向代码。有时,栈帧中的返回地址槽是 NULL。有时,栈指针 CPU 寄存器本身似乎偏移了 8 个字节,就好像 `%rsp` 在正常执行过程中被莫名其妙地减少了。在这两种情况下,崩溃都发生在返回时。

这些不是应用程序代码的正常故障模式。一个只命中保存的返回地址的随机写入是可能的,但极不可能。一个不涉及内联汇编、`setcontext` 或 `longjmp`(我们都没用)的 bug 导致 `%rsp` 偏移 8 个字节,这就更奇怪了,因为编译后的代码只在函数序言和尾声里直接调整这个寄存器。我们(或 ChatGPT)能想到的每一个假设都有强有力的证据反对它,所以这个 bug 看起来不可能。

我们最初以为是一个问题,结果发现是两个不相关的 bug,碰巧同时被发现。首先,一个 Azure 主机上的静默硬件损坏,CPU 就是不能正确做数学运算。其次,GNU libunwind 中一个 18 年的竞态条件,这是一个广泛使用的开源库中未被注意到的 bug。

这篇文章讲述了我们如何通过像流行病学家一样思考,并构建一个关于所有崩溃的高质量数据集,来识别和修复看似无法解释的崩溃。

## 第一次调试尝试:仔细检查几个核心转储

首先,让我们深入了解 Rockset。它是一个用于搜索和实时分析的云原生数据系统,我们在 OpenAI 内部用于许多场景,比如同步连接器(Rockset 在 2024 年被 OpenAI 收购)。流式更新用于维护工作区知识库的最新索引,这样 ChatGPT 在回答问题或执行操作时可以搜索相关信息。

Rockset 的执行层是用 C++ 编写的。C++ 语言提供对 CPU 的底层访问,这对性能和效率有好处,但这意味着应用程序 bug 可能导致无效内存访问和段错误。为了帮助追踪这些问题,我们使用 folly 的致命信号处理程序在崩溃发生时记录堆栈跟踪,并将相应的核心转储(程序崩溃时状态的快照)上传到 Azure blob 存储以供后续分析。Rockset 的所有查询处理副本都是复制的,这最小化了崩溃对客户端的影响。然而,每个段错误都对应一个需要修复的 bug,以达到我们的可靠性和质量目标。

我们最初的方法是把这些核心转储当作传统的调试问题来处理:非常仔细地检查几个核心转储,形成假设,然后逐一排除。

大多数崩溃发生在一个叫 `DocumentTree::updateDocument` 的方法中。在这些崩溃中,看起来 `updateDocument` 调用了一个未知函数 X,堆栈在 X 活动时被损坏,然后 X 返回到了一个不是可执行代码的地址。在某些情况下,X 刚被弹出的帧看起来是有效的,只是它保存的返回地址是 NULL。在其他情况下,栈指针本身看起来不对,但下一个有效的帧似乎仍然是 `updateDocument`。

我们不知道堆栈是什么时候被损坏的,这留下了巨大的搜索空间。`updateDocument` 是一个很大的方法,经历了大量的内联,所以 X 的候选者数量多得吓人。

这是我们的 C++ 代码中的 bug 吗?编译器或链接问题?运行时库的问题?关于信号传递或上下文切换的 Linux 内核 bug?还是更罕见的问题?如果这是一个随机写入,为什么我们的 ASAN 测试环境没有捕获到?

我们试图用应用程序级别的日志来识别所有出现的问题,但堆栈损坏的 bug 很难仅从日志中分类,因为记录的堆栈跟踪本身就被损坏或丢失了。我们无法构建一个既没有误报也没有漏报的日志查询。我们手动检查了更多的核心转储,找到了一些额外的例子,但这个过程太费力了,无法给我们一个可信的数据集。

在调查的这个阶段,我们(错误地)排除了硬件 bug,因为我们在多个区域和多种硬件类型上都看到了崩溃,所以我们仍然在寻找纯软件原因。有几天,我们深入研究了单个 `%rsp` 偏移的崩溃,使用堆栈和寄存器内容重建崩溃前的历史。这产生了一些可能的线索,但由于我们没有放弃最初的所有 bug 都有相同原因的结论,这并没有让我们摆脱困境。

## 堆栈中的线索

在进入我们调查的转折点之前,有必要解释一下我们从核心文件中提取了什么样的信息。

Rockset 是用 `-fno-omit-frame-pointer` 编译的,所以活动的栈帧总是可以通过 `%rbp` 访问,调用者形成一个帧指针的链表。

在 Linux `x86_64` 上,AMD64 System V ABI 还在 `%rsp` 下方保留了 128 个字节作为红区。这个区域可供用户空间代码使用,重要的是,作为 ABI 契约的一部分,内核承诺在传递信号时不会覆盖它。

红区对我们调试返回后崩溃至关重要,因为它保存了返回前的一些信息。当触发 `SIGSEGV` 时,folly 的致命信号处理程序在崩溃线程的堆栈上运行。不再活动的栈帧(因为它们的函数已经返回)会被信号处理程序覆盖,除了最后 128 个字节。这就是为什么我们可以说“X 刚被弹出的栈帧看起来是有效的,只是返回地址是 NULL”。红区保存了一些不活动的帧,或者有时只是一个不活动帧的尾部。

![堆栈图显示损坏的栈帧可以覆盖返回地址并导致崩溃。](https://images.ctfassets.net/kftzwdyauwt9/3eBjjDfMl3Ja4JE1V8skqx/99caec095d4669342d2a013f0b6befda/Corrupted-stack_crashes_light_desktop.svg?w=3840&q=90)

我们发现了一个堆栈偏移的崩溃,其中涉及的所有函数都非常小。这让我们看到,`%rsp` 在一个相对简单的函数执行过程中变得偏移了,并且之后更多的调用成功了。程序只有在活动函数最终试图返回时才崩溃。这些代码路径都没有使用异常、内联汇编、`setcontext` 或 `longjmp`,所以如果栈指针真的像核心转储显示的那样改变了,用户空间代码中没有合理的 bug 能解释这个问题。

这把我们推向了内核。

Rockset 比大多数程序更积极地使用信号。查询执行被分解成许多交换数据的轻量级任务。这对于高效处理高 QPS 工作负载很重要,但它使得每个查询的 CPU 记账变得棘手,因为许多查询的工作被多路复用到同一个线程池。

我们的解决方案是我们称之为 `coarse_thread_cputime_clock` 的东西,它足够廉价地近似 `clock_gettime(CLOCK_THREAD_CPUTIME_ID, ...)`,以便在每个任务边界进行采样。`timer_create` API 可用于基于几种时间流逝的概念(包括 CPU 时间的累积)来调度周期性信号传递。我们调度一个信号(SIGUSR2)每几毫秒 CPU 时间传递一次,此时信号处理程序更新一个线程局部值。即使许多任务在执行时没有看到粗略时钟前进,但将所有增量相加会产生查询实际 CPU 时间的无偏估计。

因为我们如此频繁地传递信号,一个关于上下文切换或信号传递的罕见内核 bug 似乎很合理。我们花时间阅读 bug 报告、内核源代码和 Azure 特定的内核补丁。我们尝试了压力测试。我们没能找到任何看起来相关的东西。

那时我们决定退一步,尝试不同的方法。

## 医生还是流行病学家?

调试这样的问题有两种广泛的方法。

一种是像医生一样:专注于一个病人,做很多测试,并尝试从详细的证据中诊断单个病例。

另一种是更像流行病学家:观察整个群体,并询问是否存在单个病例无法揭示的模式。这个 bug 是在特定版本开始的吗?它是否与一个硬件 SKU(特定的 CPU 和服务器型号)、一个区域或一个内核版本相关?在看起来像一个综合征的内部是否隐藏着多个不同的集群?

我们之前主要是医生模式。关键的转变是决定我们需要收集高质量的群体数据。

## 清理数据

我们之前自动找到所有问题实例的尝试失败了,因为我们试图使用日志的文本搜索。核心转储本身有更多信息,但手动查看它们无法扩展。我们决定投入精力构建一个可以自动分析核心转储的管道。

我们让 ChatGPT 写了一个脚本,下载每个核心文件的前缀,提取寄存器,使用日志过滤已知的误报,并自动将崩溃标记为返回空、堆栈偏移或其他。然后我们在过去一年中所有的生产 Rockset 核心转储上并行运行这个脚本。

这是转折点。

一旦我们有了干净的数据集,相关性立即显现。我们之前当作一个奇怪 bug 处理的东西,实际上是_两个_独立的崩溃群体。

返回空的核心转储分布在许多集群和地理区域。它们的频率最近增加了,但没有明确的开始日期和清晰的基础设施边界。

堆栈偏移的崩溃看起来完全不同。它们都来自一个区域,有明确的开始日期,并且从未在运行了很长时间的节点上发生。尽管它们涉及多个 Azure VM(云中托管的虚拟机),但模式看起来像是一台物理机器有坏硬件,导致落在它上面的任何 VM 出现问题。

![按集群随时间变化的崩溃率点图,显示大多数崩溃集中在集群 2、3 和 6,在周期结束时集群 1 有一个峰值。](https://images.ctfassets.net/kftzwdyauwt9/2Cu7fZUNQdMpqwby9xRFkE/1d171387051612f2f8fb5e352764a2a2/Crash_rate_by_cluster_light_desktop.svg?w=3840&q=90)

那一刻我们意识到,我们一直在心理上混淆了两个 bug。因为我们一直在混合来自两个 bug 的反例,我们无法找到一个单一的连贯解释。

## Bug #1:坏主机

有了干净的 Kubernetes 节点和时间戳列表,我们能够将堆栈偏移的崩溃追溯到单个物理主机,这很容易加入黑名单。

我们无法在该主机上的受控环境中重现寄存器损坏,即使在几周的压力测试之后也是如此。然而,一旦有问题的主机被停止服务,堆栈偏移的崩溃就消失了。

移除坏主机不是一个永久性的解决方案,因为它不能防止同样问题的新发生。然而,我们可以更改软件,以便如果类似问题再次发生,可以轻松检测和处理。我们改进了致命信号处理程序,包含寄存器状态,这样我们只能从日志中检测复发(不需要核心转储)。我们更改了控制平面,以便 VM 通常被重用而不是回收,这使得在我们的基础设施堆栈层面更容易检测坏节点。我们还更新了我们的运行手册(和我们团队的思维模型),以包括这种可能性。

随着坏主机崩溃被分离出来,剩下的返回空核心转储变得更容易推理。之前我们排除了异常展开,因为我们以为有反例:在肯定不使用异常的代码路径中的崩溃。但那些反例都来自硬件损坏集群。

一旦我们带着这个想法重新审视剩下的核心转储,我们发现这个结论完全反了:崩溃都发生在异常展开期间。

## 异常处理是一种动态控制转移

当 C++ 抛出异常时,运行时必须发现哪个 catch 块应该接收它,以及哪些析构函数或清理处理程序应该沿途运行。编译器发出这些元数据,但实际的匹配在运行时动态发生。

异常展开实际上不是由调用 `throw` 的函数执行的,而是由生成的编译代码调用的辅助函数执行的。这些运行时例程检查堆栈,获取堆栈上函数的元数据,动态查找清理处理程序和 catch 块,然后将控制转移到其中一个位置。转移控制包括展开所有中间的栈帧(包括辅助函数的栈帧)。

操作上,这更接近于 `longjmp` 或协程切换,而不是普通的调用和返回。被调用者保存的寄存器必须恢复,以及栈帧寄存器 `%rbp` 和 `%rsp`。

我们的二进制文件链接了两个包含执行 C++ 异常展开的函数的库:libgcc 和 GNU libunwind。动态链接器选择了 GNU libunwind 的定义。这让我们很惊讶;我们原本期望 libgcc 的实现会因为符号版本控制规则而胜出;然而,检查正在运行的二进制文件显示情况并非如此。

## 推翻最后一个假设

此时,我们的工作假设发生了变化,因为我们放松了另一个假设,这个假设是在我们认为只有一个 bug 时做出的。

也许我们看到的不是普通函数返回 NULL。也许我们看到的是一个展开转移——实际上是一个 `setcontext` 风格的寄存器恢复——其中目标指令指针在控制转移之前变成了 NULL。换句话说,是展开库提供了错误的数据,而不是堆栈上错误的返回地址槽。

这极大地缩小了问题范围。要么是 GNU libunwind 计算了错误的目标状态,要么是它计算了正确的状态,但在应用之前被某些东西损坏了。

我们阅读了 GNU libunwind 的源代码,发现它在堆栈上合成一个 `ucontext_t`,填充清理处理程序帧所需的寄存器状态,然后将指向该结构的指针交给一个内部汇编例程:`_Ux86_64_setcontext`。

此时我们有了所有的碎片。

合成的 `ucontext_t` 位于 `_Ux86_64_setcontext` 执行期间被展开的栈帧之一中。`_Ux86_64_setcontext` 在改变 `%rsp` 之后是否从该结构读取数据?此时该结构不再属于活动堆栈?那会使它容易受到信号传递的覆盖,比如我们频繁的 `SIGUSR2`。

## Bug #2:libunwind 的 bug

答案是肯定的。

以下是我们使用的 GNU libunwind 版本中 `_Ux86_64_setcontext` 的最后六条指令,主要由从内存加载到目标寄存器的 `mov` 指令组成:

#### 纯文本

`1

74:        mov    UC_MCONTEXT_GREGS_RSP(%rdi),%rsp

2

75:

3

76:        /* 将返回地址压入堆栈 */

4

77:        mov    UC_MCONTEXT_GREGS_RIP(%rdi),%rcx

5

78:        push   %rcx

6

79:

7

80:        mov    UC_MCONTEXT_GREGS_RCX(%rdi),%rcx

8

81:        mov    UC_MCONTEXT_GREGS_RDI(%rdi),%rdi

9

82:        retq

`

(`%rdi` 指向堆栈分配的 `ucontext_t`,`UC_MCONTEXT_*` 宏只是展开为存储特定寄存器的固定偏移量。)

第一条指令是竞态窗口的开始。它更新 `%rsp` 以指向活动堆栈的新底部。一旦发生这种情况,由 `%rdi` 指向的结构就不再是活动堆栈(或红区)的一部分,并且不再对内核不可访问。

通常这不会引起问题,但如果信号恰好在正确(或错误?)的时刻到达,内核将在 `%rsp-128` 处构建信号帧。这可能会覆盖 `%rdi` 指向的内存。

如果这发生在下一条指令读取 `UC_MCONTEXT_GREGS_RIP(%rdi)` 之前,那么恢复的指令指针可能会被损坏。在我们的崩溃中,它变成了 NULL。

这就是 bug。

## 为什么核心转储伪装成普通的错误返回

这个汇编也解释了一个让我们困惑的观察:为什么函数 X 在前一个栈帧的返回地址槽中有一个 NULL。

`setcontext` 被编写为恢复所有寄存器,包括 `%rdi`,所以它不能在控制转移的最后时刻使用该寄存器来读取 `UC_MCONTEXT_GREGS_RIP(%rdi)`。相反,它更早地读取该值,将其保存到堆栈,恢复更多寄存器,然后使用 `retq` 读取保存的值并转移控制。

在核心转储中看起来像“一个函数返回到了 NULL”的东西,实际上是“展开器在堆栈上合成了一个目标返回地址,但该目标在转移完成之前被损坏了。”我们假设返回地址槽的损坏一定发生在原地,因为我们不知道有任何地方(可损坏的)数据被故意写入返回地址槽。

## 一个单指令的竞态窗口

使这个 bug 看起来荒谬的是这个竞态窗口有多窄。在这种竞态条件中,外部事件(信号)需要在另一个线程采取的两个步骤之间发生。这些步骤彼此越接近,竞态条件发生的可能性就越小。

在这种情况下,脆弱窗口实际上只有一个指令宽!信号必须在 `%rsp` 被改变之后,但在下一条指令加载 `%rip` 之前传递。在现代超标量乱序 CPU 上,几个这样简单的指令可以在一个周期内运行,所以竞态窗口大约是一百皮秒。

当我们发现这个竞态时,我们的第一反应是它一定太罕见了,无法解释观察到的崩溃率。我们在整个集群中每天看到超过十几个返回空崩溃。一个在异常清理期间的单指令竞态真的能解释这个吗?

我们转向费米估算。如果脆弱窗口大约是 10^{-10} 秒,而 SIGUSR2 每 10^{-2} 秒 CPU 时间到达一次,那么每个异常清理处理程序或 catch 块有大约 10^{-8} 的概率输掉竞态。

Rockset 使用异常作为其内部摄取背压机制的一部分。单个过载的主机每秒可以抛出大约 10^4 个异常。这意味着使用背压的主机的平均故障间隔时间是 10^4 秒,或者每几小时一次崩溃。在集群规模上,这足以解释观察到的崩溃频率。

## 为什么 libunwind 的 bug 现在才出现?

GNU libunwind 的 bug 很老——超过 18 年,存在于第一个支持 C++ 异常展开的 `x86_64` 版本中。

那么为什么它现在才出现?

崩溃率大致与抛出多少异常和传递多少信号成正比。它也取决于信号处理程序消耗多少堆栈。

Rockset 在所有三个轴上都不同寻常。我们作为正常过载控制的一部分以高速率抛出异常;由于 `coarse_thread_cputime_clock`,我们异常频繁地传递 `SIGUSR2`;今年早些时候,我们通过添加对 `timer_getoverrun` 的调用使 `SIGUSR2` 处理程序使用了更多堆栈,以便我们可以处理合并的信号。

最后一个变化似乎很重要。如果处理程序使用足够少的堆栈,它可能不会到达并覆盖过时的 `ucontext_t` 内存。在那个变化之前,我们根本没有观察到这些崩溃。变化之后,速率保持较低,直到我们为一些强调背压机制的用例增加了负载。

换句话说,libunwind 的 bug 一直存在,但我们的异常率、信号率和处理程序堆栈使用量的乘积直到最近才越过阈值,变得在操作上可见。

这个机制也解释了为什么硬件 bug 和 libunwind bug 都主要导致 `DocumentTree::updateDocument` 崩溃。libunwind 导致的崩溃特别集中在这个方法上,因为当我们抛出异常来施加数据摄入反压(ingest backpressure,即控制数据流入速度的机制)时,这个方法总是处于活跃状态。它也是 `%rsp` 未对齐崩溃的高发区,因为出问题的硬件节点属于我们用于批量数据摄入的 SKU(库存单位,这里指硬件型号),而这类节点的大部分 CPU 时间都花在这个方法里。

我们立即采取的缓解措施是把 GNU libunwind 换成 libgcc 的 unwinder(解卷器,用于在程序崩溃时回溯函数调用栈的工具)。这个替换本身就很值得:libgcc 的实现经过大量优化,减少了锁竞争(lock contention,多个线程争抢同一把锁导致的性能问题),这在扩展到大型虚拟机时非常重要。

我们还向上游的 GNU libunwind 提交了一个可独立运行的复现程序和一个[修复⁠(在新窗口中打开)](https://github.com/libunwind/libunwind/commit/a9b9293b286c14b9ed19db501aa347b46edd8a28),并验证了其他解卷器没有类似问题。

## 群体诊断的力量

这次调试之旅让我们学到了很多关于动态链接、DWARF 解卷元数据(DWARF unwind metadata,一种用于描述程序运行时栈帧信息的标准格式)、Linux 信号传递、System V ABI(应用程序二进制接口,定义了程序如何与操作系统交互)以及 C++ 异常机制的细节。但最重要的教训比这些都简单。

最关键的一步不是聪明的汇编代码阅读或对细节的深入了解,而是构建一个高质量的数据集。没有这个数据集,我们就把两种不同的现象混为一谈,试图在混乱中理出头绪。一旦我们有了准确、完整的群体数据,问题的结构就变得一目了然:一类崩溃属于有问题的硬件主机,另一类属于 libunwind 中的竞态条件(race condition,多个线程或进程因执行顺序不确定而导致的错误)。数据质量提高了,调试就变得容易了。

对于像 Rockset 这样的基础设施系统来说,这一点非常重要。这次调查进一步坚定了我们对深度监控、自动化调查和持续改进运维工具的承诺。可靠性不仅仅是事后修复 bug,更是要构建数据、工作流程和技能,把看似不可能解决的问题变成可诊断、可解决的问题。

* [2026](https://openai.com/news/?tags=2026)

## 作者

Nathan Bronson,技术团队成员

## 继续阅读

[查看全部](https://openai.com/news/)

![Tax Agent > Art Card](https://images.ctfassets.net/kftzwdyauwt9/6ojJ6B55QUlmNdaLifgMkQ/22e429ec7b3fdd119a2499358af899b7/Art_Card.png?w=3840&q=90&fm=webp)

[用 Codex 构建能自我改进的税务代理工程2026年5月27日](https://openai.com/index/building-self-improving-tax-agents-with-codex/)

![codex windows > art card](https://images.ctfassets.net/kftzwdyauwt9/6ZvTl8ZL23BOhoI6jz0EmR/49d6038b9f92773d4f866f4bfacabdbf/Art_Card__5_.png?w=3840&q=90&fm=webp)

[构建安全有效的沙箱,让 Codex 在 Windows 上运行工程2026年5月13日](https://openai.com/index/building-codex-windows-sandbox/)

![MRC 1_1](https://images.ctfassets.net/kftzwdyauwt9/IRqiqOUeNlFne8NPTbELM/9ab024f4581e7065eaf42aa18d14b724/Art_Card.png?w=3840&q=90&fm=webp)

[用超级计算机网络加速大规模 AI 训练工程2026年5月5日](https://openai.com/index/mrc-supercomputer-networking/)

研究
* [研究索引](https://openai.com/research/index/)
* [研究概览](https://openai.com/research/)
* [经济研究](https://openai.com/signals/)

最新进展
* [GPT-5.6](https://openai.com/index/gpt-5-6/)
* [GPT-5.5](https://openai.com/index/introducing-gpt-5-5/)
* [GPT-5.4](https://openai.com/index/introducing-gpt-5-4/)

安全
* [安全方法](https://openai.com/safety/)
* [部署安全(在新窗口中打开)](https://deploymentsafety.openai.com/)
* [安全与隐私](https://openai.com/security-and-privacy/)
* [信任与透明度](https://openai.com/trust-and-transparency/)

产品
* [ChatGPT(在新窗口中打开)](https://chatgpt.com/?openaicom-did=6b4e5ba4-f156-469a-86e7-c924dccbee69&openaicom%5Freferred=true)
* [ChatGPT Business(在新窗口中打开)](https://chatgpt.com/business/?openaicom-did=6b4e5ba4-f156-469a-86e7-c924dccbee69&openaicom%5Freferred=true)
* [ChatGPT Enterprise(在新窗口中打开)](https://chatgpt.com/business/enterprise/?openaicom-did=6b4e5ba4-f156-469a-86e7-c924dccbee69&openaicom%5Freferred=true)
* [ChatGPT for Education(在新窗口中打开)](https://chatgpt.com/business/education/?openaicom-did=6b4e5ba4-f156-469a-86e7-c924dccbee69&openaicom%5Freferred=true)
* [Codex](https://openai.com/codex/)
* [发布说明](https://openai.com/products/release-notes/)

API 平台
* [概览](https://openai.com/api/)
* [API 登录(在新窗口中打开)](https://platform.openai.com/login)
* [文档(在新窗口中打开)](https://developers.openai.com/api/docs)

业务
* [概览](https://openai.com/business/)
* [解决方案](https://openai.com/solutions/)
* [资源](https://openai.com/business/learn/)
* [客户案例](https://openai.com/business/customer-stories/)
* [合作伙伴网络](https://openai.com/business/partners/)
* [联系销售](https://openai.com/contact-sales/)

开发者
* [Apps SDK(在新窗口中打开)](https://developers.openai.com/apps-sdk)
* [开放模型](https://openai.com/open-models/)
* [文档(在新窗口中打开)](https://developers.openai.com/)
* [资源(在新窗口中打开)](https://developers.openai.com/learn)
* [开发者论坛(在新窗口中打开)](https://community.openai.com/)

公司
* [关于我们](https://openai.com/about/)
* [我们的章程](https://openai.com/charter/)
* [招聘](https://openai.com/careers/)
* [新闻](https://openai.com/news/)

支持
* [帮助中心(在新窗口中打开)](https://help.openai.com/)

更多
* [故事](https://openai.com/stories/)
* [学院](https://openai.com/academy/)
* [直播](https://openai.com/live/)
* [播客](https://openai.com/podcast/)
* [RSS](https://openai.com/news/rss.xml)

条款与政策
* [使用条款](https://openai.com/policies/terms-of-use/)
* [隐私政策](https://openai.com/policies/privacy-policy/)
* [其他政策](https://openai.com/policies/)

[(在新窗口中打开)](https://x.com/OpenAI)[(在新窗口中打开)](https://www.youtube.com/OpenAI)[(在新窗口中打开)](https://www.linkedin.com/company/openai)[(在新窗口中打开)](https://github.com/openai)[(在新窗口中打开)](https://www.instagram.com/openai/)[(在新窗口中打开)](https://www.tiktok.com/@openai)[(在新窗口中打开)](https://discord.gg/openai)

OpenAI © 2015–2026您的隐私选择

英语美国

![](https://bat.bing.com/action/0?ti=187204252&Ver=2&mid=db5007d3-b174-43c1-9c68-c48fa66052f7&bo=1&sid=b51b47107dc411f19bc9db3a9eb246ed&vid=b51b88f07dc411f1a7c27342f6b1e836&vids=1&msclkid=N&pi=918639831&lg=en-US&sw=800&sh=600&sc=24&nwd=1&tl=Core%20dump%20epidemiology%3A%20fixing%20an%2018-year-old%20bug%20%7C%20OpenAI&p=https%3A%2F%2Fopenai.com%2Findex%2Fcore-dump-epidemiology-data-infrastructure-bug%2F&r=&lt=2334&evt=pageLoad&sv=2&cdb=AQAQ&rn=119997)

---

英文原文:[Core dump epidemiology: fixing an 18-year-old bug](https://openai.com/index/core-dump-epidemiology-data-infrastructure-bug)