test(core): allow restarting core models via DebugLink
What changed, and why it matters
This commit adds a test-only debug feature that lets automated tests restart the Trezor device's main software loop. It is not a normal user feature and only works through the special DebugLink interface used during testing. The change is explicitly for improving backup-failure tests and is marked with '[no changelog]', meaning it is not intended for end users.
No immediate action required. Treat as test-infrastructure change. If auditing, verify that DebugLink is disabled in production builds and that DebugLinkStop cannot be reached from the normal USB/message stack.
Security signals we found
Debug-only code path: handler is inside `if __debug__:` block and only reachable via DebugLink
New message type DebugLinkStop added to debug dispatch table
RestartEventLoop exception used to reset device event loop without wiping storage
Client-side helper asserts model is in CORE_MODELS and syncs THP responses before restart
No changelog entry; explicitly test infrastructure
Evidence from the diff
The patch introduces a new DebugLink message handler, DebugLinkStop, in core/src/apps/debug/init.py. When received, it raises RestartEventLoop, causing the MicroPython event loop to restart. The python/src/trezorlib/debuglink.py client helper restart_event_loop() sends DebugLinkStop and then waits for the device to come back with DebugLinkGetState(return_empty_state=True). The old DebugLink.stop() method is removed and replaced by this context-aware helper. A test in tests/device_tests/test_msg_backup_device.py is updated to use this helper to simulate an interrupted backup on core models, while legacy models still use Cancel(). The entire functionality is gated by the debug build and the DebugLink test interface.
Changed components
core/src/apps/debug/__init__.pypython/src/trezorlib/debuglink.pytests/device_tests/test_msg_backup_device.pyInspect captured patch +27 / −10
diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py
index ed2d47bf..35ef1e03 100644
--- a/core/src/apps/debug/__init__.py
+++ b/core/src/apps/debug/__init__.py
@@ -18,7 +18,7 @@ if __debug__:
from trezor.ui import display
if TYPE_CHECKING:
- from typing import Any, Awaitable, Callable
+ from typing import Any, Awaitable, Callable, NoReturn
from trezor.enums import DebugButton, DebugPhysicalButton, DebugSwipeDirection
from trezor.messages import (
@@ -34,6 +34,7 @@ if __debug__:
DebugLinkReseedRandom,
DebugLinkSetLogFilter,
DebugLinkState,
+ DebugLinkStop,
WipeDevice,
)
from trezor.ui import Layout
@@ -430,6 +431,10 @@ if __debug__:
else:
raise wire.UnexpectedMessage("Debug console not supported")
+ async def dispatch_DebugLinkStop(msg: DebugLinkStop) -> NoReturn:
+ """Restart the event loop"""
+ raise RestartEventLoop
+
async def dispatch_WipeDevice(msg: WipeDevice) -> None:
"""Wipe the device and restart the event loop."""
from storage import wipe
@@ -556,6 +561,7 @@ if __debug__:
MessageType.DebugLinkResetDebugEvents: _no_op,
MessageType.DebugLinkGetGcInfo: dispatch_DebugLinkGetGcInfo,
MessageType.DebugLinkSetLogFilter: dispatch_DebugLinkSetLogFilter,
+ MessageType.DebugLinkStop: dispatch_DebugLinkStop,
MessageType.WipeDevice: dispatch_WipeDevice,
}
diff --git a/python/src/trezorlib/debuglink.py b/python/src/trezorlib/debuglink.py
index 8f9b6fe6..1e7dcf6c 100644
--- a/python/src/trezorlib/debuglink.py
+++ b/python/src/trezorlib/debuglink.py
@@ -860,9 +860,6 @@ class DebugLink:
x, y = click
self._decision(messages.DebugLinkDecision(x=x, y=y, hold_ms=hold_ms), wait=wait)
- def stop(self) -> None:
- self._write(messages.DebugLinkStop())
-
def reseed(self, value: int) -> None:
self._call(messages.DebugLinkReseedRandom(value=value), expect=messages.Success)
@@ -1743,6 +1740,16 @@ class TrezorTestContext:
self.debug._call(messages.WipeDevice(), expect=messages.Success)
self.reset_instance()
+ def restart_event_loop(self) -> None:
+ assert self.model in models.CORE_MODELS
+ if self.is_thp():
+ # device should not be restarted while handling THP ACK from host
+ self.sync_responses()
+
+ self.debug._write(messages.DebugLinkStop())
+ # wait until MicroPython event loop is available after a restart
+ self.debug._call(messages.DebugLinkGetState(return_empty_state=True))
+
def load_device(
session: client.Session,
diff --git a/tests/device_tests/test_msg_backup_device.py b/tests/device_tests/test_msg_backup_device.py
index e3c704c5..5d6051df 100644
--- a/tests/device_tests/test_msg_backup_device.py
+++ b/tests/device_tests/test_msg_backup_device.py
@@ -18,10 +18,10 @@
import pytest
import shamir_mnemonic as shamir
-from trezorlib import device, messages
+from trezorlib import device, messages, models
from trezorlib.debuglink import DebugSession as Session
from trezorlib.debuglink import LayoutType
-from trezorlib.exceptions import TrezorFailure
+from trezorlib.exceptions import Cancelled, TrezorFailure
from ..common import (
MNEMONIC12,
@@ -210,10 +210,14 @@ def test_interrupt_backup_fails(session: Session):
resp = session.call_raw(messages.BackupDevice())
assert isinstance(resp, messages.ButtonRequest)
- # interrupt backup by sending cancel
- session.cancel()
- resp = session.read()
- assert isinstance(resp, messages.Failure)
+ # interrupt backup
+ if session.model in models.LEGACY_MODELS:
+ with pytest.raises(Cancelled):
+ # backup can be cancelled on legacy
+ session.call(messages.Cancel())
+ else:
+ # use debuglink to fail the backup
+ session.test_ctx.restart_event_loop()
# check that device state is as expected
assert session.features.initialized is True
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.