Code Review Patterns for Embedded C
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.
The Patterns to Hunt
A security review of embedded C is largely a hunt for a recognizable catalog of patterns, each appearing where untrusted input meets an unsafe operation:
| Pattern | What goes wrong | What to check |
|---|---|---|
| Unbounded copies | A copy bounded by the source, not the destination | Every copy: which side bounds it? |
| Integer overflow in size math | count * size wraps to a small allocation | Attacker numbers feeding size calcs |
| Trusted length fields | A wire length used without validation | Every length checked vs real bounds |
| Off-by-one | A <= where < belongs, or a missing terminator | Loop bounds and string termination |
| Format string | Attacker data passed as the printf format | The format argument is always a literal |
| Use-after-free / double-free | A freed pointer used, or freed again | Object lifetime across error paths |
| TOCTOU | A checked value changed by an interrupt | Shared state touched without protection |
| Missing return checks | A verify result discarded | Security funcs' results are acted on |
Unbounded Copies
The 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.
// 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, and on an embedded target with no ASLR or stack protection these 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 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 follows where attacker-controlled numbers feed size calculations and asks whether they can wrap, which is exactly what parsers that compute buffer sizes from length fields do.
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, the pattern behind a large share of remotely exploitable embedded bugs.
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, 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
A single byte written past a buffer can corrupt a length, a pointer, or a flag with security consequences, so these 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 this 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: a format function whose format argument is not a literal. Grepping the format-family calls and checking each one’s first argument is a fast, high-value pass, because these bugs leak memory including secrets and, with %n, corrupt it.
Memory Lifetime and Race Bugs
Memory that is freed and then used, or freed twice, corrupts the allocator and can be turned into control of execution. In embedded code with manual memory management and tangled error paths, a pointer used after cleanup, or freed on two paths, is a recurring and serious pattern, so the review traces the lifetime of allocated objects across error handling, where setting pointers to null after freeing and releasing each resource exactly once are the defenses.
Time-of-check-to-time-of-use bugs are the concurrency cousin: code checks a condition and acts on it assuming nothing changed, but an interrupt alters the value in between. 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, which matter for security when the checked condition is an authorization or a bound an interrupt can invalidate.
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 examined and acted on, because a verify-then-ignore looks 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, so the human review focuses where tools are weak, the trust boundaries, the protocol parsers, the security-critical decisions. The most efficient review starts from where untrusted input enters and follows it through the code, applying this catalog at each step, which finds the attacker-reachable bugs far faster than reading the codebase top to bottom.
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.



