feat(core): add telemetry logging of battery errors
What changed, and why it matters
This commit adds a new telemetry feature that records when the hardware wallet's battery subsystem reports problems, such as the battery being disconnected, temperature jumps, or charging being limited. It also exposes a new secure-monitor call so other parts of the firmware can report these errors. The change is a feature addition, not a fix for a known vulnerability, and the commit message does not claim any security relevance.
Treat as a routine feature commit. Reviewers should verify that the new SMCALL cannot be abused to cause excessive flash wear (the OR logic only writes when new bits are set, which mitigates repeated writes), confirm that the removed __verified wrapper does not weaken the trust boundary, and ensure the telemetry data structure version bump or migration is handled if the persisted record layout changed.
Security signals we found
New secure-monitor call (SMCALL_TELEMETRY_UPDATE_BATT_ERRORS) added for cross-privilege telemetry logging
Removal of __verified wrapper for telemetry_update_battery_temp, moving direct call into smcall dispatch
Telemetry storage now persists battery error bitflags in flash-backed storage
Bitfield union used for error flags; only lower 6 bits are currently defined, upper 2 bits of uint8 reserved/zero
No input validation on the bitfield value passed via SMCALL beyond the uint8 cast
Evidence from the diff
The patch introduces telemetry_batt_errors_t, a bitfield union tracking six battery/power-management error conditions, and adds telemetry_update_battery_errors() to persist them via the existing telemetry flash storage. The power-manager polling loop now ORs current battery state flags into telemetry on every update. A new SMCALL (SMCALL_TELEMETRY_UPDATE_BATT_ERRORS) is added to allow non-secure world code to log errors through the secure monitor. The previous telemetry_update_battery_temp__verified() verifier wrapper is removed because the new dispatcher calls the unverified function directly; telemetry_get__verified() remains for the read path. The telemetry record gains a versioned init helper and a new battery_errors field.
Changed components
core/embed/io/power_manager/power_manager_poll.ccore/embed/sec/telemetry/inc/sec/telemetry.hcore/embed/sec/telemetry/telemetry.ccore/embed/sys/smcall/stm32/smcall_dispatch.ccore/embed/sys/smcall/stm32/smcall_numbers.hcore/embed/sys/smcall/stm32/smcall_stubs.ccore/embed/sys/smcall/stm32/smcall_verifiers.ccore/embed/sys/smcall/stm32/smcall_verifiers.hInspect captured patch +79 / −17
diff --git a/core/embed/io/power_manager/power_manager_poll.c b/core/embed/io/power_manager/power_manager_poll.c
index c4117b694..20df9c603 100644
--- a/core/embed/io/power_manager/power_manager_poll.c
+++ b/core/embed/io/power_manager/power_manager_poll.c
@@ -25,6 +25,10 @@
#include <sys/sysevent_source.h>
#include <sys/systick.h>
+#ifdef USE_TELEMETRY
+#include <sec/telemetry.h>
+#endif
+
#include "power_manager_poll.h"
typedef struct {
@@ -159,6 +163,19 @@ static bool pm_fsm_update(pm_fsm_t* fsm, pm_state_t* new_state) {
fsm->events.flags.battery_connected_changed = true;
}
+#ifdef USE_TELEMETRY
+ telemetry_batt_errors_t errors = {0};
+ errors.bits.ntc_disconnected = !new_state->ntc_connected;
+ errors.bits.charging_limited = new_state->charging_limited;
+ errors.bits.battery_disconnected = !new_state->battery_connected;
+ errors.bits.temp_control_active = new_state->temp_control_active;
+ errors.bits.battery_ocv_jump_detected =
+ fsm->events.flags.battery_ocv_jump_detected;
+ errors.bits.battery_temp_jump_detected =
+ fsm->events.flags.battery_temp_jump_detected;
+ telemetry_update_battery_errors(errors);
+#endif
+
fsm->last_state = *new_state;
return fsm->events.all != 0;
diff --git a/core/embed/sec/telemetry/inc/sec/telemetry.h b/core/embed/sec/telemetry/inc/sec/telemetry.h
index aec62fb98..57d16b303 100644
--- a/core/embed/sec/telemetry/inc/sec/telemetry.h
+++ b/core/embed/sec/telemetry/inc/sec/telemetry.h
@@ -21,12 +21,25 @@
#include <trezor_types.h>
+typedef union {
+ uint8_t all;
+ struct {
+ bool ntc_disconnected : 1;
+ bool charging_limited : 1;
+ bool temp_control_active : 1;
+ bool battery_disconnected : 1;
+ bool battery_temp_jump_detected : 1;
+ bool battery_ocv_jump_detected : 1;
+ } bits;
+} telemetry_batt_errors_t;
+
/**
* @brief Telemetry data structure.
*/
typedef struct {
float min_temp_c; /**< Minimum recorded battery temperature in Celsius. */
float max_temp_c; /**< Maximum recorded battery temperature in Celsius. */
+ telemetry_batt_errors_t battery_errors; /**< Bitfield of battery errors. */
} telemetry_data_t;
/**
@@ -42,7 +55,16 @@ typedef struct {
void telemetry_update_battery_temp(float temp_c);
/**
- * @brief Retrieve stored min/max battery temperature (in Celsius).
+ * @brief Record battery errors into telemetry storage.
+ *
+ * The flags are ORed with the existing errors.
+ *
+ * @param errors Power management errors as a bitfield.
+ */
+void telemetry_update_battery_errors(telemetry_batt_errors_t errors);
+
+/**
+ * @brief Retrieve stored telemetry data.
*
* @param[out] out Pointer to where the telemetry data will be stored (may be
* NULL).
diff --git a/core/embed/sec/telemetry/telemetry.c b/core/embed/sec/telemetry/telemetry.c
index a327f42b4..9524877db 100644
--- a/core/embed/sec/telemetry/telemetry.c
+++ b/core/embed/sec/telemetry/telemetry.c
@@ -53,18 +53,23 @@ static bool telemetry_write(const telemetry_t* data) {
data, sizeof(*data));
}
+static void telemetry_init_record(void) {
+ telemetry_t telemetry;
+ telemetry.version = TELEMETRY_DATA_VERSION;
+ telemetry.initialized = 1;
+ telemetry.reserved = 0;
+ telemetry.data.min_temp_c = 500.0f;
+ telemetry.data.max_temp_c = -500.0f;
+ telemetry.data.battery_errors.all = 0;
+ telemetry_write(&telemetry);
+}
+
void telemetry_update_battery_temp(float temp_c) {
telemetry_t telemetry;
bool have = telemetry_read(&telemetry) && telemetry.initialized == 1;
if (!have) {
- telemetry.version = TELEMETRY_DATA_VERSION;
- telemetry.initialized = 1;
- telemetry.reserved = 0;
- telemetry.data.min_temp_c = temp_c;
- telemetry.data.max_temp_c = temp_c;
- telemetry_write(&telemetry);
- return;
+ telemetry_init_record();
}
bool changed = false;
@@ -82,6 +87,22 @@ void telemetry_update_battery_temp(float temp_c) {
}
}
+void telemetry_update_battery_errors(telemetry_batt_errors_t errors) {
+ telemetry_t telemetry;
+ bool have = telemetry_read(&telemetry) && telemetry.initialized == 1;
+
+ if (!have) {
+ telemetry_init_record();
+ }
+
+ // Only update and write if some of OUR flags are set
+ if (errors.all != 0 &&
+ ((telemetry.data.battery_errors.all & errors.all) != errors.all)) {
+ telemetry.data.battery_errors.all |= errors.all;
+ telemetry_write(&telemetry);
+ }
+}
+
bool telemetry_get(telemetry_data_t* out) {
telemetry_t telemetry;
if (!telemetry_read(&telemetry) || telemetry.initialized != 1) {
diff --git a/core/embed/sys/smcall/stm32/smcall_dispatch.c b/core/embed/sys/smcall/stm32/smcall_dispatch.c
index 4a87cc944..e5b43ba8e 100644
--- a/core/embed/sys/smcall/stm32/smcall_dispatch.c
+++ b/core/embed/sys/smcall/stm32/smcall_dispatch.c
@@ -413,7 +413,12 @@ __attribute((no_stack_protector)) void smcall_handler(uint32_t *args,
} u32_to_float = {.u = args[0]};
float temp = u32_to_float.f;
- telemetry_update_battery_temp__verified(temp);
+ telemetry_update_battery_temp(temp);
+ } break;
+
+ case SMCALL_TELEMETRY_UPDATE_BATT_ERRORS: {
+ telemetry_batt_errors_t errors = {.all = args[0]};
+ telemetry_update_battery_errors(errors);
} break;
case SMCALL_TELEMETRY_GET: {
diff --git a/core/embed/sys/smcall/stm32/smcall_numbers.h b/core/embed/sys/smcall/stm32/smcall_numbers.h
index 8e53b4618..74a432242 100644
--- a/core/embed/sys/smcall/stm32/smcall_numbers.h
+++ b/core/embed/sys/smcall/stm32/smcall_numbers.h
@@ -105,6 +105,7 @@ typedef enum {
SMCALL_SECRET_KEYS_GET_DELEGATED_IDENTITY_KEY,
SMCALL_TELEMETRY_UPDATE_BATT_TEMP,
+ SMCALL_TELEMETRY_UPDATE_BATT_ERRORS,
SMCALL_TELEMETRY_GET,
} smcall_number_t;
diff --git a/core/embed/sys/smcall/stm32/smcall_stubs.c b/core/embed/sys/smcall/stm32/smcall_stubs.c
index be1fd81e3..d60b6c8e1 100644
--- a/core/embed/sys/smcall/stm32/smcall_stubs.c
+++ b/core/embed/sys/smcall/stm32/smcall_stubs.c
@@ -424,6 +424,10 @@ void telemetry_update_battery_temp(float temp_c) {
smcall_invoke1(float_to_u32.u, SMCALL_TELEMETRY_UPDATE_BATT_TEMP);
}
+void telemetry_update_battery_errors(telemetry_batt_errors_t errors) {
+ smcall_invoke1((uint32_t)errors.all, SMCALL_TELEMETRY_UPDATE_BATT_ERRORS);
+}
+
bool telemetry_get(telemetry_data_t *out) {
return (bool)smcall_invoke1((uint32_t)out, SMCALL_TELEMETRY_GET);
}
diff --git a/core/embed/sys/smcall/stm32/smcall_verifiers.c b/core/embed/sys/smcall/stm32/smcall_verifiers.c
index b981d2405..d7c7d7442 100644
--- a/core/embed/sys/smcall/stm32/smcall_verifiers.c
+++ b/core/embed/sys/smcall/stm32/smcall_verifiers.c
@@ -577,12 +577,6 @@ access_violation:
#ifdef USE_TELEMETRY
#include <sec/telemetry.h>
-// Telemetry verifiers
-void telemetry_update_battery_temp__verified(float temp_c) {
- // No pointers to verify; simple value pass-through
- telemetry_update_battery_temp(temp_c);
-}
-
bool telemetry_get__verified(telemetry_data_t *out) {
if (out != NULL && !probe_write_access(out, sizeof(*out))) {
goto access_violation;
diff --git a/core/embed/sys/smcall/stm32/smcall_verifiers.h b/core/embed/sys/smcall/stm32/smcall_verifiers.h
index 4c5fb653f..5a919e8fd 100644
--- a/core/embed/sys/smcall/stm32/smcall_verifiers.h
+++ b/core/embed/sys/smcall/stm32/smcall_verifiers.h
@@ -171,8 +171,6 @@ secbool secret_validate_nrf_pairing__verified(const uint8_t *message,
#include <sec/telemetry.h>
-void telemetry_update_battery_temp__verified(float temp_c);
-
bool telemetry_get__verified(telemetry_data_t *out);
#endif // USE_TELEMETRY
Why this scored 21/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.