From 7985f06946d93d9fa083f1d2dfb690a1d2d1e556 Mon Sep 17 00:00:00 2001 From: Dmitry Ilyin Date: Fri, 15 May 2026 15:23:58 +0300 Subject: [PATCH] agent: surface ACK_FLASH_ERROR instead of returning empty bytes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `FlashAgentClient.read_memory` previously broke its accumulator loop on any RSP_ACK and returned whatever the buffer held — `b""` when the agent rejected the address (e.g. a probe outside the V3+/V4+ I/O whitelist in `agent/main.c:addr_readable`). Callers couldn't tell a successful empty read from a rejection. Same shape in `FlashAgentClient.crc32`, where a clean RSP_ACK rejection was reported as a generic "CRC32 response invalid". Now both inspect the ACK status byte and raise a RuntimeError naming the status, the address, and the size — surfaced while probing eMMC controller MMIO at 0x10100000 on hi3516av300 returned empty bytes silently for ~30s. Also adds the missing `ACK_FLASH_ERROR = 0x02` to the Python protocol constants (already in agent/protocol.h) and updates the existing membw rejection test to use the constant instead of the 0x02 literal. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/defib/agent/client.py | 15 ++++++++++++++ src/defib/agent/protocol.py | 3 ++- tests/test_agent_protocol.py | 39 +++++++++++++++++++++++++++++++++++- 3 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/defib/agent/client.py b/src/defib/agent/client.py index a07f214..7557e63 100644 --- a/src/defib/agent/client.py +++ b/src/defib/agent/client.py @@ -15,6 +15,7 @@ from typing import Callable from defib.agent.protocol import ( + ACK_FLASH_ERROR, ACK_OK, CMD_CRC32, CMD_ERASE, @@ -320,6 +321,13 @@ async def read_memory( if on_progress: on_progress(len(received), size) elif cmd == RSP_ACK: + status = data[0] if data else 0 + if status != ACK_OK: + detail = "outside agent's readable whitelist or flash read failed" if status == ACK_FLASH_ERROR else "agent error" + raise RuntimeError( + f"Read rejected by agent: status=0x{status:02x} " + f"({detail}); addr={addr:#010x} size={size}" + ) break else: raise RuntimeError(f"Unexpected response: cmd=0x{cmd:02x}") @@ -627,6 +635,13 @@ async def crc32(self, addr: int, size: int) -> int: # chips (DMA reads, multi-MB/s) still get plenty of headroom. timeout = max(15.0, 5.0 + size / (100 * 1024)) cmd, data = await recv_response(self._transport, timeout=timeout) + if cmd == RSP_ACK: + status = data[0] if data else 0 + detail = "outside agent's readable whitelist or flash read failed" if status == ACK_FLASH_ERROR else "agent error" + raise RuntimeError( + f"CRC32 rejected by agent: status=0x{status:02x} " + f"({detail}); addr={addr:#010x} size={size}" + ) if cmd != RSP_CRC32 or len(data) < 4: raise RuntimeError("CRC32 response invalid") return int(struct.unpack("