Thursday, August 6, 2026

The Coldcard incident

A vulnerability in the Coldcard hardware wallet's Bitcoin key generation allowed attackers to steal more than 2,000 BTC (about $120 million). The vulnerability had existed since March 2021 and was only exploited a few days ago. The immediate root cause was the code using a less safe/lower entropy random number generator, which allowed the attackers to find wallet addresses and their keys by brute force.

For processes that require high security (like creating bitcoin wallet keys), you have to use cryptographically secure random number generators. On a PC, you might use python's secret module. On small embedded devices, you often don't have access to high-level libraries, so the firmware must implement a secure entropy source itself.

The relevant code depends on a fork of MicroPython maintained by Coldcard (a heavily stripped-down Python implementation for microcontrollers) and on the libngu library, reportedly maintained by Coinkite's CTO. The code expected a function named rng_get(), but Coldcard's implementation exposed differently named functions, so the linker silently selected MicroPython's software PRNG. The problem can be summarized as:

[ Intended Architecture ]
  Entropy Source (STM32 Hardware TRNG) ──► Cryptographically Secure Seed (128-bit)

[ Actual Vulnerable Execution ]
  Silent Check Failure (MICROPY_HW_ENABLE_RNG == 0)
    │
    ▼
  Fallback: MicroPython Software PRNG (Yasmarang)
    │
    ├─► Initialized by: Device Unique ID (UID) + Timer Registers
    └─► Entropy drop: 128-bit (Standard) ──► ~40 bits (Mk2/Mk3)

Details: The weak-entropy path (the software Yasmarang PRNG) only happened because MICROPY_HW_ENABLE_RNG was defined as 0. In libngu (ngu/random.c):

#ifndef MICROPY_HW_ENABLE_RNG
# error "get a HW TRNG plz"
#endif
...
extern uint32_t rng_get(void);      // "I need a function called rng_get()"
#define CHIP_TRNG_32()   rng_get()  // then calls it

This only checks whether the macro exists, not its value. Because it was defined (even as 0), the #error never fired and the build continued. It declares rng_get but does not define (implement) it. It expects whoever builds the final firmware to supply a real function with that exact name. In MicroPython (ports/stm32/rng.c):

#if MICROPY_HW_ENABLE_RNG
// real STM32 hardware TRNG path for rng_get()
#else
// Yasmarang software PRNG fallback for rng_get()
#endif

When the value is 0, the #else branch is taken → the weak software PRNG is compiled in and linked as rng_get(). In Coldcard’s own board RNG files (e.g. stm32/COLDCARD*/rng.c):

#if MICROPY_HW_ENABLE_RNG
#error "this code replaces normal RNG module"
#endif

They intentionally set the macro to 0 so their custom hardware RNG code would be used instead of MicroPython’s. But that custom code never exported the exact rng_get() symbol that libngu expected. Coldcard’s custom RNG code (in files like stm32/COLDCARD/rng.c or stm32/COLDCARD_MK4/rng.c) implements its own, more paranoid hardware TRNG logic. However, it only provided functions under different names, such as:

  • rng_get_or_fault()
  • random_buffer()
  • pyb_rng_get() (the Python-visible version)

It never defined a global function literally named rng_get(). MicroPython’s own ports/stm32/rng.c does define a function named rng_get(). Because MICROPY_HW_ENABLE_RNG was set to 0, the linker picks this weaker version:

uint32_t rng_get(void) {
  return pyb_rng_yasmarang(); // software PRNG
}

When the final firmware binary is built, the linker has to resolve every extern reference.

libngu says: “I need a symbol called rng_get.”

Coldcard’s custom code does not supply a symbol with that exact name.

MicroPython’s code does supply a symbol with that exact name (the software version).

So the linker binds libngu’s call to MicroPython’s software rng_get().

That’s why the custom hardware TRNG code was present in the source tree and even reviewed, yet was never actually used for seed generation. The call path never reached it because the symbol name didn’t match.

If the board-specific RNG file (e.g. stm32/COLDCARD/rng.c or the Mk4 equivalent) had contained a global function literally named rng_get, then the linker would have seen two definitions of the same symbol, would produce a duplicate symbol / multiple definition error and the build would fail.

The solution is to make the ownership of the entropy source explicit and fail-closed, instead of relying on a fragile combination of preprocessor flags and linker symbol resolution. Stop going through libngu / ngu.random for seed generation. Keep the critical path (wallet seed creation) on Coldcard’s own known-good function that already talks directly to the hardware TRNG. Only use ngu.random for non-critical things (if at all).

Another good practice would be to write unit tests to verify the of the pass/key generation. Those tests could look like the following (of course, they should be much more sophisticated than this):

# 1. Generate many seeds using the exact production call path
seeds = set()
n = 50000;
for _ in range(n):
  raw = rng.generate_bytes(32)
  seeds.add(raw)
print(f"len(seeds): {len(seeds)}")
# 2. Basic sanity: should almost never collide
assert len(seeds) == n, "Collisions detected – entropy is far too low"

People are reporting that using widely available AI without access to current events on the internet did not reveal the bug, so this incident is not an obvious coding mistake.

Music: Orhan Gencebay - Hatasız Kul Olmaz

Wednesday, July 1, 2026

Stop Using Empty Braces {} for C++ Constructors

Empty constructors can disable useful compiler optimizations.

// Avoid this
MyClass() {}

// Prefer this
MyClass() = default;

1. The Performance Secret: "Trivial" Types

With {} (the slow way), the constructor becomes user-provided, making the type non-trivially default constructible. That prevents standard library implementations from using the highly optimized code paths available for trivial types.

With "= default" (the fast way), the constructor remains compiler-generated, allowing the type to stay trivially default constructible when all its members are trivial. This enables the standard library and compiler to use optimized code paths that are unavailable for user-provided constructors.

In the following example, the compiler generates machine instructions for A but not for B. From the language's perspective "user-provided" and "does nothing" are different concepts:

// C++ code
struct A { A() {} };
struct B { B() = default;};
int main() {
  A a;
  B b;
}

//Assembly code (generated with Compiler Explorer, x86-64 clang (trunk), no optimization:
main:
  sub rsp, 16
  lea rdi, [rbp - 1]
  call A::A() [base object constructor]
  xor eax, eax
  add rsp, 16
  pop rbp
  ret

A::A() [base object constructor]:
  push rbp
  mov rbp, rsp
  mov qword ptr [rbp - 8], rdi
  pop rbp
  ret

Note that if you use the optimization flag -O1, the code reduces to its optimal form:

main:
  xor eax, eax
  ret

This is a good illustration of a broader point: at -O0, C++ codegen tends to reflect the syntax of your code fairly literally (useful for debugging, every statement maps predictably to instructions). At -O1 and above, codegen instead reflects the observable semantics and "construct two empty objects nobody uses" has no observable semantics at all.

2. Guarding Your Move Semantics

Writing an empty destructor ~MyClass() {} triggers the Rule of Five in a bad way. The compiler assumes that because you wrote a custom destructor, you are managing resources manually. It automatically deletes your implicit move constructor and move assignment operator. By trying to be clean with {}, you accidentally cripple your class's ability to be efficiently moved. Using "= default" keeps your move semantics intact.

3. Why Can't the Compiler Just Optimize {}?

It isn't a lack of compiler intelligence; it is a legal restriction. The C++ Standard explicitly states that writing {} makes a constructor user-provided. Changing this rule would break the legacy code. For example, if the standard suddenly declared that empty braces implied "trivial," code that intentionally uses {} to block unsafe memcpy operations (like cryptography classes) would instantly become vulnerable. Furthermore, if you had an empty #ifdef DEBUG block inside your constructor, your class would dangerously flip between being non-trivial in Debug mode and trivial in Release mode.

The C++ committee gave us "= default" in C++11 as an explicit toggle. {} tells the compiler, "Hands off, I'm taking control." = default tells the compiler, "I want standard behavior, optimize this as much as you can." Always give the compiler the green light.

Wednesday, June 17, 2026

Interrupt Deadlock

In embedded systems, there is a dangerous trap where standard thread safety fails completely, leaving your application permanently frozen. That trap is to use a mutex inside an interrupt handler. Consider this thread-safe function, where you protect shared state with a mutex:

void update_hardware(int data) {
    pthread_mutex_lock(&lock)
    global_hardware_buffer = data; // What if an interrupt hits right here?
    pthread_mutex_unlock(&lock);
}

If this function is called by an interrupt service routine (ISR) and a hardware interrupt fires right in the middle of that function:

  1. The Main Thread acquires the lock.
  2. The Interrupt hits. The CPU immediately freezes the main thread and jumps to your ISR.
  3. The Re-entry: The interrupt handler needs to log something, so it calls update_hardware().
  4. The Deadlock: The interrupt handler hits pthread_mutex_lock(). It sees the lock is busy, so it waits.

But who is it waiting for? It's waiting for the main thread to release the lock. Except the main thread is frozen underneath the interrupt handler, waiting for the interrupt to finish!

A common misconception is that the operating system's scheduler will see the interrupt handler is blocked, context-switch it out, let the main thread finish, and fix the mess. It can't. In embedded systems or Real-Time Operating Systems (RTOS), interrupts run at a higher execution priority than the scheduler itself. ISRs execute outside normal thread scheduling. If an ISR attempts to wait for a resource held by the interrupted thread, forward progress becomes impossible because the interrupted thread cannot run until the ISR completes. This is priority inversion taken to the extreme. The ISR (highest priority in the system) ends up waiting for a lower-priority thread that it itself has preempted.

To survive interrupts, your code cannot just be thread-safe, it must be reentrant. A reentrant function is completely self-contained. It never touches global variables, it never uses static buffers, and it never locks a mutex. It relies strictly on local variables allocated on the stack or parameters passed to it. Because it has no shared memory between calls, it can be interrupted at any instruction and safely called again without data corruption or deadlocks. Here is a reentrant function example:

// This function operates purely on local stack memory.
// It can be safely interrupted and re-entered at any microsecond.
int calculate_hardware_state(int current_state, int new_data) {
    int next_state;
    next_state = current_state + new_data;
    return next_state;
}

Also pay attention to macros because if a macro references a global variable, a static variable, or a hardcoded hardware register under the hood, any function using that macro instantly becomes non-reentrant:

int global_status = 0;
#define SET_STATUS_FLAG(mask) (global_status |= (mask))

Never use blocking synchronization primitives (like mutexes, malloc, or I/O) inside an interrupt handler or signal handler. If you must pass data between an interrupt and your main loop, stick to lock-free mechanisms like C11 atomics or volatile flags.

Wednesday, June 10, 2026

Choosing the Right CPU: Desktop vs. Industrial vs. Safety-Critical

We live in an era where a standard desktop processor has 24 cores and clock speeds past 5.5 GHz. Yet, if you walk into an automotive assembly line, you will see computers (PLCs) with processors running 100x slower, and being 10x more expensive than their desktop counterparts.

Why? Because in the world of computing, power is defined entirely by the problem you are trying to solve. We have to look past raw processing speed and examine three distinct operational philosophies: Throughput, Determinism, and Functional Safety.

1. The Desktop CPU

Desktop processors are designed to handle an unpredictable, highly dynamic workload. At any given moment, a desktop CPU might be asked to render a 3D video, compile a massive codebase, manage dozens of browser tabs, or decode high-definition audio.

To excel at this, desktop CPUs use general purpose operating systems like Windows or Linux, which rely on throughput-oriented schedulers. The OS slices up time and distributes it among running applications, trying to give everything a fair share. To squeeze out every drop of performance, the silicon itself relies on microarchitectural optimizations:

  • Out-of-Order Execution: The CPU dynamically rearranges the order of instructions to keep its execution pipelines full.
  • Speculative Execution & Branch Prediction: The processor literally guesses which path a piece of code will take before it even runs, executing the instructions ahead of time to hide latency.
  • Multi-Tiered Caches (L1/L2/L3): Large memory pools sit on the die to prevent the CPU from constantly waiting on slower system RAM.

However, this architecture is inherently non-deterministic. If a background cloud-sync app suddenly demands resources, or if a branch predictor guesses wrong, a task might take 50 milliseconds longer to execute on cycle two than it did on cycle one. In the consumer world, a dropped frame in a video game is an annoyance; in a physical system, a 50ms delay can be catastrophic.

2. The Industrial PLC CPU

Step inside a factory running a high-end programmable logic controller (PLC), like the Siemens SIMATIC S7-1500. Clock speeds range from tens to hundreds of megahertz, and memory is measured in megabytes rather than gigabytes. Yet, these processors are built for a completely opposing goal: Absolute Determinism.

An industrial CPU runs a Real-Time Operating System (RTOS). Instead of a fair share schedule, the RTOS uses strict, unyielding, priority-based cyclic execution. A PLC operates on a continuous loop:

  1. Read Inputs: Snapshot the state of every physical sensor.
  2. Execute Logic: Run the user control code sequentially.
  3. Write Outputs: Instantly update physical actuators, valves, and motors.

To guarantee that this cycle takes exactly the same amount of time down to the microsecond, industrial CPUs strip away the unpredictable optimization layers of desktop chips. There is no speculative execution, no out-of-order execution, and no virtual memory paging. Every task has a strict time budget monitored by a dedicated hardware watchdog timer. If a 1ms motion control loop fails to complete in exactly 1ms, the RTOS catches it, alerts the system, and can trigger a controlled shutdown.

Industrial CPUs are also physically engineered to survive decades in harsh environments. They are decoupled from fragile cooling fans, insulated against severe electromagnetic interference (EMI), and rated to maintain their precise timing clock cycles across extreme temperature swings (e.g., -25°C to +60°C).

3. The Safety-Critical CPU

While an industrial CPU guarantees when a command will execute, a safety-critical processor guarantees the mathematical integrity of the execution itself. Found in drive-by-wire automotive systems, avionics units complying with aerospace standards (like DO-254/DO-178C), or high-speed medical equipment, these processors are certified to standards like ISO 26262 (ASIL-D) or IEC 61508 (SIL-3).

The defining feature of a commercial safety-critical processor (such as the Texas Instruments Hercules™ or Infineon AURIX™ lines) is hardware level redundancy. Instead of using multiple cores to run different applications simultaneously, a safety-critical CPU pairs identical cores into a Dual-Core Lockstep (DCLS) configuration:

  • The Master and the Checker: Two physical hardware cores execute the exact same instruction stream, line-by-line, cycle-by-cycle.
  • Temporal Separation: To ensure that a localized physical event (like a voltage spike or a cosmic ray flipping a bit in memory) doesn't corrupt both cores simultaneously, the second core runs delayed by a micro-interval (typically 2 to 3 clock cycles).
  • Hardware Comparators: Independent hardware logic monitors the internal state and outputs of both cores. If a divergence of even a single bit is detected between the Master and the Checker, the comparator immediately strips power from the actuators or switches the system to a pre-defined, hardware-enforced "safe state."

In this realm, the code is heavily audited, features like Built-In Self-Tests (BIST) continuously sweep memory for corruption, and every single gate on the silicon must be mathematically traceable back to a design requirement.

Music: Ali Baba ve 7 Cüceler (arka jenerik)

Friday, May 8, 2026

Embed .NET runtime into C# exe

When writing a C# program, if you want to bundle the .NET runtime with the application so that it can run on a Windows PC without a separate .NET installation:
  1. Create the project as "Windows Forms App", not "Windows Forms App (.NET Framework)"
  2. Use Release x64 configuration instead of only Any CPU
  3. In Visual Studio 2022, open the terminal and run:dotnet publish -c Release -r win-x64 -p:Platform=x64 --self-contained true -p:PublishSingleFile=true
  4. The published executable is located in:
  5. bin\x64\Release\net8.0-windows\win-x64\publish\
  6. The generated EXE includes the .NET runtime and can run on systems without .NET installed.
Depending on the NuGet packages used (e.g. S7NetPlus), additional DLLs may still appear in the publish folder and should also be distributed with the EXE.

Thursday, May 7, 2026

std::system Windows vs Linux

On Windows the C++ function std::system() is essentially a wrapper around the command processor (cmd.exe). When the process finishes, the exit code is passed directly back to you. If your program exits with 1, the integer returned by std::system is 1. On Linux, it might return 256.

On Linux, a single integer return value isn't just an exit code; it's a status word containing a wealth of information about how the process died. The OS packs different data into specific bit ranges. In most Linux implementations, the exit code is shifted into the high byte. This means a return code of 1 is stored as 1 << 8, which equals 256. To get back the exit code, you have to right shift the status code by 8 bits. The portable way is to use WEXITSTATUS macro.

To write code that works on both platforms, you cannot treat the return value as a raw number. You must use the decoding macros provided in <sys/wait.h> on Linux. You should always check if the process actually finished before asking for the code. Here is the safest pattern for Linux:

#include <sys/wait.h>
int status = std::system("./my_script.sh");
if (WIFEXITED(status)) {
  int exitCode = WEXITSTATUS(status); //
  std::cout << "Success! Code: " << exitCode;
} else if (WIFSIGNALED(status)) {
  int sig = WTERMSIG(status);
  std::cout << "Killed by signal: " << sig;
}

Monday, April 6, 2026

Make C++ memory safe by never using "new"

Every C++ developer eventually encounters a memory leak. You allocate something on the heap, write some logic, hit an early return at runtime and suddenly that heap memory is gone forever:

std::vector<int>* numbers = new std::vector<int>({1, 2, 3});
// ... what if we return early?
// ... what if an exception fires?
delete numbers; // only runs if we get here

The good news is that it is entirely possible to write professional C++ without malloc or new. The above example can be written as follows, without new and delete:

std::vector<int> numbers = {1, 2, 3}; // Clean and safe

The vector internally does roughly this:

 Stack              Heap
┌───────────────┐  ┌───────────────┐
│ numbers       │  │               │
│ _data ────────┼──┼─► [1] [2] [3] │
│ _size = 3     │  │               │
│ _capacity= 3  │  │               │
└───────────────┘  └───────────────┘

The vector object itself lives on the stack, but the actual integers are allocated on the heap via new[] / allocator, inserted by the compiler. The vector acts as a "wrapper" or "manager" for a raw block of memory on the heap. The std::vector object itself is just a small, fixed-size handle (8-byte pointers = 24 bytes). It doesn't grow or shrink, helping you preserve the precious stack. It knows exactly where the heap memory starts, how much is used, and how much is left. The actual data (your integers, strings, or custom objects) lives in the heap and its size can be gigabytes.

In C++, the destructor of a stack-allocated object is automatically inserted into the generated code by the compiler at all the places where the object goes out of scope:

int complexFunction(int x) {
    std::vector<int> numbers = {1, 2, 3};

    if (x < 0) {
        // COMPILER INSERTS: numbers.~vector();
        return -1; 
    }

    // COMPILER INSERTS: numbers.~vector();
    return x * 2;
}

In the assembly listing, you will see lines like this:

call std::vector<int, std::allocator<int>>::~vector() [base object destructor]

When the std::vector destructor runs, it automatically performs two critical tasks:

  1. Element Destruction: it calls the destructor for every individual object currently stored in the vector. If you have a vector of strings, it ensures each string cleans up its own character buffer first.
  2. Deallocation: Once the elements are destroyed, the vector calls the underlying deallocation function (typically a wrapper around operator delete[] or a custom allocator) to return the entire block of heap memory to the system.

If you were using malloc or new manually, you would have to remember to call free or delete in every possible exit path of your function (including if an error occurs):

std::vector removes this "human element" by making the cleanup a language-level guarantee.

If you have a custom MyClass with a constructor that takes runtime parameters:

// ❌ with new — obj on heap, you manage lifetime manually
MyClass* obj = new MyClass(size, name);
delete obj; // must remember this at every exit point

// ✅ without new — obj on stack, lifetime managed automatically
MyClass obj(size, name);
// no delete needed

In C++11 and beyond, smart pointers cover every legitimate use case for new and delete. Example of object that outlives its scope:

// ❌ old way
MyClass* obj = new MyClass(size, name);
return obj; // caller must remember to delete

// ✅ modern way
return std::make_unique<MyClass>(size, name); // ownership transfers automatically

When you have a class OneClass with a member myMember variable that is also a class of type MyClass and myMember constructor parameters are specified at runtime:

class OneClass {
public:
    OneClass(int size, std::string name)
        : myMember(size, name) // ← myMember constructed here, with runtime args
    {
        // constructor body, myMember is already fully constructed here
    }

private:
    MyClass myMember;  // ← no "new", lives inside OneClass
};

OneClass obj(size, name) created │ ├── myMember(size, name) constructed ← initializer list │ └── OneClass constructor body runs

If myMember is not constructed in OneClass constructor but in some other method call:

#include <memory>
class OneClass {
public:
    OneClass() {} // myMember is nullptr

    void initialize(int size, std::string name) {
        myMember = std::make_unique<MyClass>(size, name);   // constructed here
    }

private:
    std::unique_ptr<MyClass> myMember; // nullptr until initialize() is called
};

unique_ptr starts as nullptr and takes ownership when assigned. ~MyClass() is called automatically when OneClass is destroyed, no manual cleanup needed.

The general term for this mechanism is called RAII (Resource Acquisition Is Initialization). In this paradigm, you use objects that manage their own memory. When the object goes out of scope, it automatically cleans up.

Music: Passenger - Let Her Go