test(core): allow joining debuglink task before restarting event loop
What changed, and why it matters
This commit changes internal test-only debug code in the Trezor firmware so that the debug communication session can be cleanly shut down between test scenarios. It adds a flag, a mailbox signal, and a timeout-based join to prevent the debug task from getting stuck when the test framework restarts the device's event loop. The production security relevance appears low because the code is wrapped in `if __debug__` and only used during automated UI tests.
No immediate action required for production security. Treat as a test-infrastructure hardening change. If reviewing, verify that `close_session()` is only invoked from trusted test harness code and that setting `_EXIT_FLAG` cannot be triggered by untrusted USB debug traffic in debug builds.
Security signals we found
Debug-only code path gated by `if __debug__`
Addition of graceful task teardown with timeout to avoid stuck coroutines during test event-loop restart
Potential for debug session to ignore an in-flight message when `_EXIT_FLAG` is set
Updated UI test fixtures suggest visible behavior change in pin-cancel test flow
Evidence from the diff
The patch modifies core/src/apps/debug/__init__.py to introduce _EXIT_FLAG, _EXIT_BOX, _SESSION_TASK, and close_session(). handle_session now races between a read wait and the exit mailbox, breaking its loop when _EXIT_FLAG is set. close_session() sets the flag, wakes the mailbox, unblocks any pending layout_change_box wait, and awaits the session task with a 5-second timeout. boot() now stores the spawned task handle. The tests/ui_tests/fixtures.json changes are only updated screen hashes for test_pin_cancel across languages, consistent with a timing/layout side effect of the debug shutdown path in tests.
Changed components
core/src/apps/debug/__init__.pytests/ui_tests/fixtures.jsonInspect captured patch +45 / −8
diff --git a/core/src/apps/debug/__init__.py b/core/src/apps/debug/__init__.py
index d57431417..e38576fa7 100644
--- a/core/src/apps/debug/__init__.py
+++ b/core/src/apps/debug/__init__.py
@@ -79,6 +79,11 @@ if __debug__:
# wait for layout change
while True:
+ if _EXIT_FLAG:
+ # a response will be sent after restarting the event loop
+ # (since `storage.layout_watcher` is set).
+ return
+
if not detect_deadlock or not layout_change_box.is_empty():
# short-circuit if there is a result already waiting
next_layout = await layout_change_box
@@ -428,6 +433,28 @@ if __debug__:
async def _no_op(_msg: Any) -> Success:
return Success()
+ _EXIT_FLAG = False
+ _EXIT_BOX = loop.mailbox()
+ _SESSION_TASK: loop.spawn | None = None
+
+ _CLOSE_TIMEOUT_MS = const(5000)
+
+ async def close_session() -> None:
+ if _SESSION_TASK is None:
+ return
+
+ global _EXIT_FLAG
+
+ _EXIT_FLAG = True
+ _EXIT_BOX.put(None)
+ if layout_change_box.is_empty():
+ # make sure `DebugLinkGetState` won't get stuck
+ layout_change_box.put(None)
+
+ res = await loop.race(_SESSION_TASK, loop.sleep(_CLOSE_TIMEOUT_MS))
+ if res is not None:
+ log.error(__name__, "debuglink session is stuck")
+
async def handle_session(iface: WireInterface) -> None:
from trezor import protobuf, wire
from trezor.wire.codec import codec_v1
@@ -443,7 +470,15 @@ if __debug__:
except Exception as e:
log.exception(__name__, e)
- while True:
+ read_wait = loop.wait(iface.iface_num() | io.POLL_READ)
+
+ while not _EXIT_FLAG:
+ await loop.race(read_wait, _EXIT_BOX)
+ if _EXIT_FLAG:
+ # in case both `read_wait` and `_EXIT_BOX` are ready,
+ # don't handle the message and exit the loop.
+ break
+
try:
try:
msg = await ctx.read_from_wire()
@@ -515,4 +550,6 @@ if __debug__:
def boot() -> None:
import usb
- loop.schedule(handle_session(usb.iface_debug))
+ global _SESSION_TASK
+
+ _SESSION_TASK = loop.spawn(handle_session(usb.iface_debug))
diff --git a/tests/ui_tests/fixtures.json b/tests/ui_tests/fixtures.json
index abfc29734..cf54b74ce 100644
--- a/tests/ui_tests/fixtures.json
+++ b/tests/ui_tests/fixtures.json
@@ -800,7 +800,7 @@
"T2T1_cs_test_passphrase_bde.py::test_passphrase_prompt_disappears": "f8ff949ad62592c2e8e7034c538ec071444d850f51a0644477a0c892a5448818",
"T2T1_cs_test_pin.py::test_last_digit_timeout": "e08c1377c568f3bae148014a2c445d5342947e2b204409126df9b13f42fe0036",
"T2T1_cs_test_pin.py::test_long_press_digit": "69759f059e1a4b38ab908e9292620face39fee527fc4123e07f293c2d12c88d7",
-"T2T1_cs_test_pin.py::test_pin_cancel": "00cd88148f3633cff32d1cf4b45515d8f5815b38839ec386334fd090d40d6b7e",
+"T2T1_cs_test_pin.py::test_pin_cancel": "c5f1339f60ad984d0686eb05b2f18e758d4792e546fd86cf2523e450e3ee0c42",
"T2T1_cs_test_pin.py::test_pin_change": "e8c993f44502edd539370c7e3273bdadabb456b10a04aa08bb85631aa4c5bffd",
"T2T1_cs_test_pin.py::test_pin_delete_hold": "e0677ccaa31dd5798c7bca4ad495f57668064914bbe9ab8af4c1e1f8abf3aff3",
"T2T1_cs_test_pin.py::test_pin_empty_cannot_send": "c686613163a308630a30e8086bd3c4aca47ea708d066c9aae48994371c30e907",
@@ -855,7 +855,7 @@
"T2T1_de_test_passphrase_bde.py::test_passphrase_prompt_disappears": "5a9f803b8252df5ca01557ef1c9b0162775708dc340214e0f0964396af057fd1",
"T2T1_de_test_pin.py::test_last_digit_timeout": "892b5959c689361e58c9d996567ddebcca24390f70da2f87623851c7b5628d24",
"T2T1_de_test_pin.py::test_long_press_digit": "09e454075c7a6552564962c53ade8daa72da821ed00d0e1d454e06114e31abe1",
-"T2T1_de_test_pin.py::test_pin_cancel": "6b4c548f7854f248ad2797f76afbc6d0bc35677184c50896c396c5b0b7796a7c",
+"T2T1_de_test_pin.py::test_pin_cancel": "703652c41a354dc68d3a8b9dfea343395bd31355c07f96b9e24551fdfdf67a34",
"T2T1_de_test_pin.py::test_pin_change": "1eccd53cf1bc9871e891effaa08fb6114273e6f147f2134b992a5db41a758e77",
"T2T1_de_test_pin.py::test_pin_delete_hold": "3a92537f7c1c10edd01eb74b4ac12d1bb7fa62fd44bd03896b5be670e200f851",
"T2T1_de_test_pin.py::test_pin_empty_cannot_send": "e84725a26b14d022a3593e4cecb73f6473180f4cdf9d5a785660ba9a6690cb70",
@@ -910,7 +910,7 @@
"T2T1_en_test_passphrase_bde.py::test_passphrase_prompt_disappears": "888767326ba692427077a717c0169a903d1eae77150a0052897c2296ebc9860f",
"T2T1_en_test_pin.py::test_last_digit_timeout": "6ca215c2f62c4d97c923f68c4a7293e55eaadc03df1ff089d6972d301ecfde56",
"T2T1_en_test_pin.py::test_long_press_digit": "2acea84d185050fd27052c74cb5ebe901263e782167b1cf29f582b654634334e",
-"T2T1_en_test_pin.py::test_pin_cancel": "6925b022e7d245154b0f2d6aa08e1e2194827e6a3b0964daa3e786c496e987b3",
+"T2T1_en_test_pin.py::test_pin_cancel": "42670d998eb490ba59f126a14a2c1fc5e7ea42089242f46bee04644ffce5fa97",
"T2T1_en_test_pin.py::test_pin_change": "615d2c9619afbfc6f7bd9107237f0896871f5dc12d4f7fafb517b0fef30908f7",
"T2T1_en_test_pin.py::test_pin_delete_hold": "0f84c4bf4f24552bfd826db46699b053db814c9c7077fd65c63acdb004d406ef",
"T2T1_en_test_pin.py::test_pin_empty_cannot_send": "126eb473b472bbd24377d68f165260427535c0ddbc20c3f8a85390f5d95b720b",
@@ -965,7 +965,7 @@
"T2T1_es_test_passphrase_bde.py::test_passphrase_prompt_disappears": "0cb973a9787b3ca94f0768c03e06437d4668f9edc4909dceaa96fbbe53b4de2d",
"T2T1_es_test_pin.py::test_last_digit_timeout": "24355add7b41290c9eedfe15c9b6f264636223d6d1bb2f32dc5ff3273e4c0480",
"T2T1_es_test_pin.py::test_long_press_digit": "56ca6d0a6737b3a34c44db222e33c04166419a9b54a88172cfa52b5e7056ea79",
-"T2T1_es_test_pin.py::test_pin_cancel": "212aae36f4359d4333db3691e8b484d4172ab124a8fe7919e5108d2ed1aacab8",
+"T2T1_es_test_pin.py::test_pin_cancel": "ad1f2c76b292a5f6cdfea96ca50ef03706bf678adb6a6bb0529f3921e132447d",
"T2T1_es_test_pin.py::test_pin_change": "0476cf9650fa9da961508662c64b6efa630bfd11a0748fdb4f2f655b3cc4b6f9",
"T2T1_es_test_pin.py::test_pin_delete_hold": "78168fb67e05d2b365d372cbf8c36452f1bce8774133f00c84945296bcf32c4b",
"T2T1_es_test_pin.py::test_pin_empty_cannot_send": "673fcf6ded02fbb7d654cd1388a5fa72741cf6a5b19578645ce3d7898fe1f6a5",
@@ -1020,7 +1020,7 @@
"T2T1_fr_test_passphrase_bde.py::test_passphrase_prompt_disappears": "0e6b209df1421cb051d97c220c568405bb60bc815fe24d198f07b0a5ed52c4f7",
"T2T1_fr_test_pin.py::test_last_digit_timeout": "781433513b748daf0e285e1fb1e906bfab97cddee3a94aaef0a0d09d1010599a",
"T2T1_fr_test_pin.py::test_long_press_digit": "edc2379a7c23439715c780a2b51098dbbb6282b1b13ff307558a74a58a421d48",
-"T2T1_fr_test_pin.py::test_pin_cancel": "591201786ba7a008a6593019b61616ce0d34f34d833d9a906ebb7a01005e8ee8",
+"T2T1_fr_test_pin.py::test_pin_cancel": "69cbc1787d0aa1733f8c643dbbd93c39c00291b80660f37ef84f90a9543f2ad0",
"T2T1_fr_test_pin.py::test_pin_change": "35be4e7ed394f809531f62cc01e30a9c73bb1a3462abf7586b5feea4043a613f",
"T2T1_fr_test_pin.py::test_pin_delete_hold": "d10a429dc769f8e67e62a06798c50d332a9fcc76ae97acdc8e7e23554ad840e0",
"T2T1_fr_test_pin.py::test_pin_empty_cannot_send": "6bc3d997f24267efe06c9ac4d37d6fadd8d18b5c9fb896095b8f2a6dfcf59265",
@@ -1075,7 +1075,7 @@
"T2T1_pt_test_passphrase_bde.py::test_passphrase_prompt_disappears": "a9f60f9d7f3cd8a1daded5709366211a5a12d172057d00a1202779f21b115e3f",
"T2T1_pt_test_pin.py::test_last_digit_timeout": "b08d854fdccdf4b14a1b8c6901786e798ea8078ac9c1704edb1ea7b54bf2d634",
"T2T1_pt_test_pin.py::test_long_press_digit": "2f38e18ba70f161f96e6a947415a937d1792ffd7dfc6824eb1c72fbc128ff630",
-"T2T1_pt_test_pin.py::test_pin_cancel": "fac3d06f69db9a73ec1162fd260406b968d71734a9a508c01ebcf9ed195cc0bc",
+"T2T1_pt_test_pin.py::test_pin_cancel": "fe7bfb5174f22f11d64db6db0c3a40f7c869caa74009033185201b3ae1f4d2fa",
"T2T1_pt_test_pin.py::test_pin_change": "9915bd713bd0ce7a7118123c9fbcf9c02f8c2d4b456a05551f275723f7f758ed",
"T2T1_pt_test_pin.py::test_pin_delete_hold": "cf7dc3062be3a17d38e74c78de441259bd58b3ae066b0b1e42046edbee277cc3",
"T2T1_pt_test_pin.py::test_pin_empty_cannot_send": "bf1b509e362ff71cbb982b320c6375173cccfcddf259f1ad48a53a5ad9f29009",
Why this scored 16/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.