The answer per the spec that you're going to hear from most people is something like this: The program crashes because you're invoking UB by writing to an uninitialized pointer. At this point, crashing is a valid behavior, so sometimes it crashes and sometimes it does something else which is also valid (because UB).
This is correct-ish, but it doesn't answer your question. Your question was, "Why doesn't it crash in all circumstances?" In your case, you only achieved a segfault when you changed the structure of your program to include a for
loop that seems to perform unrelated behavior. For this we need a basic introduction to program memory layout and the nature of segfaults, we'll start with segfaults.
Segmentation Faults and Virtual Memory
A segmentation fault is a somewhat complex beast under the hood if you're unfamiliar with CPU architecture. Its purpose is simple enough, if an executing process tries to access memory that it shouldn't, a segfault should be issued. The devil in the details being, what defines "memory the process shouldn't touch"? And how should the segfault be communicated to the operating system?
On modern operating systems and CPU architectures, a process' valid memory space is controlled using a virtual memory system. The operation of virtual memory is outside the scope of your question, but suffice to say both the operating system and the CPU itself are aware of what addresses your process can and cannot access. If your process strays outside the bounds of its allowed memory space, a segfault will be issued.
To "issue" a segfault the CPU will synchronously interrupt your program, and alert the operating system you've done a naughty thing. These are also called "exceptions" or "traps", but they're all just different nomenclature for "your program asked the CPU to do something that it can't or won't do". The operating system handles the interrupt, and then issues the signal (*Nix) or exception (Win32) to your program. If your program hasn't set up a handler for that signal/exception, the OS gracefully crashes you.
An interesting oolie about virtual memory is that it is generally only issued in packages of 2^12 continuous bytes (4KiB). So even if your process only wants, say, 10 bytes it's going to get handed at least 4KiB. This continuous grouping of bytes is called a "page" because it groups "lines" of memory.
Program Memory and the Stack
Even if your process never asks for memory using malloc
or its ilk, its going to get handed a couple pages in order to implement what's called the stack (which lends its name to certain websites). This is where your locally declared variables like src
, dest
, ret
, and s
live. It's also used to spill non-volatile CPU registers when moving between function calls, but that is also outside the scope.
So, if dest
is just a piece of memory on the stack, and is never initialized in your program, what's it pointing to? Well, whatever random data happens to exist at that memory address is now your pointer. Your program's operation is now at the whim of garbage bytes from the stack page.
Conclusion
If the garbage in the stack space happens to point somewhere inside one of the memory pages that was issued to your process for stack space, your process won't access invalid memory and will keep on chugging (or it points somewhere nearby, Linux can automatically grow the stack if you're within one page of the last valid page). However, if it points anywhere else, you cause an invalid memory access and the CPU alerts the relevant authorities. Your process is a criminal and will be treated accordingly.
"But nickelpro," you intercede, "what does any of that have to do with the for
loop?" Nothing, the for
loop is a red herring. In this case it happens to be biasing the stack allocation into a place where the garbage happens to cause a segfault. That could be related to many things, possibly as a consequence of ASLR or just random happenstance. Someone who knows more than me about virtual memory implementations could shine a light on this.
Errata
Now your program's structure also has a (I think) unintended bug in it which is exasperating the problem. You perform the initial string copy with:
ret = strncpy(dest, src, 5);
Which does not null-terminate the destination string, which means when you call:
size_t s = strlen(ret);
strlen
is going to keep reading until it hits a null byte. So even if dest
happened to point somewhere valid, bad luck with the memory garbage will cause strlen
to read its way into invalid memory.