A function’s return value and parameters may be decorated with attributes to convey additional information to the compiler and/or other
developers.
A commonly used attribute is nonnull which can be used to mark a function’s return value and parameters as shown in the following:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
__attribute__((returns_nonnull)) int *
make_array_copy(int *src, size_t len) __attribute__((nonnull(1)));
int *make_array_copy(int *src, size_t len) {
int *dst = (int *)malloc(len * sizeof(int));
if (dst == NULL) {
perror("malloc failed");
exit(1);
}
memcpy(dst, src, len);
return dst;
}
The nonnull attribute is meant for other developers and as a hint for compilers. Values marked as nonnull are assumed to
have non-null values.
However, developers may accidentally break the nonnull attribute as shown in the following code snippet:
__attribute__((returns_nonnull))
int *foo(__attribute__((nonnull)) int *x) {
x = 0; // This is compliant but might be surprising, use with caution
foo(0); // Noncompliant: null is passed as an argument marked as "nonnull"
return 0; // Noncompliant: return value is marked "nonnull" but null is returned
}
Failing to adhere to the attribute may introduce serious program errors. In particular, the compiler does not enforce that values marked as
nonnull are indeed non-null at runtime; it is the developers' responsibility to adhere to the attribute. These values are typically
not null-checked before use. Passing null (i.e., NULL, 0 or nullptr) as an argument to a parameter
that is marked as nonnull or returning null from a function marked as returns_nonnull is, hence, likely to cause a
null-pointer dereference. Compilers may even apply optimizations based on this attribute and might, for instance, remove an explicit
null-check if the parameter is declared as nonnull — even in code outside of the function with the attribute.
Note that the nonnull attribute is a GNU extension (see nonnull and returns_nonnull) which many
compiler vendors have implemented.
Note that it is allowed to assign null to a parameter marked as nonnull. This attribute is only concerned with the function
call contract and does not control the evolution of the parameter variable. For example, a linked-list search could be implemented as follows:
struct List {
int value;
List *next; // nullptr for a tail node.
};
List *findElement(List *l, int elem) __attribute__((nonnull(1)));
List *findElement(List *l, int elem) {
while(l && l->value != elem)
l = l->next;
return l;
}
What is the potential impact?
In case a program dereferences a null pointer, it’s behavior is undefined. For programs that exercise undefined behavior the compiler no longer
needs to adhere to the language standard and the program has no meaning assigned to it.
In practice, dereferencing a null pointer may lead to program crashes, or the application may appear to execute correctly while losing data or
producing incorrect results.
Besides affecting the application’s availability, null-pointer dereferences may lead to malicious code execution, in rare circumstances. If null is
equivalent to the 0x0 memory address that can be accessed by privileged code, writing, and reading memory is possible, which compromises the integrity
and confidentiality of the application.
Because compilers may apply optimizations based on the nonnull attribute, not respecting nonnull can also introduce more
complex bugs such as resource leaks or infinite recursion as indicated in the following code snippet:
struct List {
int value;
List *next; // nullptr for a tail node.
};
size_t len(List *n) __attribute__((nonnull));
size_t len(List *l) {
if (!l) return 0; // Impossible branch according to the attribute
return 1 + len(l->next);
}