I realized I never really thought of this.
If I made a large enough recursive call chain, wouldn't the stack eventually grow down enough that it will overlap with other things, like shared libraries (that are loaded right below it on linux), etc...
On usual modern operating systems, like Windows or Linux, stack space is reserved during thread creation. You can pass the value to CreateThread (a dedicated parameter) or pthread_create (part of the attr parameter) to ask for a particular size. If you don't, there's a default but it's still a specific size limit for stack growth.
On Windows, the memory is then allocated by individual pages, as these pages are used. As you touch next reserved page (below the current stack region), physical pages are allocated for those virtual pages unless you try to grow past the limit. Some of the pages in the last part serve as a guard to trigger stack overflow. The very last page is not allocated, it serves as the ultimate boundary of the stack.
When you trigger stack overflow once, the guard attribute in guard page is off, and you need to recover it by using _resetstkoflw() function, otherwise you can't handle stack overflow again, instead you'll have Access Violation on touching the last page.
Many programming language runtimes, including .Net, don't try to recover after the first stack overflow, instead considering the program state unrecoverable in this case. .Net doesn't even permit catching and handling StackOverflowException from wihin the same process.
I don't know how exactly this work in other operating systems.
(added by @PeterCordes):
For details on Linux's stack-growth mechanism (for the main thread only), see How is Stack memory allocated when using 'push' or 'sub' x86 instructions? - it's more permissive than Windows, allowing growth by more than 1 page at once. (Which makes stack-clash attacks possible in code that uses unsanitized user input as a size for alloca or equivalent, unlike on Windows or if you compile with hardening options.)
A stack-clash doesn't replace code, it points the stack pointer into .data or .bss (or something mmaped at a predictable address) to overwrite data. An OS would have to be insane to handle a page fault (e.g. on a non-writeable page) by replacing an existing mapping with more stack space.
Some exotic platforms handle this differently.
In DOS, in real mode with tiny memory model, where all segments are the same segment, you can indeed overwrite your variables with stack pushes, and even your code too.
There are controllers with hardware stack only for return addresses, not for parameters or local variables. It is fixed memory region of fixed size, not accessible by other means than as stack. It is circular: an overflow overwrites some of earlier added return address.
Yes, of course, and that's what we'd call stack overflow, when there's not enough room for more stack space.
But what if it grew upwards, how would that change things?
Well, the first question to ask is: growing upwards from where in the address space? There's no way to choose the best starting point from which to grow the stack upwards, because too high means not enough stack space, yet too low means not enough heap space.
These are serious problems for smaller address spaces, but with 64-bit address spaces and virtual memory, there's so much empty space we can accommodate many stacks growing downward (i.e. many threads) without problem.
alloca (in code that doesn't sanity-check sizes before using them for stack allocation) moving the stack pointer into other writeable data (like an important global variable). This is called a stack clash attack, and is a real thing; compilers can harden your code by probing each page even on OSes like Linux where that's not required for stack growth, so you'll hit the unmapped guard page.
ulimitset for stack which will be exhausted way before you overlap with other memory (the default seems to be 8MiB). Thread stacks are typically not even growing, they are allocated at thread creation.