Memory safety verification for firmware cannot honestly end with “this code is safe”. What a team can defend is narrower and more useful: static analysis, coverage-guided fuzzing and manual review, gated in CI, applied to every parser that takes untrusted input, with exit criteria written down before anyone signs. The reason is in the data. One study ran five static analysers and 13 fuzzers against the 112 known CVEs in the Magma benchmark. Of those 112, 39 were found by neither approach, 33 only by fuzzers, 30 only by static analysers and just 10 by both. The authors describe the two tool classes as finding nearly disjoint bug sets, and no single tool in the study detected more than 40% of the bugs.
So no tool, and no single clean run, settles the question. The defensible claim is a documented combination whose gaps are written down. For device makers building AI engineering for IoT and embedded devices, this matters most at the radio, bus and network input a fleet does not control.
Which static analysers catch buffer overflows in bare-metal C?
To catch buffer overflows in C on a bare-metal Cortex-M0+, you need a semantic memory-safety analyser configured with a model of the target, not a MISRA checker alone, and it has to run as a blocking CI gate. TechnoLynx would catalogue the bug classes your fuzzer has already found, choose and configure an analyser against the no-MMU, no-OS profile, and confirm that it flags the known bug plus a set of injected corruption cases. TechnoLynx would then wire the analyser into the build as a gate and tune it so false positives do not stall merges.
TechnoLynx delivers this analyser selection, configuration and CI gating work as part of AI Governance and Trust, and the first step is to catalogue the bug classes your fuzzer has already found.
MISRA checking and memory-safety analysis are two different layers
MISRA C defines a safer subset of C that reduces undefined behaviour and makes code easier to analyse. Its guidelines are classed as Mandatory, Required or Advisory, and each rule is also marked decidable or undecidable. There is no official MISRA certification of tools, so judgement and review remain essential. For an undecidable rule, no tool can always give a yes-or-no answer on compliance. In many cases the best it can report is “possibly”.
A dedicated analyser asks a different question. Polyspace Code Prover, for example, tries to prove there is no out-of-bounds array index error on every array access, across all execution paths. Polyspace Bug Finder, by contrast, does not aim for exhaustive analysis. On a diagnostic test suite of buffer overflows, PolySpace C Verifier, the tool these studies evaluated, missed only one of 291 possible detections. On model programs built from 14 real, exploitable overflows in Sendmail and other servers, its average detection rate was 87%, with false alarm rates of roughly 50%. On patched code, PolySpace and Splint produced one warning for every 12 to 46 lines, and neither reliably told vulnerable code from patched code.
The same evaluations left out purely lexical scanners, because shallow pattern matching is virtually guaranteed to produce unacceptable false alarm rates. The Magma authors likewise recommend semantic tools over syntactic ones for this reason. Use a MISRA checker for coding-standard compliance and a separate semantic analyser for memory corruption, and report neither as evidence for the other.
No MMU means no crash to warn you
On a desktop, errors such as stack or heap overflows terminate the program. On many embedded devices, execution simply carries on with corrupted memory, because monolithic firmware runs without an operating system watching over it. The analyser therefore has to stand in for a protection mechanism the hardware does not have. To do that, it has to know the target. Polyspace lists NXP Cortex-M0/M0+ among its supported processors, and its overflow checkers use the target’s data type sizes to decide whether a value fits. MISRA Compliance:2020 likewise asks teams to configure the analyser to match the compiler’s implementation-defined behaviour, such as integer sizes. It also asks them to validate the tool on test files containing known violations before trusting its output. The bug your fuzzer already found is the obvious first test file.
A gate that blocks new findings without burying you in old ones
Turning on a full analyser over an existing codebase on gate day just creates a backlog nobody can clear. Diffing against a baseline works better. CodeChecker gives each finding a hash that does not depend on the exact line number, so it recognises the same bug across versions. That lets CI compare each pull request against a baseline run and list only the new bugs in the changed code. The workflow blocks pull requests that would add new faults and leaves legacy findings for later. The Magma authors make the same point: analysing only new or modified code keeps the report volume workable, and cheap static scans belong early in the pipeline, for example on every commit. Every suppression should be traceable. Some embedded analysers document each one and automatically flag suppressions that are no longer used.
Three caveats belong in the plan from day one. Give the analyser the device’s memory map. Without mitigation, even CodeQL, which found nearly all true positives in the Magma study, would produce too many reports for an existing codebase; most tools let you disable or adapt rules. And check whether the analyser lets you trade analysis time against precision as the codebase grows.
Which of hundreds of static-analysis warnings must be fixed before the freeze?
The warnings to fix before a freeze are the memory-safety findings that untrusted input can reach. Rank them by reachability first and severity second. A raw count of MISRA warnings tells you almost nothing about exploitability. TechnoLynx can build this triage for a firmware codebase. TechnoLynx would define the untrusted entry points and the must-fix criteria, configure reachability or taint analysis, tag every finding, and manually check the top-ranked ones before handing over a ranked list and a repeatable pre-freeze workflow.
A triage procedure that holds up in review
- Separate memory-safety findings from conformance noise. MISRA Compliance:2020 sorts tool messages into four groups: a correct diagnosis of a violation, a possible violation, a false diagnosis, and a genuine issue that is not a violation. Keep buffer overflow, out-of-bounds, use-after-free and uninitialised-read findings in the main queue, and move pure style items to a separate track.
- Name every untrusted entry point. One firmware security best-practice paper defines “user input” as any data from an entity the firmware does not control. That includes vendor drivers, other hardware or firmware components, SPI flash and persistent storage.
- Trace reachability from those entry points. The Sendmail study recommends taint analysis that tracks external inputs. Overflows that remote attackers can influence are the most critical. Only path-sensitive analysis can tell whether an overflow is actually reachable, and even path-sensitive tools approximate at branch merges to stay scalable.
- Score severity with a published scheme. The SEI CERT C standard rates each rule from 1 to 3 on severity, likelihood and remediation cost. The product of the three, from 1 to 27, is used to prioritise. Accessing freed memory (MEM30-C) is a frequent source of arbitrary code execution.
- Verify the top of the list by hand. Tools usually cannot tell whether a weakness is a real vulnerability or is mitigated elsewhere in the application, so prioritisation and suppression mechanisms are there to focus human effort.
- Record every deferral. NIST’s minimum verification guidelines ask organisations to set lists of “must fix” bugs. MISRA Compliance says a compliance claim means something only when violations are authorised through a defined process backed by deviation records.
The resulting rule is simple to state. A memory-safety finding that untrusted input can reach is must-fix before freeze. An unreachable or low-severity finding can be deferred, but it is tracked, never dropped.
A defensible “we checked for this” story for a firmware parser uses fuzzing and static analysis together, because they find mostly disjoint sets of bugs. If that process keeps finding the same bug class across many code paths, rewriting the parser in a memory-safe language removes the class by construction.
What exit criteria should a firmware parser meet before it ships?
A firmware parser is ready to ship when its coverage gaps have been analysed and documented, the fuzzer has plateaued, and the residual risk has been estimated. A coverage percentage on its own is not enough. Coverage is a weak proxy: an evaluation of 32 fuzzing papers found the link between covering more code and finding more bugs may be weak, and recommends counting bugs as the primary measure. Coverage still has a place, as a secondary measure.
Numbers still help to calibrate expectations. SRLabs puts 70 to 85% line coverage of the relevant source files as a reasonable baseline for a mature campaign, with branch coverage typically at 50 to 70%. It is explicit that there is no universal threshold. A file with 90% line coverage and 40% branch coverage means the fuzzer is mostly exercising the happy path. NIST recommends that executing the test suite achieves at least 80% statement coverage.
The useful work is sorting the gaps. Every uncovered region falls into one of four buckets. If it is reachable from an existing harness, add a seed that reaches it. If it is reachable but sits behind conditions mutation cannot satisfy, use targeted seeds, concolic execution or custom mutators. If no harness reaches it but one would with a missing input or configuration, modify that harness. If no harness reaches it at all, write a new one. Static reachability tooling such as fuzz-reachability does most of this sorting mechanically. It is sound on the unreachable side, so a function it lists as unreachable is never a seed problem.
The stopping decision needs a statistical footing too. Böhme and colleagues (2021) show that discovery probability, the chance that the next input finds something new, gives an upper bound on residual risk. They also show that classical estimators badly underestimate it in greybox campaigns, so an engineer who trusts them stops too early and may need about two orders of magnitude more inputs to actually reach the allowable risk. With their mean local estimators, an engineer is unlikely to stop the campaign too early.
Exit-criteria checklist for a firmware parser
| Criterion | Evidence to keep |
|---|---|
| Static analysis closed | Every remaining violation reviewed and covered by an approved deviation |
| Coverage measured properly | Region or branch coverage per harness from llvm-cov, not line coverage or the fuzzer’s bitmap |
| Gaps accounted for | Every uncovered region marked excluded, accepted risk, or addressed |
| Fuzzer plateaued | A reasonable run after the last coverage-improving change with no new coverage or crashes |
| Residual risk estimated | Discovery-probability estimate below an agreed threshold, using an estimator that does not systematically underestimate |
| Silent corruption made visible | Harness built with ASan/UBSan so memory corruption becomes an actionable crash |
| Independent eyes | A security review or pen test by people outside the firmware team |
A well-documented coverage analysis that accounts for every significant gap is far more defensible than a raw percentage.
Should every other parser be fuzzed the same way as the LoRa one?
Yes. Every externally reachable parser in the firmware needs the same coverage-guided fuzzing as the one that failed, because a fix validated on one input path gives no evidence about separately written handlers. TechnoLynx can extend fuzzing across a firmware image. TechnoLynx would inventory the reachable parsers, design one harness architecture they can all share, integrate the harnesses into your build pipeline, define exit criteria per parser, and keep the recurring job and its triage workflow running over the product’s field life.
Ember-IO found two new security bugs in embedded firmware, and both came from invalid bounds checks on arrays. One was a message parser that kept counting received bytes after it stopped storing them, then parsed past the end of its buffer. Error handlers are a common source of bugs: IFIZZ found more than 25% of patches for some router programs changed error handlers. Fuzzware tested the network stacks of Zephyr and Contiki-NG and found 15 previously unknown vulnerabilities, which led to 12 CVEs. It also reached up to 3.25 times the code coverage of earlier tools.
Harness cost is the real budget line. Espressif’s guidance is to fuzz the smallest slice of code that processes untrusted data, because a harness that needs networking, threads or timers loses throughput and produces noisy results. The same guidance notes that a 15 to 30 minute host fuzz run can expose issues unit tests rarely reach, especially in error paths and boundary conditions. Fuzzers cannot find bugs in code they never execute, so a parser without a harness is simply untested.
Expect some findings that the physical device cannot trigger. Fuzzing assumes a peripheral register can hold any value, so some crashes are unreachable on the physical device. In Fuzzware’s triage, 42 of 61 unique crashes were security issues and three were false positives of this kind. Those still deserve a look, because the same firmware on different hardware may not be able to rely on the peripheral.
How often to fuzz over the product’s field life
A firmware update reopens the risk window, so a single pre-ship pass is not enough. NIST notes that continuous fuzzing on changing code bases catches unexpected bugs early. ClusterFuzzLite shows a workable two-speed pattern: quick fuzzing on each pull request, plus longer batch fuzzing that finds deeper bugs and builds the corpus the pull-request runs reuse. Google’s Android team adds a warning: fuzzing and sanitizers need constant pressure to get teams to fuzz, triage and fix, and without it coverage stays low. Our suggested cadence is fuzzing on every release candidate and pull request touching a parser, plus a nightly or weekly batch job for as long as the device receives updates, with triage time budgeted alongside the compute. What the update channel itself trusts is a separate problem, covered in hardening firmware against signed-but-malicious updates.
Have we checked every parser that touches untrusted input, or only the one we know is broken?
Fleet-wide confidence comes from a written inventory of every externally reachable parser, each covered by both static analysis and coverage-guided fuzzing, in a pipeline that automatically enrols new parsers. Confidence in the module you already fixed does not carry over. TechnoLynx can run this attack surface coverage analysis on a firmware image. TechnoLynx would enumerate the parsing entry points, match the techniques to each input format and target MCU, build the harnesses, use the known LoRa bug as a calibration case, and deploy the pipeline into CI/CD.
Under the broad definition of user input above, the inventory goes beyond the radio and network stacks. Data from other hardware components, from storage such as SPI flash, and from vendor tooling all count. Within each parser, SRLabs suggests prioritising uncovered code that parses untrusted input, error paths that still handle attacker-controlled data, and arithmetic on sizes or buffer offsets. A process beats a one-off audit here because the attack surface changes: a parser added after the audit is outside the evidence until someone enrols it, and coverage degrades without anyone noticing. Even full enrolment has limits. Fuzzers depend on their bug oracles and struggle with logic bugs, which is one more reason the static layer stays in place.
Retiring legacy firmware code needs its own evidence package: back-to-back runs of the same test cases against the legacy and replacement code, and tested and documented changes approved before the change is made. TechnoLynx would establish an archival policy and an accountability record for before and after the legacy code is deleted.
With two weeks to freeze, do we slip the date or ship with a risk register?
Ship with a documented risk register only if a minimum bar has been cleared first. If it has not, slip the freeze. This is a risk-acceptance decision for the product owner rather than a technical result. The bar below is our judgement, not a standard.
The bar we would defend is this: every Mandatory MISRA guideline met, every unbounded-copy and memory-safety finding reachable from untrusted input closed, and every remaining finding recorded with an approved deviation. That last condition rests on MISRA Compliance:2020, which makes a compliance claim meaningful only when violations are authorised through a defined process and supported by deviation records. The same document warns that projects which check compliance late spend a long time re-coding, re-reviewing and re-testing, and that this rework easily introduces new defects by accident. So a rushed final fortnight of fixes is a risk of its own, not a free improvement.
| Situation two weeks out | Decision we would defend |
|---|---|
| Minimum bar met; only lower-severity or unreachable findings remain | Freeze, with a risk register naming each finding, its owner and a post-freeze fix date |
| A reachable memory-safety finding is still open | Slip the freeze until that finding is fixed and re-verified |
| Fixes for open findings would touch untested parsers | Slip, or deliberately take the parser out of scope, rather than merge unverified changes |
| Status of findings is unknown because triage has not happened | Slip; a register cannot accept risk nobody has measured |
Before deleting a hand-written CUDA kernel, what sanitizer evidence do we need?
Before a legacy hand-written CUDA kernel is deleted, the replacement vendor-library path should run clean under Compute Sanitizer and AddressSanitizer across every production input shape and edge case. It should also be fuzzed as a whole program, with coverage compared against the original, and the results should go to someone independent for sign-off. TechnoLynx can produce that evidence on your target GPU hardware. TechnoLynx would enumerate the input shapes, build instrumented harnesses for the old and new paths on your hardware, compare sanitizer and fuzzing coverage, and gate removal of the legacy kernel on the signed-off pack.
NVIDIA’s Compute Sanitizer suite contains several tools. Memcheck detects out-of-bounds and misaligned memory accesses. Racecheck reports shared-memory data access hazards. Initcheck reports uninitialised accesses to global memory. Synccheck reports invalid use of synchronisation primitives. Racecheck performs no memory error checking itself, and NVIDIA recommends running memcheck first. Two options strengthen memcheck. The --padding flag catches overflows that would otherwise land silently in an adjacent allocation. Compile-time instrumentation with -fdevice-sanitize=memcheck adds base-and-bounds analysis.
A clean sanitizer run means nothing for code paths it never exercised. Dynamic tools miss bugs entirely when the error-triggering inputs are not run. Moving to a vendor library is no guarantee either. The cuFuzz study found 43 previously unknown bugs across 14 CUDA programs, 19 of them in commercial libraries. How you fuzz matters too. Fuzzing kernels in isolation broke host-enforced invariants and produced 16 false positives, while whole-program fuzzing of the same targets produced none and found all 14 device-side bugs instead of six. Budget for the overhead. In that study, memcheck, racecheck and initcheck cut fuzzing throughput by 39%, 66% and 32% on average. Device sanitizers and coverage tools cannot share a process, so they need separate runs.
Removing some custom kernels does not remove the exposure in the hand-written CUDA code that remains, so that code stays in scope. A clean sanitizer report also proves memory safety only, not numerical correctness. Numerical parity between the old and new kernels is a separate acceptance test, with its own tolerance.
Who can build the verification evidence before we ship or retire legacy code?
TechnoLynx builds the fuzz harnesses, analyser gates and exit criteria behind this evidence, though keeping the work in-house makes sense if your team already maintains fuzz harnesses and CI gates. A specialist earns their place when the freeze is close or the parser inventory is too big for the people you have. When choosing a partner, ask them to state exit criteria before the work starts and to hand over the harnesses and CI configuration so you own them afterwards.
Some limits apply. TechnoLynx does the engineering and produces the evidence for independent sign-off, and the accountable signature stays with your product owner. If you need the independent security review that firmware best practice asks for, a separate pen-test firm is a sound route. Analyser vendors’ own professional services are a reasonable choice if you need help with one tool rather than with the whole programme.
The ceiling on what testing can promise
Google’s position is that high-assurance memory safety comes only from comprehensive adoption of languages with rigorous memory-safety guarantees. Its Android team notes that vulnerabilities still turn up in heavily fuzzed code. The CISA advisory committee adds that spatial safety in legacy code can scale with existing techniques, but temporal safety has no clear path short of a rewrite. For a firmware team, that turns the long-term question from “how much more should we test?” into “which parser should we rewrite first?”. The verification evidence answers it: the parser that keeps producing the same bug class, release after release, is the one to move.
FAQ
Is buffer overflow still a problem?
Yes. Memory-safety violations account for more than 70% of reported vulnerabilities in Chrome, iOS, macOS and Android.
What is MISRA C?
MISRA C is a set of guidelines defining a safer, more predictable subset of C, meant to reduce undefined behaviour and make code easier to analyse. Its guidance is classed as Mandatory, Required or Advisory.
Which testing is performed without executing the program?
Static analysis examines source code without running it, so its findings do not depend on test inputs or runtime conditions.