* [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure
@ 2026-08-28 9:17 Ye Liu
2026-08-28 16:54 ` Uladzislau Rezki
2026-08-28 18:05 ` Andrew Morton
0 siblings, 2 replies; 14+ messages in thread
From: Ye Liu @ 2026-08-28 9:17 UTC (permalink / raw)
To: Andrew Morton, Uladzislau Rezki; +Cc: Ye Liu, Dev Jain, linux-mm, linux-kernel
From: Ye Liu <liuye@kylinos.cn>
The vmap_purge_lock mutex can be held for an extended period by
__purge_vmap_area_lazy() which calls flush_work() to wait for
purge_vmap_node workers while holding the lock. Under memory
pressure, those workers may themselves be blocked in direct
reclaim trying to acquire the same lock via the
vmap_node_shrink_scan() shrinker callback, creating a circular
dependency that deadlocks the entire system.
Two places acquire vmap_purge_lock from paths that can be reached
during direct reclaim:
1. vmap_node_shrink_scan(): replace blocking guard(mutex) with
mutex_trylock(). This is a shrinker that only decays the vmap
pool and returns SHRINK_STOP without freeing memory; skipping a
decay cycle when the lock is contended is harmless and prevents
tasks from piling up on the mutex in the direct reclaim path.
2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with
mutex_trylock(). This is called from the vmalloc allocation
overflow path; if trylock fails, another thread is already
purging and the allocator's retry will find freed space. The
notifier chain provides a fallback if the retry still fails.
Both trylock failures break the circular dependency: the lock
holder's flush_work() can complete because workers are no longer
blocked on vmap_purge_lock in the direct reclaim path.
Fixes: 7679ba6b36db ("mm: vmalloc: add a shrinker to drain vmap pools")
Suggested-by: Uladzislau Rezki <urezki@gmail.com>
Suggested-by: Dev Jain <dev.jain@arm.com>
Signed-off-by: Ye Liu <liuye@kylinos.cn>
---
v2:
- Use mutex_trylock instead of mutex_lock to acquire vmap_purge_lock,
as suggested by Uladzislau Rezki and Dev Jain.
- Link: https://lore.kernel.org/all/20260824095020.1225189-1-ye.liu@linux.dev/
mm/vmalloc.c | 15 +++++++++++++--
1 file changed, 13 insertions(+), 2 deletions(-)
diff --git a/mm/vmalloc.c b/mm/vmalloc.c
index bea9f76ed7e7..e5c68b795a3e 100644
--- a/mm/vmalloc.c
+++ b/mm/vmalloc.c
@@ -2440,7 +2440,8 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end,
static void reclaim_and_purge_vmap_areas(void)
{
- mutex_lock(&vmap_purge_lock);
+ if (!mutex_trylock(&vmap_purge_lock))
+ return;
purge_fragmented_blocks_allcpus();
__purge_vmap_area_lazy(ULONG_MAX, 0, true);
mutex_unlock(&vmap_purge_lock);
@@ -5519,10 +5520,20 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc)
{
struct vmap_node *vn;
- guard(mutex)(&vmap_purge_lock);
+ /*
+ * This shrinker is invoked from direct reclaim where memory
+ * pressure is already high. Blocking on vmap_purge_lock here
+ * can deadlock the system: the lock holder may be blocked in
+ * flush_work() waiting for a worker that is stuck in this same
+ * reclaim path trying to acquire the same lock. Use trylock
+ * to avoid this; skipping a pool decay cycle is harmless.
+ */
+ if (!mutex_trylock(&vmap_purge_lock))
+ return SHRINK_STOP;
for_each_vmap_node(vn)
decay_va_pool_node(vn, true);
+ mutex_unlock(&vmap_purge_lock);
return SHRINK_STOP;
}
--
2.25.1
^ permalink raw reply related [flat|nested] 14+ messages in thread* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-08-28 9:17 [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure Ye Liu @ 2026-08-28 16:54 ` Uladzislau Rezki 2026-08-28 18:05 ` Andrew Morton 1 sibling, 0 replies; 14+ messages in thread From: Uladzislau Rezki @ 2026-08-28 16:54 UTC (permalink / raw) To: Ye Liu Cc: Andrew Morton, Uladzislau Rezki, Ye Liu, Dev Jain, linux-mm, linux-kernel On Fri, Aug 28, 2026 at 05:17:53PM +0800, Ye Liu wrote: > From: Ye Liu <liuye@kylinos.cn> > > The vmap_purge_lock mutex can be held for an extended period by > __purge_vmap_area_lazy() which calls flush_work() to wait for > purge_vmap_node workers while holding the lock. Under memory > pressure, those workers may themselves be blocked in direct > reclaim trying to acquire the same lock via the > vmap_node_shrink_scan() shrinker callback, creating a circular > dependency that deadlocks the entire system. > > Two places acquire vmap_purge_lock from paths that can be reached > during direct reclaim: > > 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > mutex_trylock(). This is a shrinker that only decays the vmap > pool and returns SHRINK_STOP without freeing memory; skipping a > decay cycle when the lock is contended is harmless and prevents > tasks from piling up on the mutex in the direct reclaim path. > > 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > mutex_trylock(). This is called from the vmalloc allocation > overflow path; if trylock fails, another thread is already > purging and the allocator's retry will find freed space. The > notifier chain provides a fallback if the retry still fails. > > Both trylock failures break the circular dependency: the lock > holder's flush_work() can complete because workers are no longer > blocked on vmap_purge_lock in the direct reclaim path. > > Fixes: 7679ba6b36db ("mm: vmalloc: add a shrinker to drain vmap pools") > Suggested-by: Uladzislau Rezki <urezki@gmail.com> > Suggested-by: Dev Jain <dev.jain@arm.com> > Signed-off-by: Ye Liu <liuye@kylinos.cn> > --- > v2: > - Use mutex_trylock instead of mutex_lock to acquire vmap_purge_lock, > as suggested by Uladzislau Rezki and Dev Jain. > - Link: https://lore.kernel.org/all/20260824095020.1225189-1-ye.liu@linux.dev/ > mm/vmalloc.c | 15 +++++++++++++-- > 1 file changed, 13 insertions(+), 2 deletions(-) > > diff --git a/mm/vmalloc.c b/mm/vmalloc.c > index bea9f76ed7e7..e5c68b795a3e 100644 > --- a/mm/vmalloc.c > +++ b/mm/vmalloc.c > @@ -2440,7 +2440,8 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end, > static void reclaim_and_purge_vmap_areas(void) > > { > - mutex_lock(&vmap_purge_lock); > + if (!mutex_trylock(&vmap_purge_lock)) > + return; > purge_fragmented_blocks_allcpus(); > __purge_vmap_area_lazy(ULONG_MAX, 0, true); > mutex_unlock(&vmap_purge_lock); > @@ -5519,10 +5520,20 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > { > struct vmap_node *vn; > > - guard(mutex)(&vmap_purge_lock); > + /* > + * This shrinker is invoked from direct reclaim where memory > + * pressure is already high. Blocking on vmap_purge_lock here > + * can deadlock the system: the lock holder may be blocked in > + * flush_work() waiting for a worker that is stuck in this same > + * reclaim path trying to acquire the same lock. Use trylock > + * to avoid this; skipping a pool decay cycle is harmless. > + */ > + if (!mutex_trylock(&vmap_purge_lock)) > + return SHRINK_STOP; > for_each_vmap_node(vn) > decay_va_pool_node(vn, true); > > + mutex_unlock(&vmap_purge_lock); > return SHRINK_STOP; > } > > -- > 2.25.1 > Reviewed-by: Uladzislau Rezki (Sony) <urezki@gmail.com> -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-08-28 9:17 [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure Ye Liu 2026-08-28 16:54 ` Uladzislau Rezki @ 2026-08-28 18:05 ` Andrew Morton 2026-08-31 6:09 ` Dev Jain 1 sibling, 1 reply; 14+ messages in thread From: Andrew Morton @ 2026-08-28 18:05 UTC (permalink / raw) To: Ye Liu; +Cc: Uladzislau Rezki, Ye Liu, Dev Jain, linux-mm, linux-kernel On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > From: Ye Liu <liuye@kylinos.cn> > > The vmap_purge_lock mutex can be held for an extended period by > __purge_vmap_area_lazy() which calls flush_work() to wait for > purge_vmap_node workers while holding the lock. Under memory > pressure, those workers may themselves be blocked in direct > reclaim trying to acquire the same lock via the > vmap_node_shrink_scan() shrinker callback, creating a circular > dependency that deadlocks the entire system. > > Two places acquire vmap_purge_lock from paths that can be reached > during direct reclaim: > > 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > mutex_trylock(). This is a shrinker that only decays the vmap > pool and returns SHRINK_STOP without freeing memory; skipping a > decay cycle when the lock is contended is harmless and prevents > tasks from piling up on the mutex in the direct reclaim path. > > 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > mutex_trylock(). This is called from the vmalloc allocation > overflow path; if trylock fails, another thread is already > purging and the allocator's retry will find freed space. The > notifier chain provides a fallback if the retry still fails. > > Both trylock failures break the circular dependency: the lock > holder's flush_work() can complete because workers are no longer > blocked on vmap_purge_lock in the direct reclaim path. Thanks. AI review expressed a couple of concerns: https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-08-28 18:05 ` Andrew Morton @ 2026-08-31 6:09 ` Dev Jain 2026-08-31 9:54 ` Uladzislau Rezki 0 siblings, 1 reply; 14+ messages in thread From: Dev Jain @ 2026-08-31 6:09 UTC (permalink / raw) To: Andrew Morton, Ye Liu; +Cc: Uladzislau Rezki, Ye Liu, linux-mm, linux-kernel On 28/08/26 11:35 pm, Andrew Morton wrote: > On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > >> From: Ye Liu <liuye@kylinos.cn> >> >> The vmap_purge_lock mutex can be held for an extended period by >> __purge_vmap_area_lazy() which calls flush_work() to wait for >> purge_vmap_node workers while holding the lock. Under memory >> pressure, those workers may themselves be blocked in direct >> reclaim trying to acquire the same lock via the >> vmap_node_shrink_scan() shrinker callback, creating a circular >> dependency that deadlocks the entire system. >> >> Two places acquire vmap_purge_lock from paths that can be reached >> during direct reclaim: >> >> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with >> mutex_trylock(). This is a shrinker that only decays the vmap >> pool and returns SHRINK_STOP without freeing memory; skipping a >> decay cycle when the lock is contended is harmless and prevents >> tasks from piling up on the mutex in the direct reclaim path. >> >> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with >> mutex_trylock(). This is called from the vmalloc allocation >> overflow path; if trylock fails, another thread is already >> purging and the allocator's retry will find freed space. The >> notifier chain provides a fallback if the retry still fails. >> >> Both trylock failures break the circular dependency: the lock >> holder's flush_work() can complete because workers are no longer >> blocked on vmap_purge_lock in the direct reclaim path. > > Thanks. AI review expressed a couple of concerns: > https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev Sounds legit to me. Now there is no guarantee of purge being successful, and we can get a spurious failure. How about using a WQ_RECLAIM workqueue: vmap_purge_wq = alloc_workqueue("vmap_purge", WQ_MEM_RECLAIM | WQ_PERCPU, 0); I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-08-31 6:09 ` Dev Jain @ 2026-08-31 9:54 ` Uladzislau Rezki 2026-09-01 1:42 ` Andrew Morton 2026-09-01 6:22 ` Dev Jain 0 siblings, 2 replies; 14+ messages in thread From: Uladzislau Rezki @ 2026-08-31 9:54 UTC (permalink / raw) To: Dev Jain, Ye Liu Cc: Andrew Morton, Ye Liu, Uladzislau Rezki, Ye Liu, linux-mm, linux-kernel On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: > > > On 28/08/26 11:35 pm, Andrew Morton wrote: > > On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > > > >> From: Ye Liu <liuye@kylinos.cn> > >> > >> The vmap_purge_lock mutex can be held for an extended period by > >> __purge_vmap_area_lazy() which calls flush_work() to wait for > >> purge_vmap_node workers while holding the lock. Under memory > >> pressure, those workers may themselves be blocked in direct > >> reclaim trying to acquire the same lock via the > >> vmap_node_shrink_scan() shrinker callback, creating a circular > >> dependency that deadlocks the entire system. > >> > >> Two places acquire vmap_purge_lock from paths that can be reached > >> during direct reclaim: > >> > >> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > >> mutex_trylock(). This is a shrinker that only decays the vmap > >> pool and returns SHRINK_STOP without freeing memory; skipping a > >> decay cycle when the lock is contended is harmless and prevents > >> tasks from piling up on the mutex in the direct reclaim path. > >> > >> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > >> mutex_trylock(). This is called from the vmalloc allocation > >> overflow path; if trylock fails, another thread is already > >> purging and the allocator's retry will find freed space. The > >> notifier chain provides a fallback if the retry still fails. > >> > >> Both trylock failures break the circular dependency: the lock > >> holder's flush_work() can complete because workers are no longer > >> blocked on vmap_purge_lock in the direct reclaim path. > > > > Thanks. AI review expressed a couple of concerns: > > https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev > > > Sounds legit to me. Now there is no guarantee of purge being successful, and we > can get a spurious failure. > > How about using a WQ_RECLAIM workqueue: > > vmap_purge_wq = alloc_workqueue("vmap_purge", > WQ_MEM_RECLAIM | WQ_PERCPU, 0); > > I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). > WQ_MEM_RECLAIM makes sense but this is another patch. I copied here AI comment: > > Does replacing this blocking lock with a trylock break synchronization for > the callers? > No it does not. If someone is doing reclaim we do not wait and do not try to do it again thus fail allocation. > When vmalloc space is exhausted, alloc_vmap_area() calls > reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure > that free space has actually been reclaimed before looping back to retry: > mm/vmalloc.c:alloc_vmap_area() { > ... > overflow: > if (!purged) { > reclaim_and_purge_vmap_areas(); > purged = 1; > goto retry; > } > ... >} > With this patch, if another thread holds vmap_purge_lock, mutex_trylock() > fails and the function returns immediately. > If reclaim is in progress and trylock fails a caller repeats only one time to retry an allocation. There is no any infinite loop. > > The allocator then retries > instantly without waiting for the concurrent purge to complete. > Because the retry fails and purged is already 1, could this cause the > allocation to abort and return a spurious vmalloc allocation failure > (-EBUSY or -ENOMEM)? > vmap space can be fragmented and not avail for 32-bit systems. For 64-bit system it is likely impossible. But, i think we can overt mutex_lock() into mutex_trylock() just only in the: static unsigned long vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) { struct vmap_node *vn; guard(mutex)(&vmap_purge_lock); for_each_vmap_node(vn) decay_va_pool_node(vn, true); return SHRINK_STOP; } so the reclaim path is not blocked. It should also address an issue reported by the Ye Liu <ye.liu@linux.dev>. -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-08-31 9:54 ` Uladzislau Rezki @ 2026-09-01 1:42 ` Andrew Morton 2026-09-01 16:36 ` Uladzislau Rezki 2026-09-01 6:22 ` Dev Jain 1 sibling, 1 reply; 14+ messages in thread From: Andrew Morton @ 2026-09-01 1:42 UTC (permalink / raw) To: Uladzislau Rezki; +Cc: Dev Jain, Ye Liu, Ye Liu, linux-mm, linux-kernel On Mon, 31 Aug 2026 11:54:22 +0200 Uladzislau Rezki <urezki@gmail.com> wrote: > But, i think we can overt mutex_lock() into mutex_trylock() just only > in the: > > static unsigned long > vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > { > struct vmap_node *vn; > > guard(mutex)(&vmap_purge_lock); > for_each_vmap_node(vn) > decay_va_pool_node(vn, true); > > return SHRINK_STOP; > } I *think* that means we can expect a v3. Ho hum. I guess I'll queue v2, under the assumption that my assumptions are usually wrong. ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-01 1:42 ` Andrew Morton @ 2026-09-01 16:36 ` Uladzislau Rezki 0 siblings, 0 replies; 14+ messages in thread From: Uladzislau Rezki @ 2026-09-01 16:36 UTC (permalink / raw) To: Andrew Morton Cc: Uladzislau Rezki, Dev Jain, Ye Liu, Ye Liu, linux-mm, linux-kernel On Mon, Aug 31, 2026 at 06:42:52PM -0700, Andrew Morton wrote: > On Mon, 31 Aug 2026 11:54:22 +0200 Uladzislau Rezki <urezki@gmail.com> wrote: > > > But, i think we can overt mutex_lock() into mutex_trylock() just only > > in the: > > > > static unsigned long > > vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > > { > > struct vmap_node *vn; > > > > guard(mutex)(&vmap_purge_lock); > > for_each_vmap_node(vn) > > decay_va_pool_node(vn, true); > > > > return SHRINK_STOP; > > } > > I *think* that means we can expect a v3. > > Ho hum. I guess I'll queue v2, under the assumption that my > assumptions are usually wrong. > Yep :) -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-08-31 9:54 ` Uladzislau Rezki 2026-09-01 1:42 ` Andrew Morton @ 2026-09-01 6:22 ` Dev Jain 2026-09-01 9:07 ` Ye Liu 2026-09-01 16:40 ` Uladzislau Rezki 1 sibling, 2 replies; 14+ messages in thread From: Dev Jain @ 2026-09-01 6:22 UTC (permalink / raw) To: Uladzislau Rezki, Ye Liu; +Cc: Andrew Morton, Ye Liu, linux-mm, linux-kernel On 31/08/26 3:24 pm, Uladzislau Rezki wrote: > On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: >> >> >> On 28/08/26 11:35 pm, Andrew Morton wrote: >>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: >>> >>>> From: Ye Liu <liuye@kylinos.cn> >>>> >>>> The vmap_purge_lock mutex can be held for an extended period by >>>> __purge_vmap_area_lazy() which calls flush_work() to wait for >>>> purge_vmap_node workers while holding the lock. Under memory >>>> pressure, those workers may themselves be blocked in direct >>>> reclaim trying to acquire the same lock via the >>>> vmap_node_shrink_scan() shrinker callback, creating a circular >>>> dependency that deadlocks the entire system. >>>> >>>> Two places acquire vmap_purge_lock from paths that can be reached >>>> during direct reclaim: >>>> >>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with >>>> mutex_trylock(). This is a shrinker that only decays the vmap >>>> pool and returns SHRINK_STOP without freeing memory; skipping a >>>> decay cycle when the lock is contended is harmless and prevents >>>> tasks from piling up on the mutex in the direct reclaim path. >>>> >>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with >>>> mutex_trylock(). This is called from the vmalloc allocation >>>> overflow path; if trylock fails, another thread is already >>>> purging and the allocator's retry will find freed space. The >>>> notifier chain provides a fallback if the retry still fails. >>>> >>>> Both trylock failures break the circular dependency: the lock >>>> holder's flush_work() can complete because workers are no longer >>>> blocked on vmap_purge_lock in the direct reclaim path. >>> >>> Thanks. AI review expressed a couple of concerns: >>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev >> >> >> Sounds legit to me. Now there is no guarantee of purge being successful, and we >> can get a spurious failure. >> >> How about using a WQ_RECLAIM workqueue: >> >> vmap_purge_wq = alloc_workqueue("vmap_purge", >> WQ_MEM_RECLAIM | WQ_PERCPU, 0); >> >> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). >> > WQ_MEM_RECLAIM makes sense but this is another patch. > > I copied here AI comment: >> >> Does replacing this blocking lock with a trylock break synchronization for >> the callers? >> > No it does not. If someone is doing reclaim we do not wait and do not try > to do it again thus fail allocation. > >> When vmalloc space is exhausted, alloc_vmap_area() calls >> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure >> that free space has actually been reclaimed before looping back to retry: >> mm/vmalloc.c:alloc_vmap_area() { >> ... >> overflow: >> if (!purged) { >> reclaim_and_purge_vmap_areas(); >> purged = 1; >> goto retry; >> } >> ... >> } >> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() >> fails and the function returns immediately. >> > If reclaim is in progress and trylock fails a caller repeats only one > time to retry an allocation. There is no any infinite loop. > >> >> The allocator then retries >> instantly without waiting for the concurrent purge to complete. >> Because the retry fails and purged is already 1, could this cause the >> allocation to abort and return a spurious vmalloc allocation failure >> (-EBUSY or -ENOMEM)? >> > vmap space can be fragmented and not avail for 32-bit systems. For > 64-bit system it is likely impossible. > > But, i think we can overt mutex_lock() into mutex_trylock() just only > in the: > > static unsigned long > vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > { > struct vmap_node *vn; > > guard(mutex)(&vmap_purge_lock); > for_each_vmap_node(vn) > decay_va_pool_node(vn, true); > > return SHRINK_STOP; > } > > so the reclaim path is not blocked. It should also address an issue > reported by the Ye Liu <ye.liu@linux.dev>. IIUC you are suggesting mutex_trylock() only in the shrinker path. But then, the following is possible no: take purge lock, try to get a worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area -> reclaim_and_purge_vmap_areas -> take purge lock? > > -- > Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-01 6:22 ` Dev Jain @ 2026-09-01 9:07 ` Ye Liu 2026-09-01 16:43 ` Uladzislau Rezki 2026-09-01 16:40 ` Uladzislau Rezki 1 sibling, 1 reply; 14+ messages in thread From: Ye Liu @ 2026-09-01 9:07 UTC (permalink / raw) To: Dev Jain, Uladzislau Rezki; +Cc: Andrew Morton, Ye Liu, linux-mm, linux-kernel 在 2026/9/1 14:22, Dev Jain 写道: > > > On 31/08/26 3:24 pm, Uladzislau Rezki wrote: >> On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: >>> >>> >>> On 28/08/26 11:35 pm, Andrew Morton wrote: >>>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: >>>> >>>>> From: Ye Liu <liuye@kylinos.cn> >>>>> >>>>> The vmap_purge_lock mutex can be held for an extended period by >>>>> __purge_vmap_area_lazy() which calls flush_work() to wait for >>>>> purge_vmap_node workers while holding the lock. Under memory >>>>> pressure, those workers may themselves be blocked in direct >>>>> reclaim trying to acquire the same lock via the >>>>> vmap_node_shrink_scan() shrinker callback, creating a circular >>>>> dependency that deadlocks the entire system. >>>>> >>>>> Two places acquire vmap_purge_lock from paths that can be reached >>>>> during direct reclaim: >>>>> >>>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with >>>>> mutex_trylock(). This is a shrinker that only decays the vmap >>>>> pool and returns SHRINK_STOP without freeing memory; skipping a >>>>> decay cycle when the lock is contended is harmless and prevents >>>>> tasks from piling up on the mutex in the direct reclaim path. >>>>> >>>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with >>>>> mutex_trylock(). This is called from the vmalloc allocation >>>>> overflow path; if trylock fails, another thread is already >>>>> purging and the allocator's retry will find freed space. The >>>>> notifier chain provides a fallback if the retry still fails. >>>>> >>>>> Both trylock failures break the circular dependency: the lock >>>>> holder's flush_work() can complete because workers are no longer >>>>> blocked on vmap_purge_lock in the direct reclaim path. >>>> >>>> Thanks. AI review expressed a couple of concerns: >>>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev >>> >>> >>> Sounds legit to me. Now there is no guarantee of purge being successful, and we >>> can get a spurious failure. >>> >>> How about using a WQ_RECLAIM workqueue: >>> >>> vmap_purge_wq = alloc_workqueue("vmap_purge", >>> WQ_MEM_RECLAIM | WQ_PERCPU, 0); >>> >>> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). >>> >> WQ_MEM_RECLAIM makes sense but this is another patch. >> >> I copied here AI comment: >>> >>> Does replacing this blocking lock with a trylock break synchronization for >>> the callers? >>> >> No it does not. If someone is doing reclaim we do not wait and do not try >> to do it again thus fail allocation. >> >>> When vmalloc space is exhausted, alloc_vmap_area() calls >>> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure >>> that free space has actually been reclaimed before looping back to retry: >>> mm/vmalloc.c:alloc_vmap_area() { >>> ... >>> overflow: >>> if (!purged) { >>> reclaim_and_purge_vmap_areas(); >>> purged = 1; >>> goto retry; >>> } >>> ... >>> } >>> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() >>> fails and the function returns immediately. >>> >> If reclaim is in progress and trylock fails a caller repeats only one >> time to retry an allocation. There is no any infinite loop. >> >>> >>> The allocator then retries >>> instantly without waiting for the concurrent purge to complete. >>> Because the retry fails and purged is already 1, could this cause the >>> allocation to abort and return a spurious vmalloc allocation failure >>> (-EBUSY or -ENOMEM)? >>> >> vmap space can be fragmented and not avail for 32-bit systems. For >> 64-bit system it is likely impossible. >> >> But, i think we can overt mutex_lock() into mutex_trylock() just only >> in the: >> >> static unsigned long >> vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) >> { >> struct vmap_node *vn; >> >> guard(mutex)(&vmap_purge_lock); >> for_each_vmap_node(vn) >> decay_va_pool_node(vn, true); >> >> return SHRINK_STOP; >> } >> >> so the reclaim path is not blocked. It should also address an issue >> reported by the Ye Liu <ye.liu@linux.dev>. > > IIUC you are suggesting mutex_trylock() only in the shrinker path. But > then, the following is possible no: take purge lock, try to get a > worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area > -> reclaim_and_purge_vmap_areas -> take purge lock? > > Personally, I lean toward the WQ_MEM_RECLAIM workqueue solution. After taking a closer look at the code, I noticed a subtle but potentially problematic scenario: When drain_vmap_area_work acquires vmap_purge_lock and calls into __purge_vmap_area_lazy, it may subsequently invoke queue_work/queue_work_on on the same CPU's system_wq. If the newly queued work ends up waiting for an available worker on that same CPU, while the current worker is blocked waiting for that very work to complete (via flush_work), we could end up with a self-deadlock on a single CPU. Theoretically, this seems possible. I suspect the reason we don't see widespread reports of such deadlocks is that the nr_purge_helpers logic limits the number of asynchronous workers; when resources are tight, it falls back to synchronous execution (purge_vmap_node directly), which avoids queuing additional work. Using a dedicated workqueue with WQ_MEM_RECLAIM would provide a clean, explicit isolation—ensuring forward progress under memory pressure and eliminating the risk of interfering with other subsystems' workqueues. I believe this approach is more robust in the long run. Perhaps like the code below: the dedicated queue eliminates the self‑deadlock risk, and the trylock in the shrinker prevents recursive lock attempts from reclaim contexts. diff --git a/mm/vmalloc.c b/mm/vmalloc.c index bea9f76ed7e7..68fc1f5acb2f 100644 --- a/mm/vmalloc.c +++ b/mm/vmalloc.c @@ -2218,6 +2218,9 @@ static unsigned long lazy_max_pages(void) */ static DEFINE_MUTEX(vmap_purge_lock); +/* Workqueue for lazy vmap purging; WQ_MEM_RECLAIM guarantees progress. */ +static struct workqueue_struct *vmap_purge_wq; + /* for per-CPU blocks */ static void purge_fragmented_blocks_allcpus(void); @@ -2408,9 +2411,9 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end, INIT_WORK(&vn->purge_work, purge_vmap_node); if (cpumask_test_cpu(i, cpu_online_mask)) - schedule_work_on(i, &vn->purge_work); + queue_work_on(i, vmap_purge_wq, &vn->purge_work); else - schedule_work(&vn->purge_work); + queue_work(vmap_purge_wq, &vn->purge_work); nr_purge_helpers--; } else { @@ -5519,10 +5522,14 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) { struct vmap_node *vn; - guard(mutex)(&vmap_purge_lock); + if (!mutex_trylock(&vmap_purge_lock)) + return SHRINK_STOP; + for_each_vmap_node(vn) decay_va_pool_node(vn, true); + mutex_unlock(&vmap_purge_lock); + return SHRINK_STOP; } @@ -5575,6 +5582,17 @@ void __init vmalloc_init(void) * Now we can initialize a free vmap space. */ vmap_init_free_space(); + + /* + * A dedicated workqueue for lazy vmap purging. WQ_MEM_RECLAIM + * reserves a rescue worker so queued purge work items are executed + * even under memory pressure, when workers of the system workqueue + * may be stuck in direct reclaim. + */ + vmap_purge_wq = alloc_workqueue("vmap_purge", + WQ_MEM_RECLAIM | WQ_PERCPU, 0); + WARN_ON(!vmap_purge_wq); + vmap_initialized = true; >> >> -- >> Uladzislau Rezki > -- Thanks, Ye Liu ^ permalink raw reply related [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-01 9:07 ` Ye Liu @ 2026-09-01 16:43 ` Uladzislau Rezki 2026-09-01 16:59 ` Uladzislau Rezki 0 siblings, 1 reply; 14+ messages in thread From: Uladzislau Rezki @ 2026-09-01 16:43 UTC (permalink / raw) To: Ye Liu Cc: Dev Jain, Uladzislau Rezki, Andrew Morton, Ye Liu, linux-mm, linux-kernel On Tue, Sep 01, 2026 at 05:07:30PM +0800, Ye Liu wrote: > > > 在 2026/9/1 14:22, Dev Jain 写道: > > > > > > On 31/08/26 3:24 pm, Uladzislau Rezki wrote: > >> On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: > >>> > >>> > >>> On 28/08/26 11:35 pm, Andrew Morton wrote: > >>>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > >>>> > >>>>> From: Ye Liu <liuye@kylinos.cn> > >>>>> > >>>>> The vmap_purge_lock mutex can be held for an extended period by > >>>>> __purge_vmap_area_lazy() which calls flush_work() to wait for > >>>>> purge_vmap_node workers while holding the lock. Under memory > >>>>> pressure, those workers may themselves be blocked in direct > >>>>> reclaim trying to acquire the same lock via the > >>>>> vmap_node_shrink_scan() shrinker callback, creating a circular > >>>>> dependency that deadlocks the entire system. > >>>>> > >>>>> Two places acquire vmap_purge_lock from paths that can be reached > >>>>> during direct reclaim: > >>>>> > >>>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > >>>>> mutex_trylock(). This is a shrinker that only decays the vmap > >>>>> pool and returns SHRINK_STOP without freeing memory; skipping a > >>>>> decay cycle when the lock is contended is harmless and prevents > >>>>> tasks from piling up on the mutex in the direct reclaim path. > >>>>> > >>>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > >>>>> mutex_trylock(). This is called from the vmalloc allocation > >>>>> overflow path; if trylock fails, another thread is already > >>>>> purging and the allocator's retry will find freed space. The > >>>>> notifier chain provides a fallback if the retry still fails. > >>>>> > >>>>> Both trylock failures break the circular dependency: the lock > >>>>> holder's flush_work() can complete because workers are no longer > >>>>> blocked on vmap_purge_lock in the direct reclaim path. > >>>> > >>>> Thanks. AI review expressed a couple of concerns: > >>>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev > >>> > >>> > >>> Sounds legit to me. Now there is no guarantee of purge being successful, and we > >>> can get a spurious failure. > >>> > >>> How about using a WQ_RECLAIM workqueue: > >>> > >>> vmap_purge_wq = alloc_workqueue("vmap_purge", > >>> WQ_MEM_RECLAIM | WQ_PERCPU, 0); > >>> > >>> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). > >>> > >> WQ_MEM_RECLAIM makes sense but this is another patch. > >> > >> I copied here AI comment: > >>> > >>> Does replacing this blocking lock with a trylock break synchronization for > >>> the callers? > >>> > >> No it does not. If someone is doing reclaim we do not wait and do not try > >> to do it again thus fail allocation. > >> > >>> When vmalloc space is exhausted, alloc_vmap_area() calls > >>> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure > >>> that free space has actually been reclaimed before looping back to retry: > >>> mm/vmalloc.c:alloc_vmap_area() { > >>> ... > >>> overflow: > >>> if (!purged) { > >>> reclaim_and_purge_vmap_areas(); > >>> purged = 1; > >>> goto retry; > >>> } > >>> ... > >>> } > >>> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() > >>> fails and the function returns immediately. > >>> > >> If reclaim is in progress and trylock fails a caller repeats only one > >> time to retry an allocation. There is no any infinite loop. > >> > >>> > >>> The allocator then retries > >>> instantly without waiting for the concurrent purge to complete. > >>> Because the retry fails and purged is already 1, could this cause the > >>> allocation to abort and return a spurious vmalloc allocation failure > >>> (-EBUSY or -ENOMEM)? > >>> > >> vmap space can be fragmented and not avail for 32-bit systems. For > >> 64-bit system it is likely impossible. > >> > >> But, i think we can overt mutex_lock() into mutex_trylock() just only > >> in the: > >> > >> static unsigned long > >> vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > >> { > >> struct vmap_node *vn; > >> > >> guard(mutex)(&vmap_purge_lock); > >> for_each_vmap_node(vn) > >> decay_va_pool_node(vn, true); > >> > >> return SHRINK_STOP; > >> } > >> > >> so the reclaim path is not blocked. It should also address an issue > >> reported by the Ye Liu <ye.liu@linux.dev>. > > > > IIUC you are suggesting mutex_trylock() only in the shrinker path. But > > then, the following is possible no: take purge lock, try to get a > > worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area > > -> reclaim_and_purge_vmap_areas -> take purge lock? > > > > > > Personally, I lean toward the WQ_MEM_RECLAIM workqueue solution. > After taking a closer look at the code, I noticed a subtle but > potentially problematic scenario: > > When drain_vmap_area_work acquires vmap_purge_lock and calls into > __purge_vmap_area_lazy, it may subsequently invoke queue_work/queue_work_on > on the same CPU's system_wq. If the newly queued work ends up waiting > for an available worker on that same CPU, while the current worker is > blocked waiting for that very work to complete (via flush_work), > we could end up with a self-deadlock on a single CPU. > > Theoretically, this seems possible. I suspect the reason we don't > see widespread reports of such deadlocks is that the nr_purge_helpers > logic limits the number of asynchronous workers; when resources are tight, > it falls back to synchronous execution (purge_vmap_node directly), > which avoids queuing additional work. > > Using a dedicated workqueue with WQ_MEM_RECLAIM would provide a clean, > explicit isolation—ensuring forward progress under memory pressure and > eliminating the risk of interfering with other subsystems' workqueues. > I believe this approach is more robust in the long run. > > Perhaps like the code below: > the dedicated queue eliminates the self‑deadlock risk, and the trylock > in the shrinker prevents recursive lock attempts from reclaim contexts. > > diff --git a/mm/vmalloc.c b/mm/vmalloc.c > index bea9f76ed7e7..68fc1f5acb2f 100644 > --- a/mm/vmalloc.c > +++ b/mm/vmalloc.c > @@ -2218,6 +2218,9 @@ static unsigned long lazy_max_pages(void) > */ > static DEFINE_MUTEX(vmap_purge_lock); > > +/* Workqueue for lazy vmap purging; WQ_MEM_RECLAIM guarantees progress. */ > +static struct workqueue_struct *vmap_purge_wq; > + > /* for per-CPU blocks */ > static void purge_fragmented_blocks_allcpus(void); > > @@ -2408,9 +2411,9 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end, > INIT_WORK(&vn->purge_work, purge_vmap_node); > > if (cpumask_test_cpu(i, cpu_online_mask)) > - schedule_work_on(i, &vn->purge_work); > + queue_work_on(i, vmap_purge_wq, &vn->purge_work); > else > - schedule_work(&vn->purge_work); > + queue_work(vmap_purge_wq, &vn->purge_work); > > nr_purge_helpers--; > } else { > @@ -5519,10 +5522,14 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > { > struct vmap_node *vn; > > - guard(mutex)(&vmap_purge_lock); > + if (!mutex_trylock(&vmap_purge_lock)) > + return SHRINK_STOP; > + > for_each_vmap_node(vn) > decay_va_pool_node(vn, true); > > + mutex_unlock(&vmap_purge_lock); > + > return SHRINK_STOP; > } > > @@ -5575,6 +5582,17 @@ void __init vmalloc_init(void) > * Now we can initialize a free vmap space. > */ > vmap_init_free_space(); > + > + /* > + * A dedicated workqueue for lazy vmap purging. WQ_MEM_RECLAIM > + * reserves a rescue worker so queued purge work items are executed > + * even under memory pressure, when workers of the system workqueue > + * may be stuck in direct reclaim. > + */ > + vmap_purge_wq = alloc_workqueue("vmap_purge", > + WQ_MEM_RECLAIM | WQ_PERCPU, 0); > + WARN_ON(!vmap_purge_wq); > + > vmap_initialized = true; > I agree. We should have it and it should be as separate patch, i.e. split vmap_node_shrink_scan() and dedicated per-cpu WQs per vmap drain. -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-01 16:43 ` Uladzislau Rezki @ 2026-09-01 16:59 ` Uladzislau Rezki 2026-09-02 4:30 ` Dev Jain 0 siblings, 1 reply; 14+ messages in thread From: Uladzislau Rezki @ 2026-09-01 16:59 UTC (permalink / raw) To: Uladzislau Rezki, Andrew Morton Cc: Ye Liu, Dev Jain, Andrew Morton, Ye Liu, linux-mm, linux-kernel On Tue, Sep 01, 2026 at 06:43:43PM +0200, Uladzislau Rezki wrote: > On Tue, Sep 01, 2026 at 05:07:30PM +0800, Ye Liu wrote: > > > > > > 在 2026/9/1 14:22, Dev Jain 写道: > > > > > > > > > On 31/08/26 3:24 pm, Uladzislau Rezki wrote: > > >> On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: > > >>> > > >>> > > >>> On 28/08/26 11:35 pm, Andrew Morton wrote: > > >>>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > > >>>> > > >>>>> From: Ye Liu <liuye@kylinos.cn> > > >>>>> > > >>>>> The vmap_purge_lock mutex can be held for an extended period by > > >>>>> __purge_vmap_area_lazy() which calls flush_work() to wait for > > >>>>> purge_vmap_node workers while holding the lock. Under memory > > >>>>> pressure, those workers may themselves be blocked in direct > > >>>>> reclaim trying to acquire the same lock via the > > >>>>> vmap_node_shrink_scan() shrinker callback, creating a circular > > >>>>> dependency that deadlocks the entire system. > > >>>>> > > >>>>> Two places acquire vmap_purge_lock from paths that can be reached > > >>>>> during direct reclaim: > > >>>>> > > >>>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > > >>>>> mutex_trylock(). This is a shrinker that only decays the vmap > > >>>>> pool and returns SHRINK_STOP without freeing memory; skipping a > > >>>>> decay cycle when the lock is contended is harmless and prevents > > >>>>> tasks from piling up on the mutex in the direct reclaim path. > > >>>>> > > >>>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > > >>>>> mutex_trylock(). This is called from the vmalloc allocation > > >>>>> overflow path; if trylock fails, another thread is already > > >>>>> purging and the allocator's retry will find freed space. The > > >>>>> notifier chain provides a fallback if the retry still fails. > > >>>>> > > >>>>> Both trylock failures break the circular dependency: the lock > > >>>>> holder's flush_work() can complete because workers are no longer > > >>>>> blocked on vmap_purge_lock in the direct reclaim path. > > >>>> > > >>>> Thanks. AI review expressed a couple of concerns: > > >>>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev > > >>> > > >>> > > >>> Sounds legit to me. Now there is no guarantee of purge being successful, and we > > >>> can get a spurious failure. > > >>> > > >>> How about using a WQ_RECLAIM workqueue: > > >>> > > >>> vmap_purge_wq = alloc_workqueue("vmap_purge", > > >>> WQ_MEM_RECLAIM | WQ_PERCPU, 0); > > >>> > > >>> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). > > >>> > > >> WQ_MEM_RECLAIM makes sense but this is another patch. > > >> > > >> I copied here AI comment: > > >>> > > >>> Does replacing this blocking lock with a trylock break synchronization for > > >>> the callers? > > >>> > > >> No it does not. If someone is doing reclaim we do not wait and do not try > > >> to do it again thus fail allocation. > > >> > > >>> When vmalloc space is exhausted, alloc_vmap_area() calls > > >>> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure > > >>> that free space has actually been reclaimed before looping back to retry: > > >>> mm/vmalloc.c:alloc_vmap_area() { > > >>> ... > > >>> overflow: > > >>> if (!purged) { > > >>> reclaim_and_purge_vmap_areas(); > > >>> purged = 1; > > >>> goto retry; > > >>> } > > >>> ... > > >>> } > > >>> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() > > >>> fails and the function returns immediately. > > >>> > > >> If reclaim is in progress and trylock fails a caller repeats only one > > >> time to retry an allocation. There is no any infinite loop. > > >> > > >>> > > >>> The allocator then retries > > >>> instantly without waiting for the concurrent purge to complete. > > >>> Because the retry fails and purged is already 1, could this cause the > > >>> allocation to abort and return a spurious vmalloc allocation failure > > >>> (-EBUSY or -ENOMEM)? > > >>> > > >> vmap space can be fragmented and not avail for 32-bit systems. For > > >> 64-bit system it is likely impossible. > > >> > > >> But, i think we can overt mutex_lock() into mutex_trylock() just only > > >> in the: > > >> > > >> static unsigned long > > >> vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > > >> { > > >> struct vmap_node *vn; > > >> > > >> guard(mutex)(&vmap_purge_lock); > > >> for_each_vmap_node(vn) > > >> decay_va_pool_node(vn, true); > > >> > > >> return SHRINK_STOP; > > >> } > > >> > > >> so the reclaim path is not blocked. It should also address an issue > > >> reported by the Ye Liu <ye.liu@linux.dev>. > > > > > > IIUC you are suggesting mutex_trylock() only in the shrinker path. But > > > then, the following is possible no: take purge lock, try to get a > > > worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area > > > -> reclaim_and_purge_vmap_areas -> take purge lock? > > > > > > > > > > Personally, I lean toward the WQ_MEM_RECLAIM workqueue solution. > > After taking a closer look at the code, I noticed a subtle but > > potentially problematic scenario: > > > > When drain_vmap_area_work acquires vmap_purge_lock and calls into > > __purge_vmap_area_lazy, it may subsequently invoke queue_work/queue_work_on > > on the same CPU's system_wq. If the newly queued work ends up waiting > > for an available worker on that same CPU, while the current worker is > > blocked waiting for that very work to complete (via flush_work), > > we could end up with a self-deadlock on a single CPU. > > > > Theoretically, this seems possible. I suspect the reason we don't > > see widespread reports of such deadlocks is that the nr_purge_helpers > > logic limits the number of asynchronous workers; when resources are tight, > > it falls back to synchronous execution (purge_vmap_node directly), > > which avoids queuing additional work. > > > > Using a dedicated workqueue with WQ_MEM_RECLAIM would provide a clean, > > explicit isolation—ensuring forward progress under memory pressure and > > eliminating the risk of interfering with other subsystems' workqueues. > > I believe this approach is more robust in the long run. > > > > Perhaps like the code below: > > the dedicated queue eliminates the self‑deadlock risk, and the trylock > > in the shrinker prevents recursive lock attempts from reclaim contexts. > > > > diff --git a/mm/vmalloc.c b/mm/vmalloc.c > > index bea9f76ed7e7..68fc1f5acb2f 100644 > > --- a/mm/vmalloc.c > > +++ b/mm/vmalloc.c > > @@ -2218,6 +2218,9 @@ static unsigned long lazy_max_pages(void) > > */ > > static DEFINE_MUTEX(vmap_purge_lock); > > > > +/* Workqueue for lazy vmap purging; WQ_MEM_RECLAIM guarantees progress. */ > > +static struct workqueue_struct *vmap_purge_wq; > > + > > /* for per-CPU blocks */ > > static void purge_fragmented_blocks_allcpus(void); > > > > @@ -2408,9 +2411,9 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end, > > INIT_WORK(&vn->purge_work, purge_vmap_node); > > > > if (cpumask_test_cpu(i, cpu_online_mask)) > > - schedule_work_on(i, &vn->purge_work); > > + queue_work_on(i, vmap_purge_wq, &vn->purge_work); > > else > > - schedule_work(&vn->purge_work); > > + queue_work(vmap_purge_wq, &vn->purge_work); > > > > nr_purge_helpers--; > > } else { > > @@ -5519,10 +5522,14 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > > { > > struct vmap_node *vn; > > > > - guard(mutex)(&vmap_purge_lock); > > + if (!mutex_trylock(&vmap_purge_lock)) > > + return SHRINK_STOP; > > + > > for_each_vmap_node(vn) > > decay_va_pool_node(vn, true); > > > > + mutex_unlock(&vmap_purge_lock); > > + > > return SHRINK_STOP; > > } > > > > @@ -5575,6 +5582,17 @@ void __init vmalloc_init(void) > > * Now we can initialize a free vmap space. > > */ > > vmap_init_free_space(); > > + > > + /* > > + * A dedicated workqueue for lazy vmap purging. WQ_MEM_RECLAIM > > + * reserves a rescue worker so queued purge work items are executed > > + * even under memory pressure, when workers of the system workqueue > > + * may be stuck in direct reclaim. > > + */ > > + vmap_purge_wq = alloc_workqueue("vmap_purge", > > + WQ_MEM_RECLAIM | WQ_PERCPU, 0); > > + WARN_ON(!vmap_purge_wq); > > + > > vmap_initialized = true; > > > I agree. We should have it and it should be as separate patch, i.e. > split vmap_node_shrink_scan() and dedicated per-cpu WQs per vmap drain. > And i sent out already the WQ_UNBOUND | WQ_MEM_RECLAIM and separate WQ for vmap drain logic. It looks like Andrew/me forgot about it: https://lore.kernel.org/all/20260331202352.879718-1-urezki@gmail.com/ -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-01 16:59 ` Uladzislau Rezki @ 2026-09-02 4:30 ` Dev Jain 2026-09-03 9:10 ` Uladzislau Rezki 0 siblings, 1 reply; 14+ messages in thread From: Dev Jain @ 2026-09-02 4:30 UTC (permalink / raw) To: Uladzislau Rezki, Andrew Morton; +Cc: Ye Liu, Ye Liu, linux-mm, linux-kernel On 01/09/26 10:29 pm, Uladzislau Rezki wrote: > On Tue, Sep 01, 2026 at 06:43:43PM +0200, Uladzislau Rezki wrote: >> On Tue, Sep 01, 2026 at 05:07:30PM +0800, Ye Liu wrote: >>> >>> >>> 在 2026/9/1 14:22, Dev Jain 写道: >>>> >>>> >>>> On 31/08/26 3:24 pm, Uladzislau Rezki wrote: >>>>> On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: >>>>>> >>>>>> >>>>>> On 28/08/26 11:35 pm, Andrew Morton wrote: >>>>>>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: >>>>>>> >>>>>>>> From: Ye Liu <liuye@kylinos.cn> >>>>>>>> >>>>>>>> The vmap_purge_lock mutex can be held for an extended period by >>>>>>>> __purge_vmap_area_lazy() which calls flush_work() to wait for >>>>>>>> purge_vmap_node workers while holding the lock. Under memory >>>>>>>> pressure, those workers may themselves be blocked in direct >>>>>>>> reclaim trying to acquire the same lock via the >>>>>>>> vmap_node_shrink_scan() shrinker callback, creating a circular >>>>>>>> dependency that deadlocks the entire system. >>>>>>>> >>>>>>>> Two places acquire vmap_purge_lock from paths that can be reached >>>>>>>> during direct reclaim: >>>>>>>> >>>>>>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with >>>>>>>> mutex_trylock(). This is a shrinker that only decays the vmap >>>>>>>> pool and returns SHRINK_STOP without freeing memory; skipping a >>>>>>>> decay cycle when the lock is contended is harmless and prevents >>>>>>>> tasks from piling up on the mutex in the direct reclaim path. >>>>>>>> >>>>>>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with >>>>>>>> mutex_trylock(). This is called from the vmalloc allocation >>>>>>>> overflow path; if trylock fails, another thread is already >>>>>>>> purging and the allocator's retry will find freed space. The >>>>>>>> notifier chain provides a fallback if the retry still fails. >>>>>>>> >>>>>>>> Both trylock failures break the circular dependency: the lock >>>>>>>> holder's flush_work() can complete because workers are no longer >>>>>>>> blocked on vmap_purge_lock in the direct reclaim path. >>>>>>> >>>>>>> Thanks. AI review expressed a couple of concerns: >>>>>>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev >>>>>> >>>>>> >>>>>> Sounds legit to me. Now there is no guarantee of purge being successful, and we >>>>>> can get a spurious failure. >>>>>> >>>>>> How about using a WQ_RECLAIM workqueue: >>>>>> >>>>>> vmap_purge_wq = alloc_workqueue("vmap_purge", >>>>>> WQ_MEM_RECLAIM | WQ_PERCPU, 0); >>>>>> >>>>>> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). >>>>>> >>>>> WQ_MEM_RECLAIM makes sense but this is another patch. >>>>> >>>>> I copied here AI comment: >>>>>> >>>>>> Does replacing this blocking lock with a trylock break synchronization for >>>>>> the callers? >>>>>> >>>>> No it does not. If someone is doing reclaim we do not wait and do not try >>>>> to do it again thus fail allocation. >>>>> >>>>>> When vmalloc space is exhausted, alloc_vmap_area() calls >>>>>> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure >>>>>> that free space has actually been reclaimed before looping back to retry: >>>>>> mm/vmalloc.c:alloc_vmap_area() { >>>>>> ... >>>>>> overflow: >>>>>> if (!purged) { >>>>>> reclaim_and_purge_vmap_areas(); >>>>>> purged = 1; >>>>>> goto retry; >>>>>> } >>>>>> ... >>>>>> } >>>>>> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() >>>>>> fails and the function returns immediately. >>>>>> >>>>> If reclaim is in progress and trylock fails a caller repeats only one >>>>> time to retry an allocation. There is no any infinite loop. >>>>> >>>>>> >>>>>> The allocator then retries >>>>>> instantly without waiting for the concurrent purge to complete. >>>>>> Because the retry fails and purged is already 1, could this cause the >>>>>> allocation to abort and return a spurious vmalloc allocation failure >>>>>> (-EBUSY or -ENOMEM)? >>>>>> >>>>> vmap space can be fragmented and not avail for 32-bit systems. For >>>>> 64-bit system it is likely impossible. >>>>> >>>>> But, i think we can overt mutex_lock() into mutex_trylock() just only >>>>> in the: >>>>> >>>>> static unsigned long >>>>> vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) >>>>> { >>>>> struct vmap_node *vn; >>>>> >>>>> guard(mutex)(&vmap_purge_lock); >>>>> for_each_vmap_node(vn) >>>>> decay_va_pool_node(vn, true); >>>>> >>>>> return SHRINK_STOP; >>>>> } >>>>> >>>>> so the reclaim path is not blocked. It should also address an issue >>>>> reported by the Ye Liu <ye.liu@linux.dev>. >>>> >>>> IIUC you are suggesting mutex_trylock() only in the shrinker path. But >>>> then, the following is possible no: take purge lock, try to get a >>>> worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area >>>> -> reclaim_and_purge_vmap_areas -> take purge lock? >>>> >>>> >>> >>> Personally, I lean toward the WQ_MEM_RECLAIM workqueue solution. >>> After taking a closer look at the code, I noticed a subtle but >>> potentially problematic scenario: >>> >>> When drain_vmap_area_work acquires vmap_purge_lock and calls into >>> __purge_vmap_area_lazy, it may subsequently invoke queue_work/queue_work_on >>> on the same CPU's system_wq. If the newly queued work ends up waiting >>> for an available worker on that same CPU, while the current worker is >>> blocked waiting for that very work to complete (via flush_work), >>> we could end up with a self-deadlock on a single CPU. >>> >>> Theoretically, this seems possible. I suspect the reason we don't >>> see widespread reports of such deadlocks is that the nr_purge_helpers >>> logic limits the number of asynchronous workers; when resources are tight, >>> it falls back to synchronous execution (purge_vmap_node directly), >>> which avoids queuing additional work. >>> >>> Using a dedicated workqueue with WQ_MEM_RECLAIM would provide a clean, >>> explicit isolation—ensuring forward progress under memory pressure and >>> eliminating the risk of interfering with other subsystems' workqueues. >>> I believe this approach is more robust in the long run. >>> >>> Perhaps like the code below: >>> the dedicated queue eliminates the self‑deadlock risk, and the trylock >>> in the shrinker prevents recursive lock attempts from reclaim contexts. >>> >>> diff --git a/mm/vmalloc.c b/mm/vmalloc.c >>> index bea9f76ed7e7..68fc1f5acb2f 100644 >>> --- a/mm/vmalloc.c >>> +++ b/mm/vmalloc.c >>> @@ -2218,6 +2218,9 @@ static unsigned long lazy_max_pages(void) >>> */ >>> static DEFINE_MUTEX(vmap_purge_lock); >>> >>> +/* Workqueue for lazy vmap purging; WQ_MEM_RECLAIM guarantees progress. */ >>> +static struct workqueue_struct *vmap_purge_wq; >>> + >>> /* for per-CPU blocks */ >>> static void purge_fragmented_blocks_allcpus(void); >>> >>> @@ -2408,9 +2411,9 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end, >>> INIT_WORK(&vn->purge_work, purge_vmap_node); >>> >>> if (cpumask_test_cpu(i, cpu_online_mask)) >>> - schedule_work_on(i, &vn->purge_work); >>> + queue_work_on(i, vmap_purge_wq, &vn->purge_work); >>> else >>> - schedule_work(&vn->purge_work); >>> + queue_work(vmap_purge_wq, &vn->purge_work); >>> >>> nr_purge_helpers--; >>> } else { >>> @@ -5519,10 +5522,14 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) >>> { >>> struct vmap_node *vn; >>> >>> - guard(mutex)(&vmap_purge_lock); >>> + if (!mutex_trylock(&vmap_purge_lock)) >>> + return SHRINK_STOP; >>> + >>> for_each_vmap_node(vn) >>> decay_va_pool_node(vn, true); >>> >>> + mutex_unlock(&vmap_purge_lock); >>> + >>> return SHRINK_STOP; >>> } >>> >>> @@ -5575,6 +5582,17 @@ void __init vmalloc_init(void) >>> * Now we can initialize a free vmap space. >>> */ >>> vmap_init_free_space(); >>> + >>> + /* >>> + * A dedicated workqueue for lazy vmap purging. WQ_MEM_RECLAIM >>> + * reserves a rescue worker so queued purge work items are executed >>> + * even under memory pressure, when workers of the system workqueue >>> + * may be stuck in direct reclaim. >>> + */ >>> + vmap_purge_wq = alloc_workqueue("vmap_purge", >>> + WQ_MEM_RECLAIM | WQ_PERCPU, 0); >>> + WARN_ON(!vmap_purge_wq); >>> + >>> vmap_initialized = true; >>> >> I agree. We should have it and it should be as separate patch, i.e. >> split vmap_node_shrink_scan() and dedicated per-cpu WQs per vmap drain. >> > And i sent out already the WQ_UNBOUND | WQ_MEM_RECLAIM and separate WQ > for vmap drain logic. It looks like Andrew/me forgot about it: > > https://lore.kernel.org/all/20260331202352.879718-1-urezki@gmail.com/ Great! Perhaps resend it and we can review it? > > -- > Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-02 4:30 ` Dev Jain @ 2026-09-03 9:10 ` Uladzislau Rezki 0 siblings, 0 replies; 14+ messages in thread From: Uladzislau Rezki @ 2026-09-03 9:10 UTC (permalink / raw) To: Dev Jain Cc: Uladzislau Rezki, Andrew Morton, Ye Liu, Ye Liu, linux-mm, linux-kernel On Wed, Sep 02, 2026 at 10:00:57AM +0530, Dev Jain wrote: > > > On 01/09/26 10:29 pm, Uladzislau Rezki wrote: > > On Tue, Sep 01, 2026 at 06:43:43PM +0200, Uladzislau Rezki wrote: > >> On Tue, Sep 01, 2026 at 05:07:30PM +0800, Ye Liu wrote: > >>> > >>> > >>> 在 2026/9/1 14:22, Dev Jain 写道: > >>>> > >>>> > >>>> On 31/08/26 3:24 pm, Uladzislau Rezki wrote: > >>>>> On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: > >>>>>> > >>>>>> > >>>>>> On 28/08/26 11:35 pm, Andrew Morton wrote: > >>>>>>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > >>>>>>> > >>>>>>>> From: Ye Liu <liuye@kylinos.cn> > >>>>>>>> > >>>>>>>> The vmap_purge_lock mutex can be held for an extended period by > >>>>>>>> __purge_vmap_area_lazy() which calls flush_work() to wait for > >>>>>>>> purge_vmap_node workers while holding the lock. Under memory > >>>>>>>> pressure, those workers may themselves be blocked in direct > >>>>>>>> reclaim trying to acquire the same lock via the > >>>>>>>> vmap_node_shrink_scan() shrinker callback, creating a circular > >>>>>>>> dependency that deadlocks the entire system. > >>>>>>>> > >>>>>>>> Two places acquire vmap_purge_lock from paths that can be reached > >>>>>>>> during direct reclaim: > >>>>>>>> > >>>>>>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > >>>>>>>> mutex_trylock(). This is a shrinker that only decays the vmap > >>>>>>>> pool and returns SHRINK_STOP without freeing memory; skipping a > >>>>>>>> decay cycle when the lock is contended is harmless and prevents > >>>>>>>> tasks from piling up on the mutex in the direct reclaim path. > >>>>>>>> > >>>>>>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > >>>>>>>> mutex_trylock(). This is called from the vmalloc allocation > >>>>>>>> overflow path; if trylock fails, another thread is already > >>>>>>>> purging and the allocator's retry will find freed space. The > >>>>>>>> notifier chain provides a fallback if the retry still fails. > >>>>>>>> > >>>>>>>> Both trylock failures break the circular dependency: the lock > >>>>>>>> holder's flush_work() can complete because workers are no longer > >>>>>>>> blocked on vmap_purge_lock in the direct reclaim path. > >>>>>>> > >>>>>>> Thanks. AI review expressed a couple of concerns: > >>>>>>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev > >>>>>> > >>>>>> > >>>>>> Sounds legit to me. Now there is no guarantee of purge being successful, and we > >>>>>> can get a spurious failure. > >>>>>> > >>>>>> How about using a WQ_RECLAIM workqueue: > >>>>>> > >>>>>> vmap_purge_wq = alloc_workqueue("vmap_purge", > >>>>>> WQ_MEM_RECLAIM | WQ_PERCPU, 0); > >>>>>> > >>>>>> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). > >>>>>> > >>>>> WQ_MEM_RECLAIM makes sense but this is another patch. > >>>>> > >>>>> I copied here AI comment: > >>>>>> > >>>>>> Does replacing this blocking lock with a trylock break synchronization for > >>>>>> the callers? > >>>>>> > >>>>> No it does not. If someone is doing reclaim we do not wait and do not try > >>>>> to do it again thus fail allocation. > >>>>> > >>>>>> When vmalloc space is exhausted, alloc_vmap_area() calls > >>>>>> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure > >>>>>> that free space has actually been reclaimed before looping back to retry: > >>>>>> mm/vmalloc.c:alloc_vmap_area() { > >>>>>> ... > >>>>>> overflow: > >>>>>> if (!purged) { > >>>>>> reclaim_and_purge_vmap_areas(); > >>>>>> purged = 1; > >>>>>> goto retry; > >>>>>> } > >>>>>> ... > >>>>>> } > >>>>>> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() > >>>>>> fails and the function returns immediately. > >>>>>> > >>>>> If reclaim is in progress and trylock fails a caller repeats only one > >>>>> time to retry an allocation. There is no any infinite loop. > >>>>> > >>>>>> > >>>>>> The allocator then retries > >>>>>> instantly without waiting for the concurrent purge to complete. > >>>>>> Because the retry fails and purged is already 1, could this cause the > >>>>>> allocation to abort and return a spurious vmalloc allocation failure > >>>>>> (-EBUSY or -ENOMEM)? > >>>>>> > >>>>> vmap space can be fragmented and not avail for 32-bit systems. For > >>>>> 64-bit system it is likely impossible. > >>>>> > >>>>> But, i think we can overt mutex_lock() into mutex_trylock() just only > >>>>> in the: > >>>>> > >>>>> static unsigned long > >>>>> vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > >>>>> { > >>>>> struct vmap_node *vn; > >>>>> > >>>>> guard(mutex)(&vmap_purge_lock); > >>>>> for_each_vmap_node(vn) > >>>>> decay_va_pool_node(vn, true); > >>>>> > >>>>> return SHRINK_STOP; > >>>>> } > >>>>> > >>>>> so the reclaim path is not blocked. It should also address an issue > >>>>> reported by the Ye Liu <ye.liu@linux.dev>. > >>>> > >>>> IIUC you are suggesting mutex_trylock() only in the shrinker path. But > >>>> then, the following is possible no: take purge lock, try to get a > >>>> worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area > >>>> -> reclaim_and_purge_vmap_areas -> take purge lock? > >>>> > >>>> > >>> > >>> Personally, I lean toward the WQ_MEM_RECLAIM workqueue solution. > >>> After taking a closer look at the code, I noticed a subtle but > >>> potentially problematic scenario: > >>> > >>> When drain_vmap_area_work acquires vmap_purge_lock and calls into > >>> __purge_vmap_area_lazy, it may subsequently invoke queue_work/queue_work_on > >>> on the same CPU's system_wq. If the newly queued work ends up waiting > >>> for an available worker on that same CPU, while the current worker is > >>> blocked waiting for that very work to complete (via flush_work), > >>> we could end up with a self-deadlock on a single CPU. > >>> > >>> Theoretically, this seems possible. I suspect the reason we don't > >>> see widespread reports of such deadlocks is that the nr_purge_helpers > >>> logic limits the number of asynchronous workers; when resources are tight, > >>> it falls back to synchronous execution (purge_vmap_node directly), > >>> which avoids queuing additional work. > >>> > >>> Using a dedicated workqueue with WQ_MEM_RECLAIM would provide a clean, > >>> explicit isolation—ensuring forward progress under memory pressure and > >>> eliminating the risk of interfering with other subsystems' workqueues. > >>> I believe this approach is more robust in the long run. > >>> > >>> Perhaps like the code below: > >>> the dedicated queue eliminates the self‑deadlock risk, and the trylock > >>> in the shrinker prevents recursive lock attempts from reclaim contexts. > >>> > >>> diff --git a/mm/vmalloc.c b/mm/vmalloc.c > >>> index bea9f76ed7e7..68fc1f5acb2f 100644 > >>> --- a/mm/vmalloc.c > >>> +++ b/mm/vmalloc.c > >>> @@ -2218,6 +2218,9 @@ static unsigned long lazy_max_pages(void) > >>> */ > >>> static DEFINE_MUTEX(vmap_purge_lock); > >>> > >>> +/* Workqueue for lazy vmap purging; WQ_MEM_RECLAIM guarantees progress. */ > >>> +static struct workqueue_struct *vmap_purge_wq; > >>> + > >>> /* for per-CPU blocks */ > >>> static void purge_fragmented_blocks_allcpus(void); > >>> > >>> @@ -2408,9 +2411,9 @@ static bool __purge_vmap_area_lazy(unsigned long start, unsigned long end, > >>> INIT_WORK(&vn->purge_work, purge_vmap_node); > >>> > >>> if (cpumask_test_cpu(i, cpu_online_mask)) > >>> - schedule_work_on(i, &vn->purge_work); > >>> + queue_work_on(i, vmap_purge_wq, &vn->purge_work); > >>> else > >>> - schedule_work(&vn->purge_work); > >>> + queue_work(vmap_purge_wq, &vn->purge_work); > >>> > >>> nr_purge_helpers--; > >>> } else { > >>> @@ -5519,10 +5522,14 @@ vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > >>> { > >>> struct vmap_node *vn; > >>> > >>> - guard(mutex)(&vmap_purge_lock); > >>> + if (!mutex_trylock(&vmap_purge_lock)) > >>> + return SHRINK_STOP; > >>> + > >>> for_each_vmap_node(vn) > >>> decay_va_pool_node(vn, true); > >>> > >>> + mutex_unlock(&vmap_purge_lock); > >>> + > >>> return SHRINK_STOP; > >>> } > >>> > >>> @@ -5575,6 +5582,17 @@ void __init vmalloc_init(void) > >>> * Now we can initialize a free vmap space. > >>> */ > >>> vmap_init_free_space(); > >>> + > >>> + /* > >>> + * A dedicated workqueue for lazy vmap purging. WQ_MEM_RECLAIM > >>> + * reserves a rescue worker so queued purge work items are executed > >>> + * even under memory pressure, when workers of the system workqueue > >>> + * may be stuck in direct reclaim. > >>> + */ > >>> + vmap_purge_wq = alloc_workqueue("vmap_purge", > >>> + WQ_MEM_RECLAIM | WQ_PERCPU, 0); > >>> + WARN_ON(!vmap_purge_wq); > >>> + > >>> vmap_initialized = true; > >>> > >> I agree. We should have it and it should be as separate patch, i.e. > >> split vmap_node_shrink_scan() and dedicated per-cpu WQs per vmap drain. > >> > > And i sent out already the WQ_UNBOUND | WQ_MEM_RECLAIM and separate WQ > > for vmap drain logic. It looks like Andrew/me forgot about it: > > > > https://lore.kernel.org/all/20260331202352.879718-1-urezki@gmail.com/ > > Great! Perhaps resend it and we can review it? > I will and add you to Cc. -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
* Re: [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure 2026-09-01 6:22 ` Dev Jain 2026-09-01 9:07 ` Ye Liu @ 2026-09-01 16:40 ` Uladzislau Rezki 1 sibling, 0 replies; 14+ messages in thread From: Uladzislau Rezki @ 2026-09-01 16:40 UTC (permalink / raw) To: Dev Jain Cc: Uladzislau Rezki, Ye Liu, Andrew Morton, Ye Liu, linux-mm, linux-kernel On Tue, Sep 01, 2026 at 11:52:11AM +0530, Dev Jain wrote: > > > On 31/08/26 3:24 pm, Uladzislau Rezki wrote: > > On Mon, Aug 31, 2026 at 11:39:14AM +0530, Dev Jain wrote: > >> > >> > >> On 28/08/26 11:35 pm, Andrew Morton wrote: > >>> On Fri, 28 Aug 2026 17:17:53 +0800 Ye Liu <ye.liu@linux.dev> wrote: > >>> > >>>> From: Ye Liu <liuye@kylinos.cn> > >>>> > >>>> The vmap_purge_lock mutex can be held for an extended period by > >>>> __purge_vmap_area_lazy() which calls flush_work() to wait for > >>>> purge_vmap_node workers while holding the lock. Under memory > >>>> pressure, those workers may themselves be blocked in direct > >>>> reclaim trying to acquire the same lock via the > >>>> vmap_node_shrink_scan() shrinker callback, creating a circular > >>>> dependency that deadlocks the entire system. > >>>> > >>>> Two places acquire vmap_purge_lock from paths that can be reached > >>>> during direct reclaim: > >>>> > >>>> 1. vmap_node_shrink_scan(): replace blocking guard(mutex) with > >>>> mutex_trylock(). This is a shrinker that only decays the vmap > >>>> pool and returns SHRINK_STOP without freeing memory; skipping a > >>>> decay cycle when the lock is contended is harmless and prevents > >>>> tasks from piling up on the mutex in the direct reclaim path. > >>>> > >>>> 2. reclaim_and_purge_vmap_areas(): replace mutex_lock() with > >>>> mutex_trylock(). This is called from the vmalloc allocation > >>>> overflow path; if trylock fails, another thread is already > >>>> purging and the allocator's retry will find freed space. The > >>>> notifier chain provides a fallback if the retry still fails. > >>>> > >>>> Both trylock failures break the circular dependency: the lock > >>>> holder's flush_work() can complete because workers are no longer > >>>> blocked on vmap_purge_lock in the direct reclaim path. > >>> > >>> Thanks. AI review expressed a couple of concerns: > >>> https://sashiko.dev/#/patchset/20260828091753.299295-1-ye.liu@linux.dev > >> > >> > >> Sounds legit to me. Now there is no guarantee of purge being successful, and we > >> can get a spurious failure. > >> > >> How about using a WQ_RECLAIM workqueue: > >> > >> vmap_purge_wq = alloc_workqueue("vmap_purge", > >> WQ_MEM_RECLAIM | WQ_PERCPU, 0); > >> > >> I see the same pattern in __lru_add_drain_all() and kmem_cache_init_late(). > >> > > WQ_MEM_RECLAIM makes sense but this is another patch. > > > > I copied here AI comment: > >> > >> Does replacing this blocking lock with a trylock break synchronization for > >> the callers? > >> > > No it does not. If someone is doing reclaim we do not wait and do not try > > to do it again thus fail allocation. > > > >> When vmalloc space is exhausted, alloc_vmap_area() calls > >> reclaim_and_purge_vmap_areas() and relies on its blocking behavior to ensure > >> that free space has actually been reclaimed before looping back to retry: > >> mm/vmalloc.c:alloc_vmap_area() { > >> ... > >> overflow: > >> if (!purged) { > >> reclaim_and_purge_vmap_areas(); > >> purged = 1; > >> goto retry; > >> } > >> ... > >> } > >> With this patch, if another thread holds vmap_purge_lock, mutex_trylock() > >> fails and the function returns immediately. > >> > > If reclaim is in progress and trylock fails a caller repeats only one > > time to retry an allocation. There is no any infinite loop. > > > >> > >> The allocator then retries > >> instantly without waiting for the concurrent purge to complete. > >> Because the retry fails and purged is already 1, could this cause the > >> allocation to abort and return a spurious vmalloc allocation failure > >> (-EBUSY or -ENOMEM)? > >> > > vmap space can be fragmented and not avail for 32-bit systems. For > > 64-bit system it is likely impossible. > > > > But, i think we can overt mutex_lock() into mutex_trylock() just only > > in the: > > > > static unsigned long > > vmap_node_shrink_scan(struct shrinker *shrink, struct shrink_control *sc) > > { > > struct vmap_node *vn; > > > > guard(mutex)(&vmap_purge_lock); > > for_each_vmap_node(vn) > > decay_va_pool_node(vn, true); > > > > return SHRINK_STOP; > > } > > > > so the reclaim path is not blocked. It should also address an issue > > reported by the Ye Liu <ye.liu@linux.dev>. > > IIUC you are suggesting mutex_trylock() only in the shrinker path. But > then, the following is possible no: take purge lock, try to get a > worker thread, worker thread is stuck in vmalloc -> alloc_vmap_area > -> reclaim_and_purge_vmap_areas -> take purge lock? > It is possible what you describe. Usually when no memory, the work item can be delayed for a while. As it was noted WQ_RECLAIM and separate WQ which has a rescue worker. IMO, it should be a separate patch. -- Uladzislau Rezki ^ permalink raw reply [flat|nested] 14+ messages in thread
end of thread, other threads:[~2026-09-03 9:11 UTC | newest] Thread overview: 14+ messages (download: mbox.gz follow: Atom feed -- links below jump to the message on this page -- 2026-08-28 9:17 [PATCH v2] mm: vmalloc: fix vmap_purge_lock livelock under memory pressure Ye Liu 2026-08-28 16:54 ` Uladzislau Rezki 2026-08-28 18:05 ` Andrew Morton 2026-08-31 6:09 ` Dev Jain 2026-08-31 9:54 ` Uladzislau Rezki 2026-09-01 1:42 ` Andrew Morton 2026-09-01 16:36 ` Uladzislau Rezki 2026-09-01 6:22 ` Dev Jain 2026-09-01 9:07 ` Ye Liu 2026-09-01 16:43 ` Uladzislau Rezki 2026-09-01 16:59 ` Uladzislau Rezki 2026-09-02 4:30 ` Dev Jain 2026-09-03 9:10 ` Uladzislau Rezki 2026-09-01 16:40 ` Uladzislau Rezki
This is a public inbox, see mirroring instructions for how to clone and mirror all data and code used for this inbox; as well as URLs for NNTP newsgroup(s).