fix(python): avoid dropping WebUSB chunks in case of a timeout
What changed, and why it matters
This commit fixes a bug in the Python Trezor library's WebUSB communication code. Previously, if a USB read operation timed out but had already received some data, that partial data was silently thrown away. Now the code checks whether any data was received before the timeout and uses it if it is a valid full chunk. This could prevent communication failures or dropped messages when talking to a Trezor device over USB, especially under timing pressure.
Update the trezorlib Python package to a release containing this commit. Host applications using WebUSB should ensure they are not running an older version that discards timeout-partial USB reads, which could cause intermittent communication errors or hung sessions.
Security signals we found
Host-side transport could drop valid USB chunks on timeout
Partial data from timed-out USB reads is now recovered and validated
Communication reliability fix in hardware wallet client library
No device firmware change; only Python host library affected
Evidence from the diff
In python/src/trezorlib/transport/webusb.py, read_chunk() previously caught usb1.USBErrorTimeout and only retried or raised Timeout, ignoring any bytes already transferred. python-libusb1’s USBErrorTimeout exposes a .received attribute containing data returned before timeout. The patch extracts that data, validates it via a new _check_chunk_size helper, and returns it if it forms a complete WEBUSB_CHUNK_SIZE chunk. A separate refactor moves chunk-size validation into the helper for both write_chunk and read_chunk. The fix is defensive and corrects a reliability issue in the host-side transport layer.
Changed components
python/src/trezorlib/transport/webusb.pyTrezor Python client library WebUSB transportInspect captured patch +15 / −6
diff --git a/python/.changelog.d/6112.fixed b/python/.changelog.d/6112.fixed
new file mode 100644
index 000000000..7f14d166a
--- /dev/null
+++ b/python/.changelog.d/6112.fixed
@@ -0,0 +1 @@
+Avoid dropping WebUSB chunks in case of a timeout.
diff --git a/python/src/trezorlib/transport/webusb.py b/python/src/trezorlib/transport/webusb.py
index 0d60723b5..6942ce4bf 100644
--- a/python/src/trezorlib/transport/webusb.py
+++ b/python/src/trezorlib/transport/webusb.py
@@ -136,8 +136,7 @@ class WebUsbTransport(Transport):
def write_chunk(self, chunk: bytes) -> None:
assert self.handle is not None
- if len(chunk) != WEBUSB_CHUNK_SIZE:
- raise TransportException(f"Unexpected chunk size: {len(chunk)}")
+ _check_chunk_size(chunk)
LOG.log(DUMP_PACKETS, f"writing packet: {chunk.hex()}")
while True:
try:
@@ -166,10 +165,13 @@ class WebUsbTransport(Transport):
endpoint, WEBUSB_CHUNK_SIZE, USB_COMM_TIMEOUT_MS
)
LOG.log(DUMP_PACKETS, f"read packet: {chunk.hex()}")
- if len(chunk) != WEBUSB_CHUNK_SIZE:
- raise TransportException(f"Unexpected chunk size: {len(chunk)}")
- return chunk
- except usb1.USBErrorTimeout:
+ return _check_chunk_size(chunk)
+ except usb1.USBErrorTimeout as exc:
+ if exc.received:
+ # `libusb1` may return the received data even in case of a timeout
+ # https://github.com/vpelletier/python-libusb1/blob/292143c8f4465fdcb2c35ed40cdd7e4dd8d031e1/usb1/__init__.py#L1567
+ return _check_chunk_size(exc.received)
+
if timeout is not None and time.time() - start > timeout:
raise Timeout(f"Timeout reading WebUSB packet ({timeout}s)")
except Exception as e:
@@ -183,6 +185,12 @@ class WebUsbTransport(Transport):
return self.handle is not None
+def _check_chunk_size(chunk: bytes) -> bytes:
+ if len(chunk) != WEBUSB_CHUNK_SIZE:
+ raise TransportException(f"Unexpected chunk size: {len(chunk)}")
+ return chunk
+
+
def is_vendor_class(dev: usb1.USBDevice) -> bool:
configurationId = 0
altSettingId = 0
Why this scored 45/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.