在 MicroPython 和 WASM 沙箱中运行 Python 代码
Running Python code in a sandbox with MicroPython and WASM
Simon Willison 发布了 `micropython-wasm`(alpha 包),用于在 Python 应用中通过 WebAssembly 沙箱安全执行代码。该方案使用 MicroPython 编译为 WASM,通过 wasmtime 运行时实现内存/CPU 限制、受控文件与网络访问,并支持宿主函数交互。关键实现包括:通过线程与队列维持持久解释器状态,用 78 行 C 代码暴露宿主函数,默认 2000 万燃料限制 CPU。已用于 Datasette Agent 的代码执行插件 `datasette-agent-micropython`,并挑战 GPT-5.5 突破沙箱未成功。
我多年来一直在尝试不同的沙箱内运行代码的方法,但最新的尝试感觉终于具备了所有我一直在寻找的特性。我已将其作为名为 micropython-wasm 的 alpha 包发布,并正在将其用于 Datasette Agent 的代码执行沙箱插件 datasette-agent-micropython。
为什么我需要沙箱?
我的核心开源项目——Datasette、LLM,甚至 sqlite-utils——都支持插件。我非常喜欢插件这种扩展软件的机制。精心设计的插件系统能将尝试新功能的风险降至几乎为零——即使是最疯狂的想法也不会对核心应用本身产生持久影响。我的软件可以在一夜之间增加新功能,而我甚至不需要审查 pull request!
但有一个主要缺点:我的插件系统都使用 Python 和 Pluggy,插件代码在我的应用中拥有完全权限执行。有缺陷或恶意的插件可能破坏一切或泄露私有数据。我希望能在无法读取未授权文件、无法连接网络、或以其他方式对应用或用户计算机造成风险或危害的环境中运行插件式代码。
我的兴趣不仅限于插件。特别是对于 Datasette,有许多功能我希望支持,其中任意代码执行会很有用。我已经在 Datasette Enrichments 中对此进行了实验,其中代码可用于转换表中存储的值。我希望构建一种机制,可以按计划从授权位置获取 JSON,运行一小段代码将其重新格式化为字典列表,然后将这些字典作为行插入 SQLite 数据库表。
我对沙箱的要求
我的目标是在自己的 Python 应用中安全地执行代码。以下是我需要的:
- 依赖项能干净地从 PyPI 安装,必要时包括跨平台的二进制 wheel。我不希望用户在使用我的软件时,除了直接安装我的 Python 包外,还需要执行任何额外步骤。
- 执行的代码必须受内存和 CPU 限制。我不希望
while True: s += "longer string"导致我的应用或用户计算机崩溃。 - 文件访问必须严格受控。要么完全没有文件系统访问权限,要么我能精确定义哪些文件可读、哪些文件可写。
- 网络访问也受控。沙箱代码不能与任何东西通信,除非通过我完全控制的层。
- 支持与宿主函数交互。如果我不能小心地向运行中的代码暴露选定的平台功能,沙箱就没多大用处。
- 必须健壮、有支持、文档清晰。我数不清在仓库中看到过多少带有“未积极维护”警告的沙箱项目了!
WebAssembly 看起来很有希望
Web 浏览器在恶意代码方面运行在最恶劣的环境中。它们的工作是在几乎每次页面加载时从网络下载并执行不受信任的代码。鉴于此,JavaScript 引擎应该是沙箱的绝佳候选。遗憾的是,这些引擎也极其复杂,并且不是为了易于嵌入其他项目而设计的。我见过的大多数 v8-in-Python 项目维护频率低,并带有警告,不要将其用于完全不受信任的代码。
WebAssembly 是一个更好的候选。它从一开始就被设计为支持所有我关心的特性,并且已经在浏览器中测试了近十年。wasmtime Python 库得到积极维护,并提供了二进制 wheel。
WebAssembly 中的 MicroPython
像 wasmtime 这样的 WebAssembly 引擎运行 WebAssembly 二进制文件。一些编程语言(如 Rust)很容易直接编译为 WebAssembly。动态语言(如 JavaScript 和 Python)则更难——它们支持像 eval() 这样的语言原语,这意味着它们在运行时需要一个完整的解释器。
要运行 Python,我们需要一个编译为 WebAssembly 的完整 Python 解释器,并以一种易于向其提供代码、连接宿主函数和访问结果的方式连接起来。Pyodide 提供了一个出色的包,用于在浏览器中使用 WebAssembly 运行 Python,但不支持在服务器端 Python 中使用 Pyodide。我能找到的最新建议来自 2024 年 10 月,其中指出“Pyodide 由 Emscripten 工具链构建,只能在浏览器或 Node.js 中运行”。
前几天,我决定看看 MicroPython 作为这个选项的可能性。MicroPython 网站说:
MicroPython 是 Python 3 编程语言的一个精简高效实现,包含 Python 标准库的一个小子集,并针对微控制器和受限环境进行了优化。
WebAssembly 对我来说确实感觉像是一个受限环境!
构建第一个版本
我让 GPT-5.5 Pro 为我做了一些研究,结果发现了 Yamamoto Takahashi 针对 MicroPython 的一个 PR,标题为“Experimental WASI support for ports/unix”。然后它生成了这个 research.md 文档,所以我让 Codex Desktop 和 GPT-5.5 high 自由发挥,看看会发生什么:阅读 research.md 文档并构建这个。你可能需要编写一个脚本,作为该项目的一部分编译 MicroPython 的自定义 WASM 版本——作为该脚本的一部分,将 MicroPython 代码获取到 /tmp 目录。
它成功了。我现在有了一个原型 Python 库,可以在 WebAssembly 沙箱内执行 Python 代码!
最棘手的部分是解决持久解释器状态的问题。我们这里使用的 WASM 构建暴露了一个单一的入口点,该入口点启动解释器,运行代码,然后在最后停止解释器。这对于一次性脚本来说工作正常,但对于 Datasette Agent,我希望变量和函数能常驻内存,以便在多次代码执行调用中重用它们。
与编码 agent 合作的一个好处是,你可以快速从想法到概念验证。我提示道:
为了保持变量常驻:如果我们让 micropython 内部的代码调用一个宿主函数
get_next_python_code(),然后将其传递给eval()——并且该宿主函数阻塞直到新代码可用,也许通过在一个带有队列的线程中运行?这个或类似的想法能帮上忙吗?
经过一些迭代,我们得到了一个可行的版本!现在在 Python 代码中你可以这样做:
from micropython_wasm import MicroPythonSession
with MicroPythonSession() as session:
print(session.run("x = 10\nprint(x)").stdout)
print(session.run("x += 5\nprint(x)").stdout)
print(session.run("print(x * 2)").stdout)
在底层,这会启动一个线程,设置一个请求队列,然后向该队列发送消息以执行 session.run() 命令,每次等待回复队列以获取该执行的结果。在 WASM 内部,MicroPython 解释器阻塞等待 __session_next__() 宿主函数返回下一行代码,它对该代码运行 eval(),然后在每个块成功执行后调用 __session_result__({"id": request_id, "ok": True})。
另一个复杂之处是支持宿主函数,这样我的 Python 库可以有选择地暴露函数,然后由 MicroPython 中运行的代码调用。Codex 最终用 78 行 C 代码解决了这个问题,这些代码被编译成我正在随包分发的 362KB WebAssembly blob。我绝不是 C 程序员,但我阅读了这些 C 代码,并让两个不同的模型向我解释(这里是 Claude 的解释),并且我对其进行了大量测试。使用 WebAssembly 的好处是,如果 C 代码有致命缺陷,最坏的情况是 WebAssembly 执行会因异常而失败。我可以接受这种风险。
内存限制由 wasmtime 直接支持。CPU 限制稍微难一些:wasmtime 提供了一个“燃料”概念来限制 WebAssembly 调用可以执行的操作数量,这很适合这个问题,但单位很难推理。我现在正在试验 2000 万的默认“燃料”设置,但我不确定这是否是最合适的值。
自己试试
micropython-wasm alpha 版现已上线 PyPI。你可以按照 README 中的描述从自己的 Python 代码中尝试。我还在 0.1a2 版本中添加了一个简单的 CLI 模式,这意味着你可以使用 uvx 尝试,而无需先安装它,如下所示:
uvx micropython-wasm -c 'print("Hello world")'
# 查看燃料耗尽的情况:
uvx micropython-wasm -c 's = ""; while True: s += "longer"'
# 输出:micropython-wasm: guest exited with code 1
你也可以在 Datasette Agent 中这样尝试:
uvx llm keys set openai
# 粘贴 OpenAI 密钥,然后:
uvx --with datasette-agent \
--with datasette-agent-micropython \
--prerelease allow \
datasette --internal internal.db \
-s plugins.datasette-llm.default_model gpt-5.5 \
--root -o
然后导航到 http://127.0.0.1:8001/-/agent 并运行提示:
show me some micropython
你应该信任我的 vibe-coded 沙箱吗?
在抱怨过不成熟、维护松散的沙箱库之后,我现在自己构建了一个,这真是极具讽刺意味!我故意给它打上了 alpha 版本的标签,并且不准备推荐给任何不愿承担重大风险的人。我已经对其进行了足够的测试,我自己使用是没问题的。我已经发布了第一个使用它的插件 datasette-agent-micropython。我还将 GPT-5.5 xhigh 锁定在该 Datasette Agent 插件中,并挑战它突破沙箱,到目前为止它还没有成功。
我希望这个实现能说服一些拥有专业安全团队和高风险问题的公司,承诺使用 Python in WebAssembly 作为沙箱方法,并开源他们自己的解决方案。
标签:python, sandboxing, ai, datasette, webassembly, generative-ai, llms, ai-assisted-programming, codex, datasette-agent, micropython