feat(core): introduce device notification system
What changed, and why it matters
This commit adds a new device-to-host notification feature to Trezor hardware wallets. When the device boots (either in normal firmware or bootloader mode), it can now send a small BLE notification to a connected host announcing that it has started. The change introduces a new BLE characteristic for notifications and a small kernel module to format and send the message. There is no direct evidence in the commit that this fixes a security bug; it appears to be a feature addition. However, the new code path involves copying untrusted-length data into a fixed-size buffer and allocating/freeing memory in an embedded BLE stack, which are places where future bugs could hide.
Treat this as a feature commit rather than a security fix. Reviewers should verify that the `sizeof(data)` typo in `ble_notify()` is corrected to `sizeof(cmd)`, add a NULL check after `k_malloc()` in `service_notify()`, and confirm that the new notify characteristic cannot be used to leak sensitive state beyond the intended boot flag. Because the commit is a new feature with no changelog, downstream consumers should ensure the notification subsystem is covered by tests before release.
Security signals we found
New BLE GATT notify characteristic with encrypted permissions only
Kernel-mode notification syscall added to syscall dispatch table
Automatic boot-time notification sent from both bootloader and firmware
Fixed-size 32-byte command buffer with length-based memcpy in BLE driver
Nordic BLE service allocates dynamic memory without NULL check before memcpy
No input validation on notification event enum in syscall handler
Evidence from the diff
The patch introduces sys/notify, a push-notification subsystem, and a new BLE GATT notify characteristic (BT_UUID_TRZ_NOTIFY). notify_send() builds a 3-byte notification_data_t (version, event, flags) and calls ble_notify(). On STM32, ble_notify() copies up to 31 bytes into a 32-byte local command buffer prefixed with INTERNAL_CMD_NOTIFY and forwards it to the Nordic BLE coprocessor. The Nordic firmware receives the command and calls service_notify(), which allocates a buffer with k_malloc(), copies the payload, and sends it via bt_gatt_notify_cb() if the peer is subscribed. The commit also exposes notify_send() as a syscall. The implementation has a few rough edges: ble_notify() uses sizeof(data) (pointer size) instead of sizeof(cmd) in its MIN() clamp, which on 32-bit ARM clamps to 3 bytes and is therefore harmless but incorrect; on 64-bit Unix it would clamp to 7 bytes. The service_notify() function does not check the k_malloc() return value, so a NULL dereference is possible under memory pressure. The notification is sent automatically from both bootloader_main() and firmware_main() immediately after boot, before user interaction.
Changed components
core/embed/sys/notify (new)core/embed/io/ble/stm32/ble.ccore/embed/io/ble/unix/ble.ccore/embed/sys/syscall/stm32/syscall_dispatch.ccore/embed/sys/syscall/stm32/syscall_stubs.ccore/embed/projects/bootloader/main.ccore/embed/projects/firmware/main.cnordic/trezor/trezor-ble/src/ble/service.cnordic/trezor/trezor-ble/src/ble/ble_management.cInspect captured patch +242 / −0
diff --git a/core/embed/io/ble/inc/io/ble.h b/core/embed/io/ble/inc/io/ble.h
index e83f9696..0068f3f5 100644
--- a/core/embed/io/ble/inc/io/ble.h
+++ b/core/embed/io/ble/inc/io/ble.h
@@ -216,3 +216,8 @@ bool ble_get_mac(bt_le_addr_t *addr);
// When enabled, the connection parameters will be set to achieve
// higher data throughput, at the cost of increased power consumption.
void ble_set_high_speed(bool enable);
+
+// BLE notify
+//
+// Sends notification to host over BLE
+void ble_notify(const uint8_t *data, size_t len);
diff --git a/core/embed/io/ble/stm32/ble.c b/core/embed/io/ble/stm32/ble.c
index aa747a44..749b7f7c 100644
--- a/core/embed/io/ble/stm32/ble.c
+++ b/core/embed/io/ble/stm32/ble.c
@@ -18,6 +18,8 @@
*/
#ifdef KERNEL_MODE
+#include <stdint.h>
+#include <string.h>
#include <trezor_bsp.h>
#include <trezor_model.h>
@@ -1180,6 +1182,19 @@ void ble_set_high_speed(bool enable) {
irq_unlock(key);
}
+void ble_notify(const uint8_t *data, size_t len) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!drv->initialized) {
+ return;
+ }
+
+ uint8_t cmd[32] = {0};
+ cmd[0] = INTERNAL_CMD_NOTIFY;
+ memcpy(&cmd[1], data, MIN(len, sizeof(data) - 1));
+
+ nrf_send_msg(NRF_SERVICE_BLE_MANAGER, cmd, MIN(32, len + 1), NULL, NULL);
+}
+
static void on_ble_iface_event_poll(void *context, bool read_awaited,
bool write_awaited) {
UNUSED(context);
diff --git a/core/embed/io/ble/stm32/ble_comm_defs.h b/core/embed/io/ble/stm32/ble_comm_defs.h
index aa527ac6..d0d4da26 100644
--- a/core/embed/io/ble/stm32/ble_comm_defs.h
+++ b/core/embed/io/ble/stm32/ble_comm_defs.h
@@ -74,6 +74,7 @@ typedef enum {
INTERNAL_CMD_GET_BOND_LIST = 0x0B,
INTERNAL_CMD_SET_SPEED_HIGH = 0x0C,
INTERNAL_CMD_SET_SPEED_LOW = 0x0D,
+ INTERNAL_CMD_NOTIFY = 0x0E,
} internal_cmd_t;
typedef struct {
diff --git a/core/embed/io/ble/unix/ble.c b/core/embed/io/ble/unix/ble.c
index bc80f5d4..257fa71e 100644
--- a/core/embed/io/ble/unix/ble.c
+++ b/core/embed/io/ble/unix/ble.c
@@ -40,3 +40,5 @@ bool ble_unpair(const bt_le_addr_t *addr) { return false; }
uint8_t ble_get_bond_list(bt_le_addr_t *bonds, size_t count) { return 0; }
void ble_set_high_speed(bool enable){};
+
+void ble_notify(const uint8_t *data, size_t len){};
diff --git a/core/embed/projects/bootloader/main.c b/core/embed/projects/bootloader/main.c
index 2c9e407c..4d728954 100644
--- a/core/embed/projects/bootloader/main.c
+++ b/core/embed/projects/bootloader/main.c
@@ -27,6 +27,7 @@
#include <sec/secret.h>
#include <sys/bootargs.h>
#include <sys/bootutils.h>
+#include <sys/notify.h>
#include <sys/system.h>
#include <sys/systick.h>
#include <sys/types.h>
@@ -692,6 +693,8 @@ int bootloader_main(void) {
ensure(dont_optimize_out_true * (firmware_present == firmware_present_backup),
NULL);
+ notify_send(NOTIFY_BOOT);
+
// start the bootloader ...
// ... if user touched the screen on start
// ... or we have stay_in_bootloader flag to force it
diff --git a/core/embed/projects/firmware/main.c b/core/embed/projects/firmware/main.c
index b986bb90..4b88c360 100644
--- a/core/embed/projects/firmware/main.c
+++ b/core/embed/projects/firmware/main.c
@@ -35,6 +35,7 @@
#include <io/display.h>
#include <sys/linker_utils.h>
+#include <sys/notify.h>
#include <sys/systask.h>
#include <sys/system.h>
#include <util/boot_image.h>
@@ -49,6 +50,10 @@
#include "zkp_context.h"
#endif
+#ifdef USE_BLE
+#include <io/ble.h>
+#endif
+
#ifdef USE_NRF
#include <io/nrf.h>
@@ -106,6 +111,8 @@ int main_func(uint32_t cmd, void *arg) {
screen_boot_stage_2(fading);
+ notify_send(NOTIFY_BOOT);
+
#ifdef USE_SECP256K1_ZKP
ensure(sectrue * (zkp_context_init() == 0), NULL);
#endif
diff --git a/core/embed/sys/notify/inc/sys/notify.h b/core/embed/sys/notify/inc/sys/notify.h
new file mode 100644
index 00000000..f6c4f66d
--- /dev/null
+++ b/core/embed/sys/notify/inc/sys/notify.h
@@ -0,0 +1,89 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+/**
+ * @file notify.h
+ * @brief Device-to-host push notification system
+ *
+ * This module provides functionality for sending push notifications from the
+ * Trezor device to a connected host. It allows the device to proactively
+ * communicate status changes, events, and other important information without
+ * waiting for host requests.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+/**
+ * @brief Enumeration of notification event types
+ *
+ * Defines the various types of events that can trigger push notifications
+ * from the device to the connected host.
+ */
+
+typedef enum {
+ NOTIFY_BOOT = 0, /**< Device boot/startup notification */
+ // Additional notification types can be added here as needed
+} notification_event_t;
+
+/**
+ * @brief Notification data structure
+ *
+ * Contains the event type and associated flags/data that will be sent
+ * to the host as part of the push notification.
+ */
+typedef struct {
+ uint8_t version; /**< Version of the notification data structure */
+ uint8_t event; /**< Event type from notification_event_t enum */
+
+ /**
+ * @brief Event-specific flags and data
+ *
+ * Union allows for flexible data representation - can be accessed
+ * as structured flags or as a single byte value.
+ */
+ union {
+ /**
+ * @brief Structured flag representation
+ *
+ * Provides bit-level access to individual flags within the data byte.
+ */
+ struct {
+ uint8_t bootloader : 1; /**< Set if device is in bootloader mode */
+ uint8_t reserved : 7; /**< Reserved bits for future use */
+ } flags;
+
+ uint8_t all_flags; /**< Raw byte access to all flags */
+ } flags;
+
+} notification_data_t;
+
+/**
+ * @brief Send a push notification to the connected host
+ *
+ * Transmits a notification event to the host, allowing the device to
+ * proactively communicate status changes or important events.
+ *
+ * @param event The type of notification event to send
+ *
+ * @note This function handles the underlying communication protocol
+ * and data formatting automatically based on the event type.
+ */
+void notify_send(notification_event_t event);
diff --git a/core/embed/sys/notify/notify.c b/core/embed/sys/notify/notify.c
new file mode 100644
index 00000000..2aca7d2a
--- /dev/null
+++ b/core/embed/sys/notify/notify.c
@@ -0,0 +1,50 @@
+/*
+ * This file is part of the Trezor project, https://trezor.io/
+ *
+ * Copyright (c) SatoshiLabs
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 3 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+#ifdef KERNEL_MODE
+
+#include <trezor_model.h>
+#include <trezor_rtl.h>
+
+#include <sys/notify.h>
+
+#define NOTIFICATION_VERSION 1
+
+#ifdef USE_BLE
+#include <io/ble.h>
+#endif
+
+void notify_send(notification_event_t event) {
+ notification_data_t data = {0};
+
+ data.version = NOTIFICATION_VERSION;
+ data.event = event;
+
+#ifdef BOOTLOADER
+ data.flags.flags.bootloader = 1;
+#endif
+
+#ifdef USE_BLE
+ ble_notify((uint8_t*)&data, sizeof(data));
+#endif
+
+ (void)data;
+}
+
+#endif
diff --git a/core/embed/sys/syscall/inc/sys/syscall_numbers.h b/core/embed/sys/syscall/inc/sys/syscall_numbers.h
index 612ffd82..33732c3b 100644
--- a/core/embed/sys/syscall/inc/sys/syscall_numbers.h
+++ b/core/embed/sys/syscall/inc/sys/syscall_numbers.h
@@ -54,6 +54,8 @@ typedef enum {
SYSCALL_REBOOT_TO_BOOTLOADER,
SYSCALL_REBOOT_AND_UPGRADE,
+ SYSCALL_NOTIFY_SEND,
+
SYSCALL_DISPLAY_SET_BACKLIGHT,
SYSCALL_DISPLAY_GET_BACKLIGHT,
SYSCALL_DISPLAY_SET_ORIENTATION,
diff --git a/core/embed/sys/syscall/stm32/syscall_dispatch.c b/core/embed/sys/syscall/stm32/syscall_dispatch.c
index 854ccc68..dfa327b5 100644
--- a/core/embed/sys/syscall/stm32/syscall_dispatch.c
+++ b/core/embed/sys/syscall/stm32/syscall_dispatch.c
@@ -28,6 +28,7 @@
#include <sec/secret.h>
#include <sys/bootutils.h>
#include <sys/irq.h>
+#include <sys/notify.h>
#include <sys/sysevent.h>
#include <sys/systask.h>
#include <sys/system.h>
@@ -204,6 +205,11 @@ __attribute((no_stack_protector)) void syscall_handler(uint32_t *args,
reboot_and_upgrade__verified(hash);
} break;
+ case SYSCALL_NOTIFY_SEND: {
+ notification_event_t event = (notification_event_t)args[0];
+ notify_send(event);
+ } break;
+
case SYSCALL_DISPLAY_SET_BACKLIGHT: {
int level = (int)args[0];
args[0] = display_set_backlight(level);
diff --git a/core/embed/sys/syscall/stm32/syscall_stubs.c b/core/embed/sys/syscall/stm32/syscall_stubs.c
index f81d1a69..463496d8 100644
--- a/core/embed/sys/syscall/stm32/syscall_stubs.c
+++ b/core/embed/sys/syscall/stm32/syscall_stubs.c
@@ -150,6 +150,16 @@ void reboot_device(void) {
;
}
+// =============================================================================
+// notify.h
+// =============================================================================
+
+#include <sys/notify.h>
+
+void notify_send(notification_event_t event) {
+ syscall_invoke1((uint32_t)event, SYSCALL_NOTIFY_SEND);
+}
+
// =============================================================================
// display.h
// =============================================================================
diff --git a/core/site_scons/models/stm32f4_common.py b/core/site_scons/models/stm32f4_common.py
index 0b970c8a..675ee66e 100644
--- a/core/site_scons/models/stm32f4_common.py
+++ b/core/site_scons/models/stm32f4_common.py
@@ -21,6 +21,7 @@ def stm32f4_common_files(env, features_wanted, defines, sources, paths):
"embed/sys/irq/inc",
"embed/sys/linker/inc",
"embed/sys/mpu/inc",
+ "embed/sys/notify/inc",
"embed/sys/pvd/inc",
"embed/sec/secret/inc",
"embed/sys/stack/inc",
@@ -74,6 +75,7 @@ def stm32f4_common_files(env, features_wanted, defines, sources, paths):
"embed/sys/irq/stm32/irq.c",
"embed/sys/linker/linker_utils.c",
"embed/sys/mpu/stm32f4/mpu.c",
+ "embed/sys/notify/notify.c",
"embed/sys/pvd/stm32/pvd.c",
"embed/sys/stack/stm32/stack_utils.c",
"embed/sys/startup/stm32/bootutils.c",
diff --git a/core/site_scons/models/stm32u5_common.py b/core/site_scons/models/stm32u5_common.py
index b3a3b26b..c34b1c5b 100644
--- a/core/site_scons/models/stm32u5_common.py
+++ b/core/site_scons/models/stm32u5_common.py
@@ -23,6 +23,7 @@ def stm32u5_common_files(env, features_wanted, defines, sources, paths):
"embed/sys/irq/inc",
"embed/sys/linker/inc",
"embed/sys/mpu/inc",
+ "embed/sys/notify/inc",
"embed/sys/pvd/inc",
"embed/sys/stack/inc",
"embed/sys/startup/inc",
@@ -95,6 +96,7 @@ def stm32u5_common_files(env, features_wanted, defines, sources, paths):
"embed/sys/irq/stm32/irq.c",
"embed/sys/linker/linker_utils.c",
"embed/sys/mpu/stm32u5/mpu.c",
+ "embed/sys/notify/notify.c",
"embed/sys/pvd/stm32/pvd.c",
"embed/sys/smcall/stm32/smcall_dispatch.c",
"embed/sys/smcall/stm32/smcall_probe.c",
diff --git a/core/site_scons/models/unix_common.py b/core/site_scons/models/unix_common.py
index 5190e745..707843d6 100644
--- a/core/site_scons/models/unix_common.py
+++ b/core/site_scons/models/unix_common.py
@@ -21,6 +21,7 @@ def unix_common_files(env, features_wanted, defines, sources, paths):
"embed/sec/secret/inc",
"embed/sys/irq/inc",
"embed/sys/mpu/inc",
+ "embed/sys/notify/inc",
"embed/sys/startup/inc",
"embed/sys/task/inc",
"embed/sys/time/inc",
@@ -43,6 +44,7 @@ def unix_common_files(env, features_wanted, defines, sources, paths):
"embed/sys/dbg/dbg_console.c",
"embed/sys/dbg/unix/dbg_console_backend.c",
"embed/sys/mpu/unix/mpu.c",
+ "embed/sys/notify/notify.c",
"embed/sys/startup/unix/bootutils.c",
"embed/sys/task/sysevent.c",
"embed/sys/task/unix/sdl_event.c",
diff --git a/nordic/trezor/trezor-ble/src/ble/ble_internal.h b/nordic/trezor/trezor-ble/src/ble/ble_internal.h
index 6256e8f7..d63b3aac 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble_internal.h
+++ b/nordic/trezor/trezor-ble/src/ble/ble_internal.h
@@ -43,9 +43,14 @@
#define BT_UUID_TRZ_RX_VAL \
BT_UUID_128_ENCODE(0x8c000002, 0xa59b, 0x4d58, 0xa9ad, 0x073df69fa1b1)
+/** @brief UUID of the Notify Characteristic. **/
+#define BT_UUID_TRZ_NOTIFY_VAL \
+ BT_UUID_128_ENCODE(0x8c000004, 0xa59b, 0x4d58, 0xa9ad, 0x073df69fa1b1)
+
#define BT_UUID_TRZ_SERVICE BT_UUID_DECLARE_128(BT_UUID_TRZ_VAL)
#define BT_UUID_TRZ_RX BT_UUID_DECLARE_128(BT_UUID_TRZ_RX_VAL)
#define BT_UUID_TRZ_TX BT_UUID_DECLARE_128(BT_UUID_TRZ_TX_VAL)
+#define BT_UUID_TRZ_NOTIFY BT_UUID_DECLARE_128(BT_UUID_TRZ_NOTIFY_VAL)
#define BLE_TX_PACKET_SIZE 244
#define BLE_RX_PACKET_SIZE 244
@@ -105,6 +110,7 @@ typedef enum {
INTERNAL_CMD_GET_BOND_LIST = 0x0B,
INTERNAL_CMD_SET_SPEED_HIGH = 0x0C,
INTERNAL_CMD_SET_SPEED_LOW = 0x0D,
+ INTERNAL_CMD_NOTIFY = 0x0E,
} internal_cmd_t;
typedef struct {
@@ -208,5 +214,7 @@ typedef void (*service_received_cb)(struct bt_conn *conn,
int service_init(service_received_cb callbacks);
// Send data to the connected device
int service_send(struct bt_conn *conn, trz_packet_t *data);
+// Notify listener of device state change
+int service_notify(struct bt_conn *conn, uint8_t *data, size_t len);
// Send hard-coded error response
void service_send_busy(void);
diff --git a/nordic/trezor/trezor-ble/src/ble/ble_management.c b/nordic/trezor/trezor-ble/src/ble/ble_management.c
index 090ad4a3..093d42ff 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble_management.c
+++ b/nordic/trezor/trezor-ble/src/ble/ble_management.c
@@ -212,6 +212,12 @@ static void process_command(uint8_t *data, uint16_t len) {
case INTERNAL_CMD_SET_SPEED_LOW: {
connection_set_low_speed();
} break;
+ case INTERNAL_CMD_NOTIFY: {
+ struct bt_conn *conn = connection_get_current();
+ if (conn != NULL) {
+ service_notify(conn, &data[1], len - 1);
+ }
+ } break;
default:
break;
}
diff --git a/nordic/trezor/trezor-ble/src/ble/service.c b/nordic/trezor/trezor-ble/src/ble/service.c
index 857140aa..d3f15a3b 100644
--- a/nordic/trezor/trezor-ble/src/ble/service.c
+++ b/nordic/trezor/trezor-ble/src/ble/service.c
@@ -61,6 +61,11 @@ BT_GATT_SERVICE_DEFINE(
BT_GATT_PERM_READ_ENCRYPT, NULL, NULL, NULL),
BT_GATT_CCC(service_ccc_cfg_changed,
BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT),
+
+ BT_GATT_CHARACTERISTIC(BT_UUID_TRZ_NOTIFY, BT_GATT_CHRC_NOTIFY,
+ BT_GATT_PERM_READ_ENCRYPT, NULL, NULL, NULL),
+ BT_GATT_CCC(service_ccc_cfg_changed,
+ BT_GATT_PERM_READ_ENCRYPT | BT_GATT_PERM_WRITE_ENCRYPT),
BT_GATT_CHARACTERISTIC(BT_UUID_TRZ_RX,
BT_GATT_CHRC_WRITE | BT_GATT_CHRC_WRITE_WITHOUT_RESP,
BT_GATT_PERM_READ_ENCRYPT |
@@ -89,6 +94,33 @@ int service_send(struct bt_conn *conn, trz_packet_t *data) {
}
}
+int service_notify(struct bt_conn *conn, uint8_t *data, size_t len) {
+ struct bt_gatt_notify_params params = {0};
+ const struct bt_gatt_attr *attr = &trz_svc.attrs[4];
+
+ uint8_t *buf = k_malloc(len);
+ memcpy(buf, data, len);
+
+ params.attr = attr;
+ params.data = buf;
+ params.len = len;
+ params.func = on_sent;
+ params.user_data = (void *)buf;
+
+ int result = 0;
+ if (conn && bt_gatt_is_subscribed(conn, attr, BT_GATT_CCC_NOTIFY)) {
+ result = bt_gatt_notify_cb(conn, ¶ms);
+ } else {
+ result = -EINVAL;
+ }
+
+ if (result < 0) {
+ k_free(buf);
+ }
+
+ return result;
+}
+
void service_send_busy(void) {
struct bt_conn *conn = connection_get_current();
Why this scored 27/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.