Stack guards

This neatly brings us to the next point: is there a way to have the application know that stack memory is in danger of being, or rather, has been, overflowed? Indeed: stack guards. Guard memory is a region of one or more virtual memory pages that has been deliberately placed, and with appropriate permissions, to ensure that any attempt to access that memory results in failure (or a warning of some sort; for example, a signal handler for SIGSEGV could provide just such a semantic - with the caveat that once we've received the SIGSEGV, we are in an undefined state and must terminate; but at least we'll know and can fix the stack size!):

#include <pthread.h>
int pthread_attr_setguardsize(pthread_attr_t *attr, size_t guardsize);
int pthread_attr_getguardsize(const pthread_attr_t *attr,
size_t *guardsize);

The guard region is an additional memory region allocated at the end of the thread stack for the number of bytes specified. The default (guard) size is the system page size. Note, again, that the guard size is an attribute of the thread and can thus only be specified at thread creation time (and not later). We will run the (code: ch14/stack_test.c) app like so:

$ ./stack_test 
Usage: ./stack_test size-of-thread-stack-in-KB
$ ./stack_test 2560
Default thread stack size : 8388608 bytes
Thread stack size now set to : 2621440 bytes
Default thread stack guard size : 4096 bytes

main: creating thread #0 ...
main: creating thread #1 ...
main: creating thread #2 ...
worker #0:
main: joining (waiting) upon thread #0 ...
worker #1:

*** In danger(): here, sizeof long is 8
worker #2:
Thread #0 successfully joined; it terminated with status=1
main: joining (waiting) upon thread #1 ...
dummy(): parameter val = 115709118
Thread #1 successfully joined; it terminated with status=0
main: joining (waiting) upon thread #2 ...
Thread #2 successfully joined; it terminated with status=1
main: now dying... <Dramatic!> Farewell!
$

In the preceding code, we specify 2,560 KB (2.5 MB) as the thread stack size. Though this is far less than the default (8 MB), it turns out to be enough (for x86_64 at least, a quick back-of-the-envelope calculation shows that, for the given program parameters, we shall require a minimum of 1,960 KB to be allocated for each thread stack).

In the following code, we run it again, but this time specify the thread stack size as a mere 256 KB:

$ ./stack_test 256
Default thread stack size : 8388608 bytes
Thread stack size now set to : 262144 bytes
Default thread stack guard size : 4096 bytes

main: creating thread #0 ...
main: creating thread #1 ...
worker #0:
main: creating thread #2 ...
worker #1:
main: joining (waiting) upon thread #0 ...
Segmentation fault (core dumped)
$

And, as expected, it segfaults.

Examining the core dump with GDB will reveal a lot of clues regarding why the segfault occurred – including, very importantly, the state of the thread stacks (in effect, the stack backtrace(s)), at the time of the crash. This, however, goes beyond the scope of this book.
We definitely encourage you to learn about using a powerful debugger such as GDB (see the Further reading section on the GitHub repository as well).

Also (on our test system at least), the kernel emits a message into the kernel log regarding this crash; one way to look up the kernel log messages is via the convenience utility dmesg(1). The following output is from an Ubuntu 18.04 box:

$ dmesg
[...]
kern :info : [<timestamp>] stack_test_dbg[27414]: segfault at 7f5ad1733000 ip 0000000000400e68 sp 00007f5ad164aa20 error 6 in stack_test_dbg[400000+2000]
$

The code for the preceding application can be found here: ch14/stack_test.c :

For readability, only key parts of the source code are displayed; to view the complete source code, build it, and run it, the entire tree is available for cloning from GitHub here: https://github.com/PacktPublishing/Hands-on-System-Programming-with-Linux.
int main(int argc, char **argv)
{
[...]
stack_set = atoi(argv[1]) * 1024;
[...]
/* Init the thread attribute structure to defaults */
pthread_attr_init(&attr);
[...]
/* Set thread stack size */
ret = pthread_attr_setstacksize(&attr, stack_set);
if (ret)
FATAL("pthread_attr_setstack(%u) failed! [%d] ", TSTACK, ret);
printf("Thread stack size now set to : %10u bytes ", stack_set);
[...]

In main, we show the thread stack size attribute being initialized to the parameter passed by the user (in KB). The code then goes on to create three worker threads and then joins (waits) on them.

In the thread worker routine, we have only thread #2 performing some actual work—you guessed it, stack-intensive work. The code for this is as follows:

void * worker(void *data)
{
long datum = (long)data;

printf(" worker #%ld: ", datum);
if (datum != 1)
pthread_exit((void *)1);

danger();
...

The danger function, of course, is the one where this dangerous, potentially stack-overflowing work is carried out:

static void danger(void)
{
#define NEL 500
long heavylocal[NEL][NEL], alpha=0;
int i, j;
long int k=0;

srandom(time(0));

printf(" *** In %s(): here, sizeof long is %ld ",
__func__, sizeof(long));
/* Turns out to be 8 on an x86_64; so the 2d-array takes up
* 500 * 500 * 8 = 2,000,000 ~= 2 MB.
* So thread stack space of less than 2 MB should result in a segfault.
* (On a test box, any value < 1960 KB = 2,007,040 bytes,
* resulted in segfault).
*/

/* The compiler is quite intelligent; it will optimize away the
* heavylocal 2d array unless we actually use it! So lets do some
* thing with it...
*/
for (i=0; i<NEL; i++) {
k = random() % 1000;
for (j=0; j<NEL-1; j++)
heavylocal[i][j] = k;
/*printf("hl[%d][%d]=%ld ", i, j, (long)heavylocal[i][j]);*/
}

for (i=0; i<NEL; i++)
for (j=0; j<NEL; j++)
alpha += heavylocal[i][j];
dummy(alpha);
}

The preceding function uses large amounts of (thread) stack space since we have declared a local variable called heavylocal – a 2D-array of NEL*NEL elements (NEL=500). On an x86_64 with a long data type occupying 8 bytes, this works out to approximately 2 MB of space! Thus, specifying the thread stack size as any less than 2 MB should result in a stack overflow (the stack guard memory region will in fact detect this) and therefore result in a segmentation violation (or segfault); this is precisely what happened (as you can see in our trial run).

Interestingly, if we merely declare the local variable but do not actually make use of it, modern compilers will just optimize the code out; hence, in the code, we strive to make some (silly) use of the heavylocal variable.

A few additional points on the stack guard memory region, to round off this discussion, are as follows:

  • If an application has used pthread_attr_setstack(3), it implies that it is managing thread stack memory itself, and any guard size attribute will be ignored.
  • The guard region must be aligned to a page boundary.
  • If the size of the guard memory region is less than a page, the actual (internal) size will be rounded to a page; pthread_attr_getguardsize(3) returns the theoretical size.
  • The man page on pthread_attr_[get|set]guardsize(3) does provide additional information, including possible glibc bugs within the implementation.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.118.138.141