feat(core): firmware suspend by power button
What changed, and why it matters
This commit adds a new power-saving feature: pressing the physical power button on a Trezor device can now suspend the firmware. If the device has a PIN and is currently unlocked, the button press first locks the device (ending any active workflow) and then triggers a shutdown. Otherwise, the device is suspended and later woken up. The change is a feature addition, not a fix for a known security bug, and there is no evidence in the commit or supplied references that it addresses a vulnerability.
No immediate security action required. Treat as a normal feature commit. If reviewing for product security, verify that `lock_device(interrupt_workflow=True)` reliably clears sensitive state before `Shutdown()` is raised, and that the wakeup-flag whitelist cannot be bypassed to leave the device in an unintended state.
Security signals we found
New power-management feature, not a security patch
PIN-locked devices are locked before shutdown when the power button is pressed while unlocked
Workflow interruption is explicitly requested via `interrupt_workflow=True`
Unhandled wakeup flags are ignored and cause re-suspend
No mention of CVE, vulnerability, researcher credit, or security advisory in commit message or diff
Evidence from the diff
The patch introduces apps.management.pm.suspend.suspend_device(), which wraps io.pm.suspend() and only accepts a fixed set of wakeup flags (button, BLE, power). In core/src/trezor/ui/__init__.py, a new _handle_power_button_press() callback is registered: when config.has_pin() and config.is_unlocked() are true, it calls lock_device(interrupt_workflow=True) and raises Shutdown(); otherwise it suspends and requests a full repaint. The power-button event is intercepted in the button event loop before being forwarded to the Rust layout, by checking event_button == 2 and event_type == 0.
Changed components
core/src/trezor/ui/__init__.pycore/src/apps/management/pm/suspend.pycore/embed/upymod/qstrdefsport.hInspect captured patch +44 / −1
diff --git a/core/embed/upymod/qstrdefsport.h b/core/embed/upymod/qstrdefsport.h
index c80eb2f80..6e879e5cd 100644
--- a/core/embed/upymod/qstrdefsport.h
+++ b/core/embed/upymod/qstrdefsport.h
@@ -135,6 +135,7 @@ Q(apps.management.get_next_u2f_counter)
Q(apps.management.get_nonce)
Q(apps.management.pm)
Q(apps.management.pm.autodim)
+Q(apps.management.pm.suspend)
Q(apps.management.reboot_to_bootloader)
Q(apps.management.recovery_device)
Q(apps.management.recovery_device.homescreen)
@@ -299,6 +300,7 @@ Q(storage.recovery_shares)
Q(storage.resident_credentials)
Q(storage.sd_salt)
Q(strings)
+Q(suspend)
Q(trezor)
Q(trezor.crypto)
Q(trezor.crypto.base32)
diff --git a/core/src/apps/management/pm/suspend.py b/core/src/apps/management/pm/suspend.py
new file mode 100644
index 000000000..bdce87a63
--- /dev/null
+++ b/core/src/apps/management/pm/suspend.py
@@ -0,0 +1,17 @@
+from trezor import io
+
+_HANDLED_WAKEUP_FLAGS = (
+ io.pm.WAKEUP_FLAG_BUTTON,
+ io.pm.WAKEUP_FLAG_BLE,
+ io.pm.WAKEUP_FLAG_POWER,
+)
+
+
+def suspend_device() -> int:
+ """Suspend the device and wait for a wakeup event. Wakeup flag is returned."""
+ while True:
+ wakeup_flag = io.pm.suspend()
+ if wakeup_flag not in _HANDLED_WAKEUP_FLAGS:
+ # other wakeup flags are ignored, suspend again
+ continue
+ return wakeup_flag
diff --git a/core/src/trezor/ui/__init__.py b/core/src/trezor/ui/__init__.py
index 31f63feec..4d3cd89ca 100644
--- a/core/src/trezor/ui/__init__.py
+++ b/core/src/trezor/ui/__init__.py
@@ -100,6 +100,24 @@ def set_current_layout(layout: "Layout | ProgressLayout | None") -> None:
CURRENT_LAYOUT = layout
+if utils.USE_POWER_MANAGER:
+
+ def _handle_power_button_press() -> None:
+ """Handle power button press event during firmware operation."""
+ from trezor import config
+
+ from apps.base import lock_device
+ from apps.management.pm.suspend import suspend_device
+
+ if config.has_pin() and config.is_unlocked():
+ lock_device(interrupt_workflow=True)
+ raise Shutdown()
+ else:
+ suspend_device()
+ if CURRENT_LAYOUT is not None:
+ CURRENT_LAYOUT.layout.request_complete_repaint()
+
+
class Layout(Generic[T]):
"""Python-side handler and runner for the Rust based layouts.
@@ -359,7 +377,8 @@ class Layout(Generic[T]):
"""Set up background tasks for a layout.
Called from `start()`. Creates and yields a list of background tasks, typically
- event handlers for different interfaces. Event handlers are enabled conditionally based on build options to prevent stale events in the event queue.
+ event handlers for different interfaces. Event handlers are enabled conditionally
+ based on build options to prevent stale events in the event queue.
Override and then `yield from super().create_tasks()` to add more tasks."""
if utils.USE_BUTTON:
@@ -380,6 +399,11 @@ class Layout(Generic[T]):
while True:
# Using `yield` instead of `await` to avoid allocations.
event = yield button
+ if utils.USE_POWER_MANAGER:
+ event_type, event_button = event
+ # check for POWER_BUTTON (2), BUTTON_UP (0)
+ if event_button == 2 and event_type == 0:
+ _handle_power_button_press()
workflow.idle_timer.touch()
self._event(self.layout.button_event, *event)
except Shutdown:
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.