security/stack_clash: Add new test
Adapt https://bugzilla.novell.com/show_bug.cgi?id=CVE-2017-1000364
to run inside LTP.
Signed-off-by: Pavel Boldin <pboldin@cloudlinux.com>
Acked-by: Jan Stancek <jstancek@redhat.com>
diff --git a/runtest/cve b/runtest/cve
index 32a39cf..6e3e52d 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -7,3 +7,4 @@
cve-2017-2671 cve-2017-2671
cve-2017-5669 cve-2017-5669
cve-2017-6951 cve-2017-6951
+cve-2017-1000364 stack_clash
diff --git a/testcases/cve/.gitignore b/testcases/cve/.gitignore
index 1577cf9..298cf81 100644
--- a/testcases/cve/.gitignore
+++ b/testcases/cve/.gitignore
@@ -5,3 +5,4 @@
cve-2017-2671
cve-2017-6951
cve-2017-5669
+stack_clash
diff --git a/testcases/cve/stack_clash.c b/testcases/cve/stack_clash.c
new file mode 100644
index 0000000..2ef1a82
--- /dev/null
+++ b/testcases/cve/stack_clash.c
@@ -0,0 +1,267 @@
+/*
+ * Copyright (c) 2017 Pavel Boldin <pboldin@cloudlinux.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License as published by
+ * the Free Software Foundation, either version 2 of the License, or
+ * (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Adapted from code by Michal Hocko.
+ */
+
+/* This is a regression test of the Stack Clash [1] vulnerability. This tests
+ * that there is at least 256 PAGE_SIZE of stack guard gap which is considered
+ * hard to hop above. Code adapted from the Novell's bugzilla [2].
+ *
+ * The code `mmap(2)`s region close to the stack end. The code then allocates
+ * memory on stack until it hits guard page and SIGSEGV or SIGBUS is generated
+ * by the kernel. The signal handler checks that fault address is further than
+ * THRESHOLD from the mmapped area.
+ *
+ * We read /proc/self/maps to examine exact top of the stack and `mmap(2)`
+ * our region exactly GAP_PAGES * PAGE_SIZE away. We read /proc/cmdline to
+ * see if a different stack_guard_gap size is configured. We set stack limit
+ * to infinity and preallocate REQ_STACK_SIZE bytes of stack so that no calls
+ * after `mmap` are moving stack further.
+ *
+ * [1] https://blog.qualys.com/securitylabs/2017/06/19/the-stack-clash
+ * [2] https://bugzilla.novell.com/show_bug.cgi?id=CVE-2017-1000364
+ */
+
+#include <sys/mman.h>
+#include <sys/wait.h>
+#include <stdio.h>
+#include <unistd.h>
+#include <alloca.h>
+#include <signal.h>
+#include <stdlib.h>
+
+#include "tst_test.h"
+#include "tst_safe_stdio.h"
+
+static unsigned long PAGE_SIZE;
+static unsigned long PAGE_MASK;
+static unsigned long GAP_PAGES = 256;
+static unsigned long THRESHOLD;
+static int STACK_GROWSDOWN;
+
+#define SIGNAL_STACK_SIZE (1UL<<20)
+#define FRAME_SIZE 1024
+#define REQ_STACK_SIZE (1024 * 1024)
+
+#define EXIT_TESTBROKE TBROK
+
+void exhaust_stack_into_sigsegv(void)
+{
+ volatile char * ptr = alloca(FRAME_SIZE - sizeof(long));
+ *ptr = '\0';
+ exhaust_stack_into_sigsegv();
+}
+
+#define MAPPED_LEN PAGE_SIZE
+static unsigned long mapped_addr;
+
+void segv_handler(int sig, siginfo_t *info, void *data LTP_ATTRIBUTE_UNUSED)
+{
+ unsigned long fault_addr = (unsigned long)info->si_addr;
+ unsigned long mmap_end = mapped_addr + MAPPED_LEN;
+ ssize_t diff;
+
+ if (sig != SIGSEGV && sig != SIGBUS)
+ return;
+
+ if (STACK_GROWSDOWN)
+ diff = fault_addr - mmap_end;
+ else
+ diff = mapped_addr - fault_addr;
+
+ tst_res(TINFO,
+ "mmap = [%lx, %lx), addr = %lx, diff = %lx, THRESHOLD = %lx",
+ mapped_addr, mmap_end, fault_addr, diff, THRESHOLD);
+ if (diff < 0 || (unsigned long)diff < THRESHOLD)
+ _exit(EXIT_FAILURE);
+ else
+ _exit(EXIT_SUCCESS);
+}
+
+unsigned long read_stack_addr_from_proc(unsigned long *stack_size)
+{
+ FILE *fh;
+ char buf[1024];
+ unsigned long stack_top = -1UL, start, end;
+
+ fh = SAFE_FOPEN("/proc/self/maps", "r");
+
+ while (!feof(fh)) {
+ if (fgets(buf, sizeof(buf), fh) == NULL) {
+ tst_brk(TBROK | TERRNO, "fgets");
+ goto out;
+ }
+
+ if (!strstr(buf, "[stack"))
+ continue;
+
+ if (sscanf(buf, "%lx-%lx", &start, &end) != 2) {
+ tst_brk(TBROK | TERRNO, "sscanf");
+ goto out;
+ }
+
+ *stack_size = end - start;
+
+ if (STACK_GROWSDOWN)
+ stack_top = start;
+ else
+ stack_top = end;
+ break;
+ }
+
+out:
+ SAFE_FCLOSE(fh);
+ return stack_top;
+}
+
+void dump_proc_self_maps(void)
+{
+ static char buf[64];
+ static const char *cmd[] = {"cat", buf, NULL};
+ sprintf(buf, "/proc/%d/maps", getpid());
+ tst_run_cmd(cmd, NULL, NULL, 0);
+}
+
+void preallocate_stack(unsigned long required)
+{
+ volatile char *garbage;
+
+ garbage = alloca(required);
+ garbage[0] = garbage[required - 1] = '\0';
+}
+
+void do_child(void)
+{
+ unsigned long stack_addr, stack_size;
+ stack_t signal_stack;
+ struct sigaction segv_sig = {.sa_sigaction = segv_handler, .sa_flags = SA_ONSTACK|SA_SIGINFO};
+ void *map;
+ unsigned long gap = GAP_PAGES * PAGE_SIZE;
+ struct rlimit rlimit;
+
+ rlimit.rlim_cur = rlimit.rlim_max = RLIM_INFINITY;
+ SAFE_SETRLIMIT(RLIMIT_STACK, &rlimit);
+
+ preallocate_stack(REQ_STACK_SIZE);
+
+ stack_addr = read_stack_addr_from_proc(&stack_size);
+ if (stack_addr == -1UL) {
+ tst_brk(TBROK, "can't read stack top from /proc/self/maps");
+ return;
+ }
+
+ if (STACK_GROWSDOWN)
+ mapped_addr = stack_addr - gap - MAPPED_LEN;
+ else
+ mapped_addr = stack_addr + gap;
+
+ mapped_addr &= PAGE_MASK;
+ map = SAFE_MMAP((void *)mapped_addr, MAPPED_LEN,
+ PROT_READ|PROT_WRITE,
+ MAP_ANON|MAP_PRIVATE|MAP_FIXED, -1, 0);
+ tst_res(TINFO, "Stack:0x%lx+0x%lx mmap:%p+0x%lx",
+ stack_addr, stack_size, map, MAPPED_LEN);
+
+ signal_stack.ss_sp = SAFE_MALLOC(SIGNAL_STACK_SIZE);
+ signal_stack.ss_size = SIGNAL_STACK_SIZE;
+ signal_stack.ss_flags = 0;
+ if (sigaltstack(&signal_stack, NULL) == -1) {
+ tst_brk(TBROK | TERRNO, "sigaltstack");
+ return;
+ }
+ if (sigaction(SIGSEGV, &segv_sig, NULL) == -1 ||
+ sigaction(SIGBUS, &segv_sig, NULL) == -1) {
+ tst_brk(TBROK | TERRNO, "sigaction");
+ return;
+ }
+
+#ifdef DEBUG
+ dump_proc_self_maps();
+#endif
+
+ exhaust_stack_into_sigsegv();
+}
+
+void setup(void)
+{
+ char buf[4096], *p;
+
+ PAGE_SIZE = sysconf(_SC_PAGESIZE);
+ PAGE_MASK = ~(PAGE_SIZE - 1);
+
+ buf[4095] = '\0';
+ SAFE_FILE_SCANF("/proc/cmdline", "%4095[^\n]", buf);
+
+ if ((p = strstr(buf, "stack_guard_gap=")) != NULL) {
+ if (sscanf(p, "stack_guard_gap=%ld", &GAP_PAGES) != 1) {
+ tst_brk(TBROK | TERRNO, "sscanf");
+ return;
+ }
+ tst_res(TINFO, "stack_guard_gap = %ld", GAP_PAGES);
+ }
+
+ THRESHOLD = (GAP_PAGES - 1) * PAGE_SIZE;
+
+ {
+ volatile int *a = alloca(128);
+
+ {
+ volatile int *b = alloca(128);
+
+ STACK_GROWSDOWN = a > b;
+ tst_res(TINFO, "STACK_GROWSDOWN = %d == %p > %p", STACK_GROWSDOWN, a, b);
+ }
+ }
+}
+
+void stack_clash_test(void)
+{
+ int status;
+ pid_t pid;
+
+ pid = SAFE_FORK();
+ if (!pid) {
+ do_child();
+ exit(EXIT_TESTBROKE);
+ return;
+ }
+
+ SAFE_WAITPID(pid, &status, 0);
+
+ if (WIFEXITED(status)) {
+ switch (WEXITSTATUS(status)) {
+ case EXIT_FAILURE:
+ tst_res(TFAIL, "stack is too close to the mmaped area");
+ return;
+ case EXIT_SUCCESS:
+ tst_res(TPASS, "stack is far enough from mmaped area");
+ return;
+ default:
+ case EXIT_TESTBROKE:
+ break;
+ }
+ }
+
+ tst_brk(TBROK, "child did not exit gracefully");
+}
+
+static struct tst_test test = {
+ .forks_child = 1,
+ .needs_root = 1,
+ .setup = setup,
+ .test_all = stack_clash_test,
+};