March 24, 2020·5 min read·engineeringcclangdiagnosticspointers

Clang 10 makes invalid pointer arithmetic harder to overlook

Clang 10 lets optimization use more pointer-arithmetic guarantees and extends UBSan to diagnose the corresponding errors. New warnings also expose misleading C source.

A null pointer check can appear to protect an operation while occurring after the program has already crossed a language boundary. Clang 10 brings this issue into focus by allowing LLVM's optimizer to use guarantees about pointer arithmetic that existing code may violate. The release notes pair that change with an extension to the pointer-overflow sanitizer. The LLVM 10 release therefore gives C programmers a reason to examine how pointer values are formed, before considering where the program dereferences them.

Modern optimizers make C's relationship with machine instructions less direct than its small syntax suggests. I regard staying within defined behavior as part of writing the language, because a plausible sequence of machine operations does not establish what a C expression means. This release supplies a concrete example of the difference.

Forming the pointer is an operation

The release notes explain that C pointer arithmetic does not permit adding even a zero offset to a null pointer. They also identify forming a null pointer by subtracting an integer from a non-null pointer as undefined. The optimizer can now use those guarantees, and -fsanitize=pointer-overflow diagnoses these cases.

That matters to an interface which accepts a pointer and a count. The programmer might intend a null pointer with a zero count to describe an empty input. Such an interface can be coherent, but its implementation needs to respect that convention before evaluating arithmetic on the pointer. A count of zero does not make every possible expression involving the pointer harmless.

The following example keeps the empty-input decision before pointer arithmetic. It assumes that a nonempty input points to an array containing at least count elements.

#include <stddef.h>

int sum_values(const int *values, size_t count)
{
    if (count == 0)
        return 0;

    int total = 0;
    for (size_t i = 0; i < count; ++i)
        total += values[i];

    return total;
}

Each intermediate sum must also remain representable as an int. That is a separate precondition on this deliberately small example. Avoiding one undefined operation does not establish that an entire function is correct.

Compare the interface decision with an implementation that first computes an end pointer from values + count. If values is null, a later empty-input check cannot change the earlier evaluation. An early return prevents that evaluation for the empty case and leaves the nonempty case with its stated array precondition. The order of expressions is part of the function's meaning. Describing an input as empty is an application convention, while evaluating pointer arithmetic is an operation governed by C.

Why an optimizer can change the symptom

The November C working draft, in its additive-operator rules, ties pointer arithmetic to array objects. That provides the language context for the release-note warning. It does not describe arbitrary integer arithmetic over an address space.

A useful interpretation is that the compiler reasons about the states a defined execution can reach. If a program's explanation of an operation depends on a state excluded by the language rules, the explanation and the optimizer's reasoning have different premises. Seeing expected behavior in one generated binary does not reconcile those premises.

This also explains why the visible failure can occur away from the expression that introduced the problem. Later code may rely on a condition that the earlier operation makes impossible in a defined execution. Looking only at the place where the output first differs can therefore obscure the cause.

For the illustrative empty-input interface, the repair is structural. Decide whether there is input before deriving addresses that require an array. This expresses the intended exceptional case in control flow that does not need the invalid expression. It also gives a reader a direct statement of the interface's empty-input behavior.

I prefer this explanation to treating optimization levels as definitions of the language. Compiler options affect what transformations occur, but an application still needs a coherent account of its own preconditions. A sanitizer report can identify an evaluated operation that violates those preconditions.

Diagnostics describe different kinds of evidence

The pointer sanitizer observes instrumented executions. A warning based on source structure asks a different question. Clang 10 also adds -Wmisleading-indentation, which reports statements whose indentation suggests membership in a control statement that their syntax does not establish.

This example illustrates the discrepancy.

if (ready)
    send_header();
    send_payload();

The indentation places both calls at the same apparent nesting level. The grammar associates only the first statement with the condition. A reader who interprets the indentation as the control structure can reach a different conclusion from the compiler.

Adding braces is useful when both calls belong under the condition. Correcting the indentation is appropriate when the second call is intentionally unconditional. The warning cannot choose the program's intended behavior. Its value is identifying a disagreement between two descriptions of that behavior.

These diagnostics complement each other because they investigate different evidence. The pointer sanitizer can report an invalid operation reached by a particular execution. An indentation warning can identify a misleading source arrangement without running that path. Neither result should be interpreted as a complete account of every possible defect in the function.

The interface determines the useful fix

Both examples involve an implicit assumption becoming visible. The empty-input function assumes a relationship between a count and a pointer. The indented calls imply a relationship between a condition and an operation. In each case, a useful repair makes that relationship explicit in the code that implements it.

This is particularly relevant to C interfaces because callers often supply separate values whose relationship matters more than either value alone. A pointer type cannot express how many objects the caller provides. A count cannot establish that the pointer describes those objects. The function's contract has to explain the combination, and its implementation has to honor any empty case it accepts.

Clang 10's pointer change makes that contract relevant even before a load or store. Code that derives an address has already performed an operation with language-level requirements. The new sanitizer coverage provides a way to observe the invalid cases during execution, while source warnings can expose control flow that conceals the intended ordering of those operations.