dma: return empty dirty bitmap for unmapped or message-based regions (#877)

To align with kernel VFIO uAPI behavior, the server should not fail the
migration when asked to report dirty pages for unmapped or non-FD DMA
regions.

For regions accessed via messages (REGION_ACCESS_MODE_MSG) or unmapped
firmware/MMIO regions, the client physically executes the memory writes
and natively tracks them in its own dirty bitmap.

Return an empty (zeroed) bitmap instead of -EINVAL for these regions
so the client migration does not crash.

Signed-off-by: Hugo Komatsu <hugo.komatsu@nutanix.com>
diff --git a/lib/dma.c b/lib/dma.c
index c0bea6d..087dbf6 100644
--- a/lib/dma.c
+++ b/lib/dma.c
@@ -730,10 +730,12 @@
      */
     ret = dma_addr_to_sgl(dma, addr, len, &sg, 1, PROT_NONE);
     if (unlikely(ret != 1)) {
-        vfu_log(dma->vfu_ctx, LOG_DEBUG, "failed to translate %#llx-%#llx: %m",
-                (unsigned long long)(uintptr_t)addr,
-		(unsigned long long)(uintptr_t)addr + len - 1);
-        return ret;
+        /*
+         * Unmapped region (e.g. firmware or MMIO). The client tracks any 
+         * message-based DMA that might occur here. Return an empty bitmap.
+         */
+        memset(bitmap, 0, size);
+        return 0;
     }
 
     if (unlikely(sg.dma_addr != addr || sg.length != len)) {
@@ -780,9 +782,12 @@
     region = sg.region;
 
     if (region->access_mode == REGION_ACCESS_MODE_MSG) {
-        vfu_log(dma->vfu_ctx, LOG_ERR, "region [%p-%p] isn't accessed directly",
-                region->info.iova.iov_base, iov_end(&region->info.iova));
-        return ERROR_INT(EINVAL);
+        /*
+         * The client automatically tracks dirty pages for message-based DMA
+         * operations, so we safely return an empty bitmap here.
+         */
+        memset(bitmap, 0, size);
+        return 0;
     }
 
     if (client_pgsize == dma->dirty_pgsize) {
diff --git a/test/py/test_dirty_pages.py b/test/py/test_dirty_pages.py
index b4f2468..aad2c77 100644
--- a/test/py/test_dirty_pages.py
+++ b/test/py/test_dirty_pages.py
@@ -341,9 +341,6 @@
 
 
 def test_dirty_pages_invalid_arguments():
-    # Failed to translate
-    get_dirty_page_bitmap(addr=0xdeadbeef, expect=errno.ENOENT)
-
     # Does not exactly match a region (libvfio-user limitation)
     get_dirty_page_bitmap(addr=(0x10 << PAGE_SHIFT) + 1,
                           length=(0x20 << PAGE_SHIFT) - 1,
@@ -352,8 +349,19 @@
     # Invalid requested bitmap size
     get_dirty_page_bitmap(page_size=1 << 24, expect=errno.EINVAL)
 
-    # Region not mapped
-    get_dirty_page_bitmap(addr=0x40 << PAGE_SHIFT, expect=errno.EINVAL)
+
+def test_dirty_pages_unmapped_and_msg_regions():
+    """
+    Regions that are unmapped or accessed via message-based DMA should
+    return success and an empty bitmap (all zeroes).
+    """
+    # Unmapped region
+    bitmap = get_dirty_page_bitmap(addr=0xdeadbeef, expect=0)
+    assert bitmap == 0
+
+    # Message-based DMA region
+    bitmap = get_dirty_page_bitmap(addr=0x40 << PAGE_SHIFT, expect=0)
+    assert bitmap == 0
 
 
 def stop_logging(addr=None, length=None):
@@ -479,10 +487,11 @@
         addr=0x10 << PAGE_SHIFT, size=0x10 << PAGE_SHIFT)
     msg(ctx, client.sock, VFIO_USER_DMA_UNMAP, payload)
 
-    # Verify first region is actually unmapped (should fail)
-    get_dirty_page_bitmap(addr=0x10 << PAGE_SHIFT,
-                          length=0x10 << PAGE_SHIFT,
-                          expect=errno.ENOENT)
+    # Verify first region is actually unmapped (returns empty bitmap)
+    bitmap = get_dirty_page_bitmap(addr=0x10 << PAGE_SHIFT,
+                                   length=0x10 << PAGE_SHIFT,
+                                   expect=0)
+    assert bitmap == 0
 
     # Verify second region still works and dirty bit is correctly set
     bitmap = get_dirty_page_bitmap(addr=0x30 << PAGE_SHIFT,