fix(lf): flush stale input before bulk download (desync -> garbage/DEADBEEF)

A real download came back as BigBuf memory (0xDEADBEEF poison) with a PM3 response
magic (0x62334d50) embedded — the classic signature of a desynced read: the stream
had stale bytes (a prior partial transaction, or the C client having touched the
port), and the bulk reader consumes fixed 544-byte OLD frames that carry no magic to
resync on, so once misaligned it stays misaligned and reads memory as "samples".

transport.download_bulk now flushes the input first (reset_input_buffer + drain the
asyncio StreamReader until an 80ms quiet gap), so the transfer always begins byte-
aligned. Drains no-op when disconnected. Test asserts the flush runs before the request.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
michael
2026-07-14 16:52:22 -07:00
parent 81625a0e93
commit f09946c4a3
2 changed files with 47 additions and 3 deletions

View File

@@ -133,3 +133,21 @@ def test_download_bulk_uses_oldarg_length_not_padding():
t = _transport_with([_bigbuf_frame(0, b"\xAA\xBB\xCC"), _ack()])
got = _run(t.download_bulk(Cmd.DOWNLOAD_BIGBUF, 0, 3, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK))
assert got == b"\xAA\xBB\xCC" # trimmed to oldarg length, not 512 of padding
def test_download_bulk_drains_stale_input_first():
# a bulk download must flush stale bytes before sending, so OLD frames start byte-aligned
payload = bytes(range(100))
t = _transport_with([_bigbuf_frame(0, payload), _ack()])
calls = []
async def fake_drain(quiet=0.08):
calls.append("drain")
return 7
orig_send = t.send_frame
async def tracking_send(frame):
calls.append("send")
return await orig_send(frame)
t._drain_input = fake_drain
t.send_frame = tracking_send
_run(t.download_bulk(Cmd.DOWNLOAD_BIGBUF, 0, 100, Cmd.DOWNLOADED_BIGBUF, Cmd.ACK))
assert calls[0] == "drain" and calls[1] == "send" # drained BEFORE the request went out