# # Copyright (c) 2021 Nutanix Inc. All rights reserved. # # Authors: John Levon # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * 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. # * Neither the name of Nutanix nor the names of its contributors may be # used to endorse or promote products derived from this software without # specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS 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 BE LIABLE FOR ANY # DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES # (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR # SERVICESLOSS 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. # from libvfio_user import * import ctypes as c import errno import mmap import tempfile ctx = None client = None quiesce_errno = 0 @vfu_dma_register_cb_t def dma_register(ctx, info): return 0 @vfu_dma_unregister_cb_t def dma_unregister(ctx, info): 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, client ctx = vfu_create_ctx(flags=LIBVFIO_USER_FLAG_ATTACH_NB) assert ctx is not None 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 ret = vfu_realize_ctx(ctx) assert ret == 0 client = connect_client(ctx) f = tempfile.TemporaryFile() f.truncate(0x10 << PAGE_SHIFT) payload = vfio_user_dma_map(argsz=len(vfio_user_dma_map()), flags=(VFIO_USER_F_DMA_REGION_READ | VFIO_USER_F_DMA_REGION_WRITE), offset=0, addr=0x10 << PAGE_SHIFT, size=0x20 << PAGE_SHIFT) msg(ctx, client.sock, VFIO_USER_DMA_MAP, payload, fds=[f.fileno()]) payload = vfio_user_dma_map(argsz=len(vfio_user_dma_map()), flags=(VFIO_USER_F_DMA_REGION_READ | VFIO_USER_F_DMA_REGION_WRITE), offset=0, addr=0x40 << PAGE_SHIFT, size=0x10 << PAGE_SHIFT) msg(ctx, client.sock, VFIO_USER_DMA_MAP, payload) def test_setup_migration(): ret = vfu_setup_device_migration_callbacks(ctx) assert ret == 0 def start_logging(addr=None, length=None, page_size=PAGE_SIZE, expect=0): """ Start logging dirty writes. If a region and page size are specified, they will be sent to the server to start logging. Otherwise, all regions will be logged and the default page size will be used. Note: in the current implementation, all regions are logged whether or not you specify a region, as the additional constraint of only logging a certain region is considered an optimisation and is not yet implemented. """ if addr is not None: ranges = vfio_user_device_feature_dma_logging_range( iova=addr, length=length ) num_ranges = 1 else: ranges = bytearray() num_ranges = 0 feature = vfio_user_device_feature( argsz=len(vfio_user_device_feature()) + len(vfio_user_device_feature_dma_logging_control()) + len(ranges), flags=VFIO_DEVICE_FEATURE_DMA_LOGGING_START | VFIO_DEVICE_FEATURE_SET) payload = vfio_user_device_feature_dma_logging_control( page_size=page_size, num_ranges=num_ranges, reserved=0) msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, bytes(feature) + bytes(payload) + bytes(ranges), expect=expect) def test_dirty_pages_start_zero_pgsize(): start_logging(page_size=0, expect=errno.EINVAL) def test_dirty_pages_start(): start_logging() # should be idempotent start_logging() def test_dirty_pages_start_different_pgsize(): """ Once we've started logging with page size PAGE_SIZE, any request to start logging at a different page size should be rejected. """ start_logging(page_size=PAGE_SIZE >> 1, expect=errno.EINVAL) start_logging(page_size=PAGE_SIZE << 1, expect=errno.EINVAL) def get_dirty_page_bitmap(addr=0x10 << PAGE_SHIFT, length=0x10 << PAGE_SHIFT, page_size=PAGE_SIZE, expect=0): """ Get the dirty page bitmap from the server for the given region and page size as a 64-bit integer. This function only works for bitmaps that fit within a 64-bit integer because that's what it returns. """ bitmap_size = get_bitmap_size(length, page_size) assert bitmap_size == 8 argsz = len(vfio_user_device_feature()) + \ len(vfio_user_device_feature_dma_logging_report()) + \ bitmap_size feature = vfio_user_device_feature( argsz=argsz, flags=VFIO_DEVICE_FEATURE_DMA_LOGGING_REPORT | VFIO_DEVICE_FEATURE_GET ) report = vfio_user_device_feature_dma_logging_report( iova=addr, length=length, page_size=page_size ) payload = bytes(feature) + bytes(report) result = msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, payload, expect=expect) if expect != 0: return assert len(result) == argsz _, result = vfio_user_device_feature.pop_from_buffer(result) _, result = \ vfio_user_device_feature_dma_logging_report.pop_from_buffer(result) assert len(result) == bitmap_size return struct.unpack("Q", result)[0] def test_dirty_pages_get_unmodified(): bitmap = get_dirty_page_bitmap() assert bitmap == 0 sg3 = None iovec3 = None def write_to_page(ctx, page, nr_pages, get_bitmap=True): """Simulate a write to the given address and size.""" ret, sg = vfu_addr_to_sgl(ctx, dma_addr=page << PAGE_SHIFT, length=nr_pages << PAGE_SHIFT) assert ret == 1 iovec = iovec_t() ret = vfu_sgl_get(ctx, sg, iovec) assert ret == 0 vfu_sgl_put(ctx, sg, iovec) if get_bitmap: return get_dirty_page_bitmap() return None def test_dirty_pages_get_modified(): ret, sg1 = vfu_addr_to_sgl(ctx, dma_addr=0x10 << PAGE_SHIFT, length=PAGE_SIZE) assert ret == 1 iovec1 = iovec_t() ret = vfu_sgl_get(ctx, sg1, iovec1) assert ret == 0 # read only ret, sg2 = vfu_addr_to_sgl(ctx, dma_addr=0x11 << PAGE_SHIFT, length=PAGE_SIZE, prot=mmap.PROT_READ) assert ret == 1 iovec2 = iovec_t() ret = vfu_sgl_get(ctx, sg2, iovec2) assert ret == 0 # simple single bitmap entry map ret, sg3 = vfu_addr_to_sgl(ctx, dma_addr=0x12 << PAGE_SHIFT, length=PAGE_SIZE) assert ret == 1 iovec3 = iovec_t() ret = vfu_sgl_get(ctx, sg3, iovec3) assert ret == 0 # write that spans bytes in bitmap ret, sg4 = vfu_addr_to_sgl(ctx, dma_addr=0x16 << PAGE_SHIFT, length=0x4 << PAGE_SHIFT) assert ret == 1 iovec4 = iovec_t() ret = vfu_sgl_get(ctx, sg4, iovec4) assert ret == 0 # not put yet, dirty bitmap should be zero bitmap = get_dirty_page_bitmap() assert bitmap == 0b0000000000000000 # put SGLs, dirty bitmap should be updated vfu_sgl_put(ctx, sg1, iovec1) vfu_sgl_put(ctx, sg4, iovec4) bitmap = get_dirty_page_bitmap() assert bitmap == 0b0000001111000001 # check dirty bitmap is correctly extended when we give a smaller page size vfu_sgl_put(ctx, sg1, iovec1) vfu_sgl_put(ctx, sg4, iovec4) bitmap = get_dirty_page_bitmap(page_size=PAGE_SIZE >> 1) assert bitmap == 0b00000000000011111111000000000011 # check dirty bitmap is correctly shortened when we give a larger page size vfu_sgl_put(ctx, sg1, iovec1) vfu_sgl_put(ctx, sg4, iovec4) bitmap = get_dirty_page_bitmap(page_size=PAGE_SIZE << 1) assert bitmap == 0b00011001 # check dirty bitmap is correctly shortened when we give a page size that # is so large that one bit corresponds to multiple bytes in the raw bitmap vfu_sgl_put(ctx, sg1, iovec1) vfu_sgl_put(ctx, sg4, iovec4) bitmap = get_dirty_page_bitmap(page_size=PAGE_SIZE << 4) assert bitmap == 0b1 bitmap = get_dirty_page_bitmap(page_size=PAGE_SIZE << 4) assert bitmap == 0b0 # after another two puts, should just be one dirty page vfu_sgl_put(ctx, sg2, iovec2) vfu_sgl_put(ctx, sg3, iovec3) bitmap = get_dirty_page_bitmap() assert bitmap == 0b0000000000000100 # and should now be clear bitmap = get_dirty_page_bitmap() assert bitmap == 0b0000000000000000 # # check various edge cases of bitmap values. # # very first bit bitmap = write_to_page(ctx, 0x10, 1) assert bitmap == 0b0000000000000001 # top bit of first byte bitmap = write_to_page(ctx, 0x17, 1) assert bitmap == 0b0000000010000000 # all bits except top one of first byte bitmap = write_to_page(ctx, 0x10, 7) assert bitmap == 0b0000000001111111 # all bits of first byte bitmap = write_to_page(ctx, 0x10, 8) assert bitmap == 0b0000000011111111 # all bits of first byte plus bottom bit of next bitmap = write_to_page(ctx, 0x10, 9) assert bitmap == 0b0000000111111111 # straddle top/bottom bit bitmap = write_to_page(ctx, 0x17, 2) assert bitmap == 0b0000000110000000 # top bit of second byte bitmap = write_to_page(ctx, 0x1f, 1) assert bitmap == 0b1000000000000000 # top bit of third byte bitmap = write_to_page(ctx, 0x27, 1) assert bitmap == 0b100000000000000000000000 # bits in third and first byte write_to_page(ctx, 0x26, 1, get_bitmap=False) write_to_page(ctx, 0x12, 2, get_bitmap=False) bitmap = get_dirty_page_bitmap() assert bitmap == 0b010000000000000000001100 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, expect=errno.ENOTSUP) # 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 stop_logging(addr=None, length=None): if addr is not None: ranges = vfio_user_device_feature_dma_logging_range( iova=addr, length=length ) else: ranges = [] feature = vfio_user_device_feature( argsz=len(vfio_user_device_feature()) + len(vfio_user_device_feature_dma_logging_control()) + len(ranges), flags=VFIO_DEVICE_FEATURE_DMA_LOGGING_STOP | VFIO_DEVICE_FEATURE_SET) payload = vfio_user_device_feature_dma_logging_control( page_size=PAGE_SIZE, num_ranges=(1 if addr is not None else 0), reserved=0) msg(ctx, client.sock, VFIO_USER_DEVICE_FEATURE, bytes(feature) + bytes(payload) + bytes(ranges)) def test_dirty_pages_stop(): stop_logging() def test_dirty_pages_cleanup(): client.disconnect(ctx) vfu_destroy_ctx(ctx) def test_dirty_pages_uninitialised_dma(): global ctx, client ctx = vfu_create_ctx(flags=LIBVFIO_USER_FLAG_ATTACH_NB) assert ctx is not None ret = vfu_pci_init(ctx) assert ret == 0 vfu_setup_device_quiesce_cb(ctx, quiesce_cb=quiesce_cb) ret = vfu_realize_ctx(ctx) assert ret == 0 client = connect_client(ctx) start_logging(expect=errno.EINVAL) get_dirty_page_bitmap(expect=errno.EINVAL) client.disconnect(ctx) vfu_destroy_ctx(ctx) # ex: set tabstop=4 shiftwidth=4 softtabstop=4 expandtab: