feat(eckhart): disable cancel on `confirm_action`
What changed, and why it matters
This commit adds a new option to Trezor's on-screen confirmation dialogs that lets developers hide the cancel/cross button on the new 'Eckhart' device layout. It does not remove cancel from existing layouts, and the default behavior keeps the cancel button visible. The only place it is currently used is in one voting-related screen, where the user is still shown a 'Continue' button and can navigate back via an external menu. There is no direct evidence this is a security fix or vulnerability patch.
Review whether any security-critical confirmation flows plan to use `cancel=False` on Eckhart, because removing the explicit cancel button can reduce user control and may conflict with Trezor's design principle of always allowing users to abort sensitive operations. If adopted widely, ensure an alternative escape path (e.g., hardware button long-press, back navigation, or timeout) remains available and clearly discoverable.
Security signals we found
UI flow change: ability to suppress explicit cancel button in a confirmation dialog
Default remains permissive (cancel=True), limiting blast radius
Only one production call site currently uses the new flag
No changelog entry and no security-related commit message
No vendor or researcher attribution for a security issue
Evidence from the diff
The change extends the confirm_action UI API with a cancel: bool = True parameter, threading it through the Rust firmware UI trait and all four layout implementations (Bolt, Caesar, Delizia, Eckhart). Bolt/Caesar/Delizia ignore the parameter (prefixed _cancel), preserving existing behavior. Only Eckhart consumes it: when cancel=False, the action bar switches from a two-button layout (cross/cancel + right action) to a single right action button. The Python stub and one Eckhart-specific voting flow (vote_account confirmation) set cancel=False.
Changed components
core/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/ui_firmware.rscore/embed/rust/src/ui/layout_bolt/ui_firmware.rscore/embed/rust/src/ui/layout_caesar/ui_firmware.rscore/embed/rust/src/ui/layout_delizia/ui_firmware.rscore/embed/rust/src/ui/layout_eckhart/ui_firmware.rscore/mocks/generated/trezorui_api.pyicore/src/trezor/ui/layouts/eckhart/__init__.pyInspect captured patch +18 / −4
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index c2fdc4bb..1bb389e1 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -64,6 +64,7 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
.get(Qstr::MP_QSTR_verb)
.unwrap_or_else(|_| Obj::const_none())
.try_into_option()?;
+ let cancel: bool = kwargs.get_or(Qstr::MP_QSTR_cancel, true)?;
let verb_cancel: Option<TString> = kwargs
.get(Qstr::MP_QSTR_verb_cancel)
.unwrap_or_else(|_| Obj::const_none())
@@ -84,6 +85,7 @@ extern "C" fn new_confirm_action(n_args: usize, args: *const Obj, kwargs: *mut M
description,
subtitle,
verb,
+ cancel,
verb_cancel,
hold,
hold_danger,
@@ -1515,6 +1517,7 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// description: str | None,
/// subtitle: str | None = None,
/// verb: str | None = None,
+ /// cancel: bool = True,
/// verb_cancel: str | None = None,
/// hold: bool = False,
/// hold_danger: bool = False,
diff --git a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
index d5cf612e..41892e7c 100644
--- a/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_bolt/ui_firmware.rs
@@ -52,6 +52,7 @@ impl FirmwareUI for UIBolt {
description: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
verb: Option<TString<'static>>,
+ _cancel: bool,
verb_cancel: Option<TString<'static>>,
hold: bool,
hold_danger: bool,
@@ -988,6 +989,7 @@ impl FirmwareUI for UIBolt {
Some(description),
None,
None,
+ true,
None,
false,
false,
diff --git a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
index 145a17a3..5471eeac 100644
--- a/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_caesar/ui_firmware.rs
@@ -52,6 +52,7 @@ impl FirmwareUI for UICaesar {
description: Option<TString<'static>>,
_subtitle: Option<TString<'static>>,
verb: Option<TString<'static>>,
+ _cancel: bool,
verb_cancel: Option<TString<'static>>,
hold: bool,
_hold_danger: bool,
@@ -1170,6 +1171,7 @@ impl FirmwareUI for UICaesar {
Some(description),
None,
None,
+ true,
None,
false,
false,
diff --git a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
index da8a727b..b229ff4d 100644
--- a/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_delizia/ui_firmware.rs
@@ -58,6 +58,7 @@ impl FirmwareUI for UIDelizia {
description: Option<TString<'static>>,
subtitle: Option<TString<'static>>,
_verb: Option<TString<'static>>,
+ _cancel: bool,
verb_cancel: Option<TString<'static>>,
hold: bool,
_hold_danger: bool,
@@ -1006,6 +1007,7 @@ impl FirmwareUI for UIDelizia {
Some(description),
None,
None,
+ true,
None,
false,
false,
diff --git a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
index 77ce72d2..2dde01f6 100644
--- a/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/ui_firmware.rs
@@ -63,6 +63,7 @@ impl FirmwareUI for UIEckhart {
description: Option<TString<'static>>,
subtitle: Option<TString<'static>>,
verb: Option<TString<'static>>,
+ cancel: bool,
_verb_cancel: Option<TString<'static>>,
hold: bool,
hold_danger: bool,
@@ -111,10 +112,11 @@ impl FirmwareUI for UIEckhart {
let mut screen = TextScreen::new(paragraphs)
.with_header(Header::new(title))
- .with_action_bar(ActionBar::new_double(
- Button::with_icon(theme::ICON_CROSS),
- right_button,
- ));
+ .with_action_bar(if cancel {
+ ActionBar::new_double(Button::with_icon(theme::ICON_CROSS), right_button)
+ } else {
+ ActionBar::new_single(right_button)
+ });
if let Some(subtitle) = subtitle {
screen = screen.with_hint(Hint::new_instruction(subtitle, None));
}
diff --git a/core/embed/rust/src/ui/ui_firmware.rs b/core/embed/rust/src/ui/ui_firmware.rs
index 95eac95d..af11bab6 100644
--- a/core/embed/rust/src/ui/ui_firmware.rs
+++ b/core/embed/rust/src/ui/ui_firmware.rs
@@ -27,6 +27,7 @@ pub trait FirmwareUI {
description: Option<TString<'static>>,
subtitle: Option<TString<'static>>,
verb: Option<TString<'static>>,
+ cancel: bool,
verb_cancel: Option<TString<'static>>,
hold: bool,
hold_danger: bool,
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index 7a636ba0..923f2327 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -125,6 +125,7 @@ def confirm_action(
description: str | None,
subtitle: str | None = None,
verb: str | None = None,
+ cancel: bool = True,
verb_cancel: str | None = None,
hold: bool = False,
hold_danger: bool = False,
diff --git a/core/src/trezor/ui/layouts/eckhart/__init__.py b/core/src/trezor/ui/layouts/eckhart/__init__.py
index ff42c7a7..55f21eda 100644
--- a/core/src/trezor/ui/layouts/eckhart/__init__.py
+++ b/core/src/trezor/ui/layouts/eckhart/__init__.py
@@ -1453,6 +1453,7 @@ if not utils.BITCOIN_ONLY:
action=vote_account,
description=description,
verb=TR.buttons__continue,
+ cancel=False,
external_menu=True,
)
),
Why this scored 21/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.