Berkner Tech

Code Review Patterns for Embedded C

Recurring security vulnerability patterns to look for when reviewing embedded C firmware code

Embedded C is powerful, close to the metal, and unforgiving. The same language features that make it efficient make it easy to introduce memory-safety and input-handling bugs that become security vulnerabilities. A security-focused code review hunts for recurring patterns, and knowing them makes the review faster and more effective. Here are the ones I look for first in embedded firmware.

Why Embedded C Reviews Differ

Reviewing embedded C for security is not the same as reviewing application code. The bugs that matter are memory-safety and input-handling flaws that lead to corruption and control, the language gives no safety net, and the context, code that parses attacker-controlled input from a radio or a bus on a device with no memory protection, raises the stakes of each mistake.

A security review focuses on the boundaries where untrusted data enters and on the operations most likely to go wrong: copies, allocations, arithmetic on sizes, and parsing. The patterns below recur across embedded codebases because the language invites them and the deadline-driven culture rarely prunes them. Knowing them turns a review from a slow read into a targeted hunt.

Unbounded Copies

The first and most common pattern is a copy with no bound, the classic buffer overflow. A strcpy, strcat, sprintf, or a memcpy whose length comes from the input rather than the destination size writes past the buffer when the input is larger than expected, corrupting whatever sits beyond it.

// dangerous: input length controls the copy, not the buffer size
char buf[64];
strcpy(buf, input);              // overflows if input > 63 bytes
memcpy(buf, pkt->data, pkt->len);// overflows if pkt->len > 64

// safer: bound by the destination size
strlcpy(buf, input, sizeof(buf));
if (pkt->len > sizeof(buf)) return ERR;
memcpy(buf, pkt->data, pkt->len);

Every copy is a question: what bounds it, the source or the destination. If the answer is the source, and the source is attacker-influenced, it is a finding. These are the highest-value patterns to hunt because they lead directly to memory corruption, and on an embedded target with no ASLR or stack protection they are often cleanly exploitable.

Integer Overflow in Size Math

A subtler pattern is arithmetic on sizes that overflows, producing a small value that then sizes an allocation or a copy. A length field multiplied by an element size, or a length plus a header, can wrap around, and the undersized buffer that results is then overflowed by the real data.

// dangerous: count * size can overflow to a small value
uint16_t need = count * sizeof(item);   // wraps for large count
item *p = malloc(need);                 // tiny allocation
for (i = 0; i < count; i++) p[i] = ...;  // writes far past it

// safer: check for overflow before allocating
if (count > MAX_ITEMS || count > SIZE_MAX / sizeof(item)) return ERR;

Integer overflows hide because the dangerous operation looks innocent, just a multiply or an add. The review has to follow where attacker-controlled numbers feed size calculations and ask whether they can wrap. These bugs are common in parsers that compute buffer sizes from length fields in the input, which is exactly where embedded code spends much of its time.

Trusting Length Fields

Embedded protocols are full of length fields, and trusting one without validating it against the actual buffer is a recurring flaw. The code reads a length from the input and uses it to copy or index, assuming it is honest, when an attacker sets it to whatever causes the most damage.

// dangerous: the wire-supplied length is trusted
uint16_t len = (buf[2] << 8) | buf[3];
memcpy(dest, &buf[4], len);      // len can exceed both buffers

// safer: validate against what was actually received and the destination
if (len > received - 4 || len > sizeof(dest)) return ERR_BAD_LEN;
memcpy(dest, &buf[4], len);

Any length, count, or offset that comes from outside the device is hostile until checked against the real bounds. A review of a protocol parser is largely a hunt for length fields used without validation, because that single pattern underlies a large share of the remotely exploitable bugs in embedded devices.

Off-by-One and Boundary Errors

The small boundary mistakes, a <= where a < belongs, forgetting the null terminator’s byte, an index that reaches one past the end, corrupt exactly one byte, which is often enough to matter. They are easy to write and easy to miss, and they cluster around loops and array indexing.

// off-by-one: writes index [n], one past a buffer of size n
for (i = 0; i <= n; i++) buf[i] = src[i];   // should be i < n

// missing room for the null terminator
char name[16];
strncpy(name, input, 16);        // may leave name unterminated

The review pays special attention to loop bounds and to string functions that may or may not terminate. A single byte written past a buffer can corrupt a length, a pointer, or a flag with security consequences, so these small errors are not cosmetic. They reward the slow, careful read that a security review is meant to be.

Format String Bugs

Passing attacker-controlled data as the format argument to a printf-family function lets the attacker read and sometimes write memory through format specifiers. On embedded systems with logging over a console or to a buffer, this pattern appears when input is logged carelessly.

// dangerous: input used as the format string
printf(input);                   // attacker controls %x, %n, etc.
log_msg(user_supplied);          // same bug if log_msg forwards to printf

// safe: input is data, not the format
printf("%s", input);

The fix is trivial and the bug is easy to spot once you look: a format function whose format argument is not a literal. Grepping for the format-family calls and checking each one’s first argument is a fast, high-value pass, because format-string bugs can leak memory contents including secrets and, with %n, corrupt memory.

Use-After-Free and Double-Free

Memory that is freed and then used, or freed twice, corrupts the allocator’s state and can be turned into control of execution. In embedded code with manual memory management and complex error paths, a pointer used after a cleanup, or freed on two paths, is a recurring and serious pattern.

The review traces the lifetime of dynamically allocated objects, especially across error handling where cleanup logic gets tangled. Setting pointers to null after freeing, and structuring error paths so each resource is released exactly once, are the defenses, and their absence is what the review looks for around every free and every early return in a function that allocated.

TOCTOU and State Assumptions

Time-of-check-to-time-of-use bugs arise when code checks a condition and then acts on it, assuming nothing changed in between. In embedded code this shows up with interrupts and concurrency: a value validated in the main flow is changed by an interrupt before it is used, defeating the check.

The review looks for shared state touched by both interrupt handlers and main code without protection, and for checks separated from the actions they guard. These bugs are easy to miss because the code looks correct in a single-threaded reading, and they matter for security when the checked condition is an authorization or a bound that an interrupt can invalidate at the wrong moment.

Missing Return-Value Checks

Embedded code frequently ignores return values, from allocations, from cryptographic operations, from validation functions, and an unchecked failure becomes a security bug. A signature verification whose result is not checked, or a malloc whose null return is used, are the dangerous cases.

// dangerous: verification result ignored -> always proceeds
verify_signature(img, sig);
boot(img);                       // runs even if verification failed

// safe: act on the result
if (verify_signature(img, sig) != OK) { halt(); }
boot(img);

A security check whose result is discarded is no check at all, and this pattern silently defeats verification that looks present in the code. The review confirms that every security-relevant function’s return value is actually examined and acted on, because a verify-then-ignore is one of the most insidious patterns, looking secure while doing nothing.

Reviewing With Tools and Focus

Manual review is sharpest when paired with tooling. Static analyzers flag many of these patterns automatically, and compiler warnings turned up to maximum catch a surprising number. The human review then focuses where tools are weak: the trust boundaries, the protocol parsers, the security-critical decisions, and the logic that ties them together.

The most efficient review starts from where untrusted input enters and follows it through the code, applying this catalog of patterns at each step. That input-driven, pattern-aware approach finds the bugs that matter, the ones reachable by an attacker, far faster than reading the codebase top to bottom, and it is how a security review of embedded C earns its time.

Where This Fits

Security-focused code review of embedded firmware, hunting these patterns where attacker input reaches them, is part of the assessment and secure-development work I do. If you want a security review of your embedded C, or help teaching your team to spot these patterns, that is the kind of work we do at Berkner Tech.


References and Further Reading