# 异常表安全内存拷贝方案设计 — DragonOS dev 文档

# 异常表安全内存拷贝方案设计

备注

本文作者：龙进 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. 执行修复代码                │          │
    │                   │    └─ 设置错误码(-1)           │          │
    │                   │         ↓                    │          │
    │                   │ 6. 返回EFAULT给用户            │          │
    │                   └──────────────────────────────┘          │
    └─────────────────────────────────────────────────────────────┘

### 核心数据结构

异常表条目 (8字节对齐):

    ┌─────────────────┬──────────────────┐
    │  指令相对偏移     │  修复代码相对偏移   │
    │    (4 bytes)    │    (4 bytes)     │
    └─────────────────┴──────────────────┘

设计要点:

- 使用相对偏移支持ASLR(地址空间布局随机化)

- 8字节对齐提高缓存性能

- 存储于只读段防止篡改

### 工作流程

    编译期:
        源码 ──→ 带标记的指令 ──→ 生成异常表条目 ──→ 链接到内核镜像
                  (rep movsb)       (insn→fixup)

    运行期:
        执行拷贝 ──→ 触发页错误? ─否→ 正常返回
                         │
                        是
                         ↓
                  查找异常表 ──→ 找到? ─否→ 内核panic
                                  │
                                 是
                                  ↓
                        修改RIP到修复代码 ──→ 返回错误码

## 典型执行场景

### 场景: 系统调用传入无效地址

以`open()`系统调用为例,展示异常表的工作过程:

    用户程序: open(0x1000, O_RDONLY)  // 0x1000未映射
             │
             ↓
        ┌────────────────────────────────┐
        │ 1. 进入系统调用                  │
        │    ├─ 解析路径字符串             │
        │    └─ 逐字节拷贝直到'\0'         │
        └────────────────────────────────┘
             │
             ↓
        ┌────────────────────────────────┐
        │ 2. 拷贝第一个字节时触发页错误      │
        │    (地址0x1000不在VMA中)         │
        └────────────────────────────────┘
             │
             ↓
        ┌────────────────────────────────┐
        │ 3. 页错误处理器                  │
        │    ├─ 检测到访问用户地址          │
        │    ├─ 查找异常表                 │
        │    └─ 找到对应的修复代码          │
        └────────────────────────────────┘
             │
             ↓
        ┌────────────────────────────────┐
        │ 4. 修改指令指针到修复代码          │
        │    └─ 设置返回值为错误码          │
        └────────────────────────────────┘
             │
             ↓
        ┌────────────────────────────────┐
        │ 5. 系统调用返回EFAULT            │
        └────────────────────────────────┘
             │
             ↓
    用户程序: fd = -1, errno = EFAULT

关键点:

- 无需预检查地址有效性

- 页错误自动转换为错误码

- 内核不会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`指令序列

核心思想保持不变,只需调整汇编语法。

Other Versions v: V0.3.0

Tags
  V0.1.0
  V0.1.1
  V0.1.10
  V0.1.2
  V0.1.3
  V0.1.4
  V0.1.5
  V0.1.6
  V0.1.7
  V0.1.8
  V0.1.9
  V0.2.0
  V0.3.0
  V0.4.0

Branches
  master