feat(nordic/ble): enable controlling TX power level from main MCU
What changed, and why it matters
This commit adds a feature that lets the main Trezor MCU tell the Bluetooth chip what radio transmit power to use. It is a normal feature addition and does not, by itself, look like a security vulnerability. The only notable risk is that if an attacker could already send malicious commands to the internal Bluetooth-to-main-MCU channel, they might be able to lower the Bluetooth signal strength, which could make the device harder to detect or more likely to disconnect unexpectedly. There is no evidence in the commit that such command injection is possible, and the commit does not claim to fix a security issue.
Treat as a feature commit, not a security patch. If reviewing for security, verify that the internal SPI/UART channel between the main MCU and Nordic BLE chip is authenticated and that only the main MCU can issue INTERNAL_CMD_SET_TX_POWER. Consider adding explicit bounds checking on the requested power level before calling ble_configure_tx_power(), even if the HCI layer currently clamps values, to make the code more robust against future controller changes.
Security signals we found
New internal command 0x10 added to BLE management interface
No explicit bounds validation on tx_power_level before HCI call
Relies on Zephyr HCI vendor command to enforce valid TX power levels
Default TX power set to +4 dBm
Status event now includes current power_level field
Evidence from the diff
The change introduces a new internal command INTERNAL_CMD_SET_TX_POWER (0x10) and a corresponding ble_set_tx_power() API on the STM32 main MCU side. The Nordic BLE firmware receives the command, validates the requested level by passing it to Zephyr’s vendor-specific HCI command BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, and reports the actually selected level back in status events. The default power level is +4 dBm. The implementation does not appear to perform explicit bounds checking on the requested int8_t level before passing it to the HCI layer, but the underlying Zephyr HCI command is expected to clamp or reject invalid values. No buffer overflow, use-after-free, or authentication bypass is visible in the diff.
Changed components
core/embed/io/ble/stm32/ble.ccore/embed/io/ble/stm32/ble_comm_defs.hcore/embed/io/ble/inc/io/ble.hnordic/trezor/trezor-ble/src/ble/ble.cnordic/trezor/trezor-ble/src/ble/ble_internal.hnordic/trezor/trezor-ble/src/ble/ble_management.cnordic/trezor/trezor-ble/src/ble/connection.cnordic/trezor/trezor-ble/prj.confInspect captured patch +138 / −1
diff --git a/core/embed/io/ble/inc/io/ble.h b/core/embed/io/ble/inc/io/ble.h
index 0068f3f58..1f981fff0 100644
--- a/core/embed/io/ble/inc/io/ble.h
+++ b/core/embed/io/ble/inc/io/ble.h
@@ -53,6 +53,15 @@ typedef enum {
BLE_MODE_DFU,
} ble_mode_t;
+typedef enum {
+ BLE_TX_POWER_PLUS_4_DBM = 4,
+ BLE_TX_POWER_PLUS_0_DBM = 0,
+ BLE_TX_POWER_MINUS_4_DBM = -4,
+ BLE_TX_POWER_MINUS_8_DBM = -8,
+ BLE_TX_POWER_MINUS_12_DBM = -12,
+ BLE_TX_POWER_MINUS_16_DBM = -16,
+} ble_tx_power_level_t;
+
/*
* Address types:
* BT_ADDR_LE_PUBLIC 0x00
@@ -217,6 +226,11 @@ bool ble_get_mac(bt_le_addr_t *addr);
// higher data throughput, at the cost of increased power consumption.
void ble_set_high_speed(bool enable);
+// Set TX power
+//
+// Set TX power level
+void ble_set_tx_power(ble_tx_power_level_t level);
+
// BLE notify
//
// Sends notification to host over BLE
diff --git a/core/embed/io/ble/stm32/ble.c b/core/embed/io/ble/stm32/ble.c
index 0b9cd65dc..a752819d6 100644
--- a/core/embed/io/ble/stm32/ble.c
+++ b/core/embed/io/ble/stm32/ble.c
@@ -85,6 +85,7 @@ typedef struct {
bt_le_addr_t mac;
bool mac_ready;
bool high_speed;
+ ble_tx_power_level_t power_level;
uint8_t bond_count;
bt_le_addr_t bonds[BLE_MAX_BONDS];
@@ -153,6 +154,14 @@ static bool ble_send_speed_request(ble_driver_t *drv, bool high_speed) {
0;
}
+static bool ble_send_power_level_request(ble_driver_t *drv,
+ ble_tx_power_level_t power_level) {
+ (void)drv;
+ uint8_t cmd[2] = {INTERNAL_CMD_SET_TX_POWER, (uint8_t)power_level};
+ return nrf_send_msg(NRF_SERVICE_BLE_MANAGER, cmd, sizeof(cmd), NULL, NULL) >=
+ 0;
+}
+
static bool ble_send_advertising_off(ble_driver_t *drv) {
(void)drv;
uint8_t cmd = INTERNAL_CMD_ADVERTISING_OFF;
@@ -369,6 +378,11 @@ static void ble_process_rx_msg_status(const uint8_t *data, uint32_t len) {
ble_send_speed_request(drv, drv->high_speed);
}
+ // in case power level differs from request, send a command
+ if ((ble_tx_power_level_t)msg.power_level != drv->power_level) {
+ ble_send_power_level_request(drv, drv->power_level);
+ }
+
drv->status_valid = true;
}
@@ -645,6 +659,7 @@ bool ble_init(void) {
goto cleanup;
}
+ drv->power_level = BLE_TX_POWER_PLUS_4_DBM;
drv->initialized = true;
return true;
@@ -1211,6 +1226,17 @@ void ble_set_high_speed(bool enable) {
irq_unlock(key);
}
+void ble_set_tx_power(ble_tx_power_level_t level) {
+ ble_driver_t *drv = &g_ble_driver;
+ if (!drv->initialized) {
+ return;
+ }
+
+ irq_key_t key = irq_lock();
+ drv->power_level = level;
+ irq_unlock(key);
+}
+
void ble_notify(const uint8_t *data, size_t len) {
ble_driver_t *drv = &g_ble_driver;
if (!drv->initialized) {
diff --git a/core/embed/io/ble/stm32/ble_comm_defs.h b/core/embed/io/ble/stm32/ble_comm_defs.h
index 9436b6322..96ef8d0c1 100644
--- a/core/embed/io/ble/stm32/ble_comm_defs.h
+++ b/core/embed/io/ble/stm32/ble_comm_defs.h
@@ -46,6 +46,8 @@ typedef struct {
uint8_t connected_addr[6]; // MAC address of the connected device
uint8_t connected_addr_type;
+
+ int8_t power_level;
} event_status_msg_t;
typedef enum {
@@ -76,6 +78,7 @@ typedef enum {
INTERNAL_CMD_SET_SPEED_LOW = 0x0D,
INTERNAL_CMD_NOTIFY = 0x0E,
INTERNAL_CMD_BATTERY_UPDATE = 0x0F,
+ INTERNAL_CMD_SET_TX_POWER = 0x10,
} internal_cmd_t;
typedef struct {
diff --git a/nordic/trezor/trezor-ble/prj.conf b/nordic/trezor/trezor-ble/prj.conf
index c7e2b1410..f23998a92 100644
--- a/nordic/trezor/trezor-ble/prj.conf
+++ b/nordic/trezor/trezor-ble/prj.conf
@@ -76,6 +76,7 @@ CONFIG_BT_DIS_STR_MAX=21
#PHY update needed for updating PHY request
CONFIG_BT_PHY_UPDATE=y
CONFIG_BT_USER_PHY_UPDATE=y
+CONFIG_BT_CTLR_TX_PWR_DYNAMIC_CONTROL=y
# HCI ACL buffers size
CONFIG_BT_BUF_ACL_RX_SIZE=251
diff --git a/nordic/trezor/trezor-ble/src/ble/ble.c b/nordic/trezor/trezor-ble/src/ble/ble.c
index 73579af04..9f03942e2 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble.c
+++ b/nordic/trezor/trezor-ble/src/ble/ble.c
@@ -20,11 +20,13 @@
#include <zephyr/logging/log.h>
#include <zephyr/settings/settings.h>
#include <zephyr/sys/atomic.h>
+#include <zephyr/sys/byteorder.h>
#include <zephyr/bluetooth/bluetooth.h>
#include <zephyr/bluetooth/conn.h>
#include <zephyr/bluetooth/gatt.h>
#include <zephyr/bluetooth/hci.h>
+#include <zephyr/bluetooth/hci_vs.h>
#include <zephyr/bluetooth/uuid.h>
#include <zephyr/settings/settings.h>
@@ -46,6 +48,14 @@ static K_SEM_DEFINE(ble_init_ok, 0, 1);
atomic_t g_busy_flag = ATOMIC_INIT(0);
+#if IS_ENABLED(CONFIG_BT_CTLR_TX_PWR_PLUS_4)
+static int8_t g_act_tx_power_level = 4;
+static int8_t g_set_tx_power_level = 4;
+#else
+static int8_t g_act_tx_power_level = 0;
+static int8_t g_set_tx_power_level = 0;
+#endif
+
static void bt_receive_cb(struct bt_conn *conn, const uint8_t *const data,
uint16_t len) {
if (atomic_get(&g_busy_flag) != 0) {
@@ -151,5 +161,71 @@ void ble_set_busy_flag(uint8_t flag) { atomic_set(&g_busy_flag, flag); }
uint8_t ble_get_busy_flag(void) { return atomic_get(&g_busy_flag); }
+static int ble_configure_tx_power(int8_t tx_power_level, struct bt_conn *conn) {
+ struct bt_hci_cp_vs_write_tx_power_level *cp;
+ struct bt_hci_rp_vs_write_tx_power_level *rp;
+ struct net_buf *buf, *rsp = NULL;
+ int err;
+
+ buf = bt_hci_cmd_create(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, sizeof(*cp));
+ if (!buf) {
+ LOG_ERR("Unable to allocate command buffer for TX power");
+ return -ENOMEM;
+ }
+
+ cp = net_buf_add(buf, sizeof(*cp));
+
+ if (conn == NULL) {
+ // No connection, set for advertising
+ cp->handle = sys_cpu_to_le16(0); // Handle 0 for advertising
+ cp->handle_type = BT_HCI_VS_LL_HANDLE_TYPE_ADV; // Advertising handle type
+ } else {
+ uint16_t handle = 0;
+ bt_hci_get_conn_handle(conn, &handle);
+ cp->handle = handle; // Connection handle
+ cp->handle_type = BT_HCI_VS_LL_HANDLE_TYPE_CONN; // Connection handle type
+ }
+ cp->tx_power_level = tx_power_level;
+
+ err = bt_hci_cmd_send_sync(BT_HCI_OP_VS_WRITE_TX_POWER_LEVEL, buf, &rsp);
+ if (err) {
+ LOG_ERR("Set TX power failed: %d", err);
+ return err;
+ }
+
+ if (rsp) {
+ rp = (void *)rsp->data;
+ LOG_INF("Actual TX Power set to: %d dBm", rp->selected_tx_power);
+ net_buf_unref(rsp);
+ g_act_tx_power_level = rp->selected_tx_power;
+ }
+ return 0;
+}
+
+int8_t ble_get_tx_power(void) { return g_act_tx_power_level; }
+
+int ble_set_tx_power(int8_t tx_power_level) {
+ g_set_tx_power_level = tx_power_level;
+
+ int8_t res = ble_configure_tx_power(tx_power_level, NULL);
+
+ struct bt_conn *conn = connection_get_current();
+
+ if (conn != NULL) {
+ return ble_configure_tx_power(tx_power_level, conn);
+ }
+ return res;
+}
+
+int ble_reconfigure_tx_power(void) {
+ struct bt_conn *conn = connection_get_current();
+
+ if (conn != NULL) {
+ return ble_configure_tx_power(g_set_tx_power_level, conn);
+ }
+
+ return -1;
+}
+
K_THREAD_DEFINE(ble_write_thread_id, CONFIG_DEFAULT_THREAD_STACK_SIZE,
ble_write_thread, NULL, NULL, NULL, 7, 0, 0);
diff --git a/nordic/trezor/trezor-ble/src/ble/ble_internal.h b/nordic/trezor/trezor-ble/src/ble/ble_internal.h
index bfdb28f4a..a3cca65ab 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble_internal.h
+++ b/nordic/trezor/trezor-ble/src/ble/ble_internal.h
@@ -82,6 +82,7 @@ typedef struct {
uint8_t connected_addr[6]; // MAC address of the connected device
uint8_t connected_addr_type;
+ int8_t power_level;
} event_status_msg_t;
typedef enum {
@@ -112,6 +113,7 @@ typedef enum {
INTERNAL_CMD_SET_SPEED_LOW = 0x0D,
INTERNAL_CMD_NOTIFY = 0x0E,
INTERNAL_CMD_BATTERY_UPDATE = 0x0F,
+ INTERNAL_CMD_SET_TX_POWER = 0x10,
} internal_cmd_t;
typedef struct {
@@ -143,6 +145,12 @@ void ble_set_busy_flag(uint8_t flag);
uint8_t ble_get_busy_flag(void);
+int ble_set_tx_power(int8_t tx_power_level);
+
+int8_t ble_get_tx_power(void);
+
+int ble_reconfigure_tx_power(void);
+
// BLE management functions
// Initialization
void ble_management_init(void);
diff --git a/nordic/trezor/trezor-ble/src/ble/ble_management.c b/nordic/trezor/trezor-ble/src/ble/ble_management.c
index 997c63d93..f482abc70 100644
--- a/nordic/trezor/trezor-ble/src/ble/ble_management.c
+++ b/nordic/trezor/trezor-ble/src/ble/ble_management.c
@@ -73,6 +73,8 @@ void ble_management_send_status_event(void) {
msg.connected_addr_type = 0;
}
+ msg.power_level = ble_get_tx_power();
+
trz_comm_send_msg(NRF_SERVICE_BLE_MANAGER, (uint8_t *)&msg, sizeof(msg));
}
@@ -226,7 +228,12 @@ static void process_command(uint8_t *data, uint16_t len) {
case INTERNAL_CMD_BATTERY_UPDATE: {
management_update_battery(data[1]);
} break;
- ;
+ case INTERNAL_CMD_SET_TX_POWER: {
+ int8_t tx_power = (int8_t)data[1];
+ if (ble_set_tx_power(tx_power) != 0) {
+ success = false;
+ }
+ } break;
default:
break;
}
diff --git a/nordic/trezor/trezor-ble/src/ble/connection.c b/nordic/trezor/trezor-ble/src/ble/connection.c
index 7121aa4fc..344d9d126 100644
--- a/nordic/trezor/trezor-ble/src/ble/connection.c
+++ b/nordic/trezor/trezor-ble/src/ble/connection.c
@@ -83,6 +83,8 @@ void connected(struct bt_conn *conn, uint8_t err) {
connection_update_params();
+ ble_reconfigure_tx_power();
+
// Prefer 2M both directions; 0 options = no specific constraints
// const struct bt_conn_le_phy_param phy_2m = {
// .options = 0,
Why this scored 17/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.