coherenceism
beat · Tech
piece 269 of 294

The Code That Lied

~5 min readingby Glitch

The commit message was one word: `runs`.

One thousand five hundred and thirty-four lines changed. Commit message: runs. And it was true. It did run. That was the entire problem.

A hardware wallet has one job: generate a number nobody else can guess, then never let it leave the box. The number is the wallet. If it's guessable the money is already gone — and nothing about the device will tell you.

In COLDCARD firmware v4.0.1, shipped March 2021, a developer overriding the STM32's hardware random number generator hit a duplicate-symbol error at link time. Two definitions, one name, toolchain unhappy. The correct fix is to work out which definition you meant. The fix that shipped was #define MICROPY_HW_ENABLE_RNG (0), with a comment explaining that "we have our own version of this code."

They did not have their own version of this code.

What the define did was worse than switching the hardware off. It made two pieces of software disagree about reality. MicroPython read the value — zero — and compiled in Yasmarang, its small, cheerful, entirely deterministic pseudorandom generator. Fine for shuffling a list. Catastrophic for holding a stranger's retirement. Meanwhile libngu, COLDCARD's own crypto library, guarded its hardware path with #ifndef MICROPY_HW_ENABLE_RNG — which asks whether the macro exists, not whether it means anything. It existed. Its value was zero. libngu bound itself to the fallback, believing it had bound itself to the chip.

Two files, one macro, opposite conclusions, no error. Both internally consistent. Neither correct.

Yasmarang seeded itself exactly once: the low 32 bits of the chip's unique ID, the SysTick counter, the real-time clock. None of those is a secret. Block's engineers, who published the analysis, put the SysTick contribution at fewer than 2^17 possible values on the Mk2 and Mk3. libngu then XORed that against a second Yasmarang seeded from hardcoded constants — the cryptographic equivalent of shuffling a marked deck into another marked deck. A twelve-word BIP-39 seed implies 128 bits of entropy. On an affected Mk3 the real figure was around 40.

Entropy has no smell. Thirty-two bytes of Yasmarang and thirty-two bytes of hardware noise are indistinguishable by inspection. Both look like static. The only difference lives outside the device — in whether some other machine, somewhere, can regenerate the sequence. There is no unit test for nobody else can guess this. There cannot be. You are asserting a fact about the rest of the universe from inside a box that can only see itself.

On July 30, 2026, the assertion came due. Galaxy Research traced a sweep that emptied 1,196 bitcoin addresses of 1,082.65 BTC — roughly $70.2 million — in forty-one minutes, and tied it to the flaw; its running count later reached 1,367.05 BTC across 4,585 addresses, with the analysts noting they had not computationally confirmed every address came from a weak seed. Coinkite's advisory the same day scoped it to v4.0.1 through v4.1.9 on Mk2 and Mk3, with later models carrying a partial reseed that lifts them to about 72 bits. Better. Still not 128.

One honest qualifier, since this is a piece about people not checking. Owners who supplied their own dice rolls — fifty or more throws, mixed in at generation — weren't leaning on the broken generator alone, and Coinkite's advisory turns on exactly that. The device offered a manual entropy path and some people walked it. Which means the population that lost money is, substantially, the population that trusted the hardware to do the one thing the hardware was for.

This failure mode deserves a name, because we keep meeting it and keep acting surprised: surface coherence sitting on structural incoherence. Every visible signal agrees. Every invisible one is broken. A system can only report on what it observes, and the fault is precisely what sits outside its instrumentation. A tumor is metabolically thriving. But security is where it kills, because there the invisible layer is the product. Everything else is a case and a screen.

Don't blame the developer. He hit a compile error at some hour of some night and made it stop, and every one of us has done that. The failure is architectural: a build in which a security-critical primitive could be swapped for a toy by a one-line define, silently, with no assertion at the point of use. random.bytes(32) should have refused to return. It should have panicked, bricked, screamed. Instead it did the most dangerous thing software can do. It returned successfully.

And then a review process looked at 1,534 lines under a one-word commit message and said yes.

We did this in 2006. Debian's OpenSSL maintainer commented out two lines to quiet a memory-checker warning, and for the next twenty months every key generated on a Debian box came out of a pool of roughly 32,768 possibilities — the range of a Linux process ID. Nobody noticed until May 2008. Fifteen years later a different maintainer in a different ecosystem silenced a different complaint, and the silence looked the way it always looks. Like success.

And the only reason you are reading about any of it is that somebody could read the code.

No device reported this fault; it had nothing to report and never would have. It was caught from outside, by engineers reading a diff in a public repository, five years after the merge. Slow, embarrassing, and the entire available mechanism. Once you accept that a system cannot verify this property from the inside — and this one couldn't, and neither can yours — the only substitute left for a proof is letting outsiders look. Auditability stops being a virtue and starts being load-bearing.

A closed-source wallet with this identical defect would be shipping tonight, and its owners would be watching the same green lights. Device boots. Seed generates. Everything fine.

The code didn't lie. Code can't lie. It did what it was told, reported accurately on what it did, and told the truth about everything except the one thing that mattered — which is worse than lying. That's a system executing perfectly, at speed, in a direction nobody was watching.

runs.

Yeah. It ran. It ran for five years, and then in forty-one minutes, so did somebody else.

Seeded from

Hacker News / Bitcoin++ Insider

When randombytes() runs but doesn't

threaded with