Pitfalls of VLA in C

It generates much more code, and much slower code (and more fragile code), than just using a fixed key size would have done ~ Linus Torvalds

VLA (variable-length array, an array – array, not just block of memory acting like one – that has size determined during runtime instead of at compile time) is a feature introduced to C with the revision C99 of the standard. A very useful feature one may think, and indeed… in some cases… But since the world we live in is less than ideal, one needs to know well what are the pitfalls of using VLA in their code before doing so.

If you want to know the few cases when VLA may actually be useful you can check my other blogpost.

A fair share of the text here will focus on problems caused by automatic VLA, thus to further reflect on that an abbreviation aVLA will be used when refferng to those cases.

Allocation on stack #

Let's address the elephant in the room: the standard doesn't say anything about it, but in practice aVLA usually are allocated on stack. This is the source of the most of the problems, the source of discontent among programmers, the reason why even allowing any VLA into the codebase is usually a code smell.

Let's consider a painfully simple, very favourable to aVLA, example:

#include <stdio.h>

int main(void) {
    int n;
    scanf("%d", &n);
    char arr[n];
    printf("%d", arr[0]);
    return 0;
}

As we can see, it takes a number from user then makes array of that size. Compile and try it. Check how big values you can input before getting segfault caused by stack overflow. In my case, it was around 8 MiB. How much is that? One raw image? a MP3 or two? few seconds of video? And the program wasn't doing anything meaningful - what if it wasn't just main()? Maybe a recursive function? The limit shrinks tremendously.

And you don't have any (portable, standard) way to react after a stack overflow - the program already crashed, you lost control. So you either need to make elaborate checks before declaring an array or betting that user won't input too large values (the outcome of such gamble ought to be obvious).

So the programmer must ensure that aVLA size doesn't exceed some safe maximum, but in reality, if you know safe maximum, there is rarely any reason for not using it always.

Worst of it is… #

… that segfault is actually one of the best outcomes of improperly handled aVLA. The worst case is an exploitable vulnerability, where attacker may choose a value that causes an array to overlap with other allocations, giving them control over those values as well. A security nightmare.

So how to fix this example? #

What if I need to let user define size and creating ridiculously large fixed array would be too wasteful? It's simple: use malloc()!

#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int n;
    scanf("%d", &n);
    char* arr = malloc(n * (sizeof *arr));
    printf("%d", arr[0]);
    free(arr);
    return 0;
}

In this case I was able to request over 4.5 GB before segfault. Almost few orders of magnitude more! But I still got the segfault, right? Well, the difference is in getting at least some* chance of checking the value returned by malloc() and thus being able to, for example, inform the user about the error:

    char* arr = malloc(n * (sizeof *arr));
    if (arr == NULL) {
        perror("malloc()"); // output: "malloc(): Cannot allocate memory"
    }

"but I cannot use malloc()!" #

I've encountered a counterargument, that as C is often used as a systems/embedded language, there are situations where using malloc() may not even be possible.

I'm basically going to repeat myself here, but it is really important:

  1. Such device rather is not going to have a lot of stack either. So instead of allocating dynamically, you (probably) should determine how much you need and just always use that fixed amount.

  2. When using aVLA on system with small amounts of stack, it's really easy to make something which seems to work, but which blows your stack if your function gets called from a deep call stack combined with the large amount of data.

  3. If you always allocate fixed amounts of stack space everywhere, and you test it, you know you're good. If you dynamically allocate on stack, you have to test all your code paths with all the largest sizes of allocated space, which is much harder and much easier to make a mistake. Don't make it even easier to shoot yourself in the foot for no real advantage.

Creation by accident #

Unlike most other dangerous C functionality, aVLA doesn't have the barrier of being not known. Many newbies learn to use them via trial and error, but don't learn about the pitfalls.
The following is a simple mistake I observed even experienced developers making (especially those with C++ background); it will silently create an aVLA when it's clearly not necessary:

const int n = 10;
int A[n];

Thankfully, any half-decent compiler would notice and optimize aVLA away, but… what if it doesn't notice? Or what if, for some reason (safety?), the optimizations were not turned on? But it surely isn't so much worse, right? Well…

Way slower than fixed size #

Without compiler optimizations a function with aVLA from previous example will result in 7 times more Assembly instructions than its fixed size counterpart before moving past the array definition (look at the body before jmp .L2). But it's without optimizations, with them the produced Assembly is exactly the same.

So an example where aVLA is not used by mistake:

#include <stdio.h>
void bar(int*, int);

void foo(int n) {

#if VLA
    int A[n];
#else
    int A[1000];  // Let's make it bigger than 10! (or there won't be what to examine)
#endif

    for (int i = n; i--;) {
        scanf("%d", &A[i]);
    }
    bar(A, n);
}

int main(void) {
    foo(10);
    return 0;
}

For our educational purposes in this example, -O1 level of optimisation will work best (as Assembly will be clearer and -O2 won't help aVLA's case here really much).

When we compile aVLA version, before instructions corresponding to for loop, we get:

push    rbp
mov     rbp, rsp
push    r14
push    r13
push    r12
push    rbx
mov     r13d, edi
movsx   r12, edi       ; here aVLA "starts"...
sal     r12, 2         ;
lea     rax, [r12+15]  ;
and     rax, -16       ;
sub     rsp, rax       ;
mov     r14, rsp       ; ... and there "ends"

The aVLA-free version on the other hand generates:

push    r12
push    rbp
push    rbx
sub     rsp, 4000      ; this is caused by array definition
mov     r12d, edi

So not only fixed array spawns less code, but also way simpler code. Why, aVLA even causes more overhead at the beginning of the function. It's not so much more in the grand scheme of things, but it still isn't just a pointer bump.

But are those differences significant enough to care? Yes, they are.

No initialization #

To add more to the issue with inadvertent aVLA, the following isn't allowed:

int n = 10;
int A[n] = { 0 };

Even with optimizations, initialisation isn't allowed for aVLA. So despite wanting fixed size array and compiler being technically able to provide one, it won't work (and if it does… it's breaking the specification…).

sizeof not resolved at compile time #

int n;
scanf("%d", &n);
int A[5];
int B[n];

sizeof A;  // compile-time
sizeof B;  // run-time

The above is quite obvious, B isn't fully known until n gets its value, thus it needs to be resolved at run-time. Nothing to write home about.

On the other hand, the following is no longer so obvious (source of example):

The operand [of sizeof operator] must be evaluated if its type is a VLA; for example:

int i = 0;
static const int n = 5;

int A[5];
int B[n];
sizeof A[i++];   // operand is not evaluated, `i` is still 0
sizeof B[i++];   // operand is evaluated, `i` is 1
sizeof &B[i++];  // operand is not evaluated, `i` stays 1

Mess for compiler writers #

Few months ago I saved a comment on Reddit listing problems encountered with VLA from compiler writer perspective. I'll allow myself to cite the listed issues:

And believe me, if you take a stroll around some C forums (or the meeting of standard committee [sic!]) you will see even more different complaints.

Reduced portability #

Due to all previously presented problems, some compiler providers decided to not fully support C99. The primary example is Microsoft with its MSVC. The C Standard Committee also noticed the problem and with C11 revision all instances of VLAs were made optional; C23 partially reverts that decision mandating VM types (aVLA are still optional; there is even a slight sentiment towards deprecating them entirely, but removing something from the, nomen omen, standard is way harder than putting it in).

That means code using a VLA won't necessarily be compiled by a C11 compiler, so you need, assuming you target for portability, to check whether it is supported with __STDC_NO_VLA__ macro and make version without (a)VLA as fallback. Wait… if you need to implement VLA-free version either way then what's the point of doubling the code and creating VLA in the first place?!

(nitpick) Breaking conventions #

This one is more of a nitpick, but still another reason to dislike VLA. There is a widely used convention of first passing object then its parameters, what in terms of arrays means:

void foo(int** arr, int n, int m) { /* arr[i][j] = ... */ }

C99 specified that array sizes need to be parsed immediately when encountered within a function definition's parameter list, what means that when using VLA you cannot do an equivalent of the above:

void foo(int arr[n][m], int n, int m) { /* arr[i][j] = ... */ } // INVALID!

You need to break up with the convention and write:

void foo(int n, int m, int arr[n][m]) { /* arr[i][j] = ... */ }

Alternatively, you could use the obsolete syntax (obsolescent even in ANSI C; finally removed in C23), but that would be pointless, as compilers don't make parameters checks in such case, so any benefits from using VLA would be lost.

void foo(int[*][*], int, int);
void foo(arr, n, n)
    int n;
    int m;
    int arr[n][m]
{
    // arr[i][j] = ...
}

Conclusion #

In short, refrain from using VLA and avoid automatic VLA like devil avoids holy water; if your compiler has it, rather compile with -Wvla flag or similar (and definitely with -Wvla-larger-than=0 - this allows VM types, while warning about aVLA).

If you find yourself in one of the situations where VLA (or VM type) is a valid/good solution, of course, do use them, but keep in mind the limits I've outlined here.