2025-11-18 13:02:07 +00:00
|
|
|
# 异常表安全内存拷贝方案设计
|
|
|
|
|
|
|
|
|
|
:::{note}
|
|
|
|
|
|
|
|
|
|
本文作者:龙进 <longjin@dragonos.org>
|
|
|
|
|
|
|
|
|
|
:::
|
|
|
|
|
|
|
|
|
|
## 概述
|
|
|
|
|
|
|
|
|
|
本文档描述DragonOS中基于异常表(Exception Table)机制的安全内存拷贝方案的核心设计思想。该方案解决内核在系统调用上下文中安全访问用户空间内存的问题,防止因访问无效用户地址而导致的内核panic。
|
|
|
|
|
|
|
|
|
|
## 设计背景与动机
|
|
|
|
|
|
|
|
|
|
### 问题定义
|
|
|
|
|
|
|
|
|
|
在系统调用处理中,内核需要访问用户空间传入的指针(如路径字符串、参数结构体等)。这些访问可能失败:
|
|
|
|
|
|
|
|
|
|
1. **地址未映射**: 用户传入的地址没有对应的VMA(Virtual Memory Area)
|
|
|
|
|
2. **权限不足**: 页面存在但缺少所需权限
|
|
|
|
|
3. **恶意输入**: 用户故意传入非法地址
|
|
|
|
|
|
|
|
|
|
### 传统方案的局限
|
|
|
|
|
|
|
|
|
|
**预检查方案的TOCTTOU问题:**
|
|
|
|
|
- 检查时地址有效,使用时可能已被其他线程修改
|
|
|
|
|
- 存在竞态窗口
|
|
|
|
|
|
|
|
|
|
**直接访问的困境:**
|
|
|
|
|
- 无法区分"正常缺页"和"非法访问"
|
|
|
|
|
- 页错误处理器无法判断是内核bug还是用户错误
|
|
|
|
|
|
|
|
|
|
## 异常表机制原理
|
|
|
|
|
|
|
|
|
|
### 核心思想
|
|
|
|
|
|
|
|
|
|
异常表机制通过**编译期标记 + 运行时查找**实现安全的用户空间访问:
|
|
|
|
|
|
|
|
|
|
1. **编译期**: 在可能触发页错误的指令处生成异常表条目
|
|
|
|
|
2. **运行时**: 页错误发生时,查找异常表并跳转到修复代码
|
|
|
|
|
3. **零开销**: 正常路径无性能损失
|
|
|
|
|
|
|
|
|
|
### 架构示意图
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌─────────────────────────────────────────────────────────────┐
|
|
|
|
|
│ 系统调用执行流程 │
|
|
|
|
|
├─────────────────────────────────────────────────────────────┤
|
|
|
|
|
│ │
|
|
|
|
|
│ 用户空间 内核空间 │
|
|
|
|
|
│ ┌──────┐ ┌──────────────────────────────┐ │
|
|
|
|
|
│ │0x1000│ │ 1. 系统调用入口 │ │
|
|
|
|
|
│ │(未映射)─────────→ 2. 拷贝用户数据(带标记) │ │
|
|
|
|
|
│ └──────┘ │ ├─ 正常完成 ──→ 返回成功 │ │
|
|
|
|
|
│ │ └─ 触发#PF │ │
|
|
|
|
|
│ │ ↓ │ │
|
|
|
|
|
│ │ 3. 页错误处理器 │ │
|
|
|
|
|
│ │ ├─ 查找异常表 │ │
|
|
|
|
|
│ │ └─ 找到修复代码地址 │ │
|
|
|
|
|
│ │ ↓ │ │
|
|
|
|
|
│ │ 4. 修改指令指针(RIP) │ │
|
|
|
|
|
│ │ ↓ │ │
|
|
|
|
|
│ │ 5. 执行修复代码 │ │
|
2025-11-21 06:26:52 +00:00
|
|
|
│ │ └─ 返回剩余未拷贝字节数 │ │
|
2025-11-18 13:02:07 +00:00
|
|
|
│ │ ↓ │ │
|
|
|
|
|
│ │ 6. 返回EFAULT给用户 │ │
|
|
|
|
|
│ └──────────────────────────────┘ │
|
|
|
|
|
└─────────────────────────────────────────────────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
### 核心数据结构
|
|
|
|
|
|
|
|
|
|
**异常表条目 (8字节对齐):**
|
|
|
|
|
```
|
|
|
|
|
┌─────────────────┬──────────────────┐
|
|
|
|
|
│ 指令相对偏移 │ 修复代码相对偏移 │
|
|
|
|
|
│ (4 bytes) │ (4 bytes) │
|
|
|
|
|
└─────────────────┴──────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**设计要点:**
|
|
|
|
|
- 使用相对偏移支持ASLR(地址空间布局随机化)
|
|
|
|
|
- 8字节对齐提高缓存性能
|
|
|
|
|
- 存储于只读段防止篡改
|
|
|
|
|
|
|
|
|
|
### 工作流程
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
编译期:
|
|
|
|
|
源码 ──→ 带标记的指令 ──→ 生成异常表条目 ──→ 链接到内核镜像
|
|
|
|
|
(rep movsb) (insn→fixup)
|
|
|
|
|
|
|
|
|
|
运行期:
|
|
|
|
|
执行拷贝 ──→ 触发页错误? ─否→ 正常返回
|
|
|
|
|
│
|
|
|
|
|
是
|
|
|
|
|
↓
|
|
|
|
|
查找异常表 ──→ 找到? ─否→ 内核panic
|
|
|
|
|
│
|
|
|
|
|
是
|
|
|
|
|
↓
|
2025-11-21 06:26:52 +00:00
|
|
|
修改RIP到修复代码 ──→ 返回剩余未拷贝字节数
|
2025-11-18 13:02:07 +00:00
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 典型执行场景
|
|
|
|
|
|
|
|
|
|
### 场景: 系统调用传入无效地址
|
|
|
|
|
|
|
|
|
|
以`open()`系统调用为例,展示异常表的工作过程:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
用户程序: open(0x1000, O_RDONLY) // 0x1000未映射
|
|
|
|
|
│
|
|
|
|
|
↓
|
|
|
|
|
┌────────────────────────────────┐
|
|
|
|
|
│ 1. 进入系统调用 │
|
|
|
|
|
│ ├─ 解析路径字符串 │
|
|
|
|
|
│ └─ 逐字节拷贝直到'\0' │
|
|
|
|
|
└────────────────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
↓
|
|
|
|
|
┌────────────────────────────────┐
|
|
|
|
|
│ 2. 拷贝第一个字节时触发页错误 │
|
|
|
|
|
│ (地址0x1000不在VMA中) │
|
|
|
|
|
└────────────────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
↓
|
|
|
|
|
┌────────────────────────────────┐
|
|
|
|
|
│ 3. 页错误处理器 │
|
|
|
|
|
│ ├─ 检测到访问用户地址 │
|
|
|
|
|
│ ├─ 查找异常表 │
|
|
|
|
|
│ └─ 找到对应的修复代码 │
|
|
|
|
|
└────────────────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
↓
|
|
|
|
|
┌────────────────────────────────┐
|
|
|
|
|
│ 4. 修改指令指针到修复代码 │
|
2025-11-21 06:26:52 +00:00
|
|
|
│ └─ 设置返回值为剩余未拷贝字节数 │
|
2025-11-18 13:02:07 +00:00
|
|
|
└────────────────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
↓
|
|
|
|
|
┌────────────────────────────────┐
|
|
|
|
|
│ 5. 系统调用返回EFAULT │
|
|
|
|
|
└────────────────────────────────┘
|
|
|
|
|
│
|
|
|
|
|
↓
|
|
|
|
|
用户程序: fd = -1, errno = EFAULT
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**关键点:**
|
|
|
|
|
- 无需预检查地址有效性
|
2025-11-21 06:26:52 +00:00
|
|
|
- 页错误自动转换为返回剩余未拷贝字节数
|
2025-11-18 13:02:07 +00:00
|
|
|
- 内核不会panic,用户程序收到明确的错误信息
|
|
|
|
|
|
|
|
|
|
## 使用场景分析
|
|
|
|
|
|
|
|
|
|
### ✅ 适合使用异常表保护的场景
|
|
|
|
|
|
|
|
|
|
#### 1. 小数据的系统调用参数
|
|
|
|
|
|
|
|
|
|
**特征:**
|
|
|
|
|
- 数据量小 (通常 < 4KB)
|
|
|
|
|
- 一次性拷贝
|
|
|
|
|
- 无法预知数据长度(如字符串)
|
|
|
|
|
|
|
|
|
|
**典型应用:**
|
|
|
|
|
- 路径字符串: `open()`, `stat()`, `execve()`等
|
|
|
|
|
- 固定大小结构体: `sigaction`, `timespec`, `stat`等
|
|
|
|
|
- 小型数组: `iovec[]`, `pollfd[]`等
|
|
|
|
|
|
|
|
|
|
**优势:**
|
|
|
|
|
- **避免TOCTTOU竞态**: 无需预检查
|
|
|
|
|
- **高鲁棒性**: 用户错误不会导致内核panic
|
|
|
|
|
- **性能可接受**: 数据量小,即使多拷贝一次也影响不大
|
|
|
|
|
|
|
|
|
|
#### 2. 不确定地址有效性的场景
|
|
|
|
|
|
|
|
|
|
当无法通过其他方式验证地址时,异常表是最安全的选择:
|
|
|
|
|
- 用户直接传入的原始指针
|
|
|
|
|
- 多线程环境下可能被并发修改的地址
|
|
|
|
|
- 需要原子性保证的操作
|
|
|
|
|
|
|
|
|
|
### ❌ 不适合使用异常表保护的场景
|
|
|
|
|
|
|
|
|
|
#### 1. 大数据传输
|
|
|
|
|
|
|
|
|
|
**反模式: read/write系统调用中双重缓冲**
|
|
|
|
|
```
|
|
|
|
|
用户缓冲区 → 内核临时缓冲区 → 用户缓冲区 ❌
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
**问题:**
|
|
|
|
|
- 内存浪费: 需要额外的内核缓冲区
|
|
|
|
|
- 双重拷贝: 数据被拷贝两次
|
|
|
|
|
- OOM风险: 大量并发读写耗尽内存
|
|
|
|
|
|
|
|
|
|
**正确方案: 零拷贝**
|
|
|
|
|
- 预先验证地址在有效VMA中
|
|
|
|
|
- 直接在用户缓冲区上操作
|
|
|
|
|
- 页错误触发正常的缺页处理(非错误)
|
|
|
|
|
|
|
|
|
|
#### 2. 已验证的VMA内地址
|
|
|
|
|
|
|
|
|
|
如果地址已通过VMA检查,异常表是多余的:
|
|
|
|
|
- `mmap()`后的立即访问
|
|
|
|
|
- DMA缓冲区
|
|
|
|
|
- 共享内存区域
|
|
|
|
|
|
|
|
|
|
在这些场景下,页错误是**正常的缺页处理**(如COW),不是错误。
|
|
|
|
|
|
|
|
|
|
#### 3. 性能敏感的热路径
|
|
|
|
|
|
|
|
|
|
避免在循环中频繁调用带异常表保护的函数:
|
|
|
|
|
- **批量处理**: 一次拷贝整个数组,而非逐元素拷贝
|
|
|
|
|
- **提前验证**: 在循环外验证地址,循环内直接访问
|
|
|
|
|
|
|
|
|
|
### 决策矩阵
|
|
|
|
|
|
|
|
|
|
| 场景特征 | 数据量 | 推荐方案 | 核心考虑 |
|
|
|
|
|
|---------|--------|----------|---------|
|
|
|
|
|
| 系统调用小参数 | < 4KB | 异常表保护 | 避免TOCTTOU,提高鲁棒性 |
|
|
|
|
|
| 文件读写 | 可变(MB级) | 零拷贝 | 性能优先,避免双重缓冲 |
|
|
|
|
|
| mmap后访问 | 任意 | 直接访问 | VMA已验证,正常缺页 |
|
|
|
|
|
| 批量小数据 | 累计KB级 | 批量拷贝 | 减少系统调用次数 |
|
|
|
|
|
| 字符串解析 | 未知 | 异常表保护 | 逐字节扫描,需要健壮性 |
|
|
|
|
|
|
|
|
|
|
## 安全性分析
|
|
|
|
|
|
|
|
|
|
### 防御能力
|
|
|
|
|
|
|
|
|
|
异常表机制可以防御:
|
|
|
|
|
|
|
|
|
|
1. **空指针解引用**: 返回EFAULT而非段错误
|
|
|
|
|
2. **内核地址注入**: 用户传入内核地址被安全拒绝
|
|
|
|
|
3. **竞态攻击**: TOCTTOU窗口被消除
|
|
|
|
|
4. **越界访问**: 访问VMA外地址被捕获
|
|
|
|
|
|
|
|
|
|
### 安全边界
|
|
|
|
|
|
|
|
|
|
异常表**不能**防御:
|
|
|
|
|
|
|
|
|
|
1. **内核自身bug**: 如野指针解引用
|
|
|
|
|
2. **硬件故障**: 内存物理损坏
|
|
|
|
|
3. **其他异常类型**: 仅处理页错误
|
|
|
|
|
|
|
|
|
|
### 多层防御
|
|
|
|
|
|
|
|
|
|
异常表是纵深防御的一部分:
|
|
|
|
|
|
|
|
|
|
```
|
|
|
|
|
┌─────────────────────────────────────┐
|
|
|
|
|
│ 用户空间权限检查 (SELinux/AppArmor) │ ← 权限层
|
|
|
|
|
├─────────────────────────────────────┤
|
|
|
|
|
│ 系统调用参数验证 │ ← 逻辑层
|
|
|
|
|
├─────────────────────────────────────┤
|
|
|
|
|
│ 异常表机制 │ ← 内存安全层
|
|
|
|
|
├─────────────────────────────────────┤
|
|
|
|
|
│ 硬件页保护 (MMU) │ ← 硬件层
|
|
|
|
|
└─────────────────────────────────────┘
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## 实现要点
|
|
|
|
|
|
|
|
|
|
### 关键技术
|
|
|
|
|
|
|
|
|
|
1. **相对偏移编码**: 支持地址随机化(ASLR)
|
|
|
|
|
2. **二分查找**: O(log n)时间复杂度快速定位
|
|
|
|
|
3. **内联汇编**: 精确控制指令和异常表生成
|
|
|
|
|
4. **零开销抽象**: 正常路径无性能损失
|
|
|
|
|
|
|
|
|
|
### 架构移植
|
|
|
|
|
|
|
|
|
|
异常表机制可移植到其他架构:
|
|
|
|
|
|
|
|
|
|
- **x86_64**: 使用`rep movsb`指令
|
|
|
|
|
- **ARM64**: 使用`ldp/stp`指令序列
|
|
|
|
|
- **RISC-V**: 使用`ld/sd`指令序列
|
|
|
|
|
|
|
|
|
|
核心思想保持不变,只需调整汇编语法。
|