אִם יִרְצֶה הַשֵּׁם
TL;DR
Coldcard was not a one-off. It sits in a short list of wallet entropy collapses that share a shape and differ in mechanism.
Four cases: Coldcard/libngu (2026), Milk Sad (CVE-2023-39910), Trust Wallet (CVE-2023-31290), Randstorm (2023). In each, the cryptography held. BIP39 checksums passed. Derived keys signed. The failure was upstream of the curve: a build bind, a source choice, a platform workaround, or a semantic fallback decided which generator actually ran.
Four defect classes. Remediation is the same every time: move the funds. What varies is where the defect lived, and whether source review would have caught it before the ledger did.
Earlier on this blog: Coldcard as a Weird Machine.
The shared shape
Each incident below collapses a claimed cryptographic keyspace into something enumerable with ordinary compute. Nominal seeding width is usually 32 bits. Realized keyspace is often worse once boot state, timers, and public constants are in the model. Compiles are clean. Tests are green. No later patch restores entropy to a seed that was born small.
| Incident | Root-cause class | Where the defect lived | Primary RNG | Realized keyspace (order) | Source-visible? |
|---|---|---|---|---|---|
| Coldcard / libngu (2026) | Linkage collision / shadowed symbol | System composition | Yasmarang fallback | ~2^22 observed / 2^24 ceiling (Mk2/Mk3) | No (clean link) |
| Milk Sad (CVE-2023-39910) | Source choice | Single expression in new_seed() |
std::mt19937 |
2^32 | Yes |
| Trust Wallet (CVE-2023-31290) | Target workaround | Wasm platform driver wrapper | MT19937 via Wasm path | 2^32 | Yes |
| Randstorm (2023) | Semantic fallback | Type comparison in JSBN | Browser LCG / timer | ≪ 48 bits, date-dependent | Yes at runtime |
Motion Abstract's Coldcard reconstruction recovered 245 seeds from an observed ~2^21.9 boot-jitter band for about $26 of cloud GPU time. Useful as cost evidence. The column that matters is root-cause class.
1. Linkage collision: Coldcard / libngu
No single file was wrong.
Libngu declared extern uint32_t rng_get(void) and meant the STM32 TRNG. The board's real wrapper used a different name and never exported that global. MicroPython's STM32 port, compiled because MICROPY_HW_ENABLE_RNG was defined as 0, did export rng_get, as Yasmarang. The linker saw one definition, not two. No conflict, no diagnostic.
The compile-time guard meant to refuse this build used #ifndef MICROPY_HW_ENABLE_RNG. On this platform the macro is always defined. Existence is not value. The error was unreachable from the day it was committed.
A second Yasmarang, seeded from public constants inside libngu, was XORed in as "whitening." XOR with a known stream adds zero entropy. On Mk2/Mk3 the RTC path contributed reset zeros. What remained: a ~24-bit structural ceiling, empirically a ~22-bit boot-latency corridor.
Catchability. High reverse-engineering cost after the flaw is known (days of firmware archaeology). Near-zero signal at build time. Source review of any one file passes. Composition review of the link graph fails, if you do it.
LANGSEC case: the build boundary accepted an ambiguous program.
2. Source choice: Milk Sad
Someone picked Mersenne Twister.
Libbitcoin Explorer's new_seed() used std::mt19937 seeded in a 32-bit space. The defect lived in a single expression. No linker drama. No dead preprocessor guard. The wrong generator was named in the source, in cleartext, and shipped.
Catchability. Immediate on code inspection. Reverse-engineering cost: low. Realized loss: over $900k. Remediation: move funds.
Milk Sad is what you get when the check for "cryptographic RNG" is a human reading a PR and the human does not fire.
3. Target workaround: Trust Wallet
The defect lived one layer below the wallet's own code.
Trust Wallet Core's Wasm target path worked around a platform constraint by routing through a 32-bit MT19937-backed generator. The workaround was documented. The compile was clean. The keyspace was 2^32.
Catchability. Low reverse-engineering cost once you read the platform driver. Visible in source if you audit that target. Easy to miss if review stays on the native path and never exercises the Wasm production.
Trust Wallet reimbursed roughly $170k. Same remediation otherwise: move funds. This is not an argument that Wasm is unsafe. It is an argument that target-specific entropy paths are part of the security contract. A review that only checks the desktop build is checking a different product.
4. Semantic fallback: Randstorm
Math.random is fine until it is the entropy source for keys.
Randstorm (Unciphered, 2023) traced weak Bitcoin keys to a browser/JSBN path where a type comparison degraded the generator into a timer-dependent LCG-class fallback. Nominal width looked like "up to 48 bits." Realized width was date-conditioned and often much smaller. Not a missing #error. A silent semantic downgrade when the preferred entropy source was unavailable or mistyped.
Catchability. High analysis cost historically (~22 months to map the cohort). Low once the fallback is identified. Clean execution, no compile failure; the API degraded politely.
Randstorm is the web cousin of Coldcard's link collision. Preferred path, unsafe path, unsafe path reachable without a hard fault.
What changes in the assurance model
Three distinctions matter more than any one CVE string.
1. Composition failures are invisible to file-scoped review.
Milk Sad and Trust Wallet are findable by reading the right file. Coldcard is findable by reading the link map and asking which object supplies rng_get in the release artifact. If assurance stops at git blame on random.c, you clear Coldcard and ship it for 1,978 days.
2. Whitening and mixing are not entropy. Coldcard's XOR, Trust Wallet's wrapper, Randstorm's fallback chain: each looked like defense in depth from inside the brochure. Against an adversary who knows the firmware or the JS bundle, a public or deterministic mixer is an invertible substitution. Measure the composition. Do not credit the diagram.
3. Remediation is always "move funds." None of these four get fixed by a firmware update applied to an already-generated seed. The seed is the asset boundary. Distributional gates (reboot N times, fail on collision) and negative CI (delete the HW object, demand an unresolved symbol; force the unsafe flag, demand a compile failure) belong before first secret derivation, not after the ledger has priced the miss.
Coldcard's place in the set
Coldcard is the expensive member because it failed the way safety-critical systems fail: every local contract held, and the composition accepted a language nobody meant to accept. Milk Sad named the wrong PRNG. Trust Wallet worked around a target. Randstorm fell back semantically. Coldcard linked the wrong supplier behind a guard that could not fire.
The prior post framed this as a weird machine at the build boundary rather than a bad random number for that reason. The RNG output was the symptom. The accepting parse was the defect.
If the entropy claim cannot reject the unsafe production (compile time, link time, or across restarts), it will eventually accept it in production.
Sources
- Motion Abstract, libngu Entropy Collapse / The $116 Million Typo (comparative lineage; Coldcard reconstruction economics)
- Coinkite, Technical Deep Dive into the Entropy Issue
- Wizardsardine, Coldcard: the technical autopsy of an entropy failure
- Milk Sad / Libbitcoin Explorer, CVE-2023-39910
- Trust Wallet Core, CVE-2023-31290
- Unciphered, Randstorm (November 2023)
- NIST SP 800-90B §3.1.4 (restart / health testing)
- Prior dyb: Coldcard as a Weird Machine