Back to articles

MISRA C Violations We Actually Argue About in Code Review

Code review in modern tech workspace

Five rules, two sides each, and the verdict our team actually ships with.


Every automotive firmware team has a MISRA C checker running in CI, and every automotive firmware team has a Slack thread somewhere titled something like "can we PLEASE talk about rule 15.5" that's been quietly simmering for two years. Most MISRA violations aren't actually controversial — nobody's out here defending unreachable code or implicit type conversions. But a small handful of rules generate genuine, recurring, sometimes heated debate in code review. Here are the five that come up most in ours, presented fairly on both sides, before I tell you where we actually landed.

1. Rule 13.4 — No Assignment in Conditional Expressions

What it bans: writing if ((x = read_sensor()) != 0) — combining an assignment and a condition check in one expression.

The case for the rule: this is a genuinely notorious source of bugs in C generally, not just an automotive-specific concern — if (x = 5) instead of if (x == 5) is one of the most famous typo classes in the language's history, and it compiles silently. Banning the pattern entirely removes an entire category of typo from being possible, and the "fix" (split into two statements) costs you two extra lines and zero expressiveness.

The case against: the pattern while ((c = getchar()) != EOF) — and its embedded equivalents, like polling a hardware register or reading from a ring buffer — is a genuinely clean, readable idiom for "keep doing this and check the result each time." Banning it means either duplicating the read call awkwardly (once before the loop, once at the end of the loop body) or restructuring with a sentinel variable that arguably makes the control flow less obvious, not more.

Where we landed: we keep the rule as a hard line, no exceptions, including for the read-and-check idiom. The typo risk this rule eliminates has bitten real teams in ways that cost real debugging time, and "slightly less elegant loop structure" is a cheap price for removing an entire bug class that's genuinely difficult to catch in review — a misplaced = versus == doesn't jump out visually nearly as reliably as people assume when they're defending the pattern.

Coding workspace with static analysis tool

2. Rule 17.2 — No Recursion

What it bans: any function calling itself, directly or indirectly.

The case for the rule: stack usage from recursive functions is bounded only by runtime input, not by anything visible at compile time or in static analysis of a fixed call graph — which is precisely the kind of thing that makes worst-case stack analysis, a real requirement for safety-relevant embedded systems, essentially impossible to do with confidence. A recursive function that's fine in every test case can still blow the stack in the field on an input nobody tested.

The case against: some algorithms are genuinely, dramatically more readable recursively — tree traversal being the standard example — and the iterative rewrite (usually involving an explicit stack data structure to simulate the call stack manually) is often more code, with more places to introduce a bug, in service of avoiding a bug class that a competent static stack analysis could arguably catch anyway with sufficiently bounded recursion depth guarantees.

Where we landed: rule stays, full stop, no "well-bounded recursion" exceptions. We've seen the "well-bounded recursion" argument made in good faith by good engineers, and we've also seen "well-bounded" turn out to be wrong after a downstream code change altered an assumption the recursion depth bound depended on, in a part of the codebase nobody thought to re-check specifically for that assumption. Static, guaranteed stack bounds are worth more to us than the readability gain, in code that runs where it does.

3. Cyclomatic Complexity Limits (commonly enforced alongside MISRA, e.g. via Rule 1.2 advisory guidance)

What it bans, practically: functions above a configured complexity threshold (commonly 10-15 independent paths) get flagged and typically need to be refactored or justified.

The case for the limit: a function with 30 branching paths is extremely difficult to fully test — full branch coverage becomes a combinatorial problem, code review effectiveness drops sharply past a certain complexity (reviewers demonstrably miss more in dense, highly-branched functions), and complexity limits force decomposition into smaller units that are each individually easier to reason about and test in isolation.

The case against: splitting a genuinely cohesive piece of hardware-specific state-machine logic into five smaller functions purely to satisfy a complexity number doesn't necessarily make the system easier to understand — it can scatter closely related logic across multiple functions, forcing a reader to jump between five places to reconstruct the one mental model that used to live in one place. Complexity metrics measure something real, but they're a proxy, and proxies can be gamed in ways that satisfy the metric while making actual comprehension worse.

Where we landed: we keep the limit, but treat threshold violations as "justify or refactor," not an automatic hard block — a small number of genuinely irreducible state-machine dispatch functions get an explicit, reviewed waiver with a comment explaining why, rather than being forced into an artificially fragmented decomposition. In practice this waiver gets used rarely — maybe two or three functions per project — which is itself a useful signal that the limit is doing its job on everything else.

Collaborative coding discussion in office

4. Rule 21.3 (and the broader dynamic memory allocation rules) — No malloc/free After Initialization

What it bans: dynamic memory allocation, generally restricted to a fixed set of calls during system initialization if allowed at all, with no allocation during normal runtime operation.

The case for the rule: heap fragmentation in a long-running embedded system with no memory management unit and no process isolation is a real, patient killer — a system that's been allocating and freeing for six days straight can fail from fragmentation in a way that never shows up in a two-hour bench test, and allocation failure handling in C is notoriously easy to get subtly wrong (a missed NULL check on malloc is a classic, recurring defect class).

The case against: static allocation of every possible buffer at compile time, sized for worst-case usage, can waste a genuinely significant amount of RAM on a resource-constrained MCU compared to a well-managed dynamic pool — and modern embedded allocators (fixed-size block pools, for instance, rather than general-purpose heap allocation) largely sidestep the classic fragmentation concern while still providing some runtime flexibility.

Where we landed: general-purpose malloc/free stays banned after init, no debate there — that part of the argument was settled a long time ago industry-wide. But we do permit pre-sized, fixed-block memory pools (allocated once at init, "borrowed and returned" at runtime from a fixed set of same-sized slots) as a deliberate middle ground, since fixed-block pools don't fragment the way general heap allocation does, and the worst-case memory footprint is still fully static and analyzable at compile time.

5. Rule 15.1 — No goto (with the age-old error-handling exception debate)

What it bans: the goto statement, categorically.

The case for the rule: unrestricted goto is the textbook example of code that can jump anywhere, in any direction, making control flow genuinely hard to trace — it's not a coincidence this is one of the oldest and most universally cited "considered harmful" constructs in programming history.

The case against: the single most common counter-argument, and the one that shows up in nearly every embedded C codebase we've ever audited from other teams: cleanup-on-error patterns, where a function needs to release several resources (close a file descriptor, free a buffer, release a lock) in reverse order of acquisition, and a forward-only goto cleanup; at each failure point is often measurably more readable than the nested-if alternative, which tends to produce deep indentation and duplicated cleanup code at every exit path.

Where we landed: this is the one rule where we've formally carved out a documented, reviewed exception — forward-only jumps to a single cleanup label at the end of a function, for resource release on error paths, are permitted, and this exception is written explicitly into our coding standard rather than left as an ad-hoc judgment call in review. Backward jumps, jumps into the middle of other control structures, and multiple different cleanup labels within one function are still hard violations. This is, notably, close to how MISRA C:2012 itself already frames the rule with its documented deviation guidance — we didn't invent this compromise so much as formally adopt the one the standard's authors clearly anticipated teams would need.

Code review in a developer's workspace

The Actual Takeaway

Notice that four of these five rules survive intact in our standard, and the one exception we carve out (goto for cleanup) is one the standard's own documentation explicitly anticipates. That's not a coincidence, and it's not us being unusually conservative — it reflects something genuinely true about MISRA C as a document: most of its "controversial" rules were controversial to someone, once, for a defensible reason, and the standard's authors were often aware of the counter-argument when they wrote the rule anyway, because the failure mode being prevented was judged worse than the readability or flexibility cost.

The debates are worth having in code review, every time they come up — a rule you've stopped questioning is a rule you've stopped understanding. But in our experience, after actually having the argument out loud, the rule almost always wins. The exceptions we do carry — fixed-block pools instead of a blanket allocation ban, a formally scoped goto-for-cleanup pattern — are narrow, documented, and rare enough that they haven't become the thin end of a wedge. That's really the whole discipline here: not "never deviate," but "deviate rarely, explicitly, and only where the standard's own logic clearly leaves room for it."

avatar-hung-nguyen
Mr. Hung Nguyen
Simulation Tools Engineer