]> git.ipfire.org Git - thirdparty/linux.git/commitdiff
KVM: s390: Fix cmma dirty tracking
authorClaudio Imbrenda <imbrenda@linux.ibm.com>
Tue, 23 Jun 2026 15:33:28 +0000 (17:33 +0200)
committerClaudio Imbrenda <imbrenda@linux.ibm.com>
Wed, 24 Jun 2026 08:08:57 +0000 (10:08 +0200)
It is possible that some guest memory areas have not been touched yet
when starting migration mode, and thus have no ptes allocated. Only
existing and allocated ptes should count toward the total of dirty cmma
entries.

When starting migration mode, enable the migration_mode flag
immediately, so that any subsequent ESSA will trap in the host and
cause cmma_dirty_pages to be increased as needed.
Subsequently, set the cmma_d bit on all existing cmma-clean PGSTEs,
increasing cmma_dirty_pages as needed. Skipping cmma-dirty pages
prevents double counting.

Conversely, when disabling migration mode, set cmma_dirty_pages to 0
and clear the cmma_d bit in all existing PGSTEs.

The invariant is that when migration mode is off, no PGSTE has its
cmma_d bit set, and cmma_dirty_pages is 0. kvm->slots_lock protects
kvm_s390_vm_start_migration() and kvm_s390_vm_stop_migration() from
each other and from kvm_s390_get_cmma_bits().

Also fix dat_get_cmma() to properly wrap around if the first attempt
reached the end of guest memory without finding cmma-dirty pages.

[ imbrenda: Moved kvm_s390_sync_request_broadcast() before gmap_set_cmma_all_dirty() ]

Fixes: e38c884df921 ("KVM: s390: Switch to new gmap")
Signed-off-by: Claudio Imbrenda <imbrenda@linux.ibm.com>
Message-ID: <20260623153331.233784-8-imbrenda@linux.ibm.com>

arch/s390/kvm/dat.c
arch/s390/kvm/gmap.c
arch/s390/kvm/gmap.h
arch/s390/kvm/kvm-s390.c
arch/s390/kvm/priv.c

index cffac7782c4bf6f36344982f7695a235d1294e8f..0ad4ebc80ebafd2b0dba49507600827304c64ed8 100644 (file)
@@ -1253,6 +1253,9 @@ int dat_get_cmma(union asce asce, gfn_t *start, unsigned int *count, u8 *values,
        };
 
        _dat_walk_gfn_range(*start, asce_end(asce), asce, &ops, DAT_WALK_IGN_HOLES, &state);
+       /* If no dirty pages were found, wrap around and continue searching */
+       if (*start && state.start == -1)
+               _dat_walk_gfn_range(0, *start, asce, &ops, DAT_WALK_IGN_HOLES, &state);
 
        if (state.start == -1) {
                *count = 0;
index e6e786811db863b6ed9a517b50ebfcb5bdf12f16..0f944944badf0f9cf15937ea23c7cbd381a97cc8 100644 (file)
@@ -1073,23 +1073,46 @@ int gmap_protect_rmap(struct kvm_s390_mmu_cache *mc, struct gmap *sg, gfn_t p_gf
        return 0;
 }
 
+static long __set_cmma_clean_pte(union pte *ptep, gfn_t gfn, gfn_t next, struct dat_walk *walk)
+{
+       union pgste pgste;
+
+       pgste = pgste_get_lock(ptep);
+       pgste.cmma_d = 0;
+       pgste_set_unlock(ptep, pgste);
+
+       if (need_resched())
+               return next;
+       return 0;
+}
+
 static long __set_cmma_dirty_pte(union pte *ptep, gfn_t gfn, gfn_t next, struct dat_walk *walk)
 {
-       __atomic64_or(PGSTE_CMMA_D_BIT, &pgste_of(ptep)->val);
+       union pgste pgste;
+
+       pgste = pgste_get_lock(ptep);
+       if (!pgste.cmma_d)
+               atomic64_inc(walk->priv);
+       pgste.cmma_d = 1;
+       pgste_set_unlock(ptep, pgste);
+
        if (need_resched())
                return next;
        return 0;
 }
 
-void gmap_set_cmma_all_dirty(struct gmap *gmap)
+void _gmap_set_cmma_all(struct gmap *gmap, bool dirty)
 {
-       const struct dat_walk_ops ops = { .pte_entry = __set_cmma_dirty_pte, };
+       const struct dat_walk_ops ops = {
+               .pte_entry = dirty ? __set_cmma_dirty_pte : __set_cmma_clean_pte,
+       };
        gfn_t gfn = 0;
 
        do {
                scoped_guard(read_lock, &gmap->kvm->mmu_lock)
                        gfn = _dat_walk_gfn_range(gfn, asce_end(gmap->asce), gmap->asce, &ops,
-                                                 DAT_WALK_IGN_HOLES, NULL);
+                                                 DAT_WALK_IGN_HOLES,
+                                                 &gmap->kvm->arch.cmma_dirty_pages);
                cond_resched();
        } while (gfn);
 }
index 5374f21aaf8dfa2cd2259b4c901f3465ba373925..4e04fbd07696934c956cc000e4d8ba8498eff826 100644 (file)
@@ -103,7 +103,7 @@ int gmap_pv_destroy_range(struct gmap *gmap, gfn_t start, gfn_t end, bool interr
 int gmap_insert_rmap(struct gmap *sg, gfn_t p_gfn, gfn_t r_gfn, int level);
 int gmap_protect_rmap(struct kvm_s390_mmu_cache *mc, struct gmap *sg, gfn_t p_gfn, gfn_t r_gfn,
                      kvm_pfn_t pfn, int level, bool wr);
-void gmap_set_cmma_all_dirty(struct gmap *gmap);
+void _gmap_set_cmma_all(struct gmap *gmap, bool dirty);
 void _gmap_handle_vsie_unshadow_event(struct gmap *parent, gfn_t gfn);
 struct gmap *gmap_create_shadow(struct kvm_s390_mmu_cache *mc, struct gmap *gmap,
                                union asce asce, int edat_level);
@@ -197,6 +197,16 @@ static inline bool pte_needs_unshadow(union pte oldpte, union pte newpte, union
        return !newpte.h.p || !newpte.s.pr;
 }
 
+static inline void gmap_set_cmma_all_dirty(struct gmap *gmap)
+{
+       _gmap_set_cmma_all(gmap, true);
+}
+
+static inline void gmap_set_cmma_all_clean(struct gmap *gmap)
+{
+       _gmap_set_cmma_all(gmap, false);
+}
+
 static inline union pgste _gmap_ptep_xchg(struct gmap *gmap, union pte *ptep, union pte newpte,
                                          union pgste pgste, gfn_t gfn, bool needs_lock)
 {
index 221b2fb199d4ea8a2fdb1582964f8cd3c4a2fc82..9ad6bd4edbce335062f5921cd17e846262b4dcf1 100644 (file)
@@ -1187,13 +1187,13 @@ static void kvm_s390_sync_request_broadcast(struct kvm *kvm, int req)
 
 /*
  * Must be called with kvm->srcu held to avoid races on memslots, and with
- * kvm->slots_lock to avoid races with ourselves and kvm_s390_vm_stop_migration.
+ * kvm->slots_lock to avoid races with ourselves, kvm_s390_vm_stop_migration(),
+ * and kvm_s390_get_cmma_bits().
  */
 static int kvm_s390_vm_start_migration(struct kvm *kvm)
 {
        struct kvm_memory_slot *ms;
        struct kvm_memslots *slots;
-       unsigned long ram_pages = 0;
        int bkt;
 
        /* migration mode already enabled */
@@ -1210,28 +1210,54 @@ static int kvm_s390_vm_start_migration(struct kvm *kvm)
        kvm_for_each_memslot(ms, bkt, slots) {
                if (!ms->dirty_bitmap)
                        return -EINVAL;
-               ram_pages += ms->npages;
        }
-       /* mark all the pages as dirty */
-       gmap_set_cmma_all_dirty(kvm->arch.gmap);
-       atomic64_set(&kvm->arch.cmma_dirty_pages, ram_pages);
-       kvm->arch.migration_mode = 1;
+       /*
+        * Set the flag and let KVM handle ESSA manually, potentially setting
+        * the cmma_d bit in some PGSTEs and increasing cmma_dirty_pages.
+        * At this point cmma_dirty_pages is still 0, and all existing PGSTEs
+        * have their cmma_d bit set to 0.
+        * Any newly allocated page table has its entries marked as cmma-clean,
+        * which is fine because the CMMA values are not dirty.
+        */
+       WRITE_ONCE(kvm->arch.migration_mode, 1);
        kvm_s390_sync_request_broadcast(kvm, KVM_REQ_START_MIGRATION);
+       /*
+        * Mark all PGSTEs as cmma-dirty, increasing cmma_dirty_pages as needed,
+        * but without double-counting pages that have become dirty on their own
+        * in the meantime.
+        * At this point some pages might have become dirty on their own already
+        * and cmma_dirty_pages might therefore be non-zero.
+        */
+       gmap_set_cmma_all_dirty(kvm->arch.gmap);
        return 0;
 }
 
 /*
- * Must be called with kvm->slots_lock to avoid races with ourselves and
- * kvm_s390_vm_start_migration.
+ * Must be called with kvm->slots_lock to avoid races with ourselves,
+ * kvm_s390_vm_start_migration() and kvm_s390_get_cmma_bits().
  */
 static int kvm_s390_vm_stop_migration(struct kvm *kvm)
 {
        /* migration mode already disabled */
        if (!kvm->arch.migration_mode)
                return 0;
-       kvm->arch.migration_mode = 0;
+       /*
+        * Unset the flag and propagate to all vCPUs. From now on the cmma_d
+        * bit will not be touched on any PGSTE.
+        * At this point cmma_dirty_pages is possibly non-zero, and thus some
+        * PGSTEs might have cmma_d set.
+        */
+       WRITE_ONCE(kvm->arch.migration_mode, 0);
        if (kvm->arch.use_cmma)
                kvm_s390_sync_request_broadcast(kvm, KVM_REQ_STOP_MIGRATION);
+       /* Clear cmma_d on all existing PGSTEs and set cmma_dirty_pages to 0. */
+       gmap_set_cmma_all_clean(kvm->arch.gmap);
+       atomic64_set(&kvm->arch.cmma_dirty_pages, 0);
+       /*
+        * At this point the system has the expected state: migration_mode is 0,
+        * cmma_dirty_pages is 0, and all existing PGSTEs have their cmma_d bit
+        * set to 0.
+        */
        return 0;
 }
 
index 9bc6fd02ff77702cf4213b096c3b5b1e6d240968..ad0ddc433a73c30cc7a326086f3c1b39934291cc 100644 (file)
@@ -1236,7 +1236,7 @@ static int handle_essa(struct kvm_vcpu *vcpu)
                                                : ESSA_SET_STABLE_IF_RESIDENT))
                return kvm_s390_inject_program_int(vcpu, PGM_SPECIFICATION);
 
-       if (!vcpu->kvm->arch.migration_mode) {
+       if (!READ_ONCE(vcpu->kvm->arch.migration_mode)) {
                /*
                 * CMMA is enabled in the KVM settings, but is disabled in
                 * the SIE block and in the mm_context, and we are not doing