feat(core): implement button event emulation on driver level
What changed, and why it matters
This commit adds a debug-only feature that lets software simulate physical button presses (press, release, click) on Trezor hardware wallets. It is compiled in only when the DEBUGLINK build flag is set, which is intended for development and automated testing, not production firmware. The feature could let a connected debugging host or test harness inject fake button events into the normal button-handling path.
Verify that DEBUGLINK builds are never shipped to end users and that the debug link interface is disabled or authenticated in production. If DEBUGLINK can be enabled at runtime or via a bootloader/testmode path, add an explicit runtime authorization check before button_debug state is merged into button_get_event(). Review whether the debug queue should be isolated from real button events in security-sensitive flows such as PIN/seed confirmation.
Security signals we found
Synthetic button events are merged into the production button state path (state |= button_debug_get_state())
No authentication, authorization, or rate-limiting logic is visible in the injected event path
Feature is gated only by the DEBUGLINK compile-time macro, not runtime enablement
Emulated events use the same button_event_t structures and BTN_EVENT_DOWN/BTN_EVENT_UP types as real events
Queue overflow is logged but does not hard-fail or signal an error to callers
Evidence from the diff
The change introduces core/embed/io/button/button_debug.c/h, a small queue-backed state machine that enqueues synthetic BTN_EVENT_DOWN/BTN_EVENT_UP events and exposes a merged 32-bit button state. button_poll.c is modified so that, under #ifdef DEBUGLINK, the synthetic state is OR-ed into the real hardware button state before the button finite-state machine processes events. This means debug-emulated button events are indistinguishable from real button events to the rest of the firmware. A syslog config entry for the new module is also added.
Changed components
core/embed/io/button/button_poll.ccore/embed/io/button/button_debug.ccore/embed/io/button/button_debug.hcore/embed/sys/dbg/inc/sys/syslog_config.hInspect captured patch +185 / −0
diff --git a/core/embed/io/button/button_debug.c b/core/embed/io/button/button_debug.c
new file mode 100644
index 00000000..6e14cf30
--- /dev/null
+++ b/core/embed/io/button/button_debug.c
@@ -0,0 +1,92 @@
+/*
+ * 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/>.
+ */
+
+#include "button_debug.h"
+
+#include <trezor_rtl.h>
+
+#include <io/tsqueue.h>
+#include <sys/logging.h>
+
+LOG_DECLARE(button_debug)
+
+#define BUTTON_DEBUG_QUEUE_SIZE 8
+
+typedef struct {
+ button_event_t queue_items[BUTTON_DEBUG_QUEUE_SIZE];
+ tsqueue_entry_t queue_entries[BUTTON_DEBUG_QUEUE_SIZE];
+ tsqueue_t queue;
+ uint32_t state;
+} button_debug_t;
+
+static button_debug_t button_debug;
+
+void button_debug_init(void) {
+ memset(&button_debug, 0, sizeof(button_debug_t));
+ tsqueue_init(&button_debug.queue, button_debug.queue_entries,
+ (uint8_t*)button_debug.queue_items, sizeof(button_event_t),
+ BUTTON_DEBUG_QUEUE_SIZE);
+}
+
+void button_debug_deinit(void) {
+ memset(&button_debug, 0, sizeof(button_debug_t));
+}
+
+void button_debug_click(button_t button) {
+ button_debug_press(button);
+ button_debug_release(button);
+}
+
+void button_debug_press(button_t button) {
+ button_event_t event = {0};
+ event.button = button;
+ event.event_type = BTN_EVENT_DOWN;
+ if (!tsqueue_enqueue(&button_debug.queue, (uint8_t*)&event, sizeof(event),
+ NULL)) {
+ LOG_WARN("button debug queue full");
+ }
+}
+
+void button_debug_release(button_t button) {
+ button_event_t event = {0};
+ event.button = button;
+ event.event_type = BTN_EVENT_UP;
+ if (!tsqueue_enqueue(&button_debug.queue, (uint8_t*)&event, sizeof(event),
+ NULL)) {
+ LOG_WARN("button debug queue full");
+ }
+}
+
+void button_debug_next(void) {
+ button_event_t event = {0};
+
+ if (!tsqueue_dequeue(&button_debug.queue, (uint8_t*)&event, sizeof(event),
+ NULL, NULL)) {
+ return;
+ }
+
+ if (event.event_type == BTN_EVENT_DOWN) {
+ button_debug.state |= (1 << event.button);
+ }
+ if (event.event_type == BTN_EVENT_UP) {
+ button_debug.state &= ~(1 << event.button);
+ }
+}
+
+uint32_t button_debug_get_state(void) { return button_debug.state; }
diff --git a/core/embed/io/button/button_debug.h b/core/embed/io/button/button_debug.h
new file mode 100644
index 00000000..daf66896
--- /dev/null
+++ b/core/embed/io/button/button_debug.h
@@ -0,0 +1,67 @@
+/*
+ * 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/>.
+ */
+
+#pragma once
+
+#include <trezor_types.h>
+
+#include <io/button.h>
+
+/**
+ * @brief Initialize the button debug module.
+ */
+void button_debug_init(void);
+
+/**
+ * @brief Deinitialize the button debug module.
+ */
+void button_debug_deinit(void);
+
+/**
+ * @brief Signal a click event for the given button.
+ *
+ * @param button The button that was clicked.
+ */
+void button_debug_click(button_t button);
+
+/**
+ * @brief Signal a press event for the given button.
+ *
+ * @param button The button that was pressed.
+ */
+void button_debug_press(button_t button);
+
+/**
+ * @brief Signal a release event for the given button.
+ *
+ * @param button The button that was released.
+ */
+void button_debug_release(button_t button);
+
+/**
+ * @brief Advance to the next button debug state.
+ */
+void button_debug_next(void);
+
+/**
+ * @brief Get the current button debug state.
+ *
+ * @return The current state as a 32-bit unsigned integer.
+ */
+uint32_t button_debug_get_state(void);
diff --git a/core/embed/io/button/button_poll.c b/core/embed/io/button/button_poll.c
index 54e8e3bc..14839e98 100644
--- a/core/embed/io/button/button_poll.c
+++ b/core/embed/io/button/button_poll.c
@@ -28,6 +28,10 @@
#include "sys/sysevent_source.h"
+#ifdef DEBUGLINK
+#include "button_debug.h"
+#endif
+
typedef struct {
// Time of last update of pressed/released data
uint64_t time;
@@ -47,10 +51,18 @@ static button_fsm_t g_button_tls[SYSTASK_MAX_TASKS];
bool button_poll_init(void) {
memset(g_button_tls, 0, sizeof(g_button_tls));
+#ifdef DEBUGLINK
+ button_debug_init();
+#endif
+
return syshandle_register(SYSHANDLE_BUTTON, &g_button_handle_vmt, NULL);
}
void button_poll_deinit(void) {
+#ifdef DEBUGLINK
+ button_debug_deinit();
+#endif
+
memset(g_button_tls, 0, sizeof(g_button_tls));
syshandle_unregister(SYSHANDLE_BUTTON);
}
@@ -118,6 +130,10 @@ bool button_get_event(button_event_t* event) {
uint32_t new_state = button_get_state();
+#ifdef DEBUGLINK
+ new_state |= button_debug_get_state();
+#endif
+
button_fsm_t* fsm = &g_button_tls[systask_id(systask_active())];
return button_fsm_get_event(fsm, new_state, event);
}
@@ -133,6 +149,12 @@ static void on_event_poll(void* context, bool read_awaited,
if (read_awaited) {
uint32_t state = button_get_state();
+
+#ifdef DEBUGLINK
+ button_debug_next();
+ state |= button_debug_get_state();
+#endif
+
syshandle_signal_read_ready(SYSHANDLE_BUTTON, &state);
}
}
diff --git a/core/embed/sys/dbg/inc/sys/syslog_config.h b/core/embed/sys/dbg/inc/sys/syslog_config.h
index baa4f544..db9ef764 100644
--- a/core/embed/sys/dbg/inc/sys/syslog_config.h
+++ b/core/embed/sys/dbg/inc/sys/syslog_config.h
@@ -44,6 +44,10 @@
#define SYSLOG_bootutils_MAX_LOG_LEVEL SYSLOG_DEFAULT_LOG_LEVEL
#endif
+#ifndef SYSLOG_button_debug_MAX_LOG_LEVEL
+#define SYSLOG_button_debug_MAX_LOG_LEVEL SYSLOG_DEFAULT_LOG_LEVEL
+#endif
+
#ifndef SYSLOG_touch_driver_MAX_LOG_LEVEL
#define SYSLOG_touch_driver_MAX_LOG_LEVEL SYSLOG_DEFAULT_LOG_LEVEL
#endif
Why this scored 19/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.