Add a selftest to verify that converting a shared guest_memfd page to a
private page fails if the page has an elevated reference count.

When KVM converts a shared page to a private one, it expects the page to
have a reference count equal to the reference counts taken by the
filemap. If another kernel subsystem holds a reference to the page, for
example via pin_user_pages(), the conversion must be aborted.

This test uses the gup_test debugfs interface (which requires
CONFIG_GUP_TEST) to call pin_user_pages() on a specific page, artificially
increasing its reference count. It then attempts to convert a range of
pages, including the pinned page, from shared to private.

The test asserts that both bulk and single-page conversion attempts
correctly fail with EAGAIN for the pinned page. After the page is unpinned,
the test verifies that subsequent conversions succeed.

Signed-off-by: Ackerley Tng <[email protected]>
Co-developed-by: Sean Christopherson <[email protected]>
Signed-off-by: Sean Christopherson <[email protected]>
---
 .../kvm/guest_memfd_conversions_test.c        | 82 +++++++++++++++++++
 1 file changed, 82 insertions(+)

diff --git a/tools/testing/selftests/kvm/guest_memfd_conversions_test.c 
b/tools/testing/selftests/kvm/guest_memfd_conversions_test.c
index e6abf2d30c62d..856166f1b1dfc 100644
--- a/tools/testing/selftests/kvm/guest_memfd_conversions_test.c
+++ b/tools/testing/selftests/kvm/guest_memfd_conversions_test.c
@@ -14,6 +14,7 @@
 #include "kselftest_harness.h"
 #include "test_util.h"
 #include "ucall_common.h"
+#include "../../../../mm/gup_test.h"
 
 FIXTURE(gmem_conversions) {
        struct kvm_vcpu *vcpu;
@@ -404,6 +405,87 @@ GMEM_CONVERSION_TEST_INIT_SHARED(forked_accesses)
        kvm_munmap(test_state, sizeof(*test_state));
 }
 
+static int gup_test_fd;
+
+static void pin_pages(void *vaddr, uint64_t size)
+{
+       const struct pin_longterm_test args = {
+               .addr = (uint64_t)vaddr,
+               .size = size,
+               .flags = PIN_LONGTERM_TEST_FLAG_USE_WRITE,
+       };
+
+       gup_test_fd = open("/sys/kernel/debug/gup_test", O_RDWR);
+       TEST_REQUIRE(gup_test_fd >= 0);
+
+       TEST_ASSERT_EQ(ioctl(gup_test_fd, PIN_LONGTERM_TEST_START, &args), 0);
+}
+
+static void unpin_pages(void)
+{
+       if (gup_test_fd > 0)
+               TEST_ASSERT_EQ(ioctl(gup_test_fd, PIN_LONGTERM_TEST_STOP), 0);
+}
+
+static void test_convert_to_private_fails(test_data_t *t, loff_t pgoff,
+                                         size_t nr_pages,
+                                         loff_t expected_error_offset)
+{
+       loff_t offset = pgoff * page_size;
+       loff_t error_offset = -1ul;
+       int ret;
+
+       do {
+               ret = __gmem_set_private(t->gmem_fd, offset,
+                                        nr_pages * page_size, &error_offset);
+       } while (ret == -1 && errno == EINTR);
+       TEST_ASSERT(ret == -1 && errno == EAGAIN,
+                   "Wanted EAGAIN on page %lu, got %d (ret = %d)", pgoff,
+                   errno, ret);
+       TEST_ASSERT_EQ(error_offset, expected_error_offset);
+}
+
+/*
+ * This test depends on CONFIG_GUP_TEST to provide a kernel module that exposes
+ * pin_user_pages() to userspace.
+ */
+GMEM_CONVERSION_MULTIPAGE_TEST_INIT_SHARED(elevated_refcount, 4)
+{
+       int i;
+
+       pin_pages(t->mem + test_page * page_size, page_size);
+
+       for (i = 0; i < nr_pages; i++)
+               test_shared(t, i, 0, 'A', 'B');
+
+       /*
+        * Converting in bulk should fail as long any page in the range has
+        * unexpected refcounts.
+        */
+       test_convert_to_private_fails(t, 0, nr_pages, test_page * page_size);
+
+       for (i = 0; i < nr_pages; i++) {
+               /*
+                * Converting page-wise should also fail as long any page in the
+                * range has unexpected refcounts.
+                */
+               if (i == test_page)
+                       test_convert_to_private_fails(t, i, 1, test_page * 
page_size);
+               else
+                       test_convert_to_private(t, i, 'B', 'C');
+       }
+
+       unpin_pages();
+
+       gmem_set_private(t->gmem_fd, 0, nr_pages * page_size);
+
+       for (i = 0; i < nr_pages; i++) {
+               char expected = i == test_page ? 'B' : 'C';
+
+               test_private(t, i, expected, 'D');
+       }
+}
+
 int main(int argc, char *argv[])
 {
        TEST_REQUIRE(kvm_check_cap(KVM_CAP_VM_TYPES) & 
BIT(KVM_X86_SW_PROTECTED_VM));
-- 
2.51.0.858.gf9c4a03a3a-goog


Reply via email to