Skip site navigation (1)Skip section navigation (2)
Date:      Sat, 3 Jul 2021 00:28:31 GMT
From:      Ryan Libby <rlibby@FreeBSD.org>
To:        src-committers@FreeBSD.org, dev-commits-src-all@FreeBSD.org, dev-commits-src-branches@FreeBSD.org
Subject:   git: 16d43e560229 - stable/13 - shared shadow vm object invalidation regression test
Message-ID:  <202107030028.1630SVmi096451@gitrepo.freebsd.org>

next in thread | raw e-mail | index | archive | help
The branch stable/13 has been updated by rlibby:

URL: https://cgit.FreeBSD.org/src/commit/?id=16d43e5602296bcaa7a7a4fdaf6e8e336d6a795f

commit 16d43e5602296bcaa7a7a4fdaf6e8e336d6a795f
Author:     Ryan Libby <rlibby@FreeBSD.org>
AuthorDate: 2021-04-07 19:39:05 +0000
Commit:     Ryan Libby <rlibby@FreeBSD.org>
CommitDate: 2021-07-02 21:14:32 +0000

    shared shadow vm object invalidation regression test
    
    Add a regression test for a scenario where a shadow vm object is shared
    by multiple mappings.  If a page COW occurs through one of the mappings,
    then the virtual-to-physical mapping may become invalidated.
    
    This tests the scenario from CVE-2021-29626 which was fixed by
    982693bb729badac4e65ecd59772979f2849a2b2.
    
    Reviewed by:    markj
    Sponsored by:   Dell EMC Isilon
    
    (cherry picked from commit 13d4f96130b64b42f74dfc484305b08c4da54669)
---
 tests/sys/vm/Makefile                   |   3 +-
 tests/sys/vm/shared_shadow_inval_test.c | 360 ++++++++++++++++++++++++++++++++
 2 files changed, 362 insertions(+), 1 deletion(-)

diff --git a/tests/sys/vm/Makefile b/tests/sys/vm/Makefile
index c2c95d25407f..8ef8a45e5f39 100644
--- a/tests/sys/vm/Makefile
+++ b/tests/sys/vm/Makefile
@@ -6,6 +6,7 @@ TESTSDIR=	${TESTSBASE}/sys/vm
 
 ATF_TESTS_C+=	mlock_test \
 		mmap_test \
-		page_fault_signal
+		page_fault_signal \
+		shared_shadow_inval_test
 
 .include <bsd.test.mk>
diff --git a/tests/sys/vm/shared_shadow_inval_test.c b/tests/sys/vm/shared_shadow_inval_test.c
new file mode 100644
index 000000000000..75e3655e3bd8
--- /dev/null
+++ b/tests/sys/vm/shared_shadow_inval_test.c
@@ -0,0 +1,360 @@
+/*
+ * Copyright (c) 2021 Dell Inc. or its subsidiaries. All Rights Reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ *    notice, this list of conditions and the following disclaimer.
+ * 2. Redistributions in binary form must reproduce the above copyright
+ *    notice, this list of conditions and the following disclaimer in the
+ *    documentation and/or other materials provided with the distribution.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
+ * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
+ * ARE DISCLAIMED.  IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
+ * OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
+ * HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
+ * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
+ * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
+ * SUCH DAMAGE.
+ */
+
+/*
+ * Test behavior when a mapping of a shared shadow vm object is
+ * invalidated by COW from another mapping.
+ *
+ * This is a regression test for an issue isolated by rlibby@FreeBSD.org
+ * from an issue detected by stress2's collapse.sh by jeff@FreeBSD.org.
+ * The issue became CVE-2021-29626.
+ *
+ * This file is written as an ATF test suite but may be compiled as a
+ * standalone program with -DSTANDALONE (and optionally -DDEBUG).
+ */
+
+#include <sys/types.h>
+#include <sys/mman.h>
+#include <sys/procctl.h>
+#include <sys/wait.h>
+#include <machine/atomic.h>
+
+#include <err.h>
+#include <errno.h>
+#include <stdbool.h>
+#include <stddef.h>
+#include <stdio.h>
+#include <stdlib.h>
+#include <unistd.h>
+
+#ifdef	STANDALONE
+#define	ATF_REQUIRE(x)	do {		\
+	if (!(x))			\
+		errx(1, "%s", #x);	\
+} while (0)
+#else
+#include <atf-c.h>
+#endif
+
+#ifdef	DEBUG
+#define	dprintf(...)	printf(__VA_ARGS__)
+#else
+#define	dprintf(...)
+#endif
+
+#define	DEPTH	5
+
+struct shared_state {
+	void *p;
+	size_t len;
+	size_t modlen;
+	bool collapse;
+	bool block_xfer;
+	bool okay;
+	volatile bool exiting[DEPTH];
+	volatile bool exit;
+	volatile bool p3_did_write;
+};
+
+static long g_pagesize;
+
+/*
+ * Program flow.  There are three or four processes that are descendants
+ * of the process running the test (P0), where arrows go from parents to
+ * children, and thicker arrows indicate sharing a certain memory region
+ * without COW semantics:
+ *     P0 -> P1 -> P2 => P3
+ *             \=> P4
+ * The main idea is that P1 maps a memory region, and that region is
+ * shared with P2/P3, but with COW semantics.  When P3 modifies the
+ * memory, P2 ought to see that modification.  P4 optionally exists to
+ * defeat a COW optimization.
+ */
+
+#define	child_err(...)	do {						\
+	ss->exit = true;						\
+	err(1, __VA_ARGS__);						\
+} while (0)
+
+#define	child_errx(...)	do {						\
+	ss->exit = true;						\
+	errx(1, __VA_ARGS__);						\
+} while (0)
+
+#define	SLEEP_TIME_US	1000
+
+static void child(struct shared_state *ss, int depth);
+
+static pid_t
+child_fork(struct shared_state *ss, int depth)
+{
+	pid_t pid = fork();
+	if (pid == -1)
+		child_err("fork");
+	else if (pid == 0)
+		child(ss, depth);
+	return pid;
+}
+
+static void
+child_fault(struct shared_state *ss)
+{
+	size_t i;
+
+	for (i = 0; i < ss->len; i += g_pagesize)
+		(void)((volatile char *)ss->p)[i];
+}
+
+static void
+child_write(struct shared_state *ss, int val, size_t len)
+{
+	size_t i;
+
+	for (i = 0; i < len; i += g_pagesize)
+		((int *)ss->p)[i / sizeof(int)] = val;
+	atomic_thread_fence_rel();
+}
+
+static void
+child_wait_p3_write(struct shared_state *ss)
+{
+	while (!ss->p3_did_write) {
+		if (ss->exit)
+			exit(1);
+		usleep(SLEEP_TIME_US);
+	}
+	atomic_thread_fence_acq();
+}
+
+static void
+child_verify(struct shared_state *ss, int depth, int newval, int oldval)
+{
+	size_t i;
+	int expectval, foundval;
+
+	for (i = 0; i < ss->len; i += g_pagesize) {
+		expectval = i < ss->modlen ? newval : oldval;
+		foundval = ((int *)ss->p)[i / sizeof(int)];
+		if (foundval == expectval)
+			continue;
+		child_errx("P%d saw %d but expected %d, %d was the old value",
+		    depth, foundval, expectval, oldval);
+	}
+}
+
+static void
+child(struct shared_state *ss, int depth)
+{
+	pid_t mypid, oldval, pid;
+
+	if (depth < 1 || depth >= DEPTH)
+		child_errx("Bad depth %d", depth);
+	mypid = getpid();
+	dprintf("P%d (pid %d) started\n", depth, mypid);
+	switch (depth) {
+	case 1:
+		/* Shared memory undergoing test. */
+		ss->p = mmap(NULL, ss->len, PROT_READ | PROT_WRITE,
+		    MAP_SHARED | MAP_ANON, -1, 0);
+		if (ss->p == MAP_FAILED)
+			child_err("mmap");
+		/* P1 stamps the shared memory. */
+		child_write(ss, mypid, ss->len);
+		if (ss->block_xfer) {
+			/*
+			 * P4 is forked so that its existence blocks a page COW
+			 * path where the page is simply transferred between
+			 * objects, rather than being copied.
+			 */
+			child_fork(ss, 4);
+		}
+		/*
+		 * P1 specifies that modifications from its child processes not
+		 * be shared with P1.  Child process reads can be serviced from
+		 * pages in P1's object, but writes must be COW'd.
+		 */
+		if (minherit(ss->p, ss->len, INHERIT_COPY) != 0)
+			child_err("minherit");
+		/* Fork P2. */
+		child_fork(ss, depth + 1);
+		/* P1 and P4 wait for P3's writes before exiting. */
+		child_wait_p3_write(ss);
+		child_verify(ss, depth, mypid, mypid);
+		if (!ss->collapse) {
+			/* Hang around to prevent collapse. */
+			while (!ss->exit)
+				usleep(SLEEP_TIME_US);
+		}
+		/* Exit so the P2 -> P1/P4 shadow chain can collapse. */
+		break;
+	case 2:
+		/*
+		 * P2 now specifies that modifications from its child processes
+		 * be shared.  P2 and P3 will share a shadow object.
+		 */
+		if (minherit(ss->p, ss->len, INHERIT_SHARE) != 0)
+			child_err("minherit");
+		/*
+		 * P2 faults a page in P1's object before P1 exits and the
+		 * shadow chain is collapsed.
+		 */
+		child_fault(ss);
+		oldval = atomic_load_acq_int(ss->p);
+		/* Fork P3. */
+		pid = child_fork(ss, depth + 1);
+		if (ss->collapse) {
+			/* Wait for P1 and P4 to exit, triggering collapse. */
+			while (!ss->exiting[1] ||
+			    (ss->block_xfer && !ss->exiting[4]))
+				usleep(SLEEP_TIME_US);
+			/*
+			 * This is racy, just guess at how long it may take
+			 * them to finish exiting.
+			 */
+			usleep(100 * 1000);
+		}
+		/* P2 waits for P3's modification. */
+		child_wait_p3_write(ss);
+		child_verify(ss, depth, pid, oldval);
+		ss->okay = true;
+		ss->exit = true;
+		break;
+	case 3:
+		/*
+		 * P3 writes the memory.  A page is faulted into the shared
+		 * P2/P3 shadow object.  P2's mapping of the page in P1's
+		 * object must now be shot down, or else P2 will wrongly
+		 * continue to have that page mapped.
+		 */
+		child_write(ss, mypid, ss->modlen);
+		ss->p3_did_write = true;
+		dprintf("P3 (pid %d) wrote its pid\n", mypid);
+		break;
+	case 4:
+		/* Just hang around until P3 is done writing. */
+		oldval = atomic_load_acq_int(ss->p);
+		child_wait_p3_write(ss);
+		child_verify(ss, depth, oldval, oldval);
+		break;
+	default:
+		child_errx("Bad depth %d", depth);
+	}
+
+	dprintf("P%d (pid %d) exiting\n", depth, mypid);
+	ss->exiting[depth] = true;
+	exit(0);
+}
+
+static void
+do_shared_shadow_inval(bool collapse, bool block_xfer, bool full_mod)
+{
+	struct shared_state *ss;
+	pid_t pid;
+
+	pid = getpid();
+
+	dprintf("P0 (pid %d) %s(collapse=%d, block_xfer=%d, full_mod=%d)\n",
+	    pid, __func__, (int)collapse, (int)block_xfer, (int)full_mod);
+
+	g_pagesize = sysconf(_SC_PAGESIZE);
+	ATF_REQUIRE(g_pagesize > 0);
+
+	ATF_REQUIRE(procctl(P_PID, pid, PROC_REAP_ACQUIRE, NULL) == 0);
+
+	/* Shared memory for coordination. */
+	ss = mmap(NULL, sizeof(*ss), PROT_READ | PROT_WRITE,
+	    MAP_SHARED | MAP_ANON, -1, 0);
+	ATF_REQUIRE(ss != MAP_FAILED);
+
+	ss->len = 2 * 1024 * 1024 + g_pagesize; /* 2 MB + page size */
+	ss->modlen = full_mod ? ss->len : ss->len / 2;
+	ss->collapse = collapse;
+	ss->block_xfer = block_xfer;
+
+	pid = fork();
+	ATF_REQUIRE(pid != -1);
+	if (pid == 0)
+		child(ss, 1);
+
+	/* Wait for all descendants to exit. */
+	do {
+		pid = wait(NULL);
+	} while (pid != -1 || errno != ECHILD);
+
+	atomic_thread_fence_acq();
+	ATF_REQUIRE(ss->okay);
+
+	ATF_REQUIRE(munmap(ss, sizeof(*ss)) == 0);
+	ATF_REQUIRE(procctl(P_PID, getpid(), PROC_REAP_RELEASE, NULL) == 0);
+}
+
+#ifdef STANDALONE
+int
+main(void)
+{
+
+	do_shared_shadow_inval(false, false, false);
+	do_shared_shadow_inval(false, false, true);
+	do_shared_shadow_inval(false, true, false);
+	do_shared_shadow_inval(false, true, true);
+	do_shared_shadow_inval(true, false, false);
+	do_shared_shadow_inval(true, false, true);
+	do_shared_shadow_inval(true, true, false);
+	do_shared_shadow_inval(true, true, true);
+	printf("pass\n");
+}
+#else
+
+#define SHARED_SHADOW_INVAL_TC(suffix, collapse, block_xfer, full_mod)	\
+ATF_TC_WITHOUT_HEAD(shared_shadow_inval__##suffix);			\
+ATF_TC_BODY(shared_shadow_inval__##suffix, tc)				\
+{									\
+	do_shared_shadow_inval(collapse, block_xfer, full_mod);		\
+}
+
+SHARED_SHADOW_INVAL_TC(nocollapse_noblockxfer_nofullmod, false, false, false);
+SHARED_SHADOW_INVAL_TC(nocollapse_noblockxfer_fullmod, false, false, true);
+SHARED_SHADOW_INVAL_TC(nocollapse_blockxfer_nofullmod, false, true, false);
+SHARED_SHADOW_INVAL_TC(nocollapse_blockxfer_fullmod, false, true, true);
+SHARED_SHADOW_INVAL_TC(collapse_noblockxfer_nofullmod, true, false, false);
+SHARED_SHADOW_INVAL_TC(collapse_noblockxfer_fullmod, true, false, true);
+SHARED_SHADOW_INVAL_TC(collapse_blockxfer_nofullmod, true, true, false);
+SHARED_SHADOW_INVAL_TC(collapse_blockxfer_fullmod, true, true, true);
+
+ATF_TP_ADD_TCS(tp)
+{
+	ATF_TP_ADD_TC(tp,
+	    shared_shadow_inval__nocollapse_noblockxfer_nofullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__nocollapse_noblockxfer_fullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__nocollapse_blockxfer_nofullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__nocollapse_blockxfer_fullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__collapse_noblockxfer_nofullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__collapse_noblockxfer_fullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__collapse_blockxfer_nofullmod);
+	ATF_TP_ADD_TC(tp, shared_shadow_inval__collapse_blockxfer_fullmod);
+
+	return atf_no_error();
+}
+#endif



Want to link to this message? Use this URL: <https://mail-archive.FreeBSD.org/cgi/mid.cgi?202107030028.1630SVmi096451>