If the heap is zero-initialized for security, then why is the stack merely uninitialized?
The storage returned by malloc() is not zero-initialized. Do not ever assume it is.
In your test program, it's just a fluke: I guess the malloc()
just got a fresh block off mmap()
, but don't rely on that, either.
For an example, if I run your program on my machine this way:
$ echo 'void __attribute__((constructor)) p(void){
void *b = malloc(4444); memset(b, 4, 4444); free(b);
}' | cc -include stdlib.h -include string.h -xc - -shared -o pollute.so
$ LD_PRELOAD=./pollute.so ./your_program
a at 0x7ffd40d3aa60: 1256994848 21891 1256994464 21891 1087613792 32765 0 0
b at 0x55834c75d010: 67372036 67372036 67372036 67372036 67372036 67372036 67372036 67372036
Your second example is simply exposing an artifact of the malloc
implementation in glibc; if you do that repeated malloc
/free
with a buffer larger than 8 bytes, you will clearly see that only the first 8 bytes are zeroed, as in the following sample code.
#include <stddef.h>
#include <stdlib.h>
#include <stdio.h>
const size_t n = 4;
const size_t m = 0x10;
int main()
{
for (size_t i = n; i; --i) {
int *const p = malloc(m*sizeof(int));
printf("%p ", p);
for (size_t j = 0; j < m; ++j) {
printf("%d:", p[j]);
++p[j];
printf("%d ", p[j]);
}
free(p);
printf("\n");
}
return 0;
}
Output:
0x55be12864010 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1 0:1
0x55be12864010 0:1 0:1 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2 1:2
0x55be12864010 0:1 0:1 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3 2:3
0x55be12864010 0:1 0:1 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4 3:4
Regardless of how the stack is initialised, you’re not seeing a pristine stack, because the C library does a number of things before calling main
, and they touch the stack.
With the GNU C library, on x86-64, execution starts at the _start entry point, which calls __libc_start_main
to set things up, and the latter ends up calling main
. But before calling main
, it calls a number of other functions, which causes various pieces of data to be written to the stack. The stack’s contents aren’t cleared in between function calls, so when you get into main
, your stack contains leftovers from the previous function calls.
This only explains the results you get from the stack, see the other answers regarding your general approach and assumptions.
In both cases, you get uninitialized memory, and you can't make any assumptions about its contents.
When the OS has to apportion a new page to your process (whether that's for its stack or for the arena used by malloc()
), it guarantees that it won't expose data from other processes; the usual way to ensure that is to fill it with zeros (but it's equally valid to overwrite with anything else, including even a page worth of /dev/urandom
- in fact some debugging malloc()
implementations write non-zero patterns, to catch mistaken assumptions such as yours).
If malloc()
can satisfy the request from memory already used and released by this process, its contents won't be cleared (in fact, the clearing is nothing to do with malloc()
and it can't be - it has to happen before the memory is mapped into your address space). You may get memory that has previously been written by your process/program (e.g. before main()
).
In your example program, you're seeing a malloc()
region that hasn't yet been written by this process (i.e. it's direct from a new page) and a stack that has been written to (by pre-main()
code in your program). If you examine more of the stack, you'll find it's zero-filled further down (in its direction of growth).
If you really want to understand what's happening at the OS level, I recommend that you bypass the C Library layer and interact using system calls such as brk()
and mmap()
instead.