fix(usb): consume pending PM3 and PM5 commands

Consume zero-length OUT packets and recognize commands already buffered by
usb_read_ng on AT91 and AT32. Reuse each platform's receive cleanup to
acknowledge empty packets and rearm reception without changing frame parsing.

Cover coalesced commands and endpoint boundaries with the shared hardware
regression. Both platforms pass 1,800 pings; PM5 also passes the previously
stalling 64-byte command followed by a normal ping.

Co-Authored-By: gpt-daybreak-blue <noreply@openai.com>
This commit is contained in:
CinderSocket
2026-09-15 04:25:50 -07:00
co-authored by gpt-daybreak-blue
parent a528d5ac1b
commit 495bc201d8
3 changed files with 108 additions and 4 deletions
+14 -1
View File
@@ -431,10 +431,23 @@ uint16_t usb_available_length(void) {
* @return
*/
bool usb_poll_validate_length(void) {
#ifndef AS_BOOTROM
// A previous USB packet may already contain the next command.
if (usb_read_ng_has_buffered_data()) {
return true;
}
#endif
if (usb_poll() == false) {
return false;
}
return usb_available_length() > 0;
if (usb_available_length() > 0) {
return true;
}
// Consume a terminating zero-length packet and rearm the OUT endpoint.
// Otherwise no subsequent command can be received.
usb_vcp_get_rxdata(udev, NULL, 0);
return false;
}
/**
+17 -3
View File
@@ -184,6 +184,7 @@ static uint16_t usb_read_ng_data_available(void) {
static uint8_t usb_read_ng_data_read(void) {
return pUdp->UDP_FDR[AT91C_EP_OUT];
}
#endif
// Implemented for read_ng
static void usb_read_ng_clear(void) {
@@ -196,6 +197,7 @@ static void usb_read_ng_clear(void) {
}
}
#ifndef AS_BOOTROM
// Instance for 'read_ng' apis
static const usb_read_ng_config_t g_usb_read_ng_config = {
.is_link_ready = usb_read_ng_link_ready,
@@ -372,13 +374,25 @@ FORCE_INLINE uint16_t usb_available_length(void) {
bug.
**/
bool usb_poll_validate_length(void) {
#ifndef AS_BOOTROM
// A previous USB packet may already contain the next command.
if (usb_read_ng_has_buffered_data()) {
return true;
}
#endif
// Reuse 'usb_poll()' implemented.
if (usb_poll() == false) {
return false;
}
// Why code this: return (((pUdp->UDP_CSR[AT91C_EP_OUT] & AT91C_UDP_RXBYTECNT) >> 16) > 0);
// For speed? but 'usb_available_length()' is a inline function.
return (usb_available_length() > 0);
if (usb_available_length() > 0) {
return true;
}
// A transfer whose size is an exact multiple of the endpoint size can end
// with a zero-length packet. Acknowledge it here; otherwise this receive
// bank remains selected forever and data queued in the other bank stalls.
usb_read_ng_clear();
return false;
}
/*
+77
View File
@@ -0,0 +1,77 @@
#!/usr/bin/env python3
# Copyright (C) Proxmark3 contributors. See AUTHORS.md for details.
# SPDX-License-Identifier: GPL-3.0-or-later
"""Test USB packet boundaries and buffered commands on a connected PM3 (POSIX)."""
import argparse
import os
import select
import struct
import termios
import time
import tty
def ping(payload):
return struct.pack('<IHH', 0x61334D50, 0x8000 | len(payload), 0x0109) + payload + b'a3'
def read_exact(port, length, deadline):
data = bytearray()
while len(data) < length:
remaining = deadline - time.monotonic()
if remaining <= 0 or not select.select([port], [], [], remaining)[0]:
raise RuntimeError(f'reply timed out after {len(data)}/{length} bytes')
data.extend(os.read(port, length - len(data)))
return data
def check_pair(port, first, second):
# One write can place both commands in one USB packet. Neither response may
# depend on a subsequent write waking a command left in the receive buffer.
pending = ping(first) + ping(second)
while pending:
if not select.select([], [port], [], 2)[1]:
raise RuntimeError('write timed out')
pending = pending[os.write(port, pending):]
deadline = time.monotonic() + 2
for expected in (first, second):
header = read_exact(port, 10, deadline)
magic, length, status, _, command = struct.unpack('<IHbbH', header)
if (magic, length, status, command) != (0x62334D50, 0x8000 | len(expected), 0, 0x0109):
raise RuntimeError(f'unexpected reply header: {header.hex()}')
reply = read_exact(port, len(expected) + 2, deadline)
if reply != expected + b'b3':
raise RuntimeError(f'ping content mismatch: {reply.hex()}')
def main():
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument('--port', required=True)
parser.add_argument('--rounds', type=int, default=100)
args = parser.parse_args()
if args.rounds < 1:
parser.error('--rounds must be positive')
port = os.open(args.port, os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK)
saved = termios.tcgetattr(port)
try:
tty.setraw(port)
config = termios.tcgetattr(port)
config[2] |= termios.CLOCAL | termios.CREAD
config[2] &= ~getattr(termios, 'CRTSCTS', 0)
config[4] = config[5] = termios.B115200
termios.tcsetattr(port, termios.TCSANOW, config)
termios.tcflush(port, termios.TCIOFLUSH)
lengths = (0, 1, 53, 54, 55, 117, 118, 119, 512)
for iteration in range(args.rounds):
for length in lengths:
payload = bytes((iteration + i) & 255 for i in range(length))
check_pair(port, payload, bytes([iteration & 255]))
print(f'PASS: {args.rounds * len(lengths) * 2} pings, including coalesced commands')
finally:
termios.tcsetattr(port, termios.TCSANOW, saved)
os.close(port)
if __name__ == '__main__':
main()