rng: recover the TRNG after a seed/clock error instead of failing forever - #692
rng: recover the TRNG after a seed/clock error instead of failing forever#692Silexperience210 wants to merge 1 commit into
Conversation
…ever Since the 2026-07-31 hotfix (ca72463) rng_get() reads the hardware TRNG and raises OSError(EFAULT) on a 10ms timeout, replacing a software PRNG that could not fail. rng_init() only re-enables the peripheral when RNGEN is clear, but per RM0432 25.3.7 (RM0351 for the L4) a seed error latches SEIS and stops DRDY while leaving RNGEN set. Recovery requires clearing SEIS and then toggling RNGEN off->on, so rng_init() is a no-op in exactly the state where it is needed: one transient glitch makes every later call time out permanently. That is not survivable by the user, because rng_get() is now reached from the keypad scan-order shuffle in _start_scan(), which runs from a Pin.irq callback before login. The OSError reaches IMPT.handle_exc -> die_with_debug -> show_logout(), so the device lands on a fatal-error screen with no way to enter a PIN and therefore no way to reach the upgrade menu. rng_reset() clear the clock, drop RNGEN, clear SEIS/CEIS, re-enable, and discard the first word as the reference manual asks (the old "TODO: throw out some samples?" case) rng_init() reset when RNGEN is clear *or* any error flag is set rng_try_once() non-throwing single attempt; rejects the word if an error latched while waiting for DRDY rng_get_or_fault() up to RNG_MAX_ATTEMPTS with a reset between each, then raise as before The throw-on-persistent-failure contract is unchanged and there is no fallback to software entropy: a genuine hardware failure still raises. mempad.py / keyboard.py: wrap shuffle(self.scan_order) in try/except. Scan-order randomisation is anti-Tempest hygiene, not a secret, and must never be able to take the keypad down before login. COLDCARD_Q1/rng.c is a symlink to the Mk4 file, so Mk4/Mk5/Q are all covered by the one change.
|
Hardware entropy on this class of device is expensive, potentially blocks IO, stalls, introduces latency and interacts unpredictably with other parts of the SoC. The better fix would be to restore the software prng (which was fully disabled in the vendor's recent hot-fix ca72463) for UI cases like shuffling the login keypad, and then use the hardware trng explicitly where required, in a controlled and auditable manner that has fewer layers of abstraction. prng and trng have fundamentally different signatures, operational modalities, and both need to be exposed and used based on the caller's requirements. Shoehorning the two into one entry point isn't going to fly. |
scgbckbone
left a comment
There was a problem hiding this comment.
hey, thanks for a contribution. I think I have something better here #693
|
Agreed, #693 is better. Closing this in favour of it. Three things where you're right and I was wrong, one correction to my own writeup, and two review notes. Where #693 is right:
Correction to my own description above: I wrote "permanent" and "forever". That is wrong and I should not have put it in the title. Two review notes on #693:
Left behind: a standalone testbench that mocks RNG_CR/SR/DR with the documented flag semantics and runs a driver against four fault scenarios (nominal, transient seed error, dead 48 MHz clock, 1-in-5 glitch over 5000 reads). It reproduces the "stays dead after the condition clears" case without hardware and #693 passes it. Mk3 follow-up opened as #698, built on your design rather than mine. Thanks for the quick turnaround. |
Affects:
stm32/COLDCARD_MK4/rng.c,stm32/COLDCARD/rng.c(COLDCARD_Q1/rng.cis a symlink to the Mk4 file), as shipped in the 2026-07-31 hotfix (commitca724637, "fixes rng").Summary: the entropy fix is correct —
rng_get()now resolves to the board TRNG accessor instead of MicroPython's software fallback. Butrng_get_or_fault()has no recovery path for the STM32 RNG error flags, so a single seed error latches the peripheral into a state the code can never clear. Every subsequent call times out and raisesOSError(EFAULT), forever. Becauserng_get()is now on the keypad scan path, which runs from an IRQ callback before login, that exception is not survivable by the user.1. What changed
Before the hotfix, with
MICROPY_HW_ENABLE_RNG (0),rng_get()linked to the#elsebranch ofports/stm32/rng.c— a pure software Yasmarang PRNG. It touched no hardware and could not fail.After the hotfix,
stm32/rng.ois compiled from/dev/nullandrng_get()resolves to:which reads the hardware TRNG and calls
mp_raise_OSError(MP_EFAULT)on a 10 ms timeout.This is the right direction. The problem is what happens when the TRNG hiccups.
2. The peripheral is never recovered
Per RM0432 §25.3.7 (and RM0351 for the Mk3's L4):
SECSis set andSEISlatches.DRDYstops asserting.RNGENstays set. Recovery requires clearingSEISand then togglingRNGENoff→on.rng_init()only testsRNGEN, so in the faulted state it is a no-op.rng_get_or_fault()then busy-waits 10 ms and throws — on every call, indefinitely, across reboots of the Python layer.Two secondary issues in the same function:
// TODO: throw out some samples?was never done.RNG->DRis read without checking whetherSEIS/CEISlatched while waiting, so a suspect word can be returned as good.3. Why this is not survivable
rng_get()is now reached from the keypad scan-order shuffle:_start_scan()is called fromanypress_irq, aPin.irqcallback, at up to 60 Hz, and it runs before login. Three or more TRNG reads per keypress, each with a 10 ms worst case, in IRQ context.An
OSErrorthere reachesIMPT.handle_exc→die_with_debug→ux.show_fatal_error+callgate.show_logout(1). The user gets an error screen and cannot enter a PIN — so they cannot reach the upgrade menu either. That matches the "stuck on error screen / won't boot" reports following the hotfix.Note that scan-order randomisation is anti-Tempest hygiene, not a secret. It does not need cryptographic quality and should never be able to take the device down.
4. Reproduction
rng-testbench.c(attached) mocksRNG_CR/RNG_SR/RNG_DRwith the documented flag semantics (SEIS/CEISsticky until software clears them;SECS/CECSread-only, cleared by hardware when the condition ends) and runs both the shipped and the patchedrng_get().Case B is the bug: the shipped code stays dead after the fault condition is gone. Case C confirms the patch does not trade robustness for degraded entropy — a genuine hardware failure still raises rather than silently falling back.
5. Proposed fix
coldcard-rng-antibrick.patch(attached, applies cleanly tomaster), 4 files:stm32/COLDCARD_MK4/rng.candstm32/COLDCARD/rng.crng_reset(): enable clock, clearRNGEN, clearSEIS/CEIS, setRNGEN, discard the first word.rng_init(): trigger a reset whenRNGENis clear or any error flag is set.rng_try_once(): non-throwing single attempt; refuses the word if an error latched during the wait.rng_get_or_fault(): up toRNG_MAX_ATTEMPTS(3), resetting between attempts, then raise as before.No change to the throw-on-persistent-failure contract, and no fallback to software entropy.
shared/mempad.pyandshared/keyboard.pyWrap
shuffle(self.scan_order)intry/except. Defence in depth: an RNG fault should degrade the Tempest mitigation, not brick the device at the login screen.6. Caveats
SEIS-sticky recovery sequence is documented, but the patch should be confirmed on a real Mk4/Q before merging.7. Notes on this PR
Issues are disabled on this repo, so this is filed as a PR — but treat it as a
report first and a patch second. The diff is 4 files, +166/-36, and I have not
been able to test it on silicon, so please do not merge on my word alone.
Supporting material is kept off this branch so the change stays surgical:
rng-fault-analysiscarries the writeup, the standalone testbench and thediff under
docs/rng-fault-analysis/—https://github.com/Silexperience210/firmware/tree/rng-fault-analysis
If you would rather handle this privately given the timing, say so and I will
close this and resend to your security contact.