返回 2026-06-07
⚙️ 工程

使用 MicroPython 和 WASM 在沙箱中运行 Python 代码Running Python code in a sandbox with MicroPython and WASM

simonwillison.net·2026-06-06

在沙箱环境中安全地执行 Python 代码一直是开发者面临的挑战,而结合 MicroPython 与 WebAssembly (WASM) 提供了一种极具潜力的轻量级解决思路。作者发布了名为 `micropython-wasm` 的 alpha 版本包,成功实现了在浏览器或安全沙箱中隔离运行 Python 代码的方案。该方案不仅满足了隔离执行的安全需求,还被直接应用于 `Datasette Agent` 的代码执行沙箱插件中。通过将 Python 编译为 WASM,开发者可以以更低的性能开销实现安全的代码运行环境。

Simon Willison

2026年6月6日

几年来,我一直在尝试在沙盒中运行代码的不同方法,而我最新的尝试感觉终于具备了我一直在寻找的所有特性。我已将其作为一个名为 micropython-wasm 的 Alpha 版本包发布,并且我正在将其用作 Datasette Agent 的代码执行沙盒插件,该插件名为 datasette-agent-micropython。

  • 为什么我需要一个沙盒?
  • 我对沙盒的期望
  • WebAssembly 在此展现出了极大的前景
  • WebAssembly 中的 MicroPython
  • 构建第一个版本
  • 亲自尝试一下
  • 你应该信任我凭感觉编写的沙盒吗?
  • 为什么我需要一个沙盒?

    我的主要开源项目——Datasette、LLM,甚至 sqlite-utils——都支持插件。

    我极其喜爱将插件作为扩展软件的机制。一个精心设计的插件系统能将尝试新事物的风险降至几乎为零——即使是最天马行空的想法,也不会对核心应用程序本身产生持久的影响。我的软件可以在一夜之间增加一项新功能,而我甚至不需要审查任何拉取请求!

    但这有一个主要缺点:我的插件系统都使用 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 库将 WASM 引入了 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,我希望变量和函数能常驻在内存中,这样我就可以在多次代码执行调用中重用它们。

    使用编程智能体(coding agents)的一个妙处在于,你可以快速从构想推进到概念验证。我给出了提示:

    关于保持变量常驻:如果我们在 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 提供了一个“燃料”(fuel)概念来限制一次 WebAssembly 调用可以执行多少操作,这非常适合解决这个问题,但其单位很难直观评估。我目前正在尝试 2000 万的默认“燃料”设置,但我不确定这是否是最合适的值。

    亲自尝试一下

    micropython-wasm 的 alpha 版本现已在 PyPI 上发布。

    你可以按照 README 中的说明,在你自己的 Python 代码中尝试它。我还在 0.1a2 版本中添加了一个简单的 CLI 模式,这意味着你可以使用 uvx 来尝试它,而无需事先安装,如下所示:

    uvx micropython-wasm -c 'print("Hello world")'
    # To see it run out of fuel:
    uvx micropython-wasm -c 's = ""; while True: s += "longer"'
    # Outputs: micropython-wasm: guest exited with code 1

    你也可以像这样在 Datasette Agent 中尝试它:

    uvx llm keys set openai
    # Paste in an OpenAI key, then:
    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

    你应该信任我靠直觉写出来的沙箱吗?

    在抱怨过那些不成熟、维护松散的沙箱库之后,我现在竟然构建了自己的沙箱,这真是极具讽刺意味!

    我故意给它打上了 alpha 版本的标签,还不准备把它推荐给任何不愿承担重大风险的人。

    我已经对它进行了充分的测试,我自己使用它是没问题的。我已经发布了使用它的第一个插件 datasette-agent-micropython。我还在那个 Datasette Agent 插件中锁定了 GPT-5.5 xhigh,并挑战让它逃出沙箱,到目前为止它还没有成功。

    我希望这个实现能够说服一些拥有专业安全团队且面临高风险问题的公司,致力于使用 WebAssembly 中的 Python 作为沙箱方案,并将他们自己的解决方案开源。

    需要完整排版与评论请前往来源站点阅读。