offline_pages() migrates and isolates pages in two nested loops with no
upper bound.  A page that can never be migrated or freed (a long-term
pin, or a slab page that raced into the range) keeps the loop spinning
and the offline never returns.  This is a known issue, noted in the code
("TODO: fatal migration failures should bail out") and in the admin
guide ("memory offlining might retry for a long time (or even forever),
until aborted by the user").

The only escape is signal_pending(current), which works when userspace
drives the offline but not for in-kernel callers.  ACPI DIMM hot-unplug
(kacpi_hotplug_wq) and similar in-kernel hotplug paths run offline_pages()
on ordered workqueues where signal_pending() can never become true, so a
stuck offline wedges the workqueue and blocks all later hotplug events
with no way to abort it.

Add an opt-in backstop: a counter at the top of the inner migration loop
bounds the number of passes over the range.  The check sits in the inner
loop because that is where a stuck page spins; since every outer pass
runs the inner loop at least once, this bounds both loops.  On the limit
offline_pages() fails with -EBUSY and dump_page()s the stuck page.

The parameter (memory_hotplug.offline_migrate_max_passes) defaults to 0
(unlimited, today's behaviour) and is re-read every pass, so an offline
that is already stuck can be rescued at runtime by writing a non-zero
value.  Such callers already handle -EBUSY, so no caller changes are
needed.

Signed-off-by: Aboorva Devarajan <[email protected]>
---
 .../admin-guide/kernel-parameters.txt         | 14 +++++++
 .../admin-guide/mm/memory-hotplug.rst         | 26 ++++++++++++-
 mm/memory_hotplug.c                           | 38 +++++++++++++++++++
 3 files changed, 77 insertions(+), 1 deletion(-)

diff --git a/Documentation/admin-guide/kernel-parameters.txt 
b/Documentation/admin-guide/kernel-parameters.txt
index 53f08950a630..90b67c457c2a 100644
--- a/Documentation/admin-guide/kernel-parameters.txt
+++ b/Documentation/admin-guide/kernel-parameters.txt
@@ -3980,6 +3980,20 @@ Kernel parameters
                        Note that even when enabled, there are a few cases where
                        the feature is not effective.
 
+       memory_hotplug.offline_migrate_max_passes=
+                       [KNL] Maximum number of migration passes
+                       for memory offlining.
+                       Format: <integer>
+                       default: 0 (no limit, historical behaviour)
+                       When non-zero, memory offlining gives up with
+                       -EBUSY after this many migration passes over
+                       the range, instead of retrying forever on a
+                       stuck page.  Each pass scans the range and
+                       migrates what it can.
+                       The parameter is re-read on every pass, so an
+                       offline request that is already stuck can be
+                       rescued at runtime by writing the parameter.
+
        memtest=        [KNL,X86,ARM,M68K,PPC,RISCV,EARLY] Enable memtest
                        Format: <integer>
                        default : 0 <disable>
diff --git a/Documentation/admin-guide/mm/memory-hotplug.rst 
b/Documentation/admin-guide/mm/memory-hotplug.rst
index 0207f8725142..79b45109c408 100644
--- a/Documentation/admin-guide/mm/memory-hotplug.rst
+++ b/Documentation/admin-guide/mm/memory-hotplug.rst
@@ -224,7 +224,10 @@ increases memory offlining reliability; still, memory 
offlining can fail in
 some corner cases.
 
 Further, memory offlining might retry for a long time (or even forever), until
-aborted by the user.
+aborted by the user.  The ``memory_hotplug.offline_migrate_max_passes``
+parameter can be used to bound the number of retry passes, making an offline
+request that cannot make progress fail with ``-EBUSY`` instead of retrying
+indefinitely (see `Module Parameters`_).
 
 Offlining of a memory block can be triggered via::
 
@@ -547,6 +550,27 @@ The following module parameters are currently defined:
                                 possible.
 
                                 Parameter availability depends on CONFIG_NUMA.
+``offline_migrate_max_passes``  read-write: Maximum number of migration
+                                passes for a single memory offline
+                                request.  Memory offlining retries to
+                                migrate and isolate pages until it succeeds;
+                                a page that can never be migrated or freed
+                                makes it retry forever.  When this parameter
+                                is non-zero, an offline request gives up
+                                with ``-EBUSY`` after the configured number
+                                of migration passes and the memory block
+                                stays online.
+
+                                The parameter is re-read on every pass, so
+                                an offline request that is already stuck
+                                retrying can be rescued at runtime by
+                                writing a non-zero value, without a reboot.
+
+                                The default is "0", meaning no limit (the
+                                historical behaviour).
+
+                                Parameter availability depends on
+                                CONFIG_MEMORY_HOTREMOVE.
 ================================ 
===============================================
 
 ZONE_MOVABLE
diff --git a/mm/memory_hotplug.c b/mm/memory_hotplug.c
index 226ab9cb078a..9067829349a0 100644
--- a/mm/memory_hotplug.c
+++ b/mm/memory_hotplug.c
@@ -1785,6 +1785,12 @@ bool mhp_range_allowed(u64 start, u64 size, bool 
need_mapping)
 }
 
 #ifdef CONFIG_MEMORY_HOTREMOVE
+/* Max offline_pages() migration passes before -EBUSY; 0 = retry forever. */
+static unsigned int offline_migrate_max_passes __read_mostly;
+module_param(offline_migrate_max_passes, uint, 0644);
+MODULE_PARM_DESC(offline_migrate_max_passes,
+       "Max migration passes before memory offline gives up (0 = no limit)");
+
 /*
  * Scan pfn range [start,end) to find movable/migratable pages (LRU and
  * hugetlb folio, movable_ops pages). Will skip over most unmovable
@@ -1966,6 +1972,8 @@ int offline_pages(unsigned long start_pfn, unsigned long 
nr_pages,
        struct node_notify node_arg = {
                .nid = NUMA_NO_NODE,
        };
+       unsigned int max_passes;
+       unsigned int pass = 0;
        unsigned long flags;
        char *reason;
        int ret;
@@ -2062,6 +2070,36 @@ int offline_pages(unsigned long start_pfn, unsigned long 
nr_pages,
                                goto failed_removal_isolated;
                        }
 
+                       /*
+                        * A page that can never be migrated (e.g. a
+                        * long-term pin) makes this loop retry forever.
+                        * Give up after the configured number of passes.
+                        * The limit is re-read on every pass so that an
+                        * offline request that is already stuck here can
+                        * be aborted at runtime by writing the parameter,
+                        * which matters for in-kernel callers (e.g. ACPI
+                        * DIMM hot-unplug) that run on kworkers and can
+                        * never be aborted by a signal.
+                        */
+                       max_passes = READ_ONCE(offline_migrate_max_passes);
+                       if (max_passes && pass++ >= max_passes) {
+                               pr_warn("memory offlining [mem 
%#010llx-%#010llx]: giving up after %u passes\n",
+                                       (unsigned long long)start_pfn << 
PAGE_SHIFT,
+                                       ((unsigned long long)end_pfn << 
PAGE_SHIFT) - 1,
+                                       pass - 1);
+                               /*
+                                * Dump the page we last stopped at as a
+                                * diagnostic hint: usually the stuck page,
+                                * but just the range start after a restart.
+                                */
+                               if (pfn >= start_pfn && pfn < end_pfn)
+                                       dump_page(pfn_to_page(pfn),
+                                                 "memory offline retry limit");
+                               ret = -EBUSY;
+                               reason = "retry limit exceeded";
+                               goto failed_removal_isolated;
+                       }
+
                        cond_resched();
 
                        ret = scan_movable_pages(pfn, end_pfn, &pfn);
-- 
2.54.0


Reply via email to