require quiesce for VFIO_USER_DIRTY_PAGES (#671)

If we require a quiesce for these calls, we can be sure that it will not race
with any usage of vfu_*_sg() calls, as a first step towards concurrency.

This is not ideal for VFIO_IOMMU_DIRTY_PAGES_FLAG_GET_BITMAP, which can
potentially be called multiple times during pre-copy phase, but that's something
we can fix later.

Signed-off-by: John Levon <john.levon@nutanix.com>
Reviewed-by: Thanos Makatos <thanos.makatos@nutanix.com>
diff --git a/lib/libvfio-user.c b/lib/libvfio-user.c
index a666316..924d109 100644
--- a/lib/libvfio-user.c
+++ b/lib/libvfio-user.c
@@ -1310,6 +1310,9 @@
     case VFIO_USER_DEVICE_RESET:
         return true;
 
+    case VFIO_USER_DIRTY_PAGES:
+        return true;
+
     case VFIO_USER_REGION_WRITE:
         if (msg->in.iov.iov_len < sizeof(*reg)) {
             /*
diff --git a/test/py/test_dirty_pages.py b/test/py/test_dirty_pages.py
index 4169ae5..8af325a 100644
--- a/test/py/test_dirty_pages.py
+++ b/test/py/test_dirty_pages.py
@@ -28,11 +28,13 @@
 #
 
 from libvfio_user import *
+import ctypes as c
 import errno
 import mmap
 import tempfile
 
 ctx = None
+quiesce_errno = 0
 
 
 @vfu_dma_register_cb_t
@@ -45,6 +47,14 @@
     return 0
 
 
+@vfu_device_quiesce_cb_t
+def quiesce_cb(ctx):
+    if quiesce_errno:
+        c.set_errno(errno.EBUSY)
+        return -1
+    return 0
+
+
 def test_dirty_pages_setup():
     global ctx, sock
 
@@ -54,6 +64,8 @@
     ret = vfu_pci_init(ctx)
     assert ret == 0
 
+    vfu_setup_device_quiesce_cb(ctx, quiesce_cb=quiesce_cb)
+
     ret = vfu_setup_device_dma(ctx, dma_register, dma_unregister)
     assert ret == 0
 
@@ -271,7 +283,13 @@
     assert br.bitmap.size == 8
 
 
-def get_dirty_page_bitmap():
+def get_dirty_page_bitmap_result(resp):
+    _, resp = vfio_user_dirty_pages.pop_from_buffer(resp)
+    _, resp = vfio_user_bitmap_range.pop_from_buffer(resp)
+    return struct.unpack("Q", resp)[0]
+
+
+def send_dirty_page_bitmap(busy=False):
     argsz = len(vfio_user_dirty_pages()) + len(vfio_user_bitmap_range()) + 8
 
     dirty_pages = vfio_user_dirty_pages(argsz=argsz,
@@ -281,11 +299,12 @@
 
     payload = bytes(dirty_pages) + bytes(br)
 
-    result = msg(ctx, sock, VFIO_USER_DIRTY_PAGES, payload)
+    return msg(ctx, sock, VFIO_USER_DIRTY_PAGES, payload, busy=busy)
 
-    dirty_pages, result = vfio_user_dirty_pages.pop_from_buffer(result)
-    br, result = vfio_user_bitmap_range.pop_from_buffer(result)
-    return struct.unpack("Q", result)[0]
+
+def get_dirty_page_bitmap():
+    result = send_dirty_page_bitmap()
+    return get_dirty_page_bitmap_result(result)
 
 
 sg3 = None
@@ -345,7 +364,7 @@
     vfu_unmap_sg(ctx, sg3, iovec3)
     assert get_dirty_page_bitmap() == 0b11110000
 
-    # bitmap should be clear after it was unmapped before previous reqeust for
+    # bitmap should be clear after it was unmapped before previous request for
     # dirty pages
     assert get_dirty_page_bitmap() == 0b00000000
 
@@ -354,6 +373,68 @@
     stop_logging()
 
 
+def test_dirty_pages_start_with_quiesce():
+    global quiesce_errno
+
+    quiesce_errno = errno.EBUSY
+
+    payload = vfio_user_dirty_pages(argsz=len(vfio_user_dirty_pages()),
+                                    flags=VFIO_IOMMU_DIRTY_PAGES_FLAG_START)
+
+    msg(ctx, sock, VFIO_USER_DIRTY_PAGES, payload, rsp=False, busy=True)
+
+    ret = vfu_device_quiesced(ctx, 0)
+    assert ret == 0
+
+    # now should be able to get the reply
+    get_reply(sock, expect=0)
+
+    quiesce_errno = 0
+
+
+def test_dirty_pages_bitmap_with_quiesce():
+    global quiesce_errno
+
+    quiesce_errno = errno.EBUSY
+
+    ret, sg1 = vfu_addr_to_sg(ctx, dma_addr=0x10000, length=0x1000)
+    assert ret == 1
+    iovec1 = iovec_t()
+    ret = vfu_map_sg(ctx, sg1, iovec1)
+    assert ret == 0
+
+    send_dirty_page_bitmap(busy=True)
+
+    ret = vfu_device_quiesced(ctx, 0)
+    assert ret == 0
+
+    # now should be able to get the reply
+    result = get_reply(sock, expect=0)
+    bitmap = get_dirty_page_bitmap_result(result)
+    assert bitmap == 0b00000001
+
+    quiesce_errno = 0
+
+
+def test_dirty_pages_stop_with_quiesce():
+    global quiesce_errno
+
+    quiesce_errno = errno.EBUSY
+
+    payload = vfio_user_dirty_pages(argsz=len(vfio_user_dirty_pages()),
+                                    flags=VFIO_IOMMU_DIRTY_PAGES_FLAG_STOP)
+
+    msg(ctx, sock, VFIO_USER_DIRTY_PAGES, payload, rsp=False, busy=True)
+
+    ret = vfu_device_quiesced(ctx, 0)
+    assert ret == 0
+
+    # now should be able to get the reply
+    get_reply(sock, expect=0)
+
+    quiesce_errno = 0
+
+
 def test_dirty_pages_cleanup():
     disconnect_client(ctx, sock)
     vfu_destroy_ctx(ctx)