Kaynağa Gözat

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.
Chinesezjc 1 ay önce
ebeveyn
işleme
4ff050de71

+ 16 - 7
packages/code-runtime/code-runtime-python/py/bootstrap.py

@@ -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

+ 6 - 1
packages/code-runtime/code-runtime-python/tests/runtime.spec.ts

@@ -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: [],