fix(core): adapt THP retransmission timeouts using recent ACK latency
What changed, and why it matters
This commit adjusts how a Trezor hardware wallet waits for acknowledgements during encrypted USB communication. It now measures how long the host computer recently took to reply and adds that estimate to the retry timer. The change is a reliability/performance fix, not a clear security patch, but it touches a sensitive part of the transport protocol that could affect denial-of-service or timing behavior.
Treat as a routine reliability improvement unless paired with a disclosed security advisory. Review whether the latency estimate can be manipulated by a malicious host to force overly long timeouts or to fingerprint device state. Monitor for a follow-up changelog or security note from Trezor.
Security signals we found
Modifies encrypted transport retransmission timeout logic
Adds persistent cache storage for timing-derived value (ack latency)
Clamps latency estimate to prevent integer overflow / excessive delays
Touches THP (Trezor Host Protocol) channel state machine
No changelog entry and no explicit security disclosure in commit message
Evidence from the diff
The patch adds a new per-channel cache field CHANNEL_ACK_LATENCY_MS, records the measured round-trip time after each successful ACK, clamps it between 0 and 800 ms, and adds it to the retransmission timeout in write_encrypted_payload(). Previously the timeout was a fixed backoff starting at ~200 ms. Now it is latency-aware. The change is defensive and could reduce spurious retransmissions or premature stalls, but it does not obviously fix a known vulnerability.
Changed components
core/src/storage/cache_thp.pycore/src/storage/cache_thp_keys.pycore/src/trezor/wire/thp/channel.pyInspect captured patch +13 / −3
diff --git a/core/src/storage/cache_thp.py b/core/src/storage/cache_thp.py
index dd14dbccd..6b5d080d6 100644
--- a/core/src/storage/cache_thp.py
+++ b/core/src/storage/cache_thp.py
@@ -63,6 +63,7 @@ class ChannelCache(ThpDataCache):
8, # CHANNEL_NONCE_RECEIVE
8, # CHANNEL_NONCE_SEND
32, # CHANNEL_HOST_STATIC_PUBKEY
+ 2, # CHANNEL_ACK_LATENCY_MS
)
super().__init__()
self.set_int(CHANNEL_SYNC, 0x80)
diff --git a/core/src/storage/cache_thp_keys.py b/core/src/storage/cache_thp_keys.py
index 1c9aed036..8744a2271 100644
--- a/core/src/storage/cache_thp_keys.py
+++ b/core/src/storage/cache_thp_keys.py
@@ -14,6 +14,7 @@ if utils.USE_THP:
CHANNEL_NONCE_RECEIVE = const(7)
CHANNEL_NONCE_SEND = const(8)
CHANNEL_HOST_STATIC_PUBKEY = const(9)
+ CHANNEL_ACK_LATENCY_MS = const(10)
# Cache keys for THP session
# CHANNEL_ID = const(0)
diff --git a/core/src/trezor/wire/thp/channel.py b/core/src/trezor/wire/thp/channel.py
index 932f3b394..f72d9ff6c 100644
--- a/core/src/trezor/wire/thp/channel.py
+++ b/core/src/trezor/wire/thp/channel.py
@@ -4,6 +4,7 @@ from micropython import const
from typing import TYPE_CHECKING
from storage.cache_common import (
+ CHANNEL_ACK_LATENCY_MS,
CHANNEL_HANDSHAKE_HASH,
CHANNEL_HOST_STATIC_PUBKEY,
CHANNEL_IFACE,
@@ -416,8 +417,9 @@ class Channel:
return self.write_encrypted_payload(ctrl_byte, payload)
async def write_encrypted_payload(self, ctrl_byte: int, payload: AnyBytes) -> None:
+ ack_latency_ms = self.channel_cache.get_int(CHANNEL_ACK_LATENCY_MS) or 0
if __debug__:
- self._log("write_encrypted_payload_loop")
+ self._log(f"Sending {len(payload)} bytes, latency: {ack_latency_ms} ms")
assert ABP.is_sending_allowed(self.channel_cache)
@@ -441,8 +443,8 @@ class Channel:
log.error(__name__, "Sending is stuck for %d ms", _WRITE_TIMEOUT_MS)
break
- # starting from 200ms till ~3.52s
- timeout_ms = round(10300 - 1010000 / (100 + i))
+ # Channel's estimated latency + a variable delay (from 200ms till ~3.52s)
+ timeout_ms = ack_latency_ms + round(10300 - 1010000 / (100 + i))
try:
# wait and return after receiving an ACK, or raise in case of an unexpected message.
await self.recv_payload(expected_ctrl_byte=None, timeout_ms=timeout_ms)
@@ -450,6 +452,12 @@ class Channel:
if __debug__:
log.warning(__name__, "Retransmit after %d ms", timeout_ms)
continue
+
+ ack_latency_ms = utime.ticks_diff(utime.ticks_ms(), self.last_write_ms)
+ # Limit estimated latency to avoid integer overflows and too long delays
+ ack_latency_ms = max(0, min(800, ack_latency_ms))
+ self.channel_cache.set_int(CHANNEL_ACK_LATENCY_MS, ack_latency_ms)
+
# `ABP.set_sending_allowed()` will be called after a valid ACK
if ABP.is_sending_allowed(self.channel_cache):
ABP.set_send_seq_bit_to_opposite(self.channel_cache)
Why this scored 35/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.