PINK: fix ctypes dangling-pointer + venue.submit guard

Two bugs causing INVALID_INTENT_PARSE at FFI boundary:

1. Dangling pointer: ctypes.c_char_p stores a raw C pointer without
   incrementing the Python refcount. Temporaries passed inline are freed
   by CPython before the Rust FFI call executes, giving Rust a dangling
   pointer whose freed memory looks like truncated JSON (column 41).
   Fix: assign bytes to local vars (_pb/_mb/_vb) to hold refs alive.

2. venue.submit guard: process_intent() called venue.submit() even when
   the kernel returned INVALID_INTENT, cascading a 30s BingX timeout
   into a fatal crash. Fix: gate on outcome.accepted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Codex
2026-06-04 16:14:42 +02:00
parent a89e766da1
commit 55197b2047

View File

@@ -231,11 +231,18 @@ class _RustKernelLib:
mode: str, mode: str,
verbosity: str, verbosity: str,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
# Keep local refs so CPython's ref-count doesn't free the bytes objects
# before the Rust FFI call completes. ctypes.c_char_p stores a raw pointer
# without incrementing the Python refcount; a temporary would be freed
# after c_char_p() returns, giving Rust a dangling pointer.
_pb = _to_rust_bytes(payload)
_mb = mode.encode("ascii")
_vb = verbosity.encode("ascii")
raw = self.lib.dita_kernel_process_intent_json( raw = self.lib.dita_kernel_process_intent_json(
handle, handle,
ctypes.c_char_p(_to_rust_bytes(payload)), ctypes.c_char_p(_pb),
ctypes.c_char_p(mode.encode("ascii")), ctypes.c_char_p(_mb),
ctypes.c_char_p(verbosity.encode("ascii")), ctypes.c_char_p(_vb),
) )
return json.loads(self._take_string(raw)) return json.loads(self._take_string(raw))
@@ -247,11 +254,14 @@ class _RustKernelLib:
mode: str, mode: str,
verbosity: str, verbosity: str,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
_pb = _to_rust_bytes(payload)
_mb = mode.encode("ascii")
_vb = verbosity.encode("ascii")
raw = self.lib.dita_kernel_on_venue_event_json( raw = self.lib.dita_kernel_on_venue_event_json(
handle, handle,
ctypes.c_char_p(_to_rust_bytes(payload)), ctypes.c_char_p(_pb),
ctypes.c_char_p(mode.encode("ascii")), ctypes.c_char_p(_mb),
ctypes.c_char_p(verbosity.encode("ascii")), ctypes.c_char_p(_vb),
) )
return json.loads(self._take_string(raw)) return json.loads(self._take_string(raw))
@@ -263,11 +273,14 @@ class _RustKernelLib:
mode: str, mode: str,
verbosity: str, verbosity: str,
) -> Dict[str, Any]: ) -> Dict[str, Any]:
_pb = _to_rust_bytes(list(payload))
_mb = mode.encode("ascii")
_vb = verbosity.encode("ascii")
raw = self.lib.dita_kernel_reconcile_slots_json( raw = self.lib.dita_kernel_reconcile_slots_json(
handle, handle,
ctypes.c_char_p(_to_rust_bytes(list(payload))), ctypes.c_char_p(_pb),
ctypes.c_char_p(mode.encode("ascii")), ctypes.c_char_p(_mb),
ctypes.c_char_p(verbosity.encode("ascii")), ctypes.c_char_p(_vb),
) )
return json.loads(self._take_string(raw)) return json.loads(self._take_string(raw))
@@ -294,7 +307,8 @@ class _RustKernelLib:
def on_account_event( def on_account_event(
self, handle: ctypes.c_void_p, event: Dict[str, Any] self, handle: ctypes.c_void_p, event: Dict[str, Any]
) -> Dict[str, Any]: ) -> Dict[str, Any]:
raw = self.lib.dita_kernel_on_account_event_json(handle, ctypes.c_char_p(_to_rust_bytes(event))) _eb = _to_rust_bytes(event)
raw = self.lib.dita_kernel_on_account_event_json(handle, ctypes.c_char_p(_eb))
if not raw: if not raw:
return {} return {}
return json.loads(self._take_string(raw)) return json.loads(self._take_string(raw))
@@ -761,7 +775,7 @@ class ExecutionKernel:
self._last_settled_pnl[intent.slot_id] = 0.0 self._last_settled_pnl[intent.slot_id] = 0.0
emitted_events = [] emitted_events = []
all_venue_transitions: List[KernelTransition] = [] all_venue_transitions: List[KernelTransition] = []
if intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}: if outcome.accepted and intent.action in {KernelCommandType.ENTER, KernelCommandType.EXIT}:
emitted_events = self.venue.submit(intent) emitted_events = self.venue.submit(intent)
for event in emitted_events: for event in emitted_events:
evt_outcome = self.on_venue_event(event) evt_outcome = self.on_venue_event(event)