singularity是微软概念分布式操作系统,展示了一个“把可靠性和可信度 作为首要的设计目标”,从头来设计的软件平台。
singularity的设计者认为,通过更为可靠的技术和设计方法是可以在很大程度上提高系统的可靠性的。因此,singularity的可靠性与安全的实现依赖了许多其他方面的技术:
软件隔离的进程(Software-Isolated Processes:SIP)
和传统的操作系统中进程的概念类似,SIP是作为处理资源的载体,为程序执行提供了上下文:
与传统的进程不同的是:
软件隔离
软件隔离通过篇基于语言的类型和内存访问检查来实现。类型检查保证值或对象被正确解释和操作,内存访问检查保证引用的内存在有效对象的边界内。软件隔离能够比硬件隔离更加细粒度。硬件隔离中,进程可以按照页保护字段的规定,随意访问自己的内存空间(栈和堆),而在软件隔离中,访问的方式和范围被限制,对粒度为变量的区域提供了保护(封装)。
借助软件隔离,所有的SIP和内核可以运行于单一的环0的地址空间中,从而进程的创建,系统调用,调度与通信等的开销减小了。
另外,类型和内存安检确保了函数的执行的完整性,从而可以在安检后,在SIP中运行的信任函数中使用特权指令。例如,访问硬件I/O的特权指令可以在安装阶段,安全地内联到设备驱动中。这也促进了性能上的提升。
-
| 开销(单位:CPU周期) |
API调用 | 线程产出 | 消息反馈 | 进程创建 |
Singularity | 80 | 365 | 1,040 | 388,000 |
FreeBSD | 878 | 911 | 13,300 | 1,030,000 |
Linux | 437 | 906 | 5,800 | 719,000 |
Windows | 627 | 753 | 6,340 | 5,380,000 |
表1:进程基本开销(硬件平台:AMD Athlon 64 3000+(1.8GHz)CPU,NVIDIA nForce4 Ultra芯片组。
表1数据来源Singularity: Rethinking the Software Stack,并未指明操作系统的版本与配置。在An Overview of the Singularity Project类似测试中,指明了操作系统版本与配置,且数据与此表相似,故可能是同一测试平台(硬件:AMD Athlon 64 3000+ (1.8 GHz) CPU,NVIDIA nForce4 Ultra 芯片组,1GB内存,Western Digital WD2500JD 250GB 7200RPM SATA硬盘(不含指令重排(command queuing)),网卡为nForce4 Ultra native Gigabit NIC(不含硬件TCP加速)。操作系统为FreeBSD 5.3,Red Hat Fedora Core 4(内核版本2.6.11- 1.1369_FC4与Windows XP (SP2))据此,对照的测试OS是较老的版本,且存在配置上的问题(比如使用性能低下的pthreads),数据未必精确,但Singularity的性能优势较明显,具备参考意义。
软件隔离所带给SIP的低系统开销,也拓广了进程这一传统概念的应用范围。比如,对微内核OS最大的批评是其低下的性能,从单一内核中分离出的功能运行在各自进程中,来提高系统的稳定性和安全性。但这样作也造成了核与系统进程,系统进程与系统进程之间紧密的通信,不得不通过穿越硬件保护边界——造成了性能上巨大的开销。因此,实用的微内核操作系统总是在微内核设计上加以妥协,比如Windows NT系列系统就把NT内核与NT执行体就被合并到了单个可执行映像了。
软件隔离的不同于传统的构筑安全的思维:在传统的思维中,安全隔离应当在底层、外围实施,从而避免引不必要的依赖(这些依赖可能带来新的安全隐患),因此安全应当是专门独立出来。而在singularity中,安全并不单一存在,而是各种技术混合的结果。从最小权限原则上来说,只有越贴近应用逻辑(高层),才能实现细粒度的控制,这对开发安全的程序也是有利的——错误发现的越早就越容易排除。(后续章节将解释,singularity如何解决实施安全,带来新的依赖的问题)
在SELinux一节中,表达了这样一个观点,即强制访问控制是在操作系统支持粒度下,对程序正确的行为进行断言。而软件隔离则在语言层次上,对语言的正确使用作出断言(由编译器或者解释器负责)。(纯粹的)软件隔离同时也意味着对硬件访问接口的再次封装,即只有通过安全的编程语言才能使用硬件资源。这也是在SELinux一节中的得出的结论,明晰合理的接口相当于细化了安全检查的粒度。
可以依照纵深防御原则,把软件隔离与硬件隔离相结合,比如把单独内核运行在环0的地址空间中,其他SIP运行在环3的各自的地址空间(类似微内核那样),或者经过数字签名,信任的软件于内核一起运作在环0的地址空间中,其他未签名的不受信任的软件运作在环3的各自地址空间中。当然,还可以考虑,如把不安全的语言编写的程序运行在独立的地址空间中,而安全语言编写的程序与内核共处一地址空间,从而兼容以有程序。
这些配置也为观察硬件保护的开销带来了机会,下图源自Singularity: Rethinking the Software Stack。
图10,各种隔离措施所带来的系统性能上的开销。
图中,采用是WebFiles(基于SPECweb99的I/O密集型测试)在6种不同的配置上进行测试,并以第2个配置方案为标准进行衡量。测试采用了3个SIP:
6个配置方案分别为:
No runtime checks:不进行任何安全隔离,用于对照开启软件隔离的开销(图中,4.7%)
Physical Memory:3个SIPs与内核运行于同一地址空间和同一特权级。禁用分页。
Add 4KB Pages:在上一配置的基础上,开启分页(页大小:4KB)与TLB
Add Separate Domain:在上一配置的基础上,把客户端SIP单独移到一个地址空间中(特权级维持为环0)
Add Ring3:在上一配置的基础上,客户端SIP的特权级改变为环3。
Full Microkernel:把每个SIP移到各自环3的地址空间中。
可见软件隔离所带来的开销非常小(<5%),与之相比较,硬件隔离的开销非常大。
软件隔离的另一个好处是,脱离了对MMU的依赖,从而SIP可以运行在一些特殊的处理器上(或者说可编程I/O设备),比如GPU(Graphics Process Unit,时下渐热的流计算的基本硬件),从而支持非均一处理器组成的分布式计算的网络。
内存管理——SIP之间不共享数据
在SIP中的指针必须指向自己的内存或者交换堆中所拥有的内存。
SIP通过ABI调用内核页管理器获得新的、独占的页。这些页不定与之前分配的内存页相邻,连续的页可以通过大的对象或者数组获得。ABI不传递对象指针。
SIP的栈是链式的,调用内核ABI时,寄存器等的现场环境被保留到栈中一特殊结构中,区分SIP与内核空间。中断处理使用专用的栈。
SIP间传递的数据必须驻留在交换堆中,交换堆中的指针只能指向其自身,对于交换堆,有线性(linearity)属性:任何时候,SIP至多只有一个指向交换堆中一块的指针(由此可推,交换堆中的块任一时刻至多属于一个线程)。只能通过向另一SIP(通过channel)发送消息来转移块的所有权,发送后,原SIP不能在访问该块(SIP可能含有指向交互堆的尾随指针(dangling pointer,指向此SIP不再拥有的块),静态的检查确保SIP不通过尾随指针访问内存)。
每个SIP有其自己的收集器来回收其对象空间中的对象。
代码密封
SIPs含有的代码在运行时刻是密封的,不能动态载入和创建代码。动态载入往往用在诸如载入软件插件的环境中。糟糕的插件可能导致程序崩溃。动态创建代码往往用于某些语言的自省(reflection)机制。
原需要动态载入的代码,现需要运行在子SIP,通过channel使用父SIP的服务而不是调用函数。Singularity中动态创建代码的自省机制被移除,取而代之的是编译时刻的自省(Compile-Time Reflection:CTR)
密封代码提供了如下好处:
基于Contract的Channel
在singularity中,Channel是唯一支持的IPC手段。Channel提供无损、有序的消息队列。Channel是双向的带有两个终点的队列管道。
终点有权能,比如只有接收了来自另一SIP指向文件系统的终点当前SIP才能访问文件。Channel终点或者在程序启动是获得(由基于清单(manifest)的配置文件获得),或者通过已有的channels中某个消息获得。因为传递的消息必须在交换堆中,故终点驻留在交换堆。这意味着每次终点只明确的属于一个线程(SIP至多只有一个指向交换堆中一块的指针)。每个终点有一个接收队列,向一个终点发送消息,就是把该消息放入该终点的接收队列中。
Contract是描述channel上的通信。其由消息申明(规定参数的数目和类型,及(可选的)消息的传递方向)和一组命名的协议状态组成。借助Contract,SIP之间的通信是有效、可分析的。编译器可以静态地验证发送和接收操作,确保channel不进入错误的协议状态。
Contract可以看成对通信这种工作模式的程序正确性的断言,也可看作基于任务访问控制(Task-Based Access Control)中对工作流的限定。
Channel遵循有限(finiteness)属性:contract的每个状态中,至少含有一对接收和发送操作——终点不能不等待消息而发送无限量的数据。这样作允许在终点中静态分配接收缓冲队列。
Channel的另一个好处隔离了消息传递的实现,比如在同一特权级同一地址空间中的两个SIP之间,实现成零拷贝:发送者通过在接收终点中,放入指向消息(交换堆中)的指针,来传递交换堆中的消息的所有者关系。不同硬件保护域之间的通信,可以实现为数据拷贝或者内存页的COW映射。
基于清单的程序(Manifest-Based Programs)
MBP是一个程序:通过静态的清单(manifest)定义。在Singularity中,执行程序实际上是调用清单而非调用可执行文件。
清单是对程序的描述,是一个机器可检查的,对MBP期望行为的说明(描述了一个MBP代码资源,需要的系统资源,期望的行为能力和对其他程序的依赖)
清单用来进行静态/动态验证MBP的属性。验证包括类型和内存安全、不含特权模式指令、与channel contracts一致(申明contracts的用法)、正确的对应版本ABI的用法。
MBP安装到系统后,清单用来标识MBP代码、验证其符合系统的安全需求,确保MBP对系统的所有依赖得到满足,防止MBP的安装干扰先前安装的MBP的执行。执行前,清单被用来发现MBP相关参数的配置,并对参数进行限制。MBP执行时,清单指导代码载入SIP执行、新的SIP与其他SIP之间的Channel、对系统资源访问的许可。
清单可以内联到程序中,也可分离出来(被多个程序所共享)。
清单(一部分)类似SELinux中的规则,在操作系统的粒度下,对程序进行了限制。可以看在是在OS粒度下程序正确性的断言。
清单另外还对MBP正常运行所需的依赖关系检查,确保了MBP的可靠性。在Windows环境中这部分工作由安装程序所完成,在Linux中,则由各自发行版的包管理程序负责。清单对程序相关参数的配置的检查,这一般由执行程序负责的。之所以独立出上述两点,可以明确的保证程序设计时考虑到相关的检查,使得程序结构更加清晰。
如果比较当前普遍的基于杀毒软件构筑的安全体系,杀毒软件负责对将运行的程序进行验证——不含有病毒库中的特征,且没有触发启发式规则报警。为了让杀毒软件更加有效,可以对待验证的程序要求含有“证据”表明其清白。在Singularity中,这种“证据”即是包含在高级语言Sing#中,而“病毒库”则对应了清单。
Singularity内核
Singularity内核是个微内核,提供基本抽象——SIP、Contract、MBP ,负责调度、对硬件资源的特权访问、系统内存、线程和线程栈,创建/销毁SIP和Channel。
内核通过ABI向SIP提供内存服务。默认状态下,SIP只能操作其自身的状态、停止/开始子SIP(ABI调用只影响调用方的状态,如SIP同步对象不能够跨越SIP访问)。
ABI 强制标明版本,语义明确。没有ioctl或者CreateFile之类的模糊语义的函数。在SELinux一节的分析中得到的结论:明晰的接口有益于细化访问控制的粒度。
SIP通过channels而非ABI函数来获得高级系统服务(如访问文件、发送和接受网络包)。
ABI调用开销比函数调用开销大些:他们必须区分SIP的垃圾收集空间和内核垃圾收集空间。
抽象内核对象(如互斥、线程)的导出为强类型化的不透明的的句柄(handle表中各槽)。当SIP终结时,相应的handle表中的槽被回收。
Singularity的安全
安装时刻的安全机制
在singularity的安全模型中,核心是程序。Singularity为系统中的所有的服务(如设备驱动)、文件系统提供了单一的、树状的名空间。对程序发布者的信任体现在名上。例如,系统策略可以指定,只有由Microsoft签发的程序可以占有某个Microsoft专用的名空间,例如所有微软签发的程序位于/app/microsoft/下。名空间也可按照对程序的信任度(根据系统策略)来使用。
可以看到,上述的安全策略基于路径。由于有安全策略对名空间进行了必要的保护,故此处的安全的。
某些安全强制在安装时刻进行。Singularity对资源的访问通过Channel完成,故系统安装器可以通过静态管理程序的Channel来实现对资源的控制。每个程序的需求在静态的清单中指明。系统安装器通过提供给程序的一个配置,从而在运行时刻实例化Channel,来解决程序清单中所有未绑定的Channel。这种静态的检查有时能用来实现最小特权的安全环境,例如,如果某个程序仅负责在本地处理,安装器将不提供对网络的直接的Channel。
动态访问控制
Singularity程序在运行时刻以一个或多个进程的形式实例化。每个进程由于调用而拥有一个不可更改的标识。
进程创建所有channel终点对,用其标识来初始化终点对。当channel的一个终点传递给另一进程是,接收进程便获得另一方进程的标识。当对共享资源进行动态的访问控制决策时,这个标识与促使该请求进程执行的调用链中的程序的标识,组成访问控制决策中的主体。(在某些情形下,如使用自有认证功能的服务器程序,将忽视调用链)调用历史是以进程为粒度追踪的。
在这个环境中,用户表现为程序的角色。认证用户的程序(比如通过密码或者证书)参与标识的合成。因此,远程登录的用户登录后的标识与本地通过智能卡认证后的标识不同。
Singularity的合成标识由文本字符串表示,例如:
/sys/login@/users/fred + /apps/ms/word
这个字符串可能代表了系统登录程序(用户“fred”通过密码登录)调用Microsoft word程序。
与SELinux相比,在SELinux中,用户角色对登录程序的类型作出贡献,而之后登录程序类型影响其运行的程序:或继承登录程序的类型(默认情形下)或依据转换规则转换到新的类型下。
在访问控制列表处,Singularity使用访问控制表达式(access control expressions:ACEs)来定义匹配标识的模式。这些表达式可以非常灵活。例如,可以指定只有Word能读某个模式保护的文件或者“fred”运行的任何Microsoft的程序能够访问它。
可以设想,通过定义可被继承的策略规则,大量的不同的ACEs将被一小组规则代替。这特性将在结构化的环境中,如文件系统中极为有效。
更多的运行时刻安全机制
Channel contract可以导出子类,来指定终点持有者能发送哪些消息。例如,TcpConnectionContract的一个子类可以只描述某个主体许可的方法,来只允许该主体监听而非连接。这样,一个子类对应一组权限。
如上所描述,在默认的进程调用情形下,新的进程的标识是一个合成的标识,以调用者 + 被调用者的形式。进程调用外,至少在两种场景下,进程也许把其部分标识“借给”其他进程:
在上述两种情形下,通过专门地“赐予”Channel终点,来支持标识继承。一个“被赐予”的终点允许在某些受限的上下文中,接收者继承合作者的标识.
持有多个标识的进程可能令人混乱,造成误用。故大多数情形下,进程尽可能只使用一个标识。
Singularity的可信基
回来之前的一个问题上来,Singularity的安全是多方面技术合作的结果,这样便引入多方面的依赖,是否也随之引入了安全隐患呢?答案是否定的。在Singularity中可信基由4部分组成:
硬件抽象层
内核部分(使用非安全编程语言编写)
运行时刻系统部分
编译器
其中编译器是个很庞大的部分,将来通过引入类型化的汇编语言(typed assembly language:TAL),来使用一个相对较小的验证器来验证编译器的输出。从而庞大的编译器部分由较小的验证器部分替代,减小了可信基。
总结
通过对硬件接口再次封装(通过安全的编程语言接口),限制了编程语言对内存的访问。这与缓冲溢出防御手段2、3类似(限制执行任意地址其的数据),但限制更多。
得益于这种封装,便于实现软件隔离,在性能上获得收益。开销小的安全手段适应更多的应用场合,从而提升系统平台的安全性能。
Singularity中强调了程序的可验证性:
通过安全的编程语言确保对内存正确访问的可验证性。
Contract给予了进程间通信的可验证性。
清单从系统角度,给予了程序正确行为的可验证性。
SIP代码密封等其他简化手段有助于验证的进行。
这种可验证性源自各种形式的断言。这些断言可能是语言级的,通信级的,系统级的——断言的粒度和时刻(编译时刻、链接时刻、安装时刻及运行时刻)的灵活性使得其最小特权原则得以顺利贯彻。
断言在高层的逻辑上进行有助于最小特权,以及友好的开发环境,但也造成安全的实现依赖面太广,使得安全本身的可靠性降低。Singularity通过一个小的可信基来验证其他安全的组成部分,从而确保了安全本身的可靠。