LEC17 Singularity
预先阅读
- Singularity
- Language Support for Fast and Reliable Message-based Communication in Singularity OS
概览
Singularity是微软的研究型实验OS
- 该OS发表了很多相关论文,相当高调.
- 受到了微软OS的经验的影响,比如Windows
- 我们可以推测Singularity对于微软产品的影响
目标
- 健壮性和安全性增强,尤其针对插件
- 减少不必要的交互
- 利用最新的技术
总体架构
- 微内核: 内核,进程和IPC
page 5
声称已经将服务分解到用户流程中
2.1 NIC, TCP/IP, FS, disk driver
2.2 内核: 进程, 内存, 部分IPC, nameserver
2.3 不再追求和UNIX的兼容,因此避免了部分陷阱
- 最终,
singularity
有192个系统调用
最根本的设计理念
- 只有一个地址空间(不需要分段和分页功能),内核和所有进程均处于同一地址空间
- 用户进程均运行在特权级,即CPL=0
新设计的优势
- 性能提升
- 更快的进程切换: 不需要切换页表
- 更快的系统调用: 不需要
INT
就可以执行系统调用
- 更快的进程间通信: 不需要拷贝数据
- 用户进程可以直接访问硬件资源,比如设备驱动
- paper Table 1展示了性能对比的结果
新设计的核心目标
- 健壮性
- 安全性
- 交互
健壮性不依赖页表保护机制
- 不可靠的问题主要来自浏览器插件以及动态加载的内核模块
- 出于性能和便利性的考量,需要加载到主进程的地址空间来执行.
- 我们是否可以不依赖硬件,来完成健壮性保护
插件在Singularity中如何运行?
- 插件包含: 设备驱动,新的网络协议,浏览器插件
- 分隔的进程,和主进程间通过IPC通信
单地址空间遇到的挑战
- 阻止恶意程序访问其他进程或者内核空间
- 支持杀死进程和进程退出
SIP: software-isolated program
SIP总体理念
- 密封的
- 进程外部无法修改程序
2.1 除了启动和停止进程外,没有以进程号为参数的系统调用
2.2 可能没有debugger,只有IPC.
- 进程内部无法修改程序
3.1 没有JIT
3.2 没有class loader
3.3 没有动态库加载
SIP规则
- 指针只能指向本进程的数据
1.1 指针不允许指向其他SIP数据或者内核
1.2 尽管共享地址空间,但没有共享内存.
1.3 在exchange heap中传递的IPC消息,只支持有限的exception
- SIP可以从内核中分配物理内存
2.1 不同的分配内存是不连续的.
为什么要限制SIP的修改?即使是修改本进程?
- 限制的好处是什么?
1.1 没有代码注入攻击
1.2 更容易完成正确性推理
1.3 更容易做代码优化,比如删除未使用的函数
1.4 TODO: SIP可以作为一个安全原则,拥有文件
- 以上收益和损失相比,是否值得?
为什么不类似Java虚拟机,共享所有数据呢?
- SIPs排除了所有的进程间交互,除了显式的通过IPC通信
- SIPs更加健壮
- SIPs使得每个进程都有自己的语言runtime,GC等
3.1 尽管质量有一定保障,但这部分代码未必没有bug
3.2 内核代码也是同样敏感
3.3 所以开发者比较难自己准备runtime和GC
- SIPs使得内核杀死和退出进程比较容易
如何防止SIPs读写其他SIP的内存
- SIP只能读写内核分配的内存
- 编译器在编译时,会检查内存访问地址是否合法?
2.1 内存指针是否合法?
2.2 是否会导致代码执行速度下降
2.3 编译器是否完全可信
PL-based保护(Program-Language)
总体架构
- 编译为字节码.
- 在安装时,验证字节码
- 在安装事,将字节码编译为机器码
- 在可信的runtime,运行经过验证的机器码
问题
- 为什么不直接编译为机器码?
- 为什么不在运行时,编译运行(JIT, Just in time)?
- 为什么不在编译期验证字节码?
- 为什么不在运行期验证字节码?
对Singularity而言,字节码验证有什么好处?
字节码验证是否检查: 字节码中是否仅访问了内核分配的内存?
不完全是,但相关
- 仅使用可达的指针
- 不能创建新的指针,只有可信runtime可以创建指针
- 所以如果内核/运行时不提供超出SIP范围的指针,那么经验证的SIP只能访问其自身的内存.
要达到以上目标,检查器必须检查哪些?
- 不要创建指针,只能使用传递的指针.
- 不允许类型转换,比如将int类型转换为指针.
- 不允许use-after-free.指针重用可以指针类型已经发生了变化.
- 不允许使用未初始化的变量.
- 总体而言,不要欺骗检查器
栗子
R0 <- new SomeClass;
jmp L1
...
R0 <- 1000
jmp L1
...
L1:
mov (R0) -> R1
潜在的问题
- 第一次jmp是没问题的,相当于读取类的第一个成员.
- 第二次jmp是有风险的,
0x1000
中的内容很有可能是指向内核.
检查器尝试解析每个寄存器的类型
- 尝试运行每个代码路径
- 要求所有路径对同一寄存器的使用结果是一致的.
- 所有寄存器的使用是类型安全的
- 这里需要判断R0是int类型还是SomeClass*类型.如果是前者,检查器会报错
字节码验证可以比Singularity需要的做的更多
- 创建新的指针或许也可以,只要新指针在SIP的内存范围内.
- 检查器可能会禁止一些在Singularity上可以运行的程序
全面检查的收益
- 更快的执行速度,通常不需要再进行运行时检查.个别情况除外,比如数组越界,对象转换,栈溢出等.
- IPC消息类型检查
- 需要允许交换堆读写,但这不是SIP的内存
- 栈内存分配
- 系统调用是否运行在SIP内存栈上?
5.1 防止线程X破坏线程Y的内核系统调用栈
可以在SIP中放一个解释器来规避对自修改代码的禁令
- 这会带来麻烦吗?
哪些部分是可信的还是不可信的?
- 所有软件都有bug
- 可信的软件: 如果有bug,可以导致Singularity crash,也可能导致其他SIP crash.
- 不可信的软件: 如果有bug,只能导致其自身crash.
- 让我们考虑一些普通的应用程序,而不是服务器.
4.1 编译器.编译器输出.验证者.验证器输出.GC.
交换堆(Exchange heap)
IPC
- SIPs之间如何通信?
- 端点(endpoints),隧道(channels)
- 接收端点是个消息队列
- 消息主体在交换堆中
- 特性: 无需拷贝
交换堆是共享内存
- 风险是什么?
- 发送了错误的消息类型
- 使用时,修改了消息
- 将消息修改为完全不相关的消息
- 耗尽了交换堆内存,并且不释放.
如何防止交换堆滥用
- 验证器确保SIP字节码只对交换堆中的任何内容保留一个ptr,从不保留两个及以上ptr.
- SIP在调用
send()
后,不会保留ptr.单一的指针帮助如下:
2.1 验证器知道何时ptr失效
2.2 通过send()
2.3 通过另外一个交换堆指针指向它
2.4 通过删除
- 单一指针防止了发送后修改的错误,也保证了完成后正确的删除.
- 删除必须是显式手动的,没有GC.验证器确保了每个block仅有一个指针
- 运行时在各个交换堆block中,维护自身的SIP入口.
5.1 通过
send()
更新
5.2 通过exit()
来清除
接收消息如何工作?
- 检查共享内存中的端点,如果没有消息,则阻塞条件变量,所以发送方必须调用唤醒系统调用.
系统调用如何陷入内核?
- INT? CALL?
- 栈分布如何?
- 因为共用一个栈,GC如何工作?
- SIP可以传递一个指针给内核么?
Endpoints function as capabilities
- 不能传递它们
- 不通过channel无法和其他SIPs通信
- 通过使用channel来限制访问资源,比如文件
目前评估情况如何?
- 健壮性?
- 有利于扩展的模型?
- 性能?
3.1 在单一地址空间, 快速的系统调用, 进程切换和IPC.
3.2 论文表1, benchmarks
3.3 图5: 不安全代码的执行代价
3.3.1 物理内存 -- 不支持分页 -- 这是Singularity么?
3.3.2 支持4K分页 -- 打开分页,但是使用单一页表,且特权级均为0.
3.3.3 区域隔离 -- 为SIPs中每个SIP提供独立页表,此时进程切换会增加损耗
3.3.4 特权级3 -- CPL=3 因此INT会损耗
3.3.5 全部微内核 -- 对所有SIPs, 页表+INT