diff mbox series

[RFC,v2,5/9] igb: Add dirty page tracking for IGBVF migration

Message ID 20260902192054.3329753-6-clg@redhat.com
State New
Headers show
Series igb: Add experimental VF live migration support | expand

Commit Message

Cédric Le Goater Sept. 2, 2026, 7:20 p.m. UTC
Add per-VF dirty page tracking using bitmaps allocated per IOVA range.
The DMA data path is instrumented via igb_core_dirty_track_dma() calls at all
five DMA write sites: TX descriptor writeback (head pointer and status),
RX descriptor writeback, RX header fragment, and RX payload fragment.

The migration DVSEC exposes DIRTY_ENABLE, DIRTY_DISABLE and
DIRTY_QUERY commands. Range parameters and query results are exchanged
through the shared DMA buffer.

Add the PRE_COPY device state to support pre-copy live migration with
concurrent dirty tracking. PRE_COPY transitions:
 - RUNNING -> PRE_COPY
 - PRE_COPY -> STOP, RUNNING, STOP_COPY

Dirty tracking is automatically disabled when leaving PRE_COPY or
STOP_COPY.

AI-used-for: code (prototype)
Signed-off-by: Cédric Le Goater <clg@redhat.com>
---
 hw/net/igb_core.h      |   4 +
 hw/net/igb_migration.h |  65 +++++++-
 hw/net/igb_core.c      |  42 ++++--
 hw/net/igb_migration.c | 326 ++++++++++++++++++++++++++++++++++++++++-
 hw/net/trace-events    |   5 +
 5 files changed, 422 insertions(+), 20 deletions(-)
diff mbox series

Patch

diff --git a/hw/net/igb_core.h b/hw/net/igb_core.h
index 22e10e4e6d0b..7fdc41690b36 100644
--- a/hw/net/igb_core.h
+++ b/hw/net/igb_core.h
@@ -40,6 +40,8 @@ 
 #ifndef HW_NET_IGB_CORE_H
 #define HW_NET_IGB_CORE_H
 
+#include "igb_migration.h"
+
 #define E1000E_MAC_SIZE         (0x8000)
 #define IGB_EEPROM_SIZE         (1024)
 
@@ -99,6 +101,8 @@  struct IGBCore {
     void (*owner_start_recv)(PCIDevice *d);
 
     int64_t timadj;
+
+    IGBVfDirtyState vf_dirty[IGB_MAX_VF_FUNCTIONS];
 };
 
 void
diff --git a/hw/net/igb_migration.h b/hw/net/igb_migration.h
index b2f601e74346..996de2d2f50b 100644
--- a/hw/net/igb_migration.h
+++ b/hw/net/igb_migration.h
@@ -21,7 +21,8 @@ 
  *   +0x04  DVSEC header 1             (len | rev | vendor_id)
  *   +0x08  DVSEC header 2             (DVSEC ID)
  *   +0x0A  Reserved                   (padding for DWORD alignment)
- *   +0x0C  CAPS                       (RO: F_STATE[0])
+ *   +0x0C  CAPS                       (RO: F_STATE[0], F_DIRTY[1],
+ *                                      max_ranges[11:8], pgsize[16:12])
  *   +0x10  CTRL                       (WO: doorbell command)
  *   +0x14  STATUS                     (RO: state[7:0], error_code[15:8])
  *   +0x18  BUF_ADDR_LO                (RW: shared buffer GPA low)
@@ -44,6 +45,12 @@ 
 
 /* CAPS register layout */
 #define IGB_MIG_CAP_F_STATE             (1u << 0)
+#define IGB_MIG_CAP_F_DIRTY             (1u << 1)
+#define IGB_MIG_CAPS_MAX_RANGES_SHIFT   8
+#define IGB_MIG_CAPS_MAX_RANGES         4
+#define IGB_MIG_CAPS_PGSIZE_SHIFT       12
+#define IGB_MIG_CAPS_PGSIZE_4K          (1u << 12)
+#define IGB_MIG_CAPS_PGSIZE_64K         (1u << 16)
 
 /* CTRL register: command in [7:0] */
 #define IGB_MIG_CTRL_CMD_MASK           0xFF
@@ -53,6 +60,9 @@ 
 #define IGB_MIG_CMD_SET_STATE           1
 #define IGB_MIG_CMD_SAVE                2
 #define IGB_MIG_CMD_LOAD                3
+#define IGB_MIG_CMD_DIRTY_ENABLE        4
+#define IGB_MIG_CMD_DIRTY_DISABLE       5
+#define IGB_MIG_CMD_DIRTY_QUERY         6
 
 /* STATUS register: state in [7:0], error code [15:8] */
 #define IGB_MIG_STATUS_STATE_MASK       0xFF
@@ -66,6 +76,7 @@ 
 #define IGB_MIG_STATE_RUNNING           2
 #define IGB_MIG_STATE_STOP_COPY         3
 #define IGB_MIG_STATE_RESUMING          4
+#define IGB_MIG_STATE_PRE_COPY          5
 
 /* Error codes */
 #define IGB_MIG_ERR_UNK_CMD             1
@@ -75,10 +86,29 @@ 
 #define IGB_MIG_ERR_BAD_SIZE            5
 #define IGB_MIG_ERR_BAD_MAGIC           6
 #define IGB_MIG_ERR_BAD_VERSION         7
+#define IGB_MIG_ERR_TOO_MANY_RANGES     8
+#define IGB_MIG_ERR_BAD_RANGE           9
+#define IGB_MIG_ERR_BAD_PGSIZE          10
+#define IGB_MIG_ERR_NOT_ENABLED         11
 
 /* Shared buffer constants */
 #define IGB_VF_STATE_MAX_SIZE           4096
 
+#define IGB_MIG_DIRTY_DEFAULT_PGSIZE        4096
+
+typedef struct IGBVfDirtyRange {
+    uint64_t iova;
+    uint64_t size;
+    uint64_t page_size;
+    unsigned long *bitmap;
+    uint64_t nbits;
+} IGBVfDirtyRange;
+
+typedef struct IGBVfDirtyState {
+    IGBVfDirtyRange ranges[IGB_MIG_CAPS_MAX_RANGES];
+    uint32_t num_ranges;
+} IGBVfDirtyState;
+
 typedef struct IgbVfMigState {
     uint32_t mig_state;
     uint32_t mig_data[IGB_VF_STATE_MAX_SIZE / sizeof(uint32_t)];
@@ -86,6 +116,36 @@  typedef struct IgbVfMigState {
     uint64_t mig_data_buf_addr;
 } IgbVfMigState;
 
+/*
+ * DMA buffer layouts for dirty tracking commands.
+ *
+ * DIRTY_ENABLE: driver writes igb_mig_dirty_enable_req to buffer
+ *               before cmd.
+ * DIRTY_QUERY: driver writes iova/size fields, device writes
+ *              response + bitmap.
+ */
+struct igb_mig_dirty_enable_req {
+    uint32_t len;
+    uint32_t flags;
+    uint64_t pgsize;
+    uint64_t range_iova;
+    uint64_t range_size;
+    uint32_t reserved[4];
+};
+
+struct igb_mig_dirty_query {
+    uint32_t len;
+    uint32_t flags;
+    uint64_t iova;
+    uint64_t size;
+    uint32_t bitmap_size;
+    uint32_t dirty_page_count;
+    uint64_t dma_writes;
+    uint32_t reserved[6];
+    uint8_t bitmap[];
+};
+
+typedef struct IGBCore IGBCore;
 typedef struct IgbVfState IgbVfState;
 
 bool igbvf_add_migration_dvsec(PCIDevice *dev, Error **errp);
@@ -94,4 +154,7 @@  uint32_t igbvf_mig_config_read(IgbVfState *s, uint32_t addr, int size);
 bool igbvf_mig_config_write(IgbVfState *s, uint32_t addr, uint32_t val,
                             int size);
 
+void igb_core_dirty_track_dma(IGBCore *core, int vfn,
+                              dma_addr_t addr, dma_addr_t len);
+
 #endif
diff --git a/hw/net/igb_core.c b/hw/net/igb_core.c
index 01745fe756d0..12329a1aff58 100644
--- a/hw/net/igb_core.c
+++ b/hw/net/igb_core.c
@@ -824,6 +824,16 @@  igb_rx_ring_init(IGBCore *core, E1000E_RxRing *rxr, int idx)
     rxr->i      = &i[idx];
 }
 
+static inline void
+igb_pci_dma_write(IGBCore *core, PCIDevice *dev,
+                  dma_addr_t addr, const void *buf, dma_addr_t len)
+{
+    pci_dma_write(dev, addr, buf, len);
+    if (pci_is_vf(dev)) {
+        igb_core_dirty_track_dma(core, pcie_sriov_vf_number(dev), addr, len);
+    }
+}
+
 static uint32_t
 igb_txdesc_writeback(IGBCore *core, dma_addr_t base,
                      union e1000_adv_tx_desc *tx_desc,
@@ -847,13 +857,15 @@  igb_txdesc_writeback(IGBCore *core, dma_addr_t base,
 
     if (tdwba & 1) {
         uint32_t buffer = cpu_to_le32(core->mac[txi->dh]);
-        pci_dma_write(d, tdwba & ~3, &buffer, sizeof(buffer));
+        igb_pci_dma_write(core, d,
+                          tdwba & ~3, &buffer, sizeof(buffer));
     } else {
         uint32_t status = le32_to_cpu(tx_desc->wb.status) | E1000_TXD_STAT_DD;
 
         tx_desc->wb.status = cpu_to_le32(status);
-        pci_dma_write(d, base + offsetof(union e1000_adv_tx_desc, wb),
-            &tx_desc->wb, sizeof(tx_desc->wb));
+        igb_pci_dma_write(core, d,
+                          base + offsetof(union e1000_adv_tx_desc, wb),
+                          &tx_desc->wb, sizeof(tx_desc->wb));
     }
 
     return igb_tx_wb_eic(core, txi->idx);
@@ -1598,11 +1610,12 @@  igb_pci_dma_write_rx_desc(IGBCore *core, PCIDevice *dev, dma_addr_t addr,
         uint8_t status = d->status;
 
         d->status &= ~E1000_RXD_STAT_DD;
-        pci_dma_write(dev, addr, desc, len);
+        igb_pci_dma_write(core, dev, addr, desc, len);
 
         if (status & E1000_RXD_STAT_DD) {
             d->status = status;
-            pci_dma_write(dev, addr + offset, &status, sizeof(status));
+            igb_pci_dma_write(core, dev,
+                              addr + offset, &status, sizeof(status));
         }
     } else {
         union e1000_adv_rx_desc *d = &desc->adv;
@@ -1611,11 +1624,12 @@  igb_pci_dma_write_rx_desc(IGBCore *core, PCIDevice *dev, dma_addr_t addr,
         uint32_t status = d->wb.upper.status_error;
 
         d->wb.upper.status_error &= ~E1000_RXD_STAT_DD;
-        pci_dma_write(dev, addr, desc, len);
+        igb_pci_dma_write(core, dev, addr, desc, len);
 
         if (status & E1000_RXD_STAT_DD) {
             d->wb.upper.status_error = status;
-            pci_dma_write(dev, addr + offset, &status, sizeof(status));
+            igb_pci_dma_write(core, dev,
+                              addr + offset, &status, sizeof(status));
         }
     }
 }
@@ -1737,9 +1751,9 @@  igb_write_hdr_frag_to_rx_buffers(IGBCore *core,
 {
     assert(data_len <= pdma_st->rx_desc_header_buf_size -
                        pdma_st->bastate.written[0]);
-    pci_dma_write(d,
-                  pdma_st->ba[0] + pdma_st->bastate.written[0],
-                  data, data_len);
+    igb_pci_dma_write(core, d,
+                      pdma_st->ba[0] + pdma_st->bastate.written[0],
+                      data, data_len);
     pdma_st->bastate.written[0] += data_len;
     pdma_st->bastate.cur_idx = 1;
 }
@@ -1804,10 +1818,10 @@  igb_write_payload_frag_to_rx_buffers(IGBCore *core,
             data,
             bytes_to_write);
 
-        pci_dma_write(d,
-                      pdma_st->ba[pdma_st->bastate.cur_idx] +
-                      pdma_st->bastate.written[pdma_st->bastate.cur_idx],
-                      data, bytes_to_write);
+        igb_pci_dma_write(core, d,
+                          pdma_st->ba[pdma_st->bastate.cur_idx] +
+                          pdma_st->bastate.written[pdma_st->bastate.cur_idx],
+                          data, bytes_to_write);
 
         pdma_st->bastate.written[pdma_st->bastate.cur_idx] += bytes_to_write;
         data += bytes_to_write;
diff --git a/hw/net/igb_migration.c b/hw/net/igb_migration.c
index af7257ccc4fd..cfa73673e39a 100644
--- a/hw/net/igb_migration.c
+++ b/hw/net/igb_migration.c
@@ -8,6 +8,8 @@ 
 
 #include "qemu/osdep.h"
 #include "qemu/log.h"
+#include "qemu/bitmap.h"
+#include "qemu/units.h"
 #include "hw/pci/pci_device.h"
 #include "hw/pci/pcie.h"
 #include "net/eth.h"
@@ -400,6 +402,285 @@  static int igbvf_mig_load(IgbVfState *s, const void *buf, size_t size)
     return 0;
 }
 
+/*
+ * Per-VF dirty page tracking
+ *
+ * All VF DMA writes in igb_core.c go through igb_pci_dma_write(),
+ * which calls igb_core_dirty_track_dma() to mark the target page in a
+ * per-range bitmap before performing the actual DMA.
+ *
+ * The IGBCore::vf_dirty[] bitmaps live in IGBCore so they are easily
+ * accessible from the core TX and RX paths without reaching back into
+ * VF state.
+ */
+
+void igb_core_dirty_track_dma(IGBCore *core, int vfn,
+                              dma_addr_t addr, dma_addr_t len)
+{
+    IGBVfDirtyState *ds = &core->vf_dirty[vfn];
+    bool matched = false;
+    uint32_t i;
+
+    if (!ds->num_ranges) {
+        return;
+    }
+
+    trace_igb_core_dirty_track_dma(vfn, addr, len);
+
+    for (i = 0; i < ds->num_ranges; i++) {
+        IGBVfDirtyRange *r = &ds->ranges[i];
+        uint64_t r_end = r->iova + r->size;
+        uint64_t dma_end = addr + len;
+        uint64_t start, end, start_page, end_page, page;
+
+        if (addr >= r_end || dma_end <= r->iova) {
+            continue;
+        }
+
+        matched = true;
+        start = MAX(addr, r->iova);
+        end = MIN(dma_end, r_end);
+
+        start_page = (start - r->iova) / r->page_size;
+        end_page = (end - 1 - r->iova) / r->page_size;
+
+        for (page = start_page; page <= end_page; page++) {
+            if (page < r->nbits) {
+                set_bit(page, r->bitmap);
+            }
+        }
+    }
+
+    if (!matched) {
+        trace_igb_core_dirty_track_dma_drop(vfn, addr, len);
+    }
+}
+
+static IGBVfDirtyState *igb_core_vf_dirty_state(IgbVfState *s)
+{
+    IGBCore *core = igbvf_get_core(s);
+    return &core->vf_dirty[s->vfn];
+}
+
+#define IGB_MIG_DIRTY_MAX_PAGES      ((256ULL * GiB) / (4 * KiB))
+
+static uint32_t igb_core_vf_dirty_enable(IgbVfState *s, uint64_t pgsize,
+                                         uint64_t range_iova,
+                                         uint64_t range_size)
+{
+    uint32_t caps = pci_get_long(PCI_DEVICE(s)->config +
+                                 IGB_MIG_DVSEC_OFFSET + IGB_MIG_CAPS);
+    IGBVfDirtyState *ds = igb_core_vf_dirty_state(s);
+    IGBVfDirtyRange *r;
+
+    if (ds->num_ranges >= IGB_MIG_CAPS_MAX_RANGES) {
+        return IGB_MIG_ERR_TOO_MANY_RANGES;
+    }
+
+    if (!range_size) {
+        return IGB_MIG_ERR_BAD_RANGE;
+    }
+
+    /* Validate page size against CAPS supported page size bitmask */
+    if (!is_power_of_2(pgsize) || !(pgsize & caps)) {
+        return IGB_MIG_ERR_BAD_PGSIZE;
+    }
+
+    if ((range_iova % pgsize) || (range_size % pgsize)) {
+        return IGB_MIG_ERR_BAD_PGSIZE;
+    }
+
+    if (range_size / pgsize > IGB_MIG_DIRTY_MAX_PAGES) {
+        return IGB_MIG_ERR_BAD_RANGE;
+    }
+
+    r = &ds->ranges[ds->num_ranges];
+    r->iova = range_iova;
+    r->size = range_size;
+    r->page_size = pgsize;
+    r->nbits = range_size / pgsize;
+    r->bitmap = bitmap_new(r->nbits);
+    ds->num_ranges++;
+    return 0;
+}
+
+static void igb_core_vf_dirty_disable(IgbVfState *s)
+{
+    IGBVfDirtyState *ds = igb_core_vf_dirty_state(s);
+    uint32_t i;
+
+    for (i = 0; i < ds->num_ranges; i++) {
+        IGBVfDirtyRange *r = &ds->ranges[i];
+
+        g_free(r->bitmap);
+        r->bitmap = NULL;
+        r->nbits = 0;
+    }
+    ds->num_ranges = 0;
+    trace_igbvf_mig_dirty_disable(s->vfn);
+}
+
+static bool igb_core_vf_dirty_enabled(IgbVfState *s)
+{
+    return igb_core_vf_dirty_state(s)->num_ranges > 0;
+}
+
+static void igb_core_vf_dirty_query(IGBVfDirtyRange *r,
+                                    uint64_t range_iova, uint64_t range_size,
+                                    void *buf, size_t buf_size,
+                                    size_t *out_size)
+{
+    uint64_t start_page = (range_iova - r->iova) / r->page_size;
+    uint64_t range_pages = range_size / r->page_size;
+    uint64_t count = MIN(range_pages, (uint64_t)buf_size * 8);
+
+    memset(buf, 0, buf_size);
+
+    if (start_page < r->nbits) {
+        uint64_t avail = r->nbits - start_page;
+        uint64_t n = MIN(count, avail);
+
+        bitmap_copy_with_src_offset(buf, r->bitmap, start_page, n);
+    }
+    *out_size = bitmap_empty(buf, count) ? 0 : DIV_ROUND_UP(count, 8);
+}
+
+static void igb_core_vf_dirty_query_commit(IGBVfDirtyRange *r,
+                                           uint64_t range_iova,
+                                           uint64_t range_size)
+{
+    uint64_t start_page = (range_iova - r->iova) / r->page_size;
+
+    if (start_page < r->nbits) {
+        uint64_t avail = r->nbits - start_page;
+        uint64_t range_pages = range_size / r->page_size;
+        uint64_t n = MIN(range_pages, avail);
+
+        bitmap_clear(r->bitmap, start_page, n);
+    }
+}
+
+static uint8_t igbvf_mig_cmd_dirty_enable(IgbVfState *s)
+{
+    IgbVfMigState *ms = &s->mig;
+    struct igb_mig_dirty_enable_req req;
+    MemTxResult r;
+    uint32_t status;
+
+    if (!ms->mig_data_buf_addr) {
+        return IGB_MIG_ERR_NO_BUFFER;
+    }
+
+    r = address_space_read(&address_space_memory, ms->mig_data_buf_addr,
+                           MEMTXATTRS_UNSPECIFIED, &req, sizeof(req));
+    if (r != MEMTX_OK) {
+        return IGB_MIG_ERR_DMA_FAILED;
+    }
+
+    /* TODO: validate req.len and req.flags */
+
+    status = igb_core_vf_dirty_enable(s, le64_to_cpu(req.pgsize),
+                                      le64_to_cpu(req.range_iova),
+                                      le64_to_cpu(req.range_size));
+    if (status != 0) {
+        return status;
+    }
+    trace_igbvf_mig_dirty_enable(s->vfn, le64_to_cpu(req.pgsize),
+                                 le64_to_cpu(req.range_size) /
+                                 le64_to_cpu(req.pgsize));
+    return 0;
+}
+
+static IGBVfDirtyRange *igb_core_vf_dirty_range_valid(IGBVfDirtyState *ds,
+                                                      uint64_t range_iova,
+                                                      uint64_t range_size)
+{
+    if (!range_size) {
+        return NULL;
+    }
+
+    for (uint32_t i = 0; i < ds->num_ranges; i++) {
+        IGBVfDirtyRange *r = &ds->ranges[i];
+
+        if (range_iova >= r->iova &&
+            range_iova + range_size <= r->iova + r->size &&
+            QEMU_IS_ALIGNED(range_iova, r->page_size) &&
+            QEMU_IS_ALIGNED(range_size, r->page_size)) {
+            return r;
+        }
+    }
+    return NULL;
+}
+
+static uint8_t igbvf_mig_cmd_dirty_query(IgbVfState *s)
+{
+    IgbVfMigState *ms = &s->mig;
+    IGBVfDirtyState *ds = &igbvf_get_core(s)->vf_dirty[s->vfn];
+    uint64_t buf_addr = ms->mig_data_buf_addr;
+    uint64_t range_iova = 0, range_size = 0;
+    IGBVfDirtyRange *range;
+    uint32_t bmp_bytes, dirty_pages;
+    uint32_t val32;
+    size_t out_size;
+    g_autofree void *bitmap = NULL;
+
+    if (!buf_addr) {
+        return IGB_MIG_ERR_NO_BUFFER;
+    }
+
+    if (!igb_core_vf_dirty_enabled(s)) {
+        return IGB_MIG_ERR_NOT_ENABLED;
+    }
+
+    address_space_read(&address_space_memory,
+                       buf_addr + offsetof(struct igb_mig_dirty_query, iova),
+                       MEMTXATTRS_UNSPECIFIED, &range_iova, sizeof(range_iova));
+    range_iova = le64_to_cpu(range_iova);
+    address_space_read(&address_space_memory,
+                       buf_addr + offsetof(struct igb_mig_dirty_query, size),
+                       MEMTXATTRS_UNSPECIFIED, &range_size, sizeof(range_size));
+    range_size = le64_to_cpu(range_size);
+
+    range = igb_core_vf_dirty_range_valid(ds, range_iova, range_size);
+    if (!range) {
+        return IGB_MIG_ERR_BAD_RANGE;
+    }
+
+    bmp_bytes = BITS_TO_LONGS(range_size / range->page_size) *
+        sizeof(unsigned long);
+    bitmap = g_malloc0(bmp_bytes);
+
+    igb_core_vf_dirty_query(range, range_iova, range_size,
+                            bitmap, bmp_bytes, &out_size);
+
+    if (out_size) {
+        if (address_space_write(&address_space_memory,
+                                buf_addr +
+                                offsetof(struct igb_mig_dirty_query, bitmap),
+                                MEMTXATTRS_UNSPECIFIED, bitmap, out_size)) {
+            return IGB_MIG_ERR_DMA_FAILED;
+        }
+    }
+
+    igb_core_vf_dirty_query_commit(range, range_iova, range_size);
+
+    dirty_pages = bitmap_count_one(bitmap, range_size / range->page_size);
+
+    val32 = cpu_to_le32(out_size);
+    address_space_write(&address_space_memory,
+                        buf_addr + offsetof(struct igb_mig_dirty_query,
+                                            bitmap_size),
+                        MEMTXATTRS_UNSPECIFIED, &val32, sizeof(val32));
+    val32 = cpu_to_le32(dirty_pages);
+    address_space_write(&address_space_memory,
+                        buf_addr + offsetof(struct igb_mig_dirty_query,
+                                            dirty_page_count),
+                        MEMTXATTRS_UNSPECIFIED, &val32, sizeof(val32));
+
+    trace_igbvf_mig_dirty_query(s->vfn, (uint64_t)out_size, dirty_pages);
+    return 0;
+}
+
 /*
  * Migration command handlers
  */
@@ -419,7 +700,8 @@  static uint8_t igbvf_mig_cmd_save(IgbVfState *s)
     MemTxResult r;
     int ret;
 
-    if (ms->mig_state != IGB_MIG_STATE_STOP_COPY) {
+    if (ms->mig_state != IGB_MIG_STATE_STOP_COPY &&
+        ms->mig_state != IGB_MIG_STATE_PRE_COPY) {
         return IGB_MIG_ERR_BAD_STATE;
     }
 
@@ -487,22 +769,33 @@  static uint8_t igbvf_mig_set_state(IgbVfState *s, uint32_t new_state)
     case IGB_MIG_STATE_STOP:
         if (old != IGB_MIG_STATE_RUNNING &&
             old != IGB_MIG_STATE_STOP_COPY &&
+            old != IGB_MIG_STATE_PRE_COPY &&
             old != IGB_MIG_STATE_RESUMING &&
             old != IGB_MIG_STATE_ERROR) {
             return IGB_MIG_ERR_BAD_STATE;
         }
+        if (old == IGB_MIG_STATE_PRE_COPY ||
+            old == IGB_MIG_STATE_STOP_COPY ||
+            old == IGB_MIG_STATE_ERROR) {
+            igb_core_vf_dirty_disable(s);
+        }
         /* Restore DATA_SIZE to max, same as at reset */
         igbvf_mig_update_data_size(s, igb_core_vf_max_data_size(s));
         break;
 
     case IGB_MIG_STATE_RUNNING:
-        if (old != IGB_MIG_STATE_STOP) {
+        if (old != IGB_MIG_STATE_STOP &&
+            old != IGB_MIG_STATE_PRE_COPY) {
             return IGB_MIG_ERR_BAD_STATE;
         }
+        if (old == IGB_MIG_STATE_PRE_COPY) {
+            igb_core_vf_dirty_disable(s);
+        }
         break;
 
     case IGB_MIG_STATE_STOP_COPY:
-        if (old != IGB_MIG_STATE_STOP) {
+        if (old != IGB_MIG_STATE_STOP &&
+            old != IGB_MIG_STATE_PRE_COPY) {
             return IGB_MIG_ERR_BAD_STATE;
         }
         ret = igb_core_vf_save_state(s, ms->mig_data, sizeof(ms->mig_data));
@@ -520,6 +813,12 @@  static uint8_t igbvf_mig_set_state(IgbVfState *s, uint32_t new_state)
         igbvf_mig_update_data_size(s, 0);
         break;
 
+    case IGB_MIG_STATE_PRE_COPY:
+        if (old != IGB_MIG_STATE_RUNNING) {
+            return IGB_MIG_ERR_BAD_STATE;
+        }
+        break;
+
     default:
         return IGB_MIG_ERR_BAD_STATE;
     }
@@ -564,6 +863,18 @@  static void igbvf_mig_cmd_ctrl(IgbVfState *s, uint32_t val)
         err = igbvf_mig_cmd_load(s);
         break;
 
+    case IGB_MIG_CMD_DIRTY_ENABLE:
+        err = igbvf_mig_cmd_dirty_enable(s);
+        break;
+
+    case IGB_MIG_CMD_DIRTY_DISABLE:
+        igb_core_vf_dirty_disable(s);
+        break;
+
+    case IGB_MIG_CMD_DIRTY_QUERY:
+        err = igbvf_mig_cmd_dirty_query(s);
+        break;
+
     default:
         err = IGB_MIG_ERR_UNK_CMD;
         break;
@@ -594,8 +905,10 @@  bool igbvf_add_migration_dvsec(PCIDevice *dev, Error **errp)
     /* DVSEC header 2: DVSEC ID */
     pci_set_word(dev->config + offset + 0x8, IGB_MIG_DVSEC_ID);
 
-    /* CAPS: features (state migration only) */
-    caps = IGB_MIG_CAP_F_STATE;
+    /* CAPS: features | max_ranges | supported page sizes (4K) */
+    caps = IGB_MIG_CAP_F_STATE | IGB_MIG_CAP_F_DIRTY |
+           (IGB_MIG_CAPS_MAX_RANGES << IGB_MIG_CAPS_MAX_RANGES_SHIFT) |
+           IGB_MIG_CAPS_PGSIZE_4K;
     pci_set_long(dev->config + offset + IGB_MIG_CAPS, caps);
 
     /* STATUS: initial state is RUNNING */
@@ -657,6 +970,9 @@  void igbvf_mig_state_reset(IgbVfState *s)
     IgbVfMigState *ms = &s->mig;
 
     trace_igbvf_mig_reset(s->vfn);
+
+    igb_core_vf_dirty_disable(s);
+
     ms->mig_state = IGB_MIG_STATE_RUNNING;
     ms->mig_data_buf_addr = 0;
     igbvf_mig_update_data_size(s, igb_core_vf_max_data_size(s));
diff --git a/hw/net/trace-events b/hw/net/trace-events
index 7057fbe5f16a..c606191df2ef 100644
--- a/hw/net/trace-events
+++ b/hw/net/trace-events
@@ -300,6 +300,11 @@  igbvf_mig_set_state(uint16_t vfn, uint32_t old_state, uint32_t new_state) "VF%u:
 igbvf_mig_save_state(uint16_t vfn, uint32_t size) "VF%u: saved %u bytes of device state"
 igbvf_mig_load_state(uint16_t vfn, uint32_t size) "VF%u: loaded %u bytes of device state"
 igbvf_mig_reset(uint16_t vfn) "VF%u: migration state reset"
+igbvf_mig_dirty_enable(uint16_t vfn, uint64_t pgsize, uint64_t nbits) "VF%u: dirty tracking enabled pgsize=%"PRIu64" nbits=%"PRIu64
+igbvf_mig_dirty_disable(uint16_t vfn) "VF%u: dirty tracking disabled"
+igbvf_mig_dirty_query(uint16_t vfn, uint64_t size, uint32_t dirty_pages) "VF%u: dirty query returned %"PRIu64" bytes, %u dirty pages"
+igb_core_dirty_track_dma(int vfn, uint64_t addr, uint64_t len) "VF%d: dirty DMA addr=0x%"PRIx64" len=%"PRIu64
+igb_core_dirty_track_dma_drop(int vfn, uint64_t addr, uint64_t len) "VF%d: dirty DMA dropped addr=0x%"PRIx64" len=%"PRIu64" no matching range"
 
 # spapr_llan.c
 spapr_vlan_get_rx_bd_from_pool_found(int pool, int32_t count, uint32_t rx_bufs) "pool=%d count=%"PRId32" rxbufs=%"PRIu32