The main problem with having the stack grow up is that it either has to sit at the start of memory and have a forced upper size limit, or sit at the top of memory and have a forced location. Both of these situations limit maximum memory utilisation. If both the stack and heap grow towards each other from opposite ends, you only run out of memory when there is literally none left. In other configurations you may run out of stack or heap despite having free memory.
The "backwards stack" idea has been thrown around for quite a while now, and is generally not seen as a solid solution. For a start, you're going to run into problems if you're altering stack buffers that are in previous frames. This initially doesn't seem like it would be common, but consider that you allocate buffers for many functions before you pass them:
void foo(char* src)
{
char dest[20]; // local allocated within frame of foo
strcpy(dest, src); // new stack frame, so return ptr AFTER dest
// blah blah rest of code...
if (dest[0] == 'X')
printf("First char is X\n");
}
Notice that the stack frame for strcpy
will sit numerically above the stack frame for foo
. In a typical system, strcpy
would write past the buffer, over the return pointer of foo
, leading to control of the instruction pointer after foo
returns. With a backwards stack, strcpy
would write up, through the end of foo
's stack frame, into the bottom of strcpy
's stack frame, leading to an overwrite of the return address of strcpy
, and again giving control of the instruction pointer.
A solution that is considered by many to be quite secure is to use an architecture that has one stack for locals and parameters (the data stack), and another for stack frames and return pointers (the control stack). This isolation helps ensure that return pointers cannot be casually overwritten when a local buffer is overflowed. It again has the issues with memory management, though only one stack has to have a fixed maximum size - the other can grow into the heap.
Of course, this architecture also isn't completely robust from a security standpoint. Consider the following:
int int_sorter( const void *val_a, const void *val_b )
{
// this code isn't important here
int first = *(int*)val_a;
int second = *(int*)val_b;
if ( first == second )
return 0;
else return (first < second) ? -1 : 1;
}
void bar(char* message, int (*sorter)(const void*,const void*))
{
int array[10];
char dest[32];
// do something with array
// ...
strcpy(dest, message);
qsort(array, 10, sizeof( int ), sorter);
}
In this case, assuming a pair of classic downward-growing stacks in a dual-stacked architecture, message
overflows dest
on the data stack, copying over the sorter
function pointer that was pushed as the second parameter to bar
. The call to strcpy
returns as normal (the control stack is intact) but then the call to qsort
contains an instruction to jump to the sorter
function, again leading to control over the instruction pointer.
At the end of the day, you're not going to find a total solution to the problem. The best you can do is use good coding practices (or teach your devs to do so), and enable protections such as ASLR (a.k.a. PIE), DEP/NX, stack canaries, etc.