Skip to content

fix(sandboxd): escape C0 control chars in JSON so coloured output survives - #669

Merged
xiaods merged 1 commit into
mainfrom
fix/sandboxd-json-control-chars
Oct 22, 2026
Merged

xiaods merged 1 commit into
mainfrom
fix/sandboxd-json-control-chars

Conversation

@xiaods

@xiaods xiaods commented Oct 22, 2026

Copy link
Copy Markdown
Owner

背景

用官方 k8e-sandbox-cli 连上远端集群跑一个跑马灯(滚动 ANSI 彩色横幅)时发现:

$ k8e-sandbox-cli run "python3 /workspace/marquee.py" --background
{"run_id":"sess-...-bg-...","status":"started"}

$ k8e-sandbox-cli poll sess-...-bg-...
{"run_id":"","status":"","stdout":"","exit_code":0,"duration_ms":0}

输出完全为空——但命令明明在沙箱里跑了 16 秒、打印了 169 KB 的彩色帧。

根因

sandboxd 的 jsonEscape(sandboxd/src/exec.zig)只转义了 5 个字符:

'"' => "\\\"";  '\\' => "\\\\";  '\n' => "\\n";  '\r' => "\\r";  '\t' => "\\t";
else => try out.append(allocator, c),   // ← 其它 C0 控制字符原样输出

ESC (0x1B) 没有被转义——而它正是所有终端程序输出 ANSI 颜色码的开头。

RFC 8259 禁止 JSON 字符串里出现未转义的控制字符,所以带颜色的响应不是合法 JSON。实测抓到的响应体:

$ curl -s http://<podIP>:2024/exec/background/<run_id> | python3 -m json.tool
json.decoder.JSONDecodeError: Invalid control character at: line 1 column 108

响应体里 \r 被正确转义成了 \r(2 字符),但 ESC 是原始 0x1B 字节,出现 9145 次。

控制面 PollRun 把 decode 错误吞掉了:

var result pb.PollRunResponse
json.NewDecoder(resp.Body).Decode(&result)   // 错误被丢弃
return &result, nil                          // 返回零值

于是调用方看到的是「这个任务没有产生任何输出」,而真实字节还躺在 sandboxd 的缓冲区里。

jsonEscape 一共被 7 处使用:exec 响应、files read/list、进程表、transcript、background 的 stdout/stderr。所以任何带颜色的命令输出都会丢,含有 ESC 字节的文件也会产生无法解析的响应体。

同一个文件里的兄弟函数 execctl.zig 的 escapeJson 本来就是对的(转义全部 0...31)——只有 exec.zig 这一份漏了。

修改

  1. sandboxd/src/exec.zig — jsonEscape 把全部 0...31 转义成 \u00XX,与 execctl.zig 的实现对齐。
  2. pkg/sandboxmatrix/grpc/server.go / orchestrator.go — 不再吞掉 decode 错误。新增 decodeSandboxdResponse 收口,非法响应体以错误形式返回,而不是伪装成「没有输出」。这样下一个出问题的响应会立刻暴露,而不是静默。
  3. 回归测试 — Zig 2 个新用例(ESC 逐字节断言 + 拼进 JSON 文档后回读比对),Go 3 个新用例(合法体、含原始 ESC 的体、截断的体)。

验证

同一个跑马灯,前后对比:

poll 结果
修复前 status="" stdout="" — 0 字节
修复后 status=completed stdout=169828 字节 — 完整 24-bit 彩色帧

同步 run 路径(exec.zig 的 exec 响应)同样修复:

printf "\033[1;36m★\033[0m"   →  stdout 长度 12(= 转义序列的精确字节数,修复前为 0)
验证项 结果
sandboxd Zig 测试 ✅ 61/61(新增 2)
Go 全部包 ✅ 37/37
官方 Python SDK 2.56.0 ✅ 44/44
官方 JavaScript SDK 2.56.0 ✅ 17/17

影响面

任何输出带 ANSI 颜色码的命令都会受影响——ls --color、git diff --color、进度条、TUI 程序,以及我们自己的跑马灯。修复前它们的输出会被静默丢弃。

关联

K8E 集群:13.125.242.118(节点 ip-172-31-16-11,4C8G,gVisor)
k8e 版本:v1.37.0-20261022-rc1
sandbox 镜像:官方 ghcr.io/xiaods/k8e-sandbox@sha256:dda7465a…(含 PR #667 的修复)

…vives

sandboxd's jsonEscape handled only five characters: quote, backslash,
newline, carriage return and tab. Every other C0 control character was
copied through raw — including ESC (0x1B), which is what every terminal
program emits for an ANSI colour code.

RFC 8259 forbids raw control characters inside a JSON string, so a
response carrying coloured output was not valid JSON. The control plane
decoded it into an empty struct and swallowed the error, so the caller
saw "the run produced no output" while the bytes sat unread in the
sandboxd buffer. A background run of a marquee came back as 0 bytes;
`k8e-sandbox-cli poll` showed an empty result.

jsonEscape is used in seven places — the exec response, files read/list,
the process table, the transcript and the background-run stdout/stderr —
so any command with colour in its output lost it, and a file containing
an ESC byte produced an unparsable body.

Escape all of 0...31 as \u00XX, matching the sibling helper in
execctl.zig, which already did this.

Also stop swallowing the decode error in the Go control plane. A
malformed body now surfaces as an error instead of an empty result, so
the next malformed response is visible rather than silent.

Verified against a marquee over the official E2B SDK path:

  before: poll -> status="" stdout=""   (0 bytes)
  after:  poll -> status=completed stdout=169828 bytes

sandboxd: 61/61 tests pass (2 new). Go: 37/37 packages pass.
@sonarqubecloud

Copy link
Copy Markdown

@github-actions

Copy link
Copy Markdown
Contributor

Test Results

1 024 tests  +3   997 ✅ +3   6m 52s ⏱️ -10s
  120 suites ±0    27 💤 ±0 
    1 files   ±0     0 ❌ ±0 

Results for commit caf8635. ± Comparison against base commit 042d7d2.

@xiaods
xiaods merged commit 2ed56f1 into main Oct 22, 2026
9 checks passed
@xiaods
xiaods deleted the fix/sandboxd-json-control-chars branch October 27, 2026 06:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant