I would like to provoke a stack underflow in a C function to test security measures in my system. I could do this using inline assembler. But C would be more portable. However I can not think of a way to provoke a stack underflow using C since stack memory is safely handled by the language in that regard.
So, is there a way to provoke a stack underflow using C (without using inline assembler)?
As stated in the comments: Stack underflow means having the stack pointer to point to an address below the beginning of the stack ("below" for architectures where the stack grows from low to high).
There's a good reason why it's hard to provoke a stack underflow in C.The reason is that standards compliant C does not have a stack.
Have a read of the C11 standard, you'll find out that it talks about scopes but it does not talk about stacks. The reason for this is that the standard tries, as far as possible, to avoid forcing any design decisions on implementations. You may be able to find a way to cause stack underflow in pure C for a particular implementation but it will rely on undefined behaviour or implementation specific extensions and won't be portable.
You can't do this in C, simply because C leaves stack handling to the implementation (compiler). Similarly, you cannot write a bug in C where you push something on the stack but forget to pop it, or vice versa.
Therefore, it is impossible to produce a "stack underflow" in pure C. You cannot pop from the stack in C, nor can you set the stack pointer from C. The concept of a stack is something on an even lower level than the C language. In order to directly access and control the stack pointer, you must write assembler.
What you can do in C is to purposely write out of bounds of the stack. Suppose we know that the stack starts at 0x1000 and grows upwards. Then we can do this:
volatile uint8_t* const STACK_BEGIN = (volatile uint8_t*)0x1000;
for(volatile uint8_t* p = STACK_BEGIN; p<STACK_BEGIN+n; p++)
{
*p = garbage; // write outside the stack area, at whatever memory comes next
}
Why you would need to test this in a pure C program that doesn't use assembler, I have no idea.
In case someone incorrectly got the idea that the above code invokes undefined behavior, this is what the C standard actually says, normative text C11 6.5.3.2/4 (emphasis mine):
The unary * operator denotes indirection. If the operand points to a function, the result is
a function designator; if it points to an object, the result is an lvalue designating the
object. If the operand has type ‘‘pointer to type’’, the result has type ‘‘type’’. If an
invalid value has been assigned to the pointer, the behavior of the unary * operator is
undefined 102)
The question is then what's the definition of an "invalid value", as this is no formal term defined by the standard. Foot note 102 (informative, not normative) provides some examples:
Among the invalid values for dereferencing a pointer by the unary * operator are a null pointer, an
address inappropriately aligned for the type of object pointed to, and the address of an object after the
end of its lifetime.
In the above example we are clearly not dealing with a null pointer, nor with an object that has passed the end of its lifetime. The code may indeed cause a misaligned access - whether this is an issue or not is determined by the implementation, not by the C standard.
And the final case of "invalid value" would be an address that is not supported by the specific system. This is obviously not something that the C standard mentions, because memory layouts of specific systems are not coverted by the C standard.
It is not possible to provoke stack underflow in C. In order to provoke underflow the generated code should have more pop instructions than push instructions, and this would mean the compiler/interpreter is not sound.
In the 1980s there were implementations of C that ran C by interpretation, not by compilation. Really some of them used dynamic vectors instead of the stack provided by the architecture.
stack memory is safely handled by by the language
Stack memory is not handled by the language, but by the implementation. It is possible to run C code and not to use stack at all.
Neither the ISO 9899 nor K&R specifies anything about the existence of a stack in the language.
It is possible to make tricks and smash the stack, but it will not work on any implementation, only on some implementations. The return address is kept on the stack and you have write-permissions to modify it, but this is neither underflow nor portable.
Regarding already existing answers: I don't think that talking about undefined behaviour in the context of exploitation mitigation techniques is appropriate.
Clearly, if an implementation provides a mitigation against stack underflows, a stack is provided. In practice, void foo(void) { char crap[100]; ... } will end up having the array on the stack.
A note prompted by comments to this answer: undefined behaviour is a thing and in principle any code exercising it can end up being compiled to absolutely anything, including something not resembling the original code in the slightest. However, the subject of exploit mitigation techniques is closely tied to the target environment and what happens in practice. In practice, the code below should "work" just fine. When dealing with this kind of stuff you always have to verify generated assembly to be sure.
Which brings me to what in practice will give an underflow (volatile added to prevent the compiler from optimising it away):
static void
underflow(void)
{
volatile char crap[8];
int i;
for (i = 0; i != -256; i--)
crap[i] = 'A';
}
int
main(void)
{
underflow();
}
Valgrind nicely reports the problem.
By definition, a stack underflow is a type of undefined behaviour, and thus any code which triggers such a condition must be UB. Therefore, you can't reliably cause a stack underflow.
That said, the following abuse of variable-length arrays (VLAs) will cause a controllable stack underflow in many environments (tested with x86, x86-64, ARM and AArch64 with Clang and GCC), actually setting the stack pointer to point above its initial value:
#include <stdint.h>
#include <stdio.h>
#include <string.h>
int main(int argc, char **argv) {
uintptr_t size = -((argc+1) * 0x10000);
char oops[size];
strcpy(oops, argv[0]);
printf("oops: %s\n", oops);
}
This allocates a VLA with a "negative" (very very large) size, which will wrap the stack pointer around and result in the stack pointer moving upwards. argc and argv are used to prevent optimizations from taking out the array. Assuming that the stack grows down (default on the listed architectures), this will be a stack underflow.
strcpy will either trigger a write to an underflowed address when the call is made, or when the string is written if strcpy is inlined. The final printf should not be reachable.
Of course, this all assumes a compiler which doesn't just make the VLA some kind of temporary heap allocation - which a compiler is completely free to do. You should check the generated assembly to verify that the above code does what you actually expect it to do. For example, on ARM (gcc -O):
8428: e92d4800 push {fp, lr}
842c: e28db004 add fp, sp, #4, 0
8430: e1e00000 mvn r0, r0 ; -argc
8434: e1a0300d mov r3, sp
8438: e0433800 sub r3, r3, r0, lsl #16 ; r3 = sp - (-argc) * 0x10000
843c: e1a0d003 mov sp, r3 ; sp = r3
8440: e1a0000d mov r0, sp
8444: e5911004 ldr r1, [r1]
8448: ebffffc6 bl 8368 <strcpy#plt> ; strcpy(sp, argv[0])
This assumption:
C would be more portable
is not true. C doesn't tell anything about a stack and how it is used by the implementation. On your typical x86 platform, the following (horribly invalid) code would access the stack outside of the valid stack frame (until it is stopped by the OS), but it would not actually "pop" from it:
#include <stdarg.h>
#include <stdio.h>
int underflow(int dummy, ...)
{
va_list ap;
va_start(ap, dummy);
int sum = 0;
for(;;)
{
int x = va_arg(ap, int);
fprintf(stderr, "%d\n", x);
sum += x;
}
return sum;
}
int main(void)
{
return underflow(42);
}
So, depending on what exactly you mean with "stack underflow", this code does what you want on some platform. But as from a C point of view, this just exposes undefined behavior, I wouldn't suggest to use it. It's not "portable" at all.
Is it possible to do it reliably in standard compliant C? No
Is it possible to do it on at least one practical C compiler without resorting to inline assembler? Yes
void * foo(char * a) {
return __builtin_return_address(0);
}
void * bar(void) {
char a[100000];
return foo(a);
}
typedef void (*baz)(void);
int main() {
void * a = bar();
((baz)a)();
}
Build that on gcc with "-O2 -fomit-frame-pointer -fno-inline"
https://godbolt.org/g/GSErDA
Basically the flow in this program goes as follows
main calls bar.
bar allocates a bunch of space on the stack (thanks to the big array),
bar calls foo.
foo takes a copy of the return address (using a gcc extension). This address points into the middle of bar, between the "allocation" and the "cleanup".
foo returns the address to bar.
bar cleans up it's stack allocation.
bar returns the return address captured by foo to main.
main calls the return address, jumping into the middle of bar.
the stack cleanup code from bar runs, but bar doesn't currently have a stack frame (because we jumped into the middle of it). So the stack cleanup code underflows the stack.
We need -fno-inline to stop the optimiser inlining stuff and breaking our carefully laid-down strcture. We also need the compiler to free the space on the stack by calculation rather than by use of a frame pointer, -fomit-frame-pointer is the default on most gcc builds nowadays but it doesn't hurt to specify it explicitly.
I belive this tehcnique should work for gcc on pretty much any CPU architecture.
There is a way to underflow the stack, but it is very complicated. The only way that I can think of is define a pointer to the bottom element then decrement its address value. I.e. *(ptr)--. My parentheses may be off, but you want to decrement the value of the pointer, then dereference the pointer.
Generally the OS is just going to see the error and crash. I am not sure what you are testing. I hope this helps. C allows you to do bad things, but it tries to look after the programmer. Most ways to get around this protection is through manipulation of pointers.
Do you mean stack overflow? Putting more things into the stack than the stack can accomodate? If so, recursion is the easiest way to accomplish that.
void foo();
{foo();};
If you mean attempting to remove things from an empty stack, then please post your question to the stackunderflow web site, and let me know where you've found that! :-)
So there are older library functions in C which are not protected. strcpy is a good example of this. It copies one string to another until it reaches a null terminator. One funny thing to do is pass a program that uses this a string with the null terminator removed. It will run amuck until it reaches a null terminator somewhere. Or have a string copy to itself. So back to what I was saying before is C supports pointers to just about anything. You can make a pointer to an element in the stack at the last element. Then you can use the pointer iterator built into C to decrement the value of the address, change the address value to a location preceding the last element in the stack. Then pass that element to the pop. Now if you are doing this to the Operating system process stack that would get very dependent on the compiler and operating system implementation. In most cases a function pointer to the main and a decrement should work to underflow the stack. I have not tried this in C. I have only done this in Assembly Language, great care has to be taken in working like this. Most operating systems have gotten good at stopping this since it was for a long time an attack vector.
Related
I'm trying to figure out how alloca() actually works on a memory level. From the linux man page:
The alloca() function allocates size bytes of space in the stack
frame of the caller. This temporary space is automatically freed
when the function that called alloca() returns to its caller.
Does this mean alloca() will forward the stack pointer by n bytes? Or where exactly is the newly created memory allocated?
And isn't this exactly the same as variable length arrays?
I know the implementation details are probably left to the OS and stuff. But I want to know how in general this is accomplished.
Yes, alloca is functionally equivalent to a local variable length array, i.e. this:
int arr[n];
and this:
int *arr = alloca(n * sizeof(int));
both allocate space for n elements of type int on the stack. The only differences between arr in each case is that 1) one is an actual array and the other is a pointer to the first element of an array, and 2) the array's lifetime ends with its enclosing scope, while the alloca memory's lifetime ends when the function returns. In both cases the array resides on the stack.
As an example, given the following code:
#include <stdio.h>
#include <alloca.h>
void foo(int n)
{
int a[n];
int *b=alloca(n*sizeof(int));
int c[n];
printf("&a=%p, b=%p, &c=%p\n", (void *)a, (void *)b, (void *)c);
}
int main()
{
foo(5);
return 0;
}
When I run this I get:
&a=0x7ffc03af4370, b=0x7ffc03af4340, &c=0x7ffc03af4320
Which shows that the the memory returned from alloca sits between the memory for the two VLAs.
VLAs first appeared in the C standard in C99, but alloca was around well before that. The Linux man page states:
CONFORMING TO
This function is not in POSIX.1-2001.
There is evidence that the alloca() function appeared in 32V, PWB,
PWB.2, 3BSD, and 4BSD. There is a man page for it in 4.3BSD. Linux
uses the GNU version.
BSD 3 dates back to the late 70's, so alloca was an early nonstandardized attempt at VLAs before they were added to the standard.
Today, unless you're using a compiler that doesn't support VLAs (such as MSVC), there's really no reason to use this function since VLAs are now a standardized way to get the same functionality.
The other answer precisely describes mechanics of VLAs and alloca().
However, there is significant functional difference between alloca() and automatic VLA. The lifetime of the objects.
In case of alloca() the lifetime ends when the function returns.
For VLAs the object is released when the containing block ends.
char *a;
int n = 10;
{
char A[n];
a = A;
}
// a is no longer valid
{
a = alloca(n);
}
// is still valid
As result, it is possible to easily exhaust the stack in the loop while it is not possible to do it with VLAs.
for (...) {
char *x = alloca(1000);
// x is leaking with each iteration consuming stack
}
vs
for (...) {
int n = 1000;
char x[n];
// x is released
}
Although alloca looks like a function from a syntax point of view, it can't be implemented as a normal function in a modern programming environment*. It must be regarded as a compiler feature with a function-like interface.
Traditionally C compilers maintained two pointer registers, a "stack pointer" and a "frame pointer" (or base pointer). The stack pointer delimits the current extent of the stack. The frame pointer saved the value of the stack pointer on entry to the function and is used to access local variables and to restore the stack pointer on function exit.
Nowadays most compilers do not use a frame pointer by default in normal functions. Modern debug/exception information formats have rendered it unnessacery, but they still understand what it is and can use it where needed.
In particular for functions with alloca or variable length arrays using a frame pointer allows the function to keep track of the location of it's stack frame while dynamically modifying the stack pointer to accomodate the variable length array.
For example I built the following code at O1 for arm
#include <alloca.h>
int bar(void * baz);
void foo(int a) {
bar(alloca(a));
}
and got (comments mine)
foo(int):
push {fp, lr} # save existing link register and frame pointer
add fp, sp, #4 # establish frame pointer for this function
add r0, r0, #7 # add 7 to a ...
bic r0, r0, #7 # ... and clear the bottom 3 bits, thus rounding a up to the next multiple of 8 for stack alignment
sub sp, sp, r0 # allocate the space on the stack
mov r0, sp # make r0 point to the newly allocated space
bl bar # call bar with the allocated space
sub sp, fp, #4 # restore stack pointer and frame pointer
pop {fp, pc} # restore frame pointer to value at function entry and return.
And yes alloca and variable length arrays are very similar (though as another answer points out not exactly the same). alloca seems to be the older of the two constructoins.
* With a sufficiently dumb/predictable compiler it is posible to implement alloca as a function in assembler. Specifically the compiler needs to.
Consistently create a frame pointer for all functions.
Consistently use the frame pointer rather than the stack pointer to reference local varaibles.
Consistently use the stack pointer rather than the frame pointer when setting up parameters for calls to functions.
This is apparently how it was first implemented ( https://www.tuhs.org/cgi-bin/utree.pl?file=32V/usr/src/libc/sys/alloca.s ).
I guess it's possible one could also have the actual implementation as an assembler function, but have a special case in the compiler that made it go into dumb/predictable mode when it saw alloca, I don't know if any compiler vendors did that.
The most important difference between alloca and VLAs is the failure case. The following code:
int f(int n) {
int array[n];
return array == 0;
}
int g(int n) {
int *array = alloca(n);
return array == 0;
}
The VLA has no possibility of detecting an allocation failure; which is a very un-C thing to impose on a language construct. Alloca() is thus much better designed.
Consider the following function.
void incr(_Atomic int *restrict ptr) {
*ptr += 1;
}
I'll consider x86, but my question is about the language, not the semantics of any particular implementation of atomics. GCC and Clang both emit the following:
incr:
lock add DWORD PTR [rdi], 1
ret
Is it possible for a conforming implementation to emit simply
incr:
add DWORD PTR [rdi], 1
ret
(The same thing you get if you remove _Atomic.)
Without restrict, this would be a miscompilation because add is not atomic, so (for instance) calling incr at the same time in two threads would race. However, since the pointer is restrict-qualified, I think it's not possible for a race to occur between incr and any other access to *ptr (without causing undefined behavior anyway).
I would feel confident making this optimization manually, but no compiler I know of does so automatically. Is it correct? Or have I misunderstood restrict?
The optimization cannot be made by the compiler, because the behavior of restrict is defined only with respect to accesses to the same object via other pointers within incr.
From this reference, with my own emphasis:
During each execution of a block in which a restricted pointer P is declared (typically each execution of a function body in which P is a function parameter), if some object that is accessible through P (directly or indirectly) is modified, by any means, then all accesses to that object (both reads and writes) in that block must occur through P (directly or indirectly), otherwise the behavior is undefined...
The restriction on the pointer only applies to accesses made from incr, meaning that a conformant compiler should not be able to conclude anything about accesses made by other threads, or other functions that have their own pointers pointing to the same integer (which is perfectly fine a caller of incr might only have non-restricted pointers to it).
The open standard, section 6.7.3.1, also defines the formal behavior of restrict with respect to the block being executed, making no claim about the lifetime of any pointees. However, I think the standard's definition is too mathematically unwieldy to be worth presenting in full here.
Or have I misunderstood restrict?
Yeah - it makes no guarantees of what goes on outside the function where you used it. So it cannot be used for synchronization or thread-safety purposes.
The TL;DR about restrict is that *restrict ptr guarantees that no other pointer access to the object that ptr points at will be done from the block where ptr was declared. If there are other pointers or object references present ("lvalue accesses"), the compiler may assume that they aren't modifying that pointed-at restricted object.
This is pretty much just a contract between the programmer and the compiler: "Dear compiler, I promise I won't do stupid things behind your back with the object that this pointer points at". But the compiler can't really check if the programmer broke this contract, because in order to do so it may have to go check across several different translation units at once.
Example code:
#include <stdio.h>
int a=0;
int b=1;
void copy_stuff (int* restrict pa, int* restrict pb)
{
*pa = *pb; // assign the value 1 to a
a=2; // undefined behavior, the programmer broke the restrict contract
printf("%d\n", *pa);
}
int main()
{
copy_stuff(&a, &b);
printf("%d\n", a);
}
This prints 1 then 2 with optimization on, on all mainstream x86 compilers. Because in the printf inside the function they are free to assume that *pa has not been modified since *pa = *pb;. Drop restrict and they will print 2 then 2, because the compiler will have to add an extra read instruction in the machine code.
So for restrict to make sense, it needs to be compared with another reference to an object. That's not the case in your example, so restrict fills no apparent purpose.
As for _Atomic, it will always have to guarantee that the machine instructions in themselves are atomic, regardless of what else goes on in the program. It's not some semi-high level "nobody else seems to be using this variable anyway". But for the keyword to make sense for multi-threading and multi-core both, it would also have to act as a low level memory barrier to prevent pipelining, instruction re-ordering and data caching - which if I remember correctly is exactly what x86 lock does.
A program accessing illegal pointer to pointer does not crash with SIGSEGV. This is not a good thing, but I’m wondering how this could be and how the process survived for many days in production. It is bewildering to me.
I have given this program a go in Windows, Linux, OpenVMS, and Mac OS and they have never complained.
#include <stdio.h>
#include <string.h>
void printx(void *rec) { // I know this should have been a **
char str[1000];
memcpy(str, rec, 1000);
printf("%*.s\n", 1000, str);
printf("Whoa..!! I have not crashed yet :-P");
}
int main(int argc, char **argv) {
void *x = 0; // you could also say void *x = (void *)10;
printx(&x);
}
I am not surprised by the lack of a memory fault. The program is not dereferencing an uninitialized pointer. Instead, it is copying and printing the contents of memory beginning at a pointer variable, and the 996 (or 992) bytes beyond it.
Since the pointer is a stack variable, it is printing memory near the top of stack for a ways down. That memory contains the stack frame of main(): possibly some saved register values, a count of program arguments, a pointer to the program arguments, a pointer to a list of environment variables, and a saved instruction register for main() to return, usually in the C runtime library startup code. In all implementations I have investigated, the stack frames below that has copies of the environment variables themselves, an array of pointers to them, and an array of pointers to the program arguments. In Unix environments (which you hint you are using) the program argument strings will be below that.
All of this memory is "safe" to print, except some non-printable characters will appear which might mess up a display terminal.
The chief potential problem is whether there is enough stack memory allocated and mapped to prevent a SIGSEGV during access. A segment fault could happen if there is too little environment data. Or if the implementation puts that data elsewhere so that there are only a few words of stack here. I suggest confirming that by cleaning out the environment variables and re-running the program.
This code would not be so harmless if any of the C runtime conventions are not true:
The architecture uses a stack
A local variable (void *x) is allocated on the stack
The stack grows toward lower numbered memory
Parameters are passed on the stack
Whether main() is called with arguments. (Some light duty environments, like embedded processors, invoke main() without parameters.)
In all mainstream modern implementations, all of these are generally true.
Illegal memory access is undefined behaviour. This means that your program might crash, but is not guaranteed to, because exact behaviour is undefined.
(A joke among developers, especially when facing coworkers that are careless about such things, is that "invoking undefined behaviour might format your hard drive, it's just not guaranteed to". ;-) )
Update: There's some hot discussion going on here. Yes, system developers should know what actually happens on a given system. But such knowledge is tied to the CPU, the operating system, the compiler etc., and generally of limited usefulness, because even if you make the code work, it would still be of very poor quality. That's why I limited my answer to the most important point, and the actual question asked ("why doesn't this crash"):
The code posted in the question does not have well-defined behaviour, but that does just mean that you can't really rely on what it does, not that it should crash.
If you dereference an invalid pointer, you are invoking undefined behaviour. Which means, the program can crash, it can work, it could cook some coffee, whatever.
When you have
int main(int argc, char **argv) {
void *x = 0; // you could also say void *x = (void *)10;
printx(&x);
}
You are declaring x as a pointer with value 0, and that pointer lives in the stack since it's a local variable. Now, you are passing to printx the address of x, which means that with
memcpy(str, rec, 1000);
you are copying data from above the stack (or in fact from the stack itself), to the stack (because the stack pointer address decreases on each push). The source data is likely to be covered by the same page table entry as you are copying just 1000 bytes, so you get no segmentation fault. However, ultimately, as already written, we are talking about undefined behavior.
It would be crashed with great probability if you write to unacceed area. But you are reading, it can be ok. But the behaviour will be still undefined.
Write a portable function in C (with no assembly) that returns the size of its stack frame
int stackframe_size()
{
}
Attempted solving it as below - This function returns 228 bytes when compiled with VS 2010. Is there a way to verify its correctness?
int stackframe_size(int run)
{
int i ;
if(!run)
{
return ((int)(&i) - stackframe_size(++run));
}
return (int)(&i);
}
Invoked as:
int main()
{
printf("\nSize of stackframe_size() is: %d bytes",stackframe_size(0)) ;
return 0;
}
No such portable function is possible.
Your attempt is probably about as close as you can get, but the pointer subtraction has undefined behavior. More generally, p1 - p0, where p0 and p1 are pointers to distinct objects, has undefined behavior.
Except that your code subtracts to int values that are the result of conversions from addresses. Direct subtraction of the pointers is more likely to work -- and the pointers should be of type char* or unsigned char*. There are many implementations where int is too small to hold a converted pointer, and some where pointers have a more complicated representation than you assume, and converting them to even a sufficiently large integer type will not necessarily give you a meaningful result.
There are real-world C implementations that don't use a "stack" in the sense of a contiguous region of memory on which "stack frames" are pushed and popped. (There has to be a "stack" in the sense of a first-in last-out data structure, but the way that "stack" is implemented is entirely unspecified.) Some IBM mainframe implementations, for example, allocate memory for a function call via something like a heap, so that there is no defined relationship between the addresses of the local variables for two such calls.
You can probably write a function in pure C (without assembly language) that gives you the size of a stack frame for a particular implementation. But since the C language itself has no concept of a "stack frame" (the standard doesn't even use the word "stack"), it cannot be done portably.
I wanted to check if there is a clever/cleaner way to do this without adding local variables
You could use &run instead of &i - that would save you a local variable.
Is there a way to verify its correctness?
Use a debugger! Check the stack pointer register at the spots you care about, watch the spills take place, etc.
A program accessing illegal pointer to pointer does not crash with SIGSEGV. This is not a good thing, but I’m wondering how this could be and how the process survived for many days in production. It is bewildering to me.
I have given this program a go in Windows, Linux, OpenVMS, and Mac OS and they have never complained.
#include <stdio.h>
#include <string.h>
void printx(void *rec) { // I know this should have been a **
char str[1000];
memcpy(str, rec, 1000);
printf("%*.s\n", 1000, str);
printf("Whoa..!! I have not crashed yet :-P");
}
int main(int argc, char **argv) {
void *x = 0; // you could also say void *x = (void *)10;
printx(&x);
}
I am not surprised by the lack of a memory fault. The program is not dereferencing an uninitialized pointer. Instead, it is copying and printing the contents of memory beginning at a pointer variable, and the 996 (or 992) bytes beyond it.
Since the pointer is a stack variable, it is printing memory near the top of stack for a ways down. That memory contains the stack frame of main(): possibly some saved register values, a count of program arguments, a pointer to the program arguments, a pointer to a list of environment variables, and a saved instruction register for main() to return, usually in the C runtime library startup code. In all implementations I have investigated, the stack frames below that has copies of the environment variables themselves, an array of pointers to them, and an array of pointers to the program arguments. In Unix environments (which you hint you are using) the program argument strings will be below that.
All of this memory is "safe" to print, except some non-printable characters will appear which might mess up a display terminal.
The chief potential problem is whether there is enough stack memory allocated and mapped to prevent a SIGSEGV during access. A segment fault could happen if there is too little environment data. Or if the implementation puts that data elsewhere so that there are only a few words of stack here. I suggest confirming that by cleaning out the environment variables and re-running the program.
This code would not be so harmless if any of the C runtime conventions are not true:
The architecture uses a stack
A local variable (void *x) is allocated on the stack
The stack grows toward lower numbered memory
Parameters are passed on the stack
Whether main() is called with arguments. (Some light duty environments, like embedded processors, invoke main() without parameters.)
In all mainstream modern implementations, all of these are generally true.
Illegal memory access is undefined behaviour. This means that your program might crash, but is not guaranteed to, because exact behaviour is undefined.
(A joke among developers, especially when facing coworkers that are careless about such things, is that "invoking undefined behaviour might format your hard drive, it's just not guaranteed to". ;-) )
Update: There's some hot discussion going on here. Yes, system developers should know what actually happens on a given system. But such knowledge is tied to the CPU, the operating system, the compiler etc., and generally of limited usefulness, because even if you make the code work, it would still be of very poor quality. That's why I limited my answer to the most important point, and the actual question asked ("why doesn't this crash"):
The code posted in the question does not have well-defined behaviour, but that does just mean that you can't really rely on what it does, not that it should crash.
If you dereference an invalid pointer, you are invoking undefined behaviour. Which means, the program can crash, it can work, it could cook some coffee, whatever.
When you have
int main(int argc, char **argv) {
void *x = 0; // you could also say void *x = (void *)10;
printx(&x);
}
You are declaring x as a pointer with value 0, and that pointer lives in the stack since it's a local variable. Now, you are passing to printx the address of x, which means that with
memcpy(str, rec, 1000);
you are copying data from above the stack (or in fact from the stack itself), to the stack (because the stack pointer address decreases on each push). The source data is likely to be covered by the same page table entry as you are copying just 1000 bytes, so you get no segmentation fault. However, ultimately, as already written, we are talking about undefined behavior.
It would be crashed with great probability if you write to unacceed area. But you are reading, it can be ok. But the behaviour will be still undefined.