2. 故障模型与分类:搞懂敌人长什么样
做故障注入,第一步不是拿工具乱打。你得先搞清楚——你到底要模拟什么故障?
我刚开始带团队做安全测试时,犯过一个低级错误。我们对着一个嵌入式系统狂刷位翻转,结果啥也没测出来。后来才发现,那个系统根本没用SRAM,位翻转打上去等于挠痒痒。嗯,从那以后,我养成了一个习惯:先画故障模型,再动手。
说白了,故障模型就是给系统“画像”。你想想看,一个系统可能出问题的地方太多了——硬件、软件、网络,每个维度都不一样。我们得把它们分类,然后逐个击破。
2.1 硬件故障模型
硬件故障,我习惯叫它“物理世界的意外”。芯片在太空里被高能粒子打中,或者电路老化后信号卡住,这些都属于硬件故障。常见的模型有三种。
2.1.1 单粒子翻转(SEU)
单粒子翻转,听起来很玄乎。其实就是一个高能粒子穿过芯片,把存储单元里的0打成了1,或者1打成了0。我在做卫星通信设备测试时遇到过这玩意儿。设备在实验室跑得好好的,一上高空就随机重启。后来定位到是FPGA里的配置寄存器被粒子打翻了。
SEU的特点是:
- 瞬态性: 故障只持续一个时钟周期,不会永久损坏硬件。
- 随机性: 你不知道它什么时候来,打在哪个位置。
- 累积性: 多个SEU叠加,可能引发系统崩溃。
2.1.2 位翻转(Bit Flip)
位翻转和SEU很像,但范围更广。SEU特指单粒子效应,位翻转可以是任何原因导致的比特错误——电磁干扰、电压波动、甚至时钟抖动。
我曾经在一个工业控制项目里,发现PLC的通信数据偶尔会多一个字节。查了三个月,最后发现是电源纹波太大,导致串口芯片的移位寄存器偶尔翻一位。这就是典型的位翻转。
位翻转的测试要点:
- 定位: 明确翻转发生在哪个存储区域(代码区、数据区、配置区)。
- 频率: 单次翻转还是连续翻转?连续翻转往往更致命。
- 时机: 翻转发生在读操作前还是写操作后?效果完全不同。
2.1.3 卡滞故障(Stuck-at Fault)
卡滞故障,就是信号线永远停在0或1,动不了了。这通常是物理损坏导致的——焊点脱落、线路断裂、芯片烧毁。
我记得有一次,一个客户反馈说设备上电后屏幕一直显示全白。我远程看了半天,最后让他们量了一下数据线的电平。结果发现有一根线始终是3.3V,卡在逻辑1了。这就是典型的Stuck-at-1故障。
| 故障类型 | 持续时间 | 典型原因 | 测试难度 |
|---|---|---|---|
| 单粒子翻转 | 瞬态 | 高能粒子、辐射 | 中 |
| 位翻转 | 瞬态/间歇 | 电磁干扰、电源噪声 | 中 |
| 卡滞故障 | 永久 | 物理损坏、老化 | 低 |
2.2 软件故障模型
软件故障,说白了就是代码里的“坑”。这些坑平时不显山不露水,一旦触发,系统可能直接崩给你看。
2.2.1 内存泄漏
内存泄漏,我见过太多案例了。一个后台服务,刚启动时只占50MB内存,跑了一周后变成2GB。这就是典型的内存泄漏——申请了内存,忘了释放。
我曾经帮一个团队排查过线上事故。他们的网关服务每处理一万条消息就重启一次。我一看代码,发现每次处理消息时都new了一个对象,但只在异常分支里释放了。正常路径下,对象永远留在堆里。这就是泄漏。
内存泄漏的注入方式:
- 模拟泄漏: 在代码中插入malloc/new,但不调用free/delete。
- 加速老化: 通过脚本反复调用泄漏函数,观察系统何时OOM。
- 压力测试: 在高并发下触发泄漏路径,看系统能否自愈。
2.2.2 空指针
空指针,可能是程序员最熟悉的噩梦。你想想看,一个对象明明是null,你还去调它的方法——不崩才怪。
我习惯把空指针分为两类:
- 显式空指针: 代码里直接写了null,然后忘了判空。
- 隐式空指针: 函数返回了null,调用方没检查。
测试时,我会在关键函数入口注入null参数,或者让某个API返回null。看看系统能不能优雅处理。很多系统在正常路径上跑得飞起,一遇到null就原形毕露。
2.2.3 竞态条件
竞态条件,是并发编程的头号杀手。两个线程同时访问同一个资源,顺序不同,结果就不同。这种故障最难复现,也最难定位。
我记得有一次,一个金融交易系统在低负载时完全正常。一到交易高峰期,偶尔会出现账户余额对不上的情况。查了两个月,最后发现是一个全局计数器在并发写入时没有加锁。两个线程同时读到了同一个值,然后同时加1,结果只加了1次。
竞态条件的注入方法:
- 延迟注入: 在临界区代码前插入sleep,放大时间窗口。
- 线程调度干扰: 用工具强制线程在特定点切换。
- 资源竞争: 让多个线程同时申请同一把锁或同一块内存。
2.3 网络故障模型
网络故障,是分布式系统的噩梦。你代码写得再好,网络一抖,全白搭。
2.3.1 丢包
丢包,就是数据包发出去,对方没收到。原因很多——线路噪声、缓冲区满、路由拥塞。
我做过一个视频会议系统的测试。在实验室里,画面清晰流畅。一部署到客户现场,画面就卡成PPT。后来发现是客户的网络丢包率高达5%。我们的系统没有做丢包重传,结果就是画面花屏。
丢包测试的常用参数:
| 丢包率 | 典型场景 | 系统表现 |
|---|---|---|
| 0.1% - 1% | 有线局域网 | 轻微延迟,TCP可恢复 |
| 1% - 5% | Wi-Fi、4G网络 | 明显卡顿,UDP应用受影响 |
| 5% - 20% | 卫星通信、弱信号 | 连接中断,重传风暴 |
2.3.2 延迟
延迟,就是数据包在路上走得太久了。你发一个请求,等了两秒才收到响应。这在实时系统里是致命的。
我曾经测试过一个远程手术机器人。控制指令从医生端传到机械臂端,延迟超过100毫秒,医生就感觉“手不是自己的”。我们通过注入不同级别的延迟,找到了系统的临界点——150毫秒。超过这个值,手术精度就无法保证。
延迟注入的维度:
- 固定延迟: 每个包都延迟固定时间,模拟长距离传输。
- 抖动延迟: 延迟时间随机变化,模拟网络波动。
- 不对称延迟: 上行和下行延迟不同,模拟不对称网络。
2.3.3 乱序
乱序,就是数据包到达的顺序和发送的顺序不一样。TCP协议会帮你重排,但UDP不会。如果你的应用依赖数据顺序,乱序就是灾难。
我记得一个流媒体项目,播放器偶尔会出现画面撕裂。查了很久,发现是CDN节点把视频帧的包顺序打乱了。播放器收到的是[帧1, 帧3, 帧2],结果帧2被当成未来帧丢弃了。
乱序测试的关键点:
- 乱序深度: 最多允许几个包乱序?深度越大,对应用影响越大。
- 乱序模式: 是随机乱序还是固定模式?随机乱序更难处理。
- 协议依赖: TCP应用通常不受影响,UDP和QUIC应用需要特别关注。