From nobody Fri Jul 24 23:30:20 2026 Received: from mx0a-001b2d01.pphosted.com (mx0a-001b2d01.pphosted.com [148.163.156.1]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id 538C7470123; Wed, 22 Jul 2026 07:48:40 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=148.163.156.1 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784706526; cv=none; b=CGqpjlpObPwrW6PqEXtmtLCj+aid8HccyGZAKnJxAuEE8naTLbNeI0/VeADV7c3pX7f8XMU/MZ75hCfolDaJIgug3BpFqUpHqwBDywbf3HmOROpmzm7HfJ9gmfJMGnmryLYR47ApY016HuQdMTSR4sm/7FugSVKzNOkAtzhMTSU= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784706526; c=relaxed/simple; bh=agRYJEOF3XLrhyJ9SwnBD10ymd7y84L+yeeFfGlFgng=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=D7bNWTYz/fMfLiZs4v4+MEOyEX/+haUWZVkf50lkHLbdUed4iKy4hDqBYJYTVwux1sTzlWGj6Ba/tQeqWqPY6WtED21YXyoCtPHAhL+MtCOqhH2uekR0Jp6kilEF/aZuCKSndAT707YA0H+/Gs6pBipUxdNxsA6a/G9QWtszBho= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.ibm.com; spf=pass smtp.mailfrom=linux.ibm.com; dkim=pass (2048-bit key) header.d=ibm.com header.i=@ibm.com header.b=H2llrjcQ; arc=none smtp.client-ip=148.163.156.1 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.ibm.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.ibm.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=ibm.com header.i=@ibm.com header.b="H2llrjcQ" Received: from pps.filterd (m0353729.ppops.net [127.0.0.1]) by mx0a-001b2d01.pphosted.com (8.18.1.11/8.18.1.11) with ESMTP id 66M5C0EU1271586; Wed, 22 Jul 2026 07:48:24 GMT DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ibm.com; h=cc :content-transfer-encoding:date:from:in-reply-to:message-id :mime-version:references:subject:to; s=pp1; bh=q+SVszRD0Hy0NMbJM z4R0jwnOEB3Hazg4a4qbEeUlZs=; b=H2llrjcQ45kPBAFp85fxROUXG6XGXv5zd ZPL75X/IN+wmf5hHCPry9st57wVl9cxlOrVSlqPYnhSyOx0/VmlXLT38JG8fgPOI sj8TS04wUT8grgFnbw/vQozo86YMzl1uOykTimNlfBgSJk1KVPnwKd3rXbU5qC7c NByovbpjSoriGceuqWTzRjxU3PRUsDxfc840VM2Kx5tVVeY2Fi+JS+1HwuCbmr9W H9jqR4zw8vxhieNuvPAuCzwE49NymbOUUam0EN1IB6KluHhMg1Zq07UYPYveWSYS fdxGcvl7j9QZzrhRcPMIOs51f/LcfS5WiX7VjV/lODcjnqp06OItQ== Received: from ppma11.dal12v.mail.ibm.com (db.9e.1632.ip4.static.sl-reverse.com [50.22.158.219]) by mx0a-001b2d01.pphosted.com (PPS) with ESMTPS id 4fg77agvxx-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 22 Jul 2026 07:48:23 +0000 (GMT) Received: from pps.filterd (ppma11.dal12v.mail.ibm.com [127.0.0.1]) by ppma11.dal12v.mail.ibm.com (8.18.1.7/8.18.1.7) with ESMTP id 66M7YdHs025677; Wed, 22 Jul 2026 07:48:22 GMT Received: from smtprelay05.fra02v.mail.ibm.com ([9.218.2.225]) by ppma11.dal12v.mail.ibm.com (PPS) with ESMTPS id 4fgpgye031-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 22 Jul 2026 07:48:22 +0000 (GMT) Received: from smtpav05.fra02v.mail.ibm.com (smtpav05.fra02v.mail.ibm.com [10.20.54.104]) by smtprelay05.fra02v.mail.ibm.com (8.14.9/8.14.9/NCO v10.0) with ESMTP id 66M7mKir42598754 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-GCM-SHA384 bits=256 verify=OK); Wed, 22 Jul 2026 07:48:20 GMT Received: from smtpav05.fra02v.mail.ibm.com (unknown [127.0.0.1]) by IMSVA (Postfix) with ESMTP id 721A5200CB; Wed, 22 Jul 2026 07:48:20 +0000 (GMT) Received: from smtpav05.fra02v.mail.ibm.com (unknown [127.0.0.1]) by IMSVA (Postfix) with ESMTP id D6E5F200C7; Wed, 22 Jul 2026 07:48:16 +0000 (GMT) Received: from aboo.bl1-in.ibm.com (unknown [9.123.14.187]) by smtpav05.fra02v.mail.ibm.com (Postfix) with ESMTP; Wed, 22 Jul 2026 07:48:16 +0000 (GMT) From: Aboorva Devarajan To: Andrew Morton , David Hildenbrand , Oscar Salvador Cc: Michal Hocko , Lorenzo Stoakes , "Liam R. Howlett" , Vlastimil Babka , Mike Rapoport , Suren Baghdasaryan , Jonathan Corbet , Shuah Khan , linux-mm@kvack.org, linux-kernel@vger.kernel.org, linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org, "Ritesh Harjani (IBM)" , Aboorva Devarajan Subject: [RFC PATCH 1/2] mm/memory_hotplug: bound offline retry loops with a configurable limit Date: Wed, 22 Jul 2026 13:18:10 +0530 Message-ID: <20260722074811.378283-2-aboorvad@linux.ibm.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260722074811.378283-1-aboorvad@linux.ibm.com> References: <20260722074811.378283-1-aboorvad@linux.ibm.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-TM-AS-GCONF: 00 X-Proofpoint-Reinject: loops=2 maxloops=12 X-Proofpoint-ORIG-GUID: pkhTd_16EluoY8tLFUPZrTKOJvkWpufK X-Proofpoint-Spam-Info: AW1haW4tMjYwNzIyMDA3MCBTYWx0ZWRfXxHt0Z688QQyZ 9GuK0Sc6vh/v+M+N2lIGjUJfqNJWV7jwZfLtVPq5RrJSwobld1o9mJOjD+JGRvSzubiKXtR5tWO H8WzJeUySMhYFFuOrpDNtNd0hCuWzDk= X-Proofpoint-GUID: bxHTMwvfEkE9Dn5DcR6t9yZI0KCsbuFt X-Proofpoint-Spam-Details-Enc: AW1haW4tMjYwNzIyMDA3MCBTYWx0ZWRfX2790NWheW0MH pPM4vRXlTels4V+flZvFKgtK8WwSePrsAzT8cFJhgz9d017YD4GtfuZdSa53MUECXftZVH2UanR 2FGfzljB6spCD1BuNnpBaWgm5EdvcM5xZBgaDKSqMdjuc+Bl66i9LJmjSYXX3/gwPfEEsFv3rYp qXGHMx4dpni2tE01cI8oleg5NLOf7ox4d43VksCrzvPP8mKUC4H77y6CM1c5+fDb2TPUuFgC4vQ wRqwFBT1kDQ/rWnpZhve5jV1ceV7T/qGdAPfQurI+95A0/aE2j6OZDtPUhZXuJQN6c4SNo2evwf pfUADq5HjfScvDUGTFaKChZqkw0D+OTbzXrRdgMcXwNgityjgolMBKwexmE60yQ1IaRRCeViJyA 81dct9X7mKisBbifQVpn7w3YK+MNZpvj3UjSkc4ae4GR3ZJySdwT1EksjhU+ZAoR3/PCNlwYaq5 2U5eQ0vhOkWZFYD4cfA== X-Authority-Analysis: v=2.4 cv=K7AS2SWI c=1 sm=1 tr=0 ts=6a6075c7 cx=c_pps a=aDMHemPKRhS1OARIsFnwRA==:117 a=aDMHemPKRhS1OARIsFnwRA==:17 a=RAioF0-LDSMA:10 a=VkNPw1HP01LnGYTKEx00:22 a=RnoormkPH1_aCDwRdu11:22 a=uAbxVGIbfxUO_5tXvNgY:22 a=VnNF1IyMAAAA:8 a=_mUAl5CdPfijK7iri6sA:9 X-Proofpoint-Virus-Version: vendor=baseguard engine=ICAP:2.0.293,Aquarius:18.0.1143,Hydra:6.1.134,FMLib:17.12.100.49 definitions=2026-07-22_02,2026-07-21_01,2025-10-01_01 X-Proofpoint-Spam-Details: rule=outbound_notspam policy=outbound score=0 lowpriorityscore=0 suspectscore=0 bulkscore=0 clxscore=1015 priorityscore=1501 spamscore=0 phishscore=0 malwarescore=0 adultscore=0 impostorscore=0 classifier=typeunknown authscore=0 authtc= authcc= route=outbound adjust=0 reason=mlx scancount=1 engine=8.22.0-2606150000 definitions=main-2607220070 Content-Type: text/plain; charset="utf-8" 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 --- .../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/Documentatio= n/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. =20 + memory_hotplug.offline_migrate_max_passes=3D + [KNL] Maximum number of migration passes + for memory offlining. + Format: + 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=3D [KNL,X86,ARM,M68K,PPC,RISCV,EARLY] Enable memtest Format: default : 0 diff --git a/Documentation/admin-guide/mm/memory-hotplug.rst b/Documentatio= n/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. =20 Further, memory offlining might retry for a long time (or even forever), u= ntil -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 offli= ne +request that cannot make progress fail with ``-EBUSY`` instead of retrying +indefinitely (see `Module Parameters`_). =20 Offlining of a memory block can be triggered via:: =20 @@ -547,6 +550,27 @@ The following module parameters are currently defined: possible. =20 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. =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D=3D= =3D=3D=3D=3D=3D=3D =20 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 nee= d_mapping) } =20 #ifdef CONFIG_MEMORY_HOTREMOVE +/* Max offline_pages() migration passes before -EBUSY; 0 =3D 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 =3D 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 l= ong nr_pages, struct node_notify node_arg =3D { .nid =3D NUMA_NO_NODE, }; + unsigned int max_passes; + unsigned int pass =3D 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; } =20 + /* + * 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 =3D READ_ONCE(offline_migrate_max_passes); + if (max_passes && pass++ >=3D 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 >=3D start_pfn && pfn < end_pfn) + dump_page(pfn_to_page(pfn), + "memory offline retry limit"); + ret =3D -EBUSY; + reason =3D "retry limit exceeded"; + goto failed_removal_isolated; + } + cond_resched(); =20 ret =3D scan_movable_pages(pfn, end_pfn, &pfn); --=20 2.54.0 From nobody Fri Jul 24 23:30:20 2026 Received: from mx0b-001b2d01.pphosted.com (mx0b-001b2d01.pphosted.com [148.163.158.5]) (using TLSv1.2 with cipher ECDHE-RSA-AES256-GCM-SHA384 (256/256 bits)) (No client certificate requested) by smtp.subspace.kernel.org (Postfix) with ESMTPS id F240A46E008; Wed, 22 Jul 2026 07:48:43 +0000 (UTC) Authentication-Results: smtp.subspace.kernel.org; arc=none smtp.client-ip=148.163.158.5 ARC-Seal: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784706526; cv=none; b=AY0tM2+AFJaN7/NRJ8n1AKEqi6vfW3sQANrKbmxmvkHgewpo6DWyRQvx9oWgqnUoOF98VGYcGKUbMAaVA/bbDOjS1/uODANolId6RxweyQzJrT4jgVkujRPgUUs+50W/700JgX53jFHTK66j+3Qyfs+pkatY5FY5/7++5YYo06E= ARC-Message-Signature: i=1; a=rsa-sha256; d=subspace.kernel.org; s=arc-20240116; t=1784706526; c=relaxed/simple; bh=X6y2qwLsLrihPDwY2YKofrxkjpaNAWyeTbksXjpFs0o=; h=From:To:Cc:Subject:Date:Message-ID:In-Reply-To:References: MIME-Version; b=AhtFEQb6WEPnzU2GojeOn/dGhbxALn2Npwl0mtJ18XzcMEcHmHU335JJrrnUP+txhtsRjGf9+c2g8co097h3tJ+CLRQxtDAU/u3wV3mQOF1eB4Jma9qgGmzlcbSblbtGafQvYqMXhuX1wxJCAiLs6xs4Tn/8GVjgasks0F82XQ4= ARC-Authentication-Results: i=1; smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.ibm.com; spf=pass smtp.mailfrom=linux.ibm.com; dkim=pass (2048-bit key) header.d=ibm.com header.i=@ibm.com header.b=mmOLZrZw; arc=none smtp.client-ip=148.163.158.5 Authentication-Results: smtp.subspace.kernel.org; dmarc=pass (p=none dis=none) header.from=linux.ibm.com Authentication-Results: smtp.subspace.kernel.org; spf=pass smtp.mailfrom=linux.ibm.com Authentication-Results: smtp.subspace.kernel.org; dkim=pass (2048-bit key) header.d=ibm.com header.i=@ibm.com header.b="mmOLZrZw" Received: from pps.filterd (m0353725.ppops.net [127.0.0.1]) by mx0a-001b2d01.pphosted.com (8.18.1.11/8.18.1.11) with ESMTP id 66M5BvaI3048306; Wed, 22 Jul 2026 07:48:28 GMT DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed; d=ibm.com; h=cc :content-transfer-encoding:date:from:in-reply-to:message-id :mime-version:references:subject:to; s=pp1; bh=cTZZiiE5kktj/wuzF u+mWHvdEQ2lr2pxn7eLIBud1O0=; b=mmOLZrZwJlipqPcgY15ufGOmz5p8WZXer V8mlbnZCxaBPdaviI4F2TCzBR0RaRh8wMqYuGq3cv/ZXYqAVy7sA2NBKsc+9AGee UdTxGZxehtP/5xRr6yYoAr1hSQFohRkAiSoTS5oppfhO0BVm4xIKIAw3T5P6Mxjo hB6veMcbEBL9qXBsVv6tcE07plP0uEdyQ+Qk0+PyhIny+QY/iNL6E3f/fp+NQzbX KaAahyWB0rX0365A47kYkcQ9nPRQ5jBvA+6qZORSLAlwc5FDBpU82hmeg8Rd+u5A P2ljD83pFHqom7PWQN1OmPs4G59HhmVHuEO5KjwQyMqnkdJy7bwig== Received: from ppma13.dal12v.mail.ibm.com (dd.9e.1632.ip4.static.sl-reverse.com [50.22.158.221]) by mx0a-001b2d01.pphosted.com (PPS) with ESMTPS id 4fg78g8exc-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 22 Jul 2026 07:48:27 +0000 (GMT) Received: from pps.filterd (ppma13.dal12v.mail.ibm.com [127.0.0.1]) by ppma13.dal12v.mail.ibm.com (8.18.1.7/8.18.1.7) with ESMTP id 66M7Yauw006002; Wed, 22 Jul 2026 07:48:26 GMT Received: from smtprelay07.fra02v.mail.ibm.com ([9.218.2.229]) by ppma13.dal12v.mail.ibm.com (PPS) with ESMTPS id 4fgp1ge2dh-1 (version=TLSv1.2 cipher=ECDHE-RSA-AES256-GCM-SHA384 bits=256 verify=NOT); Wed, 22 Jul 2026 07:48:26 +0000 (GMT) Received: from smtpav05.fra02v.mail.ibm.com (smtpav05.fra02v.mail.ibm.com [10.20.54.104]) by smtprelay07.fra02v.mail.ibm.com (8.14.9/8.14.9/NCO v10.0) with ESMTP id 66M7mOSW34079040 (version=TLSv1/SSLv3 cipher=DHE-RSA-AES256-GCM-SHA384 bits=256 verify=OK); Wed, 22 Jul 2026 07:48:24 GMT Received: from smtpav05.fra02v.mail.ibm.com (unknown [127.0.0.1]) by IMSVA (Postfix) with ESMTP id 67013200C9; Wed, 22 Jul 2026 07:48:24 +0000 (GMT) Received: from smtpav05.fra02v.mail.ibm.com (unknown [127.0.0.1]) by IMSVA (Postfix) with ESMTP id D5547200C7; Wed, 22 Jul 2026 07:48:20 +0000 (GMT) Received: from aboo.bl1-in.ibm.com (unknown [9.123.14.187]) by smtpav05.fra02v.mail.ibm.com (Postfix) with ESMTP; Wed, 22 Jul 2026 07:48:20 +0000 (GMT) From: Aboorva Devarajan To: Andrew Morton , David Hildenbrand , Oscar Salvador Cc: Michal Hocko , Lorenzo Stoakes , "Liam R. Howlett" , Vlastimil Babka , Mike Rapoport , Suren Baghdasaryan , Jonathan Corbet , Shuah Khan , linux-mm@kvack.org, linux-kernel@vger.kernel.org, linux-doc@vger.kernel.org, linux-kselftest@vger.kernel.org, "Ritesh Harjani (IBM)" , Aboorva Devarajan Subject: [RFC PATCH 2/2] selftests/mm: add pc-dimm ACPI eject selftest for offline_migrate_max_passes Date: Wed, 22 Jul 2026 13:18:11 +0530 Message-ID: <20260722074811.378283-3-aboorvad@linux.ibm.com> X-Mailer: git-send-email 2.54.0 In-Reply-To: <20260722074811.378283-1-aboorvad@linux.ibm.com> References: <20260722074811.378283-1-aboorvad@linux.ibm.com> Precedence: bulk X-Mailing-List: linux-kernel@vger.kernel.org List-Id: List-Subscribe: List-Unsubscribe: MIME-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-TM-AS-GCONF: 00 X-Proofpoint-Reinject: loops=2 maxloops=12 X-Proofpoint-Spam-Details-Enc: AW1haW4tMjYwNzIyMDA3MCBTYWx0ZWRfX/0DAlQdyBnYO AvJzqV8dwi3LjsaK5FxiIFbt6lpeHzbKBTcye+qjwb43s9L0h9IbFkBr3RTnjSbcgr5mcER5q/r jl6g9gr99bDAqEZr5CYlQ99XBmd4zove6AiEs+jlCOVBZNfv72g0Ck6utzdpgUtMlBe590m1Nkc 5QJ/DKH13GzblZ+A8qXb/fXT6r+5xtbsL9yeM1uxOAkCbHviRs/1yrEFErufjGVmjx8MnivBFVL jk1VBllgU1RZG419FTt93ua60Kr8vLmuhZfFBgIsgu/ivGfrKfkkoOp6VSASItLVnSpYnN9h/fL w3nsvUYWO+LHubM6+EiCr/KZ10VDsNxEWT6Rmic8j+9oI1y3I9ffFgqKaKU3DvyR/k/JzEs+NJ8 lnuZpQ2THm7gC+p1bJw9pPx1C3Ws0SEm5U4mXvP350almIfNu5FAoVeu0vRs+brQIwy/oUikFg9 C1qJDRY/LS2F9dHcsIw== X-Proofpoint-GUID: rknPGrjsA6ydhCp5WqrH1HIG3arFpz7L X-Authority-Analysis: v=2.4 cv=MelcfZ/f c=1 sm=1 tr=0 ts=6a6075cb cx=c_pps a=AfN7/Ok6k8XGzOShvHwTGQ==:117 a=AfN7/Ok6k8XGzOShvHwTGQ==:17 a=RAioF0-LDSMA:10 a=VkNPw1HP01LnGYTKEx00:22 a=RnoormkPH1_aCDwRdu11:22 a=V8glGbnc2Ofi9Qvn3v5h:22 a=VnNF1IyMAAAA:8 a=ygpDcHnE5uXTT0XjHt0A:9 X-Proofpoint-Spam-Info: AW1haW4tMjYwNzIyMDA3MCBTYWx0ZWRfXy4n56Ytf/Mw0 Z+/IEJPjDdoZIldzin7WiMNvwOLenTmXJZ0ww6lfaAiH2CbPlULzNXnrqRKqCIqyE8PWmNHjvf/ IZ2/0eDvE/8VtIWLotJe1affXvdnYsE= X-Proofpoint-ORIG-GUID: m7iHy-OKdyRKAQ3plhBSBylSpYIYuNwj X-Proofpoint-Virus-Version: vendor=baseguard engine=ICAP:2.0.293,Aquarius:18.0.1143,Hydra:6.1.134,FMLib:17.12.100.49 definitions=2026-07-22_02,2026-07-21_01,2025-10-01_01 X-Proofpoint-Spam-Details: rule=outbound_notspam policy=outbound score=0 impostorscore=0 lowpriorityscore=0 priorityscore=1501 bulkscore=0 spamscore=0 clxscore=1015 malwarescore=0 phishscore=0 adultscore=0 suspectscore=0 classifier=typeunknown authscore=0 authtc= authcc= route=outbound adjust=0 reason=mlx scancount=1 engine=8.22.0-2606150000 definitions=main-2607220070 Content-Type: text/plain; charset="utf-8" Add a self-contained VM-based selftest that reproduces the ACPI memory hot-unplug hang in kworker context and verifies that memory_hotplug.offline_migrate_max_passes recovers from it. The test needs a real ACPI eject because the offline that hangs runs on the kacpi_hotplug_wq kworker (acpi_bus_offline -> device_offline -> offline_pages) and cannot be aborted by a signal. It boots the just-built kernel under virtme-ng with a cold-plugged pc-dimm and runs everything inside the guest: 1. online the DIMM memory blocks as ZONE_MOVABLE; 2. long-term pin one page of the DIMM via vmsplice (a small pin helper is embedded in the script and built at run time); 3. eject the DIMM via /sys/bus/acpi/devices/PNP0C80:*/eject, which runs the offline on the kacpi_hotplug_wq kworker, the same path as a hypervisor-initiated device_del; 4. observe the real hang: with the limit disabled (default), the block sits in "going-offline" while the kworker spins in offline_pages(); 5. rescue it at runtime: write offline_migrate_max_passes and verify the kworker gives up with -EBUSY and the block returns to "online". Sample output: TAP version 13 1..3 # DIMM memory blocks: 32 33 # pinned one page in block memory33 # ejecting /sys/bus/acpi/devices/PNP0C80:00 # block memory33 going-offline, holding 10s # do_migrate_range+0x1eb/0x260 # offline_pages+0x361/0x520 # memory_subsys_offline+0xdb/0x180 # device_offline+0xd0/0x130 # acpi_bus_offline+0x11f/0x190 # acpi_device_hotplug+0x1e8/0x3e0 # acpi_hotplug_work_fn+0x1e/0x30 # process_one_work+0x16a/0x310 # armed offline_migrate_max_passes=3D50 # memory offlining [mem 0x108000000-0x10fffffff]: giving up after 50 pass= es ok 1 ACPI eject kworker hangs unbounded in offline_pages() ok 2 stuck offline rescued by writing offline_migrate_max_passes ok 3 memory block online and usable after the bailout # Totals: pass:3 fail:0 xfail:0 xpass:0 skip:0 error:0 The wrapper is added as TEST_PROGS_EXTENDED (like hwpoison-panic.sh): installed and kept executable, but not run from a default kselftest invocation, because it spawns a VM itself. The config fragment gains the options the guest kernel needs (MEMORY_HOTPLUG, MEMORY_HOTREMOVE, ACPI_HOTPLUG_MEMORY). When vng, qemu, cc or the kernel knob are missing it skips cleanly (KSFT_SKIP), so it is safe to run anywhere. The vmsplice-based pin in this test is a targeted technique to reliably reproduce the hang path for testing purposes; production scenarios typically involve slab pages or busy block-device page-cache racing into the offline range. Signed-off-by: Aboorva Devarajan --- tools/testing/selftests/mm/Makefile | 3 + tools/testing/selftests/mm/config | 3 + .../selftests/mm/vmtest_memory_hotplug.sh | 369 ++++++++++++++++++ 3 files changed, 375 insertions(+) create mode 100755 tools/testing/selftests/mm/vmtest_memory_hotplug.sh diff --git a/tools/testing/selftests/mm/Makefile b/tools/testing/selftests/= mm/Makefile index eae504bd94c8..e113f3ff4655 100644 --- a/tools/testing/selftests/mm/Makefile +++ b/tools/testing/selftests/mm/Makefile @@ -177,6 +177,9 @@ TEST_PROGS +=3D ksft_vmalloc.sh # Destructive: every successful run panics the kernel. Installed and # kept executable, but not run from a default kselftest invocation. TEST_PROGS_EXTENDED +=3D hwpoison-panic.sh +# Spawns its own VM (virtme-ng); not run from a default kselftest +# invocation. +TEST_PROGS_EXTENDED +=3D vmtest_memory_hotplug.sh =20 TEST_FILES :=3D test_vmalloc.sh TEST_FILES +=3D test_hmm.sh diff --git a/tools/testing/selftests/mm/config b/tools/testing/selftests/mm= /config index 06f78bd232e2..d22f63eb9c2c 100644 --- a/tools/testing/selftests/mm/config +++ b/tools/testing/selftests/mm/config @@ -14,3 +14,6 @@ CONFIG_UPROBES=3Dy CONFIG_MEMORY_FAILURE=3Dy CONFIG_HWPOISON_INJECT=3Dm CONFIG_PROC_MEM_ALWAYS_FORCE=3Dy +CONFIG_MEMORY_HOTPLUG=3Dy +CONFIG_MEMORY_HOTREMOVE=3Dy +CONFIG_ACPI_HOTPLUG_MEMORY=3Dy diff --git a/tools/testing/selftests/mm/vmtest_memory_hotplug.sh b/tools/te= sting/selftests/mm/vmtest_memory_hotplug.sh new file mode 100755 index 000000000000..c211a0e7caa2 --- /dev/null +++ b/tools/testing/selftests/mm/vmtest_memory_hotplug.sh @@ -0,0 +1,369 @@ +#!/bin/bash +# SPDX-License-Identifier: GPL-2.0 +# +# Test memory_hotplug.offline_migrate_max_passes by reproducing the ACPI +# memory hot-unplug hang in kworker context and verifying that the retry +# limit recovers from it. +# +# offline_pages() retries page migration in an unbounded loop. A page +# that can never be migrated (long-term pin, slab, busy metadata) makes +# the loop spin forever. Userspace-driven offlines can be interrupted by +# a signal, but an ACPI DIMM eject runs on the kacpi_hotplug_wq kworker +# where signal_pending() never fires, so the kworker hangs until reboot. +# +# Flow: +# 1. Boot the just-built kernel (virtme-ng) with a cold-plugged pc-dimm. +# 2. Online the DIMM's blocks as ZONE_MOVABLE. +# 3. Pin one page of the DIMM (vmsplice long-term pin). +# 4. Eject the DIMM via sysfs; offline runs on kacpi_hotplug_wq. +# 5. Observe the hang (going-offline persists). +# 6. Rescue: write offline_migrate_max_passes at runtime, verify bailout. +# +# TAP subtests: +# 1 ACPI eject kworker hangs unbounded in offline_pages() +# 2 stuck offline rescued by writing offline_migrate_max_passes +# 3 memory block online and usable after the bailout +# +# Dependencies: virtme-ng (vng), qemu-system-, cc. +# SKIPs (never FAILs) when tooling or kernel support is missing. + +readonly SCRIPT_DIR=3D"$(cd -P -- "$(dirname -- "${BASH_SOURCE[0]}")" && p= wd -P)" +readonly KERNEL_CHECKOUT=3D"$(realpath "${SCRIPT_DIR}"/../../../../)" + +# shellcheck source=3D../kselftest/ktap_helpers.sh +source "${SCRIPT_DIR}"/../kselftest/ktap_helpers.sh + +readonly DIMM_SIZE=3D"${DIMM_SIZE:-256M}" +readonly MAX_PASSES=3D"${MAX_PASSES:-50}" +readonly HANG_HOLD=3D"${HANG_HOLD:-10}" +readonly RESCUE_TIMEOUT=3D"${RESCUE_TIMEOUT:-30}" +readonly BOOT_TIMEOUT=3D"${BOOT_TIMEOUT:-240}" + +readonly LOG=3D"$(mktemp /tmp/vmtest_mhp_XXXXXX.log)" +readonly GUEST_SCRIPT=3D"$(mktemp /tmp/vmtest_mhp_guest_XXXXXX.sh)" +readonly PIN_SRC=3D"$(mktemp /tmp/vmtest_mhp_pin_XXXXXX.c)" +# Must live in the kernel tree so the guest sees it through the 9p rootfs. +readonly PIN_BIN=3D"${SCRIPT_DIR}/.vmtest_pin_park.$$" + +VNG_PID=3D"" + +cleanup() { + [ -n "${VNG_PID}" ] && kill "${VNG_PID}" 2>/dev/null + rm -f "${LOG}" "${GUEST_SCRIPT}" "${PIN_SRC}" "${PIN_BIN}" +} + +guest_result() { + grep -o "^${1}=3D[A-Za-z0-9_-]*" "${LOG}" | tail -1 | cut -d=3D -f2 +} + +dump_log_tail() { + tail -25 "${LOG}" | while IFS=3D read -r line; do + ktap_print_msg "${line}" + done +} + +skip_all_tests() { + local i + + for i in 1 2 3; do + ktap_test_skip "$1" + done + ktap_finished +} + +# -- pre-flight checks ---------------------------------------------------= ---- + +ktap_print_header +ktap_set_plan 3 +trap cleanup EXIT + +arch=3D"$(uname -m)" +case "${arch}" in +x86_64|aarch64) ;; +*) skip_all_tests "pc-dimm ACPI hotplug not available on ${arch}" ;; +esac + +for dep in vng "qemu-system-${arch}" cc; do + if ! command -v "${dep}" >/dev/null 2>&1; then + skip_all_tests "${dep} not installed" + fi +done + +# -- pin helper (compiled on the host, visible to the guest via 9p) ------= ----- +# +# Allocate one block's worth of pages (ZONE_MOVABLE serves them from the +# DIMM), find one that landed in a target block, vmsplice-pin it into a +# pipe, print PIN_BLK=3D, and sleep forever. + +cat > "${PIN_SRC}" <<'PINEOF' +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include + +static unsigned long memory_block_size(void) +{ + char buf[64]; + ssize_t n; + int fd; + + fd =3D open("/sys/devices/system/memory/block_size_bytes", O_RDONLY); + if (fd < 0) + return 0; + n =3D read(fd, buf, sizeof(buf) - 1); + close(fd); + if (n <=3D 0) + return 0; + buf[n] =3D '\0'; + return strtoul(buf, NULL, 16); +} + +static unsigned long vaddr_to_block(int pagemap_fd, char *vaddr, + long page_size, unsigned long blk_size) +{ + unsigned long vpn =3D (unsigned long)vaddr / page_size; + uint64_t ent; + + if (pread(pagemap_fd, &ent, sizeof(ent), vpn * 8) !=3D sizeof(ent)) + return -1UL; + if (!(ent & (1ULL << 63))) + return -1UL; + return (ent & ((1ULL << 55) - 1)) * page_size / blk_size; +} + +int main(int argc, char **argv) +{ + long page_size =3D sysconf(_SC_PAGESIZE); + unsigned long blk_size =3D memory_block_size(); + unsigned long off, blk =3D 0; + char *area, *page =3D NULL; + int pagemap_fd, pipefd[2], i; + + if (argc < 2 || !blk_size) { + fprintf(stderr, "usage: pin_park ...\n"); + return 1; + } + + area =3D mmap(NULL, blk_size, PROT_READ | PROT_WRITE, + MAP_PRIVATE | MAP_ANONYMOUS | MAP_POPULATE, -1, 0); + if (area =3D=3D MAP_FAILED) { + perror("mmap"); + return 1; + } + memset(area, 0x42, blk_size); + + pagemap_fd =3D open("/proc/self/pagemap", O_RDONLY); + if (pagemap_fd < 0) { + perror("pagemap"); + return 1; + } + + for (off =3D 0; off < blk_size && !page; off +=3D page_size) { + blk =3D vaddr_to_block(pagemap_fd, area + off, page_size, + blk_size); + for (i =3D 1; i < argc; i++) { + if (blk =3D=3D strtoul(argv[i], NULL, 10)) { + page =3D area + off; + break; + } + } + } + close(pagemap_fd); + + if (!page) { + printf("PIN_NONE\n"); + fflush(stdout); + return 2; + } + + if (pipe(pipefd)) { + perror("pipe"); + return 1; + } + + struct iovec iov =3D { .iov_base =3D page, .iov_len =3D page_size }; + + if (vmsplice(pipefd[1], &iov, 1, 0) !=3D page_size) { + perror("vmsplice"); + return 1; + } + + printf("PIN_BLK=3D%lu\n", blk); + fflush(stdout); + pause(); + return 0; +} +PINEOF + +if ! cc -O2 -o "${PIN_BIN}" "${PIN_SRC}" 2>>"${LOG}"; then + skip_all_tests "cannot build the pin helper" +fi + +# -- guest script (runs as init inside the VM) ---------------------------= ----- + +cat > "${GUEST_SCRIPT}" <> "${GUEST_SCRIPT}" <<'GUESTEOF' +set -u + +readonly PARAM=3D/sys/module/memory_hotplug/parameters/offline_migrate_max= _passes +readonly MEM=3D/sys/devices/system/memory + +skip() { echo "VMTEST_SKIP: $*"; echo "VMTEST_DONE"; exit 0; } + +block_state() { + cat "${MEM}/memory${pinned_blk}/state" 2>/dev/null || echo GONE +} + +[ -f "${PARAM}" ] || skip "kernel lacks offline_migrate_max_passes" + +# 1. Online the DIMM's blocks as ZONE_MOVABLE. +dimm_blocks=3D"" +for s in "${MEM}"/memory*/state; do + [ "$(cat "${s}" 2>/dev/null)" =3D offline ] || continue + if echo online_movable > "${s}" 2>/dev/null; then + b=3D${s%/state} + dimm_blocks=3D"${dimm_blocks} ${b##*memory}" + fi +done +echo "# DIMM memory blocks:${dimm_blocks:- none}" +[ -n "${dimm_blocks}" ] || skip "no offline memory blocks (pc-dimm missing= ?)" + +# 2. Pin one page inside the DIMM. +"${PIN_PARK}" ${dimm_blocks} > /tmp/pin.out 2>&1 & +pin_pid=3D$! +for _ in $(seq 1 10); do + grep -q "PIN_" /tmp/pin.out 2>/dev/null && break + sleep 1 +done +pinned_blk=3D$(sed -n 's/^PIN_BLK=3D//p' /tmp/pin.out) +[ -n "${pinned_blk}" ] || skip "pin failed: $(cat /tmp/pin.out 2>/dev/null= )" +echo "# pinned one page in block memory${pinned_blk}" + +# 3. Eject the DIMM with the retry limit disabled (default =3D unbounded). +echo 0 > "${PARAM}" +dmesg -c > /dev/null 2>&1 || true + +ejdev=3D"" +for dev in /sys/bus/acpi/devices/PNP0C80:*; do + [ -e "${dev}/eject" ] && { ejdev=3D"${dev}"; break; } +done +[ -n "${ejdev}" ] || skip "no ejectable ACPI memory device (PNP0C80)" +echo "# ejecting ${ejdev}" +echo 1 > "${ejdev}/eject" & + +# 4. Observe the hang: the block must stay in "going-offline". +sleep 3 +stuck=3D0 +if [ "$(block_state)" =3D going-offline ]; then + echo "# block memory${pinned_blk} going-offline, holding ${HANG_HOLD}s" + sleep "${HANG_HOLD}" + if [ "$(block_state)" =3D going-offline ]; then + stuck=3D1 + echo 1 > /proc/sys/kernel/sysrq 2>/dev/null || true + echo l > /proc/sysrq-trigger 2>/dev/null || true + sleep 1 + dmesg | grep -B2 -A12 "offline_pages" | tail -20 \ + | sed 's/^/# /' + fi +fi +echo "VMTEST_STUCK=3D${stuck}" + +# 5. Rescue: arm the retry limit at runtime and wait for bailout. +echo "${MAX_PASSES}" > "${PARAM}" +echo "# armed offline_migrate_max_passes=3D$(cat "${PARAM}")" + +rescued=3D0 +for _ in $(seq 1 "${RESCUE_TIMEOUT}"); do + if dmesg | grep -q "giving up after"; then + rescued=3D1 + break + fi + sleep 1 +done +dmesg | grep "giving up after" | tail -2 | sed 's/^/# /' +echo "VMTEST_RESCUED=3D${rescued}" + +sleep 2 +echo "VMTEST_BLOCK_STATE=3D$(block_state)" + +kill "${pin_pid}" 2>/dev/null +echo "VMTEST_DONE" +GUESTEOF + +# -- launch VM -----------------------------------------------------------= ----- + +( cd "${KERNEL_CHECKOUT}" && exec vng --force-9p --no-virtme-ng-init \ + --qemu-opt=3D-m --qemu-opt=3D"2G,slots=3D4,maxmem=3D8G" \ + --qemu-opt=3D-object \ + --qemu-opt=3D"memory-backend-ram,id=3Dmem0,size=3D${DIMM_SIZE}" \ + --qemu-opt=3D-device --qemu-opt=3D"pc-dimm,id=3Ddimm0,memdev=3Dmem0" \ + bash "${GUEST_SCRIPT}" ) > "${LOG}" 2>&1 & +VNG_PID=3D$! + +for _ in $(seq 1 "${BOOT_TIMEOUT}"); do + grep -q "VMTEST_DONE" "${LOG}" 2>/dev/null && break + kill -0 "${VNG_PID}" 2>/dev/null || break + sleep 1 +done + +if ! grep -q "VMTEST_DONE" "${LOG}" 2>/dev/null; then + dump_log_tail + skip_all_tests "VM did not finish within ${BOOT_TIMEOUT}s" +fi + +kill "${VNG_PID}" 2>/dev/null +wait "${VNG_PID}" 2>/dev/null +VNG_PID=3D"" + +# -- report results ------------------------------------------------------= ----- + +skip_reason=3D"$(grep "^VMTEST_SKIP: " "${LOG}" | tail -1 | tr -d '\r')" +if [ -n "${skip_reason}" ]; then + skip_all_tests "${skip_reason#VMTEST_SKIP: }" +fi + +grep "^#" "${LOG}" | tr -d '\r' | while IFS=3D read -r line; do + ktap_print_msg "${line#\# }" +done + +stuck=3D"$(guest_result VMTEST_STUCK)" +rescued=3D"$(guest_result VMTEST_RESCUED)" +blk_state=3D"$(guest_result VMTEST_BLOCK_STATE)" + +if [ "${blk_state:-GONE}" =3D "GONE" ]; then + skip_all_tests "DIMM was ejected despite the pin (pin did not hold)" +fi + +if [ "${stuck:-0}" =3D 1 ]; then + ktap_test_pass "ACPI eject kworker hangs unbounded in offline_pages()" +else + dump_log_tail + ktap_test_fail "ACPI eject kworker hangs unbounded in offline_pages()" +fi + +if [ "${rescued:-0}" =3D 1 ]; then + ktap_test_pass "stuck offline rescued by writing offline_migrate_max_pass= es" +else + dump_log_tail + ktap_test_fail "stuck offline rescued by writing offline_migrate_max_pass= es" +fi + +if [ "${blk_state}" =3D "online" ]; then + ktap_test_pass "memory block online and usable after the bailout" +else + ktap_test_fail "memory block online and usable after the bailout (state= =3D${blk_state})" +fi + +ktap_finished --=20 2.54.0