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; // local allocated within frame of foo
strcpy(dest, src); // new stack frame, so return ptr AFTER dest
// blah blah rest of code...
if (dest == '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 )
else return (first < second) ? -1 : 1;
void bar(char* message, int (*sorter)(const void*,const void*))
// do something with array
qsort(array, 10, sizeof( int ), sorter);
In this case, assuming a pair of classic downward-growing stacks in a dual-stacked architecture,
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.