What changed, and why it matters
This commit changes what a COLDCARD Q device does when its secure element (SE) is 'bricked'—a state where the hardware security chip is no longer usable. Previously, the device immediately entered DFU (device firmware update) mode and stopped. After this change, the device first opens a calculator-like text interface that can run forever, and only then enters DFU mode. The commit title 'Q: brick into forever calculator' suggests this is intentional behavior for bricked devices, not a vulnerability. The change also removes the ability to enter a PIN from within that calculator when reached through this bricked path, so it does not appear to provide a way to bypass authentication.
Review whether the calculator REPL in allow_login=False mode can access any sensitive globals, files, or functions that could leak seed material or enable unintended actions. Verify that the blacklist in calc.py is sufficient and that no import or eval/exec path can be abused. Confirm that entering DFU after the calculator cannot be skipped by the user.
Security signals we found
Behavior change on bricked secure element
New unauthenticated calculator REPL reachable after brick detection
PIN authentication branch gated by allow_login=False in bricked path
DFU entry still occurs after calculator exits
No input validation changes for calculator expressions
Evidence from the diff
The patch moves the bricked-SE check from pincodes.py (where it immediately called callgate.enter_dfu(3)) into main.py’s more_setup(). When get_is_bricked() is true on a QWERTY model, it now imports and awaits login_repl(allow_login=False) before finally entering DFU mode. In shared/calc.py, login_repl gains an allow_login parameter defaulting to True; when False, PIN-entry and prefix-word branches are skipped, leaving only a non-authenticated calculator/REPL. The change is defensive: it prevents a bricked device from being a silent brick and gives the user a calculator, while ensuring no PIN authentication path is exposed in that state.
Changed components
shared/main.pyshared/calc.pyshared/pincodes.pyCOLDCARD Q secure-element brick handlingInspect captured patch +40 / −31
diff --git a/shared/calc.py b/shared/calc.py
index 86ccccc..7755afa 100644
--- a/shared/calc.py
+++ b/shared/calc.py
@@ -8,9 +8,8 @@ import utime, ngu, re
from utils import B2A, word_wrap
from ux_q1 import ux_input_text
-async def login_repl():
- from glob import dis, settings
- from pincodes import pa
+async def login_repl(allow_login=True):
+ from glob import dis
NUM_LINES = 7 # 10 - title - 2 for prompt
@@ -65,27 +64,31 @@ Example Commands:
elif ln in ('help', 'cls', 'rand'):
# no need for () for these commands
ans = state[ln]()
- elif re_pin.match(ln) and len(ln) <= 13:
- # try login
- m = re_pin.match(ln)
- ln = m.group(1)+ '-' + m.group(2)
- print(ln)
- try:
- pa.setup(ln)
- ok = pa.login()
- if ok: return
- except RuntimeError as exc:
- # I'm a brick and other stuff can happen here
- # - especially AUTH_FAIL when pin is just wrong.
- if exc.args[0] == 'AUTH_FAIL':
- pa.attempts_left -= 1
- ans = '%-7d # %d tries remain' % (eval(ln), pa.attempts_left)
- else:
- ans = 'Error: ' + repr(exc.args)
-
- elif re_prefix.match(ln) and len(ln) <= 7:
- # show words
- ans = pa.prefix_words(ln[:-1].encode())
+ elif allow_login:
+ # without this flag, PIN codes ignored
+ if re_pin.match(ln) and (len(ln) <= 13):
+ # try login
+ m = re_pin.match(ln)
+ ln = m.group(1)+ '-' + m.group(2)
+ print(ln)
+ from pincodes import pa
+ try:
+ pa.setup(ln)
+ ok = pa.login()
+ if ok: return
+ except RuntimeError as exc:
+ # I'm a brick and other stuff can happen here
+ # - especially AUTH_FAIL when pin is just wrong.
+ if exc.args[0] == 'AUTH_FAIL':
+ pa.attempts_left -= 1
+ ans = '%-7d # %d tries remain' % (eval(ln), pa.attempts_left)
+ else:
+ ans = 'Error: ' + repr(exc.args)
+
+ elif re_prefix.match(ln) and len(ln) <= 7:
+ # show words
+ from pincodes import pa
+ ans = pa.prefix_words(ln[:-1].encode())
else:
if any((b in ln) for b in blacklist):
ans = None
diff --git a/shared/main.py b/shared/main.py
index 2500838..6ddc972 100644
--- a/shared/main.py
+++ b/shared/main.py
@@ -86,6 +86,19 @@ async def more_setup():
from files import CardSlot
CardSlot.setup()
+ # check for bricked system early
+ import callgate
+ if callgate.get_is_bricked():
+ print("SE bricked")
+ try:
+ # regardless of settings.calc forever calculator after brickage
+ if version.has_qwerty:
+ from calc import login_repl
+ await login_repl(allow_login=False)
+ finally:
+ # die right away if it's not going to work
+ callgate.enter_dfu(3)
+
# This "pa" object holds some state shared w/ bootloader about the PIN
try:
from pincodes import pa
diff --git a/shared/pincodes.py b/shared/pincodes.py
index 150eb26..e951452 100644
--- a/shared/pincodes.py
+++ b/shared/pincodes.py
@@ -134,13 +134,6 @@ class PinAttempt:
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V1) == PIN_ATTEMPT_SIZE_V1
assert ustruct.calcsize(PIN_ATTEMPT_FMT_V2_ADDITIONS) == PIN_ATTEMPT_SIZE - PIN_ATTEMPT_SIZE_V1
- # check for bricked system early
- import callgate
- if callgate.get_is_bricked():
- # die right away if it's not going to work
- print("SE bricked")
- callgate.enter_dfu(3)
-
def __repr__(self):
return '<PinAttempt: fails/left=%d/%d tc_flag/arg=0x%x/0x%x>' % (
self.num_fails, self.attempts_left,
Why this scored 42/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.