[LTP] [PATCH v5] Add a test case for mmap MAP_GROWSDOWN flag

Cyril Hrubis chrubis@suse.cz
Fri Sep 11 15:08:36 CEST 2020


Hi!
> We assign the memory region allocated using MAP_GROWSDOWN to a thread,
> as a stack, to test the effect of MAP_GROWSDOWN. This is because the
> kernel only grows the memory region when the stack pointer, is within
> guard page, when the guard page is touched.
> 
>   1. Map an anyonymous memory region of size X, and unmap it.
>   2. Split the unmapped memory region into two.
>   3. The lower memory region is left unmapped.
>   4. The higher memory region is mapped for use as stack, using MAP_FIXED | MAP_GROWSDOWN.
>   5. The higher memory region is provided as stack to a thread, where
>      a recursive function is invoked.
>   6. The stack grows beyond the allocated region, into the lower memory area.
>   7. If this results in the memory region being extended, into the
>      unmapped region, the test is considered to have passed.
> 
> Also, to verify that(Test2) the stack grows to within a page of the high
> end of the next lower map???ping will result in a SIGSEGV signal.
> 
> Resolves #300
> Signed-off-by: Pravin Raghul S. <pravinraghul@zilogic.com>
> Reviewed-by: Vijay Kumar B. <vijaykumar@zilogic.com>
> Signed-off-by: Li Wang <liwang@redhat.com>
> Cc: Cyril Hrubis <chrubis@suse.cz>
> ---
>  runtest/syscalls                          |   1 +
>  testcases/kernel/syscalls/mmap/.gitignore |   1 +
>  testcases/kernel/syscalls/mmap/mmap18.c   | 177 ++++++++++++++++++++++
>  3 files changed, 179 insertions(+)
>  create mode 100644 testcases/kernel/syscalls/mmap/mmap18.c
> 
> diff --git a/runtest/syscalls b/runtest/syscalls
> index dc0ca5626..ed86bb593 100644
> --- a/runtest/syscalls
> +++ b/runtest/syscalls
> @@ -747,6 +747,7 @@ mmap14 mmap14
>  mmap15 mmap15
>  mmap16 mmap16
>  mmap17 mmap17
> +mmap18 mmap18
>  
>  modify_ldt01 modify_ldt01
>  modify_ldt02 modify_ldt02
> diff --git a/testcases/kernel/syscalls/mmap/.gitignore b/testcases/kernel/syscalls/mmap/.gitignore
> index c5c083d4b..4fd90ab5f 100644
> --- a/testcases/kernel/syscalls/mmap/.gitignore
> +++ b/testcases/kernel/syscalls/mmap/.gitignore
> @@ -16,3 +16,4 @@
>  /mmap15
>  /mmap16
>  /mmap17
> +/mmap18
> diff --git a/testcases/kernel/syscalls/mmap/mmap18.c b/testcases/kernel/syscalls/mmap/mmap18.c
> new file mode 100644
> index 000000000..b5008497d
> --- /dev/null
> +++ b/testcases/kernel/syscalls/mmap/mmap18.c
> @@ -0,0 +1,177 @@
> +// SPDX-License-Identifier: GPL-2.0-or-later
> +/*
> + * Copyright (c) Zilogic Systems Pvt. Ltd., 2020
> + * Email: code@zilogic.com
> + */
> +
> +/*
> + * Test mmap() MAP_GROWSDOWN flag
> + *
> + * # Test1:
> + *   We assign the memory region allocated using MAP_GROWSDOWN to a thread,
> + *   as a stack, to test the effect of MAP_GROWSDOWN. This is because the
> + *   kernel only grows the memory region when the stack pointer, is within
> + *   guard page, when the guard page is touched.
> + *
> + *   1. Map an anyonymous memory region of size X, and unmap it.
> + *   2. Split the unmapped memory region into two.
> + *   3. The lower memory region is left unmapped.
> + *   4. The higher memory region is mapped for use as stack, using
> + *      MAP_FIXED | MAP_GROWSDOWN.
> + *   5. The higher memory region is provided as stack to a thread, where
> + *      a recursive function is invoked.
> + *   6. The stack grows beyond the allocated region, into the lower memory area.
> + *   7. If this results in the memory region being extended, into the
> + *      unmapped region, the test is considered to have passed.
> + *
> + * # Test2:
> + *   Steps mostly like Test1, but mmaping a page in the space the stack is
> + *   supposed to grow into. To verify that the stack grows to within a page
> + *   of the high end of the next lower map???ping, at which point touching
> + *   the "guard" page will result in a SIGSEGV signal.
> + */
> +
> +#include <unistd.h>
> +#include <pthread.h>
> +#include <sys/mman.h>
> +#include <sys/wait.h>
> +#include <sys/types.h>
> +#include <stdlib.h>
> +#include <stdbool.h>
> +
> +#include "tst_test.h"
> +#include "tst_safe_pthread.h"
> +
> +#define UNITS(x) ((x) * PTHREAD_STACK_MIN)
> +
> +static void *stack;
> +static long stack_size = UNITS(8);
> +
> +static bool __attribute__((noinline)) check_stackgrow_up(void)
> +{
> +	char local_var;
> +	static char *addr;
> +
> +       if (!addr) {
> +               addr = &local_var;
> +               return check_stackgrow_up();
> +       }
> +
> +       return (addr < &local_var);
> +}
> +
> +static void setup(void)
> +{
> +	if (check_stackgrow_up())
> +		tst_brk(TCONF, "Test can't be performed with stack grows up architecture");
> +}
> +
> +static void allocate_stack(size_t size)
> +{
> +	void *start;
> +
> +	/*
> +	 * Since the newer kernel does not allow a MAP_GROWSDOWN mapping to grow
> +	 * closer than 'stack_guard_gap' pages away from a preceding mapping.
> +	 * The guard then ensures that the next-highest mapped page remains more
> +	 * than 'stack_guard_gap' below the lowest stack address, and if not then
> +	 * it will trigger a segfault. So, here adding 256 pages memory spacing
> +	 * for stack growing safely.
> +	 *
> +	 * Btw, kernel default 'stack_guard_gap' size is '256 * getpagesize()'.
> +	 */
> +	long total_size = 256 * getpagesize() + size * 2;
> +
> +	start = SAFE_MMAP(NULL, total_size, PROT_READ | PROT_WRITE,
> +			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +	SAFE_MUNMAP(start, total_size);
> +
> +	/* start                             stack
> +	 * +-----------+---------------------+----------------------+
> +	 * | 256 pages | unmapped (size)     | mapped (size)        |
> +	 * +-----------+---------------------+----------------------+
> +	 *
> +	 */
> +	stack = SAFE_MMAP((start + total_size - size), size, PROT_READ | PROT_WRITE,
> +			  MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN,
> +			  -1, 0);

Well it's not wrong per se but as it is we do not use the pre-allocated
part of the stack at all, we directly jump for the guard page as we use
the stack pointer as a base for the pthread stack. The actual pointer
that points to the start of the region is stack - stack_size.

There is no point in adding size * 2 here. We can as well reserve 256 *
page_size + size. Then map() a single page at the end, which would be at
start + total_size - page_size and finally return start + total_size
from this function and pass that to pthread_attr_setstack().

That way it would look like:

| 256 pages | unmapped | 1 mapped page |

            | - - -  stack_size  - - - |


> +	tst_res(TINFO, "start = %p, stack = %p", start, stack);
> +}
> +
> +static __attribute__((noinline)) void *check_depth_recursive(void *limit)
> +{
> +	if ((off_t) &limit < (off_t) limit) {
> +		tst_res(TINFO, "&limit = %p, limit = %p", &limit, limit);
> +		return NULL;
> +	}
> +
> +	return check_depth_recursive(limit);
> +}
> +
> +static void grow_stack(void *stack, size_t size, void *limit)
> +{
> +	pthread_t test_thread;
> +	pthread_attr_t attr;
> +	int ret;
> +
> +	ret = pthread_attr_init(&attr);
> +	if (ret)
> +		tst_brk(TBROK, "pthread_attr_init failed during setup");
> +
> +	ret = pthread_attr_setstack(&attr, stack, size);
> +	if (ret)
> +		tst_brk(TBROK, "pthread_attr_setstack failed during setup");
> +
> +	SAFE_PTHREAD_CREATE(&test_thread, &attr, check_depth_recursive, limit);
> +	SAFE_PTHREAD_JOIN(test_thread, NULL);
> +
> +	if (stack)
> +		SAFE_MUNMAP(stack, stack_size);

I'ts a bit unexpected to unmap the stack here. I guess that unamping it
in the run_test() after the grow_stack() call would be a bit cleaner but
we would have to move the exit(0) there as well.

> +	exit(0);
> +}
> +
> +static void run_test(void)
> +{
> +	pid_t child_pid;
> +	int wstatus;
> +
> +	/* Test 1 */
> +	child_pid = SAFE_FORK();
> +	if (!child_pid) {
> +		allocate_stack(stack_size);
> +		grow_stack(stack, stack_size, stack - stack_size + UNITS(1));


> +	}
> +
> +	SAFE_WAIT(&wstatus);
> +	if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0)
> +		tst_res(TPASS, "Stack grows in unmapped region");
> +	else
> +		tst_res(TFAIL, "Child: %s", tst_strstatus(wstatus));
> +
> +	/* Test 2 */
> +	child_pid = SAFE_FORK();
> +	if (!child_pid) {
> +		tst_no_corefile(0);
                  ^
		 This should go to the test setup.

> +		allocate_stack(stack_size);
> +
> +		SAFE_MMAP(stack - stack_size, UNITS(1), PROT_READ | PROT_WRITE,
> +			  MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
> +
> +		/* This will cause to segment fault (SIGSEGV) */
> +		grow_stack(stack, stack_size, stack - stack_size + UNITS(1));
> +	}
> +
> +	SAFE_WAIT(&wstatus);
> +        if (WIFSIGNALED(wstatus) && WTERMSIG(wstatus) == SIGSEGV)
> +		tst_res(TPASS, "Child ended by %s as expected", tst_strsig(SIGSEGV));
> +        else
> +                tst_res(TFAIL, "Child: %s", tst_strstatus(wstatus));
> +}
> +
> +static struct tst_test test = {
> +	.setup = setup,
> +	.test_all = run_test,
> +	.forks_child = 1,
> +};
> -- 
> 2.21.1
> 

-- 
Cyril Hrubis
chrubis@suse.cz


More information about the ltp mailing list