Deep dive
Decision table
| Situation | Choice | Why |
|---|---|---|
| C firmware, certified (DO-178C, ISO 26262, IEC 61508) | MISRA C:2012 or later edition, plus project deviation records | The rule set auditors and tool vendors expect; mandatory/required/advisory categories fit deviation procedures |
| C code that also faces attackers | Add CERT C rules and the CERT-style undefined-behaviour checks | MISRA is a subset for predictability; CERT targets exploitable behaviour (overflow, bounds, use after free) |
| Small team, no budget for a full standard | JPL Power of Ten as the baseline | Ten rules, each checkable by a simple tool; most of the benefit for small cost |
| New embedded code, free choice of language | Rust with #![forbid(unsafe_code)] plus the clippy lints below | Memory safety by construction; rules shrink to panics, overflow and allocation |
| Rust in a certified product | A qualified toolchain (for example Ferrocene) and a project coding guideline | Certification needs a qualified compiler and documented guideline, not only a safe language |
| Existing C you cannot rewrite | Wrap in a thin checked interface; run static analysis on the C; fuzz it (fuzzing) | Isolation limits blast radius; the rules apply where the data crosses |
| Loop or arithmetic you cannot prove bounded | Rewrite with a constant bound or checked ops; if impossible, record a deviation with a proof or test | Unbounded work breaks worst-case timing (fixed-rate loops) |
What the rule sets actually say (paraphrased)
JPL Power of Ten (Holzmann, 2006), ten rules: (1) simple control flow: no goto, setjmp/longjmp or recursion; (2) every loop has a statically provable upper bound; (3) no dynamic allocation after initialisation; (4) functions short enough for one printed page, about 60 lines; (5) at least two assertions per function on average, side-effect free, with an explicit recovery action; (6) data declared at the smallest scope; (7) check every return value and every parameter; (8) minimal preprocessor use; (9) at most one level of pointer dereference, no function pointers; (10) all warnings on from day one, strong static analysers daily with zero warnings.
MISRA C:2012 (rule numbers I verified): Rule 17.2 forbids recursion (rationale: unbounded stack depth), Rule 21.3 forbids the stdlib.h allocation functions, Directive 4.12 says not to use dynamic memory. Rules are classed mandatory, required or advisory; required ones can be deviated from only with a documented deviation. The full text is paid; do not paste it, cite numbers.
CERT C: rules are identified by area, e.g. INT32-C (signed integer overflow), ARR30-C (out-of-bounds pointers or subscripts), MEM30-C (use after free), EXP34-C (null dereference). Each lists compliant and non-compliant code and the tools that detect it.
Assurance levels (DO-178C): coverage demands grow with the software level; level A requires modified condition/decision coverage, which is why branchy, macro-heavy code is expensive to certify.
Code
A Rust frame parser written to these rules: crate-level lints, no heap, no panics, bounded loop, checked arithmetic, errors as values. It compiles, cargo clippy reports nothing under the denied lints, and the tests pass, including an exhaustive no-panic check over all two-byte inputs.
#![forbid(unsafe_code)]
#![deny(
clippy::unwrap_used, clippy::expect_used, clippy::panic,
clippy::indexing_slicing, clippy::arithmetic_side_effects,
clippy::as_conversions
)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum Err { Short, TooMany, BadSum, Full }
/// Fixed-capacity buffer: no heap, push can fail, never panics.
pub struct FixedBuf<const N: usize> { data: [u8; N], len: usize }
impl<const N: usize> FixedBuf<N> {
pub const fn new() -> Self { Self { data: [0; N], len: 0 } }
pub fn push(&mut self, b: u8) -> Result<(), Err> {
let slot = self.data.get_mut(self.len).ok_or(Err::Full)?;
*slot = b;
self.len = self.len.checked_add(1).ok_or(Err::Full)?;
Ok(())
}
pub fn as_slice(&self) -> &[u8] { self.data.get(..self.len).unwrap_or(&[]) }
}
impl<const N: usize> Default for FixedBuf<N> { fn default() -> Self { Self::new() } }
/// Frame: [count][count payload bytes][sum]; sum = wrapping byte sum of payload.
/// Every input is untrusted, every loop is bounded, every failure is a value.
pub const MAX_PAYLOAD: usize = 32;
pub fn parse_frame(input: &[u8]) -> Result<FixedBuf<MAX_PAYLOAD>, Err> {
let (&count, rest) = input.split_first().ok_or(Err::Short)?;
let count = usize::from(count);
if count > MAX_PAYLOAD { return Err(Err::TooMany); }
let payload = rest.get(..count).ok_or(Err::Short)?;
let sum_byte = *rest.get(count).ok_or(Err::Short)?;
let mut out = FixedBuf::<MAX_PAYLOAD>::new();
let mut sum: u8 = 0;
// Bound is the constant MAX_PAYLOAD, provable without knowing `input`.
for &b in payload.iter().take(MAX_PAYLOAD) {
sum = sum.wrapping_add(b); // wrap is intended, so it is written down
out.push(b)?;
}
if sum != sum_byte { return Err(Err::BadSum); }
Ok(out)
}
Cargo-level enforcement for the whole workspace:
[workspace.lints.rust]
unsafe_code = "forbid"
[workspace.lints.clippy]
unwrap_used = "deny"
indexing_slicing = "deny"
panic = "deny"
arithmetic_side_effects = "deny"
with panic = "abort" in the release profile for firmware, so a missed panic is a reset and not an unwind.
Efficiency
- Cost: developer time (deviation records, review, analyser runs) plus a small run-time price: checked indexing and arithmetic add a compare and a rarely taken branch, order of a cycle each. Bound: developer effort, not CPU. With no allocation after start-up, memory use is a link-time number.
- Levers:
- no heap after init (Power of Ten rule 3, MISRA Rule 21.3) — no allocator latency or fragmentation — all firmware; use
heapless - constant loop bounds — timing analysis gets a number — control loops
- one bounds check per block (
get(..n), iterators) — checks hoisted or elided — hot loops; measure first - fast linters per commit, proofs nightly — short feedback loop — CI
- baseline file, zero new findings — adoption without a rewrite — legacy code
- no heap after init (Power of Ten rule 3, MISRA Rule 21.3) — no allocator latency or fragmentation — all firmware; use
- Naive → standard → state of the art:
- review-only compliance · slow, inconsistent
- one analyser in CI, warnings as errors, Power of Ten rules 1-3 and 7 · most of the benefit, small cost
- qualified toolchain, full rule set with deviation records, proofs (Kani, SPARK) on critical functions · audit-ready, highest effort
Implementation notes
- Make the analyser fail the build; a report nobody reads is not enforcement. Treat new warnings as bugs and keep a baseline file for legacy findings.
- Wrap any
unsafe(register access, DMA) in one small module with// SAFETY:comments and the clippy lintundocumented_unsafe_blocks; put that module under heavier review, and use an MPU to contain what it can reach. - A failed assertion in release must reach a defined state (return an error, enter safe mode, or let the watchdog reset), not
panic!. - Integer conversions: prefer
try_from/Fromtoas(clippyas_conversions); wrapping arithmetic is fine where intended, but writewrapping_addso the intent is visible. - Recursion in Rust is not caught by the compiler; ban it by review or tooling and use explicit stacks with a fixed capacity.
- Record deviations in a table: rule, location, reason, mitigation, approver. Auditors read this first.
Pitfalls
- Chasing 100 % compliance on advisory rules while ignoring requirements and test coverage: the standard is not the safety case.
- Suppressing warnings with blanket
#[allow]or pragmas, which hides the one real finding. - Analysers disagree on the same rule; pin tool and version, and qualify it if its output is evidence.
unwrap()in "can't happen" paths: in a control loop it is a missed deadline or a reset.- Rust's
Vec,StringandBoxneed the global allocator and grow at run time; they quietly violate the no-heap rule. - Rust checked arithmetic is on in debug builds and off in release unless configured; do not rely on overflow panics as protection.
- Believing a safe language means certified: the compiler, core library subset and target also need qualification evidence.
- A
whileloop with a data-dependent exit has no static bound; add a constant cap (take(MAX)).
Variants and alternatives
In production
- Spaceflight: JPL flight software follows the Power of Ten lineage (Holzmann, JPL).
- Automotive: MISRA C began as road-vehicle software guidelines (MISRA); Ferrocene qualifies a Rust toolchain for ISO 26262 up to ASIL D.
- Industrial control: the same Ferrocene toolchain is qualified for IEC 61508 (T3; SIL 3, toward SIL 4).
- Medical devices: Ferrocene is qualified for IEC 62304 (Class C).
- Embedded RTOS: the FreeRTOS kernel conforms to MISRA C:2012 with listed deviations, checked with Coverity.
Implementations
| name | lang | repo | install | verdict | licence | health | as_of | notes |
|---|---|---|---|---|---|---|---|---|
| Clippy | Rust | rust-lang/rust-clippy | rustup component add clippy | — | — | — | — | lints used above: unwrap_used, indexing_slicing, panic, arithmetic_side_effects, as_conversions |
| Kani | Rust | model-checking/kani | cargo install --locked kani-verifier | — | — | — | — | bounded model checker; proves panic-freedom and assertions on harnesses |
| Miri | Rust | rust-lang/miri | rustup +nightly component add miri | — | — | — | — | interpreter that detects undefined behaviour in unsafe code during tests |
| heapless | Rust | rust-embedded/heapless | cargo add heapless | — | — | — | — | fixed-capacity vectors, queues and maps without an allocator |
| Cppcheck | C/C++ | cppcheck-opensource/cppcheck | brew install cppcheck | — | — | — | — | free MISRA addon; you must supply your licensed rule text |
Sources
- Gerard J. Holzmann, The Power of 10: Rules for Developing Safety-Critical Code, IEEE Computer 39(6), 2006. doi
- MISRA, MISRA C guidelines (rule texts are licensed). publisher
- SEI, SEI CERT C Coding Standard. rules
- Rust Clippy lint list. lints
- Safety-Critical Rust Consortium. repo
- Ferrous Systems, Ferrocene documentation. docs · code
- FreeRTOS-Kernel, MISRA Compliance. doc
- AdaCore, SPARK 2014. code