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 ackerleytng@google.com Co-developed-by: Sean Christopherson seanjc@google.com Signed-off-by: Sean Christopherson seanjc@google.com --- .../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));