logo资料库

汽车电子软件安全的案例-KMR暴冲Barr软件代码审查证词.docx

第1页 / 共12页
第2页 / 共12页
第3页 / 共12页
第4页 / 共12页
第5页 / 共12页
第6页 / 共12页
第7页 / 共12页
第8页 / 共12页
资料共12页,剩余部分请下载后查看
第一部分 背景简介 前几年闹得沸沸扬扬的丰田刹不住事件最近又有新进展。十月底俄克拉荷马的一次庭审, 2007 年一辆 2005 年凯美瑞暴冲(Unintended Acceleration,UA)致一死一伤事件中丰 田被判有责。引起广泛关注的是庭审中主要证人 Michael Barr 的证词让陪审团同意丰田的 动力系统软件存在巨大漏洞可能导致此类事件。这是丰田在同类事件中第一次被判有责。庭 审过后丰田马上同意支付 300 万美元进入调解程序。 出于好奇,我漫不经心地下载了 Barr 的 286 页证词,却一下子被吸引住了。几天内读 完,算是对这次事件进行了一次深入了解。下面就从外行角度总结一下这份证词并尝试以更 简单的语言解释里面提到的暴冲原因以及丰田犯下的错误。 Barr 的证词下载自他的个人博客 Barr Code,但现在该文已经被删除。见 2 楼。 Michael Barr 是谁?他是一位拥有 20 年以上行业经验的嵌入式系统工程师。在十八个 月中,有 12 位嵌入式系统专家,包 Barr,受原告诉讼团所托,被关在马里兰州一间高度保 安的房间内对丰田动力控制系统软件(主要是 2005 年的凯美瑞)源代码进行深度审查。这 房间没有英特网,没有手机信号,他们进出不能携带任何纸张、记录甚至皮带。最后的调查 结果被写入一份 800 页,13 章的详细报告。而鉴于保密协议,调查内容一直没有公布,直 至俄克拉荷马这次庭审才首度部分公开(报告本身似乎还没公开)。 回到正题。丰田的软件有没有缺陷?根据 Barr 的调查,答案是有。这其实是废话,任 何软件都会有缺陷,关键在于是什么样的缺陷。丰田的软件缺陷分为三类: 1 非常业余的结构设计。 软件设计的基本要求是模块尽量简单化,因为这样可以一来更易于阅读二来更易于维护。 但丰田的工程师显然没有遵循这原则。Barr 使用一种工具自动根据代码的可能分支数量评 估函数的复杂度,结果是丰田的软件中至少有 67 条函数复杂度超过 50,意味着运行这个 函数可能出现超过 50 种不同的执行结果,属于“非可测”级别。因为为了测试这 50 个不 同的结果,必须准备至少 50 条不同的测试用例以及相应的文档,在生产环境中一般是不现 1 / 12
实的。作为比较,Barr 表示他自己的公司严格执行的其中一条规定就是任何代码复杂度不 能超过 30,否则不合格。而在这 67 条函数中还有 12 条复杂度超过 100,达到“非可维护” 级别,意味着一旦发现缺陷(Bug)也无法修复,因为实在太复杂,修复缺陷的过程中会产 生新的缺陷。其中最复杂的一条函数有超过 1300 行代码,146 个可能执行路径——正好用 于根据各传感器数值计算节气门开关角度。 如果你不知道什么是节气门,那么这里我稍微解释一下。为了让内燃机运行,有三大要 素:燃油、空气和点火时机。空气和燃油的混合物进入气缸,被火花塞在正确的时间点燃推 动活塞并最终推动曲轴和车轮前进。在电喷技术发明以后直到 2002 年以前,三大要素的燃 油和点火时间是由电子设备控制,节气门机械连接加速踏板,由司机直接控制。节气门大致 是一个连接加速踏板的“空气龙头”——踩下去越多,“龙头”打开得越大,允许越多的 空气进入发动机输出更大的动力。2002 年以后,丰田引入的“电子油门”让电子系统掌管 了最后一个要素:空气。加速踏板不再机械连接节气门,而是连接一些传感器,由行车电脑 将这些传感器数值计算成节气门开启角度再由马达控制节气门。我们稍后会再讨论节气门开 合。 极复杂的代码带来的是极复杂的功能。下面说一下被称为“厨房洗涤盆”的 Task X。 这里先解释一下,丰田的软件系统和很多别的软件系统一样,都是由一个统领程序(称之为 “操作系统”)和若干小程序(称之为 Task)组成。就好比电脑上跑的 Windows 是统领 全局的操作系统,网络浏览器和记事本是跑在操作系统上的小程序。丰田的系统里每个 Task 都有自己的名字,但这些名字非常敏感,敏感到每次被提及的时候律师都要求法庭内的没有 阅读代码权限的人全部清场。为了减少清场次数,Barr 将这个最重要的小程序称为 Task X。 这个 Task X 有多重要呢?跟厨房里的洗涤盆一样重要。它负责非常多的事情,包括计算节 气门开启角度、速度监测和保持、定速巡航监测等等。Task X 的不正常运行被认为是暴冲 事件的元凶。稍后会再继续讨论 Task X。 还有一些别的匪夷所思的发现。比如丰田的软件包含了超过一万一千个全局变量。如果 你不知道什么是全局变量,那么只需要知道软件设计的一般原则是要尽量少使用全局变量, 因为有可能带来无法预测的结果。这里的“少”的意思是“尽量接近零”,绝对不会是一万 一千个。 2 不符合软件开发规范。 2 / 12
如同很多行业一样,汽车行业也有自己的规范。更具体一点,由于汽车的危险性质,汽 车控制系统被划分为“安全关键性系统(Safety Critical System)”——说白了就是安全 性非常重要,弄不好会死人的。为了达到这一特殊要求,汽车相关软件开发人员定期举行会 议讨论并发布编程规范,称为 MISRA C。该规范的 2004 年版的感谢列表里还能看到丰田 员工的名字,至少让外界认为丰田确实也在遵循这些规范。 真的吗?根据源代码来看,答案是否定的。对此之前的 NASA 报告也有所提及,丰田 辩称他们遵循的不是行业规范,而是丰田内部编程规范。这一规范与行业规范的吻合程度达 到 50%。但是 Barr 认为根据他的调查,两个规范之间吻合度小于 10%,甚至有若干规范 条目相互冲突。后来发现丰田的代码甚至没有遵循丰田内部规范,当然比起别的问题这个已 经无关紧要了。 MISRA C 拥有超过 100 条规范,NASA 的调查只使用了到其中 35 条进行校对,发现 超过 7000 处违规代码。Barr 使用全部条目,对照结果是丰田的程序拥有超过 80000 处违 规代码。 这些数字说明了什么?根据统计,违规数量可以用于预测代码中暗藏的缺陷(Bug)数 量。在之前提到的汽车相关软件开发人员会议中,有人就这一主题发表过专题演讲,提出每 30 处违规代码可能包含一个重大缺陷和十个轻微缺陷。讽刺的是这人是丰田员工。 特别需要指出 MISRA C 其中一个规则的内容是不得使用递归。 如果你不知道什么是递归,那么这里我稍微解释一下。递归是一种计算方法。但一般计 算方法要么是自己算,要么询问别的计算模块索要结果。而递归则是通过问一层层问自己的 方法完成计算。好处是代码简单,坏处是计算层数不固定。可能会 2 层就出结果了,也可 能会是 10000 层,在设计程序的时候无从得知。 软件计算需要消耗存储器。越繁琐、越长的计算自然需要占用越多的存储器。递归的问 题在于其嵌套层数无法预测,从而导致可能消耗的存储器容量无法控制。稍后会再讨论存储 器。 对关键变量缺乏保护 什么是变量?变量就是存在一段存储器的 0 和 1 的集合。这些变量既可以是一些函数 的处理结果,也可以是另一些函数的处理原材料。比方说前面提到有一条程序专门计算节气 门开合角度,比如说是 20 度,这个 20 就是一个变量,存在存储器的一个指定位置。另一 3 / 12
个程序专门负责开合节气门,它知道去那个指定位置读取这个 20,然后把节气门开启 20 度。 什么是保护?嵌入式系统,或者任何系统,都会在一定条件下发生硬件或者软件错误。 客观上这是无法避免的。而且汽车作为一个时常在震动、发热、位移的系统,它的内部环境 不能说不恶劣,发生硬件错误的可能性甚至更高。什么样的硬件错误呢?别忘了变量都是 0 和 1 的组合,这些 0 和 1 由存储器上的高低电平代表。由于某些不可抗原因,一个电平从 高变成低,或者反过来,那么这个变量就被更改了。这被称为“位反转(Bit Flip)”。为 了对抗这样的事情发生,需要对变量进行保护。保护的方法一般是镜像法。简单来说就是在 两个不同的地方写入同一个变量,读取的时候两边都读,比较是不是一致。如果不一致,那 么可以得知这个变量已经不可靠,需要进行容错处理。 丰田的程序总体上对其上万个变量进行了有效保护,但问题出在操作系统上。前面提到 丰田的软件本质上分为操作系统和 Task。Task 是由丰田自己开发,但是操作系统则是由芯 片供应商提供,固化在芯片里的。丰田在这里的过失是没有对供应商提供的代码进行深度审 核,拿到什么用什么。 另一个保护措施是错误校验码(Error Detective and Correction Codes,EDAC)。 这是一个硬件层面的数据保护措施。简而言之就是给内存中每一个字节(8 比特)后面物理 地增加几比特校验码。这样万一变量出错了,可以通过校验码得知,甚至可以通过校验码修 复一些轻微错误。这个措施十分简单有效,但是在 2005 年款凯美瑞的系统中完全没有使用, 丰田却告诉 NASA 他们用了。而在 2008 年款凯美瑞中使用了 3 比特长的 EDAC。Barr 认 为是为了节省成本,否则应该使用 5 比特长。 还有值得一提的是,汽车相关的软件行业有那么几家操作系统供应商,早已形成了一套 成熟标准称为 OSEK。各供应商开发的符合 OSEK 认证的操作系统至少都能达到一定的质量。 但丰田选用的操作系统却没有通过认证,让人不解。 现在我们知道丰田在编写软件的时候至少有三种缺陷。那么重点问题:丰田的这些软件 缺陷是否会导致车辆暴冲?根据 Barr 的调查,答案是有可能。暴冲的起因需要结合上述三 种缺陷来说明。 汽车正常运行需要倚靠若干程序(这里叫 Task)的同时运作。Task 有很多,CPU 只有 一块,在任何时刻只能处理一个 Task,怎么办呢?这需要操作系统的统筹规划,合理分配 CPU 的任务,让每个 Task 都能按时执行。如果出现某种意外,让某个 Task 突然不执行了, 4 / 12
那么就称为这个 Task“死亡”。Task 死了,自然不能执行它的任务。根据 Barr 的测试, 在某些特定情况下,Task X 的死亡可以导致节气门敞开——因为 Task X 的其中一个任务 就是根据司机的操作计算节气门开合角度,它死了也就没法重新计算这个角度,即使司机把 脚挪开加速踏板,节气门也无法关闭。此为暴冲的直接原因。更糟糕的是,节气门的开合角 度这个数值,被 Task X 算出来以后保存在一个变量中。这个特定的变量正好没有被保护(缺 陷 3)。意味着万一 Task X 死亡并且停止计算,这个数值有可能因为不可抗原因被改变, 而程序无从得知。 那么 Task X 为何会死亡呢?一般是因为内存出错。这个出错可能是一个小小的位反转, 也可能是内存里的数值被别的程序错误覆盖。同一系统内同时运行了若干程序,这些程序需 要共享一块内存,内存内部必然要被划分成若干块。比如第一块给程序 1,第二块给程序 2, 等等。如果程序 1 因为某些原因(比如 Bug)写到第二块内存上去,就会导致程序 2 读取 了错误的信息。这就是所谓的内存出错。丰田的系统里,正好有这么两块相邻的内存块。第 一块被称为“堆栈(Stack)”,这是所有 Task 存储它们运行状态的地方,大小为 4KB。 与之相邻的地方储存了操作系统进行任务分配的记录。那么可以想象,如果很多 Task 给堆 栈里写入太多东西,超过 4KB,那么就会错误地写入与之相邻的任务分配表。这种错误被 称为“堆栈溢出”。操作系统拿到了错误的任务分配表,就会错误地分配任务,造成某些 Task 多执行几次,某些 Task 少执行几次,某些 Task 甚至就再也不执行——死了!必须指 出的是,程序死亡并不罕见,甚至可以认为是正常现象。稍后解释处理方法。 那么堆栈为什么会溢出呢?显然是因为要写入的数据超过了堆栈的容量。在设计程序的 时候要计算最坏的情况并且允许冗余。即使作出了正确的设计,这种错误也相对常见,所以 NASA 在他们的调查中重点排查堆栈溢出的可能性。于是 NASA 问丰田,丰田的回复是最 坏的情况下 4KB 堆栈只写入了 41%的数据,换句话说发生溢出的可能性非常低。NASA 直 接取信了这个数字并没有再深入调查。但 Barr 他们发现丰田的回答有严重低估,实际上最 坏的情况会达到 94%,而且还不算递归。丰田在代码中使用了递归(缺陷 2)。因而实际 数字有可能超过 94%而且无法预计上限,因为递归计算的嵌套层数是无法预测的。所以实 际情况下堆栈溢出的可能性相当可观。一旦溢出,相邻的任务分配表不可避免就会遭到破坏。 此为暴冲的根本原因其中之一。之所以说“其中之一”,是因为堆栈溢出仅仅是损坏任务分 配表的其中一个原因,别的还有许多可能性并没有被 Barr 在法庭上深入解释。而且任务分 配表的损坏也仅仅是导致 Task 死亡的原因之一。 5 / 12
顺便提一句,2005 年的凯美瑞的这部分供应商是电装,没有搭载堆栈实时监测功能 ——溢出了也不知道。同年的卡罗拉却搭载了,因为供应商是通用。 到这里我小结一下,串链子。左边是原因,右边是后果。 堆栈溢出→(可能导致)→任务分配表被改写→(可能导致)→Task X 死亡→(可能 导致)→节气门敞开→(导致)→汽车暴冲 必须指出的是,这条链子从最左边一直到 Task X 死亡,都还算是嵌入式系统的常见故 障。虽然程序代码写得不好也许导致更容易出错,客观上丰田并没有特别大的过错。只要处 理得当,这些故障都不会导致暴冲。 到此为止还只是前奏而已,接下来我们来看看丰田到底做错了什么。 【第二部分】丰田之罪 上面反复提到,嵌入式系统中内存出错或者程序死亡其实是一种正常现象——至少从 Barr 的证词可以得出这个结论——现在连我们都知道了,嵌入式工程师肯定比我们更清楚 才对。确实,为了使系统正常运行不被错误干扰,一般的做法是设置若干层防护措施 (Failsafe),让运行中出现的错误无法轻易突破,得到妥善处理。丰田的工程师自然也懂 得这一点。很可惜,他们搞砸了。 上面那条链子应该修改成这样:(防护措施 0)→堆栈溢出→(防护措施 1)→(可能 导致)→任务分配表被改写→(防护措施 2)→(可能导致)→Task X 死亡→(防护措施 3) →(可能导致)→节气门敞开→(防护措施 4)→(导致)→汽车暴冲。 可以看到,防护措施不可谓不多。只要处理得当,这链条应该是走不通的。现在让我们 从左到右看一个小小的内存错误是如何一层层突破防护最终导致汽车暴冲的。 首先防护措施 0。这个其实上面提到了,因为设计缺陷低估了最大占用的存储容量,并 且不符合规范地使用了递归,最终可能导致堆栈溢出。 然后到防护措施 1。上面也提到了,任务分配表紧邻堆栈。作为外行我都觉得这是个十 分危险的设计——既然堆栈这么容易溢出,好歹应该将任务分配表放远一点啊。当然我是 外行,可能实际上比想象中复杂很多。这段 Barr 的证词中并未特别提到,属于我的个人理 解。 防护措施 2。从这里开始丰田的错误越发严重。任务表被改写导致某些 Task 运行异常, 在软件层应该有若干检测措施,比方说用特殊的监视 Task 来监视别的 Task 是否正常。但 6 / 12
丰田是怎么做的呢?还记得上面的“厨房洗涤盆”Task X 吗?它是如此复杂(缺陷 1), 除了控制汽车运行的任务之外竟然还兼任大部分的监视任务,比如生成 DTC。 DTC(diagnostic trouble codes),是汽车电脑系统会根据情况生成的错误代码。有的车 主可能会遇到汽车某报警灯常亮,修车师傅拿个仪器插在行车电脑上得出一个代码,再查表 就知道哪个元件坏了——这就是 DTC。除了用于修车,DTC 还被用于检测行车电脑和各传 感器的异常状态。 可以想象,这个既是运动员又是裁判的 Task X 一旦死亡,软件层的检测措施大部分就 失效了。 防护措施 3。在这里丰田的错误开始到达顶峰。即使设置正确无误,上面提到的监视 Task 也只不过是另一个 Task 而已,与它的监视对象算是平级——监视 Task 自己同样有可 能出现故障。嵌入式系统的一般做法是在所有程序之上再设置一道屏障,被称为“看门狗 (Watchdog)”。所谓看门狗,是一个优先级很高的倒计时程序。别的程序需要在运行的 时候特意去重置一下这个计时器让它重新开始倒计时,这个动作被称为“喂狗”。如果因为 程序出问题太长时间不喂狗,倒计时完成,看门狗知道什么地方卡住了,马上采取措施,比 如重启整个系统。重启系统听起来似乎很严重,实际上却是一件相当普通的事情。嵌入式系 统的重启非常快,时速 100 公里的汽车中动力系统可以在半米之内完成重启——车上的人 根本觉察不到。 通过阅读代码和拟真实验,Barr 惊讶地发现上述嵌入式系统的常识性做法竟然在丰田 软件系统内不存在!丰田的软件确实有一只看门狗,但它竟然不是用于监视 Task 异常,而 是用于防止 CPU 过载。首先这个做法不能说后无来者至少算是前无古人。还记得上面提到 的 800 页 13 章的报告吗?目瞪口呆的 Barr 将丰田看门狗的分析结果写入了报告的第一章, 因为他实在太震惊。其次,丰田看门狗的防止 CPU 过载功能也相当蹩脚,在拟真测试发现 即使它正常工作,还是会允许 CPU 过载时间长达 1.5 秒——时速 100 公里的车能跑 40 米 以上。CPU 一旦过载,就会导致所有的 Task 进入一种“假死”状态,无法处理信息,这时 司机无法控制汽车动力,十分危险。 另外,丰田的工程师还犯了一个嵌入式课堂上被反复提到的经典错误:使用硬件时钟中 断喂狗。硬件中断拥有非常高的优先级,即使 Task 卡住(比如出现死循环)也不能阻止硬 件中断——可想而知这样一来看门狗就等于完全白瞎了。 7 / 12
这里也提一句,同年的普锐斯却令人意外地搭载了一只运作正常的看门狗,反而让人摸 不着头脑。 还没完。这一层防护是嵌入式系统的关键阵地。前面都是电子系统,后面马上进入机械 运作,足以造成灾难了。所以仅仅拥有软件级别的防护还不足够,丰田的做法是在主 CPU 之外单独设置了一块监视芯片,从硬件级别对系统的运作进行监视。监视芯片有两个任务。 第一,它运行一种叫做系统卫士(System Guard)的程序,原理上来说是专门用于防止暴 冲。主 CPU 和监视芯片上都会运行系统卫士,可是研究发现 Task X 一旦死亡,这些系统 卫士统统都不起作用了。第二,它运行一个被称为“刹车回声检查(Brake Echo Check)” 的程序。这个程序从代码上来看似乎可以检测出 Task X 的死亡,并且采取相应措施:关闭 节气门。听起来像是好消息,但是同样有问题:首先这个程序不太可靠,即使正常运行,理 论上也有失效的可能。最关键的是该程序不会自动运行,需要司机先对刹车踏板有“动作” 才会触发。注意这里我特意没用“踩刹车”这个词,因为根据分析“触发动作”十分令人困 惑。它分两种情况:如果 Task X 死亡的那一刻司机的脚不在刹车踏板上,那么触发动作是 踩刹车。还算可以理解。另一种情况,如果 Task X 死亡那一刻司机的脚踩在刹车踏板上, 那么触发动作是完全释放刹车踏板。没错,察觉车子在不正常加速的司机需要停止踩刹车才 能让控制系统关闭节气门!这种违背人类认知的行为应该不是丰田工程师特意设计的。如果 是,他们到底在想什么啊? 到此为止,上面提到的都算是“战术层面”的错误,都是“小错”。在讲解这块监视芯 片的时候,可以发现丰田犯下最严重的“战略层面”错误——基础设计。Barr 认为,如果 基础设计正确,上述那些小错都完全不会导致汽车暴冲——不管代码写得多业余,不管内 存错误多严重,不管 Task 死得多频繁,统统不会致命。让我们回到 2002 年以前,没有电 子油门的时候。那时候的拉线油门是由油门踏板机械连接的。当驾驶员的右脚踩下刹车,他 的右脚必然不在油门踏板上,节气门自然而然地被关闭。这个动作如此自然,甚至算不上安 全措施,仅仅因为每人只有一只右脚,不可能同时踩油门和刹车。当丰田设计电子油门的时 候,只要稍微有点常识,都应该从设计阶段就将这一“自然而然”发生的动作考虑进去。但 是很显然,他们没这么做。监视芯片上运行的代码是用汇编语言(一种更加接近机器执行代 码,远离人类语言,更加难懂的编程语言)编写的,运行层次比主 CPU 的 C 语言更低。Barr 认为如果设计得当,现有的监视芯片完全有能力胜任上述功能,需要的仅仅是几百行代码, 别的什么都不用更改——不会提高任何生产成本。很遗憾,他们没有做到。 8 / 12
分享到:
收藏