refactor(core): enforce layout scoping for keyboards and select_menu
What changed, and why it matters
This is a code cleanup change in the Trezor hardware wallet's user-interface layer. It introduces a new 'LayoutContext' wrapper so that on-screen keyboards and menus are always used inside a 'with' block, ensuring their memory is released promptly. The commit itself does not claim to fix a security bug, but it references an internal issue (#6811) about lifetime management, suggesting it is a defensive step against possible UI object lifetime problems.
Treat as a routine defensive refactor. Review whether issue #6811 documents a concrete lifetime bug and verify the remaining LayoutObj __del__/__enter__ compatibility shims are removed once the migration is complete. No urgent action required based solely on this diff.
Security signals we found
Resource lifetime / use-after-free hardening
Refactoring tied to internal issue #6811 (lifetime management)
No changelog entry, but commit message frames change as defensive refactor
No explicit vulnerability or CVE mentioned
Evidence from the diff
The patch refactors Rust/MicroPython UI bindings. Functions such as request_bip39, request_slip39, and select_menu now return a LayoutContext instead of a LayoutObj. LayoutContext is usable only as a context manager (yielding a LayoutObj), which forces callers to scope the lifetime of the underlying Rust layout component. The Python recovery flows for four device layouts (bolt, caesar, delizia, eckhart) are updated to use ‘with ctx as obj’. The change is described as gradual and includes a TODO to remove legacy del/enter on LayoutObj once issue #6811 is resolved.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/mocks/generated/trezorui_api.pyicore/src/trezor/ui/layouts/bolt/recovery.pycore/src/trezor/ui/layouts/caesar/recovery.pycore/src/trezor/ui/layouts/delizia/recovery.pycore/src/trezor/ui/layouts/eckhart/recovery.pyInspect captured patch +44 / −22
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index dd6db480..37d741fe 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -1447,9 +1447,20 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// def return_value(self) -> T:
/// """Retrieve the return value of the layout object."""
///
+ /// # TODO: remove after https://github.com/trezor/trezor-firmware/issues/6811 is resolved.
/// def __del__(self) -> None:
/// """Calls drop on contents of the root component."""
///
+ /// # TODO: remove after https://github.com/trezor/trezor-firmware/issues/6811 is resolved.
+ /// def __enter__(self) -> LayoutObj[T]:
+ /// """Enters a context manager (checking the root component is not dropped)."""
+ ///
+ /// def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ /// """Exits a context manager (dropping the root component)."""
+ ///
+ /// class LayoutContext(Generic[T]):
+ /// """Scopes the lifetime of a Rust-based layout object."""
+ ///
/// def __enter__(self) -> LayoutObj[T]:
/// """Enters a context manager (checking the root component is not dropped)."""
///
@@ -1792,7 +1803,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// prompt: str,
/// prefill_word: str,
/// can_go_back: bool,
- /// ) -> LayoutObj[str]:
+ /// ) -> LayoutContext[str]:
/// """BIP39 word input keyboard."""
Qstr::MP_QSTR_request_bip39 => obj_fn_kw!(0, new_request_bip39).as_obj(),
@@ -1801,7 +1812,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// prompt: str,
/// prefill_word: str,
/// can_go_back: bool,
- /// ) -> LayoutObj[str]:
+ /// ) -> LayoutContext[str]:
/// """SLIP39 word input keyboard."""
Qstr::MP_QSTR_request_slip39 => obj_fn_kw!(0, new_request_slip39).as_obj(),
@@ -1864,7 +1875,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// items: Iterable[str],
/// current: int,
/// cancel: str | None = None
- /// ) -> LayoutObj[int]:
+ /// ) -> LayoutContext[int]:
/// """Select an item from a menu. Returns index in range `0..len(items)`."""
Qstr::MP_QSTR_select_menu => obj_fn_kw!(0, new_select_menu).as_obj(),
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 954d60d0..6d047d20 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -78,8 +78,19 @@ class LayoutObj(Generic[T]):
"""Return the transition type."""
def return_value(self) -> T:
"""Retrieve the return value of the layout object."""
+ # TODO: remove after https://github.com/trezor/trezor-firmware/issues/6811 is resolved.
def __del__(self) -> None:
"""Calls drop on contents of the root component."""
+ # TODO: remove after https://github.com/trezor/trezor-firmware/issues/6811 is resolved.
+ def __enter__(self) -> LayoutObj[T]:
+ """Enters a context manager (checking the root component is not dropped)."""
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
+ """Exits a context manager (dropping the root component)."""
+
+
+# rust/src/ui/api/firmware_micropython.rs
+class LayoutContext(Generic[T]):
+ """Scopes the lifetime of a Rust-based layout object."""
def __enter__(self) -> LayoutObj[T]:
"""Enters a context manager (checking the root component is not dropped)."""
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
@@ -439,7 +450,7 @@ def request_bip39(
prompt: str,
prefill_word: str,
can_go_back: bool,
-) -> LayoutObj[str]:
+) -> LayoutContext[str]:
"""BIP39 word input keyboard."""
@@ -449,7 +460,7 @@ def request_slip39(
prompt: str,
prefill_word: str,
can_go_back: bool,
-) -> LayoutObj[str]:
+) -> LayoutContext[str]:
"""SLIP39 word input keyboard."""
@@ -518,7 +529,7 @@ def select_menu(
items: Iterable[str],
current: int,
cancel: str | None = None
-) -> LayoutObj[int]:
+) -> LayoutContext[int]:
"""Select an item from a menu. Returns index in range `0..len(items)`."""
diff --git a/core/src/trezor/ui/layouts/bolt/recovery.py b/core/src/trezor/ui/layouts/bolt/recovery.py
index 949ab63f..22156da8 100644
--- a/core/src/trezor/ui/layouts/bolt/recovery.py
+++ b/core/src/trezor/ui/layouts/bolt/recovery.py
@@ -37,17 +37,17 @@ async def request_word(
can_go_back = word_index > 0
if is_slip39:
- keyboard = trezorui_api.request_slip39(
+ ctx = trezorui_api.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
else:
- keyboard = trezorui_api.request_bip39(
+ ctx = trezorui_api.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
- with keyboard:
+ with ctx as obj:
return await interact(
- keyboard,
+ obj,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
diff --git a/core/src/trezor/ui/layouts/caesar/recovery.py b/core/src/trezor/ui/layouts/caesar/recovery.py
index ac3b2253..a5809485 100644
--- a/core/src/trezor/ui/layouts/caesar/recovery.py
+++ b/core/src/trezor/ui/layouts/caesar/recovery.py
@@ -43,17 +43,17 @@ async def request_word(
can_go_back = word_index > 0
if is_slip39:
- keyboard = trezorui_api.request_slip39(
+ ctx = trezorui_api.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
else:
- keyboard = trezorui_api.request_bip39(
+ ctx = trezorui_api.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
- with keyboard:
+ with ctx as obj:
return await interact(
- keyboard,
+ obj,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
diff --git a/core/src/trezor/ui/layouts/delizia/recovery.py b/core/src/trezor/ui/layouts/delizia/recovery.py
index 5255dcc0..9c22b030 100644
--- a/core/src/trezor/ui/layouts/delizia/recovery.py
+++ b/core/src/trezor/ui/layouts/delizia/recovery.py
@@ -38,17 +38,17 @@ async def request_word(
can_go_back = word_index > 0
if is_slip39:
- keyboard = trezorui_api.request_slip39(
+ ctx = trezorui_api.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
else:
- keyboard = trezorui_api.request_bip39(
+ ctx = trezorui_api.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=can_go_back
)
- with keyboard:
+ with ctx as obj:
return await interact(
- keyboard,
+ obj,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
diff --git a/core/src/trezor/ui/layouts/eckhart/recovery.py b/core/src/trezor/ui/layouts/eckhart/recovery.py
index 95852e55..3eb92a7d 100644
--- a/core/src/trezor/ui/layouts/eckhart/recovery.py
+++ b/core/src/trezor/ui/layouts/eckhart/recovery.py
@@ -38,17 +38,17 @@ async def request_word(
) -> str:
prompt = TR.recovery__word_x_of_y_template.format(word_index + 1, word_count)
if is_slip39:
- keyboard = trezorui_api.request_slip39(
+ ctx = trezorui_api.request_slip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=True
)
else:
- keyboard = trezorui_api.request_bip39(
+ ctx = trezorui_api.request_bip39(
prompt=prompt, prefill_word=prefill_word, can_go_back=True
)
- with keyboard:
+ with ctx as obj:
return await interact(
- keyboard,
+ obj,
"mnemonic" if send_button_request else None,
ButtonRequestType.MnemonicInput,
)
Why this scored 23/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.