往沙箱里写一个大文件,为什么总是失败


你在本地写一个大文件,一切顺利;同样的操作丢给一个跑在 Modal 上的沙箱,它回你一句 ValueError: embedded null byte。换一个后端,报错变成“文件是空的”——write_file 返回成功,落到磁盘的却是 0 字节。你开始怀疑是网络、是权限、是缓存,最后发现理由朴素得让人恼火:整个文件内容被当作命令行参数传给了 shell,而命令行参数有长度上限,还会被同机器上任何进程看见。2026-09-26 合并的 PR #124206 修掉了这件事。

问题的根源:stdin 数据被塞进了 argv

Shell 里 cat file | bash -c '...' 是常见操作,但沙箱后端走的是一条更省事的捷径:把要传给命令的 stdin 数据,直接拼进那条 bash -c 的参数里(代码里叫 heredoc 路径)。

多数时候它工作正常,直到碰上两个硬限制:

限制 数值 后果
NUL 字节 参数里不允许出现 \0 抛 ValueError: embedded null byte,写入直接失败
参数长度 Linux MAX_ARG_STRLEN 约 128 KiB;Modal 侧更紧(约 64 KiB 执行参数上限) 更大的写入在 Linux 或 Modal 上整体失败

PR 里给出的复现很干脆:一个带 NUL 字节的 160 KiB payload,在主干代码上抛 embedded null byte,而那条拼接出来的 argv 元素本身有 165003 字节。注意后半句:即使内容里没有 NUL,这个尺寸也已经越线了。

还有一个容易被忽略的副作用:argv 是公开信息。同一台机器上的任何进程都能通过进程列表看到别人的命令行参数。把文件内容(可能包含密钥、token、用户数据)拼进 argv,等于顺手把它们放在了进程表里。

修复:每个后端声明自己的 stdin 传递方式

补丁引入了一个显式的模式声明(tools/environments/base.py):

# How execute() hands stdin to _run_bash: "pipe" (process stdin, default),
# "payload" (passed through as stdin_data; the backend transports it via its
# SDK -- Modal/managed Modal stream it, Daytona/Vercel stage a file) or
# "heredoc" (embedded in the command; no built-in backend, plugins only).
_stdin_mode: str = "pipe"  # "pipe" | "payload" | "heredoc"

三个内置后端里:

后端 现在的做法
Modal _stdin_mode = "payload",通过 SDK 流式写入 stdin,按字节精确(surrogateescape),分块发送(每块 1 MiB,避开旧服务端 2 MB 上限)
Daytona 把 payload 上传成一个权限 0600 的私有临时文件,再以 exec 0< f; rm -f -- f; cmd 的形式喂给命令
Vercel 同上:先把字节落到私有路径,改权限,再重定向 stdin

Daytona 的实现在细节上还更干净一些:字节直接通过 SDK 上传,不经过本机临时文件;空的 stdin 干脆跳过整个暂存步骤。

再看那条重定向前缀,它有两个动作,顺序是刻意的:

exec 0< <staged-path> || exit $?      # shell 先接管这个文件描述符
rm -f -- <staged-path> || exit $?     # 再把文件删掉——句柄已经在 shell 手里
<command>

先 exec 再 rm,意味着命令运行期间磁盘上不再有那个文件,而数据仍然可以从已打开的描述符里读出来。这两步在 BaseEnvironment 里被抽成了共享辅助函数(_staged_stdin_path、_redirect_stdin_from_file),旧的 heredoc 路径 _embed_stdin_heredoc 才保留下来给插件后端用。

顺手被重写的取消逻辑

原来的清理逻辑有两个真问题:任何一次 SDK 报错都会把整个沙箱停掉,而每一次取消都会停两遍。补丁把状态拆成三个(cancelled / staged / dispatched)并统一放进环境锁里管理:

  • 取消发生在“上传完成、命令尚未派发”之间时,暂存的敏感文件会被擦掉(Vercel 覆盖写、Daytona 删除),然后只停一次;
  • 普通的 SDK 报错不再导致沙箱停摆;
  • 已经被取消的命令永远不会被派发。

对你意味着什么

  • 不需要改任何配置。 三个后端各自换了传递方式,调用方无感。
  • 之前会静默失败或直接报错的场景——带 NUL 字节的内容、超过约 128 KiB 的写入——现在按字节精确落盘。
  • 如果你写过自己的沙箱后端插件,_embed_stdin_heredoc 仍然保留着,但新后端应优先声明 payload 模式,理由就是上面那张表。
  • 这三个补丁合并于 2026-09-26,晚于 v0.21.5 的发行标签,位于 main;hermes update 即可获得。

这件事的更大意义

同一批工作前面还有一站:PR #122012 / #122011 修的是“heredoc 绑定到了错误的命令上”,那是字节精确写入那一类问题的另一面——文件看起来写成功了,内容却是错的。这次的 argv 修复解决的是“根本写不进去”和“内容被别的进程看见”。两条线合起来说明同一件事:往远程环境传数据,最不该走的就是命令行参数。

如果你大量使用远程沙箱,值得连着读:沙箱里的媒体文件怎么送出来、并行 agent 共享一个 Docker 容器意味着什么,以及本站 v0.21.5 发版页。