更新空中引导加载程序(OTA)的注意事项

2020-08-31 23:51:02

现代电子设备越来越复杂,而且联网。一般来说,复杂性与安全性是背道而驰的,不安全的联网设备已经成熟,罪犯可以滥用。在设计这些系统时,我们必须假设所有软件都会有错误,其中一些错误会成为可利用的漏洞。解决这些问题的第一步是确保软件更新可以自动和无线(OTA)传送到您的系统。在欧盟“消费物联网的网络安全:基线要求(ETSI EN 303 645)”标准草案中,特别将及时的自动更新作为其要求之一。对于不变的第一阶段引导加载程序,它确实是一个例外,以最大限度地降低将现场设备留在非引导状态(也称为“砖块[1]”板)的风险。

本文将讨论在连接的设备中更新引导加载程序的问题。请注意,虽然这里讨论的原则适用于任何软件系统,但我们将专门讨论运行Linux的系统。使用更小、更定制的系统可能会给这些系统提供更多独特的选择。

图1显示了一个通用的Linux系统,其主要组件用于可能的更新。存储介质将是某种类型的块设备,如eMMC或SATA硬盘驱动器。在该设备中,将有引导加载程序、内核、设备树(取决于使用的CPU)和包含构建系统所需的所有文件的根文件系统。在某些情况下会使用更复杂的体系结构,但为了讨论的目的,我们将其限制在最简单的情况下。

诸如Mender[2]、swupdate[3]等系统更新实用程序能够现成地更新内核、设备树和根文件系统,并且在许多情况下,这种级别的可更新性就足够了。

引导加载程序是负责在开机时初始化系统的系统组件,从CPU重置指令开始。它负责以下任务:

正如前面所讨论的,所有软件都有错误,所以我们可以假设也会有引导加载程序错误。我们可以通过最小化引导加载程序的功能来减少攻击面,但是我们可以完全消除bug的风险。为什么更新引导加载程序比更新系统的其他组件更复杂?如果我们试一试,风险有多大?如果我们不试一试,会有什么风险呢?

此框图显示了能够进行健壮的空中(OTA)更新的系统的基本系统设计。[4]引导加载程序负责系统初始化并与OTA客户端交互,以选择要使用的内核、设备树和根文件系统。通过完全冗余运行的Linux映像所需的组件来提供健壮性。这可确保在OTA更新中断的情况下始终有已知良好的映像可回滚。此外,这可确保完全原子更新,因为在更新完成并准备运行之前,更新客户端是系统中唯一知道更新正在进行的组件。

任何更新OTA的组件都可能导致设备无法正常工作,因此系统的健壮性与引导加载程序处理回滚到先前已知良好配置的能力直接相关。这意味着系统中必须有一个不可变的组件才能正确处理错误更新。

在大多数情况下,处理Rollback_is_the bootloader的不可变组件。在典型的嵌入式Linux应用程序中是Das U-Boot[5]。如果我们尝试更新引导加载器,由于没有冗余,我们就会有损坏主板的风险。如果在我们开始写入新的引导加载器映像之后,但在写入完成之前,主板的电源重新打开,则我们的映像包含旧版本的一部分和新版本的一部分。这种情况下的行为是未定义的,唯一的缓解措施是能够物理访问设备,以便写入正确的引导加载程序,通常使用USB或其他硬连线连接。

但是我们为什么要更新引导加载器呢?至少,引导加载器仅仅被用作初始化硬件然后将控制权移交给Linux内核的一种手段。由于功能有限,引导加载程序出现问题的风险最小。

对于许多设计来说,这种风险级别是可以接受的,架构师可以决定在其部署的设备中干脆不提供OTA引导加载程序更新。在万不得已的情况下,仍然可以使用硬连接机制。

然而,对于许多设计来说,这种级别的风险被认为是不可接受的,必须为引导加载程序的OTA更新提供某种机制。此外,许多设计在引导加载程序中添加了更多功能;诸如系统诊断或其他特定于应用程序的要求等内容可能会在引导加载程序中实现,从而导致更有可能需要更新。那么我们该怎么处理这件事呢?

有许多选项允许更新引导加载程序。本讨论并不是一个完整的解决方案,而是对可能适用于您的设计的方法的高级描述。每种方法都有其权衡之处。

如果某个特定的应用程序可以接受砖板的风险,那么您可以简单地尝试部署引导加载器更新OTA,并且只需处理发生这种情况时的后果。如果您的机群规模较小,并且物理访问设备的成本较低,则此方法可能运行良好。如果需要进行引导加载程序更新,并且OTA尝试失败,则您的情况不会因尝试而变差。OTA引导加载程序更新失败的情况与没有OTA引导加载程序更新功能的情况相同。即,您必须获得对设备的物理访问,并使用制造商提供的用于重新刷新引导加载程序的机制。

此体系结构将引导加载程序功能分成两个阶段(或更多阶段,具体取决于设计的复杂程度)。*最终,这仍需要阶段1中的一段不变代码。在更新阶段2时,您确实具有冗余性和健壮性,因此,如果您仔细选择在何处实现功能,则可以提供引导加载程序功能的OTA更新。这是一个很好的选择,因为不变的阶段1二进制文件中的代码量减少了,从而降低了总体风险。

U-Boot使用SPL(二级程序加载器)和TPL(第三级程序加载器)实现多阶段启动。引入此机制是为了支持具有单独启动ROM的系统,这些启动ROM太小,无法存储完整的U-Boot映像。在这种情况下,U-Boot SPL映像将包含足够的初始化代码来加载和启动完整的U-Boot映像,通常是从大型块设备(如MMC)加载和启动。SPL需要能够初始化足够的RAM和设备

即使对于没有小引导ROM限制的设备,我们也可以利用此体系结构在阶段2中实现我们的可更新功能,同时在阶段1中保留最低限度的功能,包括正确处理冗余块。

第1阶段可能会出现问题,需要物理访问才能解决。考虑到阶段1中减少的功能,在许多情况下,此级别的风险是可以接受的。

许多主板提供从多个设备引导的功能。例如,许多主板可以从板载eMMC或可拆卸SD/MMC卡启动。或者,他们可以将专用的NOR闪存设备用于引导加载程序,但仍然可以从eMMC块介质运行引导加载程序。

这些类型的板可以配置为在其中一个支持的设备中存储不可变的引导加载器,然后在另一个设备中存储OTA可更新的引导加载器。通常,可更新的引导加载器将与根文件系统位于相同的介质(即eMMC)中,这使得更新非常容易。由于“备用”介质中的引导加载器是不可变的,因此可以依赖它从“标准”位置的引导加载器的损坏的OTA更新中恢复。

此方法的问题在于,选择引导设备通常需要物理访问电路板以移动跳线或更改交换机设置。如果您的设备位于最终用户可以访问它们的位置,这可能是一个可行的选择,因为最终用户可以在出现故障时选择恢复介质。这可以通过文档或支持人员的指导来完成。

有些系统使用外部硬件来选择引导加载程序。运行RTOS的小型MCU可以监控正确的系统活动,并在Linux系统未运行的情况下选择备用引导加载程序。*这可能很难使用外部源正确检测,但看门狗定时器触发GPIO引脚或写入共享内存可能就足够了。*这也是一个更复杂的设计,需要根据您的系统要求进行考虑。请注意,您可能需要考虑对MCU固件映像进行OTA更新,这是另一个复杂级别。

EMMC[6]规范的4.3版需要2个独立的硬件引导分区。这些分区通常每个4MB,用于存储引导加载程序。这些分区可以从Linux用户空间读取和写入,但是默认情况下它们是只读的;读写功能是通过写入/sys伪文件系统中的文件来启用的:

EMMC设备用作引导块的分区由设备自身设置的参数确定。可以从U-Boot提示符执行此操作:

利用eMMC引导分区,对分区的更新是原子的,并且独立于对根文件系统的更新。EMMC引导分区之间没有自动故障转移,因此这并不能缓解由于引导加载程序更新失败而导致的砖块设备的问题。但是,这确实使您可以轻松地仅向引导加载程序提供更新,而无需对根文件系统进行任何特定调整。

由于提供引导加载程序更新时存在积木板的风险以及OTA更新过程的健壮性降低,Mender[7]不提供开箱即用的引导加载程序更新。*如前所述,这很难以通用方式完成,并且很可能最终会非常特定于应用程序和硬件。Mender Update Module框架[8]允许插件体系结构支持自定义更新类型。任何任意有效载荷类型都可以由Mender使用自定义更新模块来支持。此插件体系结构允许提供处理特定有效负载类型的自定义脚本。允许在特定系统中进行引导加载程序更新可以使用更新模块来实现。根据应用程序的需要以及正在使用的硬件的功能,可以使用上面讨论的任何方法。

在现场部署的设备中上传系统引导加载程序有很多风险。不合时宜的停电可能会导致现场设备被砖封,导致潜在的代价高昂的召回过程。但是,不提供引导加载程序更新机制可能会带来不可接受的风险,具体取决于特定应用程序的配置文件。我们介绍了许多允许引导加载程序更新的方法,并讨论了每种方法的优缺点。作为一名系统设计人员,这有望让您为您的系统做出适当的选择,并帮助您在正确理解设计风险的情况下快速进入市场。