fix(core): don't drop layout objects after `draw_simple()`
What changed, and why it matters
This commit fixes a bug in the Trezor hardware wallet's user-interface code. Previously, certain simple on-screen messages were wrapped in a Python `with` block, which caused the underlying layout object to be cleaned up (dropped) as soon as the message was drawn. However, the system still held a reference to that layout and its background event-handling tasks were still running. Dropping it early could lead to a crash, a frozen screen, or undefined behavior while the device is showing important prompts such as asking the user to enter their passphrase on a host computer or displaying a 'please wait' message. The fix removes the `with` wrapper so the layout object stays alive until a new screen replaces it.
Treat as a stability/reliability fix. Review whether the premature drop was reachable in released firmware and whether it could be triggered by user interaction during passphrase or wait-text flows. If a release is pending, include this fix. No immediate user action is required.
Security signals we found
Use-after-free / premature object drop of UI layout with active event-handling tasks
Potential crash or UI freeze during security-sensitive prompts (passphrase entry, wait screens)
Incorrect lifetime management across Rust/MicroPython boundary
No changelog entry suggests low-profile internal fix
Evidence from the diff
The patch changes show_simple() and show_wait_text() in four product-specific layout modules (bolt, caesar, delizia, eckhart) so they no longer use a with trezorui_api.show_...() as layout: context manager. Instead they pass the returned LayoutObj directly to draw_simple(). The Rust micropython API stubs are updated to show these functions returning LayoutObj rather than LayoutContext. A comment is added to draw_simple() explaining that the layout remains referenced by trezor.ui.CURRENT_LAYOUT and its event-handling tasks continue running, so it must not be dropped until a new layout starts. The change prevents premature finalization/collection of a live layout object.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/src/trezor/ui/layouts/common.pycore/src/trezor/ui/layouts/bolt/__init__.pycore/src/trezor/ui/layouts/caesar/__init__.pycore/src/trezor/ui/layouts/delizia/__init__.pycore/src/trezor/ui/layouts/eckhart/__init__.pyInspect captured patch +15 / −28
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index d4060a91..37a430b5 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -2137,7 +2137,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// text: str,
/// title: str | None = None,
/// button: str | None = None,
- /// ) -> LayoutContext[UiResult]:
+ /// ) -> LayoutObj[UiResult]:
/// """Simple dialog with text. TT: optional button."""
Qstr::MP_QSTR_show_simple => obj_fn_kw!(0, new_show_simple).as_obj(),
@@ -2152,7 +2152,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// """Success modal. No buttons shown when `button` is empty string."""
Qstr::MP_QSTR_show_success => obj_fn_kw!(0, new_show_success).as_obj(),
- /// def show_wait_text(message: str, /) -> LayoutContext[None]:
+ /// def show_wait_text(message: str, /) -> LayoutObj[None]:
/// """Show single-line text in the middle of the screen."""
Qstr::MP_QSTR_show_wait_text => obj_fn_1!(new_show_wait_text).as_obj(),
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index e94baf67..7af02aa2 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -811,7 +811,7 @@ def show_simple(
text: str,
title: str | None = None,
button: str | None = None,
-) -> LayoutContext[UiResult]:
+) -> LayoutObj[UiResult]:
"""Simple dialog with text. TT: optional button."""
@@ -828,7 +828,7 @@ def show_success(
# rust/src/ui/api/firmware_micropython.rs
-def show_wait_text(message: str, /) -> LayoutContext[None]:
+def show_wait_text(message: str, /) -> LayoutObj[None]:
"""Show single-line text in the middle of the screen."""
diff --git a/core/src/trezor/ui/layouts/bolt/__init__.py b/core/src/trezor/ui/layouts/bolt/__init__.py
index 9cc3321f..25f3d56d 100644
--- a/core/src/trezor/ui/layouts/bolt/__init__.py
+++ b/core/src/trezor/ui/layouts/bolt/__init__.py
@@ -2189,15 +2189,11 @@ def error_popup(
def request_passphrase_on_host() -> None:
- with trezorui_api.show_simple(
- title=None, text=TR.passphrase__please_enter
- ) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_simple(title=None, text=TR.passphrase__please_enter))
def show_wait_text(message: str) -> None:
- with trezorui_api.show_wait_text(message) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_wait_text(message))
async def request_passphrase_on_device(max_len: int) -> str:
diff --git a/core/src/trezor/ui/layouts/caesar/__init__.py b/core/src/trezor/ui/layouts/caesar/__init__.py
index 9048d4a4..bbd3a68d 100644
--- a/core/src/trezor/ui/layouts/caesar/__init__.py
+++ b/core/src/trezor/ui/layouts/caesar/__init__.py
@@ -2227,15 +2227,11 @@ def error_popup(
def request_passphrase_on_host() -> None:
- with trezorui_api.show_simple(
- title=None, text=TR.passphrase__please_enter
- ) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_simple(title=None, text=TR.passphrase__please_enter))
def show_wait_text(message: str) -> None:
- with trezorui_api.show_wait_text(message) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_wait_text(message))
async def request_passphrase_on_device(max_len: int) -> str:
diff --git a/core/src/trezor/ui/layouts/common.py b/core/src/trezor/ui/layouts/common.py
index 1e115be4..436eb30a 100644
--- a/core/src/trezor/ui/layouts/common.py
+++ b/core/src/trezor/ui/layouts/common.py
@@ -153,4 +153,7 @@ async def confirm_linear_flow(
def draw_simple(layout: trezorui_api.LayoutObj[Any]) -> None:
+ # IMPORTANT: after this call, `layout` is referenced by `trezor.ui.CURRENT_LAYOUT`
+ # and its event-handling tasks are still running. Therefore, it MUST NOT be dropped,
+ # until a new layout is started.
ui.Layout(layout).start()
diff --git a/core/src/trezor/ui/layouts/delizia/__init__.py b/core/src/trezor/ui/layouts/delizia/__init__.py
index bda0fbe4..91316518 100644
--- a/core/src/trezor/ui/layouts/delizia/__init__.py
+++ b/core/src/trezor/ui/layouts/delizia/__init__.py
@@ -2184,15 +2184,11 @@ def error_popup(
def request_passphrase_on_host() -> None:
- with trezorui_api.show_simple(
- title=None, text=TR.passphrase__please_enter
- ) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_simple(title=None, text=TR.passphrase__please_enter))
def show_wait_text(message: str) -> None:
- with trezorui_api.show_wait_text(message) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_wait_text(message))
async def request_passphrase_on_device(max_len: int) -> str:
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index 1dd33c4a..273c709a 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -2306,15 +2306,11 @@ def error_popup(
def request_passphrase_on_host() -> None:
- with trezorui_api.show_simple(
- title=None, text=TR.passphrase__please_enter
- ) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_simple(title=None, text=TR.passphrase__please_enter))
def show_wait_text(message: str) -> None:
- with trezorui_api.show_wait_text(message) as layout:
- draw_simple(layout)
+ draw_simple(trezorui_api.show_wait_text(message))
async def request_passphrase_on_device(max_len: int) -> str:
Why this scored 44/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.