What changed, and why it matters
This commit adds a new 'Power' menu to the Trezor Safe 7 (Eckhart) device interface, letting the user turn the device off, reboot it, or reboot into bootloader mode. It also adds a low-level 'hibernate' function to the power-management module. The changes are user-facing feature additions and do not appear to fix or introduce a security vulnerability on their own.
No immediate security action required. As a defensive review, verify that the new TurnOff/Reboot/RebootToBootloader menu items require the same user confirmation or lock-screen protections as existing wipe-device or settings actions, and ensure pm_hibernate() cannot be triggered from untrusted code paths.
Security signals we found
New power-management syscall/hibernate wrapper added
New UI paths to reboot and reboot-to-bootloader
No authentication/confirmation flow visible in this diff for the new power actions
Evidence from the diff
The patch extends the device menu UI/UX for the Eckhart layout with three new power-related actions (TurnOff, Reboot, RebootToBootloader), wires them through the Rust UI API and MicroPython homescreen handler, and exposes pm_hibernate() as trezor.io.pm.hibernate(). The handler calls io.pm.hibernate() for turn-off, reboot_to_bootloader() for reboot, and reboot_to_bootloader(BootCommand.STOP_AND_WAIT) for bootloader mode. These are standard, user-initiated power operations already available elsewhere in firmware; the commit only surfaces them in a new menu.
Changed components
core/embed/upymod/modtrezorio/modtrezorio-pm.hcore/src/apps/homescreen/device_menu.pycore/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rscore/embed/rust/src/ui/layout/device_menu_result.rscore/embed/rust/src/ui/api/firmware_micropython.rscore/embed/rust/src/ui/layout_eckhart/component_msg_obj.rsInspect captured patch +103 / −5
diff --git a/core/embed/rust/librust_qstr.h b/core/embed/rust/librust_qstr.h
index a84d27f9..30ea27ad 100644
--- a/core/embed/rust/librust_qstr.h
+++ b/core/embed/rust/librust_qstr.h
@@ -55,6 +55,8 @@ static void _librust_qstrs(void) {
MP_QSTR_PinRemove;
MP_QSTR_RESUME;
MP_QSTR_RX_PACKET_LEN;
+ MP_QSTR_Reboot;
+ MP_QSTR_RebootToBootloader;
MP_QSTR_SWIPE_DOWN;
MP_QSTR_SWIPE_LEFT;
MP_QSTR_SWIPE_RIGHT;
@@ -64,6 +66,7 @@ static void _librust_qstrs(void) {
MP_QSTR_TRANSITIONING;
MP_QSTR_TX_PACKET_LEN;
MP_QSTR_TranslationsHeader;
+ MP_QSTR_TurnOff;
MP_QSTR_WipeCode;
MP_QSTR_WipeDevice;
MP_QSTR_WipeRemove;
@@ -965,6 +968,7 @@ static void _librust_qstrs(void) {
MP_QSTR_words__pay_attention;
MP_QSTR_words__please_check_again;
MP_QSTR_words__please_try_again;
+ MP_QSTR_words__power;
MP_QSTR_words__provider;
MP_QSTR_words__really_wanna;
MP_QSTR_words__receive;
diff --git a/core/embed/rust/src/translations/generated/translated_string.rs b/core/embed/rust/src/translations/generated/translated_string.rs
index 25ea1cc8..469d1740 100644
--- a/core/embed/rust/src/translations/generated/translated_string.rs
+++ b/core/embed/rust/src/translations/generated/translated_string.rs
@@ -1515,6 +1515,7 @@ pub enum TranslatedString {
ble__forget_all = 1124, // "Forget all"
words__connect = 1125, // "Connect"
words__forget = 1126, // "Forget"
+ words__power = 1127, // "Power"
}
impl TranslatedString {
@@ -3356,6 +3357,7 @@ impl TranslatedString {
(Self::ble__forget_all, "Forget all"),
(Self::words__connect, "Connect"),
(Self::words__forget, "Forget"),
+ (Self::words__power, "Power"),
];
#[cfg(feature = "micropython")]
@@ -4830,6 +4832,7 @@ impl TranslatedString {
(Qstr::MP_QSTR_words__pay_attention, Self::words__pay_attention),
(Qstr::MP_QSTR_words__please_check_again, Self::words__please_check_again),
(Qstr::MP_QSTR_words__please_try_again, Self::words__please_try_again),
+ (Qstr::MP_QSTR_words__power, Self::words__power),
(Qstr::MP_QSTR_words__provider, Self::words__provider),
(Qstr::MP_QSTR_words__really_wanna, Self::words__really_wanna),
(Qstr::MP_QSTR_words__receive, Self::words__receive),
diff --git a/core/embed/rust/src/ui/api/firmware_micropython.rs b/core/embed/rust/src/ui/api/firmware_micropython.rs
index bf0e32d4..6bcf4764 100644
--- a/core/embed/rust/src/ui/api/firmware_micropython.rs
+++ b/core/embed/rust/src/ui/api/firmware_micropython.rs
@@ -2126,5 +2126,8 @@ pub static mp_module_trezorui_api: Module = obj_module! {
/// HapticFeedback: ClassVar[DeviceMenuResult]
/// LedEnabled: ClassVar[DeviceMenuResult]
/// WipeDevice: ClassVar[DeviceMenuResult]
+ /// Reboot: ClassVar[DeviceMenuResult]
+ /// RebootToBootloader: ClassVar[DeviceMenuResult]
+ /// TurnOff: ClassVar[DeviceMenuResult]
Qstr::MP_QSTR_DeviceMenuResult => DEVICE_MENU_RESULT.as_obj(),
};
diff --git a/core/embed/rust/src/ui/layout/device_menu_result.rs b/core/embed/rust/src/ui/layout/device_menu_result.rs
index e3116595..bdce0957 100644
--- a/core/embed/rust/src/ui/layout/device_menu_result.rs
+++ b/core/embed/rust/src/ui/layout/device_menu_result.rs
@@ -30,6 +30,10 @@ pub static SCREEN_BRIGHTNESS: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RE
pub static HAPTIC_FEEDBACK: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
pub static LED_ENABLED: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
pub static WIPE_DEVICE: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
+// Power settings
+pub static TURN_OFF: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
+pub static REBOOT: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
+pub static REBOOT_TO_BOOTLOADER: SimpleTypeObj = SimpleTypeObj::new(&DEVICE_MENU_RESULT_BASE_TYPE);
// Create a DeviceMenuResult class that contains all result types
static DEVICE_MENU_RESULT_TYPE: Type = obj_type! {
@@ -53,6 +57,9 @@ static DEVICE_MENU_RESULT_TYPE: Type = obj_type! {
Qstr::MP_QSTR_HapticFeedback => HAPTIC_FEEDBACK.as_obj(),
Qstr::MP_QSTR_LedEnabled => LED_ENABLED.as_obj(),
Qstr::MP_QSTR_WipeDevice => WIPE_DEVICE.as_obj(),
+ Qstr::MP_QSTR_TurnOff => TURN_OFF.as_obj(),
+ Qstr::MP_QSTR_Reboot => REBOOT.as_obj(),
+ Qstr::MP_QSTR_RebootToBootloader => REBOOT_TO_BOOTLOADER.as_obj(),
} },
};
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 7d603e8d..545842a6 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
@@ -184,6 +184,10 @@ impl ComponentMsgObj for DeviceMenuScreen {
DeviceMenuMsg::HapticFeedback => Ok(HAPTIC_FEEDBACK.as_obj()),
DeviceMenuMsg::LedEnabled => Ok(LED_ENABLED.as_obj()),
DeviceMenuMsg::WipeDevice => Ok(WIPE_DEVICE.as_obj()),
+ // Power settings
+ DeviceMenuMsg::TurnOff => Ok(TURN_OFF.as_obj()),
+ DeviceMenuMsg::Reboot => Ok(REBOOT.as_obj()),
+ DeviceMenuMsg::RebootToBootloader => Ok(REBOOT_TO_BOOTLOADER.as_obj()),
// nothing selected
DeviceMenuMsg::Close => Ok(CANCELLED.as_obj()),
}
diff --git a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
index 2e098ea7..5d3e8e4c 100644
--- a/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
+++ b/core/embed/rust/src/ui/layout_eckhart/firmware/device_menu_screen.rs
@@ -40,7 +40,8 @@ use heapless::Vec;
// - pin code
// - wipe code
// - device
-const MAX_SUBMENUS: usize = 7;
+// - power
+const MAX_SUBMENUS: usize = 8;
const MAX_DEPTH: usize = 3;
// submenus, device screens, regulatory and about screens
const MAX_SUBSCREENS: usize = MAX_SUBMENUS + MAX_PAIRED_DEVICES + 2;
@@ -75,6 +76,11 @@ pub enum DeviceMenuMsg {
),
DeviceUnpairAll,
+ // Power
+ TurnOff,
+ Reboot,
+ RebootToBootloader,
+
// Security menu
PinCode,
PinRemove,
@@ -257,6 +263,7 @@ impl DeviceMenuScreen {
about,
);
let settings = screen.add_settings_menu(bluetooth, security, device);
+ let power = screen.add_power_menu();
let is_connected = connected_idx.is_some_and(|idx| idx < paired_devices.len());
let connected_subtext: Option<TString<'static>> =
@@ -278,6 +285,7 @@ impl DeviceMenuScreen {
pair_and_connect,
settings,
connected_subtext,
+ power,
);
screen.set_active_subscreen(root);
@@ -358,6 +366,25 @@ impl DeviceMenuScreen {
self.add_subscreen(Subscreen::Submenu(submenu_index))
}
+ fn add_power_menu(&mut self) -> usize {
+ let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
+ unwrap!(items.push(MenuItem::new(
+ TR::buttons__turn_off.into(),
+ Some(Action::Return(DeviceMenuMsg::TurnOff))
+ )));
+ unwrap!(items.push(MenuItem::new(
+ TR::buttons__restart.into(),
+ Some(Action::Return(DeviceMenuMsg::Reboot))
+ )));
+ unwrap!(items.push(MenuItem::new(
+ TR::reboot_to_bootloader__title.into(),
+ Some(Action::Return(DeviceMenuMsg::RebootToBootloader))
+ )));
+
+ let submenu_index = self.add_submenu(Submenu::new(items));
+ self.add_subscreen(Subscreen::Submenu(submenu_index))
+ }
+
fn add_code_menu(&mut self, wipe_code: bool) -> usize {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
let change_text = match wipe_code {
@@ -547,6 +574,7 @@ impl DeviceMenuScreen {
pair_and_connect_index: usize,
settings_index: usize,
connected_subtext: Option<TString<'static>>,
+ power_index: usize,
) -> usize {
let mut items: Vec<MenuItem, MEDIUM_MENU_ITEMS> = Vec::new();
if failed_backup {
@@ -580,6 +608,11 @@ impl DeviceMenuScreen {
Some(Action::GoTo(settings_index)),
)));
+ unwrap!(items.push(MenuItem::new(
+ TR::words__power.into(),
+ Some(Action::GoTo(power_index)),
+ )));
+
let submenu_index = self.add_submenu(Submenu::new(items).with_battery());
self.add_subscreen(Subscreen::Submenu(submenu_index))
}
diff --git a/core/embed/upymod/modtrezorio/modtrezorio-pm.h b/core/embed/upymod/modtrezorio/modtrezorio-pm.h
index d4a59050..05aeee40 100644
--- a/core/embed/upymod/modtrezorio/modtrezorio-pm.h
+++ b/core/embed/upymod/modtrezorio/modtrezorio-pm.h
@@ -52,6 +52,20 @@ STATIC mp_obj_t mod_trezorio_pm_suspend() {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_pm_suspend_obj,
mod_trezorio_pm_suspend);
+/// def hibernate() -> None:
+/// """
+/// Hibernates the device. Raises RuntimeError on failure.
+/// """
+STATIC mp_obj_t mod_trezorio_pm_hibernate() {
+ pm_status_t res = pm_hibernate();
+ if (res != PM_OK) {
+ mp_raise_msg(&mp_type_RuntimeError, MP_ERROR_TEXT("Failed to hibernate"));
+ }
+ return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_pm_hibernate_obj,
+ mod_trezorio_pm_hibernate);
+
/// def is_usb_connected() -> bool:
/// """
/// Returns True if USB is connected, False otherwise. Raises RuntimeError
@@ -72,6 +86,8 @@ STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorio_pm_is_usb_connected_obj,
STATIC const mp_rom_map_elem_t mod_trezorio_pm_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_pm)},
{MP_ROM_QSTR(MP_QSTR_suspend), MP_ROM_PTR(&mod_trezorio_pm_suspend_obj)},
+ {MP_ROM_QSTR(MP_QSTR_hibernate),
+ MP_ROM_PTR(&mod_trezorio_pm_hibernate_obj)},
{MP_ROM_QSTR(MP_QSTR_is_usb_connected),
MP_ROM_PTR(&mod_trezorio_pm_is_usb_connected_obj)},
diff --git a/core/mocks/generated/trezorio/pm.pyi b/core/mocks/generated/trezorio/pm.pyi
index 913e5605..a1de4ab1 100644
--- a/core/mocks/generated/trezorio/pm.pyi
+++ b/core/mocks/generated/trezorio/pm.pyi
@@ -23,6 +23,13 @@ def suspend() -> int:
"""
+# upymod/modtrezorio/modtrezorio-pm.h
+def hibernate() -> None:
+ """
+ Hibernates the device. Raises RuntimeError on failure.
+ """
+
+
# upymod/modtrezorio/modtrezorio-pm.h
def is_usb_connected() -> bool:
"""
diff --git a/core/mocks/generated/trezorui_api.pyi b/core/mocks/generated/trezorui_api.pyi
index d72a8282..44deb182 100644
--- a/core/mocks/generated/trezorui_api.pyi
+++ b/core/mocks/generated/trezorui_api.pyi
@@ -869,3 +869,6 @@ class DeviceMenuResult:
HapticFeedback: ClassVar[DeviceMenuResult]
LedEnabled: ClassVar[DeviceMenuResult]
WipeDevice: ClassVar[DeviceMenuResult]
+ Reboot: ClassVar[DeviceMenuResult]
+ RebootToBootloader: ClassVar[DeviceMenuResult]
+ TurnOff: ClassVar[DeviceMenuResult]
diff --git a/core/mocks/trezortranslate_keys.pyi b/core/mocks/trezortranslate_keys.pyi
index 4e15fdbb..cd331809 100644
--- a/core/mocks/trezortranslate_keys.pyi
+++ b/core/mocks/trezortranslate_keys.pyi
@@ -1054,6 +1054,7 @@ class TR:
words__pay_attention: str = "Pay attention"
words__please_check_again: str = "Please check again"
words__please_try_again: str = "Please try again"
+ words__power: str = "Power"
words__provider: str = "Provider"
words__really_wanna: str = "Do you really want to"
words__receive: str = "Receive"
diff --git a/core/src/apps/homescreen/device_menu.py b/core/src/apps/homescreen/device_menu.py
index 09411b05..8c9bf603 100644
--- a/core/src/apps/homescreen/device_menu.py
+++ b/core/src/apps/homescreen/device_menu.py
@@ -275,5 +275,20 @@ async def handle_device_menu() -> None:
from apps.management.wipe_device import wipe_device
await wipe_device(WipeDevice())
+ # Power settings
+ elif menu_result is DeviceMenuResult.TurnOff:
+ from trezor import io
+
+ io.pm.hibernate()
+ elif menu_result is DeviceMenuResult.Reboot:
+ from trezor.utils import reboot_to_bootloader
+
+ # Empty boot command results to a normal reboot
+ reboot_to_bootloader()
+ elif menu_result is DeviceMenuResult.RebootToBootloader:
+ from trezor.enums import BootCommand
+ from trezor.utils import reboot_to_bootloader
+
+ reboot_to_bootloader(BootCommand.STOP_AND_WAIT)
else:
raise RuntimeError(f"Unknown menu {menu_result}")
diff --git a/core/translations/en.json b/core/translations/en.json
index bbe74523..e2abb568 100644
--- a/core/translations/en.json
+++ b/core/translations/en.json
@@ -1276,6 +1276,7 @@
"words__pay_attention": "Pay attention",
"words__please_check_again": "Please check again",
"words__please_try_again": "Please try again",
+ "words__power": "Power",
"words__provider": "Provider",
"words__really_wanna": "Do you really want to",
"words__receive": "Receive",
diff --git a/core/translations/order.json b/core/translations/order.json
index eaddfc33..657eabc0 100644
--- a/core/translations/order.json
+++ b/core/translations/order.json
@@ -1125,5 +1125,6 @@
"1123": "words__disconnected",
"1124": "ble__forget_all",
"1125": "words__connect",
- "1126": "words__forget"
+ "1126": "words__forget",
+ "1127": "words__power"
}
diff --git a/core/translations/signatures.json b/core/translations/signatures.json
index 5fe3f685..9b5740ad 100644
--- a/core/translations/signatures.json
+++ b/core/translations/signatures.json
@@ -1,8 +1,8 @@
{
"current": {
- "merkle_root": "eb4654ab34827848bf4ce3e15aef4ef1ee47e74f83e8d1518c07b292b1de2ba5",
- "datetime": "2025-08-27T08:55:36.117610+00:00",
- "commit": "746c7925c3de19c287019e5926a3cc5e30d8d503"
+ "merkle_root": "e3766059df9be721b1496be806e72af7049c6c693a7f93f2219a3036843c472f",
+ "datetime": "2025-09-01T07:40:50.006760+00:00",
+ "commit": "bb3eb7cc5b0af90a979683bb828aff5344e3d57b"
},
"history": [
{
Why this scored 19/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.