Committed to connecting the world

PP-22
Some suggestions on how to write more safe and secure programs - Implementation

Ensure that the program doesn't try to read or write data outside an allocated memory block (buffer overflow)

This is an extremely common program flaw, so it deserves special attention. An important example of this is writing data to a memory block without first ensuring that the memory block is large enough to hold all the data. Another important example is writing to addresses immediately below or immediately above an allocated block. Writing outside the memory block is usually more dangerous than reading, but both must absolutely be avoided. For example, in C/C++, whenever a memory block is accessed through a pointer or array name (possibly with an offset) or a string variable name, the developers must take extreme care to avoid accessing data on either side of the memory block. A very common source of this error is using the C runtime-library string-manipulation functions (strcpy, sprintf, etc.) without making sure that the resulting string will fit into the buffer.

Ensure that all resource allocation errors are detected and handled

Checking for memory allocation errors is very important, but since such errors are rare in normal conditions and probably yet more so during program development, it may happen that the developers fail to write code that handles them. The same holds for any errors occurring in the allocation of other resources, such as disk space, file handles, communications sockets, windows, and so on. Failure to check for these errors is a serious program flaw. Programs released with such defects may work well for months but then they may fail under unusual system-stress conditions.

Ensure that the program's stack never overflows

Whenever a program calls a function using more than the space currently available in the stack for storing the parameters of the call and the local variables of the function (plus a few other things), the stack overflows. Certain languages (or language processors) provide some assistance, throwing an exception on any attempt to overflow the stack, but in the general case the program itself must prevent such an event from happening. As with memory allocation errors, even if the language processor checks for stack overflows, this is of little help if the program doesn't handle a resulting exception. With any programming language, it is much better to take preventive actions to avoid stack overflows altogether. Special care must be taken in the functions that implement recursive algorithms, by ensuring that function calls never nest beyond some given practical limit. Note that in some cases a recursive loop can involve other functions (not apparently recursive) due to two-way interactions with the system (presence of callbacks). All the possible cases must be carefully examined to identify the potential dangers and provide defensive measures. The most appropriate stack size should also be carefully determined.

Check boundary conditions

This depends very much on the application, but as a general principle, the developers should make sure that the program (or a given program fragment) behaves correctly when the variables take certain special values such as 0, -1, +1, the size of a container, the size of a container minus 1 or plus 1, etc. Many programming errors are related to a wrong handling of such special cases.

Use tools for checking the correctness of the program's code

The ability of a human reader to discover defects when reviewing a program's source code is often limited and tools can be very helpful in identifying troublesome code. Such tools can perform various kinds of source code analysis, which may or may not be based on observing the program while it is running. Interestingly, code analyzer tools for the C and C++ languages largely focus on identifying potential buffer overflows and on tracking the use of dynamically allocated variables to reveal potential memory leaks and misuse of pointers. These tools usually offer a tradeoff between complexity of the analysis and performance. In general, they do a good job of finding defects, although they may miss some of them and may also find "false positives" when faced with unusual coding patterns (which is not necessarily a bad thing, as it may indicate lack of clarity in the source code).

Limit use of privileged modes during program execution

In many operating systems, there are operations that can be invoked only by programs running in a privileged mode (an example of this is setting a listener on a low-numbered port in UNIX). However, while the program must be running in a privileged mode at the moment the special operating system function is called, it doesn't have to do so during the rest of the execution. If a program error (such as a buffer overflow) occurs while the program is running in a privileged mode and an attacker manages to exploit the error, he/she ends up with much greater control over the system. Special care is needed when the program invokes some external code (a library function, another program, or a system service). In such cases, you should ensure that the privilege levels at which the external function or process is executed are not higher than is necessary. In general, it is a good idea to limit the number of times, the extent of code, and the duration of time in which the application uses a privileged mode, so that the potential damage in case of attack is greatly reduced. (A historical example of this vulnerability is in the UNIX sendmail program, which unnecessarily ran in a privileged mode all the time; attackers have been able to exploit a buffer overflow to gain full control over the entire system.)

When processing an input message, check that the message can be safely decoded and that its contents are valid

In general, the application should not assume that a message received from the network is well-formed and the values of its fields after decoding are valid and consistent according to the protocol. The only case in which the application can safely make such assumptions is when it uses a toolkit (such as an ASN.1 toolkit) and the documentation of the toolkit states that any ill-formed or invalid messages are not delivered to the application or are delivered with a warning.

When processing an input message, limit the resources (memory, disk space, CPU time) used for the message

The purpose is to protect the system from any abnormal behavior of the application following the receipt of a message carrying some unexpected values. The abnormal behavior may consist of entering an infinite program loop, or in allocating a huge amount of memory or disk space, etc. In these cases, a defect in the software is the cause of the abnormal behavior. The event may occur accidentally, or be the result of an intentional attack performed by sending a specially constructed message. Obviously the problem, as soon as it is diagnosed, needs to be solved in another part of the application; yet the suggested measure prevents worse consequences.

Make generous use of assertions in your code

Assertions are not a substitute for error checks, but are very useful during program development to discover bugs in the program. The purpose of assertions is to verify those assumptions that must be true if the program is correct, and to help to catch program bugs as early as possible in the program execution flow. Whenever an assertion is not verified, the runtime library code throws an exception, so that the developer is immediately informed of the problem and the place where it occurred. (Examples of common assertions are: asserting that a pointer is not NULL, asserting that a variable has a value within a given range, asserting that the values of the member variables of an object are in a certain relationship to one another, etc.) In most C/C++ compilers, assertions are completely discarded by the language preprocessor during release builds, so that they don't at all affect the size and performance of the final program. Assertions are also useful as a form of in-line documentation of the program code.

Use restrictive language features extensively

Such a practice allows you to discover many potential logical defects at compile time and reduce the number of potential errors that can occur at a later time. Let the compiler help you. When declaring variables, use the most restrictive data type that comprises all the values needed. Exploit the name-scoping facilities of the programming language. Limit the lifetime and visibility of variables as much as possible. In C++, use protected/private member variables and methods to the largest extent possible. Also, in C++, make a clear distinction between methods that modify the state of the object and methods that don't and declare all of the latter as "const". Besides allowing early discovery of many program defects, such a programming style allows you to convey more semantic information to the human reader.

Compile with the highest warning level

Let the compiler output all the warning messages it is able to produce. In general, it is preferable to make small changes to the program to make the compiler happy rather than suppress the output of warning messages. Some of the warnings may seem unnecessary, but others are really helpful in spotting programming errors. A well-written program should compile with no warnings at all, although this is not always easy to achieve.

[ Table of Contents ]