Document Number: N2074
Submitter: Martin Sebor
Submission Date: September 18, 2016
Subject: Function declarations with [static] arrays

Summary

Consider the following program. Imagine the declarations in it were extracted from a larger code base where each of the declarations of foo is in a header file of its own so they are not visible in the same file as they are here.

      const int N = 2;

      void foo (int *A);              // #1
      void foo (int A []);            // #2
      void foo (int A [N]);           // #3
      void foo (int A [*]);           // #4
      void foo (int A [3]);           // #5
      void foo (int A [static 4]);    // #6

      int main (void)
      {
        int A [4];
        foo (A);
      }

      void foo (int A [static 8])     // #7
      {
        for (int i = 0; i != 8; ++i)
          A [i] = i;
      }
    

There is an almost glaring bug in the code in that the function foo defined on the line marked #7 to take as an argument an array of (at least eight) elements and to write into all those elements is called with an array of just four.

Writing beyond the end of an object is a serious defect with potential security consequences that engineers are anxious to avoid and compilers and code analysis tools are increasingly attempting to detect, diagnose, and help prevent. Yet even the most conforming of today's compilers known for the their ability to detect far more subtle bugs than the one in the example successfully compile the program above at the strictest diagnostic settings without issuing any messages (1). Needless to say, the defective program then crashes when run. (Crashing a program due to an out-of-bounds write is often viewed as a proof of concept when determining its exploitability.)

It's not terribly surprising that a buggy program crashes, or that compilers are unable to detect every bug. Other such examples abound. What is unexpected in this case is that the bug seems so blatant, involving constant integers just a few lines apart (two of foo's declarations clearly specify an array with fewer elements than its definition), and the caller provides fewer elements than specified by one of the declarations in scope at the point of the call, yet no compiler issues even a warning for the obvious mismatch.

What is the reason for the lack of a diagnostic? It turns out that the C standard is specified in a way that not only makes diagnosing these types of mismatches virtually impossible, it does so with no benefit to either implementers or C programmers. The basic underlying reason is that all seven declaratations of the function foo in the program are compatible with one another, despite their marked notational differences. But the problem is slightly more nuanced than that.

The C language rules dictate that an array argument in every function declaration is converted to a pointer to the type of its element. In our example this rule results in the type of each of the declarations being void (int*), or a function taking a pointer to int. In effect, all declarations of foo declare the same function.

This is old news for functions that take ordinary arrays such as those on the lines marked #2 or #5. The property of arrays to decay to pointers dates back to the inception of the language and is fundamental to it. Programmers have been trained to deal with it and have done so more or less successfully for decades. It makes sense for the declaration #1 because it doesn't declare an array. That all the function declarations are compatible with one another also makes sense so that when they are used to declarare distinct functions (with the same type) they can be used interchangeably. So what's the problem?

The problem is not so much that the declarations are compatible with one another but rather that they are all valid and required to be accepted at the same time in the same translation unit despite the presence of the those marked #3, #6, and #7, each of which specifies different and contradictory (or apparently contradictory) requirements on the caller.

Since N in the program is not a constant integer expression, according to the language rules void foo (int [N]) declares the function to take a variable length array with a runtime bound of (at least) N elements. But because N is a constant initialized to the value 2, it's clear that in reality this foo expects an array with at least two elements. void foo (int [static 4]), on the other hand, declares a foo that must be called with an array of at least four elements. Finally, the defintion of void foo (int [static 8]) requires eight elements. Having these three declartions not only in the same program but in the same file seems clearly wrong and should not be allowed to be accepted by implementations, let alone required to be.

What about #4 with the asterisk? This one is just a shorthand for "a variable array with some number of elements computed at runtime." It exists for declarations in which the computation of the number of expected elements cannot be expressed in the bound of the array as in #3, and is not allowed in defintions. For code analysis purposes in might as well be replaced with void foo (int [rand ()]). As a result, not only is this notation superfluous, it is detrimental to code inspection because its use prevents code enalyzers (tools and humans alike) from inferring anything useful about the argument beyond that it must be a valid non-null pointer.

The ability to declare a function that takes an array with a specific number of elements, either constant or computed at runtime, is new in C99. According to historical record it was apparently introduced for efficiency, not with the goal to help detect bugs. The C99 Rationale provides the following insight into the introduction of the feature into the standard.

It would be a significant advantage on some systems for the translator to initiate, at the beginning of the function, prefetches or loads of the arrays that will be referenced through the parameters. There is no way in C89 for the user to provide information to the translator about how many elements are guaranteed to be available.

The authors of this paper are not aware of any implementation that provides this optimization. However, we are aware of user requests desiring to exploit this feature to improve the ability of compilers to detect the kinds of coding mistakes discussed in this paper (2). Ironically, as has been shown, the C specification makes the feature ill-suited for this purpose.


1. The bug is detected by Address Sanitizer that comes with Clang and GCC.
2. See GCC bugs 50584 and 67793.

Related Defects

This isn't the first time that a problem with this feature has been brought to the attention of WG14. In C99 DR 205, Canada's C Working Group raises other concerns with the [static] syntax. The Committe Discussion of the defect indicates that the feature was considered to be poorly specified even at the time it was added to the standard in 1999.

This paper urges the committee to revisit the specification of the feature and make it better suited to detect the types of unnecessary and easily preventable yet serious defects discussed here.

Suggested Technical Corrigendum

The goal of the informal proposed corrigendum below is to help detect coding bugs like those in the motivating example of this paper without needlessly breaking correct and safe programs. When constraints or requirements on programs are tightened up the valid concern of breaking correct programs is raised. Although the proposal below (as well as any others like it) has such a potential in theory, implementers have long learned to deal with such changes by accepting programs that violate the new constraint in compatibility or relaxed modes and issuing helpful warnings (as opposed to rejecting such programs) in conformance modes. Thus we believe that imposing stricter constraints as proposed below is not only a reasonable but also a compatible change.

The following is an inormal proposal with no wording changes. If there is sufficient support for a change along these or similar lines within the rest of WG14 the authors will submit a formal proposal with words reflecting the desired intent.

To make [static] arrays in function declarations safer to use and programming errors involving their use easier to detect, we propose to add a constraint requiring programs to specify the same constant bound in all declarations of the same function. Notably, we propose to make it a diagnosable constraint violation for a translation unit with a declaration of a function using the [static N] form to contain another declaration of the function using any of the other forms. When N is a constant integer expression, the value of N must match in all declarations of the same function. (Since it may not be feasible to enforce the constraint across translation unit boundaries, the effect of such violations is undefined.)

In addition, for functions taking a variable length array, we propose to add a constraint requiring programs to specify the bound using the same (non-constant) expression in all declarations of the same function. (We intentionally stop short at this time of proposing how expression identity would be determined. It could be based on token comparison or on some other simple yet unabmiguous determination.)

Finally, we propose to banish the [*] form altogether. It is superfluous (it has the same meaning as any non-constant expression) and it only serves to hide an essential piece of data — the bound of the array argument — from tools and programmers alike.

Alternatively, if this proposal is not palatable, since the feature cannot be used safely and offers no known benefits, we suggest to deprecate and ultimately remove it from the standard.