fix(code-runtime-python): bind the send_done fallback primitives into locals and use a bare except

The done-frame fallback read _os_write/_memoryview/_FALLBACK_DONE_FRAME as module
globals at call time, so a single-line rebind of any of them reopened the
rebind hole the fallback exists to close. Bind them into _run locals before the
program runs, and use a bare except (which catches everything without naming
BaseException, so a rebind of that name cannot defeat the handler). The
transitive-name rebind test now also rebinds _os_write/_memoryview/
_FALLBACK_DONE_FRAME to pin the fallback's immunity.
This commit is contained in:
Chinesezjc
2026-08-31 14:33:31 +08:00
committed by Tianyi Cui
parent 31c3b425bf
commit 4ff050de71
2 changed files with 22 additions and 8 deletions
@@ -986,6 +986,15 @@ async def _run(channel: ProtocolChannel) -> None:
# bound above.
encode_plain_bound = _encode_json_plain
write_encoded_bound = channel.write_encoded
# The fallback primitives are bound into LOCALS here, before the program
# runs, so `send_done`'s except arm does not read module globals at call
# time. This bootstrap is `__main__`, so `__main__._os_write = boom` (or
# `__main__._FALLBACK_DONE_FRAME`, `__main__._memoryview`) would otherwise
# rebind exactly the names the fallback reads, reopening the single-line-
# rebind hole the fallback exists to close.
_os_write_local = _os_write
_memoryview_local = _memoryview
_fallback_frame_local = _FALLBACK_DONE_FRAME
def send_done(payload: dict[str, Any] | str) -> None:
try:
@@ -993,7 +1002,7 @@ async def _run(channel: ProtocolChannel) -> None:
write_encoded_bound(payload)
else:
write_encoded_bound(encode_plain_bound(payload))
except BaseException: # noqa: BLE001 -- a rebind must not cost the done frame
except: # noqa: BLE001, E722 -- a rebind must not cost the done frame; bare except avoids naming BaseException
# `encode_plain_bound`/`write_encoded_bound` are bound callables, but
# their BODIES still resolve transitive module globals at call time —
# `_encode_json_plain` reaches `_dump_scalar`/`_dump_string`/`json.dumps`,
@@ -1002,16 +1011,16 @@ async def _run(channel: ProtocolChannel) -> None:
# `__main__.os = ...`) makes the error-frame encode/write throw AFTER
# the `except` block, which would drop the `done` frame and downgrade a
# settled `exception` verdict to a host-side `worker-exit`. Write a fixed
# literal done frame with the import-time captured `_os_write` and
# `_memoryview` (module-level names captured before model code runs, so
# a one-line rebind cannot change them) so the host still gets a
# verdict. The literal is JSON-valid and newline-terminated; the lock
# literal done frame with the LOCALLY-BOUND `_os_write_local` and
# `_memoryview_local` (captured before the program runs, so a one-line
# rebind of the module global cannot change them) so the host still gets
# a verdict. The literal is JSON-valid and newline-terminated; the lock
# is the channel's, so the write is serialized against any concurrent
# writer.
with channel._write_lock:
view = _memoryview(_FALLBACK_DONE_FRAME)
view = _memoryview_local(_fallback_frame_local)
while view:
view = view[_os_write(channel._fd, view):]
view = view[_os_write_local(channel._fd, view):]
max_value_bytes = int(boot["maxValueBytes"])
done: dict[str, Any] | str
@@ -1488,7 +1488,8 @@ describe('PythonCodeRuntime — programs and bindings', () => {
// `__main__`, so rebinding `__main__._dump_scalar` to a raising function makes
// the error-frame encode throw AFTER the `except` block. `send_done` catches
// that and writes a fixed literal done frame (kind `exception`) with the
// captured `os.write`, so the host still gets a verdict — the run must be an
// LOCALLY-BOUND `_os_write`/`_memoryview`/`_FALLBACK_DONE_FRAME` captured
// before the program runs, so the host still gets a verdict — the run must be an
// `exception`, never a `worker-exit`. The real message is lost (the literal
// carries a fixed `<unrenderable>` text), which is acceptable: the verdict
// outranks the diagnostic detail.
@@ -1500,6 +1501,10 @@ describe('PythonCodeRuntime — programs and bindings', () => {
' raise RuntimeError("hijacked")',
'__main__._dump_scalar = boom',
'__main__.os = boom',
// The fallback must also survive a rebind of its own primitives.
'__main__._os_write = boom',
'__main__._memoryview = boom',
'__main__._FALLBACK_DONE_FRAME = boom',
'raise ValueError("real failure")',
].join('\n'),
bindings: [],