Skip to content

Commit 7ecbf5a

Browse files
GodlyDonutsImagineer99danielhanchen
authored
Use UTF-8 for Python code-execution subprocess I/O (#6489 class) (#6548)
* Use UTF-8 for Python code-execution subprocess I/O Studio's code-execution tool already tells the child to emit UTF-8 (PYTHONIOENCODING=utf-8 in _build_safe_env), but _python_exec writes the temp script and decodes the subprocess pipe with the OS default codec. On Windows (cp1252), non-ASCII in model-written code or its output -- arrows, CJK, emoji -- raises UnicodeEncodeError / UnicodeDecodeError and breaks execution. Complete the UTF-8 wiring in core/inference/tools.py: - write the temp script with encoding="utf-8" - decode _python_exec stdout as utf-8, errors="replace" - set PYTHONIOENCODING=utf-8 in _build_bypass_env too (matches _build_safe_env, so the bypass path's child also emits utf-8) The child is python with PYTHONIOENCODING=utf-8, so it emits UTF-8 regardless of the console code page and the decode is always correct. Shell execution via cmd.exe has a separate console-code-page story and is left to a follow-up. Refs #6489 * Scope Python exec UTF-8 env to Python tool * Make bash bypass test robust to a host-set PYTHONIOENCODING for PR #6548 Bypass mode preserves benign host env vars, so a host-set PYTHONIOENCODING was inherited into the bash bypass env and tripped the new assertion even though _bash_exec never adds it. Clear it in the test so the assertion checks _bash_exec, not the runner environment. --------- Co-authored-by: Lee Jackson <130007945+Imagineer99@users.noreply.github.com> Co-authored-by: Daniel Han <danielhanchen@gmail.com>
1 parent c976174 commit 7ecbf5a

3 files changed

Lines changed: 49 additions & 2 deletions

File tree

studio/backend/core/inference/tools.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2545,14 +2545,24 @@ def _python_exec(
25452545
pass
25462546
try:
25472547
fd, tmp_path = tempfile.mkstemp(suffix = ".py", prefix = "studio_exec_", dir = workdir)
2548-
with os.fdopen(fd, "w") as f:
2548+
# utf-8 so non-ASCII in model-written code survives the OS default codec
2549+
# (Windows cp1252 would otherwise raise UnicodeEncodeError).
2550+
with os.fdopen(fd, "w", encoding = "utf-8") as f:
25492551
f.write(code)
25502552

25512553
safe_env = _build_bypass_env(workdir) if disable_sandbox else _build_safe_env(workdir)
2554+
if disable_sandbox:
2555+
# Match the sandboxed Python path without changing bypass shell I/O.
2556+
safe_env = dict(safe_env)
2557+
safe_env["PYTHONIOENCODING"] = "utf-8"
25522558
popen_kwargs = dict(
25532559
stdout = subprocess.PIPE,
25542560
stderr = subprocess.STDOUT,
25552561
text = True,
2562+
# Decode child output as utf-8 (it emits utf-8 via PYTHONIOENCODING);
2563+
# replace so non-ASCII output never crashes the read on Windows.
2564+
encoding = "utf-8",
2565+
errors = "replace",
25562566
cwd = workdir,
25572567
env = safe_env,
25582568
)

studio/backend/tests/test_bypass_permissions.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,7 @@ def test_python_bypass_uses_bypass_preexec_and_bypass_env(captured_popen, monkey
135135
assert captured_popen["kwargs"]["preexec_fn"] is tools._bypass_preexec
136136
env = captured_popen["kwargs"]["env"]
137137
assert env.get("HOSTVAR") == "benign-xyz"
138+
assert env.get("PYTHONIOENCODING") == "utf-8"
138139
assert "HF_TOKEN" not in env
139140

140141

@@ -151,9 +152,12 @@ def test_bash_blocklist_skipped_when_bypassed(captured_popen):
151152

152153

153154
@_POSIX_ONLY
154-
def test_bash_bypass_uses_bypass_preexec(captured_popen):
155+
def test_bash_bypass_uses_bypass_preexec(captured_popen, monkeypatch):
156+
# bypass inherits benign host vars; clear so we assert _bash_exec adds none.
157+
monkeypatch.delenv("PYTHONIOENCODING", raising = False)
155158
_bash_exec("echo hi", None, 5, "t", disable_sandbox = True)
156159
assert captured_popen["kwargs"]["preexec_fn"] is tools._bypass_preexec
160+
assert "PYTHONIOENCODING" not in captured_popen["kwargs"]["env"]
157161

158162

159163
# ── real end-to-end python execution under bypass ───────────────────
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# SPDX-License-Identifier: AGPL-3.0-only
2+
# Copyright 2026-present the Unsloth AI Inc. team. All rights reserved.
3+
4+
"""_python_exec must round-trip non-ASCII output end to end.
5+
6+
Model-written code routinely contains non-ASCII (arrows, CJK, emoji). The temp
7+
script and the child's stdout pipe both have to be UTF-8 or it crashes/garbles
8+
on Windows, whose default codec is cp1252. Mirrors the report in
9+
unslothai/unsloth#6489. The child is ``python`` with PYTHONIOENCODING=utf-8, so
10+
it emits UTF-8 on every OS; this proves the round-trip on a UTF-8 host and
11+
guards against a regression to the OS default codec.
12+
"""
13+
14+
import sys
15+
from pathlib import Path
16+
17+
import pytest
18+
19+
_BACKEND_ROOT = Path(__file__).resolve().parents[1]
20+
if str(_BACKEND_ROOT) not in sys.path:
21+
sys.path.insert(0, str(_BACKEND_ROOT))
22+
23+
from core.inference.tools import _python_exec
24+
25+
# Arrow, em-dash, accent, CJK, check mark, astral-plane emoji -- none encodable
26+
# in cp1252, so the OS default codec would raise on write or read.
27+
_UNICODE = "café — 数字 → ✓ 😀"
28+
29+
30+
@pytest.mark.parametrize("disable_sandbox", [False, True])
31+
def test_python_exec_round_trips_non_ascii(disable_sandbox):
32+
out = _python_exec(f"print({_UNICODE!r})", disable_sandbox = disable_sandbox)
33+
assert _UNICODE in out, repr(out)

0 commit comments

Comments
 (0)