Using coding standards to improve software quality and security
Editor’s Note: In an excerpt from their book Embedded System Security, the authors assess the role of C and C++ coding standards and how compliance leads to more secure code.
Most safety and quality certification standards and guidance rules espouse the use of a coding standard that governs how developers write code. Some of them recommend or require that specific rules be included in the coding standard. The goal of the coding standard is to increase reliability by promulgating intelligent coding practices.
For example, a coding standard may contain rules that help developers avoid dangerous language constructs, limit complexity of functions, and use a consistent syntactical and commenting style. These rules can drastically reduce the occurrence of flaws, make software easier to test, and improve long term maintainability.
It is common for a coding standard to evolve and improve over time. For example, the development team may discover a new tool that can improve code reliability and recommend that management add a requirement that this tool be used during the development process.
It is also common to see a coding standard consisting of guidance rules whose enforcement is accomplished primarily with human code reviews. Developing a new coding standard with dozens of rules that must be verified manually is a sure way to reduce developer efficiency, even if it increases the reliability of the code.
Numerous static code analyzers, and some compilers, can automate large portions of a typical secure coding standard. Furthermore, although some coding standard rules are necessarily language-specific, there are some universally or almost universally applicable rules that should be a part of a high-quality coding standard. Assuming they are improving software quality, the best coding standard rules are those whose enforcement can be automated and are applicable to any software project.
Compilers and other tool chain components (e.g., the linker/loader) often emit warnings rather than halt a build with a fatal error. A warning is an indicator to the developer that a construct may be technically legal but questionable, such as exercising a corner of the language that is not well defined. Such constructs are frequently the cause of subtle bugs. To ensure that developers do not intentionally or accidentally ignore warnings, tell the compiler to treat all warnings as errors. Many compilers have such an option.
Compilers also tend to provide a variety of strictness levels in terms of language standard interpretation. Some compilers are capable of warning the developer about constructs that are technically legal but dangerous.
For example, the Motor Industry Software Reliability Association (MISRA) has published guidelines for the use of the C language in critical systems, and some compilers can optionally enforce some or all of these guidelines that essentially subset the language by excluding constructs believed to lead to unreliable software.
Some MISRA guidelines are advisory and may yield warnings instead of errors; once again, if the MISRA rule is enabled, the compiler should be forced to generate a fatal build error on any non-compliant construct.
The authors are not recommending that all development organizations adopt full MISRA compliance as part of their coding standards. On the contrary, there are good reasons for not adopting the entire standard. What we do recommend is that once management decides to enable a MISRA rule checker that will force product builds to fail on non-conformant source code constructs, the developers should immediately edit the code to ﬁx the discovered issues.
This editing phase brings cost: time spent to change the code, retesting overhead, and risk of adding new ﬂaws during the editing process. Therefore, management must be careful when adopting new coding rules. The following case study demonstrates this need.
Case Study: MISRA C:2004 and MISRA C++:2008
Like any language-related standard, MISRA has many good rules along with a few rules that are either questionable or simply inappropriate for some classes of users and applications.
MISRA 2004, with 141 rules, ﬁxed a few questionable guidelines in the original MISRA1998 standard. If MISRA is used as part of a coding standard, it may be acceptable to enforce only a subset; however, that subset must be carefully considered and approved by management.
It is also important that the MISRA checker (often built directly into the compiler) be able to selectively enable and disable speciﬁc rules within individual code modules and functions.
The following is a sampling of some MISRA rules that demonstrate some of the pitfalls of the C programming language and how selective use of MISRA will help avoid them:
1 Rule 7.1: Octal constants (other than zero) and octal escape sequences shall not be used. The following example demonstrates the utility of this rule:
a | = 256;
b | = 128;
c | = 064;
The ﬁrst statement sets the eighth bit of the variable a. The second statement sets the seventh bit of variable b. However, the third statement does not set the sixth bit of variable c. Because the constant 064 begins with a 0, it is interpreted in the C standard as an octal value. Octal 64 is equal to 0x34 in hexadecimal; the statement thus sets the second, fourth, and ﬁfth bits of variable c.
Because octal numbers range from zero to seven, developers easily misinterpret them as decimal numbers. MISRA avoids this problem by requiring all constants to be speciﬁed as decimal or hexadecimal numbers.
2 Rule 8.1: Functions shall have prototype declarations and the prototype shall be visible at both the function deﬁnition and call. The MISRA informative discussion for this rule includes the sound recommendation that function prototypes for external functions be declared in a header ﬁle and then included by all source ﬁles that contain either the function deﬁnition or one of its references.
It should be noted that a MISRA checker might only validate that some prototype declaration exists for calls to a function. The checker may be unable to validate that all references to a particular function are preceded by the same prototype. Mismatched prototypes can cause insidious bugs, which is worse than not having any prototype. For example, let’s consider the following C function deﬁnition and code reference, each located in a separate source ﬁle:
void read_temp_sensor(float *ret)
*ret = *(ﬂoat *)0xfeff0;
extern float read_temp_sensor(void);
The preceding code fragments are perfectly legal ANSI/ISO C. However,this software will fail since the reference and deﬁnition of read_temp_sensor are incompatible (the former is written to retrieve the return value of the function, and the latter is written to return the value via a reference parameter).
The preceding code fragments are perfectly legal ANSI/ISO C.
However, this software will fail since the reference and deﬁnition of read_temp_sensor are incompatible (the former is written to retrieve the return value of the function, and the latter is written to return the value via a reference parameter).
One obviously poor coding practice illuminated in the preceding example is the use of an extern function declaration near the code containing the reference. Although strict ANSI C requires a prototype declaration, the scope of this declaration is not covered by the speciﬁcation. MISRA rule 8.6, “functions shall be declared at ﬁle scope,” attempts to prevent this coding pitfall by not allowing function declarations at function code level. However, the following code fragment would pass this MISRA test yet fail in the same manner as the preceeding example:
extern float read_temp_sensor(void);
While MISRA does not explicitly disallow function declarations outside header ﬁles, this restriction is an advisable coding standard addition. Declaring all functions in header ﬁles certainly makes this error less likely yet still falls short: the header ﬁle containing the declaration may not be used in the source ﬁle containing the incompatible deﬁnition.
There is really only one way to guarantee that the declaration and deﬁnition prototypes match: detect incompatibilities using a program-wide analysis. This analysis could be performed by a static code analyzer or by the full program linker/loader. We describe the linker approach here for illustration of how a high-quality tool chain can be critical to enforcing coding standards.
When compiling the aforementioned code fragment, the compiler can insert into its output object ﬁle some marker, such as a special symbol in the symbol table or a special relocation entry, that describes the signature of the return type and parameter types used in a function call. When the function deﬁnition is compiled, the compiler also outputs the signature for the deﬁnition. At link time, when the ﬁnal executable image is being generated, the linker/loader compares the signature for same-named functions and generates an error if any incompatible signature is detected.
This additional checking should add negligible overhead to the build time (the linker already must examine the references of functions to perform relocation) yet guarantees function parameter and return type compatibility and therefore improves reliability and quality of the resulting software.
One major advantage of the link-time checking approach is the ability to encompass libraries (assuming they were compiled with this feature) whose source code may not be available for static analysis.
3 Rule 8.9: An identiﬁer with external linkage shall have exactly one external deﬁnition. This rule is analogous to the preceding rule. Mismatched variable deﬁnitions can cause vulnerabilities that will not be caught by a standard language compiler. Let’s consider the following example in which the variable temperature should take on only values between 0 and 255:
unsigned int temperature;
printf(“temperature = %d\n”, temperature);
unsigned char temperature;
temperature = 10;
Without additional error checking beyond the C standard, this program will build without error despite the mismatched deﬁnitions of temperature. On a big-endian machine with 32-bit int type and 8-bit char type, this function will execute as follows:
temperature = 167772160
As with the preceding example with function prototypes, an inter-module analysis is required to detect this mismatch. And once again, the linker/loader is a sensible tool to provide this checking.