1.3 Therac-25——三次杀死人的软件
1985年,德州
Marietta,一个61岁的乳腺癌患者躺在Therac-25的治疗床上。这是一种加拿大原子能公司(AECL)制造的放射治疗机,可以用高能电子束或X射线精确照射肿瘤。她的治疗计划是术后辅助放疗:电子束模式,约200拉德(rad),照射左侧胸壁和腋窝区域,以清除手术切除后可能残留的微小病灶。
操作员键入命令。屏幕上闪烁“MALFUNCTION 54“,故障54。这是Therac-25的一个已知错误代码,在过去几个月的使用中频繁出现。它通常表示实际输出剂量与预设值不一致。操作员按了清除键。重新输入。再次MALFUNCTION 54。再次清除。重新输入。
第三次,机器以全功率X射线模式发射了一束能量极高的射线。但屏幕上的指示仍然是“电子束模式“,显示给操作员看的模式,和机器实际执行的配置,已经不一致了。患者在照射瞬间感到“一股强烈的电击感,像被烫了一下“。
她实际接受的剂量大约是25000拉德,是处方剂量的125倍。几个月后,辐射灼伤导致组织坏死,左臂和肩部永久性功能丧失,乳房被切除。她终生生活在疼痛中,但活了下来。
同样的事故在Therac-25上发生了至少六次。FDA调查后发现,这不是硬件故障,是一段并发软件中的竞态条件(Race Condition),在特定时序下导致了一个字节的标志位被异常置位,使机器跳过了电子束散射靶的物理插入步骤。没有靶,电子束变成了致命的X射线。
Therac-25事故直接催生了医疗软件安全监管体系。
不是硬件坏了——是软件的设计拒绝了硬件的安全
Therac-25和前代产品Therac-20有一个关键区别:它移除了硬件安全互锁。
在Therac-20上,如果软件想要在不插入X射线靶的情况下发射高能电子束,有一个独立的硬件电路会物理性地断开射线源的供电,不管软件说什么,物理开关不接通,射线发不出来。但在Therac-25上,AECL的工程师认为“软件的可靠性已经足够高“,他们去掉了硬件互锁,把大部分安全决策交给了软件。
这就是竞态条件能杀死人的根源。软件没有物理的“不“。
Therac-25的操作系统是并发的:它有一个主控制任务,一个键盘扫描任务,一个剂量监控任务。这三个任务共享一组全局变量。在特定的时序窗口里(大约8到800微秒),键盘扫描任务修改了一个字节的数据,而剂量监控任务在读同一个字节。字节的高位在错误的瞬间翻转了,导致“靶已插入“的确认被错误地跳过。机器开始发射。
但这不是一个“一次性的硬件故障“。这起事故之所以在FDA调查中花了两年才定位,是因为它是可复现但不可预测的。操作员每次清除MALFUNCTION 54、重新输入,实际上在调整时序窗口。三次清除缩短了键扫任务和剂量监控任务之间的时间差:恰好落入了竞态的最窄窗口。
Therac-25教给世界的是:当安全依赖软件的时候,你必须假设软件会失败,然后用独立的、非软件的机制来兜底。ISO 26262里关于“独立安全监控“、“硬件互锁”、“冗余“的几乎所有要求,都是在回应Therac-25的教训。
从Therac-25到ISO 26262 Part 6——软件的罪与赎
FDA的调查报告(1986年发布,至今仍是软件安全领域最重要的工程文献之一)列出了Therac-25的软件缺陷清单。这张清单上的每一项,都直接催生了后来的医疗和汽车安全标准的特定条款。你看到这张清单时,会发现它几乎就是ISO 26262 Part 5和Part 6的“反例教材“。
缺陷1:全局标志变量被三个并发任务共享,无任何互斥保护。 键盘扫描任务(每200ms运行一次,处理键盘输入缓冲区)和剂量监控任务(每40ms运行一次,读取剂量计数值并和预设值比较)共享同一个8位的标志字节。标志的bit 0表示“X射线靶已就位“,bit 1表示“准直器已就位“。在竞态窗口中,键盘扫描任务正在写这个字节的一个bit,剂量监控任务正在读同一个字节,读到的值是“半更新“的中间态。这不是理论上的概率问题:Therac-25的操作频率使得两个任务的大约每10秒就有一次并发窗口重叠,而竞态在窗口重叠的约1微秒内有几率触发。实际操作日志显示MALFUNCTION 54平均每天出现若干次。
ISO 26262-6对此的响应是:软件架构设计必须为安全相关数据提供免于干扰(FFI)的保护。具体技术包括MPU分区,将安全关键数据的存储区域在硬件层面和所有非安全任务隔离,以及共享数据的互斥访问验证:任何两个不同ASIL等级的任务共享一个变量,必须有锁机制且锁本身经过安全分析。
缺陷2:错误代码MALFUNCTION 54被操作员随意清除,无故障保留。 MALFUNCTION 54在Therac-25的软件设计文件里定义为“剂量率测量值与预期值的偏差超过5%“。但它的处理是:在屏幕上显示代码、暂停照射、等待操作员按清除键。清除之后,机器不保留任何故障历史,不执行任何诊断,不限制任何后续操作。 操作员可以立即重新输入治疗参数并重新启动。FDA调查发现,事发当天的操作日志在6次MALFUNCTION 54之间没有任何记录,说明操作员在每次清除后都没有采取任何故障排查动作。
ISO 26262-6对此的响应是:安全相关的故障检测必须触发故障响应协议:记录故障类型、发生时间、发生前的操作上下文。故障响应协议必须定义分级策略:轻微故障可以记录后继续运行,中度故障限制功能降级,严重故障直接进入安全状态。故障响应必须不能被用户(操作员、驾驶员)绕过:“清除故障“必须是受控操作,通常需要特定的诊断设备权限。
缺陷3:屏幕显示状态与实际执行状态不同步。 竞态触发时,机器实际处于X射线模式:全功率、无物理散射靶、光束直接照射患者,剂量率是常规治疗的125倍。但操作员的屏幕上仍然显示“电子束模式、靶已插入、剂量率正常“。这是因为状态显示的数据来自主控制任务,而主控制任务读的是已经被竞态污染的那个标志字节。屏幕信息和真实机器状态之间的同步链断了。
ISO 26262-5/6对此的响应是:“安全状态“的显示信息必须来自“可信源”,不是主控制任务的周期性采样,而是安全机制的直接输出。例如ASIL D的EPB系统,其“驻车状态指示灯“的驱动信号不是来自仪表显示的CAN报文,而是来自制动ECU的安全状态硬线。如果CAN报文被干扰或延迟,硬线信号仍然能点亮仪表盘上的红色驻车警告灯。安全相关的信息显示不能依赖“非安全“的数据通道。
缺陷4:软件没有独立的故障关断回路。 Therac-25本身有第二枚独立的剂量监控微处理器(DAC),它从独立的电离室读取实际的辐射剂量,和预设值比较。但这枚微处理器的输出没有接入紧急断电回路。它只能“记录“剂量超标值,它是事后审计工具,不是实时安全机制。当竞态触发后DAC检测到剂量超标,它把异常写入日志,但日志是写在EEPROM里的,机器在主控逻辑层面完全不知情,继续发射。
ISO 26262-5对此的响应是:故障响应链必须是端到端的,从检测到执行。 独立的故障检测单元(如安全监控MCU)的输出必须直接连接到执行器断电回路,不能经过主MCU的“判断“。因为主MCU本身可能就是故障的源头:让它来决定“要不要因为安全监控报告而断电“,等于让被告当法官。
Therac-25之后,全世界的医疗设备监管体系发生了根本变革。美国FDA在1990年发布了第一版医疗器械软件标准。IEC 62304(医疗器械软件生命周期)在2006年发布。而ISO 26262对软件安全的严格要求,包括软件安全架构、免于干扰、故障响应协议、安全状态,在基因上就携带着Therac-25的教训。
从Therac到汽车——同一个bug,移动速度从0到120km/h
你可能会说,Therac-25是医疗设备,跟我写的ECU有什么关系?
关系在于:医疗设备和汽车安全系统共享同一个软件安全模型。 区别只有两点:移动速度和操作者。
医疗设备的操作者是培训过的技师,在受控的医院环境里操作,有冗余剂量计,有物理约束(铅墙、照射室)。但汽车安全系统的操作者是你不知道是谁的驾驶员,没有任何培训、可能是疲劳的、可能在暴雨中、在拥堵的高速上、身边坐着家人。
Therac-25的竞态bug在120km/h的车上重演,后果是即时的。不需要几个月,错误发生后的几百毫秒,方向盘被打偏了3度,车偏离了车道。LKA系统的软件架构必须保证,在任何一个竞态窗口里,错误的方向盘角度命令都不会被执行。这比Therac-25的要求更高:不只需要事后阻止,更需要事前预防和即时纠正。
这就是为什么ISO 26262在2011年从IEC 61508独立出来,成为单独的汽车安全标准。汽车的“安全“定义不同,因为伤害的速度不同。
丰田刹车门——ECU软件安全的全球警钟
2009年8月,加州San Diego。一辆雷克萨斯ES350在高速公路上冲向交叉路口,驾驶员在911报警电话里喊道“我们没办法停下来!“,最终车辆失控起火,车内四人全部丧生。这是丰田“意外加速“系列事故中最著名的一起——Saylor一家四口在事故中丧生。NHTSA的调查报告将该事故的直接原因归于脚垫卡住油门踏板。
从1999年到2011年,美国市场累计向NHTSA提交了超过3000起丰田车辆意外加速投诉。脚垫卡滞和油门踏板回位缓慢的机械问题解释了其中一部分,但仍有大量投诉无法明确归因。
2011年,NASA受美国交通部委托,对丰田的电子节气门控制系统(ETCS)进行了10个月的彻底审查。NASA的最终报告中的结论是:“NESC团队未发现任何电子或软件缺陷能够解释意外加速。”——这是一个关于证据不足的结论,而不是对“电子无缺陷“的绝对证明。
但这个故事没有在NASA这里结束。2013年,在丰田的民事诉讼中,原告方聘请的专家证人Michael Barr(Barr Group)对丰田的ETCS源代码进行了独立分析,得出了与NASA截然不同的结论。Barr在法庭上作证指出丰田的ETCS存在多个软件设计问题:
第一,缺乏独立的故障终止路径。 ETCS的主控制程序和故障监控程序运行在同一颗MCU上,共享同一个RTOS。如果主控制程序因堆栈溢出等异常而跳过节气门闭合的安全检查,监控程序恰好在同一调度周期内被干扰,两者会同时失效。这和Therac-25的竞态缺陷在架构上是同构的:安全机制和被保护的功能共享了同一个失效域。
第二,故障记录和诊断覆盖率不足。 ETCS配置了多个冗余传感器,两个独立的加速踏板位置传感器、节气门位置传感器、刹车踏板开关,但没有代码做跨传感器的一致性校验。软件分别判断每个传感器的读数是否在合理范围内,但“踏板A读40%开度、踏板B读0%、刹车已踩下“这种组合显然不可能是正常工况。
这两条批评在软件工程上是有力的,但它们来自诉讼中的专家分析,并非官方的安全结论。这个案例的复杂性本身就是一个教训:当一个系统的失效后果足够严重(多人死亡、数千投诉、国会听证),“故障原因“就不再只是一个工程问题,它同时成为了法律问题、政治问题和公关问题。
Therac-25和丰田刹车门在架构缺陷上是同构的——安全功能和非安全功能共享同一个失效域,这是ISO 26262的FFI(免于干扰)要求试图杜绝的核心问题。但丰田案例也告诉我们另一个更微妙的教训:即使安全架构留有隐患,故障可能仍然只是偶发的、难以复现的、难以归因的,而“无法证明有缺陷“不等于“没有缺陷“。
Therac-25事故直接催生了医疗软件安全监管体系。丰田刹车门事件则对ISO 26262 Part 6中软件安全架构和测试覆盖率的加严产生了重要影响。波音737 MAX的MCAS软件缺陷直接推动了DO-178C和独立安全论证的审查独立性要求。
Toyota方面始终否认这些软件问题与意外加速之间存在因果关系,并在多条诉讼中坚持这一立场。本节所述Barr Group的分析来源于公开的法庭记录,不代表作者对事故原因作出任何定论。
你现在遵循的每一条安全规范背后,可能都有一次真实的事故、一份真实的调查报告、和真实的伤亡者。你不能用事故来验证安全,你需要用规范来预防事故。
本篇小结
- Therac-25的竞态条件造成了6人伤亡,根因是软件并发bug+移除硬件互锁。它定义了软件安全的“不可信任原则“:不能假设软件正确,必须有独立硬件兜底。
- Therac-25的软件缺陷清单直接映射到ISO 26262 Part 5/6的安全需求。
- 汽车和医疗设备共享同一个安全模型,区别在于汽车的伤害速度是即时的,对反应时间的要求更严格。
- Therac-25和丰田刹车门的争议核心指向同一个架构层面的问题:安全功能与受保护功能共享了同一个失效域。ISO 26262的FFI(免于干扰)要求是对这一教训的直接回应。
【下集预告】: Therac-25告诉我们软件不可信,必须有硬件兜底。但怎么从数学上证明一套软件是“可分析的“?Alan Turing发明了图灵机,然后证明了一个定理:不存在一个算法能判断任意程序会不会跑飞。这就是停机问题。下节你会看到它如何被翻译成看门狗里的
if (timeout) reset()。