feat(core): allow reading out of telemetry data
What changed, and why it matters
This commit adds a new read-only feature that lets the host computer ask a Trezor device for internal telemetry records: minimum/maximum temperature and battery error counts. It is a feature addition, not a fix. There is no direct evidence in the commit that this introduces a security vulnerability, but it does expose previously internal device data over the USB/Protobuf interface. The security relevance depends on whether the new TelemetryGet message is protected by an existing session/access policy or can be requested by any connected host.
Verify whether TelemetryGet requires an authorized session or unlocked device before returning telemetry. If it can be invoked by any connected host, consider adding a session-policy check or requiring device unlock. Review the telemetry_get C implementation for side channels and ensure the returned battery_errors bitfield does not leak sensitive diagnostic information. Add a changelog entry for traceability.
Security signals we found
New host-facing Protobuf message handler added without visible in-handler authorization
Previously internal secure-subsystem data (battery_errors, temperature extrema) exposed to host
Feature is model-gated (T3W1 revC) and compile-time conditional, limiting exposure
No changelog entry provided despite user-visible feature addition
No input parsing or memory allocation beyond fixed-size tuple creation
Evidence from the diff
The patch wires a new TelemetryGet Protobuf message to a Python handler that calls a C helper (telemetry_get) to read telemetry_data_t from the secure telemetry subsystem and return min_temp_c, max_temp_c, and battery_errors. It is gated by the USE_TELEMETRY feature flag, currently enabled for T3W1 revC. The handler is registered in workflow_handlers.py alongside other management messages. The commit does not show any authorization check, user confirmation, or session gating inside get_telemetry.py, but the surrounding framework may enforce such checks elsewhere. No buffer handling, parsing of attacker-controlled data, or memory safety issues are visible in the diff.
Changed components
core/src/apps/telemetry/get_telemetry.pycore/src/apps/workflow_handlers.pycore/embed/upymod/modtrezorutils/modtrezorutils.ccore/site_scons/models/T3W1/trezor_t3w1_revC.pycore/src/trezor/utils.pycore/SConscript.firmwarecore/embed/rust/Cargo.tomlInspect captured patch +80 / −0
diff --git a/core/SConscript.firmware b/core/SConscript.firmware
index 386d7aa3..719135e2 100644
--- a/core/SConscript.firmware
+++ b/core/SConscript.firmware
@@ -67,6 +67,7 @@ FEATURES_WANTED = [
"serial_number",
"storage",
"suspend",
+ 'telemetry',
"tropic",
"usb",
"usb_iface_wire",
@@ -748,6 +749,8 @@ if FROZEN:
))
)
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/misc/*.py'))
+ if 'telemetry' in FEATURES_AVAILABLE:
+ SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/telemetry/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/*.py'))
SOURCE_PY.extend(Glob(SOURCE_PY_DIR + 'apps/bitcoin/*/*.py',
exclude=[
diff --git a/core/embed/rust/Cargo.toml b/core/embed/rust/Cargo.toml
index 9ebd6bab..40595f0e 100644
--- a/core/embed/rust/Cargo.toml
+++ b/core/embed/rust/Cargo.toml
@@ -54,6 +54,7 @@ smp = []
tropic = []
serial_number = []
storage = []
+telemetry = []
translations = ["crypto"]
secmon_layout = []
dbg_console = []
diff --git a/core/embed/upymod/modtrezorutils/modtrezorutils.c b/core/embed/upymod/modtrezorutils/modtrezorutils.c
index 44ac582b..2db078fe 100644
--- a/core/embed/upymod/modtrezorutils/modtrezorutils.c
+++ b/core/embed/upymod/modtrezorutils/modtrezorutils.c
@@ -46,6 +46,10 @@
#include "blake2s.h"
#include "memzero.h"
+#ifdef USE_TELEMETRY
+#include <sec/telemetry.h>
+#endif
+
#ifdef USE_BLE
#include <io/ble.h>
#endif
@@ -63,6 +67,30 @@
/// from trezor import utils
+#ifdef USE_TELEMETRY
+/// def telemetry_get() -> tuple[int, int, int] | None:
+/// """
+/// Retrieves the stored telemetry data. Returns a tuple
+/// (min_temp_milli_c, max_temp_milli_c, battery_errors)
+/// or None if telemetry is not available.
+/// """
+STATIC mp_obj_t mod_trezorutils_telemetry_get(void) {
+ telemetry_data_t data;
+ if (!telemetry_get(&data)) {
+ return mp_const_none;
+ }
+
+ mp_obj_t tuple[3];
+ tuple[0] = mp_obj_new_int((int32_t)(data.min_temp_c * 1000.0f));
+ tuple[1] = mp_obj_new_int((int32_t)(data.max_temp_c * 1000.0f));
+ tuple[2] = mp_obj_new_int(data.battery_errors.all);
+
+ return mp_obj_new_tuple(3, tuple);
+}
+STATIC MP_DEFINE_CONST_FUN_OBJ_0(mod_trezorutils_telemetry_get_obj,
+ mod_trezorutils_telemetry_get);
+#endif
+
/// def consteq(sec: AnyBytes, pub: AnyBytes) -> bool:
/// """
/// Compares the private information in `sec` with public, user-provided
@@ -763,6 +791,8 @@ STATIC const mp_obj_tuple_t mod_trezorutils_version_obj = {
/// """Whether a debug console is enabled."""
/// USE_APP_LOADING: bool
/// """Whether the firmware supports loading 3rd-party applications."""
+/// USE_TELEMETRY: bool
+/// """Whether a telemetry is supported."""
/// MODEL: str
/// """Model name."""
/// MODEL_FULL_NAME: str
@@ -831,6 +861,13 @@ STATIC const mp_rom_map_elem_t mp_module_trezorutils_globals_table[] = {
MP_ROM_PTR(&mod_trezorutils_bootloader_locked_obj)},
{MP_ROM_QSTR(MP_QSTR_notify_send),
MP_ROM_PTR(&mod_trezorutils_notify_send_obj)},
+#ifdef USE_TELEMETRY
+ {MP_ROM_QSTR(MP_QSTR_telemetry_get),
+ MP_ROM_PTR(&mod_trezorutils_telemetry_get_obj)},
+ {MP_ROM_QSTR(MP_QSTR_USE_TELEMETRY), mp_const_true},
+#else
+ {MP_ROM_QSTR(MP_QSTR_USE_TELEMETRY), mp_const_false},
+#endif
{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)},
diff --git a/core/mocks/generated/trezorutils.pyi b/core/mocks/generated/trezorutils.pyi
index d59a0c5f..942b2a8b 100644
--- a/core/mocks/generated/trezorutils.pyi
+++ b/core/mocks/generated/trezorutils.pyi
@@ -10,6 +10,15 @@ def meminfo(filename: str | None) -> None:
from trezor import utils
+# upymod/modtrezorutils/modtrezorutils.c
+def telemetry_get() -> tuple[int, int, int] | None:
+ """
+ Retrieves the stored telemetry data. Returns a tuple
+ (min_temp_milli_c, max_temp_milli_c, battery_errors)
+ or None if telemetry is not available.
+ """
+
+
# upymod/modtrezorutils/modtrezorutils.c
def consteq(sec: AnyBytes, pub: AnyBytes) -> bool:
"""
@@ -268,6 +277,8 @@ USE_DBG_CONSOLE: bool
"""Whether a debug console is enabled."""
USE_APP_LOADING: bool
"""Whether the firmware supports loading 3rd-party applications."""
+USE_TELEMETRY: bool
+"""Whether a telemetry is supported."""
MODEL: str
"""Model name."""
MODEL_FULL_NAME: str
diff --git a/core/site_scons/models/T3W1/trezor_t3w1_revC.py b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
index f05ed85a..b559fd60 100644
--- a/core/site_scons/models/T3W1/trezor_t3w1_revC.py
+++ b/core/site_scons/models/T3W1/trezor_t3w1_revC.py
@@ -241,6 +241,7 @@ def configure(
sources += ["embed/sec/telemetry/telemetry.c"]
paths += ["embed/sec/telemetry/inc"]
defines += [("USE_TELEMETRY", "1")]
+ features_available.append("telemetry")
defines += [
"FRAMEBUFFER",
diff --git a/core/src/apps/telemetry/__init__.py b/core/src/apps/telemetry/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/core/src/apps/telemetry/get_telemetry.py b/core/src/apps/telemetry/get_telemetry.py
new file mode 100644
index 00000000..143b061c
--- /dev/null
+++ b/core/src/apps/telemetry/get_telemetry.py
@@ -0,0 +1,20 @@
+from typing import TYPE_CHECKING
+
+if TYPE_CHECKING:
+ from trezor.messages import Telemetry, TelemetryGet
+
+
+async def get_telemetry(msg: TelemetryGet) -> Telemetry:
+ from trezor.messages import Telemetry
+ from trezor.utils import telemetry_get
+
+ data = telemetry_get()
+ if data:
+ min_temp_c, max_temp_c, battery_errors = data
+ return Telemetry(
+ min_temp_c=min_temp_c,
+ max_temp_c=max_temp_c,
+ battery_errors=battery_errors,
+ )
+ else:
+ return Telemetry()
diff --git a/core/src/apps/workflow_handlers.py b/core/src/apps/workflow_handlers.py
index b18596b3..59f64429 100644
--- a/core/src/apps/workflow_handlers.py
+++ b/core/src/apps/workflow_handlers.py
@@ -64,6 +64,9 @@ def _find_message_handler_module(msg_type: int) -> str:
if msg_type == MessageType.ShowDeviceTutorial:
return "apps.management.show_tutorial"
+ if utils.USE_TELEMETRY and msg_type == MessageType.TelemetryGet:
+ return "apps.telemetry.get_telemetry"
+
if utils.USE_BACKLIGHT and msg_type == MessageType.SetBrightness:
return "apps.management.set_brightness"
diff --git a/core/src/trezor/utils.py b/core/src/trezor/utils.py
index 062be0a3..6a495f2e 100644
--- a/core/src/trezor/utils.py
+++ b/core/src/trezor/utils.py
@@ -33,6 +33,7 @@ from trezorutils import ( # noqa: F401
USE_RGB_LED,
USE_SD_CARD,
USE_SERIAL_NUMBER,
+ USE_TELEMETRY,
USE_THP,
USE_TOUCH,
USE_TROPIC,
@@ -57,6 +58,9 @@ from trezorutils import ( # noqa: F401
unit_production_date,
)
+if USE_TELEMETRY:
+ from trezorutils import telemetry_get # noqa: F401
+
if USE_NRF:
from trezorutils import nrf_get_version # noqa: F401
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.