Home › Embedded, industrial & energy › Safety-critical design
guide medium

Safety-critical coding standards

Problem Teams writing firmware or control code whose failure could cause harm, or whose certification is audited, must decide which coding rules to adopt. This guide answers what the common standards enforce and how.

Which rule sets (MISRA C, CERT C, JPL Power of Ten, Rust lint subsets) apply when software must not fail, what they forbid, and how to enforce them in CI.

misracert-cpower-of-tendo-178ciso-26262ruststatic-analysis
Overview

A safety-critical coding standard is a language subset where the failures you cannot afford (unbounded loops, heap exhaustion, undefined behaviour, unchecked errors) are unwritable or mechanically detectable. MISRA C, CERT C and JPL's Power of Ten share one core: bounded control flow, no heap after start-up, every error and input checked, and a tool, not a reviewer, enforcing it on every build.

Key facts

Use when

  • Firmware or control code whose failure causes harm or whose certification evidence is audited: automotive (ISO 26262), industrial control (IEC 61508), medical devices (IEC 62304), spaceflight (JPL flight software), avionics (DO-178C).
  • You want a defensible, tool-checked baseline before a safety monitor or watchdog is even needed.

Avoid when

  • Throwaway tools or non-critical services: full MISRA compliance costs deviations and review time for no safety gain; take only the cheap rules (warnings as errors, no recursion, checked returns).
  • You hope the standard replaces design: rules catch coding faults, not wrong requirements or missing redundancy (hot standby, voting).

Deep dive

Decision table

SituationChoiceWhy
C firmware, certified (DO-178C, ISO 26262, IEC 61508)MISRA C:2012 or later edition, plus project deviation recordsThe rule set auditors and tool vendors expect; mandatory/required/advisory categories fit deviation procedures
C code that also faces attackersAdd CERT C rules and the CERT-style undefined-behaviour checksMISRA is a subset for predictability; CERT targets exploitable behaviour (overflow, bounds, use after free)
Small team, no budget for a full standardJPL Power of Ten as the baselineTen rules, each checkable by a simple tool; most of the benefit for small cost
New embedded code, free choice of languageRust with #![forbid(unsafe_code)] plus the clippy lints belowMemory safety by construction; rules shrink to panics, overflow and allocation
Rust in a certified productA qualified toolchain (for example Ferrocene) and a project coding guidelineCertification needs a qualified compiler and documented guideline, not only a safe language
Existing C you cannot rewriteWrap 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 boundedRewrite with a constant bound or checked ops; if impossible, record a deviation with a proof or testUnbounded 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
  • 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 lint undocumented_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/From to as (clippy as_conversions); wrapping arithmetic is fine where intended, but write wrapping_add so 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, String and Box need 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 while loop with a data-dependent exit has no static bound; add a constant cap (take(MAX)).

Variants and alternatives

MISRA C++ / AUTOSAR C++14the C++ counterparts for MISRA-style subsetting.
CERT C / CERT C++security-focused rule sets, often combined with MISRA.
SPARK Adaproof of absence of run-time errors in a language subset; wins when proof, not testing, is the evidence.
Formal verificationmodel checking and bounded proofs (Kani for Rust) replace coding rules with proofs on critical functions.
Safety-Critical Rust Consortium guidelinesan ongoing effort to write a Rust coding guideline for certification contexts.
Defensive architecturea safety monitor and fault isolation and recovery tolerate faults the rules cannot prevent.

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

namelangrepoinstallverdictlicencehealthas_ofnotes
ClippyRustrust-lang/rust-clippyrustup component add clippy————lints used above: unwrap_used, indexing_slicing, panic, arithmetic_side_effects, as_conversions
KaniRustmodel-checking/kanicargo install --locked kani-verifier————bounded model checker; proves panic-freedom and assertions on harnesses
MiriRustrust-lang/mirirustup +nightly component add miri————interpreter that detects undefined behaviour in unsafe code during tests
heaplessRustrust-embedded/heaplesscargo add heapless————fixed-capacity vectors, queues and maps without an allocator
CppcheckC/C++cppcheck-opensource/cppcheckbrew 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

Builds on

Used by

Systems (22):

Alternatives and related

Watchdog timerUnattended devices (field sensors, motor controllers, vehicle ECUs) eventually hang and must recover without a human. This detects a stuck program within a bounded time and forces a reset to a defined state.Fixed-rate control loopA sampled controller (motor loop, balancing, filters) on a microcontroller or real-time OS silently changes behaviour when its loop timing drifts or jitters. This keeps the loop running at a steady rate.Hot-standby redundancyA controller whose stop or restart is unacceptable (PLCs, gateways, servers in a control loop) needs a second unit that takes over seamlessly after a crash, while never letting two units drive the outputs.Alarm managementOperators supervising a plant, building or fleet from an alarm list get buried when sensors chatter around limits and a single trip fires thousands of alarms. This keeps the list limited to what needs a response.
safety monitor architecture · plannedIndependent doer/checker safety monitor that forces a safe state when the main channel misbehaves
Model checking and formal proofEngineers of concurrent or safety-critical systems must show that a protocol or a piece of control logic is correct in every case, including rare event orders that tests, reviews and simulations never reach.Memory protection unitFirmware that runs several tasks, or trusted and untrusted code, in one flat address space on a small microcontroller has no virtual memory. A buggy or hostile task can overwrite a neighbour's data, and a stack overflow corrupts memory silently.
fault detection isolation recovery · plannedMonitor/response tables, fault isolation and safe-mode recovery hierarchies for autonomous systems
Coverage-guided fuzzingAnyone shipping code that parses untrusted bytes (codecs, protocol stacks, interpreters, firmware) must find crashing inputs nobody thought to write. Hand-written test cases and random bytes both miss deep paths.