Firmware runs without most of the safety nets that protect desktop and server code, so a single out-of-bounds write in a radio parser or USB handler can give an attacker the whole device, often permanently. Many microcontrollers have no memory management unit, no address space layout randomisation and no separate kernel, and the same binary may ship to millions of units that are rarely updated. This guide covers how to write safer C and C++ for embedded targets: choosing and applying MISRA C and the SEI CERT C Coding Standard, the embedded-specific bug classes, the hardware and compiler protections worth turning on, where Rust fits, and how to prove it all in a pipeline. It is for firmware engineers, embedded team leads and product security teams. General C and C++ patterns, hardening flags and sanitizers are covered in the C and C++ Secure Coding Guide; device-level architecture is in the Embedded Device Security Guide.
Key takeaways
- Pick a coding standard and enforce it in CI. MISRA C (current edition MISRA C:2025) is the established safety-and-security subset; the SEI CERT C Coding Standard is security-focused; MISRA publishes addenda mapping its guidelines to CERT C and to CWE memory safety weaknesses.
- Treat every external byte as hostile. Radio frames, USB descriptors, sensor buses, bootloader commands and update images all cross a trust boundary; MISRA Directive 4.14 requires validating values received from external sources.
- Remove whole bug classes by design. No dynamic allocation after start-up, no recursion, fixed-size buffers with explicit lengths and checked integer arithmetic eliminate many of the faults attackers rely on.
- Use the hardware you have. An MPU, privileged and unprivileged thread modes, stack guards and execute-never regions turn silent corruption into a contained fault.
- Keep secrets out of the source. Hard-coded passwords and keys in firmware are recoverable by anyone with a device; signing keys belong in an HSM and per-device credentials in secure storage.
Why firmware code needs its own rules
Desktop secure coding advice assumes an operating system that isolates processes, randomises memory layout and can be patched weekly. Firmware often has none of that, which changes both the impact of a bug and which mitigations are available.
| Constraint | Typical on microcontrollers | Security consequence | Coding response |
|---|---|---|---|
| Flat memory, no MMU | Code, data, stack and peripherals share one address space | An overflow can reach any variable, the vector table or peripheral registers | Bounds checks at every boundary; configure the MPU to separate regions |
| No ASLR | Fixed load addresses | Exploits are reliable across every unit | Prevent the bug class rather than relying on randomisation |
| Small, fixed stacks | Hundreds of bytes to a few kilobytes per task | Deep calls or large locals overflow silently into other data | No recursion; worst-case stack analysis; stack guards |
| Interrupts and DMA | Concurrent writers to shared memory | Race conditions and buffer overruns from peripheral transfers | volatile correctly, critical sections, DMA length checks |
| Long field life, slow updates | Years between updates, or none | A bug stays exploitable for the product's life | Higher bar before release; signed update path |
| Physical access | Debug ports, flash readout | Firmware and any embedded secrets can be extracted | No secrets in code; lock debug in production builds |
Choosing a coding standard: MISRA C, CERT C and MISRA C++
A coding standard turns "write safe code" into checkable rules, and both safety and product security assessors expect to see one.
| Standard | Owner and status | Focus | Best fit |
|---|---|---|---|
| MISRA C:2025 | MISRA; published March 2025 as an incremental update to MISRA C:2023, supporting C11, C18 and earlier | A restricted, analysable subset of C for critical systems; directives plus rules, many decidable by tools | Automotive, industrial, medical and any team needing a safety case |
| MISRA C:2012 / C:2023 | MISRA; still widely referenced by projects and tools | Same model; C:2012 introduced decidability classification | Existing codebases and toolchains already certified against it |
| SEI CERT C Coding Standard | Carnegie Mellon SEI; living wiki with periodic official releases | Security: rules and recommendations grouped by area (integers, memory, strings, arrays and others) | Connected devices where security assessors ask for CERT compliance |
| MISRA C++ | MISRA; first published 2008, later editions update it for modern C++ | Safe subset of C++ for critical systems | C++ firmware and middleware |
They are complementary, not rival choices. MISRA publishes addenda that map its guidelines to other standards: MISRA C:2023 ADD3 covers the second edition of CERT C, ADD2 covers ISO/IEC 17961, and MISRA C:2025 ADD5 maps CWE memory safety weaknesses (CWE-1399) to MISRA guidance. A team can adopt MISRA C as its base and use the addenda to show CERT C coverage. MISRA Compliance:2020 explains how to claim compliance, including recording justified deviations, and MISRA C:2012 Permits give pre-approved deviation patterns for hardware access and generated code, both common in firmware.
Rules that matter most for security
The Zephyr RTOS project publishes the subset of MISRA C:2012 that it requires for new code, with CERT C equivalents. It is a useful, public example of the rules that carry the most security weight:
| MISRA C:2012 | Requirement | CERT C equivalent | Bug class it prevents |
|---|---|---|---|
| Dir 4.14 | Validity of values received from external sources shall be checked | None listed | Parser overflows, injection of out-of-range commands |
| Dir 4.12 and Rule 21.3 | No dynamic memory allocation; no malloc and free from stdlib.h | API03-C, API04-C, STR01-C, MSC24-C | Use after free, double free, heap exhaustion |
| Rule 1.3 | No undefined or critical unspecified behaviour | None listed | Compiler optimisations that delete security checks |
| Rule 9.1 | Automatic variables shall not be read before being set | None listed | Information leaks from stale stack data |
| Rules 10.1 to 10.8 | Essential type model: no inappropriate, narrowing or mixed-type arithmetic | INT02-C, INT31-C and others | Integer truncation and sign errors that become buffer overflows |
| Rule 17.2 | Functions shall not call themselves, directly or indirectly | MEM05-C | Unbounded stack growth |
| Rule 17.7 | Return values of non-void functions shall be used | None listed | Ignored errors leaving buffers or states unchecked |
| Rule 18.1 | Pointer arithmetic shall stay within the same array | ARR30-C, ARR39-C, EXP08-C | Out-of-bounds reads and writes |
| Rule 21.17 | string.h functions shall not access beyond object bounds | None listed | Classic string buffer overflows |
| Rule 22.1 | Dynamically obtained resources shall be released | None listed | Resource leaks leading to denial of service |
Embedded bug classes and safe patterns
- Length fields from the wire: the most common firmware vulnerability is trusting a length or count in a received packet (CWE-120, CWE-787). Parse into a fixed-size structure, check the declared length against both the buffer size and the bytes actually received, and reject rather than truncate.
- Integer overflow before allocation or copy:
count * sizeoroffset + lencan wrap on 16-bit and 32-bit targets (CWE-190). Use unsigned fixed-width types from stdint.h, check before the arithmetic, and keep the essential type rules on. - Interrupt and DMA races: data shared between an interrupt service routine and the main loop must be
volatileand updated in a critical section or with atomic operations. Check DMA transfer lengths against the destination buffer, because the peripheral will happily write past it. - Stack exhaustion: with no recursion and no variable-length arrays, worst-case stack use becomes computable. Use the toolchain's stack usage output and size each task's stack with a margin, then confirm with a stack guard or watermark at run time.
- Use after free in static pools: even without malloc, block pools and message queues can hand out a buffer that is still referenced. Clear ownership rules (one owner, explicit hand-off) prevent it.
- Error paths and fault handlers: a hard fault handler that simply resets the device can be used to bypass checks or loop a device into denial of service. Log the fault cause to persistent storage and decide deliberately between safe state and reset.
- Debug and test code in production: UART consoles, test commands and unlocked JTAG or SWD ports left enabled are a direct way in. Build them out with configuration options and verify the production image. Zephyr's guidance is that rarely used features stay off by default and must be explicitly enabled.
Hardware and compiler protections for firmware
| Protection | What it does | Where available | Note |
|---|---|---|---|
| Memory protection unit (MPU) | Enforces access rights per memory region, including no-execute for RAM | Many Arm Cortex-M and other MCUs | RTOSes such as Zephyr use it for thread separation and stack protection (Zephyr added memory protection for Arm in version 1.11.0) |
| Privileged and unprivileged modes | Runs application threads without direct access to kernel data or critical peripherals | Cortex-M and RTOS user mode features | Pair with a narrow system call interface |
| Stack canaries | Detects stack buffer overwrites before return (D3FEND Stack Frame Canary Validation, D3-SFCV) | GCC and Clang -fstack-protector-strong (GCC 4.9.0 and Clang 6.0.0 onwards) | Needs a canary value and a failure handler suitable for bare metal |
| Automatic variable initialisation | Initialises locals that lack an initialiser | -ftrivial-auto-var-init=zero (GCC 12, Clang 8) | Backs up MISRA Rule 9.1 |
| Defined overflow behaviour | Stops the compiler treating signed overflow as impossible and removing checks | -fno-strict-overflow | Supports Rule 1.3 intent |
| Conversion warnings | Flags implicit narrowing and sign changes | -Wconversion, -Wsign-conversion | OpenSSF advises enabling from day one on new code, and a baseline-then-ratchet approach on existing code |
| TrustZone-M or a TEE | Separates secure services (keys, crypto, boot) from application code | Armv8-M and some other MCUs | See the Hardware Root of Trust Guide |
The OpenSSF Compiler Options Hardening Guide explicitly includes embedded and IoT targets in its audience. Some flags need runtime support that bare-metal C libraries do not provide, so test each one on the target rather than copying a Linux flag set.
Where Rust fits
Rust removes most memory safety bugs in safe code, and the Embedded Rust Book, maintained by the Rust Embedded Working Group, covers bare-metal development on microcontrollers using Arm Cortex-M examples and how to mix C and Rust in one project. MISRA C:2025 ADD6 assesses how MISRA C guideline issues apply to Rust. A practical route for existing products is to write new parsers, protocol handlers and update logic in Rust, keep vendor drivers in C, and review every unsafe block like C. See the Rust Security Guide and the Memory Safety Roadmap Guide.
Where identity fits: secrets and keys in firmware
- No shared default passwords: CISA's Secure by Design pledge names default passwords, universally shared across a product, as a goal to eliminate. Generate a unique credential per device at manufacture or require one at first use (CWE-798 Use of Hard-coded Credentials; NIST SP 800-53 IA-5(7) No Embedded Unencrypted Static Authenticators).
- Per-device identity, not a fleet key: a key compiled into every image is extracted once and works everywhere. Provision per-device keys into secure storage. See the IoT Device Identity with TPM and DICE Guide.
- Cloud tokens on devices: devices that call cloud APIs should use short-lived, scoped tokens obtained with the device identity, not long-lived API keys in flash.
- Signing keys are the crown jewels: in the MSI signing keys leak, a ransomware group published firmware source code alongside image signing keys for 57 products and Intel Boot Guard keys for 116, meaning modified firmware could in principle pass verification. Keep signing keys in an HSM, separate from source repositories and build servers. See the Code Signing Security Guide.
Implementing it: a phased approach
- Baseline: choose the standard (MISRA C:2025 or the edition your tools support, with CERT C mapping via the addenda) and write a short project profile listing which guidelines are required, which are advisory and how deviations are approved.
- Tooling in audit mode: run a MISRA-capable static analyser and compiler warnings on the existing code without failing the build. Record the baseline count per rule.
- Fix the high-value rules first: external input validation, pointer arithmetic, string bounds, essential types and dynamic allocation. These map most directly to exploitable bugs.
- Gate new code: fail CI on new violations of required rules, while existing findings burn down. Each deviation needs a written justification and reviewer sign-off under MISRA Compliance:2020.
- Add dynamic testing: fuzz parsers on the host with sanitizers, using the same source compiled for the PC, and fuzz interfaces on hardware where possible. See the Fuzzing Guide.
- Enable protections on target: configure the MPU, stack guards and compiler hardening in a test build; measure code size, timing and power; then enable in a pilot product line.
- Lock production builds: strip debug commands, disable or lock debug ports, verify no secrets in the image, and sign the release. Roll back by shipping the previous signed image through the normal update path; never by re-enabling debug access in the field. See the IoT Secure Firmware Update Guide (SUIT, signed OTA).
Decision guidance
- Safety-certified product: MISRA is normally expected; add CERT C coverage through ADD3 for the security case.
- Connected consumer or industrial device without a safety case: start with the security-critical subset above and CERT C rules for integers, memory and strings, then widen.
- Small team, one product: adopt an existing published subset (such as Zephyr's) rather than writing your own profile.
- Large portfolio: one organisation-wide profile and shared deviation register, with per-product additions, so findings are comparable across teams.
- New component on a capable MCU: consider Rust for parsers and protocol code; keep C where vendor SDKs require it.
Measuring it
- Open required-rule violations per thousand lines, trending down release by release.
- Deviation count and age: approved deviations, with any older than one release reviewed.
- Fuzzing coverage of external input handlers, and crashes found per month.
- Memory safety defects found after release, by CWE class.
- Production image checks passed: debug locked, no hard-coded secrets, signature verified, for every release.
Framework mapping
| Practice | NIST SP 800-53 Rev. 5 | NIST CSF 2.0 | ATT&CK or D3FEND |
|---|---|---|---|
| Coding standard and secure development process | SA-15 Development Process, Standards, and Tools; SA-8 Security and Privacy Engineering Principles | PR.PS-06 | None |
| Static analysis against MISRA or CERT C | SA-11(1) Static Code Analysis | PR.PS-06 | None |
| Fuzzing and developer testing | SA-11 Developer Testing and Evaluation | PR.PS-06 | None |
| MPU, stack guards, no-execute memory | SI-16 Memory Protection | PR.PS-06 | D3-SFCV Stack Frame Canary Validation |
| No hard-coded credentials | IA-5(7) No Embedded Unencrypted Static Authenticators | PR.AA-01 | None |
| Signed firmware and integrity checks | SI-7 Software, Firmware, and Information Integrity | PR.PS-06 | Counters T1542.001 System Firmware and T1195.002 Compromise Software Supply Chain |
Common failure patterns
- Standard on paper only: a MISRA claim with thousands of unreviewed violations and no deviation records will not survive an assessor and does not prevent bugs.
- Blanket deviations: suppressing a rule project-wide because vendor headers trigger it hides real violations in your own code; scope deviations to the file or line.
- Testing only the happy path: parsers that were never given malformed input are where field exploits come from; fuzz every interface that accepts external data.
- Debug left on: production units with an open SWD port or a hidden UART menu make every other control irrelevant.
- Fleet-wide secrets: one hard-coded key or password extracted from a single device compromises the whole installed base.
- Signing keys on the build server: as the MSI case shows, keys stored alongside source can leave with it.
Practitioner checklist
- Adopt MISRA C (edition supported by your tools) or CERT C, with a written project profile and deviation process.
- Run static analysis on every merge and fail the build on new required-rule violations.
- Validate every externally received length, count and command against buffer sizes and allowed ranges.
- Ban dynamic allocation after start-up and recursion; compute worst-case stack per task.
- Enable the MPU, stack guards and suitable compiler hardening on target builds.
- Fuzz parsers and protocol handlers on the host with sanitizers.
- Remove debug commands and lock debug ports in production images, and verify it per release.
- Remove hard-coded passwords and keys; provision per-device credentials into secure storage.
- Keep firmware signing keys in an HSM, separate from source and build infrastructure.
- Track violations, deviations and post-release memory safety defects as release metrics.
Standards and references
- MISRA: MISRA C
- MISRA: Publications (addenda, Compliance:2020, Permits)
- Carnegie Mellon SEI: SEI CERT C Coding Standard
- Zephyr Project: Coding Guidelines
- Zephyr Project: Secure Coding
- OpenSSF: Compiler Options Hardening Guide for C and C++
- Rust Embedded Working Group: The Embedded Rust Book
- CISA: Secure by Design Pledge
Related NHI Mgmt Group resources: C and C++ Secure Coding Guide · Embedded Device Security Guide · Firmware and Boot Security Guide · Memory-Safe Languages Guide · Secure Software Development Lifecycle (SSDLC) Guide