Using alloca to allocate automatic memory

The glibc library provides an alternate to dynamic memory-allocation with malloc (and friends); the alloca(3) API.

alloca can be thought of as something of a convenience routine: it allocates memory on the stack (of the function it is called within). The showcase feature is that free is not required and, the memory is automatically deallocated once the function returns. In fact, free(3) must not be called. This makes sense: memory allocated on the stack is called automatic memory – it will be freed upon that function's return.

As usual, there are upsides and downsides – tradeoffs – to using  alloca(3):

Here are the alloca(3) pros:

  • No free is required; this can make programming, readability, and maintainability much simpler. Thus we can avoid the dangerous memory-leakage bug – a significant gain!
  • It is considered very fast, with zero internal fragmentation (wastage).
  • The primary reason to use it: sometimes, programmers use non-local exits, typically via the longjmp(3) and siglongjmp(3) APIs. If the programmer uses malloc(3) to allocate a memory region and then abruptly leaves the function via a non-local exit, a memory leak will occur. Using alloca will prevent this, and the code is easy to implement and understand.

And here are the alloca cons:

  • The primary downside of alloca is that there is no guarantee it returns failure when passed a value large enough to cause stack overflow; thus, if this actually does occur at runtime, the process is now in an undefined behavior (UB) state and will (eventually) crash. In other words, checking alloca for the NULL return, as you do with the malloc(3) family, is of no use!
  • Portability is not a given.
  • Often, alloca is implemented as an inline function; this prevents it from being overridden via a third-party library.

Take a look at the code as follows (ch4/alloca_try.c):

[...]
static void try_alloca(const char *csz, int do_the_memset)
{
size_t sz = atol(csz);
void *aptr;

aptr = alloca(sz);
if (!aptr)
FATAL("alloca(%zu) failed ", sz);
if (1 == do_the_memset)
memset(aptr, 'a', sz);

/* Must _not_ call free(), just return;
* the memory is auto-deallocated!
*/
}

int main(int argc, char **argv)
{
[...]
if (atoi(argv[2]) == 1)
try_alloca(argv[1], 1);
else if (atoi(argv[2]) == 0)
try_alloca(argv[1], 0);
else {
fprintf(stderr, "Usage: %s size-to-alloca do_the_memset[1|0] ",
argv[0]);
exit(EXIT_FAILURE);
}
exit (EXIT_SUCCESS);
}

Let's build it and try it out:

$ ./alloca_try
Usage: ./alloca_try size-to-alloca do_the_memset[1|0]
$ ./alloca_try 50000 1
$ ./alloca_try 50000 0
$

The first parameter to alloca_try is the amount of memory to allocate (in bytes), while the second parameter, if 1, has the memset process call on that memory region; if 0, it does not.

In the preceding code snippet, we tried it with an allocation request of 50,000 bytes  it succeeded for both the memset cases.

Now, we deliberately pass -1 as the first parameter, which will be treated as an unsigned quantity (thus becoming the enormous value of 0xffffffffffffffff on a 64-bit OS!), which of course should cause alloca(3) to fail. Amazingly, it does not report failure; at least it thinks it's okay:

$ ./alloca_try -1 0
$ echo $?
0
$ ./alloca_try -1 1
Segmentation fault (core dumped)
$

But then, doing memset (by passing the second parameter as 1) causes the bug to surface; without it, we'd never know.

To further verify this, try running the program under the control of the library call tracer software, ltrace; we pass 1 as the first parameter, forcing the process to invoke memset after alloca(3):

$ ltrace ./alloca_try -1 1
atoi(0x7ffcd6c3e0c9, 0x7ffcd6c3d868, 0x7ffcd6c3d888, 0) = 1
atol(0x7ffcd6c3e0c6, 1, 0, 0x1999999999999999) = -1
memset(0x7ffcd6c3d730, 'a', -1 <no return ...>
--- SIGSEGV (Segmentation fault) ---
+++ killed by SIGSEGV +++
$

Aha! We can see that following memset, the process receives the fatal signal and dies. But why doesn't the alloca(3) API show up in ltrace? Because it's an inlined function – ahem, one of its downsides.

But watch this; here, we pass 0 as the first parameter, bypassing the call to memset after alloca(3):

$ ltrace ./alloca_try -1 0
atoi(0x7fff9495b0c9, 0x7fff94959728, 0x7fff94959748, 0) = 0
atoi(0x7fff9495b0c9, 0x7fff9495b0c9, 0, 0x1999999999999999) = 0
atol(0x7fff9495b0c6, 0, 0, 0x1999999999999999) = -1
exit(0 <no return ...>
+++ exited (status 0) +++
$

It exits normally, as though there were no bug!

Further, you will recall  from Chapter 3Resource Limits, we saw that the default stack size for a process is 8 MB. We can test this fact via our alloca_try program:

$ ./alloca_try 8000000 1
$ ./alloca_try 8400000 1
Segmentation fault (core dumped)
$ ulimit -s
8192
$

The moment we go beyond 8 MB, alloca(3) allocates too much space, but does not trigger a crash; instead, memset(3) causes segfault to occur. Also, ulimit verifies that the stack resource limit is 8,192 KB, that is, 8 MB.

To conclude, a really, really key point: you can often end up writing software that seems to be correct but is, in fact, not. The only way to gain confidence with the software is to take the trouble to perform 100% code coverage and run test cases against them! It's hard to do, but quality matters. Just do it.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset