A packet parser reads a record count and a record size from a header, multiplies them to get a byte count, allocates memory and copies the records in. The code compiles without a warning, yet if the unsigned multiplication wraps, the allocation is smaller than the copy and the parser writes past the end of the buffer. CERT C's INT30-C uses exactly this pattern to show how unsigned wrapping leads to an undersized allocation.
A useful CERT C workflow connects each finding to a rule, a reviewed change, boundary tests and a recorded result. Static analysis finds the defects at scale; the build configuration, checker coverage and manual review decide what those results can support.
Key Takeaways
-
Conformance to CERT C means no violations of its rules; recommendation findings are guidance and should be reported separately.
-
Check integer products against SIZE_MAX before multiplying, not after, and test the exact boundary.
-
Bounds checks on signed indices need both ends, and string buffers need room for the terminator.
-
A CERT priority level orders remediation work; it is not a severity rating.
-
A clean analyzer report covers only the checks that ran under the recorded configuration.
Define the CERT C Baseline Before Checking Code
The CERT C Coding Standard separates rules from recommendations. Conformance requires code to contain no violations of the rules; recommendations improve code quality but do not establish a rule violation on their own, so reports should keep the two apart.
Pin the reference
The SEI's online CERT C pages change over time. Older snapshots of a rule's risk assessment list a Remediation Cost column, for example, while current pages rate rules on whether a violation is Detectable and Repairable. Record the edition or snapshot date you adopted, the language dialect, the compiler and its flags, the target's integer widths and any recommendations adopted alongside the rules.
Integer widths matter more than they first appear in embedded software development, where a 16-bit int is still common on small microcontrollers and changes where wrapping occurs. Keep this record beside the analyzer configuration so every reviewer reads findings against the same rule text.
Map the overlap with MISRA
Teams already working to MISRA C:2012 do not have to start from zero. MISRA's Addendum 3, published in January 2018, sets out how MISRA C:2012 (including Amendment 1) covers the rules in the 2016 edition of CERT C, which helps when a safety case has to show both.
Keep claims scoped
Passing a set of enabled checks is a narrower statement than conforming to the standard, because not every rule can be checked by static analysis. Record which rules the analyzer supports, which checks were enabled and how the remaining rules were assessed by manual review. Analyzers that map checkers one-to-one to rule identifiers make that record easier to keep. Parasoft C/C++test, for example, maps its checks for the CERT C secure coding standard to CERT rule IDs, such as CERT_C-INT30-a for INT30-C, so each finding traces back to the rule text you pinned.
Check Integer Size Calculations Before Using the Result
The byte-count problem starts with two values parsed from an untrusted header:
size_t bytes = record_count * record_size;
uint8_t *buf = malloc(bytes);
Unsigned arithmetic wraps modulo one more than the type's maximum, so the product can be a valid value that is far too small. INT30-C covers this case. Signed integers fall under INT32-C, where overflow is undefined behavior, so checking whether a sum went negative afterwards is not a portable test. In both cases, compare operands against their limits before the operation.
The helper below validates the product before anything consumes it:
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
bool byte_count(size_t count, size_t element_size, size_t *out) {
if (out == NULL || element_size == 0 || count > SIZE_MAX / element_size) {
return false;
}
*out = count * element_size;
return true;
}
The zero-size check prevents division by zero, and the comparison rejects any count whose product would exceed SIZE_MAX before the multiplication runs. The caller still owns the destination's capacity, the allocation result and whether the count is plausible for the protocol, and it must stop on a false return.
Test the boundary
With element_size equal to 2, a count of SIZE_MAX / 2 returns true, and SIZE_MAX / 2 + 1 returns false without multiplying. Also test a zero count (accepted, writing zero), a zero element size (rejected) and a null output pointer (rejected). The helpers in this article compile cleanly under GCC 13.3 with -std=c11 -Wall -Wextra -Wconversion and pass these boundary cases under AddressSanitizer and UndefinedBehaviorSanitizer; rerun them on your target toolchain, where integer widths may differ.
Preserve intended wrapping
Hash functions, ring-buffer indices and sequence counters often wrap on purpose, and INT30-C lists exceptions for such cases. Confirm that a flagged operation fits an exception and that its callers expect the wrapped value before adding a guard.
Make Buffer Capacity and String Termination Explicit
ARR30-C prohibits forming or using out-of-bounds pointers or array subscripts. Its signed-index example shows why an upper-bound check alone fails: the function rejects indices beyond the table but lets negative values through.
#define TABLE_SIZE 16
static int table[TABLE_SIZE];
bool read_entry(int index, int *out) {
if (out == NULL || index < 0 || index >= TABLE_SIZE) {
return false;
}
*out = table[index];
return true;
}
Test indices -1, 0, 15 (the last valid index) and 16 (the first invalid one). An upper-bound-only check would accept -1, which is why both ends need a test.
Strings need room for the terminator as well as the characters. STR31-C requires destination storage to hold both, and two defects come up again and again: allocating space for the data alone, and assuming strncpy always terminates, which it does not when the source is at least as long as the count.
#include <string.h>
bool copy_label(char *dst, size_t capacity, const char *src, size_t length) {
if (dst == NULL || src == NULL || capacity == 0 || length >= capacity) {
return false;
}
memcpy(dst, src, length);
dst[length] = '\0';
return true;
}
The caller must ensure src holds at least length readable bytes, dst holds capacity writable bytes and the regions do not overlap. The helper does not validate character content, so callers that must reject embedded null characters need a separate check.
Reject or truncate deliberately
Rejection suits a label that must stay intact, such as an identifier used in a later comparison, while truncation may suit display text. Whichever you choose, make truncation detectable to the caller and still terminate the result.
Test the contract. Test length zero (writes only the terminator), capacity zero (fails before any write), length equal to capacity minus one (succeeds) and length equal to capacity (fails, since the terminator would not fit), plus null source and destination pointers.
Prioritize Findings Without Confusing Priority With Severity
A first full scan of a legacy module often produces more findings than a team can fix at once. CERT C gives each rule a risk assessment with a priority value and a level to help order the work.
|
CERT level |
Priority values in the level |
|
Level 1 |
12, 18, 27 |
|
Level 2 |
6, 8, 9 |
|
Level 3 |
1, 2, 3, 4 |
On the current SEI pages, INT30-C and STR31-C are both P9, Level 2, and both have a severity of High and a likelihood of Likely. Level 2 does not mean low severity, so an undersized allocation or a string overflow still needs assessment where it occurs.
Add application context. For each finding, note whether the length or index comes from untrusted input, whether the path is reachable in the shipped configuration and how the change affects callers. A wrap on a compile-time constant needs different investigation from a wrap on a field parsed from a serial link, even when both map to INT30-C.
Record the disposition. Mark each finding as fixed, a false positive, covered by a documented rule exception or still open, with the reasoning. A useful record holds the rule identifier, build configuration, owner, decision and a trigger for re-review, such as a change to the function or the analyzer version.
Put Automated CERT Checks Into the Development Pipeline
An analyzer flags repeated instances of an unchecked operation across the whole code base, and running it on every change makes the review repeatable. Its results still depend on what it can see, so the analysis setup needs the same care as the code.
Reproduce the build. Take include paths and preprocessor definitions for each build variant from the real build rather than retyping them. Record target assumptions such as integer widths, the signedness of plain char and alignment, along with the analyzer version and exact rule configuration for each source revision.
Separate rapid feedback from broader checks. Analyze changed code at commit or pull-request time so developers see findings while the change is fresh, and run scheduled full analysis across every relevant configuration. A changed-code scan may not cover effects in unchanged callers, so define which findings block a merge and how the rest are tracked.
Make reports traceable. A findings list alone does not show which rules were assessed, so reports should separate supported rules, enabled checks and rules covered by manual review. Tie each finding to its rule identifier so reviewers can check it against the pinned rule text.
Close Findings With Evidence
For the opening multiplication defect, the review record should name INT30-C, show the unchecked product replaced with byte_count and confirm the caller stops on false. It should also hold the boundary-test results at SIZE_MAX / 2 and the next count, the rerun analysis under the recorded configuration and the reviewer's decision.
Store a compact record with the commit or release. The following is a suggested team artifact rather than an official certification template:
-
Source revision and build profile analyzed.
-
Adopted CERT C reference, analyzer version and rule configuration, including supported and enabled checks.
-
Reviewed findings, each with a disposition and owner.
-
Boundary-test results tied to each fix.
-
Unresolved work, with a planned re-review trigger.
Expand coverage deliberately. Start with a bounded module, such as the parser that handles externally supplied lengths, and complete the analysis, fixes, tests and review there before extending checks to other modules and build configurations.
From Findings to Conformance
CERT C conformance is built finding by finding: a pinned reference, a reproducible analysis, a reviewed fix and a boundary test that proves it. Static analysis does the heavy lifting of finding every unchecked product and unbounded copy, while manual review covers the rules a tool cannot decide.
Start with one module, record each decision and let that record carry the claim. The same discipline that closes an INT30-C finding in a header parser scales to a whole code base.
FAQ
What is the difference between a CERT C rule and a recommendation?
Conformance requires no violations of the rules. Recommendations are guidance for improving code quality, and a recommendation finding alone does not establish a rule violation.
Does a clean static analysis report mean my code conforms to CERT C?
No. Not every rule can be checked by static analysis, so conformance also needs manual review of the rules the analyzer does not cover.
What is the difference between INT30-C and INT32-C?
INT30-C covers unsigned operations that wrap, while INT32-C covers signed operations that overflow, which is undefined behavior in C.
Is a Level 2 CERT finding low severity?
No. Levels group priority values to order remediation. INT30-C and STR31-C are both Level 2 with a severity of High.
Can MISRA C compliance help with CERT C?
Yes. MISRA C:2012 Addendum 3 maps how MISRA C:2012, including Amendment 1, covers the rules in the 2016 edition of CERT C.

