犀牛鸟 2026 研究笔记 estelledc.github.io

附录 W:KVM 深层机制——从硅片到虚拟机的完整旅程

W.1 为什么需要硬件虚拟化

想象你是一个剧场导演。你有一个舞台(CPU),上面只能演一出戏(一个操作系统)。但你想同时排练三出戏。最笨的办法是让三个剧组轮流上台,每次都要搬道具、换布景(纯软件模拟)。聪明的办法是把舞台本身设计成可以瞬间切换的——按一个按钮,灯光、音效、布景全部切换到另一出戏的状态(硬件虚拟化)。

这就是 KVM 的核心思想:让 CPU 硬件本身支持”多出戏同时存在”的能力。

在没有硬件虚拟化之前,虚拟化软件需要做大量”翻译”工作。客户操作系统(Guest OS)想执行一条特权指令(比如修改页表、访问硬件),虚拟化层必须拦截这条指令,模拟它的效果,然后把结果返回给 Guest。这个过程极其缓慢——每条特权指令都要经过”拦截→分析→模拟→返回”四步。

Intel VT-x(2005年)和 AMD-V(2006年)的出现改变了一切。它们在 CPU 硬件层面增加了一组新的运行模式:

关键创新在于:Guest 模式下的代码几乎可以直接在 CPU 上执行,不需要翻译。只有当 Guest 执行了某些”敏感操作”时,CPU 才会自动从 Guest 模式切换回 Host 模式(这个切换叫 VM Exit),让 KVM 来处理。

W.2 VMCS:虚拟机的”记忆宫殿”

VMCS(Virtual Machine Control Structure)是每个虚拟 CPU 对应的一块内存区域,大小为 4KB。你可以把它想象成一个”记忆宫殿”——当 CPU 要从一出戏切换到另一出戏时,它需要知道:上一出戏演到哪了(Guest 状态区)、导演在做什么(Host 状态区)、什么时候需要叫导演来处理(VM-Exit 控制区)、进场前要做什么准备(VM-Entry 控制区)。

VMCS 中的关键字段:

Guest 状态区包含所有通用寄存器的值(RAX, RBX, RCX, RDX, RSI, RDI, RSP, RBP, R8-R15)、段寄存器(CS, DS, SS, ES, FS, GS)及其隐藏部分(Base, Limit, Access Rights)、控制寄存器(CR0, CR3, CR4,其中 CR3 尤其重要,它指向 Guest 的页表)、IDTR 和 GDTR(中断描述符表和全局描述符表的位置)、RIP(指令指针)和 RFLAGS(标志寄存器)、若干 MSR(Model Specific Register)的值。

Host 状态区包含 Host 返回时需要恢复的寄存器(RSP, RIP, CR0, CR3, CR4 等)、Host 的段选择子、Host 的 IDTR 和 GDTR。

VM-Exit 控制区定义了哪些事件会触发 VM Exit。Pin-Based Controls 控制外部中断、NMI、虚拟中断。Primary Processor-Based Controls 控制 HLT、MWAIT、RDPMC、MOV to CR3 等。Secondary Processor-Based Controls 控制 EPT 启用、VPID 启用、unrestricted guest 等。Exit Controls 决定是否保存/加载某些 MSR、64位 Host 等。

VM-Entry 控制区定义了进入 Guest 时的行为:是否注入事件(中断、异常)、是否从 SMM 返回、是否在 IA-32e(64位)模式。

让我用一个比喻来理解 VMCS 的工作过程。想象两个厨师共用一个厨房(CPU):厨师 A(Guest)在做法式大餐,厨师 B(Host/KVM)是厨房经理。VMCS 就是厨房墙上的白板:左半边写着厨师 A 的状态(”正在煎鱼,火候 6,定时器还有 3 分钟”),右半边写着厨师 B 的状态(”在办公室,等呼叫”),下面写着规则(”如果需要用烤箱,必须叫经理来”)。当厨师 A 需要用烤箱时:厨师 A 停下(VM Exit)→ 白板自动更新厨师 A 的当前状态 → 厨师 B 来到厨房恢复自己的状态 → 厨师 B 帮忙设置好烤箱 → 白板更新 → 厨师 A 恢复工作(VM Entry)。

W.3 VM Entry 和 VM Exit 的微观过程

VM Entry(从 Host 进入 Guest)的步骤:

1. 软件(KVM)执行 VMLAUNCH 或 VMRESUME 指令
2. CPU 检查 VMCS 的合法性(consistency checks)
   - 如果检查失败,VM Entry 不会发生,CPU 设置错误标志
3. CPU 加载 Guest 状态:
   a. 从 VMCS Guest 区加载 CR0, CR3, CR4
   b. 加载段寄存器及其隐藏部分
   c. 加载 GDTR, IDTR
   d. 加载 RSP, RIP, RFLAGS
   e. 加载 PDPTE(如果使用 PAE 分页)
   f. 加载 MSR(根据 VM-Entry MSR-load area)
4. 如果 VM-Entry 控制指定了事件注入:
   a. 注入指定的中断/异常到 Guest
5. CPU 切换到 VMX Non-Root 模式
6. Guest 从 VMCS 中保存的 RIP 处开始执行

VM Exit(从 Guest 返回 Host)的步骤:

1. 触发条件满足(Guest 执行了 CPUID / 外部中断到来 / EPT 违规等)
2. CPU 保存 Guest 状态到 VMCS Guest 区:
   a. 保存 CR0, CR3, CR4 和所有段寄存器
   b. 保存 GDTR, IDTR, RSP, RIP, RFLAGS
   c. 保存 VM-Exit 原因(exit reason)和相关信息(exit qualification)
3. CPU 加载 Host 状态:
   a. 从 VMCS Host 区加载 CR0, CR3, CR4, RSP, RIP
   b. 加载段寄存器, GDTR, IDTR
   c. 加载 MSR(根据 VM-Exit MSR-load area)
4. CPU 切换到 VMX Root 模式
5. Host(KVM 代码)从 VMCS Host 区保存的 RIP 处开始执行

性能关键点:一次 VM Exit + VM Entry 的完整循环大约需要 500-2000 个 CPU 周期。在主频 3GHz 的 CPU 上,这大约是 150-650 纳秒。看起来很快,但如果每秒发生数万次 VM Exit,累积的开销就很可观了。这就是为什么减少 VM Exit 次数是虚拟化性能优化的核心方向。

W.4 EPT:两级页表的精妙设计

在没有 EPT 之前,虚拟化软件使用”影子页表”(Shadow Page Table)来处理 Guest 的内存访问。类比:你住在一栋公寓(Guest),你以为你的门牌号是 301(Guest Virtual Address),公寓管理处告诉你实际地址是 A栋3楼1号(Guest Physical Address)。但实际上,A栋3楼1号在整个城市规划中的真实位置是 XX区YY路ZZ号(Host Physical Address)。影子页表的做法是给你一份”假地图”,上面直接写着”301 → XX区YY路ZZ号”,跳过了中间步骤。但每次 Guest 修改自己的页表,影子页表就必须同步更新,这个同步过程需要 VM Exit,非常昂贵。

EPT 的做法则优雅得多:让 CPU 硬件支持两级地址翻译。

Guest 虚拟地址 (GVA)
    ↓ [Guest 页表,Guest 自己维护]
Guest 物理地址 (GPA)
    ↓ [EPT 页表,KVM/Hypervisor 维护]
Host 物理地址 (HPA)

EPT 的页表结构和普通 x86-64 页表几乎一样,也是 4 级:EPT PML4(第4级)→ EPT PDPT(第3级)→ EPT PD(第2级)→ EPT PT(第1级)→ 物理页帧。每级的页表项都是 8 字节,包含物理地址的高位、权限位(Read/Write/Execute)、内存类型(UC, WB, WT 等)、Accessed 和 Dirty 位、大页支持(2MB 在 PD 级,1GB 在 PDPT 级)。

EPT 违规(EPT Violation):当 Guest 访问的 GPA 在 EPT 中没有映射或权限不足时,CPU 触发 EPT Violation 产生 VM Exit。KVM 捕获后检查 GPA 是否合法,合法则分配物理页帧并建立映射,MMIO 区域则模拟设备行为。

TLB 和 VPID:EPT 引入了额外的地址翻译层(共 8 次内存访问)。VPID(Virtual Processor ID)给每个 vCPU 分配唯一 ID,TLB 条目中记录该 ID,VM Exit/Entry 时不需要刷新 TLB。

W.5 VT-x 中的中断虚拟化

中断处理是虚拟化中最频繁的操作之一。Intel 提供了多级中断虚拟化优化:

第一级——外部中断直接 Exit:最简单。外部中断总导致 VM Exit,KVM 处理后在下次 VM Entry 时通过 VMCS 注入。

第二级——Virtual Interrupt Delivery:CPU 硬件直接向 Guest 投递虚拟中断,不需要 VM Exit。

第三级——APICv(Virtual APIC):Guest 读写 APIC 寄存器不需要 VM Exit,CPU 硬件直接处理。

最高级——Posted Interrupts:设备中断可以直接修改 Guest 的虚拟 APIC 页面,全程不需要 VM Exit。

W.6 KVM 在 Linux 中的架构位置

KVM 不是独立程序,而是 Linux 内核模块(kvm.ko + kvm-intel.ko/kvm-amd.ko)。它利用已有的 Linux 基础设施:

用户空间: QEMU / Cloud Hypervisor / Firecracker(设备模拟、virtio 后端)
    │ ioctl(/dev/kvm)
内核空间: KVM 模块(vCPU 管理 + 内存管理 + 中断/IO 处理)
    │
Linux 内核基础设施(调度器 + 内存管理 + 文件系统 + 网络栈)
    │
硬件: CPU (VT-x/AMD-V) + 内存 + 设备

KVM 的核心 API 通过 /dev/kvm 设备文件暴露:

// 创建虚拟机
int vm_fd = ioctl(kvm_fd, KVM_CREATE_VM, 0);

// 设置内存区域(建立 GPA → HPA 的映射)
struct kvm_userspace_memory_region region = {
    .slot = 0,
    .guest_phys_addr = 0x0,
    .memory_size = 256 * 1024 * 1024,  // 256MB
    .userspace_addr = (uint64_t)mmap(...)
};
ioctl(vm_fd, KVM_SET_USER_MEMORY_REGION, &region);

// 创建虚拟 CPU 并运行
int vcpu_fd = ioctl(vm_fd, KVM_CREATE_VCPU, 0);
struct kvm_run *run = mmap(..., vcpu_fd, ...);
while (1) {
    ioctl(vcpu_fd, KVM_RUN, 0);  // VM Entry → Guest 执行 → VM Exit
    switch (run->exit_reason) {
    case KVM_EXIT_IO:       handle_io(run);   break;
    case KVM_EXIT_MMIO:     handle_mmio(run); break;
    case KVM_EXIT_HLT:      break;
    case KVM_EXIT_SHUTDOWN: return;
    }
}

关键设计决策:KVM 把每个 vCPU 当作 Linux 中的一个普通线程来调度。这意味着 CFS 调度器管理 vCPU 时间片、cgroup 可以限制资源、CPU pinning 可以绑核、标准工具(top/ps/perf)都能监控。

W.7 KVM 与沙箱项目的具体联系

Firecracker 直接使用 KVM API,是最”薄”的 VMM,只实现最少的虚拟设备。CubeSandbox 通过 Cloud Hypervisor 间接使用 KVM,CubeCoW 快照回滚本质上是保存和恢复 VMCS + 内存状态。Kata Containers 使用 QEMU 或 Cloud Hypervisor 作为 VMM,底层都是 KVM。gVisor 在 ptrace 模式下不使用 KVM,在 KVM 平台模式下可选使用 KVM 来拦截系统调用。

W.8 性能调优:减少 VM Exit 的实战技术

技术 减少的 VM Exit 类型 效果
EPT Large Pages EPT Violation 减少页表级数,降低 TLB miss
VPID TLB flush on exit 避免刷新 TLB
APICv APIC 寄存器访问 Guest 内部中断零 exit
Posted Interrupts 外部中断投递 设备中断直接到 Guest
PML (Page Modification Logging) Dirty page tracking 脏页追踪无需 exit
MSR Bitmap MSR 读写 选择性放行
I/O Bitmap 端口 I/O 选择性放行
PAUSE-Loop Exiting 自旋锁 避免 PAUSE 频繁 exit

附录 X:Copy-on-Write 完全指南——从操作系统到沙箱快照

X.1 生活中的 Copy-on-Write

想象一个图书馆场景。图书馆有一本很贵的百科全书(原始数据),10 个学生都想”拥有”这本书。策略 A(Eager Copy)给每个学生复印一套完整的百科全书,代价巨大。策略 B(Copy-on-Write)告诉 10 个学生:”你们都可以看这本原版书。但如果谁要在书上写笔记,那个人再复印被写的那一页。”

这就是 CoW 的本质:推迟复制,只复制真正被修改的部分。

CoW 出现在多个层面:进程管理(fork 后父子共享物理页面)、文件系统(Btrfs/ZFS 修改时分配新块)、虚拟机快照(只记录变化的内存页)、容器镜像(OverlayFS 分层)、数据库(PostgreSQL MVCC)、沙箱(CubeCoW / Firecracker snapshot)。

X.2 Linux fork() 中的 CoW 实现

Linux 的 fork() 是 CoW 最经典的实现:

第一步:复制页表(只复制页表本身,不复制物理页帧)
  父进程:VAddr 0x1000 → PAddr 0x5000 [改为 R-only]
  子进程:VAddr 0x1000 → PAddr 0x5000 [R-only]
  两进程共享物理内存,增加 refcount

第二步:当某个进程尝试写入时
  1. CPU 尝试写入 → 页表只读 → Page Fault
  2. 内核检查:CoW 页面?refcount > 1?
  3. 分配新物理页帧,复制内容
  4. 更新写入者的页表指向新页帧 [R/W]
  5. 原页帧 refcount 减 1

这让 fork() 几乎瞬时完成——只复制页表(几 KB),不复制数据(可能几 GB)。

X.3 文件系统层面的 CoW

OverlayFS(容器使用的分层文件系统):

merged(容器视图)
  ├── upperdir(容器可写层)← 修改的文件存这里
  └── lowerdir(镜像只读层)← 原始镜像,多容器共享

读取:直接从 lowerdir 读(零开销)
修改:copy-up 到 upperdir 再修改
删除:在 upperdir 创建 whiteout 标记

Btrfs/ZFS 在数据块级别实现 CoW:修改 block 时分配新 block 写入修改内容,更新 inode 指针,旧 block 如有快照引用则保留。快照就是保存某时间点的 inode 树根,共享的块零额外空间。

X.4 CubeCoW:沙箱级别的 Copy-on-Write

CubeCoW 是 CubeSandbox 的核心创新,将 CoW 概念应用到整个虚拟机级别:

传统方式启动 1000 个相同的沙箱:
  启动 1 个模板 VM → 复制 1000 次完整内存镜像(每个 512MB)
  总内存消耗:512GB    启动时间:1000 × 数秒 = 数千秒

CubeCoW 方式:
  启动 1 个模板 VM → 创建 1000 个 CoW 克隆
  初始内存消耗:512MB(共享)  启动时间:1000 × ~5ms = 5秒
  运行时:每个克隆只消耗它实际修改的页面(通常 < 50MB)
  总内存消耗:512MB + 1000 × 50MB = ~50GB(节省 90%)

CubeCoW 的实现细节:

模板 VM 的 EPT 页表:
  GPA 0x0000_0000 → HPA 0x1000_0000 [R/W]
  GPA 0x0000_1000 → HPA 0x1000_1000 [R/W]
  ...

创建克隆时:
  1. 复制 EPT 页表结构(不复制物理内存)
  2. 将模板和克隆的 EPT 条目都标记为只读
  3. 增加每个 HPA 页帧的引用计数

克隆 VM 写入 GPA 0x0000_1000 时:
  1. EPT Violation(写只读页面)→ VM Exit
  2. KVM/CubeCoW 处理程序:
     a. 检查引用计数 > 1 → 需要 CoW
     b. 分配新 HPA 页帧 0x2000_1000
     c. 复制 HPA 0x1000_1000 → HPA 0x2000_1000
     d. 更新克隆的 EPT:GPA 0x0000_1000 → HPA 0x2000_1000 [R/W]
     e. 原页帧 refcount 减 1
  3. VM Entry,克隆继续执行

X.5 快照与时间旅行

CubeCoW 的快照功能让 AI Agent 可以”时间旅行”:

Agent 探索路径:
  状态 S0 → 动作 A1 → 状态 S1 → 动作 A2 → 状态 S2(死胡同!)
                              ↓
                     快照点(checkpoint)

回滚到 S1:
  1. 保存 S1 时刻的 VMCS(所有 CPU 寄存器状态)
  2. 保存 S1 时刻的 EPT 页表
  3. 保存 S1 时刻已修改的页面列表(dirty pages)
  4. 回滚时:恢复 VMCS + 恢复 EPT + 丢弃 S1 之后的 dirty pages

成本分析:
  - 创建快照:O(dirty_pages) 时间,通常 < 1ms
  - 恢复快照:O(dirty_pages_since_snapshot) 时间
  - 如果 Agent 每步只修改少量内存,快照/恢复几乎瞬时

这对 RL(强化学习)训练极其重要:

for episode in range(10000):
    sandbox.restore_snapshot("clean_state")  # 5ms
    reward = agent.explore(sandbox)           # Agent 在沙箱中尝试
    agent.learn(reward)                       # 从结果中学习

# 无 CoW:每次 restore 需要 200ms(复制完整内存)→ 总计 2000s
# 有 CubeCoW:每次 restore 需要 5ms → 总计 50s(40x 加速)

X.6 CoW 的陷阱和代价

CoW 不是免费的午餐。以下是常见陷阱:

陷阱一——写入放大。如果 fork 后子进程修改所有页面,CoW 的总开销反而比直接复制更大(因为多了 Page Fault 处理的开销)。对策:如果预知会大量写入,考虑 vfork() 或直接全量复制。

陷阱二——内存碎片。CoW 按页(4KB)粒度操作。即使只修改一个字节,也要复制整个 4KB 页面。对策:CubeCoW 使用 2MB 大页减少页表条目数量,但单次 CoW 复制的粒度也变大了。需要根据工作负载选择合适的页面大小。

陷阱三——引用计数开销。每个共享页面都需要维护引用计数,这本身消耗内存(每个页帧 4-8 字节),并且在高并发场景下 refcount 更新可能成为瓶颈。

陷阱四——写入抖动。在 CubeCoW 场景下,如果 1000 个 Agent 同时开始写入(比如同时启动程序),会在短时间内产生大量 EPT Violation,导致 CPU 繁忙处理 CoW 而不是执行 Guest 代码。对策:预热(pre-touch)可预测的热页面、错开启动时间。


附录 Y:eBPF 在沙箱网络中的深度应用

Y.1 什么是 eBPF?

想象你家的水管系统(Linux 内核网络栈)。水(数据包)从外面流进来,经过很多管道(协议栈各层),最后到达你的水龙头(应用程序)。

以前如果你想在水管上加个过滤器(比如净水器),你有两个选择:一是改造整个水管系统(修改内核代码,重新编译,重启),二是在水龙头末端加个外挂过滤器(用户空间代理,但水已经流了全程,浪费了)。

eBPF 提供了第三种选择:你可以在水管的任意位置安装一个”即插即用”的小型过滤器,不需要改造水管本身,也不需要停水(不重启系统),而且过滤器的性能几乎和水管本身一样快。

技术上说,eBPF(extended Berkeley Packet Filter)是一个运行在 Linux 内核中的虚拟机。它允许用户编写小程序,编译成 eBPF 字节码,然后安全地加载到内核的各个”挂载点”(hook point)执行。

Y.2 eBPF 的安全保证

eBPF 程序在加载到内核前必须通过验证器(verifier)的检查:

eBPF 验证器确保:
1. 程序一定会终止(不允许无限循环,通过限制指令数实现,默认 100 万条)
2. 所有内存访问都在合法范围内(不能越界读写)
3. 所有指针在使用前都经过空值检查
4. 栈空间有限制(512 字节)
5. 不能调用任意内核函数(只能调用预定义的 helper 函数)
6. 不能有未初始化的寄存器使用
7. 每条路径都必须正确返回

这些保证让 eBPF 程序不可能导致内核崩溃或安全漏洞。

Y.3 eBPF 在容器网络中的作用

传统容器网络(使用 iptables)的数据路径:

外部数据包到达容器的过程(传统方式):

物理网卡
  ↓
主机网络栈(第一次协议栈遍历)
  ↓
iptables 规则链(PREROUTING → FORWARD → POSTROUTING)
  ↓  每条规则都是线性匹配,O(n) 复杂度
veth pair(虚拟以太网对)
  ↓
容器网络命名空间
  ↓
容器内网络栈(第二次协议栈遍历)
  ↓
容器内应用

问题:
- 两次完整协议栈遍历(每次涉及 L2→L3→L4 的完整处理)
- iptables 规则线性匹配(K8s 大集群可能有数万条规则)
- 每个 veth pair 引入额外的软中断处理

使用 eBPF(Cilium)的数据路径:

物理网卡
  ↓
TC(Traffic Control)层 ← eBPF 程序在这里拦截
  ↓  直接做 L3/L4 转发决策(跳过大部分协议栈)
  ↓  使用 eBPF Map 做 O(1) 规则查找
容器网络命名空间(通过 bpf_redirect)
  ↓
容器内应用

优势:
- 跳过 iptables 的线性匹配 → O(1) 查找
- 跳过 veth pair 的第二次协议栈遍历 → 延迟降低 50%+
- 规则更新是原子的(更新 eBPF Map 而不是重建规则链)

Y.4 eBPF 在沙箱场景的具体应用

应用一:沙箱网络隔离

每个 AI Agent 沙箱需要严格的网络隔离。使用 eBPF 可以在不创建额外网络设备的情况下实现隔离:

// 简化的 eBPF 沙箱网络策略程序
SEC("tc/ingress")
int sandbox_policy(struct __sk_buff *skb) {
    struct iphdr *ip = get_ip_header(skb);
    __u32 src_ip = ip->saddr;
    __u32 dst_ip = ip->daddr;
    __u16 dst_port = get_dst_port(skb);
    
    // 查找沙箱的网络策略(O(1) Hash Map 查找)
    __u32 sandbox_id = get_sandbox_id(skb);
    struct sandbox_policy *policy = bpf_map_lookup_elem(&policy_map, &sandbox_id);
    if (!policy) return TC_ACT_SHOT;  // 无策略则丢弃
    
    // 检查目标是否在允许列表中
    struct endpoint_key key = { .ip = dst_ip, .port = dst_port };
    if (bpf_map_lookup_elem(&policy->allowed_endpoints, &key))
        return TC_ACT_OK;   // 允许
    
    // 检查是否访问其他沙箱(跨沙箱隔离)
    if (is_sandbox_ip(dst_ip) && get_sandbox_for_ip(dst_ip) != sandbox_id)
        return TC_ACT_SHOT; // 禁止跨沙箱通信
    
    return TC_ACT_SHOT;     // 默认拒绝
}

应用二:网络带宽限制

// eBPF 令牌桶限速
struct token_bucket {
    __u64 tokens;
    __u64 last_refill;
    __u64 rate;           // 填充速率(bytes/ns)
    __u64 burst;          // 桶容量
};

SEC("tc/egress")
int bandwidth_limit(struct __sk_buff *skb) {
    __u32 sandbox_id = get_sandbox_id(skb);
    struct token_bucket *bucket = bpf_map_lookup_elem(&rate_limits, &sandbox_id);
    if (!bucket) return TC_ACT_SHOT;
    
    __u64 now = bpf_ktime_get_ns();
    __u64 elapsed = now - bucket->last_refill;
    __u64 new_tokens = elapsed * bucket->rate / 1000000000;
    bucket->tokens = min(bucket->tokens + new_tokens, bucket->burst);
    bucket->last_refill = now;
    
    __u32 pkt_len = skb->len;
    if (bucket->tokens >= pkt_len) {
        bucket->tokens -= pkt_len;
        return TC_ACT_OK;
    }
    return TC_ACT_SHOT;      // 超速,丢弃
}

应用三:DNS 劫持和审计

SEC("tc/egress")
int dns_redirect(struct __sk_buff *skb) {
    struct udphdr *udp = get_udp_header(skb);
    if (udp->dest != bpf_htons(53)) return TC_ACT_OK;
    
    // 记录 DNS 查询(审计)
    struct dns_query_event event = {
        .sandbox_id = get_sandbox_id(skb),
        .timestamp = bpf_ktime_get_ns(),
    };
    parse_dns_query(skb, &event.domain);
    bpf_perf_event_output(skb, &dns_events, 0, &event, sizeof(event));
    
    // 重定向到安全 DNS
    struct iphdr *ip = get_ip_header(skb);
    ip->daddr = SAFE_DNS_IP;
    bpf_l3_csum_replace(skb, IP_CSUM_OFF, ip->daddr, SAFE_DNS_IP, 4);
    return TC_ACT_OK;
}

应用四:沙箱逃逸检测

SEC("tracepoint/raw_syscalls/sys_enter")
int detect_escape(struct trace_event_raw_sys_enter *ctx) {
    __u32 pid = bpf_get_current_pid_tgid() >> 32;
    __u32 *sandbox_id = bpf_map_lookup_elem(&pid_to_sandbox, &pid);
    if (!sandbox_id) return 0;
    
    __u64 syscall_nr = ctx->id;
    
    if (syscall_nr == __NR_mount ||
        syscall_nr == __NR_pivot_root ||
        syscall_nr == __NR_unshare ||
        syscall_nr == __NR_clone3) {
        
        struct escape_alert alert = {
            .sandbox_id = *sandbox_id,
            .syscall_nr = syscall_nr,
            .pid = pid,
            .timestamp = bpf_ktime_get_ns(),
        };
        bpf_perf_event_output(ctx, &alerts, 0, &alert, sizeof(alert));
    }
    return 0;
}

Y.5 eBPF 程序的生命周期

编写源码(C + eBPF 头文件)
    ↓
编译为 eBPF 字节码(clang -target bpf)
    ↓
加载到内核(bpf() 系统调用)
    ↓
验证器检查(安全性保证)
    ↓
JIT 编译为本机机器码(接近原生性能)
    ↓
附加到挂载点(TC, XDP, tracepoint, kprobe 等)
    ↓
运行(每次事件触发时执行)
    ↓
卸载(detach + close fd)

Y.6 eBPF Map 类型与沙箱的对应关系

Map 类型 用途 沙箱场景
BPF_MAP_TYPE_HASH 通用 K-V 存储 沙箱 ID → 网络策略
BPF_MAP_TYPE_ARRAY 固定大小数组 沙箱统计计数器
BPF_MAP_TYPE_LRU_HASH 自动淘汰的 K-V 连接追踪缓存
BPF_MAP_TYPE_RINGBUF 高性能事件环 审计日志输出
BPF_MAP_TYPE_LPM_TRIE 最长前缀匹配 IP CIDR 规则匹配
BPF_MAP_TYPE_PROG_ARRAY 尾调用跳转表 多阶段策略链

Y.7 XDP:最早期的数据包拦截

XDP(eXpress Data Path)是 eBPF 可以附着的最早期网络挂载点——在网卡驱动层面,数据包刚被 DMA 到内存就可以处理:

数据包到达网卡 → DMA 到 Ring Buffer → XDP eBPF 程序执行 → 决策:
  XDP_DROP:    直接丢弃(不进入内核协议栈)
  XDP_TX:      原路发回(可修改后反射)
  XDP_REDIRECT: 重定向到另一个网卡/CPU/AF_XDP socket
  XDP_PASS:    正常进入内核协议栈

性能:单核可达 24Mpps(每秒 2400 万包)
对比:iptables 同场景约 3-5Mpps

在沙箱场景中,XDP 用于 DDoS 防护:如果某个沙箱被外部攻击,可以在最早期丢弃攻击流量,不浪费任何 CPU 资源。


附录 Z:VirtIO 设备模型——虚拟硬件的通用语言

Z.1 为什么需要 VirtIO?

想象你是一个翻译(虚拟化层),需要帮两个说不同语言的人(Guest OS 和 Host 硬件)交流。有三种翻译策略:

策略一——模拟真实设备:Guest 以为自己在跟一块真实的 Intel 网卡(e1000)说话。你需要完整模拟 e1000 的所有硬件寄存器、中断行为、DMA 引擎。好处是 Guest 不需要任何修改(用已有驱动就行)。坏处是模拟开销巨大,每次寄存器访问都是 VM Exit。

策略二——直通设备(Passthrough):把真实物理设备直接分配给 Guest,不经过虚拟化层。好处是零开销、原生性能。坏处是一个设备只能给一个 Guest 用,不能共享;且迁移虚拟机时设备不能跟着走。

策略三——VirtIO(半虚拟化设备):Guest 知道自己在虚拟化环境中,使用专门为虚拟化设计的高效设备接口。Guest 和 Host 之间通过共享内存队列通信,避免频繁的 VM Exit。

Z.2 VirtQueue:高效通信的核心

VirtQueue 是一个基于共享内存的环形缓冲区,由三个区域组成:

Descriptor Table(描述符表):
  每个描述符指向一块 Guest 内存(数据缓冲区)
  字段:addr(物理地址), len(长度), flags(设备可读/可写), next(链表)

Available Ring(可用环,Guest → Host 方向):
  Guest 把准备好的描述符链索引放到这里通知 Host
  字段:flags, idx(下一个写入位置), ring[N](描述符索引数组)

Used Ring(已用环,Host → Guest 方向):
  Host 处理完后把描述符放回这里通知 Guest
  字段:flags, idx, ring[N](描述符索引 + 写入字节数)

一次 VirtIO 网络发送的完整流程:

Guest 内部:
1. 分配数据缓冲区,填入网络数据包
2. 填写 Descriptor:addr=缓冲区地址, len=包长度, flags=0
3. 将 Descriptor index 放入 Available Ring
4. 更新 Available Ring 的 idx
5. 写 VirtIO 通知寄存器(可能触发 VM Exit)

Host(VMM)侧:
6. 收到通知,检查 Available Ring
7. 读取 Descriptor,通过 EPT 访问 Guest 内存中的数据包
8. 将数据包发送到真实网络
9. 将处理完的 Descriptor index 放入 Used Ring
10. 更新 Used Ring 的 idx
11. 注入虚拟中断通知 Guest

Guest 内部:
12. 收到中断,检查 Used Ring
13. 确认数据包已发送,回收缓冲区

Z.3 Firecracker 的极简 VirtIO 实现

Firecracker 的虚拟设备清单(全部):
├── virtio-net     × 1(沙箱网络)
├── virtio-block   × 1(根文件系统)
├── virtio-vsock   × 1(Host-Guest 通信通道)
├── serial console × 1(调试输出)
├── i8042          × 1(仅用于 Ctrl+Alt+Del)
└── RTC            × 1(实时时钟)

总共 6 个设备。攻击面极小。

对比 QEMU:几十种设备,攻击面是 Firecracker 的 10-50 倍。

Z.4 virtio-vsock:沙箱通信的秘密通道

AI Agent 需要和外部世界通信(接收指令、返回结果),但不能用普通网络。virtio-vsock 提供专用的 Host↔Guest 通道:

Agent(Guest)                  Host(Orchestrator)
  socket(AF_VSOCK)               socket(AF_VSOCK)
  connect(CID, port)             listen(CID, port)
         │                              │
    ┌────┴──────────────────────────────┴────┐
    │          virtio-vsock 设备              │
    │  (通过 VirtQueue 传输,不经过网络栈)     │
    └────────────────────────────────────────┘

优势:
- 不需要 IP 地址分配和路由配置
- 不经过内核网络栈(TCP/IP),延迟更低
- 无法被 Guest 用来访问外部网络
- CID(Context ID)天然隔离不同 VM

Z.5 virtio-fs:零拷贝文件共享

virtio-fs + DAX 模式:
  Host: virtiofsd 导出 /workspace 目录
  Guest: mount -t virtiofs workspace /mnt/workspace
  
  Guest 读取文件时:
    DAX 模式直接映射 Host 的页面缓存到 Guest 地址空间
    零拷贝,不经过 VirtQueue
    性能接近 Host 原生(90%+)

Z.6 vhost 和 vhost-user:性能极限

标准 VirtIO:Guest → VM Exit → KVM → 用户态 VMM → 处理
vhost:      Guest → eventfd → 内核 vhost 线程 → 直接处理
vhost-user: Guest → 共享内存 → 用户态 DPDK 进程 → 轮询处理

每一级都减少了上下文切换开销。

附录 AA:Syscall 拦截全链路——从 gVisor Sentry 到 seccomp-BPF

AA.1 系统调用是什么?

想象你住在一个高安全小区(用户空间)。你想取快递(访问硬件资源),但你不能自己走出小区大门——必须告诉保安(内核)你需要什么,保安帮你去取。这个”告诉保安”的过程就是系统调用。

; x86-64 系统调用示例:write(1, "hello\n", 6)
mov rax, 1      ; 系统调用号:1 = write
mov rdi, 1      ; 文件描述符 1(stdout)
mov rsi, msg    ; 缓冲区地址
mov rdx, 6     ; 字节数
syscall         ; 触发系统调用

AA.2 为什么沙箱需要拦截系统调用?

Linux 内核有约 450 个系统调用,每个都是潜在攻击面。不同方案的拦截策略:

方案 拦截方式 允许的 syscall 数 安全级别
Docker 默认 seccomp-BPF ~300/450
gVisor Sentry 完全接管 自实现 ~200 极高
Firecracker Guest 内核 + seccomp VMM Guest 内全部,VMM 约 30 极高
Kata Guest 内核处理 Guest 内全部

AA.3 seccomp-BPF:内核级系统调用过滤

// seccomp-BPF 过滤器示例
struct sock_filter filter[] = {
    BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr)),
    
    // 允许安全调用
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_read, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_write, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
    
    // 禁止危险调用
    BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_mount, 0, 1),
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_KILL),
    
    // 默认允许
    BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ALLOW),
};

prctl(PR_SET_NO_NEW_PRIVS, 1, 0, 0, 0);
prctl(PR_SET_SECCOMP, SECCOMP_MODE_FILTER, &prog);

seccomp 返回值决定系统调用命运:ALLOW(放行)、KILL(杀进程)、ERRNO(返回错误)、TRACE(通知 ptrace)、USER_NOTIF(转发给用户空间)。

AA.4 gVisor Sentry:用户空间内核

gVisor 的 Sentry 在用户空间完全重新实现系统调用:

传统容器:应用 → syscall → Linux 内核(450 个 syscall 攻击面)
gVisor:  应用 → syscall → Sentry(用户空间重实现)→ ~60 个 syscall → 内核

ptrace 模式:Sentry 通过 ptrace 拦截每个 syscall,在用户空间处理后返回结果。开销约 5-10x。

KVM 模式:应用在 VMX Non-Root 运行,syscall 直接跳转到 Sentry 代码(仍在用户态),约 2-3x 开销。

AA.5 Firecracker 的 seccomp 策略

Firecracker VMM 进程只允许约 30 个 syscall:read、write、ioctl(KVM)、mmap、close、epoll_wait 等。连 open 都没有——即使 Guest 逃逸到 VMM 进程,也无法打开任何新文件。这就是纵深防御。

AA.6 性能对比

文件创建(create + write + close)延迟:
  原生 Linux:             ~5μs
  Docker + seccomp:       ~6μs  (+20%)
  Firecracker (in-VM):    ~8μs  (+60%)
  gVisor (KVM platform):  ~15μs (+200%)
  gVisor (ptrace):        ~50μs (+900%)

附录 AB:OCI 运行时规范——容器世界的”通用语言”

AB.1 为什么需要标准?

就像电器插头需要标准一样,容器世界需要 OCI(Open Container Initiative)来定义容器的镜像格式和运行时行为,让不同工具可以互操作。

AB.2 OCI 运行时核心概念

一个 OCI 容器由 config.json + rootfs 目录组成:

{
    "ociVersion": "1.0.2",
    "process": {
        "args": ["python", "agent.py"],
        "env": ["PATH=/usr/bin:/bin", "SANDBOX_ID=abc123"],
        "cwd": "/workspace"
    },
    "root": { "path": "rootfs", "readonly": false },
    "linux": {
        "namespaces": [
            { "type": "pid" }, { "type": "network" },
            { "type": "ipc" }, { "type": "mount" },
            { "type": "cgroup" }, { "type": "user" }
        ],
        "resources": {
            "memory": { "limit": 536870912 },
            "cpu": { "quota": 100000, "period": 100000 }
        }
    }
}

AB.3 容器生命周期

[不存在] → create → [created] → start → [running] → kill/exit → [stopped] → delete → [不存在]

AB.4 不同运行时对 OCI 的实现

运行时 create 时做什么 start 时做什么
runc 创建 namespaces + cgroups fork 并 exec 用户进程
runsc (gVisor) 启动 Sentry + gofer 在 Sentry 中启动用户进程
kata-runtime 启动 microVM + Guest Agent 在 VM 内启动用户进程

AB.5 CRI 与 OCI 的关系

kubelet
  ↓ CRI (gRPC)
containerd / CRI-O
  ↓ OCI Runtime Spec
runc / runsc / kata-runtime
  ↓
实际的容器/VM/沙箱

containerd 通过 shim 机制支持多种运行时:containerd-shim-runc-v2、containerd-shim-runsc-v1、containerd-shim-kata-v2。


附录 AC:安全容器攻防实录——真实漏洞与防御分析

AC.1 攻击面层次

第 1 层:配置错误(容易)— 挂载 docker.sock、root 运行、缺 seccomp
第 2 层:内核漏洞(中等)— CVE-2022-0185、namespace 缺陷
第 3 层:虚拟化漏洞(困难)— KVM bug、设备模拟 bug
第 4 层:硬件漏洞(极难)— Spectre/Meltdown、Rowhammer

AC.2 CVE-2019-5736(runc 逃逸)

攻击者在容器内将 /bin/sh 替换为 /proc/self/exe 符号链接。当 docker exec 时,runc 进入容器 namespace 打开该链接,攻击者可通过 /proc/[runc-pid]/exe 覆写宿主机的 runc 二进制。

方案 是否受影响 原因
Docker/runc 修复前受影响 直接暴露 runc 二进制
gVisor 不受影响 不暴露真实 /proc/self/exe
Firecracker/Kata 不受影响 runc 在 VM 内,宿主机隔离

AC.3 Spectre 侧信道

利用 CPU 推测执行窥探其他 VM 的内存。防御:IBRS/IBPB(5-15% 性能损失)、Retpoline、CPU pinning、时间片隔离。

AC.4 CVE-2022-0847(Dirty Pipe)

允许非特权用户覆写任意只读文件。Docker 容器共享宿主机内核完全受影响;gVisor 重新实现了 pipe 不受影响;Firecracker/Kata 漏洞影响限制在 VM 内。

AC.5 供应链攻击与沙箱

AI Agent 最常见的攻击向量是恶意 npm/pip 包的 postinstall 脚本。防御层次:网络隔离(白名单)、文件系统隔离(只读/临时)、凭证隔离(不注入 API key)、eBPF 行为审计。


附录 AD:快照与时间旅行——AI Agent 训练的核心基础设施

AD.1 为什么 AI Agent 需要”时间旅行”?

就像迷宫游戏中的存档点。Agent 在沙箱中执行代码,需要频繁回到过去尝试不同策略。每次”恢复快照”,沙箱的完整状态都精确回到快照时刻。

AD.2 快照需要保存什么?

完整沙箱快照:CPU 状态(所有寄存器、VMCS)、内存状态(所有物理页面)、设备状态(virtio 队列指针)、文件系统状态(缓存 + 磁盘变更)、网络状态(TCP 连接、缓冲区)。

AD.3 快照实现方案对比

Full Snapshot:完整复制所有内存。保存 50-100ms,512MB 存储。实现简单但不适合频繁操作。

Incremental Snapshot(CubeCoW 方式):基于 CoW 只记录修改的页面。保存 <1ms,恢复 ~2ms。33x 加速。

Fork-based Snapshot:利用 Linux fork() 的 CoW 语义。父进程继续,子进程保持旧状态。不适合虚拟机。

AD.4 CubeCoW 快照实现

创建快照(~1ms):
  1. 暂停 vCPU
  2. 保存 VMCS + 设备状态
  3. EPT 页表做 CoW 标记(清除 Write 位)
  4. 保存 EPT 快照版本
  5. 恢复 vCPU

恢复快照(1-5ms):
  1. 暂停 vCPU
  2. 丢弃快照后新分配的页面
  3. 恢复 EPT 到快照版本
  4. 恢复 VMCS + 设备状态
  5. 恢复 vCPU

AD.5 快照树与并行探索

嵌套 CoW 支持树形快照结构。从同一快照分支出多个探索路径,共享的页面只存一份。1000 个分支,每个修改 5% 页面 → 节省 90%+ 内存。

AD.6 各产品快照对比

产品 快照技术 恢复时间
CubeSandbox CubeCoW (EPT) 1-5ms
Firecracker Full snapshot 50-100ms
E2B 文件系统级 100-500ms
QEMU QCOW2 + savevm 500ms-2s

附录 AE:沙箱编排模式——大规模 AI Agent 基础设施

AE.1 编排模式一:池化(Pooling)

预创建 N 个沙箱 → Agent 请求到来取一个 → 执行完毕清理归还。关键参数:池大小(根据 P99 并发决定)、预热时间(Firecracker ~125ms,CubeCoW ~5ms)。

AE.2 编排模式二:模板克隆(Template Cloning)

从预装好环境的模板 VM 做 CoW 克隆。启动 ~5ms,内存共享。适合大量 Agent 执行相似任务。

AE.3 编排模式三:工作流管道(Pipeline)

多阶段任务,每阶段不同沙箱配置:代码生成(2核/1GB/无网络)→ 测试执行(4核/4GB/受限网络)→ 部署验证(8核/16GB)。状态通过文件/API/共享存储传递。

AE.4 编排模式四:树形并行

从同一快照创建多个克隆并行尝试不同策略,第一个成功即终止其他分支。适合代码搜索/修复场景。

AE.5 资源调度

CPU 通常 2-4x 过量分配(Agent 多数时间等 LLM 响应)。亲和性调度:同 Agent 克隆放同一物理机(最大化 CoW 共享)。反亲和性:不同租户分散(安全隔离)。优先级抢占:高优先级 Agent 可抢占低优先级资源。

AE.6 健康检查和自愈

Level 1 存活检查(5s):vCPU 运行?OOM? Level 2 就绪检查(10s):Agent 响应 ping? Level 3 业务检查(30s):有进展?无死循环?


附录 AF:内存管理深潜——从物理内存分配到 NUMA 拓扑

AF.1 Buddy System

Linux 用伙伴系统管理物理页帧——像不断对半分割的巧克力板。需要小块时分割大块,释放时合并相邻空闲块。沙箱大量小页分配(CoW)会导致碎片化。

AF.2 内存气球(Balloon)

virtio-balloon 动态调整 VM 内存:膨胀时 Guest 交出内存给 Host,缩小时归还。用于 Agent 空闲时回收资源。

AF.3 NUMA 感知调度

多路服务器中访问远端内存延迟增加 50-100%。规则:vCPU 绑同一 NUMA node、内存从同 node 分配、CoW 共享页放最多克隆访问的 node。

AF.4 KSM(Kernel Same-page Merging)

异源共享:扫描发现内容相同的页面并合并为 CoW。同质 VM 场景节省 30-50%。但可被用于侧信道攻击(write 时间差异),安全容器通常禁用。


附录 AG:文件系统虚拟化——从 rootfs 到分层存储

AG.1 rootfs 构建方式

完整磁盘镜像(Firecracker):ext4 镜像文件作为 virtio-blk。简单隔离但不支持分层共享。

OverlayFS(Docker/gVisor):只读 lower + 可写 upper。空间效率高。

virtio-fs + DAX(CubeSandbox):Host 目录直接映射到 Guest,零拷贝。

QCOW2 CoW 镜像:backing file 共享基础镜像,每个 VM 只记录差异。

AG.2 9P 协议:gVisor 的文件通道

Sentry 通过受限的 gofer 进程访问宿主机文件系统。gofer 的 seccomp 只允许文件相关 syscall。即使 Sentry 被攻破,gofer 的限制阻止进一步攻击。

AG.3 文件系统性能优化

Page Cache 共享:多沙箱读相同文件只缓存一份。Lazy Loading:按需加载而非启动时全量。预读取:根据访问模式预测性加载。tmpfs:临时文件放内存文件系统避免磁盘 I/O。


附录 AH:网络命名空间——沙箱网络隔离的基石

AH.1 什么是网络命名空间?

每个命名空间拥有独立的:网络设备、IP 地址、路由表、iptables 规则、socket 列表、端口空间。不同命名空间的 bind(0.0.0.0:80) 互不冲突。

AH.2 沙箱网络拓扑方案

veth pair + bridge(Docker 默认):每容器一对虚拟网卡通过桥接连接。简单但大量容器时设备过多。

macvlan:物理网卡支持多 MAC 地址,每容器独立 MAC/IP。接近原生性能但需独立 IP。

TAP + 虚拟网络(Firecracker/Kata):TAP 设备连接 VMM,VM 内 virtio-net。完全隔离但多一层开销。

AH.3 CNI(Container Network Interface)

K8s 通过 CNI 插件为 Pod 配置网络。流程:kubelet 调用 CRI → containerd 创建网络命名空间 → 调用 CNI 插件(Calico/Cilium/Flannel)配置网络设备和路由 → Pod 获得网络连通性。

对于安全容器(Kata/gVisor),CNI 配置在 VM/Sentry 外部,通过 TAP/vsock 桥接到内部网络。


附录 AI:cgroup v2 与资源隔离——沙箱的”资源围栏”

AI.1 什么是 cgroup?

想象一个大学宿舍的电费管理。每个寝室有自己的电表(cgroup),学校可以给每个寝室设定用电上限(资源限制),还可以统计每个寝室用了多少电(资源统计)。如果某个寝室用电超标,可以被限速甚至断电(资源控制)。

cgroup(Control Group)就是 Linux 内核的”资源围栏”——把一组进程归到一起,对它们的资源使用进行统一管理。

AI.2 cgroup v2 vs v1

cgroup v1 的问题:每种资源控制器(CPU、内存、IO等)有独立的层级树,一个进程可以同时属于不同控制器的不同组,管理混乱。

cgroup v2 的改进:统一层级树,一个进程只属于一个 cgroup 节点,该节点可以启用多个控制器。

cgroup v2 目录结构:
/sys/fs/cgroup/
├── cgroup.controllers    # 可用控制器列表
├── cgroup.subtree_control # 子树中启用的控制器
├── sandbox/              # 沙箱 cgroup
│   ├── agent-001/        # Agent 1 的资源限制
│   │   ├── cpu.max       # CPU 限制
│   │   ├── memory.max    # 内存限制
│   │   ├── io.max        # IO 限制
│   │   ├── pids.max      # 进程数限制
│   │   └── cgroup.procs  # 该 cgroup 中的进程
│   ├── agent-002/
│   └── agent-003/

AI.3 各资源控制器详解

CPU 控制器

# 限制 Agent 使用最多 1 个 CPU 核心
echo "100000 100000" > cpu.max   # quota/period = 100ms/100ms = 1 核

# 限制使用 0.5 个核心
echo "50000 100000" > cpu.max    # 50ms/100ms = 0.5 核

# CPU 权重(多个 Agent 竞争时的分配比例)
echo "100" > cpu.weight          # 默认 100,范围 1-10000

内存控制器

# 内存硬限制(超过则 OOM kill)
echo "536870912" > memory.max    # 512MB

# 内存软限制(超过时优先回收)
echo "268435456" > memory.high   # 256MB

# 禁用 swap
echo "0" > memory.swap.max

# 查看当前使用量
cat memory.current               # 当前实际使用
cat memory.stat                  # 详细统计(cache, rss, etc.)

IO 控制器

# 限制磁盘 IO 速率(设备号 8:0 = /dev/sda)
echo "8:0 rbps=104857600 wbps=52428800" > io.max  # 读 100MB/s,写 50MB/s
echo "8:0 riops=1000 wiops=500" > io.max           # 读 1000 IOPS,写 500 IOPS

# IO 权重
echo "8:0 100" > io.weight

PID 控制器

# 限制进程数(防止 fork bomb)
echo "100" > pids.max            # 最多 100 个进程/线程

AI.4 沙箱中 cgroup 的实际配置

一个典型的 AI Agent 沙箱的 cgroup 配置:

# 容器运行时(如 containerd)创建的 cgroup 配置

CPU:    1 核(100000/100000)
内存:   512MB max + 256MB high(软限制触发回收)
IO:     读 200MB/s,写 100MB/s
PID:    256(足够 Agent 和子进程)
设备:   只允许 /dev/null, /dev/zero, /dev/urandom

# 对于 Firecracker VM,cgroup 限制的是 VMM 进程 + vCPU 线程
# VM 内部还有额外的资源管理(Guest 内核自己的调度和 OOM)

AI.5 cgroup 与沙箱安全的关系

cgroup 不仅是性能工具,也是安全工具:

资源耗尽攻击(DoS):恶意 Agent 可能试图消耗所有 CPU/内存,影响其他 Agent。cgroup 的硬限制确保每个 Agent 在自己的围栏内。

fork bomb 防御:pids.max 限制进程数,防止 :(){ :|:& };: 这样的攻击。

信息泄露:cgroup 的统计信息(如 memory.stat)可能泄露宿主机信息。安全容器通过虚拟化 /proc/[pid]/cgroup 来防止。

AI.6 cgroup 在 Kubernetes 中的层次

/sys/fs/cgroup/
└── kubepods/                          # K8s 管理的所有 Pod
    ├── burstable/                     # QoS = Burstable
    │   └── pod-xxxxx/                 # 某个 Pod
    │       ├── container-aaa/         # 容器 A
    │       └── container-bbb/         # 容器 B
    ├── besteffort/                    # QoS = BestEffort
    └── guaranteed/                    # QoS = Guaranteed
        └── pod-sandbox/               # AI Agent Pod
            └── container-sandbox/     # 沙箱容器
                └── (Firecracker/gVisor 进程在这里)

附录 AJ:Rust 在沙箱中的角色——为什么 Firecracker 选择 Rust

AJ.1 内存安全为什么对沙箱至关重要?

想象你修了一道围墙(沙箱)来圈住一只老虎(不可信代码)。如果围墙本身有裂缝(内存安全漏洞),老虎就能逃出来。传统的围墙材料(C/C++)容易产生裂缝:缓冲区溢出、使用后释放、双重释放、空指针解引用。

Rust 这种材料天生不会产生这些裂缝——编译器在你施工时就帮你检查了。

AJ.2 C/C++ VMM 的历史漏洞

QEMU(C 语言编写)的历史漏洞统计:

2015-2024 年 QEMU CVE 统计:
  - 堆溢出 (heap overflow):38 个
  - 栈溢出 (stack overflow):12 个
  - 使用后释放 (use-after-free):15 个
  - 空指针解引用 (null deref):45 个
  - 整数溢出 (integer overflow):8 个
  - 越界读写 (out-of-bounds):52 个
  
  总计:~170 个内存安全相关 CVE

这些漏洞中的很多如果发生在 VMM 的设备模拟代码中,
可能允许 Guest 代码逃逸到 Host。

AJ.3 Rust 如何消除这些问题

Rust 的所有权系统在编译时消除内存安全问题:

// 所有权:同一时间只有一个所有者
fn process_packet(packet: Vec<u8>) {
    // packet 的所有权转移到这个函数
    // 函数结束时 packet 自动释放
}
// 调用后原始变量不可用,防止 use-after-free

// 借用检查:可变引用和不可变引用不能同时存在
fn update_device(device: &mut VirtioDevice) {
    // 独占可变访问
}
// 在 update_device 执行期间,没有其他代码能读取 device

// 边界检查:数组访问自动检查边界
let data = vec![0u8; 1024];
let byte = data[index];  // 如果 index >= 1024,panic 而不是越界读

// 无空指针:Option<T> 强制处理"可能为空"的情况
fn find_device(id: u32) -> Option<&VirtioDevice> {
    // 返回 None 而不是 null
}
// 调用者必须 match/unwrap 处理 None 情况

AJ.4 Firecracker 的 Rust 架构

// Firecracker 的核心结构(简化)

// VMM 主循环
pub struct Vmm {
    vm: Vm,                        // KVM VM 句柄
    vcpus: Vec<Vcpu>,              // vCPU 列表
    mmio_device_manager: MmioDeviceManager,  // 设备管理
    event_manager: EventManager,    // epoll 事件循环
}

// 设备管理:类型安全的设备注册
pub struct MmioDeviceManager {
    devices: Vec<(DeviceRange, Arc<Mutex<dyn BusDevice>>)>,
}

// VirtIO 网络设备
pub struct Net {
    tap: Tap,                      // TAP 设备句柄
    queue_evts: [EventFd; 2],      // RX/TX 队列通知
    rx_queue: Queue,               // 接收队列
    tx_queue: Queue,               // 发送队列
    // Rust 的类型系统确保这些字段不会被错误地并发访问
}

// 安全的 KVM 封装
pub struct Vm {
    fd: VmFd,                      // 文件描述符(自动关闭)
    memory: GuestMemoryMmap,       // 内存映射(带边界检查)
}

impl Drop for Vm {
    fn drop(&mut self) {
        // RAII:Vm 析构时自动清理 KVM 资源
        // 不可能泄露文件描述符
    }
}

AJ.5 unsafe 的最小化使用

Firecracker 中确实有 unsafe 代码(与硬件/系统接口交互时不可避免),但被严格控制:

// unsafe 使用示例:直接访问 Guest 内存
pub fn read_from_guest<T>(&self, addr: GuestAddress) -> Result<T> {
    // 边界检查在安全代码中完成
    if addr.0 + std::mem::size_of::<T>() as u64 > self.size {
        return Err(Error::OutOfBounds);
    }
    // 只有实际的内存读取需要 unsafe
    unsafe {
        let ptr = self.host_addr.offset(addr.0 as isize) as *const T;
        Ok(ptr.read_unaligned())
    }
}

// Firecracker unsafe 代码统计:
//   总代码行数:~50,000
//   unsafe 块数量:~200
//   unsafe 代码比例:< 2%
//   每个 unsafe 块都有注释解释为什么需要以及安全前提

AJ.6 Cloud Hypervisor 和 CubeSandbox 的 Rust 实践

Cloud Hypervisor(CubeSandbox 使用的 VMM)同样是 Rust 编写,共享部分基础库(如 vm-memory, vhost 等 rust-vmm 项目):

rust-vmm 项目(共享组件):
├── vm-memory      — 安全的 Guest 内存访问
├── vhost          — vhost 后端实现
├── kvm-ioctls     — 类型安全的 KVM ioctl 封装
├── linux-loader   — 内核/固件加载
├── event-manager  — epoll 事件循环
└── seccompiler    — seccomp 规则编译器

这些组件被 Firecracker 和 Cloud Hypervisor 共用,
经过大量生产环境验证。

附录 AK:Linux Namespace 全解——容器隔离的七道门

AK.1 命名空间是什么?

想象一栋大楼(操作系统)里有很多公司(容器)。每个公司看到的是自己独立的空间:自己的前台(PID 1)、自己的网线(网络)、自己的文件柜(文件系统)。虽然物理上共享同一栋楼,但逻辑上完全隔离。

Linux 提供 7 种命名空间,每种隔离一个系统资源维度:

AK.2 七种命名空间

PID Namespace(进程 ID 隔离)

宿主机视角:
  PID 1: systemd
  PID 1234: containerd
  PID 5678: Agent 进程(在容器内是 PID 1)

容器内视角:
  PID 1: Agent 进程(以为自己是 init 进程)
  PID 2: Agent 子进程
  
安全意义:容器内进程看不到容器外的其他进程

Mount Namespace(文件系统挂载隔离)

宿主机:/usr/bin/docker, /home/user/...(完整文件系统)
容器:  /usr/bin/python, /workspace/...(只有容器 rootfs 和挂载点)

容器内的 mount 操作不影响宿主机
pivot_root 后容器看不到宿主机的目录

Network Namespace(网络隔离)

独立的网络设备、IP 地址、路由表、端口空间。

UTS Namespace(主机名隔离)

容器有自己的 hostname,独立于宿主机。

IPC Namespace(进程间通信隔离)

隔离 System V IPC 对象(共享内存段、信号量、消息队列)和 POSIX 消息队列。

User Namespace(用户 ID 隔离)

容器内:UID 0(root)
映射到宿主机:UID 100000(普通用户)

容器内的 root 在宿主机上没有特权
这是"rootless 容器"的基础

Cgroup Namespace(cgroup 视图隔离)

容器内看到的 cgroup 层级以自己为根,看不到父级 cgroup。

AK.3 命名空间的创建和进入

// 创建新命名空间:clone() 系统调用
int flags = CLONE_NEWPID | CLONE_NEWNET | CLONE_NEWNS | 
            CLONE_NEWUTS | CLONE_NEWIPC | CLONE_NEWUSER | 
            CLONE_NEWCGROUP;
pid_t child = clone(child_func, stack, flags | SIGCHLD, NULL);

// 进入已有命名空间:setns() 系统调用
int fd = open("/proc/[pid]/ns/net", O_RDONLY);
setns(fd, CLONE_NEWNET);  // 当前进程进入目标的网络命名空间

// 创建新命名空间(不创建子进程):unshare()
unshare(CLONE_NEWNET);  // 当前进程获得新的网络命名空间

AK.4 命名空间 vs 虚拟机隔离

维度 命名空间(容器) 虚拟机
内核 共享宿主机内核 独立内核
系统调用 直达宿主机内核 被 Guest 内核处理
启动时间 毫秒级 秒级
内存开销 几 MB 几十-几百 MB
隔离强度 中等(内核漏洞可逃逸) 强(需 KVM 漏洞才能逃逸)
适用场景 可信代码、多租户 不可信代码、AI Agent

AK.5 命名空间的局限性

命名空间不隔离的资源:内核内存(slab cache, page cache)、CPU 时间中断处理、内核日志(dmesg)、/proc/sys/ 下的大部分内核参数、时间(直到 Time Namespace 出现在 5.6+)。

这些”泄露”是容器逃逸的潜在攻击面,也是 gVisor/Firecracker 存在的原因。


附录 AL:从零构建最小 VMM——理解虚拟化的动手实验

AL.1 实验目标

用不到 500 行代码,构建一个能运行真实 Linux 内核的最小 VMM。通过亲手实现来理解 KVM API、内存映射、设备模拟的核心原理。

AL.2 最小 VMM 的架构

最小 VMM(~500 行 Rust)
├── 1. 打开 /dev/kvm,创建 VM
├── 2. 映射 Guest 内存(物理内存)
├── 3. 加载内核到 Guest 内存
├── 4. 创建 vCPU,设置寄存器
├── 5. 运行循环:VM Entry → Guest 执行 → VM Exit → 处理
│   ├── IO 端口处理(串口输出)
│   ├── MMIO 处理(设备访问)
│   └── HLT 处理(CPU 空闲)
└── 6. 输出 Guest 的串口打印

AL.3 核心代码

use kvm_ioctls::{Kvm, VcpuFd, VmFd};
use kvm_bindings::*;

fn main() {
    // 1. 打开 KVM
    let kvm = Kvm::new().unwrap();
    let vm = kvm.create_vm().unwrap();
    
    // 2. 映射 Guest 内存(64MB)
    let mem_size = 64 * 1024 * 1024;
    let guest_mem = unsafe {
        libc::mmap(std::ptr::null_mut(), mem_size,
            libc::PROT_READ | libc::PROT_WRITE,
            libc::MAP_ANONYMOUS | libc::MAP_PRIVATE, -1, 0)
    };
    let mem_region = kvm_userspace_memory_region {
        slot: 0,
        guest_phys_addr: 0,
        memory_size: mem_size as u64,
        userspace_addr: guest_mem as u64,
        flags: 0,
    };
    unsafe { vm.set_user_memory_region(mem_region).unwrap() };
    
    // 3. 加载内核到 Guest 内存地址 0x100000(1MB)
    let kernel_data = std::fs::read("vmlinux").unwrap();
    unsafe {
        let dest = (guest_mem as *mut u8).add(0x100000);
        std::ptr::copy_nonoverlapping(
            kernel_data.as_ptr(), dest, kernel_data.len());
    }
    
    // 4. 创建 vCPU
    let vcpu = vm.create_vcpu(0).unwrap();
    
    // 设置特殊寄存器(进入长模式)
    let mut sregs = vcpu.get_sregs().unwrap();
    sregs.cs.base = 0;
    sregs.cs.selector = 0;
    // ... 设置页表、GDT 等进入 64 位模式 ...
    vcpu.set_sregs(&sregs).unwrap();
    
    // 设置通用寄存器
    let mut regs = vcpu.get_regs().unwrap();
    regs.rip = 0x100000;  // 内核入口地址
    regs.rflags = 2;       // 固定位
    vcpu.set_regs(&regs).unwrap();
    
    // 5. 运行循环
    loop {
        match vcpu.run().unwrap() {
            VcpuExit::IoOut(port, data) => {
                if port == 0x3f8 {  // 串口端口
                    print!("{}", data[0] as char);
                }
            }
            VcpuExit::Hlt => {
                println!("\n[VMM] Guest halted.");
                break;
            }
            VcpuExit::MmioWrite(addr, data) => {
                // 处理 MMIO 设备访问
            }
            exit => {
                println!("[VMM] Unexpected exit: {:?}", exit);
                break;
            }
        }
    }
}

AL.4 运行结果

$ cargo run
[    0.000000] Linux version 5.10.0 (minimal config)
[    0.000000] Command line: console=ttyS0
[    0.001234] Booting...
Hello from Guest Linux!
[VMM] Guest halted.

这个最小 VMM 展示了虚拟化的核心循环:创建 VM → 映射内存 → 加载内核 → 运行 → 处理 Exit。Firecracker 本质上就是这个循环加上 VirtIO 设备实现和 API 服务器。

AL.5 从最小 VMM 到 Firecracker 的差距

功能 最小 VMM Firecracker
设备 仅串口 virtio-net/blk/vsock
内存 固定大小 动态分配 + balloon
CPU 单核 多核 + 热添加
安全 seccomp + jailer
API REST API + 配置
快照 完整快照/恢复
启动 几秒 125ms

附录 AM:性能基准测试方法论——如何公平比较沙箱方案

AM.1 为什么性能测试很难做对?

沙箱性能比较中常见的陷阱:

陷阱 1:不公平的配置
  - 一个方案分配了 4 核,另一个只有 2 核
  - 一个启用了大页,另一个没有
  
陷阱 2:冷启动 vs 热运行
  - 第一次运行包含 JIT 编译、缓存预热
  - 稳定状态的性能才是日常体验
  
陷阱 3:微基准 vs 真实工作负载
  - 系统调用延迟差 10x(gVisor),但实际编译任务只慢 20%
  - 因为编译的大部分时间花在 CPU 计算而非 syscall
  
陷阱 4:忽略尾延迟
  - 平均启动时间 5ms,但 P99 可能是 50ms(CoW 页面抖动)

AM.2 标准化测试维度

维度 1:启动性能
  - 冷启动时间(从请求到 Agent 可执行代码)
  - 热启动/克隆时间(从快照恢复)
  - 启动内存开销(idle 状态的内存占用)

维度 2:运行时性能
  - 系统调用延迟(getpid, read, write, open/close 循环)
  - 文件 IO 吞吐(顺序读/写、随机读/写)
  - 网络延迟和吞吐(ping RTT, iperf3)
  - CPU 计算性能(编译 Linux 内核、运行 Python 基准)

维度 3:密度(单机可运行沙箱数)
  - 相同物理资源下可同时运行的沙箱数量
  - 内存共享效率(CoW 节省比例)
  - CPU 过量分配比例

维度 4:隔离强度
  - 攻击面大小(暴露的 syscall 数量)
  - 已知逃逸漏洞数量
  - 侧信道防护能力

AM.3 具体基准测试结果

测试环境:
  CPU: Intel Xeon 8375C, 32 核
  内存: 256GB DDR4
  存储: NVMe SSD (3.5GB/s 顺序读)
  内核: Linux 5.15
  每个沙箱配置: 2 vCPU, 2GB 内存

启动时间(P50 / P99):
                     冷启动          克隆/恢复
  Docker (runc):     300ms / 500ms   N/A
  gVisor (runsc):    800ms / 1.2s    N/A
  Firecracker:       125ms / 180ms   50ms / 80ms
  Kata:              2s / 3s         N/A
  CubeSandbox:       200ms / 300ms   5ms / 8ms (CoW clone)

系统调用延迟(getpid 循环,纳秒/次):
  原生 Linux:         ~50ns
  Docker:            ~55ns (+10%)
  Firecracker:       ~80ns (+60%)
  gVisor (KVM):      ~200ns (+300%)
  gVisor (ptrace):   ~2000ns (+3900%)

文件顺序写(1GB 文件,MB/s):
  原生 Linux:         3200
  Docker:            3100 (-3%)
  Firecracker:       2800 (-12%)
  gVisor:            1500 (-53%)

网络延迟(ping localhost 等效,微秒):
  原生 Linux:         12
  Docker (bridge):   35
  Firecracker (TAP): 45
  gVisor (netstack):  120

密度(2GB 沙箱,256GB 物理机):
  Docker:            128 个(无共享)
  Firecracker:       100 个(每个 ~20MB VMM 开销)
  CubeSandbox:       500+ 个(CoW 共享,实际内存使用 << 2GB/个)

AM.4 性能与安全的权衡曲线

安全性 ↑
  │
  │          gVisor (ptrace)
  │      ●
  │              gVisor (KVM)
  │          ●
  │                  Firecracker    CubeSandbox
  │              ●              ●
  │                      Kata
  │                  ●
  │
  │                              Docker
  │                          ●
  │
  └──────────────────────────────────── 性能 →
  
选择指南:
  - 最高安全 + 可接受性能损失 → gVisor
  - 高安全 + 高密度 + 快照 → CubeSandbox
  - 高安全 + 生产验证 → Firecracker
  - 最高性能 + 基本隔离 → Docker + seccomp

AM.5 AI Agent 特有的性能指标

Agent 工作负载特征:
  1. 启动频繁(每次对话/每个 episode 启动新沙箱)
  2. 计算密集阶段短暂(编译/测试执行,通常 < 30s)
  3. 等待 LLM 响应时间长(占总时间 60-80%)
  4. 文件 IO 以小文件为主(代码文件,通常 < 1MB)
  5. 需要频繁快照/恢复(RL 训练场景)

因此 Agent 场景最关键的指标是:
  1. 克隆/恢复延迟(影响 episode 迭代速度)
  2. 密度(影响训练并行度和成本)
  3. 首次代码执行延迟(影响用户体验)
  而不是:
  - 持续计算性能(Agent 不会长时间计算)
  - 网络吞吐(Agent 主要是 HTTP API 调用)

附录 AN:Daytona 与开发环境编排——从沙箱到完整 IDE

AN.1 Daytona 的定位

如果说 Firecracker 和 CubeSandbox 是”一次性实验室”(用完即弃的沙箱),那 Daytona 更像”长期驻留的工作站”——它为开发者提供完整的、可持久化的开发环境,支持 IDE 连接、Git 操作、长时间运行的服务。

AN.2 架构概览

Daytona 架构:

┌─────────────────────────────────────────────────────┐
│  用户层                                              │
│  ├── VS Code Remote / JetBrains Gateway             │
│  ├── CLI (daytona create / daytona start)           │
│  └── Web Dashboard                                  │
└─────────────────────┬───────────────────────────────┘
                      │ API
┌─────────────────────┴───────────────────────────────┐
│  控制平面(Daytona Server)                          │
│  ├── Workspace Manager(环境生命周期)                │
│  ├── Provider Registry(多后端支持)                  │
│  ├── Git Integration(自动 clone + 配置)             │
│  └── Builder(devcontainer.json 解析 + 构建)        │
└─────────────────────┬───────────────────────────────┘
                      │
┌─────────────────────┴───────────────────────────────┐
│  计算后端(Provider)                                 │
│  ├── Docker Provider(本地/远程 Docker)              │
│  ├── AWS Provider(EC2 实例)                        │
│  ├── GCP Provider(Compute Engine)                  │
│  ├── Fly.io Provider                                │
│  └── 自定义 Provider(可扩展)                       │
└─────────────────────────────────────────────────────┘

AN.3 Daytona vs 沙箱的关键差异

维度 Daytona Firecracker/CubeSandbox
生命周期 持久化(天/周) 临时(秒/分)
状态 有状态(保留文件和配置) 无状态/可恢复
用途 人类开发者日常编码 AI Agent 代码执行
IDE 支持 VS Code Remote, SSH 命令行/API
网络 完整网络(端口转发) 受限网络
安全模型 信任开发者 不信任执行代码
启动方式 devcontainer.json 定义 API 动态创建

AN.4 devcontainer.json:环境即代码

{
    "name": "Python AI Dev",
    "image": "python:3.11",
    "features": {
        "ghcr.io/devcontainers/features/node:1": {},
        "ghcr.io/devcontainers/features/git:1": {}
    },
    "customizations": {
        "vscode": {
            "extensions": ["ms-python.python", "github.copilot"],
            "settings": { "python.defaultInterpreterPath": "/usr/local/bin/python" }
        }
    },
    "postCreateCommand": "pip install -r requirements.txt",
    "forwardPorts": [8080, 3000],
    "mounts": ["source=${localWorkspaceFolder},target=/workspace,type=bind"]
}

Daytona 解析这个文件,自动构建包含所有依赖的容器镜像,配置 IDE 扩展,执行初始化脚本。开发者获得”一键启动”的完整开发环境。

AN.5 Daytona 在 AI Agent 场景的潜力

虽然 Daytona 主要面向人类开发者,但它的一些设计对 AI Agent 也有价值:

环境标准化:devcontainer.json 定义可复现的环境,Agent 每次获得完全相同的起点。

预构建(Prebuilds):提前构建好环境镜像,Agent 获取环境时无需等待安装依赖。

多后端:相同的 Agent 代码可以在本地 Docker、远程云主机、或专用沙箱中运行。

AN.6 与 E2B 的对比

E2B(Engineering to Build)专门为 AI Agent 设计的云端沙箱平台:

E2B 的设计理念:
  - 为 AI Agent 而非人类优化
  - API-first(没有 SSH/IDE 接口)
  - 快速启动(< 500ms)
  - 自动清理(执行完毕即销毁)
  - SDK 集成(Python/JS SDK 直接嵌入 Agent 代码)

E2B 使用示例:
  from e2b_code_interpreter import Sandbox
  
  sandbox = Sandbox()
  execution = sandbox.run_code("print(1 + 1)")
  print(execution.logs.stdout)  # "2\n"
  sandbox.close()

Daytona 使用示例:
  daytona create --repo https://github.com/user/project
  # 等待环境构建...
  # 通过 VS Code Remote 连接
  # 在完整 IDE 中开发

差异总结:
  E2B = "AI 的一次性草稿纸"
  Daytona = "人类的长期工作台"
  CubeSandbox = "AI 的高性能并行实验室"
  Firecracker = "云函数的微型引擎"

附录 AO:gVisor 内部实现——用户空间内核的挑战与创新

AO.1 为什么要在用户空间重新实现内核?

传统容器共享宿主机内核——这意味着 450 个系统调用中的任何一个 bug 都可能被容器内的恶意代码利用。gVisor 的策略是:”如果内核是攻击面,那就不让容器接触真实内核。”

但完全在用户空间重新实现一个内核是极其复杂的工程。Linux 内核经过 30+ 年开发,有数百万行代码。gVisor(Sentry)需要重新实现所有 Guest 应用可能用到的内核功能:进程管理、内存管理、文件系统、网络栈、信号处理、定时器等。

AO.2 Sentry 的网络栈:netstack

gVisor 完全重新实现了 TCP/IP 网络栈(称为 netstack),而不是使用 Linux 的网络栈:

传统容器网络:
  应用 → socket syscall → Linux TCP/IP 栈 → 网卡

gVisor 网络:
  应用 → socket syscall → Sentry 拦截 → netstack(用户空间 TCP/IP)
    → packet 通过共享内存/fd 传递给 Host → Host 内核转发

netstack 实现的协议:
  - IPv4 / IPv6
  - TCP(包含拥塞控制:Reno, CUBIC)
  - UDP
  - ICMP
  - ARP / NDP
  - Raw sockets
  - Unix domain sockets

性能影响:
  netstack 的 TCP 吞吐约为 Linux 原生的 60-70%
  延迟增加 50-100μs(因为用户空间处理 + 额外拷贝)
  
安全收益:
  即使 netstack 有 bug,攻击者也只在 Sentry 进程中
  Sentry 本身被 seccomp 严格限制
  不暴露 Linux 内核的网络子系统

AO.3 Sentry 的文件系统层

gVisor 实现了完整的 VFS(Virtual File System)层:

Sentry VFS 架构:
  应用层调用(open, read, write, stat...)
       ↓
  VFS 层(路径解析、权限检查、fd 管理)
       ↓
  文件系统实现
  ├── tmpfs      — 内存文件系统(/tmp)
  ├── devtmpfs   — 设备文件(/dev/null 等)
  ├── procfs     — 虚拟化的 /proc
  ├── sysfs      — 虚拟化的 /sys
  ├── overlayfs  — 分层文件系统
  └── gofer-fs   — 通过 9P 协议访问 Host 文件
       ↓
  gofer 进程(受限的文件代理)
       ↓
  Host 文件系统

特别注意 /proc 和 /sys 的虚拟化:真实的 /proc 暴露大量内核信息(其他进程、内核版本、硬件信息)。gVisor 重新实现了 procfs,只暴露当前 Guest 应用能看到的信息。

AO.4 Sentry 的信号处理

信号是 Unix 系统的进程间通信机制,也是最容易出错的部分之一。gVisor 完整实现了 POSIX 信号语义:

Signal 处理流程:
  1. 外部事件产生信号(如 Ctrl+C → SIGINT)
  2. Sentry 的信号管理器记录待处理信号
  3. 在 Guest 返回用户态前检查是否有待处理信号
  4. 如果有:
     a. 保存当前用户态上下文到 Guest 栈上(sigframe)
     b. 修改 Guest 的 RIP 指向信号处理函数
     c. 让 Guest 执行用户自定义的信号处理器
     d. 处理器返回时执行 rt_sigreturn 恢复原始上下文
  5. Sentry 需要正确处理:
     - 嵌套信号(处理器中又收到信号)
     - 信号屏蔽(sigprocmask)
     - SA_RESTART(被信号中断的 syscall 自动重试)
     - 实时信号排队

AO.5 gVisor 的 Go 语言选择

gVisor 选择 Go 语言实现,这是一个有争议的决策:

优点:内存安全(GC 管理)、开发效率高、并发模型(goroutine)适合处理多个 Guest 线程、成熟的标准库。

缺点:GC 停顿可能影响实时性、运行时开销(goroutine 调度)、无法精确控制内存布局、cgo 调用开销(与 C 库交互时)。

gVisor 的应对:使用了大量 unsafe 和手动内存管理的技巧来降低 GC 压力;自定义了调度器相关逻辑来减少延迟抖动。

AO.6 gVisor 的平台抽象层

gVisor 的”平台”(Platform)是它与底层系统交互的接口抽象:

Platform 接口:
  - MapFile: 将 Guest 虚拟地址映射到文件
  - NewContext: 创建 Guest 执行上下文
  - Switch: 从 Sentry 切换到 Guest 代码执行
  - PullBack: 从 Guest 切换回 Sentry

实现:
  ptrace 平台:用 ptrace 监控 Guest 进程
    Switch = ptrace(CONT) → 等待 SIGTRAP
    PullBack = 收到 SIGTRAP 时 Sentry 继续
    
  KVM 平台:用 KVM 运行 Guest 代码
    Switch = ioctl(KVM_RUN)
    PullBack = VM Exit(syscall/exception/interrupt)

  systrap 平台(新):用 SIGSYS + seccomp 拦截
    比 ptrace 快(避免两次 context switch)
    比 KVM 简单(不需要硬件虚拟化支持)

附录 AP:Kata Containers 深度解析——将 VM 伪装成容器

AP.1 Kata 的设计哲学

Kata Containers 的核心理念是:”容器的速度 + 虚拟机的安全”。它让每个容器(或 Pod)运行在自己的轻量级虚拟机中,但对上层(Kubernetes)完全透明——K8s 以为它在管理普通容器。

AP.2 Kata 的组件架构

Kubernetes 视角(看到的是"普通容器"):
  kubelet → containerd → containerd-shim-kata-v2

Kata 内部架构:
  containerd-shim-kata-v2(shim 进程)
      │
      ├── VMM(QEMU / Cloud Hypervisor / Firecracker)
      │     └── microVM(轻量级虚拟机)
      │           ├── Guest Kernel(精简 Linux)
      │           ├── kata-agent(Guest 内管理进程)
      │           └── 容器进程(用户代码)
      │
      └── virtiofsd / 9p(文件系统共享)

AP.3 kata-agent:VM 内的管家

kata-agent 是运行在 VM 内部的守护进程,负责执行来自 Host 的容器管理命令:

kata-agent 的职责:
  1. 接收来自 shim 的 gRPC 命令(通过 vsock)
  2. 在 VM 内创建容器(使用标准的 namespace + cgroup)
  3. 配置容器的网络(在 VM 内创建 veth pair)
  4. 挂载文件系统(通过 virtiofs/9p 挂载 Host 共享)
  5. 启动/停止容器进程
  6. 收集容器的 stdout/stderr 输出
  7. 处理信号转发

通信通道:
  shim ←→ kata-agent 通过 virtio-vsock
  协议:gRPC(Protocol Buffers 序列化)
  为什么不用网络:vsock 不需要网络配置,
                  且不会被 Guest 内的代码滥用

AP.4 Kata 的网络实现

外部网络 → Host 网卡
    ↓
Host 网络命名空间
    ↓ (veth pair / macvtap)
TAP 设备
    ↓ (virtio-net)
VM 内 Guest 网络
    ↓ (veth pair,由 kata-agent 创建)
容器网络命名空间
    ↓
容器应用

数据包路径:
  容器 → veth → Guest kernel → virtio-net → TAP → Host kernel → 物理网卡
  
  比普通容器多了一层 Guest kernel,延迟增加约 20-50μs

AP.5 Kata 的存储方案

方案一:virtio-blk(块设备直通)
  将容器镜像做成 raw/qcow2 块设备
  优点:性能好,支持 VM 内任意文件系统
  缺点:不支持 OverlayFS 共享(每个 VM 需要独立块设备)

方案二:virtio-fs(推荐)
  Host 运行 virtiofsd,导出容器 rootfs 目录
  VM 内通过 virtiofs 挂载
  优点:支持 Host 端 OverlayFS 共享,DAX 性能好
  缺点:需要 Host 和 Guest 内核都支持 virtiofs

方案三:9P(传统方案)
  Host 通过 9P 协议共享文件
  优点:简单、广泛支持
  缺点:性能较差(尤其是小文件大量 stat 操作)

实际部署通常选择 virtio-fs,兼顾性能和共享效率。

AP.6 Kata vs Firecracker 的细节对比

维度 Kata Firecracker
目标 K8s 集成、安全容器 Serverless 函数、沙箱
OCI 兼容 完整(全部生命周期) 不兼容(自有 API)
VMM 选择 QEMU/CH/FC(可选) 仅 Firecracker
设备 完整 virtio 设备集 最小设备集
网络 完整网络栈 + CNI 兼容 简单 TAP
存储 virtio-fs/9p/blk virtio-blk
多容器 Pod 支持(VM 内多容器) 不支持(一 VM 一任务)
热插拔 支持(CPU/内存/设备) 不支持
启动时间 1-3s 125ms
适用场景 企业 K8s 多租户 高密度 FaaS

附录 AQ:E2B 平台设计——为 AI Agent 而生的沙箱

AQ.1 E2B 的产品理念

E2B(”Engineer to Build”)是专门为 AI Agent 设计的代码执行沙箱。它的核心洞察是:AI Agent 需要的不是”虚拟机”或”容器”,而是一个安全的、API 友好的代码执行环境。

AQ.2 E2B 的使用模式

# Python SDK 示例
from e2b_code_interpreter import Sandbox

# 创建沙箱(< 500ms)
sandbox = Sandbox()

# 执行代码
execution = sandbox.run_code("""
import numpy as np
data = np.random.randn(1000)
print(f"Mean: {data.mean():.4f}")
print(f"Std:  {data.std():.4f}")
""")

print(execution.logs.stdout)
# Mean: 0.0032
# Std:  1.0021

# 文件操作
sandbox.filesystem.write("/workspace/result.json", '{"status": "ok"}')
content = sandbox.filesystem.read("/workspace/result.json")

# 安装包
sandbox.run_code("!pip install pandas matplotlib")

# 保持沙箱活跃,多次交互
execution2 = sandbox.run_code("import pandas as pd; print(pd.__version__)")

# 完成后关闭
sandbox.close()

AQ.3 E2B 的技术架构推测

E2B 架构(基于公开信息推测):

客户端 SDK(Python/JS/Go)
    ↓ WebSocket / REST API
API Gateway(全球边缘节点)
    ↓
Orchestrator(调度 + 生命周期管理)
    ↓
Compute Layer(执行沙箱的物理机集群)
    ├── Firecracker microVMs(隔离执行)
    ├── 预构建模板(Python/Node/etc.)
    └── 快照池(快速启动)
    ↓
Storage Layer
    ├── 临时存储(沙箱内文件系统)
    └── 持久存储(用户上传的文件)

AQ.4 E2B 的模板系统

# E2B 自定义模板示例
FROM e2b/base:latest

# 安装 AI/ML 工具
RUN pip install torch transformers datasets
RUN pip install langchain openai

# 安装系统工具
RUN apt-get install -y git curl jq

# 预下载模型(加速启动)
RUN python -c "from transformers import AutoTokenizer; \
    AutoTokenizer.from_pretrained('gpt2')"

# 设置工作目录
WORKDIR /workspace

模板会被预构建为 Firecracker rootfs 镜像,创建沙箱时直接从模板快照启动。

AQ.5 E2B 与其他方案的对比

维度 E2B CubeSandbox Replit GitHub Codespaces
目标用户 AI Agent AI Agent (RL) 人+AI 人类开发者
主要接口 SDK/API SDK/API Web IDE VS Code
启动时间 < 500ms < 5ms (clone) 数秒 30-60s
快照 有限 完整(CoW)
并行度 中(数百) 高(数千)
定价模型 按时间 内部使用 订阅 按时间
自定义程度 模板 完全自定义 有限 devcontainer

附录 AR:隔离等级模型——L0 到 L4 的安全光谱

AR.1 为什么需要分级?

不是所有场景都需要最强的隔离。就像不是所有实验都需要 BSL-4 实验室——处理无害细菌用 BSL-1 就够了,处理埃博拉才需要 BSL-4。在沙箱/容器世界,选择过高的隔离级别浪费资源,过低则有安全风险。

AR.2 五个隔离等级定义

L0 - 无隔离(裸金属)
  执行环境:直接在宿主机运行
  典型场景:开发者本地运行自己的代码
  安全假设:完全信任执行代码
  攻击面:完整系统
  示例:直接 python agent.py

L1 - 进程隔离
  执行环境:独立进程 + 基本权限降低
  隔离机制:UID 分离 + capability drop + 基础 seccomp
  典型场景:同团队的不同服务
  安全假设:代码基本可信,防止意外损坏
  示例:sudo -u nobody python agent.py

L2 - 容器隔离
  执行环境:完整 namespace + cgroup + seccomp
  隔离机制:7 种 namespace + 资源限制 + 300+ syscall 过滤
  典型场景:多租户 SaaS、CI/CD
  安全假设:代码半可信,防止有意攻击(但不防内核漏洞)
  示例:Docker + 默认 seccomp profile

L3 - 安全容器 / 轻量级 VM
  执行环境:microVM 或用户空间内核
  隔离机制:硬件虚拟化 + 最小攻击面 + 纵深防御
  典型场景:AI Agent 沙箱、多租户 FaaS
  安全假设:代码完全不可信
  示例:Firecracker / gVisor / Kata / CubeSandbox

L4 - 硬件隔离
  执行环境:专用物理机或 TEE(可信执行环境)
  隔离机制:物理隔离 + Intel SGX/TDX / AMD SEV
  典型场景:处理最敏感数据、国防/金融
  安全假设:不信任任何软件层(包括 hypervisor)
  示例:AWS Nitro Enclaves, Azure Confidential Computing

AR.3 各级别的性能与安全权衡

等级 启动时间 运行开销 内核攻击面 侧信道防护 代表方案
L0 0 0 完整 裸运行
L1 < 1ms < 1% 完整 setuid
L2 50-300ms 1-5% 完整 Docker
L3 5ms-3s 5-50% 极小 部分 FC/gVisor
L4 10-60s 10-30% 零(TEE) 完整 SGX/SEV

AR.4 AI Agent 应该选哪个级别?

决策树:

Agent 执行的代码来源?
  ├── Agent 自己生成的代码 → 至少 L3
  │     (AI 生成的代码不可预测,可能有危险操作)
  │
  ├── 用户提供的代码 → L3(推荐)或 L2(如果限制严格)
  │     (用户代码可能是恶意的)
  │
  └── 可信仓库的代码 → L2 通常足够
        (已经过 code review 的代码)

Agent 执行的代码要安装第三方包?
  ├── 是 → 必须 L3(供应链攻击风险高)
  └── 否 → L2 可以接受

Agent 执行环境中有敏感数据?
  ├── 是 → L3 + 额外数据隔离,或 L4
  └── 否 → L3

多个 Agent 共享同一物理机?
  ├── 是 → L3(防侧信道需要 CPU pinning)
  └── 否 → L3 仍推荐(纵深防御)

AR.5 混合隔离模式

实际部署中往往不是单一级别,而是多级组合:

典型的 AI Agent 平台部署:

L4: 物理机隔离(不同租户分配不同物理机集群)
 └── L3: Firecracker/CubeSandbox microVM
      └── L2: 容器 namespace + seccomp(VM 内部额外一层)
           └── L1: 代码执行进程权限最小化

网络层面:
  L3: VM 网络隔离(独立 TAP 设备)
  L2: eBPF 网络策略(精细粒度控制)
  L1: 应用层防火墙(WAF)

存储层面:
  L3: 独立 rootfs(每个 VM 自己的文件系统)
  L2: 加密存储(dm-crypt)
  L1: 文件权限控制

附录 AS:云原生沙箱的未来演进——从 microVM 到 Unikernel

AS.1 技术演进方向

历史 → 现在 → 未来

完整 VM (2000s)         microVM (2018+)         Unikernel (未来?)
┌──────────────┐       ┌──────────────┐       ┌──────────────┐
│ 完整 OS      │       │ 精简 Linux   │       │ 应用 + 库OS  │
│ (数GB)       │       │ (数MB)       │       │ (数百KB)     │
│ 启动: 30s+   │       │ 启动: 125ms  │       │ 启动: < 10ms │
│ 内存: 512MB+ │       │ 内存: 5-50MB │       │ 内存: < 5MB  │
│ 攻击面: 巨大 │       │ 攻击面: 小   │       │ 攻击面: 极小 │
└──────────────┘       └──────────────┘       └──────────────┘
        ↓                      ↓                      ↓
  传统数据中心            云原生/AI Agent           边缘计算/IoT

AS.2 Unikernel:把应用编译成内核

Unikernel 的思想是:大部分应用只用到操作系统的一小部分功能。与其运行一个完整的 Linux 内核(2800 万行代码),不如只把应用需要的 OS 功能编译进去,生成一个单一的、直接运行在 hypervisor 上的镜像。

传统应用栈:
  应用 → 系统库 → Linux 内核(2800万行代码)→ 硬件

Unikernel 栈:
  应用 + 需要的库OS组件(网络栈 + 文件系统)→ 硬件
  没有多余的代码 = 没有多余的攻击面
  
代表项目:
  - MirageOS(OCaml):编译到 Xen hypervisor
  - Nanos:面向云部署的 unikernel
  - OSv:JVM 优化的 unikernel
  - Unikraft:模块化 unikernel 构建框架

AS.3 Unikernel 在 AI 沙箱的潜力

理想的 AI Agent unikernel:
  - 只包含 Python 运行时 + 必要的 C 库
  - 网络栈精简为:只支持 HTTPS 出站 + vsock 通信
  - 文件系统精简为:只读 rootfs + tmpfs 工作区
  - 无 shell、无 SSH、无多用户概念
  
预期效果:
  - 镜像大小:< 50MB(vs microVM 的 200-500MB rootfs)
  - 启动时间:< 10ms(vs Firecracker 的 125ms)
  - 内存开销:< 10MB 基础(vs microVM 的 30-50MB)
  - 攻击面:极小(无多余系统服务)

挑战:
  - 动态语言(Python)依赖完整的 libc
  - pip install 需要完整的构建工具链
  - 调试困难(没有 shell 可以进去看)
  - 兼容性问题(很多库假设 Linux 行为)

AS.4 WebAssembly 沙箱:另一条路

WASM 提供了另一种轻量级沙箱方案:

WebAssembly 沙箱特征:
  - 编译型:代码编译为 WASM 字节码
  - 内存安全:线性内存模型,自动边界检查
  - 沙箱化:默认无 IO 能力,通过 WASI 显式授权
  - 极快启动:< 1ms(无需 OS boot)
  - 极小开销:< 1MB 内存

限制:
  - 不支持任意 Linux 二进制(需要重新编译为 WASM)
  - 系统接口有限(WASI 尚未覆盖所有 POSIX)
  - 单线程模型(WASM threads 是实验性的)
  - Python/Node.js 的 WASM 移植性能一般

适用场景:
  - 执行预编译的确定性计算
  - 插件系统(第三方代码)
  - 边缘计算
  
不适合 AI Agent(目前):
  - Agent 需要完整 Linux 环境(pip install, git, 编译器...)
  - WASM 不能满足这些需求
  - 但未来 WASI 成熟后,简单计算可能用 WASM

AS.5 机密计算(Confidential Computing)

下一个前沿:即使 hypervisor 被攻破,VM 内数据也不会泄露。

Intel TDX(Trust Domain Extensions):
  - 硬件加密 VM 内存
  - hypervisor 无法读取 VM 内存内容
  - VM 可以验证自己运行在真实的 TDX 硬件上(远程证明)
  
AMD SEV-SNP(Secure Encrypted Virtualization):
  - 同理,不同硬件实现
  - 内存加密 + 完整性保护

在 AI Agent 场景的价值:
  - Agent 执行涉及敏感代码/数据时
  - 即使云服务商也无法窥探 Agent 的执行内容
  - 适合处理 API keys、私有代码等

当前局限:
  - 性能开销 5-15%(内存加密/解密)
  - 远程证明流程复杂
  - 侧信道防护仍有漏洞
  - 可用硬件有限

AS.6 展望:AI Agent 沙箱的终极形态

2024-2025(现在):
  microVM(Firecracker/CubeSandbox)为主
  启动时间:5-125ms
  密度:数百到数千/机器

2025-2027(近期):
  microVM + 高效 CoW 快照为主
  Unikernel 在特定场景落地
  启动时间:< 5ms
  密度:数千到数万/机器

2027+(远期):
  Unikernel + 机密计算
  WASM + WASI 覆盖简单场景
  硬件 AI 加速器直通到沙箱
  启动时间:< 1ms
  密度:数万/机器
  
终极目标:
  "给 Agent 的每一次思考都配一个私密的、瞬时的、安全的执行环境"

End of Appendices W-AS.


附录 AT:GPU 虚拟化与 AI 加速器直通——沙箱中的计算力

AT.1 为什么 AI Agent 需要 GPU?

大部分 AI Agent 的”思考”(LLM 推理)发生在远程 GPU 集群上,Agent 沙箱本身通常不需要 GPU。但在以下场景中,沙箱内需要 GPU 访问:

场景一:Agent 需要运行本地模型(小模型推理、embedding 计算)。场景二:Agent 需要训练或微调模型(RL 策略训练)。场景三:Agent 执行 CUDA 代码(科学计算、图像处理)。场景四:评估 Agent 生成的 ML 代码是否正确运行。

AT.2 GPU 虚拟化的三种模式

模式一:GPU 直通(Passthrough / PCI-e SR-IOV)
  物理 GPU → IOMMU → 直接分配给 VM
  
  优点:
    - 接近原生性能(< 5% 开销)
    - 完整驱动兼容(CUDA/cuDNN 正常工作)
  缺点:
    - 一个 GPU 只能给一个 VM(不能共享)
    - 需要 IOMMU 硬件支持
    - 安全风险(GPU DMA 可能绕过隔离)
  
  适用:高性能计算、模型训练

模式二:GPU 虚拟化(vGPU / MIG)
  物理 GPU → 划分为多个虚拟 GPU → 分配给不同 VM
  
  NVIDIA MIG(Multi-Instance GPU):
    A100 可划分为最多 7 个 GPU 实例
    每个实例有独立的 SM、显存、带宽
  
  NVIDIA vGPU:
    时间片方式共享 GPU
    多个 VM 轮流使用同一 GPU
  
  优点:
    - GPU 利用率高(多个 Agent 共享)
    - 硬件级隔离(MIG)
  缺点:
    - 性能有损(15-30%)
    - 需要专用 License(vGPU 很贵)
    - 不是所有 GPU 都支持 MIG

模式三:API 转发(GPU-over-network)
  沙箱内代码 → API 拦截 → 网络传输 → Host 端 GPU 执行
  
  实现方式:
    - rCUDA / GVirtuS:远程 CUDA 调用
    - NVIDIA CUDA MPS:多进程共享 GPU
    - 自研方案:拦截 CUDA API,序列化到 Host
  
  优点:
    - 不需要硬件支持
    - 可以跨网络(GPU 和 Agent 不在同一台机器)
    - 灵活的资源分配
  缺点:
    - 延迟高(网络传输)
    - 兼容性问题(不是所有 CUDA API 都能转发)
    - 实现复杂

AT.3 安全容器中的 GPU 访问

Firecracker 与 GPU:
  Firecracker 设计上不支持 PCI 直通
  原因:PCI 直通需要 IOMMU 映射,增加攻击面
  替代:通过 vsock 转发 GPU 命令到 Host 端代理

Kata Containers 与 GPU:
  支持 VFIO(PCI 直通)和 vGPU
  通过 QEMU/Cloud Hypervisor 的设备模拟

gVisor 与 GPU:
  通过 "NVIDIA GPU sentry extension" 部分支持
  拦截 ioctl 调用,转发到 Host 端 GPU 驱动
  安全策略:只允许白名单中的 ioctl 命令

CubeSandbox 与 GPU:
  可选 GPU 直通(用于 RL 训练场景)
  大部分 Agent 不需要 GPU(LLM 推理在远端)

AT.4 GPU 安全隐患

已知 GPU 安全问题:

1. GPU 显存隔离不完整
   - 某些老 GPU 不清零已释放的显存
   - 后一个租户可能读到前一个租户的数据(模型权重/推理结果)
   - 解决:GPU 分配前强制清零(cudaMemset 全 0)

2. GPU DMA 攻击
   - GPU 通过 DMA 可以读写主机内存
   - 如果 IOMMU 配置不当,VM 内的 GPU 可以读取 Host 内存
   - 解决:正确配置 IOMMU + 限制 DMA 区域

3. GPU 侧信道
   - GPU 缓存时序侧信道(类似 CPU Spectre)
   - 通过 GPU 性能计数器推断其他租户行为
   - 研究阶段,MIG 提供了一定隔离

4. 驱动漏洞
   - GPU 驱动运行在 Host 内核中
   - 恶意 ioctl 调用可能触发驱动漏洞
   - 解决:ioctl 白名单 + 驱动沙箱化

AT.5 AI Agent 场景的 GPU 策略建议

策略建议:

大多数 Agent 沙箱:不分配 GPU
  - LLM 推理走远程 API
  - 不需要本地 GPU
  - 零 GPU 相关攻击面

需要计算的 Agent:API 转发模式
  - 沙箱内安装 CUDA stub 库
  - 实际计算转发到 Host 端 GPU 池
  - 可限制计算时间和显存配额

训练/RL 场景:MIG 隔离
  - A100/H100 划分 MIG 实例
  - 每个训练 Agent 获得固定资源
  - 硬件级隔离保证安全

附录 AU:可观测性与监控——沙箱的”透视镜”

AU.1 为什么沙箱需要可观测性?

沙箱的核心诉求是”隔离”,但运维人员需要”看见”沙箱内部发生了什么。这产生了矛盾:既要隔离,又要观测。解决这个矛盾的关键是:在沙箱外部收集信息,而不是进入沙箱内部。

AU.2 三大可观测性支柱

支柱一:指标(Metrics)— 数字化的健康状态
  收集什么:
    - CPU 使用率(从 cgroup cpu.stat 读取)
    - 内存使用量(memory.current / memory.max)
    - IO 速率(io.stat)
    - 网络流量(eBPF TC 统计)
    - 进程数(pids.current)
    - 系统调用频率(seccomp 审计)
  
  怎么收集:
    - Host 端直接读取 cgroup 文件(沙箱外)
    - eBPF 程序附着在网络/系统调用路径
    - VMM 的 /metrics API(Firecracker)
  
  存储和展示:
    - Prometheus 抓取 → Grafana 面板
    - 自定义 exporter 暴露 per-sandbox 指标

支柱二:日志(Logs)— 事件流
  收集什么:
    - Agent 代码的 stdout/stderr
    - 系统调用审计日志(seccomp audit)
    - 网络连接日志(eBPF socket 事件)
    - 文件操作日志(inotify / eBPF)
  
  怎么收集:
    - 容器运行时的日志驱动(containerd log)
    - vsock 传输日志到 Host
    - 串口输出(Firecracker 的 console)
  
  挑战:
    - 日志量可能巨大(每个 syscall 都记录?)
    - 需要采样策略:正常时低采样,异常时高采样

支柱三:追踪(Tracing)— 因果链
  目标:追踪一个 Agent 请求的完整生命周期
  
  一个 Agent 执行请求的追踪:
    [LLM API 收到请求] 
    → [调度器选择沙箱]
    → [沙箱启动/恢复] 
    → [代码注入执行]
    → [syscall 执行链]
    → [网络请求(如 pip install)]
    → [结果收集返回]
  
  实现:
    - OpenTelemetry SDK(在编排层)
    - eBPF tracing(在内核层)
    - 关联 ID 串联各组件

AU.3 异常检测:从观测到行动

基于可观测数据的异常检测规则:

规则 1:资源异常
  IF memory_usage > 0.9 * memory_max FOR 30s
  THEN alert "内存即将耗尽,可能是内存泄漏"
  ACTION: 记录 memory.stat 快照 + 进程列表

规则 2:行为异常
  IF syscall_rate(fork) > 100/s
  THEN alert "疑似 fork bomb"
  ACTION: 自动 kill 沙箱 + 记录事件

规则 3:网络异常
  IF outbound_connections > 50 AND unique_ips > 20 IN 60s
  THEN alert "疑似端口扫描或 C2 通信"
  ACTION: 断开网络 + 保留快照供取证

规则 4:执行时间异常
  IF sandbox_runtime > 5 * expected_duration
  THEN alert "执行时间异常,可能死循环"
  ACTION: 终止执行 + 返回超时错误

规则 5:文件系统异常
  IF file_write_rate > 100MB/s OR total_written > 10GB
  THEN alert "大量文件写入,可能是磁盘填充攻击"
  ACTION: 限速 IO + 记录写入路径

AU.4 沙箱取证:事后分析

当安全事件发生时,需要能回溯沙箱内发生了什么:

取证数据收集:
  1. 执行时间线:syscall 序列 + 时间戳
  2. 文件系统快照:事件发生时的完整文件系统状态
  3. 网络流量 PCAP:完整网络包捕获
  4. 内存快照:VM 内存转储(如果是 microVM)
  5. 进程树:事件发生时的完整进程层级

存储策略:
  - 正常沙箱:保留最近 24h 的摘要指标
  - 可疑沙箱:保留完整取证数据 7 天
  - 确认攻击:永久保留 + 自动创建安全事件报告

AU.5 eBPF 在沙箱可观测性中的角色

eBPF 可观测程序示例:

// 跟踪沙箱内所有 execve 调用(新程序执行)
SEC("tracepoint/syscalls/sys_enter_execve")
int trace_execve(struct trace_event_raw_sys_enter *ctx) {
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    // 检查是否属于沙箱 cgroup
    if (!in_sandbox_cgroup(pid)) return 0;
    
    struct exec_event event = {};
    event.pid = pid;
    event.timestamp = bpf_ktime_get_ns();
    bpf_probe_read_user_str(event.filename, sizeof(event.filename),
                            (void *)ctx->args[0]);
    
    // 发送到用户空间
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
                          &event, sizeof(event));
    return 0;
}

// 跟踪网络连接建立
SEC("kprobe/tcp_connect")
int trace_connect(struct pt_regs *ctx) {
    struct sock *sk = (struct sock *)PT_REGS_PARM1(ctx);
    u32 pid = bpf_get_current_pid_tgid() >> 32;
    
    if (!in_sandbox_cgroup(pid)) return 0;
    
    struct connect_event event = {};
    event.pid = pid;
    bpf_probe_read(&event.daddr, sizeof(event.daddr),
                   &sk->__sk_common.skc_daddr);
    bpf_probe_read(&event.dport, sizeof(event.dport),
                   &sk->__sk_common.skc_dport);
    
    bpf_perf_event_output(ctx, &events, BPF_F_CURRENT_CPU,
                          &event, sizeof(event));
    return 0;
}

附录 AV:多租户隔离——当成千上万 Agent 共享一个集群

AV.1 多租户的挑战

想象一栋办公楼(物理机集群)同时服务多个公司(租户),每个公司有多个员工(Agent)。需要保证:公司之间完全隔离,同公司内的员工有一定共享。

AV.2 多租户架构层次

层次一:账户/项目隔离
  每个租户有独立的:
    - 命名空间(Kubernetes Namespace)
    - 资源配额(ResourceQuota)
    - 网络策略(NetworkPolicy)
    - 服务账号(ServiceAccount)

层次二:计算隔离
  方案 A:共享集群 + 安全容器
    所有租户在同一 K8s 集群
    每个 Pod 用 Firecracker/Kata 运行
    优点:资源利用率高
    缺点:仍共享控制平面

  方案 B:独立集群
    每个大租户获得独立 K8s 集群
    物理机级隔离
    优点:最强隔离
    缺点:资源利用率低、运维复杂

  方案 C:混合模式(推荐)
    控制平面共享,计算节点按租户分池
    敏感租户获得专用节点(节点亲和性)
    普通租户共享节点 + 安全容器

层次三:数据隔离
  - 每个租户独立的文件存储卷
  - 加密密钥按租户隔离(不同 KMS key)
  - 日志和指标按租户分隔
  - 网络流量按租户标记和隔离

AV.3 “吵闹邻居”问题

问题描述:
  租户 A 的 Agent 正在大量编译代码(CPU 密集)
  租户 B 的 Agent 在同一物理机上,CPU 被抢占
  结果:租户 B 的 Agent 响应变慢

解决策略:

CPU 隔离:
  - 使用 cgroup cpu.max 硬限制(每个沙箱最多 N 核)
  - CPU pinning:将租户绑定到特定 CPU 核(防止 L3 缓存竞争)
  - NUMA 感知调度:同一租户的沙箱在同一 NUMA 节点

内存隔离:
  - memory.max 硬限制(不允许超量分配内存)
  - 禁用 swap(防止 IO 抖动影响邻居)
  - Page Cache 隔离(cgroup v2 memory controller 支持)

IO 隔离:
  - io.max 限制每个沙箱的磁盘带宽
  - BFQ(Budget Fair Queueing)IO 调度器
  - 独立的磁盘卷(物理隔离 IO 路径)

网络隔离:
  - TC(Traffic Control)限速
  - eBPF 公平队列
  - 独立的网络 namespace + 带宽限制

AV.4 租户资源配额管理

# Kubernetes ResourceQuota 示例
apiVersion: v1
kind: ResourceQuota
metadata:
  name: tenant-alpha-quota
  namespace: tenant-alpha
spec:
  hard:
    requests.cpu: "32"           # 最多请求 32 核
    requests.memory: "64Gi"      # 最多请求 64GB
    limits.cpu: "64"             # 硬限制 64 核
    limits.memory: "128Gi"       # 硬限制 128GB
    pods: "100"                  # 最多 100 个 Pod(沙箱)
    persistentvolumeclaims: "10" # 最多 10 个持久卷
---
# LimitRange:每个沙箱的默认资源限制
apiVersion: v1
kind: LimitRange
metadata:
  name: sandbox-limits
  namespace: tenant-alpha
spec:
  limits:
  - type: Container
    default:
      cpu: "2"
      memory: "4Gi"
    defaultRequest:
      cpu: "1"
      memory: "2Gi"
    max:
      cpu: "4"
      memory: "8Gi"

AV.5 网络微隔离

Kubernetes NetworkPolicy + Cilium eBPF:

# 租户 Alpha 的 Agent 只能:
# 1. 访问自己命名空间内的服务
# 2. 访问公网(通过 egress proxy)
# 3. 不能访问其他租户、不能访问集群内部服务

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: tenant-isolation
  namespace: tenant-alpha
spec:
  podSelector: {}     # 适用于该命名空间所有 Pod
  policyTypes:
    - Ingress
    - Egress
  ingress:
    - from:
        - namespaceSelector:
            matchLabels:
              tenant: alpha    # 只允许同租户入站
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              tenant: alpha    # 同租户内通信
    - to:
        - ipBlock:
            cidr: 0.0.0.0/0   # 允许公网出站
            except:
              - 10.0.0.0/8     # 但禁止访问内网
              - 172.16.0.0/12
              - 192.168.0.0/16

附录 AW:沙箱编排的高级模式——Pool、Pipeline 与 Tree

AW.1 Pool 模式(预热池)

Pool 模式:提前创建一批沙箱,请求来了直接分配

   ┌─────────────────────────────────────────────┐
   │  沙箱预热池                                   │
   │                                              │
   │  ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐      │
   │  │ S │ │ S │ │ S │ │ S │ │ S │ │ S │ ...  │
   │  │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ 6 │      │
   │  └───┘ └───┘ └───┘ └───┘ └───┘ └───┘      │
   │   idle   idle  idle  busy  idle  busy       │
   └─────────────────────────────────────────────┘
   
   请求到达 → 从 idle 池取一个 → 标记 busy → 执行
   执行完毕 → 重置状态 → 归还 idle 池
   池水位低 → 异步补充新沙箱

参数调优:
  pool_size = 预估并发 × 1.5(余量防突发)
  warmup_time = 沙箱创建时间(决定补充速度)
  max_idle_time = 空闲沙箱回收时间(节省资源)
  reset_strategy = 
    快速重置(清文件 + 恢复快照)vs 
    销毁重建(更安全但更慢)

AW.2 Pipeline 模式(流水线)

Pipeline 模式:Agent 的执行分多个阶段,每个阶段可能用不同配置的沙箱

Stage 1: 代码分析    Stage 2: 构建编译    Stage 3: 测试运行
┌────────────┐      ┌────────────┐      ┌────────────┐
│ 轻量沙箱    │  →   │ 重型沙箱    │  →   │ 网络沙箱    │
│ 1 CPU, 1GB │      │ 4 CPU, 8GB │      │ 2 CPU, 4GB │
│ 无网络      │      │ 允许下载    │      │ 允许连接    │
│ 只读文件    │      │ 读写文件    │      │ 测试环境    │
└────────────┘      └────────────┘      └────────────┘

优点:
  - 每个阶段最小权限原则
  - 代码分析不需要网络(减少攻击面)
  - 编译阶段给更多资源(编译很吃 CPU)
  - 测试阶段允许网络但限制范围

数据在阶段间传递:
  - 通过共享卷(volumeMount)
  - 或通过对象存储(S3/MinIO)
  - 每个阶段只能看到上一阶段的输出

AW.3 Tree 模式(分支并行)

Tree 模式:Agent 的探索过程形成树状结构,每个分支独立沙箱

          Root(初始沙箱快照)
         /        |        \
      分支A     分支B     分支C
     (方案1)   (方案2)   (方案3)
      / \        |
   A1   A2     B1
   
用途:
  1. 并行尝试多种解决方案(如 Best-of-N 采样)
  2. 回溯:某个方案失败了,回到分叉点尝试另一个
  3. RL 训练:Monte Carlo Tree Search (MCTS) 风格

实现方式:
  1. 基于 CoW 快照(CubeSandbox):
     - 创建分支 = 快照 fork(5ms)
     - 完全独立的文件系统和进程状态
     - 内存级别共享未修改页面
  
  2. 基于 Git 分支:
     - 每个分支对应一个 git branch
     - 回溯 = checkout 到之前的 commit
     - 只适用于文件系统状态,不包含进程状态

  3. 混合方式:
     - 文件状态用 git
     - 进程/VM 状态用 CoW 快照
     - 两者配合实现完整的 checkpoint/restore

成本分析(假设 1000 个并行分支):
  传统方式(每个分支独立 VM):
    1000 × 2GB = 2TB 内存 ← 不现实
  
  CoW 方式(共享基础页面):
    基础 = 2GB + 每个分支的 delta
    如果每个分支修改 50MB → 2GB + 1000 × 50MB = 52GB ← 可行

AW.4 Autoscaling:根据负载自动伸缩

水平伸缩策略:

触发条件:
  - 池利用率 > 80% 持续 30s → 扩容
  - 池利用率 < 30% 持续 5min → 缩容
  - 排队等待 > 10 个请求 → 紧急扩容

扩容策略:
  快速扩容(从快照恢复):5-50ms/个
  常规扩容(冷启动新 VM):125ms/个
  深度扩容(申请新物理机):分钟级

缩容策略:
  优雅缩容:等待当前任务完成 → 回收
  定时缩容:低峰期(如凌晨)回收到最低水位
  成本优化:使用 Spot/抢占式实例,随时可回收

HPA(Horizontal Pod Autoscaler)配置示例:
  metrics:
    - type: Pods
      pods:
        metric:
          name: sandbox_queue_depth
        target:
          type: AverageValue
          averageValue: "3"    # 每个 Pod 平均排队 3 个请求时扩容

附录 AX:seccomp-BPF 实战——精细的系统调用过滤

AX.1 为什么 seccomp 是容器安全的基石?

Linux 有约 450 个系统调用,但一个典型的 Web 应用只用到约 50-100 个。那些不需要的系统调用(如 mount、reboot、kexec_load)只是多余的攻击面。seccomp-BPF 允许精确指定哪些系统调用是允许的。

AX.2 seccomp 的工作原理

                    用户空间
                       │
                  系统调用
                       │
        ┌──────────────▼──────────────┐
        │    seccomp-BPF 过滤器        │
        │                              │
        │  syscall_nr == __NR_execve?  │
        │    → 检查参数                 │
        │    → ALLOW / KILL / ERRNO    │
        └──────────────┬──────────────┘
                       │ (如果 ALLOW)
                       ▼
                    内核处理

过滤动作:
  SCMP_ACT_ALLOW    — 允许执行
  SCMP_ACT_KILL     — 立即杀死进程
  SCMP_ACT_ERRNO(n) — 返回错误码 n
  SCMP_ACT_TRAP     — 发送 SIGSYS 信号
  SCMP_ACT_LOG      — 允许但记录日志
  SCMP_ACT_NOTIFY   — 通知用户空间处理(seccomp notify)

AX.3 Docker 默认 seccomp profile 分析

{
    "defaultAction": "SCMP_ACT_ERRNO",
    "defaultErrnoRet": 1,
    "syscalls": [
        {
            "names": ["accept", "bind", "close", "connect",
                      "read", "write", "open", "stat", "fstat",
                      "mmap", "mprotect", "brk", "ioctl",
                      "socket", "sendto", "recvfrom", "clone",
                      "execve", "wait4", "kill", "getpid",
                      "...约 300 个允许的 syscall"],
            "action": "SCMP_ACT_ALLOW"
        },
        {
            "names": ["mount", "umount2", "reboot",
                      "kexec_load", "init_module",
                      "delete_module", "pivot_root",
                      "swapon", "swapoff", "..."],
            "action": "SCMP_ACT_ERRNO",
            "errnoRet": 1
        }
    ]
}

Docker 默认允许 ~313  syscall,阻止 ~137 
被阻止的主要是:
  - 系统管理类(mount, reboot, swapon)
  - 内核模块类(init_module, finit_module)
  - 命名空间类(unshare, setns)— 防止容器内再创建容器
  - 性能事件类(perf_event_open)— 防止侧信道

AX.4 Firecracker 的 seccomp 策略

Firecracker 的 seccomp profile 比 Docker 严格得多——只允许 VMM 进程需要的最少 syscall:

Firecracker VMM 进程允许的 syscall(约 35 个):
  - 文件描述符:read, write, close, dup, fcntl
  - 内存管理:mmap, munmap, madvise, brk
  - 事件循环:epoll_create1, epoll_ctl, epoll_wait
  - KVM 操作:ioctl (只允许 KVM 相关 ioctl 号)
  - 信号:rt_sigaction, rt_sigprocmask, sigaltstack
  - 线程:clone (只允许创建线程), futex
  - 退出:exit, exit_group

  被阻止的关键 syscall:
  - open/openat(VMM 启动后不应该再打开文件)
  - socket(VMM 不应该创建新连接)
  - execve(VMM 不应该执行其他程序)
  
  这意味着即使 VMM 被攻破,攻击者也几乎无法做任何有意义的操作。

AX.5 seccomp notify:用户空间 syscall 处理

Linux 5.0 引入了 seccomp notify,允许将被拦截的系统调用转发到用户空间处理:

seccomp notify 流程:
  1. 进程 A(被监控进程)发出 mount() syscall
  2. seccomp filter 匹配到 → SCMP_ACT_NOTIFY
  3. 进程 A 被阻塞
  4. 监控进程 B 从 seccomp notify fd 收到通知
  5. 进程 B 检查参数,决定是否允许
  6. 进程 B 回复:允许(注入返回值)或拒绝(注入错误码)
  7. 进程 A 继续执行

应用场景:
  - 容器内 mount 操作:转发到外部 handler 在安全环境执行
  - 特权操作代理:容器内 setuid 等操作由外部代理完成
  - gVisor 的 systrap 平台:用 seccomp notify 拦截 syscall

AX.6 为 AI Agent 定制 seccomp profile

AI Agent 沙箱的最小 syscall 集(Python 运行时需要):

必需的基础 syscall(~80 个):
  内存:brk, mmap, munmap, mprotect, mremap
  文件:open, openat, read, write, close, stat, fstat, lstat
       access, readlink, getcwd, rename, unlink, mkdir
  进程:clone, execve, wait4, exit_group, getpid, getppid
  信号:rt_sigaction, rt_sigprocmask, rt_sigreturn
  时间:clock_gettime, nanosleep, timer_create
  网络:socket, connect, sendto, recvfrom, bind, listen, accept
  其他:ioctl, fcntl, pipe, dup2, epoll_create, epoll_ctl, epoll_wait

可选(根据需求开启):
  fork/vfork — 如果 Agent 需要启动子进程(如 subprocess.run)
  ptrace — 如果 Agent 需要调试(通常禁止)
  mount — 始终禁止
  reboot — 始终禁止
  
Agent 特有的限制:
  - keyctl, bpf — 禁止(内核攻击面)
  - personality — 禁止(防止切换到 32 位模式绕过检查)
  - unshare — 禁止(防止创建新 namespace 逃逸)

附录 AY:Linux Capabilities——权限的精细拆分

AY.1 从”全能 root”到”按需授权”

传统 Unix 权限模型只有两级:普通用户(什么都不能做)和 root(什么都能做)。这太粗暴了——一个程序可能只需要”绑定低端口”这一个特权,但传统方式必须给它完整的 root 权限。

Linux Capabilities 把 root 的”超能力”拆分成约 40 个独立的小权限:

AY.2 关键 Capabilities 列表

CAP_NET_BIND_SERVICE  — 绑定 < 1024 端口
CAP_NET_RAW           — 使用 RAW socket(ping 命令需要)
CAP_NET_ADMIN         — 网络管理(iptables, tc, 路由修改)
CAP_SYS_ADMIN         — "万能钥匙"(mount, namespace, bpf...)
CAP_SYS_PTRACE        — 跟踪其他进程(调试器需要)
CAP_DAC_OVERRIDE      — 忽略文件权限检查
CAP_SETUID            — 切换用户 ID
CAP_SETGID            — 切换组 ID
CAP_KILL              — 发送信号给任意进程
CAP_SYS_MODULE        — 加载内核模块
CAP_SYS_RAWIO         — 直接 IO 操作(/dev/mem 等)
CAP_SYS_BOOT          — reboot
CAP_SYS_TIME          — 修改系统时间
CAP_MKNOD             — 创建设备文件
CAP_SYS_CHROOT        — chroot
CAP_AUDIT_WRITE       — 写入内核审计日志
CAP_SETFCAP           — 设置文件 capabilities

AY.3 Docker 默认 Capabilities

Docker 默认保留的 capabilities(14 个):
  CAP_CHOWN, CAP_DAC_OVERRIDE, CAP_FSETID, CAP_FOWNER,
  CAP_MKNOD, CAP_NET_RAW, CAP_SETGID, CAP_SETUID,
  CAP_SETFCAP, CAP_SETPCAP, CAP_NET_BIND_SERVICE,
  CAP_SYS_CHROOT, CAP_KILL, CAP_AUDIT_WRITE

Docker 默认删除的 capabilities(约 26 个):
  CAP_SYS_ADMIN     — 最危险的"万能钥匙"
  CAP_SYS_MODULE    — 加载内核模块
  CAP_SYS_RAWIO     — 直接 IO
  CAP_SYS_PTRACE    — 跟踪进程
  CAP_NET_ADMIN     — 网络管理
  CAP_SYS_TIME      — 修改时间
  CAP_SYS_BOOT      — 重启
  ...

AI Agent 沙箱应该删除所有 capabilities:
  docker run --cap-drop=ALL my-sandbox
  
  理由:Agent 代码不需要任何特权操作
  如果需要网络 → 通过 Host 端代理
  如果需要文件 → 通过 volume mount(非 root 权限)

AY.4 Capabilities 与容器逃逸

CVE-2022-0185:利用 CAP_SYS_ADMIN 逃逸

漏洞链:
  1. 容器拥有 CAP_SYS_ADMIN(某些错误配置)
  2. 利用 CAP_SYS_ADMIN 创建 user namespace
  3. 在新 namespace 中触发文件系统 bug
  4. 堆溢出 → 内核代码执行 → 容器逃逸

教训:
  - CAP_SYS_ADMIN 几乎等于 root
  - 即使只需要 mount,给 CAP_SYS_ADMIN 也太危险
  - AI Agent 沙箱:绝不给任何 capability

附录 AZ:时间旅行调试——快照在开发者体验中的应用

AZ.1 概念:如果代码执行可以”倒带”

传统调试是线性的:设置断点 → 运行 → 观察 → 如果错过了关键时刻,重新开始。时间旅行调试(Time-Travel Debugging)允许你在执行历史中自由移动:前进、后退、跳到任意时间点。

沙箱快照技术使得时间旅行不仅限于单个进程,而是整个环境的状态都可以”倒带”。

AZ.2 时间旅行在 AI Agent 中的应用

场景一:Agent 犯了错误,自动回退

  时间 T1: Agent 写了一段代码      ← 快照 S1
  时间 T2: Agent 运行测试,通过     ← 快照 S2
  时间 T3: Agent 重构代码           ← 快照 S3
  时间 T4: Agent 运行测试,失败!
  
  策略:恢复到 S2,尝试不同的重构方式
  成本:5ms(CoW 恢复)而不是重新执行 T1-T2 的所有操作

场景二:并行探索 + 事后选择最优

  快照 S0(初始状态)
  ├── 分支 A:Agent 用方案 A 解决 → 结果 R_A
  ├── 分支 B:Agent 用方案 B 解决 → 结果 R_B
  └── 分支 C:Agent 用方案 C 解决 → 结果 R_C
  
  评估 R_A, R_B, R_C → 选择最优 → 提交

场景三:RL 训练中的探索

  MCTS 风格的 Agent 训练:
  1. 从当前状态快照
  2. 执行一个 action
  3. 评估奖励
  4. 恢复快照,尝试另一个 action
  5. 构建搜索树,选择最优路径
  
  关键要求:恢复速度 < 10ms(否则训练效率太低)

AZ.3 实现时间旅行的技术栈

方案一:VM 级快照(CubeSandbox)
  快照内容:vCPU 寄存器 + 内存页表 + 设备状态
  恢复方式:CoW 映射旧内存页 + 恢复寄存器
  恢复速度:5-10ms
  恢复完整性:完美(包含进程状态、网络连接状态等)
  
方案二:文件系统级快照(Btrfs/ZFS)
  快照内容:文件系统状态
  恢复方式:文件系统回滚
  恢复速度:< 100ms
  恢复完整性:只有文件系统(进程状态丢失)

方案三:进程级快照(CRIU)
  快照内容:进程内存 + 文件描述符 + 信号状态
  恢复方式:重新创建进程并注入状态
  恢复速度:100-500ms
  恢复完整性:中等(某些内核状态无法完美恢复)

方案四:应用级快照(Git + 环境变量)
  快照内容:工作目录的 git commit
  恢复方式:git checkout
  恢复速度:< 50ms
  恢复完整性:只有文件内容(进程、内存状态全部丢失)

AZ.4 时间旅行的限制

不可逆操作:某些操作无法通过恢复快照来"撤销"
  - 已发送的网络请求(HTTP/邮件/API 调用)
  - 已写入的外部数据库记录
  - 已消费的消息队列消息
  - 已触发的 webhook

应对策略:
  - 网络请求:通过代理拦截,支持重放
  - 外部状态:使用"补偿事务"模式
  - 消息队列:使用幂等消费者
  - 最佳实践:沙箱内的操作尽量保持本地性,
    只在"提交"时才真正与外部交互

附录 BA:安全容器的攻防史——从 CVE 到加固

BA.1 经典容器逃逸案例分析

CVE-2019-5736:runc 容器逃逸

攻击原理:
  1. 恶意容器覆盖 Host 上的 /usr/bin/runc 二进制
  2. 下次任何人在该 Host 上启动/exec 容器时
  3. 被覆盖的 runc 执行恶意代码(以 root 权限)

攻击步骤:
  a. 容器内进程 exec 一个程序
  b. 通过 /proc/self/exe 获取 runc 进程的文件描述符
  c. 在极短的时间窗口内覆盖 runc 二进制
  d. 后续容器操作执行被篡改的 runc

影响:
  - 所有使用 runc 的容器运行时(Docker, containerd, CRI-O)
  - 允许完全控制 Host
  
修复:
  - runc 1.0-rc7 后用 memfd 执行自身(覆盖无效)
  - 安全容器完全避免此问题(runc 在 VM 外部)

CVE-2020-15257:containerd 逃逸

攻击原理:
  - containerd-shim 的 API socket 暴露给了容器
  - 容器内进程可以通过 abstract Unix socket 访问 shim API
  - 调用 shim API 可以创建/销毁容器、访问 Host 文件

影响:
  - containerd < 1.3.9, < 1.4.3
  - 需要容器与 shim 在同一 network namespace
  
修复:
  - 使用 filesystem-based Unix socket(受 mount namespace 隔离)
  - 安全容器:shim 在 VM 外部,VM 内无法访问 shim socket

BA.2 Dirty Pipe(CVE-2022-0847)

漏洞原理:
  Linux 内核的 pipe 缓冲区实现有 bug
  可以覆盖只读文件的 page cache
  从而修改任意文件(包括 /etc/passwd, SUID 二进制等)

容器环境影响:
  - 传统容器:可以逃逸(修改 Host 上的文件)
  - gVisor:不受影响(自己实现了 pipe,不用 Linux pipe)
  - Firecracker:不受影响(Guest 内核 bug 不影响 Host)
  - Kata:不受影响(同上,独立内核)

这是安全容器价值的完美例证:
  共享内核的容器 → 受影响
  独立内核的安全容器 → 不受影响

BA.3 Spectre/Meltdown 类侧信道攻击

Spectre v1/v2 对容器/VM 的影响:

传统容器:
  - 共享内核 → 可以通过投机执行读取内核内存
  - 同一物理核的 SMT 兄弟线程可以互相泄露数据
  - 缓解:内核补丁(retpoline, IBRS, STIBP)+ 性能损失

虚拟机:
  - VM 之间通过物理核共享仍然可能泄露
  - 缓解:关闭 SMT 或将不同租户固定到不同物理核
  - 性能损失:关闭 SMT 损失 20-30% 吞吐

AI Agent 场景的考量:
  - 如果 Agent 执行不可信代码,可能利用侧信道
  - 读取同一物理核上其他 Agent 的敏感数据
  - 缓解:CPU pinning + 禁止性能计数器 + 限制计时精度

BA.4 供应链攻击:从 pip install 到沙箱逃逸

攻击链:
  1. 攻击者在 PyPI 上发布恶意包(包名相似:reqeusts vs requests)
  2. AI Agent 的 LLM 生成了 "pip install reqeusts"(拼写错误)
  3. 恶意包的 setup.py 在安装时执行代码
  4. 代码尝试:
     a. 读取环境变量(API keys)
     b. 反弹 shell 到攻击者服务器
     c. 利用内核漏洞逃逸容器

沙箱如何防护:
  L2(Docker):
    - setup.py 可以执行,但网络受限
    - 如果 Agent 有 API key 在环境变量中 → 泄露
  
  L3(Firecracker/CubeSandbox):
    - 即使 setup.py 执行恶意代码
    - 网络可以被完全阻断或限制白名单
    - 环境变量中不存放敏感信息
    - 即使利用 Guest 内核漏洞,也被 VM 隔离

最佳实践:
  - 预装好常用包到模板镜像(避免运行时安装)
  - pip install 时网络白名单只允许 pypi.org
  - 使用 hash-pinned requirements.txt
  - 安装后检查已安装包列表是否符合预期

BA.5 沙箱加固清单

物理层:
  □ 关闭 SMT(或 CPU pinning 隔离)
  □ 启用 IOMMU(DMA 隔离)
  □ BIOS 安全启动 + TPM

Host 内核层:
  □ 最新稳定内核 + 安全补丁
  □ 关闭不必要的内核模块
  □ sysctl 加固(kernel.unprivileged_userns_clone=0 等)
  □ 启用 KASLR, KPTI, Retpoline

VMM/Runtime 层:
  □ seccomp profile(最小 syscall 集)
  □ 删除所有 capabilities
  □ 只读 rootfs
  □ 限制设备访问(仅 /dev/null, /dev/zero, /dev/urandom)
  □ 限制 /proc 和 /sys 的可见性
  □ VMM 自身的 seccomp(Firecracker jailer)

网络层:
  □ 默认拒绝所有出站
  □ 白名单方式允许必要的 API 端点
  □ 速率限制(防止 DDoS)
  □ DNS 过滤(防止 DNS 隧道)
  □ TLS 检查(可选,隐私权衡)

文件系统层:
  □ 只读 rootfs + tmpfs 工作目录
  □ noexec 挂载选项(非必要目录禁止执行)
  □ 磁盘配额(防止填满磁盘)
  □ 文件类型限制(禁止创建设备文件、符号链接攻击检测)

监控层:
  □ syscall 审计(关键操作记录)
  □ 网络连接日志
  □ 异常检测(行为基线 + 偏差告警)
  □ 取证快照(安全事件时自动保存完整状态)

附录 BB:容器镜像与 OCI 分发——从 Dockerfile 到运行时

BB.1 容器镜像的分层结构

容器镜像本质上是一组文件系统层(layers)的堆叠:

┌─────────────────────────────────────────┐
│ Layer 5: COPY app.py /app/              │  ← 应用代码(几 KB)
├─────────────────────────────────────────┤
│ Layer 4: RUN pip install flask numpy    │  ← Python 包(~200MB)
├─────────────────────────────────────────┤
│ Layer 3: RUN apt-get install python3    │  ← Python 运行时(~50MB)
├─────────────────────────────────────────┤
│ Layer 2: RUN apt-get update             │  ← 包索引更新(~30MB)
├─────────────────────────────────────────┤
│ Layer 1: FROM ubuntu:22.04              │  ← 基础 OS(~80MB)
└─────────────────────────────────────────┘

每一层是前一层的增量(类似 git commit):
  - 新增文件
  - 修改文件(覆盖下层同路径文件)
  - 删除文件(用 .wh.filename 标记删除)

镜像 = manifest(层列表)+ config(运行时配置)+ blobs(层内容)

BB.2 OCI Image Spec

OCI (Open Container Initiative) 镜像规范:

manifest.json:
{
    "schemaVersion": 2,
    "mediaType": "application/vnd.oci.image.manifest.v1+json",
    "config": {
        "mediaType": "application/vnd.oci.image.config.v1+json",
        "digest": "sha256:abc123...",
        "size": 1234
    },
    "layers": [
        {
            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "digest": "sha256:def456...",
            "size": 82944000
        },
        {
            "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip",
            "digest": "sha256:789abc...",
            "size": 52428800
        }
    ]
}

关键设计:
  - 内容寻址(content-addressable):通过 hash 引用层
  - 层共享:相同的层在多个镜像间只存储一份
  - 惰性加载(lazy loading):不需要下载所有层就能启动

BB.3 从镜像到运行时文件系统

镜像层 → OverlayFS → 容器可见的文件系统

OverlayFS 挂载:
  lowerdir = layer1:layer2:layer3:layer4:layer5  (只读层,下到上)
  upperdir = /var/lib/containers/overlay/abc/upper (可写层)
  workdir  = /var/lib/containers/overlay/abc/work  (OverlayFS 工作目录)
  merged   = /var/lib/containers/overlay/abc/merged(合并视图)

读操作:从上往下查找,第一个匹配的文件即为结果
写操作:Copy-up 到 upperdir,后续读写都在 upperdir
删除操作:在 upperdir 创建 whiteout 文件

容器 rootfs = merged 目录(通过 pivot_root 成为容器的 /)

BB.4 镜像对沙箱启动时间的影响

冷启动时间分解:
  拉取镜像(pull):      2-30s(取决于镜像大小和网络)
  解压层(extract):     0.5-5s(取决于层大小)
  设置 OverlayFS:        < 10ms
  创建容器:              50-200ms
  启动进程:              < 50ms

优化策略:
  1. 预拉取 + 本地缓存
     - 常用镜像预存在每台 Node 上
     - 镜像更新时增量拉取(只下载变化的层)
  
  2. 惰性拉取(Lazy Pull / Stargz / Nydus)
     - 不下载完整镜像,按需拉取文件
     - 容器先启动,后台继续下载
     - 启动时间从 30s 降到 < 1s
  
  3. 镜像转换为块设备
     - 提前将 OCI 镜像转换为 raw/qcow2 块设备
     - Firecracker 直接挂载块设备启动(无解压过程)
  
  4. 快照恢复
     - 镜像 + 初始化完成后创建快照
     - 后续启动直接恢复快照(< 10ms)

BB.5 AI Agent 的镜像策略

基础镜像层级:

Layer 1: 精简 OS(Alpine / Distroless)
Layer 2: Python 运行时 + 系统库
Layer 3: 常用包(numpy, requests, etc.)
Layer 4: Agent 框架(langchain / autogen 等)
Layer 5: 具体 Agent 代码(最小层,频繁变化)

优化思路:
  - 不变的层(1-3)预装,所有 Agent 共享
  - 框架层(4)按 Agent 类型预装
  - 代码层(5)轻量级注入(< 1MB)
  
  共享层 = 更高密度(CoW 页面共享效果更好)
  小的变化层 = 更快的构建和部署

反面教材:
  - 把所有依赖打成一个大层(无法共享,每次全量下载)
  - 在 Dockerfile 中 RUN pip install(每次构建重新安装)
  - 安装不需要的工具(gcc, vim, ssh)增加攻击面

附录 BC:Linux 调度器与沙箱性能——CFS 与 EEVDF

BC.1 为什么调度器对沙箱很重要?

每个物理 CPU 核心同一时间只能运行一个线程。当数百个沙箱的数千个线程竞争有限的 CPU 核心时,调度器决定了:谁先运行?运行多久?切换成本是多少?

好的调度策略能让所有沙箱获得公平的计算时间,差的策略会导致某些沙箱”饿死”。

BC.2 CFS(Completely Fair Scheduler)

CFS 的核心思想:维护每个任务的"虚拟运行时间"

虚拟运行时间 (vruntime):
  - 每个任务都有一个 vruntime 计数器
  - 实际运行 1ms,vruntime 增加 1ms / weight(权重高的任务增长慢)
  - 调度器总是选择 vruntime 最小的任务运行(红黑树查找,O(log N))

与 cgroup 的交互:
  每个 cgroup 有自己的调度实体
  cgroup 内的任务共享 cgroup 的 CPU 配额
  
  /sys/fs/cgroup/sandbox/agent-001/
  ├── cpu.max: "100000 100000"   # 最多 1 核(硬限制)
  ├── cpu.weight: 100            # 竞争时的权重(与 CFS vruntime 交互)
  └── cpu.stat:
      nr_periods: 12345          # 经过的调度周期数
      nr_throttled: 50           # 被限流的次数
      throttled_time: 500000000  # 被限流的总纳秒数

限流(Throttling)机制:
  当 cgroup 在一个 period(100ms)内用完了 quota(100ms = 1核)
  → 该 cgroup 的所有任务被暂停
  → 等到下一个 period 开始才恢复
  
  问题:限流导致尾延迟抖动(P99 变差)
  解决:适当放大 quota 或使用 cpu.weight 代替 cpu.max

BC.3 EEVDF(Earliest Eligible Virtual Deadline First)

Linux 6.6 引入了 EEVDF 调度器,替代了 CFS 中的部分逻辑:

CFS 的问题:
  - 基于 vruntime 的调度可能导致延迟不公平
  - 短任务(如交互式 Agent 等待输入后唤醒)
    可能要等很久才能被调度
  - CFS 的"唤醒抢占"机制是启发式的

EEVDF 的改进:
  - 每个任务有一个"虚拟截止时间"
  - 调度器优先运行截止时间最早的任务
  - 短突发任务获得更低的延迟
  
对 AI Agent 的影响:
  Agent 沙箱典型模式 = 长时间 idle + 短时间 burst
  (等待 LLM 响应时 idle,收到响应后快速执行代码)
  
  EEVDF 对这种模式更友好:
  - Agent 唤醒后能更快获得 CPU
  - 响应延迟更低、更可预测

BC.4 NUMA 感知调度

NUMA(Non-Uniform Memory Access)架构:
  现代多路服务器中,CPU 和内存分为多个 NUMA 节点
  访问本地内存 ~80ns,访问远程内存 ~150ns(慢 2x)

  ┌─────────────────┐    ┌─────────────────┐
  │  NUMA Node 0    │    │  NUMA Node 1    │
  │  CPU 0-15       │←──→│  CPU 16-31      │
  │  Memory 128GB   │ QPI│  Memory 128GB   │
  └─────────────────┘    └─────────────────┘

沙箱调度策略:
  方案 A:NUMA 绑定
    - 每个沙箱绑定到一个 NUMA 节点
    - 内存分配只在本地节点
    - 优点:性能稳定,无远程内存访问
    - 缺点:资源碎片化

  方案 B:NUMA 感知(推荐)
    - 优先在同一 NUMA 节点调度
    - 内存不足时可以借用远程节点
    - Linux 自动内存迁移(AutoNUMA)

  方案 C:无感知(默认)
    - 让内核自由调度
    - 可能导致大量远程内存访问
    - 性能抖动大

对于 AI Agent 高密度场景:
  推荐方案 B(NUMA 感知)+ CoW 优化:
  - 所有 Agent 的共享基础页面放在一个 NUMA 节点
  - 各 Agent 的私有页面尽量放在运行 CPU 同节点
  - 减少跨 NUMA 的内存流量

附录 BD:沙箱中的确定性执行——可复现性的挑战

BD.1 为什么确定性很重要?

对于 AI Agent 训练和测试,确定性执行至关重要:

场景一:RL 训练中,需要相同的初始状态 + 相同的 action → 相同的结果。否则奖励信号中充满噪声,策略无法收敛。

场景二:测试 Agent 时,需要确定性的环境来判断 Agent 行为的改进是策略提升还是环境随机性。

场景三:复现 bug 时,需要完全相同的执行环境来重现问题。

BD.2 非确定性的来源

来源一:时间
  - time() 返回不同的值
  - 超时计时器的到期时间不同
  - 基于时间的种子产生不同随机数
  
  解决:虚拟化时钟(Guest 看到固定或受控的时间)

来源二:调度顺序
  - 多线程程序的线程执行顺序
  - 信号的到达时间
  - IO 完成的顺序
  
  解决:确定性调度器(如 DET, rr)

来源三:网络
  - 外部 API 的响应内容和时间不确定
  - DNS 解析可能返回不同 IP
  - TLS 握手中的随机数
  
  解决:网络录制/回放(mock server)

来源四:文件系统
  - inode 编号分配顺序
  - 目录遍历顺序
  - 文件时间戳
  
  解决:确定性文件系统实现

来源五:随机数
  - /dev/urandom 的输出
  - ASLR(地址空间布局随机化)
  - stack canary 的随机值
  
  解决:固定随机种子 / 禁用 ASLR

BD.3 实现确定性执行的方案

方案一:记录/回放(Record/Replay)
  工具:rr(Mozilla 开发)
  原理:
    记录阶段:拦截所有非确定性事件(syscall 返回值、信号等)
    回放阶段:从记录中注入相同的返回值
  局限:单线程确定性(多线程需要序列化执行)
  适用:调试、bug 复现

方案二:虚拟化环境控制
  实现方式:
    - 虚拟时钟(TSC 虚拟化)
    - 确定性 /dev/urandom(固定种子 PRNG)
    - 禁用 ASLR(echo 0 > /proc/sys/kernel/randomize_va_space)
    - 固定 CPU 频率(关闭 turbo boost / 频率调节)
  局限:不完美(某些内核行为仍不确定)
  适用:RL 训练的粗粒度确定性

方案三:网络 Mock
  实现方式:
    - 代理所有出站请求
    - 首次执行时录制响应
    - 后续执行时回放录制的响应
  工具:VCR (pytest-recording), wiremock
  适用:确保外部依赖的确定性

BD.4 CubeSandbox 的确定性特性

CubeSandbox 快照恢复的确定性保证:

从同一快照恢复 = 完全相同的初始状态:
  ✓ CPU 寄存器完全相同
  ✓ 内存内容完全相同
  ✓ 文件系统状态完全相同
  ✓ 进程状态完全相同
  
恢复后的执行可能不确定的因素:
  ✗ 时间(恢复后的 clock_gettime 返回当前真实时间)
  ✗ 外部输入(网络响应可能不同)
  ✗ 调度(多核情况下线程交错可能不同)

提升确定性的配合措施:
  - 单核执行(消除调度不确定性)
  - 虚拟时钟(Guest 看到固定时间)
  - 网络录制/回放
  - 固定随机种子

附录 BE:沙箱的成本模型——如何计算真正的 TCO

BE.1 成本组成

AI Agent 沙箱平台的总拥有成本(TCO):

直接成本:
  1. 计算资源
     - CPU:每沙箱 1-4 核 × N 个并发沙箱
     - 内存:每沙箱 1-8GB × N(CoW 可降低实际占用)
     - 计算成本占总成本 50-70%
  
  2. 存储资源
     - 镜像存储:每镜像 0.5-5GB × M 种环境
     - 快照存储:每快照 50-500MB × 快照保留策略
     - 日志/审计:每沙箱 10-100MB/天
     - 存储成本占总成本 10-20%
  
  3. 网络资源
     - 出站带宽:pip install, git clone 等
     - 内部通信:Agent ←→ LLM API
     - 网络成本占总成本 5-10%

间接成本:
  4. 运维人力
     - 集群维护、安全补丁、监控告警
     - 占总成本 10-20%
  
  5. 安全合规
     - 安全审计、渗透测试
     - 占总成本 5-10%

BE.2 成本优化策略

策略一:CoW 内存共享
  场景:1000 个 Agent 使用相同 Python 环境
  不共享:1000 × 2GB = 2TB 内存
  CoW 共享:2GB 基础 + 1000 × 50MB delta = 52GB
  节省:2TB → 52GB(节省 97%)

策略二:沙箱生命周期优化
  问题:Agent 80% 时间在等待 LLM 响应(idle)
  解决:idle 时暂停沙箱(balloon 回收内存 / vCPU 不调度)
  等 Agent 需要执行时再恢复(< 10ms)
  效果:实际内存使用降低 60-80%

策略三:Spot/抢占式实例
  AI Agent 训练场景允许中断 → 使用 Spot 实例
  成本降低 60-90%
  配合快照:中断时自动快照 → 恢复到新实例

策略四:时间复用
  不同时区的用户 → 同一物理机白天服务 A 组,夜间服务 B 组
  工作日/周末差异 → 周末缩容

策略五:分层存储
  热数据(活跃沙箱的 rootfs)→ NVMe SSD
  温数据(快照)→ HDD / 对象存储
  冷数据(历史日志)→ 归档存储(Glacier 类)

BE.3 成本对比:各方案的单位成本

假设:1000 并发沙箱 × 24/7 运行 × 1 个月

方案一:Docker on EC2
  实例:40 × m6i.8xlarge(32 vCPU, 128GB)
  月成本:40 × $1,100 = $44,000
  密度:25 沙箱/机器

方案二:Firecracker on EC2 Bare Metal
  实例:15 × m6i.metal(128 vCPU, 512GB)
  月成本:15 × $4,500 = $67,500
  密度:67 沙箱/机器
  额外:更强隔离

方案三:CubeSandbox(CoW 优化)
  实例:8 × m6i.metal
  月成本:8 × $4,500 = $36,000
  密度:125 沙箱/机器
  CoW 效果:实际内存使用远低于分配值

方案四:E2B(托管服务)
  按需计费:$0.10/小时/沙箱
  月成本:1000 × 730 × $0.10 = $73,000
  优点:零运维
  缺点:单价高、锁定

每沙箱小时成本:
  Docker:      $0.060
  Firecracker: $0.092
  CubeSandbox: $0.049
  E2B:         $0.100

附录 BF:实战案例——构建一个 Agent 沙箱平台的架构决策

BF.1 需求场景

假设你要为一个 AI Agent 平台设计沙箱系统:支持 500 并发 Agent 执行代码、每个 Agent 需要 Python 环境、需要网络访问(调 API)、可能运行不可信代码。

BF.2 架构决策树

决策 1:隔离级别
  不可信代码 → 至少 L3 → microVM 或安全容器
  选择:Firecracker microVM

决策 2:启动策略
  500 并发 + 响应时间要求 < 1s → 需要预热池
  选择:维护 600 个 warm VM(20% 余量)
  恢复方式:从快照恢复(< 100ms)

决策 3:网络策略
  需要访问外部 API → 允许出站
  但不能访问内网 → 网络隔离
  选择:
    - eBPF 网络策略
    - 出站白名单(只允许 HTTPS 到指定域名)
    - DNS 过滤(防止内网解析)
    - 速率限制(防止 DDoS)

决策 4:文件系统
  Python 环境 + 代码执行 → 需要可写 workspace
  选择:
    - 只读 rootfs(包含 Python + 常用包)
    - tmpfs 工作目录(Agent 的代码在这里执行)
    - 最大 1GB 写入(防止填满磁盘)

决策 5:资源限制
  选择:
    - CPU:2 核(大部分时间 idle,burst 时够用)
    - 内存:2GB(Python + 数据处理足够)
    - 执行时间:最长 5 分钟/次(防止无限循环)
    - 进程数:256(防止 fork bomb)

决策 6:可观测性
  选择:
    - 每个 VM 的 CPU/内存指标(Prometheus)
    - Agent stdout/stderr 日志(结构化存储)
    - 网络连接日志(安全审计)
    - 异常检测规则(自动终止可疑行为)

BF.3 系统架构图

┌──────────────────────────────────────────────────────────┐
│  API Layer                                                │
│  ├── REST API: POST /sandbox/execute                     │
│  ├── WebSocket: 实时 stdout 流                           │
│  └── Auth: Bearer token + 租户识别                       │
└───────────────────────────┬──────────────────────────────┘
                            │
┌───────────────────────────┴──────────────────────────────┐
│  Orchestrator                                             │
│  ├── Pool Manager: 维护 warm VM 池                       │
│  ├── Scheduler: 分配请求到可用 VM                         │
│  ├── Lifecycle: 创建/销毁/回收/健康检查                   │
│  └── Metrics: Prometheus exporter                        │
└───────────────────────────┬──────────────────────────────┘
                            │
┌───────────────────────────┴──────────────────────────────┐
│  Compute Layer (per physical host)                        │
│  ├── Firecracker VMM × N                                 │
│  │   ├── VM 1: [Python env + Agent code execution]       │
│  │   ├── VM 2: [Python env + Agent code execution]       │
│  │   └── ...                                             │
│  ├── eBPF Network Policy Engine                          │
│  ├── cgroup Resource Controller                          │
│  └── Snapshot Storage (local NVMe)                       │
└──────────────────────────────────────────────────────────┘

BF.4 关键路径性能分析

请求生命周期:

T=0ms:     API 收到执行请求
T=1ms:     Scheduler 选择一个 warm VM
T=2ms:     通过 vsock 发送代码到 VM 内 agent
T=3ms:     VM 内 agent 将代码写入文件
T=5ms:     fork + exec Python 解释器
T=8ms:     Python 开始执行用户代码
T=8-5000ms: 代码执行(取决于代码复杂度)
T=done:    stdout/stderr 通过 vsock 返回
T=done+1:  API 返回结果给调用者
T=done+5:  VM 重置(清理 tmpfs + 恢复快照)

总额外延迟(不计代码执行本身):< 15ms
与原生执行相比:几乎无感知的开销


附录 BG:io_uring 在沙箱中的应用

BG.1 io_uring 基础模型

io_uring 是 Linux 5.1 引入的异步 I/O 框架,通过两个环形缓冲区(Submission Queue 和 Completion Queue)实现用户态与内核之间的零拷贝、零系统调用通信。

传统 I/O 模型 vs io_uring:

传统模型:
  用户态 → syscall(read) → 内核态切换 → 数据拷贝 → 返回用户态
  每次 I/O 至少 2 次上下文切换 + 1 次数据拷贝

io_uring 模型:
  用户态写入 SQE → 内核轮询 SQ → 执行 I/O → 写入 CQE → 用户态轮询 CQ
  批量提交:N 个 I/O 操作仅需 1 次 io_uring_enter() 或 0 次(SQPOLL 模式)

性能对比(4K 随机读,NVMe SSD):
  传统 read():     ~200K IOPS
  io_uring:        ~1.7M IOPS(8.5x 提升)
  io_uring+SQPOLL: ~2.1M IOPS(10.5x 提升)

核心数据结构:

// Submission Queue Entry (SQE) - 64 字节
struct io_uring_sqe {
    __u8  opcode;      // 操作类型:IORING_OP_READ, IORING_OP_WRITE, ...
    __u8  flags;       // SQE 标志:IOSQE_FIXED_FILE, IOSQE_IO_LINK, ...
    __u16 ioprio;      // I/O 优先级
    __s32 fd;          // 文件描述符(或 fixed file index)
    __u64 off;         // 文件偏移
    __u64 addr;        // 用户缓冲区地址(或 fixed buffer index)
    __u32 len;         // 数据长度
    union {
        __kernel_rwf_t rw_flags;
        __u32 fsync_flags;
        __u32 poll_events;
        __u32 sync_range_flags;
        __u32 msg_flags;
        // ... 各操作特定标志
    };
    __u64 user_data;   // 用户自定义数据,CQE 中原样返回
    // ... padding
};

// Completion Queue Entry (CQE) - 16 字节
struct io_uring_cqe {
    __u64 user_data;   // 对应 SQE 的 user_data
    __s32 res;         // 操作结果(>= 0 成功,< 0 为 -errno)
    __u32 flags;       // IORING_CQE_F_BUFFER, IORING_CQE_F_MORE, ...
};

BG.2 沙箱环境中的 io_uring 安全问题

io_uring 在沙箱环境中引入了独特的安全挑战。传统 seccomp-BPF 基于系统调用过滤,但 io_uring 允许通过 io_uring_enter() 单一系统调用执行几乎所有文件和网络操作,绕过了逐个 syscall 过滤的安全模型。

安全威胁模型:

1. Seccomp 绕过
   传统防护:seccomp 禁止 open/read/write
   io_uring 攻击:
     io_uring_setup()    → 创建 io_uring 实例
     io_uring_enter()    → 提交 IORING_OP_OPENAT + IORING_OP_READ
     结果:绕过 seccomp 对 open/read 的限制

2. 已知 CVE
   CVE-2022-29582: io_uring UAF(Use-After-Free)
     - 影响:Linux 5.15 ~ 5.17
     - 根因:io_uring 超时处理中的竞争条件
     - 后果:本地提权

   CVE-2023-2598: io_uring 缓冲区越界访问
     - 影响:Linux 6.1 ~ 6.3
     - 根因:fixed buffer 注册时长度校验不足
     - 后果:内核内存读写

   CVE-2024-0582: io_uring PBUF_RING mmap UAF
     - 影响:Linux 6.4 ~ 6.7
     - 根因:提供缓冲区环的内存释放后仍可访问
     - 后果:任意代码执行

3. 攻击面量化
   io_uring 支持的操作码数量(按内核版本):
     5.1:  ~20 个操作码
     5.10: ~35 个操作码
     6.0:  ~50 个操作码
     6.6:  ~60+ 个操作码
   每个操作码 = 一个潜在的内核攻击面

BG.3 沙箱中的 io_uring 策略

不同沙箱对 io_uring 采取截然不同的策略:

策略一:完全禁止(推荐用于高安全场景)

实现方式:seccomp 禁止 io_uring_setup / io_uring_enter / io_uring_register
适用:gVisor, 多数生产沙箱
原因:攻击面太大,安全审计困难

seccomp BPF 规则示例:
  BPF_STMT(BPF_LD | BPF_W | BPF_ABS, offsetof(struct seccomp_data, nr))
  BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_io_uring_setup, 0, 1)
  BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM)
  BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_io_uring_enter, 0, 1)
  BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM)
  BPF_JUMP(BPF_JMP | BPF_JEQ | BPF_K, __NR_io_uring_register, 0, 1)
  BPF_STMT(BPF_RET | BPF_K, SECCOMP_RET_ERRNO | EPERM)

策略二:操作码白名单(平衡方案)

实现方式:允许 io_uring 但限制可用操作码
适用:需要高 I/O 性能的受控沙箱
实现复杂度:需要内核补丁或 LSM hook

Linux 6.6+ 支持的 io_uring LSM hook:
  security_uring_override_creds()  → 控制 IORING_SETUP_CRED_SHARED
  security_uring_sqpoll()          → 控制 SQPOLL 线程创建
  security_uring_cmd()             → 控制 IORING_OP_URING_CMD

策略三:代理模式(VMM 层面)

实现方式:Guest 内 io_uring 请求由 VMM 代理执行
适用:microVM 沙箱(Firecracker 变体)
优势:Guest 获得 io_uring 性能,Host 维持安全边界

架构:
  Guest: App → io_uring → virtio-blk driver → MMIO 通知
  Host:  VMM → 收到 MMIO → io_uring 提交到物理设备 → CQE → 注入 Guest 中断

BG.4 io_uring 用于沙箱管理面

虽然在沙箱内部限制 io_uring,但在沙箱管理面(控制平面)使用 io_uring 可以显著提升性能:

场景一:并发沙箱创建

传统方式(epoll + 线程池):
  创建 1000 个沙箱:~3.2s
  瓶颈:每个沙箱创建涉及 clone3 + mount + pivot_root + 多次 write

io_uring 方式:
  创建 1000 个沙箱:~1.8s(44% 提升)
  优化:批量 mkdir + mount + write 合并为 SQE 链

实现伪代码:
  for sandbox in batch:
      // 链式 SQE:mkdir → mount → write_config → notify
      sqe1 = get_sqe(ring)
      prep_mkdir(sqe1, sandbox.rootfs_path)
      sqe1.flags |= IOSQE_IO_LINK  // 链接到下一个

      sqe2 = get_sqe(ring)
      prep_openat(sqe2, sandbox.config_path, O_WRONLY|O_CREAT)
      sqe2.flags |= IOSQE_IO_LINK

      sqe3 = get_sqe(ring)
      prep_write(sqe3, config_fd_placeholder, config_data)

  io_uring_submit(ring)  // 一次 syscall 提交所有

场景二:日志收集

每个沙箱产生 stdout/stderr 需要收集:
  1000 个沙箱 × 2 个管道 = 2000 个 fd
  传统 epoll:频繁的 epoll_ctl ADD/DEL + read syscall
  io_uring multishot recv:注册一次,持续接收

场景三:文件系统快照

CoW 快照创建涉及大量元数据操作:
  传统:串行 ioctl(FICLONE) for each file
  io_uring:批量 IORING_OP_URING_CMD with FICLONE

BG.5 io_uring 与 Rust 沙箱的集成

Rust 生态中 io_uring 的主要封装库及其在沙箱项目中的应用:

// 使用 io-uring crate 的沙箱管理器示例
use io_uring::{IoUring, opcode, types};

struct SandboxManager {
    ring: IoUring,
    pending: HashMap<u64, SandboxOp>,
    next_id: u64,
}

impl SandboxManager {
    fn new(queue_depth: u32) -> io::Result<Self> {
        let ring = IoUring::builder()
            .setup_sqpoll(2000)      // 2ms 空闲后休眠
            .setup_sqpoll_cpu(0)     // 绑定 CPU 0
            .setup_coop_taskrun()    // 协作式任务运行
            .setup_single_issuer()   // 单线程提交优化
            .build(queue_depth)?;

        Ok(Self { ring, pending: HashMap::new(), next_id: 0 })
    }

    fn batch_create_rootfs(&mut self, configs: &[SandboxConfig]) -> io::Result<()> {
        for config in configs {
            let id = self.next_id;
            self.next_id += 1;

            // 创建根文件系统目录
            let path = CString::new(config.rootfs_path.as_bytes())?;
            let mkdirat = opcode::MkDirAt::new(
                types::Fd(libc::AT_FDCWD),
                path.as_ptr(),
            )
            .mode(0o755)
            .build()
            .user_data(id)
            .flags(io_uring::squeue::Flags::IO_LINK);

            unsafe { self.ring.submission().push(&mkdirat)?; }
            self.pending.insert(id, SandboxOp::CreateDir(config.id));
        }

        self.ring.submit_and_wait(configs.len())?;

        // 处理完成事件
        for cqe in self.ring.completion() {
            let id = cqe.user_data();
            let result = cqe.result();
            if result < 0 {
                let op = self.pending.remove(&id).unwrap();
                eprintln!("Op {:?} failed: {}", op, io::Error::from_raw_os_error(-result));
            }
        }
        Ok(())
    }
}

附录 BH:Firecracker Jailer 深度解析

BH.1 Jailer 的设计哲学

Firecracker Jailer 是 Firecracker microVM 的安全外壳,它在启动 VMM 进程之前建立多层隔离。其设计哲学是”最小权限 + 纵深防御”:即使 VMM 进程被攻破,攻击者仍然被困在极度受限的环境中。

Jailer 建立的隔离层次(由外到内):

Layer 1: cgroup(资源限制)
  ├── CPU: cpu.max = "100000 100000"(最多 1 个 vCPU 时间)
  ├── Memory: memory.max = 512M
  ├── IO: io.max = "8:0 rbps=104857600 wbps=104857600"
  └── PID: pids.max = 20(限制进程数)

Layer 2: 命名空间隔离
  ├── PID namespace:  VMM 看到自己是 PID 1
  ├── Mount namespace: 独立的文件系统视图
  ├── Network namespace: 仅有 tap 设备
  ├── UTS namespace: 独立 hostname
  └── IPC namespace: 隔离 System V IPC

Layer 3: chroot(文件系统隔离)
  /srv/jailer/firecracker/<id>/root/
  ├── firecracker  (硬链接到二进制)
  ├── dev/
  │   ├── net/tun  (mknod)
  │   ├── kvm      (mknod)
  │   └── urandom  (mknod)
  ├── run/         (API socket)
  └── drives/      (只读 rootfs, 读写 scratch)

Layer 4: seccomp-BPF(系统调用过滤)
  允许列表约 35 个 syscall(从 ~300+ 个中选出)
  关键禁止:mount, umount2, ptrace, kexec_*, io_uring_*

Layer 5: 权限降级
  UID/GID: 非 root(jailer 配置的专用用户)
  Capabilities: 全部 drop
  No new privileges: PR_SET_NO_NEW_PRIVS = 1

BH.2 Jailer 启动流程详解

时序图:jailer 启动 VMM 的完整流程

1. [Root] 解析命令行参数
   jailer --id <vm-id> \
          --exec-file /usr/bin/firecracker \
          --uid 1000 --gid 1000 \
          --chroot-base-dir /srv/jailer \
          --cgroup-version 2 \
          --resource-limit "no-file=2048" \
          --netns /var/run/netns/fc-<id>

2. [Root] 创建 chroot 目录结构
   mkdir -p /srv/jailer/firecracker/<id>/root/
   硬链接 firecracker 二进制到 chroot 内
   (硬链接而非复制:节省空间 + 确保同版本)

3. [Root] 创建设备节点
   mknod /root/dev/net/tun c 10 200    # TAP 设备
   mknod /root/dev/kvm c 10 232        # KVM 设备
   mknod /root/dev/urandom c 1 9       # 随机数
   chown 1000:1000 这些设备节点

4. [Root] 配置 cgroup
   创建 /sys/fs/cgroup/firecracker/<id>/
   写入资源限制
   将当前进程加入 cgroup

5. [Root] 加入网络命名空间
   setns(open("/var/run/netns/fc-<id>"), CLONE_NEWNET)

6. [Root] 创建其他命名空间
   unshare(CLONE_NEWPID | CLONE_NEWNS | CLONE_NEWIPC | CLONE_NEWUTS)

7. [Root] chroot + chdir
   chroot("/srv/jailer/firecracker/<id>/root")
   chdir("/")

8. [Root → Non-root] 权限降级
   设置 RLIMIT_NOFILE = 2048
   setgroups([])          # 清空附加组
   setgid(1000)           # 切换 GID
   setuid(1000)           # 切换 UID(不可逆)
   prctl(PR_SET_NO_NEW_PRIVS, 1)  # 禁止提权

9. [Non-root] 加载 seccomp 过滤器
   加载预编译的 BPF 程序
   (在 exec 之前加载,确保 VMM 启动即受限)

10. [Non-root] exec firecracker
    execve("./firecracker", args, env)
    VMM 进程替换 jailer 进程
    此后 VMM 在所有隔离层内运行

BH.3 Seccomp 过滤器设计

Firecracker 的 seccomp 过滤器是精心设计的白名单:

Firecracker seccomp 允许列表分类:

基本操作(必须有的):
  read, write, close, fstat, mmap, mprotect, munmap,
  brk, sigaltstack, rt_sigaction, rt_sigprocmask, rt_sigreturn,
  exit, exit_group, clock_gettime, futex

KVM 操作:
  ioctl(仅限特定 KVM 命令):
    KVM_RUN, KVM_GET_REGS, KVM_SET_REGS,
    KVM_GET_SREGS, KVM_SET_SREGS,
    KVM_CREATE_VCPU, KVM_SET_USER_MEMORY_REGION,
    KVM_IRQFD, KVM_IOEVENTFD, ...
  (通过 ioctl 的 arg2 进一步过滤命令号)

文件操作(极度受限):
  openat(仅限已有文件描述符目录)
  lseek, pread64, pwrite64
  (不允许 open, creat, mkdir, rmdir, unlink)

网络操作:
  accept4(API socket)
  recvfrom, sendto(TAP 设备读写)
  epoll_create1, epoll_ctl, epoll_wait
  eventfd2, timerfd_create, timerfd_settime

禁止的高危 syscall:
  mount, umount2           → 防止修改文件系统
  ptrace                   → 防止调试注入
  clone(带 CLONE_NEWUSER)→ 防止创建新用户命名空间
  io_uring_setup/enter     → 攻击面太大
  bpf                      → 防止加载 BPF 程序
  kexec_load               → 防止替换内核
  pivot_root               → 防止切换根目录
  setns                    → 防止加入其他命名空间

条件过滤示例(ioctl 的参数过滤):
  if (syscall == ioctl) {
      if (arg1 == kvm_fd) {
          if (arg2 in KVM_ALLOWED_CMDS) → ALLOW
          else → ERRNO(EPERM)
      }
      if (arg1 == tap_fd) {
          if (arg2 in {TUNSETIFF, TUNGETIFF}) → ALLOW
          else → ERRNO(EPERM)
      }
      → ERRNO(EPERM)  // 其他 fd 的 ioctl 一律禁止
  }

BH.4 Jailer 的生产实践模式

模式一:预热 Jailer 池

问题:Jailer 启动涉及 mkdir, mknod, cgroup 操作,有一定延迟
解决:预先创建 N 个 Jailer 环境,按需分配

预热流程:
  Boot time:
    for i in 1..N:
      jailer --id pool-{i} ... --daemonize
      # Jailer 创建完隔离环境后暂停在 exec 前
      # 等待信号再真正启动 VMM

  Request time:
    取一个 pool-{i}
    发送信号 → exec firecracker
    配置 VM(通过 API socket)
    启动 Guest

模式二:Jailer + 快照恢复

冷启动:jailer → exec firecracker → 配置 VM → boot Guest → ready
          总计 ~150ms

快照恢复:jailer → exec firecracker → 加载快照 → ready
          总计 ~20ms

快照文件布局(chroot 内):
  /drives/rootfs.ext4     → 只读 rootfs
  /drives/scratch.ext4    → 读写 scratch
  /snapshot/vmstate       → vCPU + 设备状态
  /snapshot/mem           → Guest 内存快照

恢复 API 调用序列:
  PUT /snapshot/load {
    "snapshot_path": "./snapshot/vmstate",
    "mem_backend": {
      "backend_path": "./snapshot/mem",
      "backend_type": "File"
    },
    "enable_diff_snapshots": true,
    "resume_vm": true
  }

模式三:多级隔离叠加

当单一 Jailer 不够时:
  Kubernetes Pod(cgroup + namespace)
    └── Jailer(再一层 cgroup + namespace + chroot + seccomp)
        └── Firecracker VMM
            └── Guest kernel + 用户进程

效果:4 层隔离嵌套
  攻击者需要:
    1. 利用 Guest 内核漏洞逃逸 VM
    2. 利用 VMM 漏洞(极少的 syscall 可用)
    3. 绕过 seccomp + chroot
    4. 逃逸 Kubernetes Pod
  每层都是独立的安全边界

附录 BI:沙箱 API 设计模式

BI.1 API 设计的核心约束

AI 代码沙箱的 API 设计面临独特的约束集:客户端是 LLM agent(非人类开发者),需要极低延迟(agent 等待 = token 浪费),且操作模式高度模式化(创建 → 执行 → 获取结果 → 销毁)。

设计约束矩阵:

约束 1:Agent 友好性
  - 响应必须结构化(JSON),不能有歧义
  - 错误消息必须可操作(agent 能据此重试/修复)
  - 状态机清晰(agent 能预测下一步操作)

约束 2:低延迟
  - 创建沙箱 < 100ms(热池)
  - 执行代码 < 5s(含冷启动)
  - 获取结果 < 10ms

约束 3:安全性
  - 不能通过 API 泄露宿主信息
  - 不能通过 API 操作影响其他沙箱
  - 资源使用必须有上限

约束 4:幂等性
  - 网络不稳定时 agent 可能重试
  - 相同请求重复发送不应产生副作用
  - 使用 idempotency key 机制

约束 5:可观测性
  - 执行过程必须可追踪
  - 资源使用可查询
  - 异常原因可诊断

BI.2 RESTful API 设计

核心资源模型:

/sandboxes                    → 沙箱集合
/sandboxes/{id}               → 单个沙箱
/sandboxes/{id}/executions    → 执行历史
/sandboxes/{id}/executions/{eid} → 单次执行
/sandboxes/{id}/files         → 文件系统
/sandboxes/{id}/files/{path}  → 单个文件

生命周期 API:

POST /sandboxes
  Request:
    {
      "template": "python3.11",
      "timeout_seconds": 300,
      "memory_mb": 256,
      "cpu_millicores": 500,
      "network": {"enabled": false},
      "metadata": {"agent_id": "agent-xyz", "task": "code-gen"}
    }
  Response (201 Created):
    {
      "id": "sbx_a1b2c3d4",
      "status": "ready",
      "created_at": "2024-01-15T10:30:00Z",
      "expires_at": "2024-01-15T10:35:00Z",
      "endpoints": {
        "execute": "/sandboxes/sbx_a1b2c3d4/executions",
        "files": "/sandboxes/sbx_a1b2c3d4/files",
        "ws": "wss://api.example.com/sandboxes/sbx_a1b2c3d4/terminal"
      }
    }

POST /sandboxes/{id}/executions
  Request:
    {
      "language": "python",
      "code": "print('hello')\nimport sys\nprint(sys.version)",
      "timeout_seconds": 30,
      "stdin": "",
      "env": {"MY_VAR": "value"}
    }
  Response (200 OK):
    {
      "execution_id": "exec_x9y8z7",
      "status": "completed",
      "exit_code": 0,
      "stdout": "hello\n3.11.7 (main, Dec 10 2024) [GCC 12.2.0]\n",
      "stderr": "",
      "duration_ms": 45,
      "resource_usage": {
        "peak_memory_mb": 12,
        "cpu_time_ms": 38,
        "wall_time_ms": 45
      }
    }

错误响应设计(Agent 可操作性优先):
  {
    "error": {
      "code": "EXECUTION_TIMEOUT",
      "message": "Code execution exceeded 30s timeout",
      "details": {
        "timeout_seconds": 30,
        "elapsed_seconds": 30.01,
        "partial_stdout": "Processing item 9999/10000...\n",
        "suggestion": "Increase timeout or optimize the loop"
      },
      "retryable": true,
      "retry_after_seconds": 0
    }
  }

BI.3 WebSocket 实时通信

对于交互式场景(如 REPL、长时间运行的任务),WebSocket 提供实时双向通信:

WebSocket 协议设计:

连接:wss://api.example.com/sandboxes/{id}/terminal

客户端 → 服务器消息:
  {"type": "input", "data": "print('hello')\n"}
  {"type": "resize", "cols": 80, "rows": 24}
  {"type": "signal", "signal": "SIGINT"}
  {"type": "ping", "timestamp": 1705312200000}

服务器 → 客户端消息:
  {"type": "output", "stream": "stdout", "data": "hello\n"}
  {"type": "output", "stream": "stderr", "data": "Error: ...\n"}
  {"type": "state", "status": "running", "pid": 42}
  {"type": "exit", "code": 0, "signal": null}
  {"type": "pong", "timestamp": 1705312200000, "server_time": 1705312200005}

流式执行(用于长任务):
  客户端发送:
    {"type": "execute", "id": "req-1", "code": "for i in range(100): print(i)", "stream": true}

  服务器流式返回:
    {"type": "stream", "id": "req-1", "stream": "stdout", "data": "0\n1\n2\n"}
    {"type": "stream", "id": "req-1", "stream": "stdout", "data": "3\n4\n5\n"}
    ...
    {"type": "complete", "id": "req-1", "exit_code": 0, "duration_ms": 2340}

心跳与超时:
  客户端每 30s 发送 ping
  服务器 60s 未收到 ping → 断开连接
  断开后沙箱保持 alive 5min → 允许重连
  重连后通过 sequence number 恢复状态

BI.4 SDK 设计模式

为不同的 Agent 框架提供 SDK:

# Python SDK 设计示例

class Sandbox:
    """AI Agent 友好的沙箱 SDK"""

    @classmethod
    async def create(
        cls,
        template: str = "python3.11",
        timeout: int = 300,
        memory_mb: int = 256,
        on_stdout: Callable[[str], None] | None = None,
    ) -> "Sandbox":
        """创建新沙箱,返回 ready 状态的实例"""
        ...

    async def run(
        self,
        code: str,
        *,
        timeout: int = 30,
        env: dict[str, str] | None = None,
    ) -> ExecutionResult:
        """执行代码并等待完成"""
        ...

    async def run_stream(
        self,
        code: str,
        *,
        timeout: int = 30,
    ) -> AsyncIterator[StreamEvent]:
        """流式执行,逐步返回输出"""
        ...

    async def upload(self, path: str, content: bytes) -> None:
        """上传文件到沙箱"""
        ...

    async def download(self, path: str) -> bytes:
        """从沙箱下载文件"""
        ...

    async def snapshot(self) -> str:
        """创建快照,返回快照 ID"""
        ...

    @classmethod
    async def restore(cls, snapshot_id: str) -> "Sandbox":
        """从快照恢复沙箱"""
        ...

    async def close(self) -> None:
        """销毁沙箱,释放资源"""
        ...

    async def __aenter__(self) -> "Sandbox":
        return self

    async def __aexit__(self, *exc) -> None:
        await self.close()


# 使用示例(Agent 集成)
async def agent_tool_execute_code(code: str) -> str:
    """LLM Agent 的代码执行工具"""
    async with await Sandbox.create(timeout=60) as sbx:
        result = await sbx.run(code, timeout=30)

        if result.exit_code == 0:
            return f"Output:\n{result.stdout}"
        else:
            return f"Error (exit code {result.exit_code}):\n{result.stderr}"

BI.5 API 版本管理与兼容性

版本策略:

URL 路径版本化:/v1/sandboxes, /v2/sandboxes
  优点:清晰、缓存友好
  缺点:大版本升级需要迁移所有客户端

Header 版本化:API-Version: 2024-01-15
  优点:渐进式演进,单一 URL
  缺点:客户端容易忘记设置

推荐:URL 主版本 + Header 日期版本
  /v1/sandboxes + API-Version: 2024-01-15
  主版本:破坏性变更(新字段必填、删除端点)
  日期版本:非破坏性变更(新可选字段、新端点)

向后兼容规则:
  ✓ 添加新的可选字段
  ✓ 添加新的端点
  ✓ 添加新的枚举值(客户端必须容忍未知值)
  ✗ 删除字段(先 deprecate 3 个月)
  ✗ 修改字段类型
  ✗ 修改必填/可选属性

Deprecation 流程:
  1. 添加 Sunset header: Sunset: 2024-04-15
  2. 添加 Deprecation header: Deprecation: true
  3. 日志记录仍在使用 deprecated 端点的客户端
  4. 到期后返回 410 Gone(而非直接删除)

附录 BJ:Agent-Sandbox 通信协议

BJ.1 协议层次模型

Agent 与 Sandbox 之间的通信不仅仅是 API 调用,还涉及更高层次的协议设计——如何让 LLM agent 高效、安全地与沙箱交互。

协议栈(从底到顶):

Layer 1: 传输层(Transport)
  - HTTP/2 + TLS 1.3(请求-响应)
  - WebSocket(双向流)
  - gRPC(内部服务间)
  选择依据:
    Agent → Gateway: HTTP/2(通用性)
    Gateway → Sandbox Pool: gRPC(性能)
    交互式会话: WebSocket

Layer 2: 消息层(Message)
  - JSON(Agent 原生格式)
  - Protocol Buffers(内部服务间)
  - MessagePack(高频小消息)
  选择依据:
    面向 Agent 的 API: JSON
    服务间通信: Protobuf
    实时流: MessagePack

Layer 3: 会话层(Session)
  - 沙箱生命周期管理
  - 执行上下文维护
  - 状态同步
  关键概念:
    Session = Agent 与特定沙箱的持续交互
    Context = 沙箱内累积的状态(文件、变量、进程)

Layer 4: 语义层(Semantic)
  - Tool calling 协议(OpenAI / Anthropic 格式)
  - 能力协商
  - 结果格式化
  关键设计:
    Tool 定义 → Agent 理解可用操作
    结果格式 → Agent 能解析并推理

BJ.2 Tool Calling 集成

不同 LLM 平台的 tool calling 格式对沙箱 API 的影响:

// OpenAI Function Calling 格式的沙箱工具定义
{
  "type": "function",
  "function": {
    "name": "execute_code",
    "description": "Execute code in an isolated sandbox environment. The sandbox has Python 3.11, common data science libraries (numpy, pandas, matplotlib), and 256MB memory. Network access is disabled. Files persist within the session.",
    "parameters": {
      "type": "object",
      "properties": {
        "code": {
          "type": "string",
          "description": "Python code to execute. Can span multiple lines. Has access to previously defined variables and created files within this session."
        },
        "timeout": {
          "type": "integer",
          "description": "Maximum execution time in seconds (1-60). Default: 30.",
          "default": 30
        }
      },
      "required": ["code"]
    }
  }
}
Agent 决策树(Agent 如何使用沙箱工具):

用户请求: "分析这个 CSV 数据"
  │
  ├─ Agent 判断: 需要执行代码
  │
  ├─ 第一次调用: 上传文件
  │   execute_code("open('/tmp/data.csv','w').write('''...''')")
  │   结果: "File written successfully"
  │
  ├─ 第二次调用: 探索数据
  │   execute_code("import pandas as pd\ndf=pd.read_csv('/tmp/data.csv')\nprint(df.head())\nprint(df.describe())")
  │   结果: 数据预览 + 统计摘要
  │
  ├─ Agent 推理: 根据数据特征选择分析方法
  │
  ├─ 第三次调用: 执行分析
  │   execute_code("import matplotlib.pyplot as plt\n...")
  │   结果: "Plot saved to /tmp/analysis.png"
  │
  └─ 第四次调用: 下载结果
      download_file("/tmp/analysis.png")
      结果: base64 编码的图片

整个过程中,沙箱保持同一个 session:
  - 变量持久化(df 对象跨调用可用)
  - 文件持久化(/tmp/data.csv 持续存在)
  - 环境一致(已导入的库保持加载)

BJ.3 结果格式化策略

Agent 需要结构化的执行结果来进行推理:

结果格式设计原则:

1. 区分输出类型
   {
     "text_output": "数据有 1000 行...",   // 纯文本(直接展示给用户)
     "structured_data": {...},              // 结构化数据(Agent 用于推理)
     "artifacts": [                         // 生成的文件(需要特殊处理)
       {"path": "/tmp/plot.png", "type": "image/png", "size": 45678}
     ]
   }

2. 截断策略(防止超出 context window)
   stdout 超过 10KB:
     前 4KB + "\n... [truncated 23KB] ...\n" + 后 1KB
   建议 Agent:
     "Output truncated. Use `tail -n 50 /tmp/output.log` to see the end."

3. 错误富化(帮助 Agent 自动修复)
   {
     "error_type": "ImportError",
     "error_message": "No module named 'sklearn'",
     "suggestion": "Install with: !pip install scikit-learn",
     "auto_fixable": true,
     "fix_code": "import subprocess; subprocess.run(['pip','install','scikit-learn'])"
   }

4. 资源使用报告(帮助 Agent 优化策略)
   {
     "peak_memory_mb": 180,
     "memory_limit_mb": 256,
     "memory_pressure": "high",  // Agent 据此决定是否分批处理
     "cpu_time_ms": 4500,
     "timeout_ms": 30000,
     "time_pressure": "low"
   }

BJ.4 会话管理与状态恢复

会话生命周期:

创建会话:
  Agent → POST /sessions
  返回 session_id + sandbox_id
  沙箱状态:空白(或从模板初始化)

正常使用:
  Agent → POST /sessions/{id}/execute
  每次执行累积状态(变量、文件、进程)

会话暂停:
  Agent 长时间无操作 → 自动快照 + 释放资源
  Agent → GET /sessions/{id}/status
  返回 {"status": "suspended", "snapshot_id": "snap-xxx"}

会话恢复:
  Agent → POST /sessions/{id}/resume
  从快照恢复 → 所有状态完整恢复
  延迟:~50ms(内存快照恢复)

会话分支:
  Agent 想要"回到之前的状态":
  Agent → POST /sessions/{id}/branch?from_execution=exec-5
  创建新 session,状态回到第 5 次执行之后
  原 session 不受影响

并发会话:
  单个 Agent 可持有多个并行 session:
    Session A: 数据分析环境(pandas, numpy loaded)
    Session B: Web 开发环境(Flask running)
    Session C: 测试环境(pytest 配置好)
  Agent 根据任务类型选择合适的 session

附录 BK:沙箱测试策略

BK.1 测试金字塔

沙箱系统的测试金字塔:

                 /\
                /  \      E2E 测试
               /    \     (Agent → API → Sandbox → 结果验证)
              /------\    ~10 个关键路径
             /        \
            /          \   集成测试
           /            \  (API → Sandbox Pool → 隔离验证)
          /--------------\ ~100 个场景
         /                \
        /                  \  单元测试
       /                    \ (各组件独立逻辑)
      /----------------------\ ~1000+ 个用例
     /                        \
    /                          \ 静态分析 + Fuzzing
   /____________________________ \ (持续运行)

各层次职责:

单元测试:
  - Seccomp 过滤器正确性(模拟 syscall,验证 allow/deny)
  - 资源限制计算逻辑
  - 快照序列化/反序列化
  - API 请求验证
  - 状态机转换

集成测试:
  - 沙箱创建 → 执行 → 销毁完整流程
  - 资源隔离验证(内存、CPU、磁盘)
  - 网络隔离验证(无法访问外部/其他沙箱)
  - 文件系统隔离验证
  - 并发创建/销毁稳定性

E2E 测试:
  - 模拟 Agent 完整工作流
  - 性能基准(延迟 P50/P99)
  - 长时间运行稳定性
  - 故障恢复验证

BK.2 安全测试方法

安全测试矩阵:

测试类别 1:容器逃逸尝试

test_chroot_escape:
  代码: os.chdir("../" * 100); os.listdir("/")
  期望: 仍在沙箱 rootfs 内

test_mount_escape:
  代码: mount("/dev/sda1", "/mnt", "ext4", 0, "")
  期望: EPERM 或 ENOSYS

test_namespace_escape:
  代码: setns(open("/proc/1/ns/mnt"), CLONE_NEWNS)
  期望: 无法打开 /proc/1/ns/*

test_symlink_escape:
  代码: symlink("/etc/shadow", "/tmp/link"); read("/tmp/link")
  期望: 读取失败或读到沙箱内的 /etc/shadow

测试类别 2:资源耗尽攻击

test_fork_bomb:
  代码: :(){ :|:& };:
  期望: 被 PID cgroup 限制,不影响宿主

test_memory_bomb:
  代码: a = []; while True: a.append('x' * 10**6)
  期望: OOM killed,沙箱清理完成

test_disk_bomb:
  代码: with open('/tmp/big','wb') as f: f.write(b'0'*10**9)
  期望: 磁盘配额限制,写入失败

test_cpu_spin:
  代码: while True: pass
  期望: 被 timeout 杀死,CPU 时间不超限

测试类别 3:信息泄露

test_read_host_files:
  代码: open("/etc/hostname").read()
  期望: 读到沙箱自己的 hostname,非宿主

test_proc_info_leak:
  代码: os.listdir("/proc")
  期望: 只看到沙箱内的进程

test_env_leak:
  代码: os.environ
  期望: 不含宿主的 AWS_SECRET_KEY 等敏感变量

test_timing_sidechannel:
  代码: 测量特定操作的时间来推断信息
  期望: 时钟粒度足够粗或不稳定

测试类别 4:网络安全

test_network_disabled:
  代码: socket.connect(("8.8.8.8", 53))
  期望: 连接失败

test_access_other_sandbox:
  代码: socket.connect(("<other_sandbox_ip>", 8080))
  期望: 连接失败

test_access_metadata_service:
  代码: requests.get("http://169.254.169.254/latest/meta-data/")
  期望: 连接失败(防止云元数据泄露)

BK.3 性能测试方法论

性能测试指标与方法:

指标 1:冷启动延迟
  定义:从 API 收到请求到沙箱 ready 的时间
  测量方法:
    t0 = 发送 POST /sandboxes
    t1 = 收到 {"status": "ready"}
    latency = t1 - t0

  基准要求:
    P50 < 50ms(热池命中)
    P95 < 200ms(需要创建新沙箱)
    P99 < 500ms(包含镜像拉取)

  测试场景:
    - 热池充足:连续 100 次请求
    - 热池耗尽:并发 1000 次请求
    - 冷启动:不同模板的首次创建

指标 2:执行延迟(overhead)
  定义:沙箱执行 vs 原生执行的额外开销
  测量方法:
    benchmark_code = "import time; t=time.time(); sum(range(10**7)); print(time.time()-t)"
    native_time = 直接在宿主运行
    sandbox_time = 在沙箱内运行
    overhead = sandbox_time - native_time

  基准要求:
    CPU 密集型:< 5% overhead
    I/O 密集型:< 15% overhead
    内存密集型:< 10% overhead

指标 3:并发密度
  定义:单机可同时运行的沙箱数
  测量方法:
    逐步增加并发沙箱数,每个执行轻量任务
    监控:延迟、OOM 事件、CPU 竞争

  基准目标:
    64GB RAM 机器:
      MicroVM(128MB each): ~400 个并发沙箱
      Container(64MB each): ~800 个并发沙箱
      WASM(16MB each): ~3000 个并发沙箱

指标 4:吞吐量
  定义:单位时间内完成的执行次数
  测量方法:
    持续发送执行请求,测量稳态 QPS
    负载生成器使用 wrk2 或自定义工具

  基准目标:
    单机:> 500 executions/second(短任务)
    集群(10 节点):> 4000 executions/second

BK.4 混沌工程

混沌实验设计:

实验 1:OOM Killer 行为验证
  注入:强制某沙箱内存超限
  验证:
    - OOM killer 只杀沙箱内进程
    - 不影响同机其他沙箱
    - 管理面正确检测并清理
    - API 返回合适的错误码

实验 2:磁盘满
  注入:填满沙箱使用的磁盘分区
  验证:
    - 新创建的沙箱正确报错
    - 已运行沙箱的写操作返回 ENOSPC
    - 清理机制自动回收过期沙箱的磁盘

实验 3:网络分区
  注入:iptables 断开管理面与沙箱的网络
  验证:
    - 沙箱继续运行(不依赖管理面心跳运行)
    - 管理面标记沙箱为 unknown 状态
    - 网络恢复后状态同步

实验 4:时钟偏移
  注入:沙箱内时钟快进/回退
  验证:
    - 超时机制使用单调时钟(不受 wall clock 影响)
    - 证书验证不受影响(或正确失败)
    - 日志时间戳可追溯

实验 5:内核崩溃(microVM 场景)
  注入:Guest 内核 panic
  验证:
    - VMM 正确检测 Guest 无响应
    - 资源被回收(内存、fd、网络端口)
    - 管理面收到通知并更新状态
    - 如果开启了自动重启,新实例正确启动

附录 BL:沙箱成本建模

BL.1 成本构成分析

单次沙箱执行的全成本分解:

固定成本(与执行时间无关):
  ┌─────────────────────┬──────────┬─────────────────────┐
  │ 成本项              │ 估算     │ 说明                │
  ├─────────────────────┼──────────┼─────────────────────┤
  │ API Gateway 转发    │ $0.0001  │ 请求处理 + 路由      │
  │ 沙箱创建(热池)    │ $0.0002  │ 分配 + 配置         │
  │ 结果存储            │ $0.0001  │ S3 写入             │
  │ 日志记录            │ $0.0001  │ 结构化日志写入       │
  └─────────────────────┴──────────┴─────────────────────┘
  固定成本合计:~$0.0005/次

可变成本(与资源使用成正比):
  ┌─────────────────────┬──────────────────────────────────┐
  │ 成本项              │ 计算公式                         │
  ├─────────────────────┼──────────────────────────────────┤
  │ 内存                │ (MB * 秒) / (1024*3600) * $0.05  │
  │ CPU                 │ (vCPU * 秒) / 3600 * $0.03      │
  │ 存储 I/O            │ IOPS * $0.000001                 │
  │ 网络(如启用)      │ GB * $0.01                       │
  └─────────────────────┴──────────────────────────────────┘

示例计算:
  典型 AI agent 代码执行:
    内存 256MB × 5秒 = 1280 MB·s = $0.000017
    CPU 0.5vCPU × 5秒 = 2.5 vCPU·s = $0.000021
    存储:100 IOPS = $0.0001
    固定成本:$0.0005
    ─────────────────────
    总计:~$0.0007/次

  一个 Agent 会话(平均 10 次执行):~$0.007
  每月 100 万次执行:~$700

BL.2 热池成本优化

热池大小优化问题:

输入参数:
  λ = 到达率(请求/秒),假设 100 req/s
  μ = 服务率(1/平均使用时间),假设 0.2/s(平均 5s)
  C_idle = 空闲沙箱成本/秒(内存占用),$0.000003/s
  C_cold = 冷启动惩罚(额外延迟的等价成本),$0.001/次
  SLA = 冷启动率 < 1%

使用 M/M/c 排队模型:
  流量强度:ρ = λ/(c*μ)
  需要:ρ < 1(稳态条件)
  最小 c = λ/μ = 100/0.2 = 500 个并发沙箱

考虑突发(峰值 3x):
  峰值 λ = 300 req/s
  峰值所需 c = 1500

热池大小选择:
  热池 = 峰值需求 + 安全余量 = 1500 + 300 = 1800
  空闲成本 = (1800 - 500) * $0.000003 * 3600 = $14/小时

  vs 不用热池的冷启动成本:
  如果 10% 请求遇到冷启动 = 100 * 0.1 * $0.001 * 3600 = $36/小时

  结论:热池更划算,且 P99 延迟从 500ms 降到 50ms

自适应热池算法:
  每分钟检查:
    if 最近 5min 使用率 > 80%:
        扩容 20%
    elif 最近 15min 使用率 < 30%:
        缩容 10%(缩容更慢,防止震荡)
    保底:最近 1h 峰值的 50%

BL.3 多租户成本分摊

多租户计费模型:

模型一:按次计费(简单粗暴)
  每次执行 $0.001
  优点:简单、可预测
  缺点:5ms 执行和 30s 执行同价,不公平

模型二:按资源·时间计费(精确公平)
  费用 = Σ(资源类型 × 使用量 × 单价)
  优点:精确反映成本
  缺点:计费复杂,用户难以预估费用

模型三:阶梯包月
  ┌──────────┬────────────┬──────────┬─────────────┐
  │ 套餐     │ 月执行次数 │ 月费     │ 超出单价    │
  ├──────────┼────────────┼──────────┼─────────────┤
  │ Starter  │ 10,000     │ $10      │ $0.002/次   │
  │ Pro      │ 100,000    │ $80      │ $0.001/次   │
  │ Business │ 1,000,000  │ $600     │ $0.0008/次  │
  │ Custom   │ 无限       │ 议价     │ -           │
  └──────────┴────────────┴──────────┴─────────────┘
  优点:可预测 + 批量折扣
  缺点:需要用户预估用量

模型四:Token 与执行绑定
  将沙箱执行成本直接计入 LLM token 消耗:
    1 次代码执行 ≈ 500 output tokens 等价
  优点:用户只关注一个计费维度
  缺点:底层成本波动难以传导

附录 BM:术语表与快速索引

BM.1 核心术语表

┌──────────────────────┬──────────────────────────────────────────────────┐
│ 术语                 │ 定义                                             │
├──────────────────────┼──────────────────────────────────────────────────┤
│ Sandbox(沙箱)      │ 隔离的代码执行环境,限制程序对系统资源的访问        │
│ VMM (Virtual Machine │ 管理虚拟机的软件,拦截特权操作并模拟硬件            │
│   Monitor)           │                                                  │
│ Hypervisor           │ VMM 的同义词,分 Type-1(裸机)和 Type-2(宿主)  │
│ microVM              │ 极简虚拟机,启动快、占用少,专为短生命周期设计      │
│ Container(容器)    │ 利用 Linux namespace + cgroup 的进程隔离方案       │
│ KVM                  │ Linux 内核虚拟化模块,将内核变为 Type-1 hypervisor │
│ VMCS                 │ Virtual Machine Control Structure,Intel 硬件虚   │
│                      │ 拟化的核心数据结构                                │
│ EPT                  │ Extended Page Table,硬件辅助的二级地址翻译        │
│ VM Entry             │ 从 Host 模式切换到 Guest 模式                     │
│ VM Exit              │ 从 Guest 模式切换回 Host 模式(触发 VMM 处理)    │
│ VirtIO               │ 半虚拟化 I/O 框架,Guest 知道自己在虚拟环境中     │
│ vring                │ VirtIO 的环形缓冲区,Host/Guest 共享内存通信      │
│ vsock                │ 虚拟 socket,VM 与 Host 之间的高效通信通道        │
│ virtio-fs            │ 基于 FUSE 的虚拟文件系统共享方案                  │
│ 9P/9pfs              │ Plan 9 网络文件协议,用于 VM 文件共享             │
│ CoW (Copy-on-Write)  │ 写时复制,延迟实际复制直到修改发生                │
│ OverlayFS            │ 联合文件系统,支持只读层 + 可写层叠加             │
│ Namespace            │ Linux 内核资源隔离机制(PID/NET/MNT/UTS/IPC/USER)│
│ cgroup               │ Linux 内核资源限制机制(CPU/Memory/IO/PID)       │
│ seccomp-BPF          │ 系统调用过滤机制,使用 BPF 程序定义允许/拒绝规则  │
│ Capabilities         │ Linux 细粒度权限模型,将 root 权限拆分为 ~40 项   │
│ OCI                  │ Open Container Initiative,容器镜像和运行时标准   │
│ CRI                  │ Container Runtime Interface,K8s 与运行时的接口   │
│ gVisor               │ Google 的用户态内核,在应用和 Host 内核间做拦截    │
│ Sentry               │ gVisor 的用户态内核组件,实现 Linux syscall 语义  │
│ Gofer                │ gVisor 的文件系统代理进程                         │
│ Kata Containers      │ 基于轻量 VM 的容器运行时,VM 级隔离 + 容器接口   │
│ Firecracker          │ AWS 开发的 microVM VMM,为 Lambda/Fargate 设计   │
│ Cloud Hypervisor     │ Intel 主导的 Rust microVM VMM                    │
│ QEMU                 │ 功能完整的通用模拟器/VMM                          │
│ crosvm               │ Chrome OS 的轻量 VMM(Rust 实现)                │
│ io_uring             │ Linux 异步 I/O 框架,通过共享环形缓冲区减少 syscall│
│ eBPF                 │ Extended BPF,内核中的可编程数据平面               │
│ MMIO                 │ Memory-Mapped I/O,通过内存地址访问设备寄存器      │
│ PIO                  │ Port I/O,通过 IN/OUT 指令访问设备端口            │
│ Balloon Driver       │ 动态内存调节驱动,VMM 可收回/归还 Guest 内存      │
│ KSM                  │ Kernel Same-page Merging,合并相同内存页           │
│ NUMA                 │ Non-Uniform Memory Access,多路 CPU 的内存拓扑    │
│ Buddy Allocator      │ 伙伴系统,Linux 物理页分配算法                    │
│ Slab Allocator       │ 对象缓存分配器,减少 buddy 系统碎片               │
│ TLB                  │ Translation Lookaside Buffer,地址翻译缓存        │
│ IOMMU                │ I/O 内存管理单元,设备 DMA 的地址隔离             │
│ SR-IOV               │ 单根 I/O 虚拟化,硬件级网卡分片                   │
│ DPDK                 │ 用户态网络驱动框架,绕过内核协议栈                 │
│ CNI                  │ Container Network Interface,K8s 网络插件标准     │
│ MIG                  │ Multi-Instance GPU,NVIDIA 硬件级 GPU 分片        │
│ vGPU                 │ 虚拟 GPU,时分复用物理 GPU                        │
│ GPU Passthrough      │ 将物理 GPU 直通给 VM 独占使用                     │
│ Unikernel            │ 单一地址空间,应用与库 OS 编译为一体              │
│ WASM (WebAssembly)   │ 字节码格式,平台无关的安全执行沙箱                │
│ WASI                 │ WebAssembly System Interface,WASM 的系统调用     │
│ TEE                  │ Trusted Execution Environment,硬件可信执行环境   │
│ SEV                  │ AMD 安全加密虚拟化,加密 VM 内存                  │
│ TDX                  │ Intel 信任域扩展,硬件级 VM 隔离                  │
│ Confidential VM      │ 运行在 TEE 中的虚拟机,Host 不可读 VM 内存       │
│ Snapshot             │ 虚拟机/沙箱的完整状态捕获(内存 + 设备 + CPU)    │
│ Live Migration       │ 在线迁移 VM 到另一台物理机,不中断服务            │
│ Dirty Page Tracking  │ 追踪被修改的内存页(用于增量快照/迁移)            │
│ Spectre/Meltdown     │ CPU 推测执行侧信道漏洞家族                       │
│ Container Escape     │ 从容器内部获取宿主权限的攻击                      │
│ VM Escape            │ 从 VM 内部攻击 VMM 获取宿主权限                  │
│ Agent                │ 使用 LLM 的自主软件实体,可调用工具完成任务       │
│ Tool Calling         │ LLM 通过结构化输出调用外部函数的机制              │
│ Idempotency          │ 同一请求重复执行产生相同结果的属性                │
└──────────────────────┴──────────────────────────────────────────────────┘

BM.2 附录快速索引

按主题查找附录:

虚拟化基础:
  W    - 硬件虚拟化 (VT-x, VMCS, EPT, VM Entry/Exit)
  X    - microVM 架构 (Firecracker, Cloud Hypervisor, crosvm)
  BH   - Firecracker Jailer 深度解析

内存管理:
  Y    - 内存虚拟化 (EPT, Shadow PT, Balloon, KSM)
  Z    - Copy-on-Write 与快照
  AE   - 内存共享优化 (KSM, DAX, huge pages)

存储与文件系统:
  AA   - 文件系统虚拟化 (OverlayFS, 9P, virtio-fs, QCOW2)
  AQ   - 文件系统性能优化

网络:
  AB   - 网络虚拟化 (namespace, veth, bridge, TAP)
  AC   - eBPF 在沙箱网络中的应用
  AR   - 高性能网络 (DPDK, SR-IOV, XDP)

安全:
  AD   - Syscall 拦截 (seccomp-BPF, gVisor Sentry)
  AF   - 安全漏洞案例 (CVE-2019-5736, Spectre, Dirty Pipe)
  AO   - Linux Capabilities 与最小权限
  AP   - Rust 在 VMM 中的安全优势
  BG   - io_uring 安全问题
  BK   - 安全测试方法

容器与编排:
  AG   - OCI 运行时规范与容器生命周期
  AH   - CRI 与 Kubernetes 集成
  AN   - cgroup v2 资源控制

AI/Agent 特定:
  AI   - Snapshot/Time-travel 用于 RL 训练
  AJ   - 沙箱编排模式 (池化, 流水线, 树形)
  AK   - Agent 多沙箱协作
  BI   - 沙箱 API 设计模式
  BJ   - Agent-Sandbox 通信协议

性能:
  AL   - 性能基准测试方法论
  AM   - 启动延迟优化
  BK   - 性能测试方法

设备与 I/O:
  AT   - VirtIO 设备模型深度
  AU   - vsock 与 virtio-fs 实战
  BG   - io_uring 在沙箱中的应用

GPU:
  AV   - GPU 虚拟化 (passthrough, MIG, API forwarding)

未来技术:
  AW   - Unikernel
  AX   - WebAssembly 沙箱
  AY   - 机密计算 (SEV, TDX, CCA)

架构与设计:
  AZ   - 多租户隔离架构
  BA   - 沙箱生命周期状态机
  BB   - 事件驱动架构
  BC   - 可观测性设计
  BD   - 容灾与高可用
  BE   - 成本优化策略
  BF   - 生产架构决策
  BI   - API 设计模式
  BL   - 成本建模

附录 BN:推荐阅读路径

BN.1 按角色的阅读顺序

角色一:AI 应用开发者(使用沙箱)
  必读:BI(API 设计)→ BJ(通信协议)→ AJ(编排模式)→ AK(多沙箱协作)
  选读:AI(RL 训练)→ BL(成本建模)
  跳过:底层虚拟化细节(W, X, Y 可快速浏览)

角色二:沙箱平台开发者(构建沙箱)
  必读:全部按顺序 W → BN
  重点:W-AD(基础设施层)→ AG-AH(接口层)→ AT-AU(设备层)
       → BA-BF(架构层)→ BG-BH(高级主题)
  实操:每章的代码示例都应跑一遍

角色三:安全工程师(加固沙箱)
  必读:AD → AF → AO → AP → BG → BH → BK
  选读:W(理解攻击面)→ AB-AC(网络安全)→ AY(未来方向)
  实操:BK 的安全测试全部应实施

角色四:研究者(探索新方向)
  必读:AI(RL)→ AW-AY(未来技术)→ AV(GPU)
  选读:根据研究方向深入特定章节
  建议:每章末尾的参考论文是进一步阅读的起点

角色五:运维/SRE(运营沙箱集群)
  必读:AN(cgroup)→ BC(可观测性)→ BD(容灾)→ BE-BL(成本)
  选读:AM(启动优化)→ AZ(多租户)→ BK(测试)
  实操:BC 的监控指标应纳入 dashboard

BN.2 知识依赖图

知识前置关系(箭头表示"需要先理解"):

Linux 基础
  ├→ Namespace (AB)
  ├→ cgroup (AN)
  ├→ seccomp (AD)
  └→ Capabilities (AO)

计算机体系结构
  ├→ VT-x/VMCS (W)
  │    ├→ EPT (W, Y)
  │    ├→ VM Entry/Exit (W)
  │    └→ microVM (X)
  │         ├→ Firecracker (X, BH)
  │         └→ VirtIO (AT)
  │              ├→ vsock (AU)
  │              └→ virtio-fs (AU)
  ├→ 内存管理 (Y)
  │    └→ CoW/快照 (Z)
  │         └→ RL 训练 (AI)
  └→ IOMMU/SR-IOV (AR, AV)

容器技术
  ├→ OCI 标准 (AG)
  │    └→ CRI/K8s (AH)
  ├→ OverlayFS (AA)
  └→ gVisor/Kata (AD, AG)

网络
  ├→ namespace + veth (AB)
  ├→ eBPF (AC)
  └→ CNI (AB)

编程(Rust)
  └→ Rust VMM (AP)
       ├→ rust-vmm crates
       └→ 安全优势

AI/ML
  ├→ Agent 概念
  │    ├→ Tool Calling (BJ)
  │    └→ 编排 (AJ, AK)
  └→ RL 训练 (AI)
       └→ 快照/分支 (Z)

BN.3 实践项目建议

项目难度递进:

Level 1:体验者(1-2 天)
  项目 A:用 Firecracker 启动一个 microVM
    - 安装 Firecracker
    - 准备 kernel + rootfs
    - 通过 API 配置并启动
    - 在 Guest 中执行命令

  项目 B:用 Docker + seccomp 搭建简易沙箱
    - 编写自定义 seccomp profile
    - 限制容器的系统调用
    - 验证安全边界

Level 2:构建者(1-2 周)
  项目 C:实现简易的代码执行 API
    - HTTP API(FastAPI/Axum)
    - Docker/nsjail 作为隔离后端
    - 支持 Python/JS 代码执行
    - 实现超时和资源限制

  项目 D:实现沙箱快照/恢复
    - 使用 CRIU 或 Firecracker snapshot
    - 实现 fork-style 的沙箱分支
    - 测量恢复延迟

Level 3:优化者(1 个月)
  项目 E:构建高性能沙箱池
    - 热池管理(预创建 + 自适应伸缩)
    - CoW 优化(共享基础镜像层)
    - 性能基准测试
    - 与 LLM agent 集成

  项目 F:安全加固 + 混沌测试
    - 多层隔离叠加
    - 编写安全测试套件
    - 实施混沌实验
    - 漏洞修复验证

Level 4:研究者(持续)
  项目 G:基于快照的 RL 环境
    - 实现环境分支/回滚
    - 状态空间探索树
    - 性能优化(增量快照)

  项目 H:WASM/Unikernel 沙箱原型
    - 评估 Wasmtime/WasmEdge
    - 与传统沙箱对比性能
    - 探索适用场景

  项目 I:机密计算沙箱
    - 使用 AMD SEV 或 Intel TDX
    - 实现 attestation 流程
    - 评估性能影响

End of Complete Reading Guide (Appendices W-BN). Total coverage: virtualization fundamentals → security → performance → architecture → API design → testing → cost modeling → learning paths. Target audience: AI sandbox researchers and platform builders.