refactor(core/rust): add TryFrom<Option<T>> for Obj
What changed, and why it matters
This is a small internal code cleanup in the Trezor firmware's Rust code. It replaces a custom helper function with a standard Rust conversion trait so that optional values can be converted to MicroPython objects in a more idiomatic way. The change itself does not fix a known bug or add a security feature; it is a refactoring that may make future errors slightly harder to miss.
No immediate action required. Treat as routine refactoring. If reviewing for security, verify that all fallible `Option<T>` to `Obj` conversions now correctly propagate errors and that no caller ignores the new `Result`.
Security signals we found
Refactoring of object conversion traits in Rust/MicroPython bridge
Propagation of conversion errors via TryFrom instead of infallible From
Removal of custom helper `Obj::from_option`
Changes in THP host-channel info and pairing functions
Changes in UI layout message handling
Evidence from the diff
The commit refactors Option<T> conversion into MicroPython Obj. Previously there was a fallible Obj::from_option<T>() helper and an infallible From<Option<T>> for Obj implementation. The patch removes the helper and changes the From impl to a TryFrom impl, so Option<T>::try_into() can be used directly. Call sites in THP (Trezor Host Protocol), UI layout, and the Eckhart layout are updated to use try_into()?. This makes conversion failures propagate as errors instead of being silently accepted via the old From impl, but the old From impl already required T: Into<Obj> (infallible), so the behavioral change is limited to cases where T is fallible. The patch is partial in that some call sites still use .into() where the type remains infallible.
Changed components
core/embed/rust/src/micropython/obj.rscore/embed/rust/src/thp/micropython.rscore/embed/rust/src/ui/layout/obj.rscore/embed/rust/src/ui/layout_eckhart/component_msg_obj.rsInspect captured patch +15 / −22
diff --git a/core/embed/rust/src/micropython/obj.rs b/core/embed/rust/src/micropython/obj.rs
index 23d771ef..99e488a3 100644
--- a/core/embed/rust/src/micropython/obj.rs
+++ b/core/embed/rust/src/micropython/obj.rs
@@ -430,14 +430,17 @@ impl TryFrom<Obj> for usize {
}
}
-impl<T> From<Option<T>> for Obj
+impl<T, E> TryFrom<Option<T>> for Obj
where
- T: Into<Obj>,
+ T: TryInto<Obj, Error = E>,
+ E: Into<Error>,
{
- fn from(val: Option<T>) -> Self {
+ type Error = Error;
+
+ fn try_from(val: Option<T>) -> Result<Self, Error> {
match val {
- Some(v) => v.into(),
- None => Self::const_none(),
+ Some(v) => v.try_into().map_err(|e| e.into()),
+ None => Ok(Self::const_none()),
}
}
}
@@ -457,16 +460,6 @@ impl Obj {
Err(e) => Err(e.into()),
}
}
-
- pub fn from_option<T>(val: Option<T>) -> Result<Self, Error>
- where
- T: TryInto<Obj, Error = Error>,
- {
- match val {
- Some(v) => v.try_into(),
- None => Ok(Self::const_none()),
- }
- }
}
impl Obj {
diff --git a/core/embed/rust/src/thp/micropython.rs b/core/embed/rust/src/thp/micropython.rs
index 21a76d68..707f7d7e 100644
--- a/core/embed/rust/src/thp/micropython.rs
+++ b/core/embed/rust/src/thp/micropython.rs
@@ -186,12 +186,12 @@ extern "C" fn thp_channel_info(channel_id: Obj) -> Obj {
let credential = {
let aux = THP_AUX.try_lock().ok_or(CANNOT_UNLOCK)?;
- Obj::from_option(aux.get_credential(channel_id))?
+ aux.get_credential(channel_id).try_into()?
};
attr_tuple! {
- Qstr::MP_QSTR_last_write => Obj::from_option(last_write_age_ms)?,
- Qstr::MP_QSTR_pairing_state => pairing_state.into(),
+ Qstr::MP_QSTR_last_write => last_write_age_ms.try_into()?,
+ Qstr::MP_QSTR_pairing_state => pairing_state.try_into()?,
Qstr::MP_QSTR_handshake_hash => hash.try_into()?,
Qstr::MP_QSTR_host_static_public_key => remote_static_pubkey.try_into()?,
Qstr::MP_QSTR_credential => credential,
@@ -207,7 +207,7 @@ extern "C" fn thp_channel_paired(channel_id: Obj) -> Obj {
let mut thp = THP_CONTEXT.try_lock().ok_or(CANNOT_UNLOCK)?;
let replaced_channel_id = thp.channel_paired(channel_id)?;
- Ok(replaced_channel_id.into())
+ replaced_channel_id.try_into()
};
unsafe { util::try_or_raise(block) }
diff --git a/core/embed/rust/src/ui/layout/obj.rs b/core/embed/rust/src/ui/layout/obj.rs
index 68b7819c..248348fb 100644
--- a/core/embed/rust/src/ui/layout/obj.rs
+++ b/core/embed/rust/src/ui/layout/obj.rs
@@ -279,7 +279,7 @@ impl LayoutObjInner {
let msg = root.event(&mut self.event_ctx, event);
match msg {
- Some(LayoutState::Done) => return Ok(msg.into()), // short-circuit
+ Some(LayoutState::Done) => return msg.try_into(), // short-circuit
Some(LayoutState::Attached(br)) => {
assert!(self.button_request.is_none());
self.button_request = br;
@@ -319,7 +319,7 @@ impl LayoutObjInner {
self.page_count = count;
}
- Ok(msg.into())
+ msg.try_into()
}
/// Run a paint pass over the component tree. Returns true if any component
diff --git a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
index 5b7691da..171fa845 100644
--- a/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/component_msg_obj.rs
@@ -171,7 +171,7 @@ impl ComponentMsgObj for DeviceMenuScreen {
new_tuple(&[
action_obj,
result_obj,
- next_menu_id.to_u8().into(),
+ next_menu_id.to_u8().try_into()?,
vertical_offset.into(),
])
}
Why this scored 18/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.