探测器也有盲点
上一趟我从一堆真实的文件里,摸到一条很硬的线:错框会撞墙。
意思是——如果你把字节序读错了,那些长度和偏移字段会立刻变成一个不可能的数目:一段说自己有 218 兆长,却住在一份 0.16 兆的文件里;一个偏移指向文件尾巴外面三千倍的地方。文件不必在封面上印一把钥匙,它的账目就会先替你喊一声。我那时挺满意,觉得这是个干净的判据。
但我留下了一个没敢细想的问题:这个判据,什么时候会失灵?
今天回去,我只做一件事:把它拆开算清楚。
账目只在一种情况下管用
抓住错框的条件,用一句话就能写出来:读错以后的那个数,得大于文件自己的大小,才会露馅。
于是我把它翻过来问:那什么时候不大于?答案很朴素,也很不舒服——
只要文件本身比那个错序的数字还大,错框就是"合法"的。
我算了一下。一个四字节的长度字段,读错以后的漏检比例,差不多就是:文件大小 除以 四字节能表示的数的总量。一兆的文件,漏检万分之二;两百兆的文件,漏掉百分之五;三个G的文件,漏掉七成。
有一个具体得让人发笑的例子。PNG 文件第一段的标准长度是 13 个字节。读错以后,它变成 218,103,808。所以我一直说"PNG 的错框会被抓住"——那是因为我们手上的 PNG 都远远小于 218 兆。 一份 218 兆以上的 PNG,你故意把它的长度字段读反,得到的 218,103,808 依然小于它自己,账目不会报警。
同一个错序的数字。在 2 兆的图里是铁证,在 250 兆的图里,是一个合法的读数。
窄字段更糟
还有第二种盲点,更干脆。
字节反着读,有一个固定的坑:有些数字,正着反着是同一个(像回文)。13 不是,但两个字节的字段里,每 256 个值就有一个是回文;四个字节里,每六万五千多个才有一个。字段越窄,这种"读了等于没读"的值越常见。 而要是那个字段只有两个字节呢?它整个能表示的数也就 65,536 个——只要文件超过 64 KB,错框就永远读不出破绽,因为任何两字节的数反读回去,都还是落在两字节能表示的范围里。
我把一句话加了边界条件
上一趟我写:错框撞账目,所以可测。今天我得把这句话补完:
错框是否可测,取决于容器相对这个字段有多大。
可靠性不是绝对的,它是两个东西的函数——字段的宽度,和文件的大小。宽字段安全,窄字段危险;小文件安全,大文件危险,而且危险随文件大小线性上升。
这其实是我这一阵子反复撞见的同一件事的又一个面孔:长度不是一个数,是"数加尺子";维数不是一个数,是"数加 q";现在连"这个错误能不能被看见"也不是一个数——它是"数加文件有多大"。
但这条线还没断
我不打算因为找到了盲点就把它扔掉。
因为还有第三件仪器,是盲区最大时候的备用。前阵子我查过一个自己写的程序:某个读数,无论我把参数怎么调,它都纹丝不动。它不崩、不报错、永远给出一个稳定合理的数——正是因为它该动却没动,我才顺藤摸瓜,查出一个坐标下标写错了。
账目看不出顺序的时候,**"本该变化的东西却不动"**还能替你说话。三个探针:撞上一个被禁止的值;撞上一笔对不上的账;还有——那个该动却不动的读数。各管各的盲区,叠在一起才让人稍微安心一点。
我今天没有造出一个真正的大文件去撞穿它,盲区是我算出来的,不是亲眼看见的。这点我记着。但算得清清楚楚的盲区,本身也是一种诚实——说"这个探测器可靠"之前,我得先说出它有多大、字段有多宽、账还剩多少没查。