使用了存储键(storage keys)的 AIX Version 6.1 内核扩展
2008-09-06 08:19:27 来源:WEB开发网引言
内存覆盖和地址错误使非常难以诊断和处理的问题。随着软件的大小以及复杂程度不断增加,情况变得更为复杂。在 AIX® 操作系统中,许多软件组件共享内核地址空间。
现在,POWER6™ 处理器和 AIX Version 6.1 提供了存储保护键,其中可以使用内核扩展和设备驱动程序来提高系统的可靠性和服务能力。 在本文中,将介绍新的存储保护机制,以及如何利用存储保护键来提高现有设备驱动程序,或内核扩展的可靠性、可用性和服务能力 (RAS) 特征。
存储保护键
POWER6 处理器和 AIX Version 6.1 引入了一些新的功能,包括存储保护键以及 AIX 内核对它们的支持。在本文中,存储保护键也称为存储键,或者键。键 为虚拟内存页面提供了上下文相关存储保护机制。软件可以使用键来分别保护多种数据种类,并在各个上下文的基础上控制对数据的访问。这与以前的页面保护机制是不同的,以前的页面保护机制从本质上说是全局的。
在内核模式和用户模式应用程序二进制接口 (ABI) 中,都可以使用存储键。在内核模式的 ABI 中,将存储键支持称为内核键。在用户空间中,将存储键称为用户键。内核扩展本身必须关注这两种类型的键。
内存保护域通常使用多个存储保护键,以实现额外的保护。AIX Version 6.1 将系统划分为四个内存保护域:
公共内核 可供使用(不仅限于内核及其扩展)的内核数据,如堆栈段、bss 段、数据段,以及从内核或者固定的存储堆中分配的区域。 私有内核 AIX 内核中基本上私有的数据,如表示一个进程的相关结构。 内核扩展 主要由内核扩展使用的数据,如文件系统的 buf 结构。 用户 应用程序地址空间中的数据,可能使用键保护来控制对它自己的数据的访问。
这些不同域(除了公共内核之外)的一个目的是,保护一个域中的数据不受另一个域中编码错误的影响。在一定的程度上,您还可以保护一个域内的数据不受该域中其他子组件的影响。在内核扩展中编写存储保护功能时,您可以实现下面的部分,或者所有 RAS 优势:
保护用户空间中的数据,以避免被您的扩展意外地覆盖。
考虑在应用程序中使用私有用户键保护,以保护它自己的私有数据。
保护内核私有数据,以避免被您的扩展意外地覆盖。
保护您的私有内核扩展数据,以避免被内核、其他内核扩展,甚至您自己的内核扩展的子组件意外地覆盖。
存储保护键不应该作为一种安全机制使用。应该遵循一组自愿的协议来使用键,而协作子系统设计人员根据这些协议可以更好地检测各种编程错误,并随后对其进行纠正。
存储键保护的程度
内核扩展可能在不同的程度上支持存储保护键,这取决于其独特的需求。这些程度包括:
键不安全的内核扩展 键不安全的内核扩展不包含任何对存储保护键的显式支持。这种类型中的扩展是遗留 代码(过去编写的代码,没有考虑到存储键保护),它需要对所有内存进行无限制访问。
内核的职责是,确保遗留代码继续工作(就像在以前的 AIX 发行版中,以及没有提供存储键支持的硬件中一样地工作),即使这样的代码可能会访问内核私有数据。
键安全的内核扩展 键安全的内核扩展可以根据内核和用户域的边界,管理对内存的访问。它并不直接引用内核私有数据结构或者用户空间地址。要成为键安全的,扩展必须明确地选择它想要访问的现有内存域。这将保护系统的其余部分不受键安全模块中错误的影响。 键保护的内核扩展 键保护的内核扩展超出了键安全的范围;它标识并保护它自己的私有数据,以及其他域中的数据,以避免意外地访问。可以通过使用私有内核键、或者利用您已经使用的共享键,来完成这个任务。
键安全和键保护的内核扩展统称为可以识别键的 内核扩展。要使得一个内核扩展成为可以识别键的内核扩展,您必须了解键在内核中的使用情况。要使得一个内核扩展成为键保护的内核扩展,您还必须定义它的私有或者半私有数据,以及它如何使用键来保护这些数据。半私有键可能用于在几个相关的内核扩展中共享数据,而私有键则由单个扩展独自使用。
硬件体系结构
从 POWER6 处理器开始,支持使用键来实现硬件存储保护。这种硬件体系结构添加了两项新的内容:
每个虚拟内存页面都具有一个与其相关联的存储保护键。这个键是从 0 到 31 之间的一个整数,31 是所设计的最大值,当前各种计算机中的最大值都比这个最大值要小。
每个处理器都具有一个 64 位的权限屏蔽寄存器(Authority Mask Register,AMR)。AMR 由 32 对二进制位组成,每对二进制位对应于一个存储保护键。每对二进制位包含一个读取访问位和一个写入访问位,用以确定该处理器是否可以读取、或者写入使用关联的存储保护键进行保护的某些虚拟内存页面。
如果一个程序在执行,对于与正在引用的数据相关联的键,AMR 中不包含足够的权限,那么将出现数据存储中断(Data Storage Interrupt,DSI)异常。
只有很少的几个硬件键可以使用。内核提供了一种称为内核键的抽象,它映射到多对一的硬件键。有限的硬件键数目意味着,关于数据的、某些程度的错误共享 是不可避免的。因为许多内核键可能映射到同一个硬件键,所以这些内核键的用户之间没有提供相应的保护。
键不安全的内核扩展支持
为了继续在键保护的环境中运行,内核对一些遗留的内核扩展提供了特殊的支持。所有经过转换以使用键的扩展仍然处于可识别键的和键不安全的函数的混合环境中。可识别键的内核扩展可能调用键不安全的内核扩展中的服务。
当调用一个键不安全的函数时,事实上,内核必须在调用栈中(在调用函数和被调用的键不安全的函数之间)透明地插入特殊的粘合代码。虽然这个操作是自动完成的,但是我们需要了解相应的机制,因为所插入的粘合代码在进行堆栈回调跟踪时是可见的。
在调用遗留代码的时候,可以通过调用导出的函数进行直接地调用,也可以通过使用函数指针进行间接地调用,但是内核必须完成以下工作:
保存调用者的当前键访问权限(保存于 AMR 中)。
保存调用者的连接寄存器 (LR)。
使用授予广泛数据访问权限的值取代当前的 AMR 值。
继续调用键不安全的函数,使用所设置的连接寄存器,以便被调用的函数返回下面的下一个步骤。
恢复原始调用者的 AMR 和 LR 的值。
返回到原始调用者。
除了标准的 C 调用栈之外,还需要一个附加的堆栈,该堆栈必须不显示这种篡改的任何证据。这个新的资源称为 AMR 或者上下文堆栈。在活动的 kmstsave 结构中维护当前上下文堆栈指针,kmstsave 结构可以为线程或者中断上下文保存计算机的状态。使用 mst 内核调试器命令来显示这个信息。为键不安全的内核进程自动地固定上下文堆栈。setjmpx、longjmpx 和 frr_add 内核服务可以维护 AMR 和上下文堆栈指针。
当需要按照逻辑将上下文堆栈帧插入到标准的堆栈帧之间的时候,可以使用一个指示器标记受到影响的函数(事实上,是函数的回溯表)。调试器能够识别这个指示器,并且能够向您提供用于堆栈跟踪的完整信息。所插入的例程命名为 hkey_legacy_gate。对于许多指向内核的导出入口点,也可以应用类似的机制,其中您可以观察到 kernel_add_gate 和 kernel_replace_gate 的使用。
当调用导出的键不安全的函数时,这个处理将增加相应的开销,但是仅当被调用的函数位于调用模块外部的时候才会出现这种情况。通过函数描述符表示导出的函数,由加载器对这些描述符进行修改,以使得 AMR 能够将服务更改为前端 导出服务。模块间的调用并不依赖于直接调用的函数描述符,因此,不会受到任何影响。
在可识别键的内核扩展中,所有间接的函数指针 调用都将执行特殊的、常驻内核的粘合代码,该代码用于执行上述的自动 AMR 操作。如果您按照这个方式调用键不安全的函数,那么粘合代码将能够识别具体的情况,并帮助您对其进行相应处理。因此,不能使用粘合代码的 -q inlglue 内联选项来编译可识别键的内核扩展。
内核键和键集
可以根据具体的用途,将内核的数据划分为不同的内核键。内核键 是一种软件键,它允许内核创建数据保护类,而无需考虑可用的硬件键的数目。内核键集是内核键集合,以及对它们进行读写所需的权限的表示形式。请记住,几种内核键可能共享一个给定的硬件键。大多数内核键仅在内核中使用(有关完整的列表,请参见 sys/skeys.h)。表 1 显示了一些对于内核扩展开发人员来说有价值的内核键。
表 1. 有价值的内核键
KKEY_PUBLIC | 通常,这个内核键是访问程序的堆栈段、bss 段和数据区域所必需的。从 pinned_heap 和 kernel_heap 中分配的数据也是公共的。 |
KKEY_BLOCK_DEV | 这个内核键是块设备驱动程序所必需的。它们的 buf struct 必须是公共的,或者位于这个键中。 |
KKEY_COMMO | 这个内核键是通信驱动程序所必需的。CDLI 结构必须是公共的,或者位于这个键中。 |
KKEY_NETM | 这个内核键是网络和其他驱动程序所必需的,以便引用通过 net_malloc 分配的内存。 |
KKEY_USB | 这个内核键是 USB 设备驱动程序所必需的。 |
KKEY_GRAPHICS | 这个内核键是图形设备驱动程序所必需的。 |
KKEY_DMA | 这个内核键是 DMA 信息(DMA 句柄和 EEH 句柄)所必需的。 |
KKEY_TRB | 这个内核键是计时器服务 (struct trb) 所必需的。 |
KKEY_IOMAP | 这个内核键是对 I/O 映射的段进行访问所必需的。 |
KKEY_FILE_SYSTEM | 这个内核键是访问 vnode 和 gnode(vnop 调用者)所必需的。 |
因为完整的键列表可能会随时间的推移而发生变化,所以找出典型内核扩展所需键集的唯一安全的方式是,使用一种预定义的内核键集,如下面的表 2 所示。
表 2. 预定义的内核键集
KKEYSET_KERNEXT | 内核扩展所需要的最小集合。 |
KKEYSET_COMMO | 通信或者网络驱动程序所需要的键。 |
KKEYSET_BLOCK | 块设备驱动程序所需要的键。 |
KKEYSET_GRAPHICS | 图形设备驱动程序所需要的键。 |
KKEYSET_USB | USB 设备驱动程序所需要的键。 |
请参见 sys/skeys.h,以获取关于预定义内核键集的完整列表。这些键集通过它们的键,提供了对保护数据的读写权限。如果您只需要对这些键的读取权限,那么通过简单地追加 _READ(与在 KKEYSET_KERNEXT_READ 中一样)对这些集合进行命名。
从上述的集合副本中删除不必要的键,是可接受的,但并不是必需的。例如,KKEY_TRB 可能是不必要的,因为您的代码并不使用计时器服务。可以通过显式地添加您所需要的内核键,从头开始构建键集,但是我们并不推荐这种方式。键在内核中的使用很可能会随着时间的推移而发生变化,这可能使得您的键集无法满足将来的需求。内核扩展可能需要定义的任何新的内核键都将添加到上述的基本预定义键集中,从而确保您将自动地保存这些新的键。
保护门
通过前面所描述的机制,可以为键不安全的内核扩展授予广泛的数据访问权限,这种机制是一种保护门 类型,我们将其称为隐式保护门。如果您需要使内核扩展成为可识别键的内核扩展,那么您必须添加显式的保护门,通常在您的模块的所有条目和退出点处。这些门可以确保您的模块对它所需要的数据具有访问权限,而对它不需要的数据则没有访问权限。
如果在入口点处(或者附近)没有这样的门,那么代码将使用调用者当时持有的任何键来运行。我们不应该冒这个险。要使内核扩展成为可识别键的内核扩展,其中的一部分工作是确定其各种入口点的存储保护键需求,并使用各种显式保护门对它们进行控制。在一个入口点处,可以选择两种类型的门:
添加门 允许您使用自己的键集来扩充调用者的键集。
为服务选择一个添加门,在该服务中,调用者将传递指向数据(可能位于任意一个键中)的指针。因为可能使用了某个键对这些数据进行保护,而您并不知道这个键,所以您保留了一些键以确保可以引用该数据,这一点是很重要的,同时添加一些附加的键,以便您还可以引用任何所需要的私有数据。
替换门 您可以切换到自定义的键集。
可以在独立的入口点处选择替换门,例如,通常是已经使用设备切换表注册的回调。您的参数是隐式的,并且您需要找出引用这个数据所需的已知的键。
替换门也是非常重要的,它可以用于放弃调用者的键;通常在这些情况中,内核就是您的调用者,并且保持对内核内部数据的访问权限是不合理的。前面描述了预定义的内核键集,它们应该可以构成典型的替换门的基础。
在这两种情况下,保护门服务都将返回原始的 AMR 值,因此您可以在退出点处恢复它。
如果没有向直接调用的服务传递任何指向数据(可能位于任意一个键中)的参数,那么应该优先使用替换门而不是添加门,因为这是一种更强的保护形式。通常,在您的模块之内进行调用不需要这些门,除非您希望更改模块内的保护域,将其作为多键组件设计中的一部分。
在程序流程中的任何位置都可以放置保护门,但通常最简单地标识和放置这些门的位置是在您的模块的所有外部可见的入口点处。然而,通常也有一种例外的情况。在利用调用者的键来复制潜在的私有数据(传递到公共存储的数据)时,您可以暂时延迟该门,然后使用一个替换门切换到您自己的键集。与入口点处相对简单的添加门相比,这种技术可以产生更强的存储保护。当使用这一方法时,在可以通过参数指针将公共数据复制回来之前,您必须恢复调用者的键。如果您同时需要调用者的键和您自己的键,那么您必须使用添加门。
为了确定您的内核扩展的入口点,请确保考虑下面的典型的入口点:
设备切换表回调,如:
open
close
read
write
ioctl
strategy
select
dump
mpx
revoke
设备驱动程序的配置入口点通常是没有保护门的,但是它包括各种保护门、堆等等所需的初始化,以便将来能够由其他的入口点使用。入口点配置设备实例通常包含各种保护门。
可以在网络驱动程序中使用 struct ndd 回调,如:
ndd_open
ndd_close
ndd_output
ndd_ctl
nd_receive
nd_status
ndd_trace
Trb(计时器事件)处理程序
Watchdog 处理程序
增强的 I/O 错误处理 (EER) 处理程序
中断处理程序(INTCLASSx、iodone 和 offlevel)
环境和电源报警 (EPOW) 处理程序
导出的系统调用
动态重新配置 (DR) 和高可用性 (HA) 处理程序
关机通知处理程序
RAS 回调
转储回调(例如,使用 dmp_add 或者 dmp_ctl(DMPCTL_ADD,…) 进行设置)
流回调函数
进程状态更改通知 (Proch) 处理程序
将函数指针传递到您的模块之外
通常仅在为您的函数所引用的、非参数的私有数据设置相应的访问权限时,您才需要使用保护门。被调用的程序将负责确保引用它们自己的任何私有数据的能力。如果知道您的调用者的访问权限是足够的,那么就不需要使用保护门。
构造硬件键集
在确定了保护门的内核键需求之后,您需要构造一个硬件键集 (hkeyset_t),以将其传递给保护门服务。在这个多步骤的操作中,您需要完成以下工作:
构建内核键集 (kkeyset_t),通常根据一个导出的内核键集进行构建,如 KKEYSET_KERNEXT。
可能需要添加或者删除个别内核键访问权限。
将这个内核键集转换为硬件键集。
预先完成这项工作,将使您的保护门的开销实现最小化。内核和硬件键集的创建必须在完全支持的进程模式中完成。
为构造内核键集以及等价的硬件键集,提供了如下服务:
kkeyset_create 创建一个空的内核,随后可以将所需的内核键(其访问权限是您所需要的)放置于其中。例如:
#include <sys/skeys.h>
kerrno_t kerrno;
kkeyset_t kset = KKEYSET_INVALID;
kerrno = kkeyset_create(&kset);
kkeyset_add_set 我们推荐的下一个步骤(通常为了以后在替换门中使用)是,将某个导出的内核键集的所有键访问权限添加到您的新内核键集。如果不需要向您的键集添加任何单独的内核键、或者从您的键集删除任何单独的内核键,那么这个操作就可以完成您的内核键集的构造。例如:
kerrno = kkeyset_add_set(kset, KKEYSET_KERNEXT);
kkeyset_add_key 如果您需要访问特定的私有数据,那么将内核键的数据添加到您的键集。您可以选择添加读取、写入或者读写访问的键(使用所需的内核键传递 KA_READ、KA_WRITE 或者 KA_RW 标记)。例如:
kerrno = kkeyset_add_key(kset,
KKEY_BLOCK_DEV,
KA_RW);
kkey_assign_private 对于键保护的驱动程序,您可能希望选择至少一个唯一的私有内核键,并使用这个键(或者这些键)保护您的数据。可以通过基础操作系统中提供的内核扩展,使用唯一的名称(可以在 sys/skeys.h 中找到)来注册键。内核还可以识别 32 种预定义的私有键。最好通过调用 kkey_assign_private 服务,让系统为您挑选一个键。
kkey_t my_key;
kerrno = kkey_assign_private("myID",
0, /* instance # */
0, /* always 0 */
&my_key);
这个服务对作为第一个参数传递的字符串进行分析,以确定私有内核键的编号,然后加入实例编号。如果您需要两个私有键,那么您应该使用相同的 ID 字符串调用这个服务两次,第一次使用实例编号 0,第二次使用实例编号 1。在可能的情况下,kkey_assign_private 将尝试返回最有可能映射到不同硬件键的连续的键实例。
kkeyset_remove_key 使用这个服务,从您的键集中删除一个不需要的、众所周知的内核键。例如,如果您的扩展不需要使用计时器服务,那么可以进行下面的操作:
kerrno = kkeyset_remove_key(kset,
KKEY_TRB,
KA_RW);
kkeyset_remove_set 这个服务可以从一个键集中删除另一个键集中所有的键。
kerrno = kkeyset_remove_set(kset1, kset2);
kkeyset_to_hkeyset 这个服务将内核键集中定义的访问权限转换为硬件键集(采用与 AMR 中相同的格式)。稍后您将在保护门中使用这个硬件键集,因此,它应该位于全局可见的公共内存中(该内存通常包含在您程序的 bss 段或者数据区域中)。
hkeyset_t my_hset;
kerrno = kkeyset_to_hkeyset(kset, &my_hset);
kkeyset_delete 在创建了硬件键集之后,就不再需要保留在其构造过程中使用的内核键集了。这个服务将释放由 kkeyset_create 所分配的资源。
kerror = kkeyset_delete(kset);
内核键集中的数据是内核私有数据,由 KKEY_KKEYSET 进行保护。上面的服务是关于键保护设计的示例,其中使用保护门授予对 KKEY_KKEYSET 的访问权限。对您来说,内核键集的内容是不透明的,并且您并不希望通过所提供的服务之外的方式来访问这些数据。
保护门服务
使用下面的服务,以编写实际的保护门。您必须使用 XLC 编译器,以便正确地对调用这些服务的函数进行编译,这些服务可以通过使用 #pragma 指定的内联汇编程序语言来实现。
hkeyset_add 这个服务实现一个添加保护门,其中使用您所提供的硬件键集中的访问权限对 AMR 中现有的内容进行了扩充。
hkeyset_t old_hset;
old_hset = hkeyset_add(my_hset);
hkeyset_replace 这个服务提供了一个替换保护门,其中使用您所提供的硬件键集中的访问权限对 AMR 中现有的内容进行了替换。
old_hset = hkeyset_replace(my_hset);
hkeyset_restore 这个服务补充了上面两种服务的不足,可以在您的函数的退出点处使用它,以便使用上述服务的返回值,将 AMR 恢复到它在入口点处的值。
hkeyset_restore(old_hset);
与内核键集相同,创建和使用硬件键集的服务有意地隐藏了硬件键集的内部格式(尽管 hkeyset_t 实际上是64 位长整数,您可以获得它,并对其进行检测)。显示硬件和内核键集的最好的方式是,使用内核调试器。
没有删除门 服务。请记住,多个内核键可以共享一个硬件键,从 AMR 删除一个硬件键很可能会产生副作用,即同时删除许多内核键访问权限。从硬件键集删除内核键的唯一正确的方式是,从用于构造该硬件键集的内核键集中删除它。
内核调试器
内核调试器中包含一些新的和经过更改的命令,以便帮助您使用存储保护键,如表 3 所示。
表 3. 新的和经过更改的命令
kkeymap | 显示可用的硬件键以及映射到各个硬件键的内核键。 |
kkeymap <十进制内核键编号> | 显示指定的内核键到硬件键的映射(-1 表示该内核键没有经过映射)。 |
hkeymap <十进制硬件键编号> | 显示所有映射到指定硬件键的内核键。 |
kkeyset <kkeyset_t 的地址> | 显示由内核键集表示的内核键访问权限。这个命令的操作数是指向不透明内核键集的地址的指针,而不是内核键集结构自身。 |
hkeyset <64 位十六进制值> | 显示由这个值(如果在 AMR 中使用的话)表示的硬件键访问权限,以及所涉及的内核键的样本。 |
dr amr | 显示当前 AMR 和它所表示的访问权限。 |
dr sp | 包含 AMR 值。 |
mr amr | 允许 AMR 的修改。 |
dk 1 <eaddr> | 显示包含 eaddr 的驻留虚拟页面的硬件键。 |
mst | 显示 AMR 和上下文堆栈值。在 excp_type 中作为 DSISR_SKEY 指出存储键保护异常。 |
iplcb | 显示 CPU 的 IBM 处理器存储键属性,该属性说明了支持的硬件键的数目。这位于设备树的 /cpus/PowerPC 部分中。 |
vmlog | 显示存储键违规的 EXCEPT_SKEY 的异常值。 |
pft | 显示该页面的硬件键值(标注为 hkey)。 |
pte | 显示该页面的硬件键值(标注为 sk)。 |
scb | 显示段的硬件键缺省设置。 |
t | 堆显示内核以及与 xmalloc 堆相关联的硬件键。 |
键安全的内核扩展
如果您希望创建一个键安全的内核扩展,那么您应该完成以下操作:
确定哪一个导出的内核键集(如果存在导出的内核键集的话)应该作为您模块的键集的基础。
另外,从这个内核键集的副本中删除所有不需要的键。
将该内核键集转换为硬件键集。
在所有入口点(除了驱动程序的初始化之外)处或者附近,根据需要放置添加或者替换门,以获得各个入口点所需的特定数据访问权限。
在缺省情况下,您将不再拥有对用户空间的直接访问权限,并且您可能需要在以后处理该问题。
在退出点处或者附近,放置您的恢复门。
使用新的 –b RAS 标记对您的扩展进行连接,以便在系统中将其标识为可识别 RAS 的(可识别键的一个超集,因此您还需要在您的扩展中放置恢复障碍,稍候将对其进行描述)。
如前所述,无需指定内联指针粘合 -q inlglue。
如果您正在使用 v9 或更高版本 xlC 编译器进行编译,那么您必须指定 –q noinlglue,因为缺省值已经发生了更改。
当然,您的初始化或者配置入口点操作不能以保护门(必须首先计算其基础硬件键集)作为开始。只有在设置了所需的硬件键集之后,您才能够实现您的保护门。这些键集的计算应该仅进行一次(例如,在创建第一个适配器实例的时候)。这些都是全局的资源,由驱动程序的所有实例使用。在使用您的新保护门之前,您必须确保仅引用不受保护的数据,如您的堆栈段、bss 段和数据区域。
如果出于某种原因使得这项工作特别困难,那么您可以将您的硬件键集静态地初始化为 HKEYSET_GLOBAL。这个初始值允许您的保护门在您完成内核和硬件键集的构造之前就开始工作,尽管直到对硬件键集进行了正确地初始化之后,它们将向紧跟其后的代码授予对内存的全局访问权限。如果您的扩展需要接受数据并对数据进行排队,以便将来进行异步访问,那么您可能还需要使用 HKEYSET_GLOBAL,但是仅在您的调用者允许该数据使用任意的键保护时才需要这样做。应该严格地最小化全局键集的使用。
如果您希望确定不会意外地使用一个硬件键集,那么可以将其静态初始化为 HKEYSET_INVALID。使用这个硬件键集的替代门将撤销所有对内存的访问权限,并立即产生一个 DSI。
您的保护门可以对内核的数据和其他模块的数据进行保护,使其避免意外地覆盖(可能来自您的扩展)。要使您的内核扩展成为键安全的内核扩展,不需要更改模块的任何逻辑。但是,您的模块自身的数据仍然是不受保护的。接下来要做的是保护您的内核扩展。
键保护的内核扩展
要使一个内核扩展成为全面的键保护的内核扩展,这需要添加更多的操作。您现在还必须完成下面的工作:
分析您的私有数据,并确定您的结构中可能为键保护的部分。
您可能会决定将您的内部数据对象划分为多种类型(根据引用它们的内部子系统),并且使用多个私有键来实现这项工作。
考虑特殊服务为您分配的数据,可能需要您持有特定的键。
构造您的保护门所需的硬件键集。
考虑使用只读访问权限,实现额外的保护。例如,对于不受信任的函数可以使用的私有数据,您可能会切换到只读访问权限。
分配一个或者多个私有内核键,以保护您的私有数据。
构造一个堆(最好这样,或者使用另一种方法来分配存储),使用您分配的各个内核键对其进行保护,并在您现有的 xmalloc 和 xmfree 调用中一致地替换这个堆或者这些堆。
当进行替换的时候,请特别注意,您使用可分页的堆取代 kernel_heap 的使用,并使用固定的堆取代 pinned_heap 的使用。还请留意,始终将分配的存储释放回分配该存储的堆。您可以使用 malloc 和 free 分别作为 kernel_heap 中 xmalloc 和 xmfree 的简写,因此还请确保对它们进行检查。
了解您所调用的服务的键需求。对于某些服务,必须向它们传递公共数据才能正常工作。
您需要将一些单独的全局变量整理到一个结构中,可以使用键保护的方式对该结构进行 xmalloc。只有指向该结构和访问该结构所需的硬件键集的指针需要是公共的。
您所分配的私有键或键与其他内核键(甚至彼此之间)共享硬件键。这将影响到您可能实现的保护粒度,但是它并不影响您设计和编写代码的方式。您在编写代码时,只需要考虑它的内核键,而无需为了进行测试而考虑将内核键映射到硬件键。使用多个键需要额外的保护门,这些保护门在性能敏感的领域中可能是不合理的。
创建使用键的堆
在保护您自己的数据时,通常最容易的方法是,创建一个共享的、使用键的堆,以便使用 xmalloc。然后,您可以容易地分配由所选择的键进行保护的内存,处理由改变页面大小而引发的问题。与内核键集一样,您的堆是全局资源,仅需要在初始化或者配置期间创建一次。
使用一个创建的共享堆与使用现有的 kernel_heap 和 pinned_heap 一样简单,它们将返回不受保护的 (KKEY_PUBLIC) 内存。您不需要编写特殊代码以检查是否支持硬件键。如果它们不支持的话,您所创建的堆则仅返回公共存储。
可以通过填写 heapattr_t 来指定您所创建的堆的相关属性,这个结构将作为 heap_create 的参数进行传递。例如,使用在 kkey_assign_private 示例中确定的私有键,您可以创建两个堆:一个用以分配固定的对象,而另一个用以分配可分页的对象。例如:
#include <sys/malloc.h>
heapattr_t heapattr;
heapaddr_t my_pinned_heap = HPA_INVALID_HEAP;
heapaddr_t my_pageable_heap = HPA_INVALID_HEAP;
bzero(&heapattr, sizeof(heapattr));
heapattr.hpa_eyec = EYEC_HEAPATTR;
heapattr.hpa_version = HPA_VERSION;
heapattr.hpa_flags = HPA_PINNED | HPA_SHARED;
heapattr.hpa_kkey = my_key;
kerrno = heap_create(&heapattr, &my_pinned_heap);
heapattr.hpa_flags = HPA_PAGED | HPA_SHARED;
kerrno = heap_create(&heapattr, &my_pageable_heap);
上面创建的两个堆都是共享的堆,这意味着所有按这种方式创建的堆实际上都是从内核中相同的基础内存池分配而来的。
另一种备选的方法(稍微有点复杂)是使用私有堆。对于私有堆,指定 HPA_PRIVATE 标记,而不是 HPA_SHARED。在这种情况下,您将明确地为您的分配预定连续存储块。这时,将为您分配存储块,它的大小为您在 hpa_heapsize 字段中指定的大小。对于私有堆,所支持的最小大小是 8M。另外,对于可以从您的堆中分配的内存量,您可以使用 hpa_limit 为其设置更小的限制。例如,这将允许您随时改变在您的私有堆中可用的存储量,将其作为负载、联机处理器的数目等等因素的函数。具体的限制可能会发生更改,如下所示。
kerrno = heap_modify(my_pinned_heap,
XMHM_HEAP_LIMIT,
new_limit);
使用私有堆,您可以获得额外的灵活性。您所有的数据都将聚集在存储中,在系统转储中更容易进行查找,并且更不容易被内核的另一个部分意外地覆盖。要想有效地设置堆的大小,您必须了解所需的存储量。使用私有堆,还可能增加系统的内存空间占用。
对于使用键的堆和可识别关联的堆,它们的使用是互斥的。在向 xmalloc_srad 传递使用键的堆,并且启用内核键的时候,它将返回与指定的 srad 不具有特定关联的内存。在不支持键的系统中,xmalloc_srad 可以继续支持关联。(从 kernel_heap 和 pinned_heap 中分配的公共存储始终支持关联。)
可以对私有堆进行垃圾收集。从任何堆中分配的存储都是真实的资源,因此请记得在终止或者取消配置期间,清理所有已创建的堆。应该释放从一个堆中分配的所有存储,并销毁这个堆,可以使用下面的操作:
kerror = heap_destroy(my_pinned_heap, 0);
当调用 xmalloc 或者 xmfree 的时候,对于与您的堆相关联的键,您必须拥有当前读写访问权限。要使用 heap_create、heap_modify 或者 heap_destroy,并不需要持有这个键。
创建一个使用键的 ldata 存储池
与 xmalloc 堆一样,ldata 池可能具有与其相关联的存储保护键。
int ldata_create(size_t element_size,
long initcount,
long maxcount,
kkey_t kkey,
ldata_t *ldatap);
在早期的发行版中,第四个参数必须为零,但是现在,这个参数可以指定应用于稍后将分配的存储的内核键。如果您希望在您的池中继续拥有公共存储,那么您可以指定 KKEY_PUBLIC 作为 kkey 参数,或者简单地将其保留为 0。
使用键的 ldata 池仍然是可识别关联的。
当调用 ldata_alloc 或者 ldata_free 的时候,对于与您的 ldata 池相关联的键,您必须具有当前读写访问权限。当调用其他 ldata 服务的时候,不需要持有这个键:ldata_create, ldata_destroy 和 ldata_grow。
创建私有段
虽然前面所描述的 xmalloc 堆或许是更为方便的方法,但是您的内核扩展可能已经使用了分配的内存段。使用下面的操作,可以在您的段中设置一个缺省存储键:
kerrno_t vm_setseg_kkey(vmid_t sid, kkey_t kkey);
这个服务并没有为该段中已经分配的页面设置键。它为以后的分配设置缺省键。因此,最好在创建段之后,在其中存在任何页面之前,立即使用这个服务。只能够使用工作存储和 rmmap 段,但是它们可能包含一些任何所支持的大小的页面。
您还可以使用下面的操作,为工作存储段中按页排列的页面的完整块直接设置键:
kerrno_t vm_protect_kkey(void *addr,
size_t nbytes,
kkey_t kkey,
ulong flags);
标记参数通常是 0,需要保护对内存的写入访问。为绕过这个需求,指定 VMPK_NO_CHECK_AUTHORITY 标记。通过指定 KKEY_PUBLIC 公共键,内存可以是不受保护的。请记住,AIX 内核支持多种页面大小。
您不应该使用 vm_protect_kkey 来更改经过 xmalloc 或者 ldata_alloc 的页面的键。当这些页面经过 xmfree 或者 ldata_free,并且随后进行重新分配的时候,它们可能导致在不相关的代码中出现存储键违规,这种情况是很难以调试的。
访问用户空间
最好使用专门用于允许内核访问用户空间的服务,如 copyin、copyout、uiomove 等等。它们具有自己的保护门,这些保护门支持对用户内存的适当的访问。如果内核扩展代码直接附加到一个用户段,那么它必须获得适当的用户保护键以避免 DSI,因为在缺省情况下,AMR 中并不包括用户空间访问权限。用户空间应用程序可能正在使用保护键,记住这一点是很重要的。您并不希望您的内核扩展以违反用户的应用程序设计的方式访问受到保护的用户内存。
为了将当前活动用户键集添加到 AMR 中已有的内核硬件键集中,然后恢复您的状态,可以使用下面的操作:
kerror = hkeyset_update_userkeys(&old_hset);
kerror = hkeyset_restore_userkeys(old_hset);
前文确保您具有 AMR 中表示的当前活动用户键集。键保护是虚拟内存映射的部分;仅当使用自然映射寻址用户空间的时候用户键才有效。
这些服务只能由进程模式代码使用,因为活动用户键集仅适用于当前处于运行状态的线程。
交叉内存服务
对交叉内存描述符进行了增强,以使其包含 AMR 值,允许将键集绑定到一个缓冲区,并为您保护内存引用的下面半部分。通过下面的内容来应用该保护:
xmemin
xmemout
xmemzero
xmemdma64
xlate_pin
相关服务
您可以使用下面的操作,将所需的硬件键集添加到交叉内存描述符:
kerrno_t xmsethkeyset(struct xmem *, hkeyset_t, 0);
然而,如果您使用 vm_att、xm_mapin 或者 xm_att 执行临时的附加操作,那么您必须为所附加到的页面单独地激活您的键集。您可以使用 xmgethkeyset 以获取交叉内存描述符的键集,并使用一个普通的替换门来激活它。
通过指定新的 SYS_ADSPACE_ASSIGN_KEYSET segflag 参数,您还可以在 xmattach 时将当前 AMR 自动地放置到交叉内存描述符中。现有的 SYS_ADSPACE segflag 导致对内存的全局访问,而 USER_ADSPACE segflag 自动地使用活动用户键集。
在存储键不使用的时候
您不需要在内核扩展中设计两条不同的路径,以分别处理系统中存在或者不存在内核存储键的情况。所有键相关的服务都可以为您处理这个内容。在一个没有启用内核键的系统中调用保护门的时候,将根据 No OPeration (NOP) 说明动态地修补调用代码,这从本质上消除了编码门的开销。例如,来自 hkeyset_add 的返回值并不是先前的 AMR 值(如果启用了内核键的话,它应该是先前的 AMR 值)。将这个值传递给 hkeyset_restore 是没有任何危害的,因为没有进行调用。
您可以使用 sys/systemcfg.h 中的 __KKEY_ENABLED() 宏,来测试是否在运行时启用了内核键(如果需要的话)。
性能注意事项
保护门使得包含它们的函数添加了一定的开销,无论它们是由键不安全的扩展使用的隐式门,还是您用以使得您的扩展成为可识别键的扩展的显式门。
如果您仅仅通过添加最小限度的条目和退出点保护门来创建键安全的扩展,实际上与在启用键的系统中相比,它运行的速度要更快一点,因为显式门不使用上下文堆栈。在键保护的领域中,您必须在保护的粒度与开销之间进行权衡。例如,在一个循环中添加保护门,以实现对某些私有对象的精确访问控制,这可能导致难以接受的开销。如果有可能的话,在您的特定键保护的设计框架中,尝试避免这样的情况。
特殊注意事项
通常,您不需要更改内核扩展的功能逻辑以使其成为键安全的、或者甚至是键保护的,只需要添加保护门,进行重组,并将数据移动到键保护的页面。然而,还存在一些限制和注意事项值得进行说明:
当调用 xmalloc 分配保护的内存、或者调用 xmfree 释放保护的内存的时候,您必须持有对保护的内存进行访问所需的键。
当调用 ldata_alloc 分配保护的内存、或者调用 ldata_free 释放保护的内存的时候,您必须持有对保护的内存进行访问所需的键。
如果为内核堆配置了 16M 或者更大的页面,那么将禁用内核键。
大多数现有的接口可以接受键保护的存储中的参数,因为在进行调用的时候,您对保护的存储具有访问权限。因为函数调用不会影响 AMR,所以被调用的函数将继承这些访问权限。
然而,某些接口放置相应的存储(可以将链表传递给它们),比如在注册将来的回调时。将不受保护的存储传递给这样的例程,通常是很安全的,尽管已经对其中的许多例程进行了更新,以处理该问题。
既不设置 XMEM_ACC_CHK,也不设置 XMEM_WRITE_ONLY 的 xmemdma64 的调用者将禁用页面和存储键保护。
如果您从可识别键的内核扩展启动一个 kproc,那么假定该 kproc 是可识别用户键的。只能使用所持有的内核公共键来调用 kproc 的初始函数(使用 initp 进行设置),而不是使用该进程创建者的键集进行调用。对于使用 kthread_start 设置的初始函数来说,情况也是这样。
如果您固定(或者不固定)您的内核堆栈,那么您还必须固定(或者不固定)上下文堆栈。使用 pin_context_stack 和 unpin_context_stack 内核服务。使用不固定的上下文堆栈来启动可识别键的 kproc。
如果您切换到预固定的内核堆栈,那么您还必须固定上下文堆栈。您不能切换上下文堆栈。
vm_release 内核服务将页面的键重置为它的段的缺省值。
从内核空间调用的任何 ioctl 存储驱动程序(如 scsidisk_ioctl)必须传递一个 arg 结构,该结构要么是公共的,要么位于 KKEY_BLOCK_DEV 中。
可识别键的代码必须与内核数据的需求保持一致。在许多情况下,存在一个众所周知的内核键与这样的数据相关联。在访问由内核回调传递给您的保护对象时,您必须持有这个键。
您必须通过您所使用的服务所预期的键对分配的对象进行保护。您不能返回公共的条目来由子系统保护的分配器进行后续分配。下面的数据对象必须位于规定的键中,或者是公共的:
内核键 | 结构名称 | 函数接口 |
KKEY_TRB | trb | talloc,... |
KKEY_DMA | d_handle, eeh_handle | d_map_init, ... eeh_init, … |
KKEY_NETM | mbuf | m_get, … |
KKEY_BLOCK_DEV | buf | bread, bwrite, clrbuf, iodone, iowait, geterror, devstrat, ddstrategy, … |
dkstat | iostadd, iostdel | |
KKEY_COMMO | ndd | ns_attach, ndd_open, … |
KKEY_IOMAP | (经过 iomap 的段) | |
KKEY_FILE_SYSTEM | vnode | vfs_vget, vnop_open, … |
gnode | gn_opencnt, … | |
file | fp_open, fp_close, … |
某些数据对象只可以是公共的,如下所述:
向下列函数传递、或者由它们返回的 struct cblock:
putcf
putcb
getcf
将 struct clist 传递给下列函数:
getcs
putcs
getc
putc
getcb
putcb
getcx
putcx
getcbp
putcbp
putcfl
将 struct cfgncb 传递给下列函数:
cfgnadd
cfgndel
将 dr_dma_handler_t 传递给下列函数:
dr_register_dma_mapper
dr_register_dma_mapperx
dr_unregister_dma_mapper
将键 参数传递给下列函数:
kext_service_register
kext_service_unregister
kext_service_request
将 struct busprt 传递给下列函数:
reg_display_acc
unreg_display_acc
grant_display_owner
revoke_display_owner
键安全的路径控制模块 (PCM) 必须具有对 KKEY_BLOCK_DEV 的访问权限。
用于注册关机通知例程的 shutdown_notify_t 可以使用任何键进行键保护。
用于注册 watchdog 计时器的 struct watchdog 可以使用任何键进行键保护。
用于注册中断处理程序的 struct intr 可以使用任何键进行键保护。
事件列表标题可以使用任何键进行键保护。
mstsave 结构是公共的。
请注意并使用特殊用途的分配器。例如,使用 talloc 分配 struct trb,而不是使用 xmalloc 进行分配。
添加恢复障碍
使您的内核扩展成为可识别键的内核扩展中的部分工作,同样可以使其成为可识别恢复的内核扩展,但是对这个主题的详细讨论超出了本文的范围。其基本思想是在您的内核扩展的入口点和退出点放置恢复障碍,直到您可以实现实际的恢复例程为止。
通过添加下面的点来完成这项工作:在入口点添加frr_barrier_add()
,在退出点添加frr_barrier_delete()
。如果保护门碰巧也位于函数的入口点和退出点,那么尝试将恢复障碍嵌套到保护门中。
测试注意事项
请确保对在启用键和禁用键的环境中经过更改的代码进行测试。
请记住,在现在的计算机中,两个内核键可能映射到同一个硬件键,但是在将来的计算机中,这两个内核键可能分别映射到唯一的硬件键。如果所支持的硬件键的数目增加了,那么需要重新测试您的键保护的代码。
如果您可以控制测试计算机,那么您可以临时修改内核键到硬件键的映射,以便使得一个内核键独占其硬件键。像这样对您的新键进行测试是一种极好的方式,可以找出您是否错误地放置了任何保护门,因为它消除了对您的键进行的所有共享错误。独占的键映射仅仅是为了进行测试。IBM 并不支持客户采用这个模式运行。但是下面说明了如何对映射进行调整:
如果您还没有使用 –I 标记启动内核,以使得系统在重新启动之后立即进入内核调试器,那么请执行这个操作。
确定所感兴趣的内核键的数字编号。完成这个任务的一种方式是,在您的代码初始化期间使用 printf 来显示它。这样的 printf 将仅在您启动、并启用了调试器的之后才是有效。
另一种可能的方式是,放置 kdb 断点,以便您可以显示键值,例如,返回对 kkey_assign_private 的调用。当使用相同的参数调用 kkey_assign_private 的时候,它将始终返回相同的内核键。
重新启动。然后,在初始 kdb 提示符处(这是完成该任务唯一有效的时间),将 skey_xkey 整数的值,从 -1 改为您的内核键的十六进制值。
例如,如果您正在使用 KKEY_PRIVATE1,它是内核键编号 43(十进制),那么在初始 kdb 提示符处,输入下面使用粗体表示的内容。在这个对话中键入每个部分之后,您都必须按 Enter。
在 kdb 提示符处 KDB (0) >,输入 mw skey_xkey 。
在 kdb 显示 skey_xkey+000000: FFFFFFFF = 之后,输入 2B。
在 kdb 显示 skey_xkey+000004: 00000000 = 之后,输入一个点号(.)。
在 kdb 提示符处,输入 g。
KDB(0)> mw skey_xkey
skey_xkey+000000: FFFFFFFF = 2B
skey_xkey+000004: 00000000 = .
KDB(0)> g
在系统启动之后,使用 kdb kkeymap 命令显示内核键到硬件键的当前映射。通过在计算机的虚拟控制台 (/dev/vty0) 中键入 ^(ctrl + 反斜杠),您可以激活 kdb,并使用 g(如上所述)恢复正常的操作。或者,您可以为此在命令行使用 kdb。按这种方式选择独占键,仅在当前启动的持续时间内有效。
在紧急情况下,如果您在启动时无法启用内核键,那么您将需要一种重新启动的方式,以便您可以修复问题并再次尝试。下面是一些可能的操作:
如果您已经使用 -I 标记启动了内核(如前所述),那么您可以在初始 kdb 提示符处将 skey_kmode 修改为 0(如前所述),以便选择上面的一个独占内核键。这个操作将在当前启动的持续时间内禁用内核键,并且可能绕过与键相关的错误。
如果您已经准备好了备用的可启动介质,与 mkcd 命令一样,那么您可以从它开始启动,以便进行恢复工作。
您可以采用维护模式,从原始安装介质(包括可用的 NIM 服务器)进行启动,以便进行恢复工作。
结束语
为内核扩展或者设备驱动程序添加存储保护,通过潜在地对难以调试的内存覆盖进行检测,可以增强扩展和 AIX 内核的可靠性。
更多精彩
赞助商链接