在2.11BSD恢复项目期间发现的修补程序中存在35年的错误

2020-08-18 00:26:40

Larry Wall在1985年5月8日向mod.source发布了1.3补丁。多年来,许多版本相继问世。很长很长一段时间以来,它一直是一条忠实的小巷。在我开始2.11BSD修复项目之前,我从来没有遇到过补丁的问题。在非常仔细地检查日志的过程中,我发现了一个错误,它会两次破坏这一努力。这是相当有趣的是使用27年前的补丁来发现这个错误,同时恢复一个29年前的操作系统…。

经过仔细研究,这是一个相当隐蔽的漏洞,是由20世纪80年代的电子邮件状态引起的一个奇怪的边缘案例。可以将其归入历史的垃圾箱……。

为什么没有其他人注意到这个错误呢?嗯,只有当我们重新处理文件中的最后一个补丁程序块时才会发生这种情况,并且该补丁程序块只删除行,而新型上下文diff省略了替换文本(因为这是隐含的)。哦,你还得做-R补丁吗?那很难懂,是吧?

我在2.11BSD系列的以下补丁(补丁107)中找到了它。结局是这样的:

这看起来相当例行公事和平淡无奇,不是吗?然而,当用户试图反向应用(-R)补丁代码时,它会与补丁代码中的一个非常老的bug发生冲突。我得到了以下输出:

这看起来很奇怪。为什么它要抱怨一条不在那里的队伍呢?为什么它会在早些时候错误地应用补丁6行?它认为它成功了,但实际上还为时过早地重新添加了MAX宏行。

在调试时,我很快发现反向补丁文件看起来很奇怪(补丁将在.rej文件中为您生成它)。

注意空行,它们稍后会变得重要。他们不应该出现在那里。补丁程序的开始应如下所示:

所有的东西都紧紧地贴在一起。这是我们关于哪里出了问题的第一条线索。因为这只适用于反向补丁,所以我们需要确保PCH_SWAP正在做它应该做的事情。当-R标志被赋予重写补丁的规格化形式时,它是触及内部表示的东西。

设置断点显示PCH_SWAP正在产生垃圾输出,因为它正在接收垃圾。出于某种原因,这3个额外的空行进入此例程以进行交换。因此,这不是反向补丁的错误。这很好:这个错误没有,但如果它不是补丁文件中的最后一个块。

那么,插入这些空行是什么意思呢?*稍作调试后,我们将看到pch.c中的另一个_hunk()中的以下代码(在FreeBSD中,其他实现与此类似):

这有点难以理解,但它基本上是说,如果pgets()返回0(它在文件末尾返回0),那么我们将尝试退出。如果p_max-p_end<;4,则插入空行。否则,如果我们已经开始查找替换文本,它将假定替换文本丢失,并且它可能会丢失。相当直截了当。

在原始修补程序中解析";-59,61--";行时,将p_max设置为另一个_hunk()中其他代码中修补程序的最大可能范围。在本例中,p_max为9,p_end为6(设置为p_end+61-59+1)。对于普通的差异,我们预计这里会有额外的3行上下文。但我们没有这种不同之处,因为它们被省略了。

那么,为什么要在上面引用的代码中的第二个';if';中出现';4&39;呢?它有什么神奇之处?事实上,如果我们把补丁改成6行上下文,而不是3行,它就能正确应用。那是怎么回事呢?如果我们删除整个IF,补丁也会正确应用。因此,这是一个可能的解决办法,但我们这样做会失去什么呢?

如前所述,如果我们完全删除第二行,并将其替换为';Else';子句中的行,补丁就会应用。现在我需要证明去掉if就行了。另一种解决方法是,如果p_end!=repl_starting应用启发式方法,否则就不应用启发式方法。然而,我认为这个解决方法更糟糕,因为整个IF并不需要。

我能找到的最老的补丁版本是拉里·沃尔1985年5月8日发布给mod.source的1.3补丁。它是旧的Usenet层次结构中的源代码(嗯,我猜现在都是旧的了,所以可能是重组前的层次结构)。文件中的SCCS注释表明它是在前一年的圣诞节左右开始的,但我找不到任何现存的版本。代码显然在那里:

虽然我不认为错误实际上咬了那个版本,因为它没有试图填补空白。1986年10月27日发布的2.0版本的代码与我们今天使用的代码非常相似:

因此,假设空白行被砍掉,实际上只与其他类型的补丁相关(我认为老式的上下文不同)。也许也可以仅针对旧式上下文和普通差异来修复此问题。然而,我认为这也是错误的解决办法。它是众多处理差异的补丁之一,从A到B会以某种可以预见的方式被扭曲,我们不再需要处理这些问题。

那么为什么要添加代码呢?我已经给拉里·沃尔发了一封电子邮件,但没有收到他的回复(他在Perl上声名鹊起,可能从1990年开始就不经常处理补丁问题了,所以我对他的回复不抱希望)。然而,如果没有这一点,我可以讲述我在20世纪80年代末在Usenet的有限经历,这些可能是相关的。电子邮件有时被许多作者视为通过非常昂贵的日期链接将文本从A点传送到B点的一种方式。因此,对为实现这些目标而发送的电子邮件进行微小编辑几乎没有什么愧疚之处。当时的共享程序认识到了这个问题,并在通过它们的文件中的所有行前面加上了X。一个常见的问题是导致空白被删除,这就解决了这个问题。邮件程序和邮件软件的其他问题包括在每一行回复的开头插入空格。补丁(1)本身通过尝试调整缩进的补丁文件来处理这种情况,方法是删除恰好足够的前导空格,以便从这些扭曲的影响中挖掘出diff的适用部分。补丁范围中的模糊和其他启发式的概念在一定程度上解决了这些困难。难怪除了所有这些问题之外,它还解决了几行尾随空格被删除的问题,破坏了补丁。

我们不再生活在一个补丁遭受如此敌意条件的世界。与其调整这个旨在处理BITNET、UUCP、SMTP、VMS、VM和任何数量的其他邮件程序的启发式方法来处理我的案例,我建议我们应该删除这个启发式方法,因为它不再相关。补丁文件不再受到这种级别的破坏。如果是这样的话,在补丁的末尾添加几行空行比破坏基本功能似乎是一个小得多的问题。这有可能打破没有格式良好的补丁。填充的新样式上下文差异会忽略此填充。补丁支持的统一差异和其他变体不需要此填充,并将忽略它。Ed;脚本不采用此代码路径。陈旧的风格背景差异在这些日子里是极其罕见的。

那么,是什么程序产生了所谓的老式上下文差异呢?我能找到的最早的差异是在4.0BSD中产生的上下文差异。修补程序为旧样式查找";*XX,YY&34;,但为新样式查找";*XX,YY*&34;。查看4BSD来源,我们发现它们产生了前一种风格。从4.2版到4.2版都包含此样式。从版本4.3BSD开始,产生了新样式。所以任何基于4.2BSD的系统都有旧的风格,而从4.3BSD开始的所有系统都有新的风格(包括gnu diff,它从来没有产生过我可以说的旧风格)。从那时起,所有的不同程序都产生了新风格的上下文差异(或者更短的新的统一差异)。4.3BSD是在1986年发布的,也就是补丁第一次发布之后,但在2.0之前,这说明了它对这两个变体的理解。

我已经在这里提交了FreeBSD的修复。对于我已经审查过的其他版本的补丁,采用它应该是微不足道的。

因此,我在2.11BSD版本发布时注意到的一个小故障导致我在补丁中发现了一个已经存在了35年的错误(并且在这些年中至少有34年是一个错误)。该错误是一种极端的边缘情况,它会触发对删除的尾随空行的启发式操作,这反过来会导致补丁程序反转时出现问题,但仅当它是补丁文件末尾的最后一个且仅删除行时才会出现问题。尽管如此,在我的职业生涯中发现并修复了35岁的错误,我认为我会写下这一点,这是很少见的。我用27年前的补丁发现了这一点,这也太疯狂了……