diff mbox series

[v2] Add a test case for mmap MAP_GROWSDOWN flag

Message ID 20200703030811.11521-1-pravinraghul@zilogic.com
State Superseded
Headers show
Series [v2] Add a test case for mmap MAP_GROWSDOWN flag | expand

Commit Message

pravin July 3, 2020, 3:08 a.m. UTC
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.

Resolves #300
Signed-off-by: Pravin Raghul S. <pravinraghul@zilogic.com>
Reviewed-by: Vijay Kumar B. <vijaykumar@zilogic.com>

---
 runtest/syscalls                          |   1 +
 testcases/kernel/syscalls/mmap/.gitignore |   1 +
 testcases/kernel/syscalls/mmap/Makefile   |   7 ++
 testcases/kernel/syscalls/mmap/mmap18.c   | 141 ++++++++++++++++++++++
 4 files changed, 150 insertions(+)
 create mode 100644 testcases/kernel/syscalls/mmap/mmap18.c

Comments

Li Wang July 28, 2020, 3:20 a.m. UTC | #1
Hi,


On Fri, Jul 3, 2020 at 11:09 AM pravin <pravinraghul@zilogic.com> wrote:

> ...
> +/*
> + * Test mmap() MAP_GROWSDOWN flag
> + *
> + * 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.
> + */
> +
> +#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 bool check_stackgrow_up(int *local_var_1)
> +{
> +       int local_var_2;
> +
> +       return !(local_var_1 < &local_var_2);
>

Shouldn't local_var_1 less than local_var_2 on a stack grow up arch? why we
return the reverse value here?

And the worth to say that the optimization of GCC will break this rule in
the compilation.

 -O2  (ltp default gcc flag)
mmap18.c:46: INFO: local_var_1 = 0x3ffd177dea0, loval_var_2 = 0x3ffd177dea4
-O0
mmap18.c:46: INFO: local_var_1 = 0x3ffc86fe614, loval_var_2 = 0x3ffc86fe56c

--------

Apart from that, mmap18 also gets FAIL with s390x platform like:

# ./mmap18
tst_test.c:1247: INFO: Timeout per run is 0h 05m 00s
mmap18.c:46: INFO: local_var_1 = 0x3fff537e5d4, loval_var_2 = 0x3fff537e52c
mmap18.c:126: INFO: mem = 0x3ff8dd3a000, stack = 0x3ff8dd5a000
mmap18.c:136: FAIL: Child killed by SIGSEGV

Summary:
passed   0
failed   1
skipped  0
warnings 0
Li Wang July 28, 2020, 1:29 p.m. UTC | #2
On Tue, Jul 28, 2020 at 11:20 AM Li Wang <liwang@redhat.com> wrote:

> ...
>
>> +       return !(local_var_1 < &local_var_2);
>>
>
> Shouldn't local_var_1 less than local_var_2 on a stack grow up arch? why
> we return the reverse value here?
>
> And the worth to say that the optimization of GCC will break this rule in
> the compilation.
>
> -O2  (ltp default gcc flag)
> mmap18.c:46: INFO: local_var_1 = 0x3ffd177dea0, loval_var_2 = 0x3ffd177dea4
> -O0
> mmap18.c:46: INFO: local_var_1 = 0x3ffc86fe614, loval_var_2 = 0x3ffc86fe56c
>

To avoid the compiler optimization disturbing the address order of
variables, I'd
suggest using only one local variable as the baseline to compare with
itself new
address in another recursive function calling.

Something maybe like this:

static int check_stackgrow_up(void)
{
    char local_var;
    static char *addr = 0;

    if (addr == 0) {
        addr = &local_var;
        return check_stackgrow_up();
    }

    return ((&local_var > addr) ? 1 : 0);
}


--------
>
> Apart from that, mmap18 also gets FAIL with s390x platform like:
>
> # ./mmap18
> tst_test.c:1247: INFO: Timeout per run is 0h 05m 00s
> mmap18.c:46: INFO: local_var_1 = 0x3fff537e5d4, loval_var_2 = 0x3fff537e52c
> mmap18.c:126: INFO: mem = 0x3ff8dd3a000, stack = 0x3ff8dd5a000
> mmap18.c:136: FAIL: Child killed by SIGSEGV
>

From my observation, the failure only occurs on s390x, and it could not
overstride to the unmap memory area. With the following debug-code
adding to run_test():

+       memset(stack, 1, getpagesize());
+       tst_res(TINFO, "write to *stack sucess");
+       memset(stack - getpagesize(), 1, getpagesize());
+       tst_res(TINFO, "write to *(stack - %d) sucess", getpagesize());

mmap18 (on s390x) prints:

  tst_test.c:1247: INFO: Timeout per run is 0h 05m 00s
  mmap18.c:137: INFO: write to *stack sucess
  tst_test.c:1292: BROK: Test killed by SIGSEGV!

But trying with other architectures(x86_64, aarch64), they all override
the stack and write into the unmapped area.

  tst_test.c:1247: INFO: Timeout per run is 0h 05m 00s
  mmap18.c:139: INFO: write to *stack sucess
  mmap18.c:141: INFO: write to *(stack - 65536) sucess
  mmap18.c:151: PASS: Stack grows in unmapped region


And the Linux Manual makes me feel confusing here, what is the correct
behavior when writing the "guard" page? Can someone help explain this?

“"”
    MAP_GROWSDOWN
    ...
    Touching an address in the "guard" page below the mapping will
    cause the mapping to grow by a page. This growth can be repeated
    until the mapping grows to within a page of the high end of the next
    lower mapping, at which point touching the "guard" page will result
    in a SIGSEGV signal.
"""
diff mbox series

Patch

diff --git a/runtest/syscalls b/runtest/syscalls
index b4d523319..d8c9dbe92 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/Makefile b/testcases/kernel/syscalls/mmap/Makefile
index 743ca36e7..bdc49e4be 100644
--- a/testcases/kernel/syscalls/mmap/Makefile
+++ b/testcases/kernel/syscalls/mmap/Makefile
@@ -8,3 +8,10 @@  include $(top_srcdir)/include/mk/testcases.mk
 include $(top_srcdir)/include/mk/generic_leaf_target.mk
 
 LDLIBS 			+= -lpthread
+#
+# We use recursive calls to to grow the stack, to test the
+# MAP_GROSWDOWN flag. But tail call optimization by the compiler
+# can prevent the recusive call and stack growth. Disable tail
+# call optmization using -fno-optimize-sibling-calls
+#
+mmap18: CFLAGS          += -fno-optimize-sibling-calls
diff --git a/testcases/kernel/syscalls/mmap/mmap18.c b/testcases/kernel/syscalls/mmap/mmap18.c
new file mode 100644
index 000000000..497d5997e
--- /dev/null
+++ b/testcases/kernel/syscalls/mmap/mmap18.c
@@ -0,0 +1,141 @@ 
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (c) Zilogic Systems Pvt. Ltd., 2020
+ * Email: code@zilogic.com
+ */
+
+/*
+ * Test mmap() MAP_GROWSDOWN flag
+ *
+ * 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.
+ */
+
+#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 bool check_stackgrow_up(int *local_var_1)
+{
+	int local_var_2;
+
+	return !(local_var_1 < &local_var_2);
+}
+
+static void setup(void)
+{
+	int local_var_1;
+
+	if (check_stackgrow_up(&local_var_1))
+		tst_brk(TCONF, "Test can't be performed with stack grows up architecture");
+}
+
+static void cleanup(void)
+{
+	if (stack)
+		SAFE_MUNMAP(stack, UNITS(8));
+}
+
+static void *find_free_range(size_t size)
+{
+	void *mem;
+
+	mem = SAFE_MMAP(NULL, size, PROT_READ | PROT_WRITE,
+			MAP_PRIVATE | MAP_ANONYMOUS, -1, 0);
+	SAFE_MUNMAP(mem, size);
+
+	return mem;
+}
+
+static void split_unmapped_plus_stack(void *start, size_t size)
+{
+	/* start           start + size
+	 * +---------------------+----------------------+
+	 * + unmapped            | mapped               |
+	 * +---------------------+----------------------+
+	 *                       stack
+	 */
+	stack = SAFE_MMAP(start + size, size, PROT_READ | PROT_WRITE,
+			  MAP_FIXED | MAP_PRIVATE | MAP_ANONYMOUS | MAP_GROWSDOWN,
+			  -1, 0);
+}
+
+static void *check_depth_recursive(void *limit)
+{
+	if ((off_t) &limit < (off_t) 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);
+
+	exit(0);
+}
+
+static void run_test(void)
+{
+	void *mem;
+	pid_t child_pid;
+	int wstatus;
+
+	mem = find_free_range(UNITS(16));
+	split_unmapped_plus_stack(mem, UNITS(8));
+
+	child_pid = SAFE_FORK();
+	if (!child_pid)
+		grow_stack(stack, UNITS(8), mem + UNITS(1));
+
+	SAFE_WAIT(&wstatus);
+	if (WIFEXITED(wstatus) && WEXITSTATUS(wstatus) == 0)
+		tst_res(TPASS, "Stack grows in unmapped region");
+	else if (WIFSIGNALED(wstatus))
+		tst_res(TFAIL, "Child killed by %s", tst_strsig(WTERMSIG(wstatus)));
+	else
+		tst_res(TFAIL, "Child %s", tst_strstatus(wstatus));
+}
+
+static struct tst_test test = {
+	.setup = setup,
+	.cleanup = cleanup,
+	.test_all = run_test,
+	.forks_child = 1,
+};