refactor(rust/trezor-thp): more detailed PacketInResult
What changed, and why it matters
This commit refactors how a Trezor hardware wallet's Rust transport layer reports the outcome of receiving a network or USB packet. It replaces a simple success/failure result with a richer set of statuses (accepted, buffer too small, peer error, ignored, failed, route to another channel, etc.) and changes several error-handling paths so that malformed or unexpected packets are now explicitly marked as ignored or failed rather than silently treated as 'nothing happened.' The commit is labeled a refactor with no changelog and no security disclosure. The changes appear to be defensive hardening, but because the patch is partial and the surrounding protocol code is not fully shown, it is not possible to confirm whether it fixes an exploitable vulnerability or merely improves robustness.
Treat as a routine defensive refactor. Reviewers should verify that the new Ignored/Failed classification does not hide errors that callers previously handled via Result, and that the InvalidChecksum exception in host.rs does not allow a corrupted handshake message to proceed. No immediate security response is indicated absent additional context or a vendor advisory.
Security signals we found
Error-handling refactor in protocol packet parsing
Malformed/unexpected packets now explicitly return errors instead of silent no-op
Channel state reset on oversized payload to prevent continuation confusion
Invalid checksums during handshake no longer mark handshake as failed
No changelog and no vendor security disclosure
Evidence from the diff
The change refactors PacketInResult in rust/trezor-thp from a struct with three bool/option fields into a non-exhaustive enum with variants Accepted, EnlargeBuffer, TransportError, Ignored, Failed, Route, and ChannelAllocation. The ChannelIO::packet_in signature changes from Result
Changed components
rust/trezor-thp/src/channel/mod.rsrust/trezor-thp/src/channel/host.rsrust/trezor-thp/examples/host-cli/client.rsrust/trezor-thp/src/error.rsInspect captured patch +257 / −121
diff --git a/rust/trezor-thp/examples/host-cli/client.rs b/rust/trezor-thp/examples/host-cli/client.rs
index 5be25b7c..263f0f04 100644
--- a/rust/trezor-thp/examples/host-cli/client.rs
+++ b/rust/trezor-thp/examples/host-cli/client.rs
@@ -75,10 +75,13 @@ impl<C: ChannelIO> Client<C> {
}
fn read_ack(&mut self, packet_buffer: &[u8]) -> bool {
- self.channel
- .packet_in(packet_buffer, &mut [])
- .unwrap()
- .got_ack()
+ let pir = self
+ .channel
+ .packet_in(packet_buffer, &[])
+ .check_failed()
+ .unwrap();
+ assert!(!pir.got_transport_error());
+ pir.got_ack()
}
pub fn write(&mut self, sid: u8, message_type: u16, message: &[u8]) {
@@ -130,6 +133,7 @@ impl<C: ChannelIO> Client<C> {
message_ready = self
.channel
.packet_in(&sockbuf[..reply_len], recv_buf.as_mut_slice())
+ .check_failed()
.unwrap()
.got_message();
}
diff --git a/rust/trezor-thp/src/channel/host.rs b/rust/trezor-thp/src/channel/host.rs
index e75599d0..ba2513f0 100644
--- a/rust/trezor-thp/src/channel/host.rs
+++ b/rust/trezor-thp/src/channel/host.rs
@@ -204,6 +204,10 @@ impl<C: CredentialStore, B: Backend> ChannelOpen<C, B> {
_ => return Err(Error::unexpected_input()),
})
}
+
+ pub fn channel_id(&self) -> u16 {
+ self.channel.channel_id
+ }
}
impl<C, B> ChannelIO for ChannelOpen<C, B>
@@ -211,36 +215,38 @@ where
C: CredentialStore,
B: Backend,
{
- fn packet_in(
- &mut self,
- packet_buffer: &[u8],
- _receive_buffer: &mut [u8],
- ) -> Result<PacketInResult, Error> {
+ fn packet_in(&mut self, packet_buffer: &[u8], _receive_buffer: &mut [u8]) -> PacketInResult {
let res = self
.channel
- .packet_in(packet_buffer, &mut self.internal_buffer)?;
+ .packet_in(packet_buffer, &mut self.internal_buffer);
+ if let PacketInResult::EnlargeBuffer { buffer_size, .. } = res {
+ log::error!(
+ "[{}] Payload length {} exceeds handshake limit.",
+ self.channel_id(),
+ buffer_size
+ );
+ // Possibly damaged length field, ignore continuations.
+ self.channel.state = ChannelState::Idle;
+ return PacketInResult::ignore(Error::MalformedData);
+ }
if res.got_ack() {
self.zero_internal_buffer();
}
if res.got_message() {
let handled = self.incoming_internal();
- if handled.is_err() {
- self.state = HostHandshakeState::Failed;
+ if let Err(e) = handled {
+ if e != Error::InvalidChecksum {
+ self.state = HostHandshakeState::Failed;
+ return PacketInResult::fail(e);
+ }
}
- handled?;
}
- Ok(res)
+ res
}
fn packet_out(&mut self, packet_buffer: &mut [u8], _send_buffer: &[u8]) -> Result<(), Error> {
self.channel
- .packet_out(packet_buffer, &self.internal_buffer)?;
-
- if self.channel.is_broadcast() && matches!(self.channel.state, ChannelState::Idle) {
- // no ack/retransmits on broadcast
- self.zero_internal_buffer();
- }
- Ok(())
+ .packet_out(packet_buffer, &self.internal_buffer)
}
fn packet_out_ready(&self) -> bool {
@@ -313,11 +319,7 @@ impl<B: Backend> ChannelPairing<B> {
}
impl<B: Backend> ChannelIO for ChannelPairing<B> {
- fn packet_in(
- &mut self,
- packet_buffer: &[u8],
- receive_buffer: &mut [u8],
- ) -> Result<PacketInResult, Error> {
+ fn packet_in(&mut self, packet_buffer: &[u8], receive_buffer: &mut [u8]) -> PacketInResult {
self.channel.packet_in(packet_buffer, receive_buffer)
}
diff --git a/rust/trezor-thp/src/channel/mod.rs b/rust/trezor-thp/src/channel/mod.rs
index bf4b63d0..abe5d4e2 100644
--- a/rust/trezor-thp/src/channel/mod.rs
+++ b/rust/trezor-thp/src/channel/mod.rs
@@ -162,38 +162,97 @@ impl<R: Role, B: Backend> Channel<R, B> {
Ok((header, len))
}
- fn handle_ack(&mut self, packet_buffer: &[u8]) -> Result<PacketInResult> {
- if self.is_broadcast() {
- // Ignore ACKs on broadcast channel.
- return PacketInResult::nothing();
+ fn handle_packet(
+ &mut self,
+ packet_buffer: &[u8],
+ receive_buffer: &mut [u8],
+ ) -> Result<PacketInResult> {
+ let (cb, channel_id, _rest) = parse_cb_channel(packet_buffer)?;
+ if channel_id != self.channel_id {
+ log::warn!(
+ "[{}] Invalid channel {}, ignoring.",
+ self.channel_id,
+ channel_id
+ );
+ return Err(Error::malformed_data());
+ }
+ if cb.is_ack() {
+ self.handle_ack(packet_buffer)?;
+ return Ok(PacketInResult::ack());
+ } else if cb.is_error() {
+ let te = self.handle_error(packet_buffer)?;
+ return Ok(PacketInResult::transport_error(te));
}
+ let is_cont = cb.is_continuation();
+ if !(is_cont
+ || cb.is_channel_allocation_request()
+ || cb.is_channel_allocation_response()
+ || cb.is_handshake()
+ || cb.is_encrypted_transport())
+ {
+ log::warn!(
+ "[{}] Invalid control byte {}.",
+ self.channel_id,
+ u8::from(cb)
+ );
+ return Err(Error::malformed_data());
+ }
+ Ok(match &mut self.state {
+ // First fragment.
+ ChannelState::Receiving(_) | ChannelState::Idle if !is_cont => {
+ let (is_done, enlarge) = self.handle_init(packet_buffer, receive_buffer)?;
+ PacketInResult::accept(is_done).with_buffer(enlarge)
+ }
+ // Continuation fragments.
+ ChannelState::Receiving(r) => {
+ r.update(packet_buffer, receive_buffer)?;
+ PacketInResult::accept(r.is_done())
+ }
+ // Ignore unexpected continuations.
+ ChannelState::Idle => return Err(Error::malformed_data()),
+ // Might possibly happen when we've sent an ACK and it got lost.
+ // We end up sending reply while the other side is retransmitting.
+ // Is this recoverable?
+ ChannelState::Sending(_) => return Err(Error::malformed_data()),
+ ChannelState::Failed(_e) => return Err(Error::unexpected_input()),
+ })
+ }
+
+ fn handle_ack(&mut self, packet_buffer: &[u8]) -> Result<()> {
if matches!(self.state, ChannelState::Sending(_)) {
let sb = SyncBits::try_from(packet_buffer)?;
self.sync.send_mark_delivered(sb);
if self.sync.can_send() {
self.state = ChannelState::Idle;
- return PacketInResult::ack();
+ return Ok(());
}
}
log::warn!("[{}] Unexpected ACK.", self.channel_id);
- PacketInResult::nothing()
+ Err(Error::malformed_data())
}
- fn handle_error(&mut self, packet_buffer: &[u8]) -> Result<PacketInResult> {
+ fn handle_error(&mut self, packet_buffer: &[u8]) -> Result<TransportError> {
let mut err_buf = [0u8; 16];
if let Ok((header, payload)) = Reassembler::<R>::single(packet_buffer, &mut err_buf) {
- if let Ok(te) = TransportError::try_from(payload) {
- if header.is_error() {
+ if header.is_error() {
+ if let Ok(te) = TransportError::try_from(payload) {
log::error!("[{}] Peer sent an error: {}.", self.channel_id, te as u8);
if !te.is_recoverable() {
self.state = ChannelState::Failed(Some(te));
}
- return PacketInResult::transport_error(te);
+ return Ok(te);
+ } else {
+ log::error!(
+ "[{}] Peer sent unknown error {}.",
+ self.channel_id,
+ payload.first().unwrap_or(&0)
+ );
}
}
+ self.state = ChannelState::Failed(None);
+ return Err(Error::malformed_data());
}
- log::error!("[{}] Peer sent unknown error.", self.channel_id);
- self.state = ChannelState::Failed(None);
+ log::warn!("[{}] Peer sent an error with invalid CRC.", self.channel_id);
Err(Error::malformed_data())
}
@@ -201,73 +260,187 @@ impl<R: Role, B: Backend> Channel<R, B> {
&mut self,
packet_buffer: &[u8],
receive_buffer: &mut [u8],
- ) -> Result<PacketInResult> {
+ ) -> Result<(bool, Option<u16>)> {
let sb = SyncBits::try_from(packet_buffer)?;
if !self.is_broadcast() && !self.sync.receive_start(sb) {
// Bad sync bit, drop this packet and continuations.
log::debug!("[{}] Bad sync bit, ignoring packet.", self.channel_id);
self.state = ChannelState::Idle;
- return PacketInResult::nothing();
+ return Err(Error::malformed_data());
}
receive_buffer.fill(0);
let r = Reassembler::new(packet_buffer, receive_buffer)?;
let is_done = r.is_done();
+ let payload_len = r.header().payload_len();
+ let enlarge = (usize::from(payload_len) > receive_buffer.len()).then_some(payload_len);
self.state = ChannelState::Receiving(r);
- PacketInResult::message(is_done)
+ Ok((is_done, enlarge))
}
}
/// Whether channel state changed after calling [`ChannelIO::packet_in`].
-pub struct PacketInResult {
- ack_received: bool,
- message_ready: bool,
- error: Option<TransportError>,
- // enlarge_buffer: Option<usize>,
+#[cfg_attr(any(test, debug_assertions), derive(Debug))]
+#[derive(PartialEq, Eq)]
+#[non_exhaustive]
+pub enum PacketInResult {
+ /// Channel ingested the packet and updated its state.
+ Accepted {
+ /// True if the packet contained valid ACK and channel is ready to send next message.
+ ack_received: bool,
+ /// If true the event loop should schedule calling [`ChannelIO::message_out`].
+ message_ready: bool,
+ /// True if the packet was a valid keep-alive reply ("PONG") message.
+ pong: bool,
+ },
+ /// Channel ingested the packet and started reassembling a message that is larger
+ /// than the current receive buffer. Resize it (keep the initial part) or destroy the channel.
+ EnlargeBuffer {
+ /// True if the packet contained valid ACK and channel is ready to send next message.
+ /// Reserved for ACK piggybacking.
+ ack_received: bool,
+ /// Message size including checksum, the minimum new size of receive buffer.
+ buffer_size: u16,
+ },
+ /// Peer sent a `TRANSPORT_ERROR` message.
+ TransportError {
+ /// True if the packet contained valid ACK and channel is ready to send next message.
+ /// Reserved for ACK piggybacking.
+ ack_received: bool,
+ /// Error sent by the peer.
+ error: TransportError,
+ },
+ /// Channel cannot process the packet, possibly because it was damaged in transit.
+ /// Channel remains usable after this error.
+ Ignored { error: Error },
+ /// Channel became inoperable due to this packet. Event loop should destroy it.
+ Failed { error: Error },
+ /// This packet is addressed to different channel. Event loop should look up the channel
+ /// by its ID and call its [`ChannelIO::packet_in`].
+ /// Only [`device::Mux`] and [`host::Mux`] return this variant.
+ Route {
+ /// Channel id of the destination. Never a broadcast.
+ channel_id: u16,
+ },
+ /// Channel allocation request/response was received. Event loop should call
+ /// [`Mux::channel_alloc`] to create new channel object. Only [`device::Mux`] and[`host::Mux`]
+ /// return this variant. There is no queue, do it before processing the next packet.
+ ChannelAllocation { channel_id: u16 },
}
impl PacketInResult {
- const fn new(ack_received: bool, message_ready: bool) -> Self {
- Self {
- ack_received,
+ const fn accept(message_ready: bool) -> Self {
+ Self::Accepted {
+ ack_received: false,
message_ready,
- error: None,
+ pong: false,
+ }
+ }
+
+ const fn with_buffer(self, enlarge_receive_buffer: Option<u16>) -> Self {
+ match enlarge_receive_buffer {
+ Some(buffer_size) => Self::EnlargeBuffer {
+ ack_received: self.got_ack(),
+ buffer_size,
+ },
+ None => self,
+ }
+ }
+
+ const fn ignore(error: Error) -> Self {
+ Self::Ignored { error }
+ }
+
+ const fn ack() -> Self {
+ Self::Accepted {
+ ack_received: true,
+ message_ready: false,
+ pong: false,
+ }
+ }
+
+ const fn transport_error(e: TransportError) -> Self {
+ Self::TransportError {
+ ack_received: false,
+ error: e,
}
}
- const fn nothing() -> Result<Self> {
- Ok(Self::new(false, false))
+ const fn route(channel_id: u16) -> Self {
+ Self::Route { channel_id }
}
- const fn ack() -> Result<Self> {
- Ok(Self::new(true, false))
+ const fn fail(error: Error) -> Self {
+ Self::Failed { error }
}
- const fn message(is_done: bool) -> Result<Self> {
- Ok(Self::new(false, is_done))
+ const fn channel_allocation(channel_id: u16) -> Self {
+ Self::ChannelAllocation { channel_id }
}
- const fn transport_error(e: TransportError) -> Result<Self> {
- Ok(Self {
+ const fn pong() -> Self {
+ Self::Accepted {
ack_received: false,
message_ready: false,
- error: Some(e),
- })
+ pong: true,
+ }
}
/// True if the received packet was valid ACK.
pub const fn got_ack(&self) -> bool {
- self.ack_received
+ match self {
+ Self::Accepted { ack_received, .. } => *ack_received,
+ Self::EnlargeBuffer { ack_received, .. } => *ack_received,
+ Self::TransportError { ack_received, .. } => *ack_received,
+ _ => false,
+ }
}
/// True if the received packet was the last fragment of incoming message.
/// Event loop should call [`ChannelIO::message_out`]. The message is
/// not guaranteed to be valid.
pub const fn got_message(&self) -> bool {
- self.message_ready
+ matches!(
+ self,
+ Self::Accepted {
+ message_ready: true,
+ ..
+ }
+ )
}
- pub const fn got_error(&self) -> bool {
- self.error.is_some()
+ pub const fn got_channel(&self) -> bool {
+ matches!(self, Self::ChannelAllocation { .. })
+ }
+
+ pub const fn got_pong(&self) -> bool {
+ matches!(self, Self::Accepted { pong: true, .. })
+ }
+
+ pub const fn got_transport_error(&self) -> bool {
+ matches!(self, Self::TransportError { .. })
+ }
+
+ pub fn check_failed(self) -> Result<Self> {
+ if let Self::Failed { error: e } = self {
+ return Err(e);
+ }
+ Ok(self)
+ }
+
+ // Fold Result<PacketInResult> into PacketInResult, separating fatal and nonfatal errors.
+ fn from_result(res: Result<Self>) -> Self {
+ match res {
+ Err(e)
+ if matches!(
+ e,
+ Error::MalformedData | Error::InvalidChecksum | Error::NotReady
+ ) =>
+ {
+ Self::ignore(e)
+ }
+ Err(e) => Self::fail(e),
+ Ok(x) => x,
+ }
}
}
@@ -294,11 +467,7 @@ pub trait ChannelIO {
///
/// If [`PacketInResult::got_message()`] of the returned value evaluates to true the application
/// should call [`ChannelIO::packet_out`].
- fn packet_in(
- &mut self,
- packet_buffer: &[u8],
- receive_buffer: &mut [u8],
- ) -> Result<PacketInResult>;
+ fn packet_in(&mut self, packet_buffer: &[u8], receive_buffer: &mut [u8]) -> PacketInResult;
/// Is channel ready to accept incoming packet?
///
@@ -382,60 +551,15 @@ pub trait ChannelIO {
}
impl<R: Role, B: Backend> ChannelIO for Channel<R, B> {
- fn packet_in(
- &mut self,
- packet_buffer: &[u8],
- receive_buffer: &mut [u8],
- ) -> Result<PacketInResult> {
+ fn packet_in(&mut self, packet_buffer: &[u8], receive_buffer: &mut [u8]) -> PacketInResult {
if let ChannelState::Failed(_e) = self.state {
- return PacketInResult::nothing();
+ return PacketInResult::fail(Error::UnexpectedInput);
}
- let (cb, channel_id, _rest) = parse_cb_channel(packet_buffer)?;
- if channel_id != self.channel_id {
- log::warn!(
- "[{}] Invalid channel {}, ignoring.",
- self.channel_id,
- channel_id
- );
- return PacketInResult::nothing();
- }
- if cb.is_ack() {
- return self.handle_ack(packet_buffer);
- } else if cb.is_error() {
- return self.handle_error(packet_buffer);
- }
- let is_cont = cb.is_continuation();
- if !(is_cont
- || cb.is_channel_allocation_request()
- || cb.is_channel_allocation_response()
- || cb.is_handshake()
- || cb.is_encrypted_transport())
- {
- log::warn!(
- "[{}] Invalid control byte {}.",
- self.channel_id,
- u8::from(cb)
- );
- return PacketInResult::nothing();
- }
- match &mut self.state {
- // First fragment.
- ChannelState::Receiving(_) | ChannelState::Idle if !is_cont => {
- self.handle_init(packet_buffer, receive_buffer)
- }
- // Continuation fragments.
- ChannelState::Receiving(r) => {
- r.update(packet_buffer, receive_buffer)?;
- PacketInResult::message(r.is_done())
- }
- // Ignore unexpected continuations.
- ChannelState::Idle => PacketInResult::nothing(),
- // Might possibly happen when we've sent an ACK and it got lost.
- // We end up sending reply while the other side is retransmitting.
- // Is this recoverable?
- ChannelState::Sending(_) => PacketInResult::nothing(),
- ChannelState::Failed(_e) => unreachable!(),
+ let res = PacketInResult::from_result(self.handle_packet(packet_buffer, receive_buffer));
+ if let PacketInResult::Failed { .. } = res {
+ self.state = ChannelState::Failed(None);
}
+ res
}
fn packet_out(&mut self, packet_buffer: &mut [u8], send_buffer: &[u8]) -> Result<()> {
diff --git a/rust/trezor-thp/src/error.rs b/rust/trezor-thp/src/error.rs
index b80a3bfb..59aa2b97 100644
--- a/rust/trezor-thp/src/error.rs
+++ b/rust/trezor-thp/src/error.rs
@@ -27,6 +27,12 @@ impl TransportError {
}
}
+impl From<TransportError> for u8 {
+ fn from(val: TransportError) -> u8 {
+ val as u8
+ }
+}
+
impl TryFrom<u8> for TransportError {
type Error = Error;
@@ -52,7 +58,7 @@ impl TryFrom<&[u8]> for TransportError {
}
#[cfg_attr(any(test, debug_assertions), derive(Debug))]
-#[derive(PartialEq, Eq)]
+#[derive(Clone, Copy, PartialEq, Eq)]
pub enum Error {
/// Invalid data/operation from crate user.
UnexpectedInput,
Why this scored 27/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.