feat(core): send lock&unlock notifications when hard-locking
What changed, and why it matters
This commit adds a notification system so the Trezor device can tell the connected computer when it becomes 'unlocked' (ready to accept commands) and 'locked' (hard-locked). It also exposes a way for the device's Python code to send these notifications. There is no obvious security bug in the change itself, but the new MicroPython binding does not validate that the event number is one of the defined values, so a bug or compromised app running inside the device could pass an out-of-range value to the underlying C notification function.
Review the implementation of notify_send() to confirm it safely ignores or clamps out-of-range event values. Consider adding an upper-bound check in mod_trezorutils_notify_send() so only NOTIFY_BOOT, NOTIFY_UNLOCK, and NOTIFY_LOCK can be sent from Python. Treat this commit as a feature addition rather than a vulnerability unless further review shows the notification channel can be abused.
Security signals we found
New host notification channel added for lock/unlock state
MicroPython binding exposes notify_send() to Python code with only a negative-value check
No range validation against the notification_event_t enum in the Python binding
Bootloader workflows now send NOTIFY_UNLOCK/NOTIFY_LOCK around USB/message interfaces
Evidence from the diff
The patch introduces NOTIFY_UNLOCK and NOTIFY_LOCK events in sys/notify.h and sends them around bootloader workflows (wf_auto_update.c, wf_bootloader.c, wf_empty_device.c) and at the start of core/src/main.py. It also adds mod_trezorutils_notify_send() in modtrezorutils.c, which lets MicroPython call notify_send(). The binding only rejects negative integers; it does not cap the value to the notification_event_t enum range, so an invalid positive value can be passed to notify_send(). The actual security impact depends on how notify_send() handles unknown values, which is not shown in this diff.
Changed components
core/embed/sys/notifycore/embed/projects/bootloader workflowcore/embed/upymod/modtrezorutilscore/src/main.pycore/src/trezor/utils.pyInspect captured patch +65 / −6
diff --git a/core/embed/projects/bootloader/workflow/wf_auto_update.c b/core/embed/projects/bootloader/workflow/wf_auto_update.c
index 149b78d6..328f68ed 100644
--- a/core/embed/projects/bootloader/workflow/wf_auto_update.c
+++ b/core/embed/projects/bootloader/workflow/wf_auto_update.c
@@ -21,6 +21,7 @@
#include <trezor_model.h>
#include <trezor_rtl.h>
+#include <sys/notify.h>
#include <util/image.h>
#include "bootui.h"
@@ -36,7 +37,9 @@ workflow_result_t workflow_auto_update(const vendor_header *const vhdr,
uint32_t ui_result = CONNECT_CANCEL;
protob_ios_t ios;
+
workflow_ifaces_init(secfalse, &ios);
+ notify_send(NOTIFY_UNLOCK);
c_layout_t layout;
memset(&layout, 0, sizeof(layout));
@@ -45,10 +48,10 @@ workflow_result_t workflow_auto_update(const vendor_header *const vhdr,
if (res == WF_OK_UI_ACTION && ui_result == CONNECT_CANCEL) {
bootargs_set(BOOT_COMMAND_NONE, NULL, 0);
- workflow_ifaces_deinit(&ios);
- return WF_OK_REBOOT_SELECTED;
+ res = WF_OK_REBOOT_SELECTED;
}
+ notify_send(NOTIFY_LOCK);
workflow_ifaces_deinit(&ios);
return res;
}
diff --git a/core/embed/projects/bootloader/workflow/wf_bootloader.c b/core/embed/projects/bootloader/workflow/wf_bootloader.c
index 683a355d..ced2a683 100644
--- a/core/embed/projects/bootloader/workflow/wf_bootloader.c
+++ b/core/embed/projects/bootloader/workflow/wf_bootloader.c
@@ -20,6 +20,7 @@
#include <trezor_model.h>
#include <trezor_rtl.h>
+#include <sys/notify.h>
#include <sys/types.h>
#include <util/image.h>
@@ -141,6 +142,8 @@ static screen_t handle_wait_for_host(const vendor_header* vhdr,
protob_ios_t ios;
workflow_ifaces_init(secfalse, &ios);
+ notify_send(NOTIFY_UNLOCK);
+
screen_t next_screen = SCREEN_WAIT_FOR_HOST;
while (next_screen == SCREEN_WAIT_FOR_HOST) {
@@ -205,7 +208,9 @@ static screen_t handle_wait_for_host(const vendor_header* vhdr,
}
}
+ notify_send(NOTIFY_LOCK);
workflow_ifaces_deinit(&ios);
+
return next_screen;
}
diff --git a/core/embed/projects/bootloader/workflow/wf_empty_device.c b/core/embed/projects/bootloader/workflow/wf_empty_device.c
index b676b444..345ab03a 100644
--- a/core/embed/projects/bootloader/workflow/wf_empty_device.c
+++ b/core/embed/projects/bootloader/workflow/wf_empty_device.c
@@ -20,6 +20,7 @@
#include <trezor_model.h>
#include <trezor_rtl.h>
+#include <sys/notify.h>
#include <sys/systick.h>
#include <sys/types.h>
#include <util/flash_utils.h>
@@ -66,6 +67,7 @@ workflow_result_t workflow_empty_device(void) {
protob_ios_t ios;
workflow_ifaces_init(sectrue, &ios);
+ notify_send(NOTIFY_UNLOCK);
workflow_result_t res = WF_CANCELLED;
uint32_t ui_result = WELCOME_CANCEL;
@@ -83,7 +85,7 @@ workflow_result_t workflow_empty_device(void) {
ui_result = WELCOME_CANCEL;
continue;
}
- return res;
+ break;
}
#endif
if (res == WF_OK_UI_ACTION && ui_result == WELCOME_MENU) {
@@ -96,10 +98,10 @@ workflow_result_t workflow_empty_device(void) {
ui_result = WELCOME_CANCEL;
continue;
}
- workflow_ifaces_deinit(&ios);
- return res;
+ break;
}
}
+ notify_send(NOTIFY_LOCK);
workflow_ifaces_deinit(&ios);
return res;
}
diff --git a/core/embed/sys/notify/inc/sys/notify.h b/core/embed/sys/notify/inc/sys/notify.h
index f6c4f66d..aa00b2e9 100644
--- a/core/embed/sys/notify/inc/sys/notify.h
+++ b/core/embed/sys/notify/inc/sys/notify.h
@@ -39,7 +39,9 @@
*/
typedef enum {
- NOTIFY_BOOT = 0, /**< Device boot/startup notification */
+ NOTIFY_BOOT = 0, /**< Device boot/startup notification */
+ NOTIFY_UNLOCK = 1, /**< Device unlocked and ready to accept messages */
+ NOTIFY_LOCK = 2 /**< Device hard-locked and won't accept messages */
// Additional notification types can be added here as needed
} notification_event_t;
diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c
index 1e224f51..59e5f506 100644
--- a/core/embed/upymod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c
@@ -36,6 +36,7 @@
#include <io/usb.h>
#include <sys/bootutils.h>
+#include <sys/notify.h>
#include <util/fwutils.h>
#include <util/scm_revision.h>
#include <util/unit_properties.h>
@@ -561,6 +562,21 @@ STATIC mp_obj_t mod_trezorutils_bootloader_locked() {
STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_bootloader_locked_obj,
mod_trezorutils_bootloader_locked);
+/// def notify_send(event: int) -> None:
+/// """
+/// Sends a notification to host
+/// """
+STATIC mp_obj_t mod_trezorutils_notify_send(const mp_obj_t event) {
+ mp_int_t value = mp_obj_get_int(event);
+ if (value < 0) {
+ mp_raise_ValueError(MP_ERROR_TEXT("Invalid event."));
+ }
+ notify_send((notification_event_t)value);
+ return mp_const_none;
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorutils_notify_send_obj,
+ mod_trezorutils_notify_send);
+
STATIC mp_obj_str_t mod_trezorutils_revision_obj = {
{&mp_type_bytes}, 0, sizeof(SCM_REVISION), (const byte *)SCM_REVISION};
@@ -635,6 +651,12 @@ STATIC mp_obj_tuple_t mod_trezorutils_version_obj = {
/// """UI layout identifier ("BOLT"-T, "CAESAR"-TS3, "DELIZIA"-TS5)."""
/// USE_THP: bool
/// """Whether the firmware supports Trezor-Host Protocol (version 2)."""
+/// NOTIFY_BOOT: int
+/// """Notification event: boot completed."""
+/// NOTIFY_UNLOCK: int
+/// """Notification event: device unlocked from hardlock"""
+/// NOTIFY_LOCK: int
+/// """Notification event: device locked to hardlock"""
/// if __debug__:
/// DISABLE_ANIMATION: bool
/// """Whether the firmware should disable animations."""
@@ -657,6 +679,12 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
MP_ROM_PTR(&mod_trezorutils_check_firmware_header_obj)},
{MP_ROM_QSTR(MP_QSTR_bootloader_locked),
MP_ROM_PTR(&mod_trezorutils_bootloader_locked_obj)},
+ {MP_ROM_QSTR(MP_QSTR_notify_send),
+ MP_ROM_PTR(&mod_trezorutils_notify_send_obj)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_BOOT), MP_ROM_INT(NOTIFY_BOOT)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_UNLOCK), MP_ROM_INT(NOTIFY_UNLOCK)},
+ {MP_ROM_QSTR(MP_QSTR_NOTIFY_LOCK), MP_ROM_INT(NOTIFY_LOCK)},
+
{MP_ROM_QSTR(MP_QSTR_unit_color),
MP_ROM_PTR(&mod_trezorutils_unit_color_obj)},
{MP_ROM_QSTR(MP_QSTR_unit_packaging),
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index a45faed5..99144dc4 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -179,6 +179,13 @@ def bootloader_locked() -> bool | None:
Returns True/False if the the bootloader is locked/unlocked and None if
the feature is not supported.
"""
+
+
+# upymod/modtrezorutils/modtrezorutils.c
+def notify_send(event: int) -> None:
+ """
+ Sends a notification to host
+ """
SCM_REVISION: bytes
"""Git commit hash of the firmware."""
VERSION: VersionTuple
@@ -223,6 +230,12 @@ UI_LAYOUT: str
"""UI layout identifier ("BOLT"-T, "CAESAR"-TS3, "DELIZIA"-TS5)."""
USE_THP: bool
"""Whether the firmware supports Trezor-Host Protocol (version 2)."""
+NOTIFY_BOOT: int
+"""Notification event: boot completed."""
+NOTIFY_UNLOCK: int
+"""Notification event: device unlocked from hardlock"""
+NOTIFY_LOCK: int
+"""Notification event: device locked to hardlock"""
if __debug__:
DISABLE_ANIMATION: bool
"""Whether the firmware should disable animations."""
diff --git a/core/src/main.py b/core/src/main.py
index 54987f04..f6e048a8 100644
--- a/core/src/main.py
+++ b/core/src/main.py
@@ -53,6 +53,8 @@ if utils.USE_BLE:
import ble # noqa: F401
del ble
+# send unlock notification
+utils.notify_send(utils.NOTIFY_UNLOCK)
# run the endless loop
unimport_manager = utils.unimport()
diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py
index cec8e0d2..73761bd2 100644
--- a/core/src/trezor/utils.py
+++ b/core/src/trezor/utils.py
@@ -9,6 +9,9 @@ from trezorutils import ( # noqa: F401
MODEL_FULL_NAME,
MODEL_USB_MANUFACTURER,
MODEL_USB_PRODUCT,
+ NOTIFY_BOOT,
+ NOTIFY_LOCK,
+ NOTIFY_UNLOCK,
SCM_REVISION,
UI_LAYOUT,
USE_BACKLIGHT,
@@ -31,6 +34,7 @@ from trezorutils import ( # noqa: F401
halt,
memcpy,
memzero,
+ notify_send,
presize_module,
reboot_to_bootloader,
sd_hotswap_enabled,
Why this scored 22/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.