Skip to content

fix(sandbox): pin utf-8 decoding on the local sandbox backend's subprocess calls - #5905

Merged
WillemJiang merged 3 commits into
bytedance:mainfrom
siye566:fix/local-sandbox-subprocess-utf8
Oct 3, 2026
Merged

WillemJiang merged 3 commits into
bytedance:mainfrom
siye566:fix/local-sandbox-subprocess-utf8

Conversation

@siye566

@siye566 siye566 commented Sep 26, 2026

Copy link
Copy Markdown
Contributor

Fixes #5904

Why

While auditing the local container sandbox backend I found all 22 text-mode subprocess.run calls decode docker / container-runtime output with the platform's preferred encoding (no encoding= anywhere in the module). On hosts whose ANSI code page is not UTF-8 — cp936 Chinese Windows is the big one for this project's user base — any non-ASCII byte in the CLI output raises inside subprocess's reader thread, which subprocess.run swallows: stdout/stderr come back None. The JSON-parsing call sites then crash on None (network-proxy pending/deny-pending paths) or silently return an empty result (json.loads(result.stdout or "[]")), and stderr diagnostics are dropped. Issue #5904 has the decoding-mechanism reproduction and the per-shape breakdown.

What changed

Every text-mode subprocess call in local_backend.py now pins encoding="utf-8" with errors="replace". The container CLIs write UTF-8 on every platform, so UTF-8-locale hosts are byte-identical to before; non-UTF-8-locale hosts stop losing entire streams, and stray non-conforming bytes degrade to replacement characters instead of discarding the rest of the output. No call-site semantics changed: same commands, same parsing, same error handling.

Surface area

  • Sandbox — docker/ or sandboxed execution
  • Docs / tests / CI only — two new tests in backend/tests/test_aio_sandbox_local_backend.py (no runtime behavior change for UTF-8 locales)

Test

  • test_docker_subprocess_calls_pin_utf8_decoding — drives the docker-detection path with a recorded fake subprocess.run and asserts encoding="utf-8" / errors="replace" are passed.
  • test_every_text_mode_subprocess_call_pins_utf8 — static guard: the module must never carry a text=True call without an encoding="utf-8" pin, so future call sites can't reintroduce the gap. Both tests fail on main and pass with this change.
  • backend/tests/test_aio_sandbox_local_backend.py: 104 passed / 3 skipped. The two pre-existing failures in test_aio_sandbox_provider.py (test_get_lark_cli_runtime_mounts_uses_user_auth_dirs, test_get_extra_mounts_provisioner_payload_has_unique_container_paths) fail identically on a clean main checkout on this machine and are unrelated (Windows-specific fixtures).
  • ruff check / ruff format --check clean on both files.

Notes

  • AI assistance: the diff was produced by an AI agent (Claude). I reviewed every line and can explain each and how it interacts with the rest of the codebase without asking an AI tool — the change only adds decoding kwargs to existing calls; commands, parsing, and error handling are untouched.

…ocess calls

The local container backend shells out to docker / the container runtime
with text-mode subprocess calls that do not pass an encoding, so output is
decoded with the platform's preferred encoding. On hosts whose ANSI code
page is not UTF-8 (cp936 Chinese Windows, cp1252 Western European), any
non-ASCII byte in the CLI output raises inside subprocess's reader thread:
stdout/stderr come back None, the JSON-parsing call sites then crash on
None or silently mis-parse, and stderr diagnostics are dropped.

Pin encoding="utf-8" with errors="replace" on every text-mode call in
the module. The container CLIs write UTF-8 on every platform; replace
keeps the parseable portion of any non-conforming output alive instead of
discarding it wholesale.
@CLAassistant

CLAassistant commented Sep 26, 2026 •

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@github-actions github-actions Bot added risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines labels Sep 26, 2026
@WillemJiang

Copy link
Copy Markdown
Collaborator

@siye566 Thanks for your contribution, please click the CLA button to sign the CLA first.

@willem-bd willem-bd left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified against head e9a951c: all 22 text-mode subprocess.run calls in local_backend.py now pass encoding="utf-8" + errors="replace" (counts are exactly 22/22 in the head file), the one remaining binary-mode call (the copied docker-exec in _start_network_proxy) intentionally avoids locale decoding via read_bytes() and an explicit .decode(errors='replace') on stderr, and no other file in the aio_sandbox package shells out in text mode — so the fix is complete for the module and consistent with the repo's explicit-encoding-over-locale convention. Behavior is byte-identical on UTF-8 locales and strictly better on cp936-style hosts. One suggestion below on making the new static guard structurally robust rather than count-based.

source = Path(local_backend_module.__file__).read_text(encoding="utf-8")
text_modes = len(re.findall(r"\btext=True\b", source))
pinned = len(re.findall(r'encoding="utf-8"', source))
assert text_modes == 0 or pinned >= text_modes, f"{text_modes} text=True call(s) but only {pinned} encoding pins; new text-mode subprocess calls must pass encoding='utf-8' + errors='replace'"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This guard is count-based module-wide, which leaves two gaps it claims to close. (1) It compares total text=True occurrences against total encoding="utf-8" occurrences rather than pairing them per call, so once any unrelated encoding="utf-8" literal shows up in the module (a docstring, an open(..., encoding="utf-8"), a comment), a new unpinned text=True subprocess call passes the guard. Counts are exactly 22/22 today so it is tight right now, but that balance is accidental, not structural. (2) It never inspects errors= at all, even though the module contract (the docstrings above and the PR) requires errors="replace"; test_docker_subprocess_calls_pin_utf8_decoding does assert errors=, but it only observes the calls on the runtime-detection/_docker_server_is_desktop path it happens to drive, not the other ~20 sites. Suggestion: walk the module with ast and, for every subprocess.run(...) call carrying text=True, assert the call's own keywords include both encoding="utf-8" and errors="replace" — that makes the guard per-call and closes both gaps.

@github-actions github-actions Bot added the area:docs Documentation and Markdown only label Oct 1, 2026
@WillemJiang WillemJiang added this to the 2.2.0 milestone Oct 2, 2026
@WillemJiang
WillemJiang merged commit f11be0a into bytedance:main Oct 3, 2026
17 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:docs Documentation and Markdown only risk:medium Medium risk: regular code changes size/M PR changes 100-300 lines

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[bug] Local sandbox backend decodes docker CLI output with the platform locale: non-UTF-8 hosts lose stdout/stderr and crash JSON parsing

4 participants