十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

deer-flow:基于虚拟内存保护的确定性执行沙盒

deer-flow:基于虚拟内存保护的确定性执行沙盒 1. “deer-flow”不是框架是内存沙盒的命名隐喻第一次在 GitHub 上看到deer-flow这个仓库名时我下意识点开 README —— 没有文档没有安装说明连一行npm install都没写。只有一行注释式标题“A memory-constrained sandbox for deterministic execution”。当时我正被一个 Node.js 服务反复崩溃折磨得焦头烂额错误日志里反复出现process exited with code 3221225477Windows 下经典的0xc0000005内存访问违例而eclipse mat分析堆转储后却只显示“无明显泄漏”堆大小稳定在 80MBGC 频率正常。直觉告诉我问题不在堆而在更底层虚拟内存布局、页保护、非法指针解引用——也就是操作系统级的内存访问控制失效。后来翻到项目源码根目录下的mem.c第 776 行赫然写着mem_virtual_alloc0: fatal error: out of memory。这不是 Java 的OutOfMemoryError也不是 V8 的堆溢出提示而是直接调用VirtualAlloc失败后抛出的原生错误。那一刻我才真正理解deer-flow这个名字的用意Deer鹿象征警觉、轻盈、对环境变化的敏感响应Flow流则指向执行路径的确定性与可控性。它不试图模拟一个完整的运行时比如 Node.js 或 Python 解释器而是构建一个极简的、带内存边界检查的执行流沙盒——像一头在密林中穿行的鹿每一步都踩在可预判的落叶层上绝不踏入腐朽松软的沼泽地带即未映射/受保护的内存页。这和市面上常见的sandbox工具有本质区别。vm2、isolated-vm或Pyodide的沙盒重心在作用域隔离和API 权限控制它们假设底层内存是可信的而deer-flow的沙盒重心在地址空间完整性它假设任何未经显式授权的内存访问都是危险的。它不拦你调用fs.readFile但会拦你用Buffer.from([0x00, 0x01, 0x02]).readInt32BE(0)去读取一个根本没分配的地址。这种设计哲学让它天然适配两类场景一是需要硬实时响应的嵌入式脚本引擎比如工业 PLC 的逻辑块解释器二是对内存安全要求严苛的代码评测系统比如 OJ 平台防malloc溢出攻击。它不是为 Web 开发者写的而是为那些天天和PAGE_GUARD、MEM_COMMIT、PROT_READ打交道的系统程序员准备的。所以如果你搜索deer-flow是为了找一个类似 Express 的 Web 框架或者一个能直接pip install的 Python 库那你会失望。它没有package.json没有setup.py甚至没有main.py。它的核心就是一个 C 语言编写的、约 1200 行的内存管理器外加一个极简的指令解释器支持 12 条基础指令如LOAD,STORE,ADD,JMP。它不处理 HTTP不解析 JSON不连接数据库。它只做一件事确保每一条指令执行时其访问的内存地址都在预先划定的、带保护属性的虚拟页范围内。这种“窄口径、深控制”的设计正是它能在process exited with code 3221225477这类疑难杂症中一击制胜的关键。提示deer-flow的命名逻辑和seccompsecure computing mode、pledgeOpenBSD 系统调用白名单一脉相承——用一个具象生物抽象概念的组合暗示其核心能力在复杂环境中保持对关键资源这里是内存的绝对掌控力。理解这一点是读懂它所有设计选择的前提。2. 为什么0xc0000005错误无法被常规工具捕获process exited with code 3221225477十六进制0xc0000005是 Windows 系统返回的STATUS_ACCESS_VIOLATION错误代码。它意味着进程试图读写一个它无权访问的内存地址。这个错误非常“底层”它发生在 CPU 级别当处理器执行一条mov eax, [ebx]指令而ebx寄存器里的值指向一个未映射的虚拟地址或者一个标记为PAGE_NOACCESS的页时CPU 立即触发一个硬件异常#GP操作系统内核捕获后向进程发送STATUS_ACCESS_VIOLATION最终导致进程终止。整个过程绕过了所有用户态的 JavaScript 引擎、Python 解释器或 JVM 的异常处理机制。这就是为什么eclipse matMemory Analyzer Tool对它束手无策。MAT 分析的是 Java 堆Heap的快照它能看到Object实例、String数组、HashMap的桶链表但它看不到VirtualAlloc分配的保留区Reserved Region、看不到VirtualProtect设置的页保护位、看不到RAX寄存器里那个非法的指针值。同样Node.js 的--inspect或v8.getHeapStatistics()也无能为力因为 V8 的垃圾回收器只管理它自己malloc出来的堆内存而0xc0000005往往源于 V8 之外的 C 插件Native Addon——比如一个用N-API编写的图像处理模块它直接调用libjpeg的jpeg_decompress而后者内部的一个memcpy操作越界了。我曾在一个实际项目中复现过这个经典场景一个基于sharpNode.js 图像处理库的服务在处理一张特定尺寸的 PNG 图片时总是以0xc0000005崩溃。sharp本身是安全的但它的底层依赖libvips在某个版本中存在一个边界计算错误。当图片宽度恰好是 1024 像素高度是 768 像素时libvips的一个内部缓冲区计算会少分配 16 字节导致后续的像素填充操作写到了相邻内存页的末尾。这个页恰好是PAGE_NOACCESS于是0xc0000005就出现了。node --trace-uncaught-exceptions只能告诉你“未捕获异常”straceLinux或Process MonitorWindows能看到VirtualAlloc调用失败但无法精确定位到是哪一行 C 代码出了问题。deer-flow的应对思路非常直接它不尝试去修复libvips的 bug而是从根本上杜绝这种越界访问发生的可能性。它在进程启动时就通过VirtualAlloc预先申请一块固定大小的、连续的虚拟内存比如 64MB然后用VirtualProtect将这块内存的首尾各 4KB 设置为PAGE_NOACCESS即“防护页”。所有用户代码的内存操作都被强制限制在这块“安全岛”之内。如果libvips的 bug 试图写到安全岛之外CPU 立即触发异常deer-flow的异常处理函数SetUnhandledExceptionFilter会立刻捕获并生成一份包含寄存器状态、指令指针、访问地址的详细报告而不是让整个进程静默退出。这相当于给内存地址空间装上了物理围栏和监控摄像头而不是指望每个路过的人即每个第三方库都自觉遵守交通规则。2.1mem.c第 776 行的out of memory真实含义回到.\src\mem.c(776): mem_virtual_alloc0: fatal error: out of memory这条错误。它看起来像内存耗尽但其实是个精妙的误导。mem_virtual_alloc0函数的职责不是分配堆内存而是为沙盒内的“虚拟机”分配一块受控的虚拟地址空间。它的核心逻辑是// mem.c 伪代码 void* mem_virtual_alloc0(size_t size) { // 1. 尝试在指定基址附近预留一块虚拟地址空间不提交物理内存 void* base VirtualAlloc(NULL, size, MEM_RESERVE, PAGE_NOACCESS); if (!base) { // 预留失败通常是地址空间碎片化严重或已被其他模块占用 return NULL; // 此处触发 out of memory 错误 } // 2. 将预留空间的中间部分比如去掉首尾各4KB设置为可读写 VirtualProtect((char*)base 4096, size - 8192, PAGE_READWRITE, old_protect); return (char*)base 4096; // 返回安全可用区域的起始地址 }所以这里的out of memory并非物理内存不足而是虚拟地址空间耗尽。32 位 Windows 进程的用户态地址空间只有 2GB默认如果进程中加载了大量 DLL每个 DLL 默认占 64KB 对齐的地址块或者之前进行了大量VirtualAlloc/VirtualFree操作导致地址空间碎片化那么即使物理内存充足VirtualAlloc(NULL, ...)也可能找不到一块足够大的连续空闲地址块来预留。deer-flow在这种情况下选择立即报错并退出而不是降级使用不连续的内存块——因为不连续的内存块会破坏其核心的“确定性执行流”模型让JMP指令的地址计算变得不可预测。2.2 与NODE_OPTIONS--max-old-space-size的本质区别很多开发者遇到内存问题第一反应是加大 Node.js 的堆内存限制NODE_OPTIONS--max-old-space-size4096。这只能解决 V8 堆Heap的OutOfMemoryError对0xc0000005完全无效。V8 堆只是进程虚拟地址空间的一小部分通常几 MB 到几 GB而0xc0000005的根源往往在堆之外的“原生内存”Native Memorylibuv的线程池缓冲区、openssl的 SSL 上下文、sqlite3的 WAL 日志页、甚至是printf的内部格式化缓冲区。这些内存由 C 标准库或操作系统直接管理完全独立于 V8 的 GC。deer-flow的沙盒恰恰是为这片“法外之地”而生。它不关心 V8 堆有多大它只关心所有原生内存分配是否发生在它划定的安全区域内。你可以把deer-flow想象成一个“内存海关”所有进出沙盒的内存访问请求无论是malloc、mmap还是VirtualAlloc都必须经过它的检查和路由。而--max-old-space-size只是给 V8 堆这个“保税区”划了一条更大的红线对海关本身没有任何影响。3.deer-flow的沙盒架构三层内存保护模型deer-flow的核心价值不在于它实现了多么炫酷的指令集而在于它构建了一个清晰、可验证、可复现的三层内存保护模型。这个模型不是理论上的而是每一行代码都落实在mem.c和vm.c中的工程实践。理解这三层是掌握其全部能力的基础。3.1 第一层虚拟地址空间的静态划分Static Layout这是最基础也是最重要的一层。deer-flow在沙盒初始化时就用VirtualAllocWindows或mmapLinux/macOS一次性申请一大块连续的虚拟地址空间。这块空间的大小是固定的默认 64MB并且被严格划分为三个区域区域名称大小访问权限用途Guard Pages (Top)4KBPAGE_NOACCESS防止栈溢出向上生长Stack Area1MBPAGE_READWRITE用户代码的调用栈Heap Area62MBPAGE_READWRITE用户代码的动态内存分配malloc/newGuard Pages (Bottom)4KBPAGE_NOACCESS防止堆溢出向下生长这个静态布局的关键在于确定性。无论你运行多少次deer-flow只要参数不变这块 64MB 地址空间的起始地址、各区域的相对偏移、以及它们的保护属性都是完全一致的。这意味着同一个二进制程序在deer-flow沙盒内每次运行时其栈指针RSP的初始值、堆的基址HEAP_BASE都是固定的。这种确定性是实现“可重现调试”的基石。当你在一次崩溃中捕获到RIP0x7ff8a1234567你可以在下一次运行中精确地在同一个地址设置断点观察寄存器和内存的变化。相比之下现代操作系统的 ASLRAddress Space Layout Randomization机制会为每次进程启动随机化 DLL 加载地址、栈基址、堆基址。这虽然提高了安全性但也让调试变得极其困难。deer-flow主动关闭了 ASLR通过链接器标志/DYNAMICBASE:NO因为它追求的不是对抗外部攻击而是内部执行的绝对可预测性。3.2 第二层内存访问的动态拦截Dynamic Interception静态划分只是画好了地图第二层则负责在运行时确保所有指令都“按图索骥”。deer-flow的指令解释器VM并非直接执行 x86-64 机器码而是执行一种自定义的、极简的字节码Bytecode。每条字节码指令如0x01 LOAD R1, [R2]在执行前都会被 VM 的核心循环vm_exec_loop进行严格的地址检查// vm.c 伪代码 switch (opcode) { case OP_LOAD: addr reg[R2]; // 从寄存器 R2 读取地址 if (addr heap_base || addr heap_base heap_size) { // 地址超出 Heap Area 范围 vm_panic(LOAD: address %p out of heap bounds, addr); } reg[R1] *(int32_t*)addr; // 安全读取 break; case OP_STORE: addr reg[R2]; if (addr heap_base || addr heap_base heap_size) { vm_panic(STORE: address %p out of heap bounds, addr); } *(int32_t*)addr reg[R1]; // 安全写入 break; }这个检查是零成本的。因为heap_base和heap_size是常量编译器可以将其优化为直接的寄存器比较cmp rax, rbx比一次真正的内存访问还要快。更重要的是它发生在用户代码的“语义层”而非操作系统内核层。这意味着即使你的代码里有一百万次LOAD指令deer-flow也能保证每一次都经过检查而不会像VirtualProtect那样因为频繁的页保护切换而导致性能暴跌。3.3 第三层原生 API 的沙盒化重定向Sandboxed Redirection第三层是最具工程挑战性的一层它处理的是那些“不听话”的原生 C/C 函数。printf、fopen、malloc这些函数它们的实现直接调用操作系统 API完全绕过deer-flow的字节码解释器。为了让沙盒真正生效deer-flow必须在链接阶段将这些标准库函数的符号重定向到它自己实现的、受控的版本。例如它的malloc实现长这样// malloc.c void* malloc(size_t size) { // 1. 检查请求大小是否过大防止耗尽整个 Heap Area if (size HEAP_SIZE / 4) { return NULL; // 直接拒绝 } // 2. 在 Heap Area 内部维护一个简单的空闲链表buddy system // 3. 分配成功后返回的地址必然在 [heap_base, heap_base heap_size) 范围内 void* ptr heap_alloc(size); return ptr; } // 所有对 malloc 的调用都会链接到这个函数而不是 libc 的 malloc同样它的printf不会真的调用 Windows 的WriteConsoleA而是将输出内容写入一个环形缓冲区Ring Buffer然后由沙盒的主控线程统一读取、格式化、并决定是否允许输出到终端。这使得printf(secret: %s, password)这样的敏感信息可以在沙盒层面就被截断和过滤而无需修改应用代码。这三层模型共同构成了一个坚固的“内存护城河”。第一层是城墙Static Layout第二层是城门守卫Dynamic Interception第三层是城内的巡逻队Sandboxed Redirection。任何试图越界的尝试都会在某一层被拦截、记录、并优雅地终止而不是让整个城市进程陷入混乱。4. 从零开始构建一个deer-flow兼容的最小化沙盒理解了deer-flow的设计哲学和三层模型我们就可以动手用最基础的工具链构建一个功能等效的最小化沙盒。这个过程不是为了替代deer-flow而是为了彻底吃透它的每一个设计决策。我们将使用纯 C 语言在 Windows 平台上从CreateProcess开始一步步搭建。4.1 环境准备一个干净的 Windows SDK 工作区首先你需要一个支持Windows.h的编译环境。我推荐使用 Visual Studio Community免费或 MinGW-w64。这里以 VS 2022 为例创建一个新的“空项目”Empty Project命名为my-deer-sandbox。在项目属性中将“配置类型”设为“应用程序 (.exe)”。关键设置在“链接器 - 高级”中将“随机基址”/DYNAMICBASE设为“否”No。这是为了确保每次运行地址空间布局一致。在“C/C - 代码生成”中将“安全检查”/GS设为“否”No。/GS会插入栈保护 cookie这会干扰我们对原始栈行为的观察而deer-flow的目标是暴露而非隐藏底层问题。完成设置后你的项目就拥有了一个“纯净”的、无 ASLR、无栈保护的执行环境这是构建沙盒的第一步。4.2 核心模块一mem_manager.c—— 静态内存布局的实现这是整个沙盒的基石。我们编写一个mem_manager.c文件它负责申请和保护那块 64MB 的“安全岛”。// mem_manager.c #include windows.h #include stdio.h #define SAFE_ISLAND_SIZE (64 * 1024 * 1024) // 64MB #define GUARD_PAGE_SIZE 4096 typedef struct { void* base; // 整个安全岛的基址 void* heap_start; // Heap Area 的起始地址 size_t heap_size; // Heap Area 的大小 } mem_context_t; static mem_context_t g_ctx {0}; // 初始化内存沙盒 bool mem_init() { // 1. 预留 64MB 2*4KB 的虚拟地址空间 g_ctx.base VirtualAlloc(NULL, SAFE_ISLAND_SIZE 2 * GUARD_PAGE_SIZE, MEM_RESERVE, PAGE_NOACCESS); if (!g_ctx.base) { printf(ERROR: VirtualAlloc reserve failed. Address space exhausted?\n); return false; } // 2. 将首尾 4KB 设置为 NOACCESS防护页 // 首防护页 if (!VirtualProtect(g_ctx.base, GUARD_PAGE_SIZE, PAGE_NOACCESS, dwOldProtect)) { printf(ERROR: VirtualProtect top guard failed\n); VirtualFree(g_ctx.base, 0, MEM_RELEASE); return false; } // 尾防护页 void* bottom_guard (char*)g_ctx.base SAFE_ISLAND_SIZE GUARD_PAGE_SIZE; if (!VirtualProtect(bottom_guard, GUARD_PAGE_SIZE, PAGE_NOACCESS, dwOldProtect)) { printf(ERROR: VirtualProtect bottom guard failed\n); VirtualFree(g_ctx.base, 0, MEM_RELEASE); return false; } // 3. 将中间 64MB 设置为 READWRITE安全岛主体 g_ctx.heap_start (char*)g_ctx.base GUARD_PAGE_SIZE; g_ctx.heap_size SAFE_ISLAND_SIZE; if (!VirtualAlloc(g_ctx.heap_start, g_ctx.heap_size, MEM_COMMIT | MEM_RESERVE, PAGE_READWRITE)) { printf(ERROR: VirtualAlloc commit failed\n); VirtualFree(g_ctx.base, 0, MEM_RELEASE); return false; } printf(INFO: Memory sandbox initialized. Base%p, Heap[%p, %p)\n, g_ctx.base, g_ctx.heap_start, (char*)g_ctx.heap_start g_ctx.heap_size); return true; } // 检查地址是否在安全岛内 bool mem_is_in_sandbox(void* addr) { if (!g_ctx.base) return false; char* p (char*)addr; return (p (char*)g_ctx.heap_start p (char*)g_ctx.heap_start g_ctx.heap_size); }这段代码的核心就是VirtualAlloc的两次调用第一次MEM_RESERVE是画地为牢第二次MEM_COMMIT是开垦耕种。VirtualProtect则是在耕种好的土地四周竖起两道不可逾越的高墙。mem_is_in_sandbox函数就是我们未来所有内存检查的“宪法”。4.3 核心模块二vm_interpreter.c—— 动态拦截的字节码引擎接下来我们实现一个极简的字节码解释器。它只支持LOAD,STORE,ADD,JMP四条指令但足以演示动态拦截的威力。// vm_interpreter.c #include stdio.h #include stdint.h #include mem_manager.h // 寄存器文件4个通用寄存器 typedef struct { uint64_t r[4]; } vm_regs_t; // 指令格式[OPCODE][REG_A][REG_B][IMMEDIATE] // OPCODE: 1 byte, REG_A/B: 1 byte each, IMMEDIATE: 4 bytes typedef enum { OP_LOAD 0x01, // LOAD R[A], [R[B]] OP_STORE 0x02, // STORE [R[B]], R[A] OP_ADD 0x03, // ADD R[A], R[B], IMM OP_JMP 0x04, // JMP IMM } vm_opcode_t; // 指令解释器主循环 bool vm_exec(uint8_t* bytecode, size_t size, vm_regs_t* regs) { size_t pc 0; // program counter while (pc size) { uint8_t opcode bytecode[pc]; uint8_t ra bytecode[pc]; uint8_t rb bytecode[pc]; int32_t imm *(int32_t*)bytecode[pc]; pc 4; switch (opcode) { case OP_LOAD: { // 检查 R[B] 是否指向安全岛内 if (!mem_is_in_sandbox((void*)regs-r[rb])) { printf(PANIC: LOAD from invalid address %p at PC%zu\n, (void*)regs-r[rb], pc-4); return false; } regs-r[ra] *(uint64_t*)regs-r[rb]; break; } case OP_STORE: { if (!mem_is_in_sandbox((void*)regs-r[rb])) { printf(PANIC: STORE to invalid address %p at PC%zu\n, (void*)regs-r[rb], pc-4); return false; } *(uint64_t*)regs-r[rb] regs-r[ra]; break; } case OP_ADD: regs-r[ra] regs-r[rb] imm; break; case OP_JMP: pc (size_t)imm; continue; // 跳过 pc 自增 default: printf(PANIC: Unknown opcode %02x at PC%zu\n, opcode, pc-4); return false; } } return true; }注意OP_LOAD和OP_STORE中的mem_is_in_sandbox调用。这就是第二层保护的体现。它在每一条可能引发0xc0000005的指令执行前就完成了地址合法性检查。如果检查失败它会打印一条清晰的错误信息指出是哪条指令、在哪个地址、哪个程序计数器位置出的问题然后优雅退出。这比让整个进程崩溃要友好得多。4.4 核心模块三sandbox_main.c—— 沙盒的入口与集成最后我们将所有模块集成到主程序中。这个sandbox_main.c就是deer-flow的“灵魂”。// sandbox_main.c #include stdio.h #include stdlib.h #include mem_manager.h #include vm_interpreter.h // 一个故意制造崩溃的测试程序字节码 // 功能将一个值存入 R0然后试图从一个非法地址 LOAD uint8_t crash_bytecode[] { 0x02, 0x00, 0x01, 0x00, 0x00, 0x00, 0x00, // STORE [R1], R0 (R10x00000000) 0x01, 0x02, 0x01, 0x00, 0x00, 0x00, 0x00, // LOAD R2, [R1] (从地址0读取) }; int main(int argc, char* argv[]) { printf( Starting my-deer-sandbox \n); // 1. 初始化内存沙盒 if (!mem_init()) { return 1; } // 2. 初始化寄存器 vm_regs_t regs {0}; regs.r[0] 0x12345678; // R0 some value regs.r[1] 0x00000000; // R1 0 (illegal address!) // 3. 执行字节码 printf(Executing test bytecode...\n); bool success vm_exec(crash_bytecode, sizeof(crash_bytecode), regs); if (success) { printf(SUCCESS: Sandbox executed without error.\n); } else { printf(FAILURE: Sandbox caught an illegal memory access.\n); } // 4. 清理 VirtualFree(g_ctx.base, 0, MEM_RELEASE); return success ? 0 : 2; }编译并运行这个程序。你会看到 Starting my-deer-sandbox INFO: Memory sandbox initialized. Base0x00007ff7a1230000, Heap[0x00007ff7a1231000, 0x00007ff7a1631000) Executing test bytecode... PANIC: LOAD from invalid address 0x0000000000000000 at PC7 FAILURE: Sandbox caught an illegal memory access.它没有崩溃没有0xc0000005而是被我们的沙盒精准捕获并给出了明确的诊断信息。这就是deer-flow的力量它把一个难以调试的系统级崩溃转化成了一个可读、可定位、可修复的应用级错误。注意这个最小化沙盒已经具备了deer-flow的核心能力。它没有复杂的 JIT 编译没有高级的垃圾回收甚至没有网络 I/O。但它证明了一个真理对内存的绝对控制不在于功能的繁多而在于边界的清晰与执行的确定性。任何试图在此之上添加的功能比如支持 Python 字节码、集成 Node.js 的vm模块都应该是这个坚实地基之上的建筑而非替代地基本身。5. 实战排错如何用deer-flow思维诊断一个真实的0xc0000005崩溃理论和玩具代码终归是纸上谈兵。现在让我们进入实战环节用deer-flow的思维模式去诊断一个真实世界中困扰了我三天的0xc0000005崩溃案例。这个案例完美体现了deer-flow的价值所在。5.1 问题现象与初步排查客户的一个基于 Electron 的桌面应用在 Windows 10 21H2 上当用户点击“导出报表”按钮时有 30% 的概率会崩溃错误代码正是0xc0000005。应用日志里只有一行“Renderer process crashed.”。electron-log没有捕获到任何 JavaScript 异常。coredump文件分析显示崩溃点在libpdfium.dll的CPDF_Page::DrawPage函数内部但堆栈信息不完整无法定位到具体的 C 行号。常规手段失效--enable-logging没有额外输出。--v1V8 verbose logging只显示 JS 执行流程不涉及原生渲染。Process Monitor显示崩溃前libpdfium.dll有大量的ReadFile和WriteFile操作但没有明显的失败。此时deer-flow的思维开始介入既然崩溃点在libpdfium.dll而libpdfium是一个 C 编写的 PDF 渲染引擎那么问题几乎肯定出在它的内存管理上——要么是malloc后的越界读写要么是delete后的悬垂指针访问。而deer-flow的核心优势就是能将这类“黑盒”问题变成“白盒”问题。5.2 构建deer-flow风格的诊断沙盒我并没有直接去修改 Electron 的源码那太庞大而是创建了一个极简的pdf-crash-diagnoser.exe它只做三件事加载libpdfium.dll。调用FPDF_InitLibrary初始化 PDFium。用deer-flow的mem_manager初始化一块 128MB 的安全岛并将libpdfium.dll的所有malloc/free调用通过Detours库重定向到我的sandbox_malloc和sandbox_free。sandbox_malloc的实现如下// sandbox_malloc.c #include mem_manager.h // 全局的、受控的内存池 static char* g_sandbox_heap NULL; static size_t g_sandbox_heap_size 0; static size_t g_sandbox_used 0; void* sandbox_malloc(size_t size) { if (!g_sandbox_heap) { // 首次调用初始化沙盒内存池 g_sandbox_heap (char*)mem_get_heap_start(); // 从 deer-flow 的 mem_manager 获取 g_sandbox_heap_size mem_get_heap_size(); g_sandbox_used 0; } // 在沙盒内存池内进行简单的首次适配分配First-Fit if (g_sandbox_used size g_sandbox_heap_size) { return NULL; // 沙盒内存耗尽 } void* ptr g_sandbox_heap g_sandbox_used; g_sandbox_used size; // 关键记录本次分配的元数据 printf(ALLOC: %p, size%zu, total_used%zu\n, ptr, size, g_sandbox_used); return ptr; } void sandbox_free(void* ptr) { // 在沙盒中free 是一个空操作因为我们用的是线性分配器 // 真实场景中这里会维护一个空闲链表 printf(FREE: %p\n, ptr); }然后我用DetoursHook 了libpdfium.dll的malloc和free// hook_pdfium.cpp #include detours.h #include sandbox_malloc.h // 原始 malloc 函数指针 static void* (__cdecl *real_malloc)(size_t) malloc; static void (__cdecl *real_free)(void*) free; // Hook 后的 malloc void* __cdecl hooked_malloc(size_t size) { // 优先使用沙盒内存 void* ptr sandbox_malloc(size); if (ptr) { return ptr; } // 沙盒内存耗尽回退到系统 malloc仅用于诊断生产环境应禁止 printf(WARNING: Falling back to system malloc for %zu bytes\n, size); return real_malloc(size); } // Hook free void __cdecl hooked_free(void* ptr) { // 如果 ptr 在沙盒内存池内则调用 sandbox_free if (ptr g_sandbox_heap ptr g_sandbox_heap g_sandbox_heap_size) { sandbox_free(ptr); } else { real_free(ptr); } } // 安装 Hook void install_pdfium_hooks() { DetourTransactionBegin(); DetourUpdateThread(GetCurrentThread()); Detour
返回列表