From 168e613ed7562aaeaca31ad89a5ccdf0162b4a65 Mon Sep 17 00:00:00 2001 From: Matthew Wolter Date: Sun, 12 Apr 2026 04:52:19 -0700 Subject: [PATCH] =?UTF-8?q?G7:=20R03=20=E2=80=94=20pre-register=20binary?= =?UTF-8?q?=20request=20before=20send()=20to=20close=20race=20window?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Why: send_binary_req() registered the pending request with the reader AFTER send() returned. If a BINARY_RESPONSE arrives between send() returning and registration (reachable for TCP-companion proxies), the reader logs "No tracked request found" and the caller's wait_for_event times out. Fix: pre-register a placeholder keyed by object id before send(), then swap it for the real tag from MSG_SENT. On send() failure, the placeholder is cleaned up. Refs: Forensics report finding R03 --- src/meshcore/commands/base.py | 35 ++++++++++++++++++++++++----------- 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/src/meshcore/commands/base.py b/src/meshcore/commands/base.py index 1c0d36e..dcd4673 100644 --- a/src/meshcore/commands/base.py +++ b/src/meshcore/commands/base.py @@ -243,18 +243,31 @@ class CommandHandlerBase: logger.debug(f"Binary request to {dst_bytes.hex()}") data = b"\x32" + dst_bytes + request_type.value.to_bytes(1, "little", signed=False) + (data if data else b"") - result = await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) - - # Register the request with the reader if we have both reader and request_type - if (result.type == EventType.MSG_SENT and - self._reader is not None and - request_type is not None): - - exp_tag = result.payload["expected_ack"].hex() - # Use provided timeout or fallback to suggested timeout (with 5s default) - actual_timeout = timeout if timeout is not None and timeout > 0 else result.payload.get("suggested_timeout", 4000) / 800.0 + # Pre-register a placeholder binary request before send() to close the race + # window where a BINARY_RESPONSE could arrive between send() returning and + # registration. The placeholder tag is patched to the real tag once MSG_SENT + # returns. If send() fails, the placeholder is cleaned up. + placeholder_tag = None + if self._reader is not None and request_type is not None: + placeholder_tag = f"_pending_{id(data)}" + actual_timeout = timeout if timeout is not None and timeout > 0 else self.default_timeout actual_timeout = min_timeout if actual_timeout < min_timeout else actual_timeout - self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context) + self._reader.register_binary_request(pubkey_prefix.hex(), placeholder_tag, request_type, actual_timeout, context=context) + + result = await self.send(data, [EventType.MSG_SENT, EventType.ERROR]) + + # Patch the placeholder tag with the real tag from MSG_SENT, or clean up on failure + if placeholder_tag is not None and self._reader is not None: + # Remove the placeholder entry + self._reader.pending_binary_requests.pop(placeholder_tag, None) + if (result.type == EventType.MSG_SENT and + request_type is not None): + exp_tag = result.payload["expected_ack"].hex() + # Use suggested_timeout from the result if available + actual_timeout = timeout if timeout is not None and timeout > 0 else result.payload.get("suggested_timeout", 4000) / 800.0 + actual_timeout = min_timeout if actual_timeout < min_timeout else actual_timeout + # Register with the real tag + self._reader.register_binary_request(pubkey_prefix.hex(), exp_tag, request_type, actual_timeout, context=context) return result