* [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-09-02 14:53 ` Nhat Pham
2026-08-27 9:44 ` [PATCH 02/16] mm: xswap support for zswap Baoquan He
` (18 subsequent siblings)
19 siblings, 1 reply; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
zswap_writeback_entry() returns -EEXIST when get_swap_device() finds no
device. -EEXIST is the shrinker's "page already in swap cache" signal,
which makes zswap_shrinker_scan() stop shrinking entirely. A NULL
get_swap_device() instead means the device is being swapped off, so the
entry is simply stale.
Return -ENOENT so the shrinker skips the stale entry and keeps scanning.
Independent of xswap; affects all swap devices.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/zswap.c | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/mm/zswap.c b/mm/zswap.c
index 37f34e406c8e..b9948d4657d2 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -998,7 +998,7 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
/* try to allocate swap cache folio */
si = get_swap_device(swpentry);
if (!si)
- return -EEXIST;
+ return -ENOENT;
mpol = get_task_policy(current);
folio = swap_cache_alloc_folio(swpentry, GFP_KERNEL, BIT(0), NULL, mpol,
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
@ 2026-09-02 14:53 ` Nhat Pham
2026-09-03 7:54 ` Baoquan He
0 siblings, 1 reply; 36+ messages in thread
From: Nhat Pham @ 2026-09-02 14:53 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 5:45 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
>
> zswap_writeback_entry() returns -EEXIST when get_swap_device() finds no
> device. -EEXIST is the shrinker's "page already in swap cache" signal,
> which makes zswap_shrinker_scan() stop shrinking entirely. A NULL
> get_swap_device() instead means the device is being swapped off, so the
> entry is simply stale.
>
> Return -ENOENT so the shrinker skips the stale entry and keeps scanning.
> Independent of xswap; affects all swap devices.
>
> Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
I think this should be submitted independently of this patch series!
It's a small, self-contained fix for an existing issue, no?
Acked-by: Nhat Pham <nphamcs@gmail.com>
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
2026-09-02 14:53 ` Nhat Pham
@ 2026-09-03 7:54 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-03 7:54 UTC (permalink / raw)
To: Nhat Pham
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, baohua, youngjun.park,
hannes, yosry, shikemeng, chengming.zhou, david, linux-kernel
On 09/02/26 at 10:53am, Nhat Pham wrote:
> On Thu, Aug 27, 2026 at 5:45 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
> >
> > zswap_writeback_entry() returns -EEXIST when get_swap_device() finds no
> > device. -EEXIST is the shrinker's "page already in swap cache" signal,
> > which makes zswap_shrinker_scan() stop shrinking entirely. A NULL
> > get_swap_device() instead means the device is being swapped off, so the
> > entry is simply stale.
> >
> > Return -ENOENT so the shrinker skips the stale entry and keeps scanning.
> > Independent of xswap; affects all swap devices.
> >
> > Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
>
> I think this should be submitted independently of this patch series!
> It's a small, self-contained fix for an existing issue, no?
Right, this should be sent out as a standalone patch. I will split it
from v2. Thanks for careful checking.
>
> Acked-by: Nhat Pham <nphamcs@gmail.com>
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH 02/16] mm: xswap support for zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct Baoquan He
` (17 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
From: Chris Li <chrisl@kernel.org>
Introduce extendable swap device support — xswap.
An xswap device is a swap device with no backing storage and
no swap data section, so it wastes no disk space. Any write to an
xswap device will fail; to prevent accidental read or write, bdev of
swap_info_struct is set to NULL. Xswap devices set the SSD flag
because there is no rotational disk access when using zswap. Creation
is via a sysfs interface added in a later patch.
Zswap writeback is disabled if all swapfiles in the system are
xswap devices (tracked via nr_real_swapfiles).
Co-developed-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
Signed-off-by: Chris Li <chrisl@kernel.org>
---
include/linux/swap.h | 2 ++
mm/page_io.c | 16 ++++++++++++++++
mm/swap_state.c | 7 +++++++
mm/swapfile.c | 37 ++++++++++++++++++++++++++++++++++---
mm/zswap.c | 7 ++++++-
5 files changed, 65 insertions(+), 4 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 5658a1634b85..787fe463dcbb 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -207,6 +207,7 @@ enum {
SWP_STABLE_WRITES = (1 << 11), /* no overwrite PG_writeback pages */
SWP_SYNCHRONOUS_IO = (1 << 12), /* synchronous IO is efficient */
SWP_HIBERNATION = (1 << 13), /* pinned for hibernation */
+ SWP_XSWAP = (1 << 14), /* extendable swap device */
/* add others here before... */
};
@@ -356,6 +357,7 @@ void free_folio_and_swap_cache(struct folio *folio);
void free_pages_and_swap_cache(struct encoded_page **, int);
/* linux/mm/swapfile.c */
extern atomic_long_t nr_swap_pages;
+extern atomic_t nr_real_swapfiles;
extern long total_swap_pages;
extern atomic_t nr_rotate_swap;
diff --git a/mm/page_io.c b/mm/page_io.c
index 88962571cb93..5483c943e3e3 100644
--- a/mm/page_io.c
+++ b/mm/page_io.c
@@ -248,6 +248,17 @@ int swap_writeout(struct swap_io_ctx *ctx, struct folio *folio)
}
rcu_read_unlock();
+ /*
+ * ctx->sis is set by swap_add_folio() which is called from
+ * __swap_writepage() below. Since we must avoid the writepage
+ * path for xswap devices, use the swap_info from the folio's
+ * swap entry directly instead of going through ctx.
+ */
+ if (unlikely(__swap_entry_to_info(folio->swap)->flags & SWP_XSWAP)) {
+ folio_mark_dirty(folio);
+ return AOP_WRITEPAGE_ACTIVATE;
+ }
+
__swap_writepage(ctx, folio);
return 0;
out_unlock:
@@ -480,6 +491,11 @@ void swap_read_folio(struct swap_io_ctx *ctx, struct folio *folio)
if (zswap_load(folio) != -ENOENT)
goto finish;
+ if (unlikely(sis->flags & SWP_XSWAP)) {
+ folio_unlock(folio);
+ goto finish;
+ }
+
/* We have to read from slower devices. Increase zswap protection. */
zswap_folio_swapin(folio);
swap_add_folio(ctx, folio, READ);
diff --git a/mm/swap_state.c b/mm/swap_state.c
index b76eb3d876fd..2eedb7a3d7bb 100644
--- a/mm/swap_state.c
+++ b/mm/swap_state.c
@@ -830,6 +830,13 @@ struct folio *swap_cluster_readahead(swp_entry_t entry, gfp_t gfp_mask,
struct blk_plug plug;
swp_entry_t ra_entry;
+ /*
+ * The entry may have been freed by another task. Avoid swap_info_get()
+ * which will print error message if the race happens.
+ */
+ if (si->flags & SWP_XSWAP)
+ goto skip;
+
mask = swapin_nr_pages(offset) - 1;
if (!mask)
goto skip;
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 53bf01d5f7f1..5aa1ffb97df8 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -66,6 +66,7 @@ static void move_cluster(struct swap_info_struct *si,
static DEFINE_SPINLOCK(swap_lock);
static unsigned int nr_swapfiles;
atomic_long_t nr_swap_pages;
+atomic_t nr_real_swapfiles;
/*
* Some modules use swappable objects and may try to swap them out under
* memory pressure (via the shrinker). Before doing so, they may wish to
@@ -1223,6 +1224,8 @@ static void del_from_avail_list(struct swap_info_struct *si, bool swapoff)
goto skip;
}
+ if (!(si->flags & SWP_XSWAP))
+ atomic_sub(1, &nr_real_swapfiles);
plist_del(&si->avail_list, &swap_avail_head);
skip:
@@ -1265,6 +1268,8 @@ static void add_to_avail_list(struct swap_info_struct *si, bool swapon)
}
plist_add(&si->avail_list, &swap_avail_head);
+ if (!(si->flags & SWP_XSWAP))
+ atomic_add(1, &nr_real_swapfiles);
skip:
spin_unlock(&swap_avail_lock);
@@ -2959,6 +2964,19 @@ static int setup_swap_extents(struct swap_info_struct *sis,
struct inode *inode = mapping->host;
int ret;
+ if (sis->flags & SWP_XSWAP) {
+ *span = 0;
+ /*
+ * xswap devices have no backing block device and
+ * physical writeout is skipped in swap_writeout(),
+ * but sis->ops must still be set so that callers
+ * like shrink_folio_list() can safely dereference
+ * ops->flags.
+ */
+ sis->ops = &swap_bdev_ops;
+ return 0;
+ }
+
ret = sio_pool_init();
if (ret)
return ret;
@@ -3167,7 +3185,8 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
destroy_swap_extents(p, p->swap_file);
- if (!(p->flags & SWP_SOLIDSTATE))
+ if (!(p->flags & SWP_XSWAP) &&
+ !(p->flags & SWP_SOLIDSTATE))
atomic_dec(&nr_rotate_swap);
mutex_lock(&swapon_mutex);
@@ -3277,6 +3296,19 @@ static void swap_stop(struct seq_file *swap, void *v)
mutex_unlock(&swapon_mutex);
}
+static const char *swap_type_str(struct swap_info_struct *si)
+{
+ struct file *file = si->swap_file;
+
+ if (si->flags & SWP_XSWAP)
+ return "xswap\t";
+
+ if (S_ISBLK(file_inode(file)->i_mode))
+ return "partition";
+
+ return "file\t";
+}
+
static int swap_show(struct seq_file *swap, void *v)
{
struct swap_info_struct *si = v;
@@ -3296,8 +3328,7 @@ static int swap_show(struct seq_file *swap, void *v)
len = seq_file_path(swap, file, " \t\n\\");
seq_printf(swap, "%*s%s\t%lu\t%s%lu\t%s%d\n",
len < 40 ? 40 - len : 1, " ",
- S_ISBLK(file_inode(file)->i_mode) ?
- "partition" : "file\t",
+ swap_type_str(si),
bytes, bytes < 10000000 ? "\t" : "",
inuse, inuse < 10000000 ? "\t" : "",
si->prio);
diff --git a/mm/zswap.c b/mm/zswap.c
index b9948d4657d2..064970a4393f 100644
--- a/mm/zswap.c
+++ b/mm/zswap.c
@@ -1000,6 +1000,11 @@ static int zswap_writeback_entry(struct zswap_entry *entry,
if (!si)
return -ENOENT;
+ if (si->flags & SWP_XSWAP) {
+ put_swap_device(si);
+ return -EINVAL;
+ }
+
mpol = get_task_policy(current);
folio = swap_cache_alloc_folio(swpentry, GFP_KERNEL, BIT(0), NULL, mpol,
NO_INTERLEAVE_INDEX);
@@ -1545,7 +1550,7 @@ bool zswap_store(struct folio *folio)
zswap_pool_put(pool);
put_objcg:
obj_cgroup_put(objcg);
- if (!ret && zswap_pool_reached_full)
+ if (!ret && zswap_pool_reached_full && atomic_read(&nr_real_swapfiles))
queue_work(shrink_wq, &zswap_shrink_work);
check_old:
/*
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
2026-08-27 9:44 ` [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone Baoquan He
2026-08-27 9:44 ` [PATCH 02/16] mm: xswap support for zswap Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Baoquan He
` (16 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Add CONFIG_XSWAP Kconfig option (depends on SWAP && 64BIT) for
extendable (virtual) swap device support.
Add three fields to struct swap_info_struct under CONFIG_XSWAP:
- cluster_vm: the VM_SPARSE vm_struct backing the cluster_info array
- nr_clusters: total number of clusters in the xswap address space
- nr_clusters_mapped: number of clusters currently mapped (lazy grow)
These fields enable lazy vmalloc-based dynamic cluster management.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 5 +++++
mm/Kconfig | 9 +++++++++
2 files changed, 14 insertions(+)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 787fe463dcbb..35d357a393c6 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -248,6 +248,11 @@ struct swap_info_struct {
signed char type; /* strange name for an index */
unsigned int max; /* size of this swap device */
struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
+#ifdef CONFIG_XSWAP
+ struct vm_struct *cluster_vm; /* VM_SPARSE area for xswap dynamic cluster_info */
+ unsigned long nr_clusters; /* total cluster count for xswap */
+ unsigned long nr_clusters_mapped; /* currently mapped cluster count */
+#endif
struct list_head free_clusters; /* free clusters list */
struct list_head full_clusters; /* full clusters list */
struct list_head nonfull_clusters[SWAP_NR_ORDERS];
diff --git a/mm/Kconfig b/mm/Kconfig
index 604c58199acb..26cf26a2c59f 100644
--- a/mm/Kconfig
+++ b/mm/Kconfig
@@ -122,6 +122,15 @@ config ZSWAP_COMPRESSOR_DEFAULT
default "zstd" if ZSWAP_COMPRESSOR_DEFAULT_ZSTD
default ""
+config XSWAP
+ bool "Extendable (virtual) swap device"
+ depends on SWAP && 64BIT
+ help
+ Adds support for extendable swap devices (xswap) that decouple
+ PTE swap entries from physical backing storage. The cluster_info
+ array is backed by a sparse vmalloc area that grows and shrinks
+ on demand, avoiding static pre-allocation overhead.
+
config ZSMALLOC
tristate
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (2 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc Baoquan He
` (15 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Change free_swap_cluster_info() to accept struct swap_info_struct*
instead of (cluster_info, maxpages) directly. Extract cluster_info
and maxpages from si inside the function. Also clean up swapoff:
remove the snapshot locals (maxpages/cluster_info) and move the
p->max/p->cluster_info clearing after free_swap_cluster_info().
This is a preparatory refactoring, no functional change. The new
signature will allow the xswap path (added in the next patch) to
access si->flags and call xswap_unmap_clusters() from within
free_swap_cluster_info().
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 25 +++++++++++++------------
1 file changed, 13 insertions(+), 12 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5aa1ffb97df8..cd7e889f073d 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3054,14 +3054,17 @@ static void wait_for_allocation(struct swap_info_struct *si)
}
}
-static void free_swap_cluster_info(struct swap_cluster_info *cluster_info,
- unsigned long maxpages)
+static void free_swap_cluster_info(struct swap_info_struct *si)
{
+ struct swap_cluster_info *cluster_info = si->cluster_info;
+ unsigned long maxpages = si->max;
struct swap_cluster_info *ci;
- int i, nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
+ int i, nr_clusters;
if (!cluster_info)
return;
+
+ nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
for (i = 0; i < nr_clusters; i++) {
ci = cluster_info + i;
/* Cluster with bad marks count will have a remaining table */
@@ -3100,11 +3103,9 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
{
struct swap_info_struct *p = NULL;
- struct swap_cluster_info *cluster_info;
struct file *swap_file, *victim;
struct address_space *mapping;
struct inode *inode;
- unsigned int maxpages;
int err, found = 0;
if (!capable(CAP_SYS_ADMIN))
@@ -3196,10 +3197,6 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
swap_file = p->swap_file;
p->swap_file = NULL;
- maxpages = p->max;
- cluster_info = p->cluster_info;
- p->max = 0;
- p->cluster_info = NULL;
spin_unlock(&p->lock);
spin_unlock(&swap_lock);
arch_swap_invalidate_area(p->type);
@@ -3207,7 +3204,9 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
mutex_unlock(&swapon_mutex);
kfree(p->global_cluster);
p->global_cluster = NULL;
- free_swap_cluster_info(cluster_info, maxpages);
+ free_swap_cluster_info(p);
+ p->max = 0;
+ p->cluster_info = NULL;
inode = mapping->host;
@@ -3574,6 +3573,8 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
if (!cluster_info)
goto err;
+ si->cluster_info = cluster_info;
+
for (i = 0; i < nr_clusters; i++)
spin_lock_init(&cluster_info[i].lock);
@@ -3637,7 +3638,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
si->cluster_info = cluster_info;
return 0;
err:
- free_swap_cluster_info(cluster_info, maxpages);
+ free_swap_cluster_info(si);
return err;
}
@@ -3856,7 +3857,7 @@ SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
si->global_cluster = NULL;
inode = NULL;
destroy_swap_extents(si, swap_file);
- free_swap_cluster_info(si->cluster_info, si->max);
+ free_swap_cluster_info(si);
si->cluster_info = NULL;
/*
* Clear the SWP_USED flag after all resources are freed so
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (3 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 06/16] mm, swap: add sysfs create interface for xswap Baoquan He
` (14 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Implement dynamic cluster_info array growth for xswap devices using a
VM_SPARSE vmalloc area:
1. xswap_map_clusters(): Allocate physical pages and map them into
the pre-reserved VM_SPARSE KVA region via vm_area_map_pages().
2. xswap_unmap_clusters(): Unmap pages from the VM_SPARSE area via
vm_area_unmap_pages() (used by the error/teardown paths, shrink
comes later).
3. setup_swap_clusters_info() xswap path: Use get_vm_area(VM_SPARSE)
for the cluster_info array, lazily mapping only the initial chunk.
4. free_swap_cluster_info() xswap path: Unmap all clusters and
free_vm_area(). Built on the refactoring in the previous patch.
5. wait_for_allocation() xswap guard: Skip shrinker-unmapped clusters
beyond nr_clusters_mapped.
The grow path avoids emergency reserves via __GFP_HIGH|__GFP_NOMEMALLOC
and wraps allocations with memalloc_noreclaim_save(). A per-device
mutex (xswap_lock) serializes concurrent map/unmap page table
modifications.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 259 ++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 258 insertions(+), 2 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 35d357a393c6..c2744fa8a009 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -252,6 +252,7 @@ struct swap_info_struct {
struct vm_struct *cluster_vm; /* VM_SPARSE area for xswap dynamic cluster_info */
unsigned long nr_clusters; /* total cluster count for xswap */
unsigned long nr_clusters_mapped; /* currently mapped cluster count */
+ struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
struct list_head full_clusters; /* full clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index cd7e889f073d..06f73c0a504a 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -49,6 +49,25 @@
#include "internal.h"
#include "swap.h"
+#ifdef CONFIG_XSWAP
+/*
+ * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ *
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
+ * operation. It is set to the number of cluster_info structs that
+ * fit in a single page (at least 16), so that the vmalloc page table
+ * overhead is proportional to the number of clusters mapped.
+ */
+#define XSWAP_GROW_CLUSTERS \
+ max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+
+static int xswap_map_clusters(struct swap_info_struct *si,
+ unsigned long start_idx, unsigned long nr);
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+ unsigned long start_idx, unsigned long nr);
+static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
+#endif
+
static void swap_range_alloc(struct swap_info_struct *si,
unsigned int nr_entries);
static bool folio_swapcache_freeable(struct folio *folio);
@@ -2715,15 +2734,27 @@ static unsigned int find_next_to_unuse(struct swap_info_struct *si,
unsigned int prev)
{
unsigned int i;
+ unsigned int end = si->max;
unsigned long swp_tb;
+#ifdef CONFIG_XSWAP
+ /* xswap may have shrunk and unmapped the cluster_info tail. */
+ if (si->flags & SWP_XSWAP) {
+ unsigned long mapped_end;
+
+ mapped_end = READ_ONCE(si->nr_clusters_mapped) * SWAPFILE_CLUSTER;
+ if (mapped_end < end)
+ end = mapped_end;
+ }
+#endif
+
/*
* No need for swap_lock here: we're just looking
* for whether an entry is in use, not modifying it; false
* hits are okay, and sys_swapoff() has already prevented new
* allocations from this area (while holding swap_lock).
*/
- for (i = prev + 1; i < si->max; i++) {
+ for (i = prev + 1; i < end; i++) {
swp_tb = swap_table_get(__swap_offset_to_cluster(si, i),
i % SWAPFILE_CLUSTER);
if (!swp_tb_is_null(swp_tb) && !swp_tb_is_bad(swp_tb))
@@ -2732,7 +2763,7 @@ static unsigned int find_next_to_unuse(struct swap_info_struct *si,
cond_resched();
}
- if (i == si->max)
+ if (i == end)
i = 0;
return i;
@@ -3048,6 +3079,13 @@ static void wait_for_allocation(struct swap_info_struct *si)
BUG_ON(si->flags & SWP_WRITEOK);
+#ifdef CONFIG_XSWAP
+ /* Skip shrinker-unmapped cluster tail. */
+ if (si->flags & SWP_XSWAP)
+ end = min(end, READ_ONCE(si->nr_clusters_mapped) *
+ SWAPFILE_CLUSTER);
+#endif
+
for (offset = 0; offset < end; offset += SWAPFILE_CLUSTER) {
ci = swap_cluster_lock(si, offset);
swap_cluster_unlock(ci);
@@ -3064,6 +3102,19 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
if (!cluster_info)
return;
+#ifdef CONFIG_XSWAP
+ if (si->flags & SWP_XSWAP) {
+ /* Unmap all mapped clusters and free the VM_SPARSE area */
+ if (si->nr_clusters_mapped > 0)
+ xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+ free_vm_area(si->cluster_vm);
+ si->cluster_vm = NULL;
+ si->nr_clusters = 0;
+ si->nr_clusters_mapped = 0;
+ return;
+ }
+#endif
+
nr_clusters = DIV_ROUND_UP(maxpages, SWAPFILE_CLUSTER);
for (i = 0; i < nr_clusters; i++) {
ci = cluster_info + i;
@@ -3560,6 +3611,150 @@ static unsigned long read_swap_header(struct swap_info_struct *si,
return maxpages;
}
+#ifdef CONFIG_XSWAP
+static int xswap_map_clusters(struct swap_info_struct *si,
+ unsigned long start_idx, unsigned long nr)
+{
+ unsigned long start_addr = (unsigned long)si->cluster_info +
+ (size_t)start_idx * sizeof(struct swap_cluster_info);
+ unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+ /* Round to page boundaries for vm_area_map_pages(). */
+ unsigned long vm_start = PAGE_ALIGN(start_addr);
+ unsigned long vm_end = PAGE_ALIGN(end_addr);
+ unsigned int noreclaim_flags;
+ unsigned long npages;
+ struct page **pages;
+ unsigned long i;
+ int err;
+
+ mutex_lock(&si->xswap_lock);
+
+ if (vm_start >= vm_end) {
+ /* All requested clusters fall within already-mapped pages. */
+ for (i = start_idx; i < start_idx + nr; i++)
+ spin_lock_init(&si->cluster_info[i].lock);
+ WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+ mutex_unlock(&si->xswap_lock);
+ return 0;
+ }
+
+ npages = (vm_end - vm_start) >> PAGE_SHIFT;
+
+ /* Prevent recursive reclaim during vmap page table allocation. */
+ noreclaim_flags = memalloc_noreclaim_save();
+
+ pages = kmalloc_array(npages, sizeof(*pages),
+ __GFP_HIGH | __GFP_NOMEMALLOC | GFP_KERNEL);
+ if (!pages) {
+ memalloc_noreclaim_restore(noreclaim_flags);
+ mutex_unlock(&si->xswap_lock);
+ return -ENOMEM;
+ }
+
+ for (i = 0; i < npages; i++) {
+ /* __GFP_ZERO: cluster_info pointer fields must start NULL. */
+ pages[i] = alloc_page(__GFP_HIGH | __GFP_NOMEMALLOC |
+ GFP_KERNEL | __GFP_ZERO);
+ if (!pages[i])
+ goto fail;
+ }
+
+ /* Detect racing grower that already mapped these pages. */
+ if (apply_to_existing_page_range(&init_mm, vm_start,
+ vm_end - vm_start,
+ xswap_check_mapped, NULL)) {
+ i = npages;
+ goto fail_nounmap;
+ }
+
+ err = vm_area_map_pages(si->cluster_vm, vm_start, vm_end, pages);
+ if (err) {
+ /* -EBUSY: defensive, the page was already mapped. */
+ if (err == -EBUSY) {
+ i = npages;
+ goto fail_nounmap;
+ }
+ i = npages;
+ goto fail;
+ }
+
+ kfree(pages);
+ memalloc_noreclaim_restore(noreclaim_flags);
+
+ /* Initialize spinlocks for newly mapped clusters */
+ for (i = start_idx; i < start_idx + nr; i++)
+ spin_lock_init(&si->cluster_info[i].lock);
+
+ /*
+ * Pairs with READ_ONCE() in shrink/grow paths.
+ */
+ WRITE_ONCE(si->nr_clusters_mapped, start_idx + nr);
+ mutex_unlock(&si->xswap_lock);
+ return 0;
+
+fail_nounmap:
+ /*
+ * The concurrent grower already mapped the range, initialized the
+ * cluster spinlocks and advanced nr_clusters_mapped. It may still
+ * be holding those locks while adding clusters to the free list, so
+ * do not touch them here; just free our unused pages.
+ */
+ while (i > 0) {
+ i--;
+ if (pages[i])
+ __free_page(pages[i]);
+ }
+ kfree(pages);
+ memalloc_noreclaim_restore(noreclaim_flags);
+ mutex_unlock(&si->xswap_lock);
+ return 0;
+
+fail:
+ while (i > 0) {
+ i--;
+ if (pages[i])
+ __free_page(pages[i]);
+ }
+ memalloc_noreclaim_restore(noreclaim_flags);
+ kfree(pages);
+ mutex_unlock(&si->xswap_lock);
+ return -ENOMEM;
+}
+
+static void xswap_unmap_clusters(struct swap_info_struct *si,
+ unsigned long start_idx, unsigned long nr)
+{
+ unsigned long start_addr = (unsigned long)si->cluster_info +
+ (size_t)start_idx * sizeof(struct swap_cluster_info);
+ unsigned long end_addr = start_addr + (size_t)nr * sizeof(struct swap_cluster_info);
+ /* Round to page boundaries for vm_area_unmap_pages(). */
+ unsigned long vm_start = PAGE_ALIGN(start_addr);
+ unsigned long vm_end = PAGE_ALIGN(end_addr);
+
+ mutex_lock(&si->xswap_lock);
+
+ if (vm_start >= vm_end) {
+ WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+ mutex_unlock(&si->xswap_lock);
+ return;
+ }
+
+ vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
+ /* vm_area_unmap_pages() clears PTEs but does not free pages. */
+ /* TODO: free backing pages via page table walk or tracking bitmap */
+
+ /* Pairs with READ_ONCE() in shrink/grow paths. */
+ WRITE_ONCE(si->nr_clusters_mapped, start_idx);
+ mutex_unlock(&si->xswap_lock);
+}
+
+/* Return 1 at first present PTE to signal range is already mapped. */
+static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data)
+{
+ return 1;
+}
+#endif /* CONFIG_XSWAP */
+
static int setup_swap_clusters_info(struct swap_info_struct *si,
union swap_header *swap_header,
unsigned long maxpages)
@@ -3569,6 +3764,66 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
int err = -ENOMEM;
unsigned long i;
+#ifdef CONFIG_XSWAP
+ if (si->flags & SWP_XSWAP) {
+ unsigned long size = PAGE_ALIGN(nr_clusters * sizeof(*cluster_info));
+ struct vm_struct *vm;
+
+ vm = get_vm_area(size, VM_SPARSE);
+ if (!vm)
+ goto err;
+
+ cluster_info = vm->addr;
+ si->cluster_vm = vm;
+ si->nr_clusters = nr_clusters;
+ si->cluster_info = cluster_info;
+
+ /* Must be initialized before xswap_map_clusters() locks it. */
+ mutex_init(&si->xswap_lock);
+
+ /* Map the initial chunk (at least cluster 0) */
+ if (xswap_map_clusters(si, 0, min_t(unsigned long,
+ XSWAP_GROW_CLUSTERS, nr_clusters)))
+ goto err_free_vm;
+
+ /* xswap: only cluster 0 slot 0 is bad */
+ err = swap_cluster_setup_bad_slot(si, cluster_info, 0, false);
+ if (err)
+ goto err_unmap;
+
+ INIT_LIST_HEAD(&si->free_clusters);
+ INIT_LIST_HEAD(&si->full_clusters);
+ INIT_LIST_HEAD(&si->discard_clusters);
+ for (i = 0; i < SWAP_NR_ORDERS; i++) {
+ INIT_LIST_HEAD(&si->nonfull_clusters[i]);
+ INIT_LIST_HEAD(&si->frag_clusters[i]);
+ }
+
+ /* Mark mapped clusters: cluster 0 has 1 bad slot, rest free */
+ for (i = 0; i < si->nr_clusters_mapped; i++) {
+ struct swap_cluster_info *ci = &cluster_info[i];
+
+ if (i == 0) {
+ ci->flags = CLUSTER_FLAG_NONFULL;
+ list_add_tail(&ci->list, &si->nonfull_clusters[0]);
+ } else {
+ ci->flags = CLUSTER_FLAG_FREE;
+ list_add_tail(&ci->list, &si->free_clusters);
+ }
+ }
+
+ return 0;
+
+err_unmap:
+ xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
+err_free_vm:
+ free_vm_area(si->cluster_vm);
+ si->cluster_vm = NULL;
+ si->cluster_info = NULL;
+ return err;
+ }
+#endif /* CONFIG_XSWAP */
+
cluster_info = kvzalloc_objs(*cluster_info, nr_clusters);
if (!cluster_info)
goto err;
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 06/16] mm, swap: add sysfs create interface for xswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (4 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
` (13 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
xswap devices have no backing storage, so there is no file to swapon.
Add a sysfs interface to create them directly:
/sys/kernel/mm/xswap/create write a percent (0 = use default) to
create a device
The initial cluster ceiling (nr_clusters) for a new device is set by
XSWAP_DEFAULT_CLUSTER_PERCENT of totalram_pages. The device appears in
/proc/swaps as "xswap<N>". Per-device runtime sizing is tuned via the
debugfs knob added later in this series.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 165 ++++++++++++++++++++++++++++++++++++++++++++++++--
1 file changed, 161 insertions(+), 4 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 06f73c0a504a..1127e3d6596b 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -16,6 +16,8 @@
#include <linux/kernel_stat.h>
#include <linux/swap.h>
#include <linux/vmalloc.h>
+#include <linux/kobject.h>
+#include <linux/sysfs.h>
#include <linux/pagemap.h>
#include <linux/namei.h>
#include <linux/shmem_fs.h>
@@ -60,13 +62,79 @@
*/
#define XSWAP_GROW_CLUSTERS \
max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
+#define XSWAP_DEFAULT_CLUSTER_PERCENT 30
static int xswap_map_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr);
static void xswap_unmap_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr);
static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
-#endif
+
+#ifdef CONFIG_SYSFS
+static int xswap_create(int percent);
+/* /sys/kernel/mm/xswap/: create.
+ * Per-device runtime size is tuned via debugfs type<N>_cluster_limit.
+ */
+
+static ssize_t xswap_create_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ unsigned long percent;
+ int err;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ err = kstrtoul(buf, 0, &percent);
+ if (err)
+ return err;
+
+ /* 0 means "use the default percent" */
+ if (percent == 0)
+ percent = XSWAP_DEFAULT_CLUSTER_PERCENT;
+
+ err = xswap_create(percent);
+ if (err < 0)
+ return err;
+
+ return count;
+}
+
+static struct kobj_attribute xswap_create_attr = __ATTR(create, 0200, NULL,
+ xswap_create_store);
+
+static struct attribute *xswap_attrs[] = {
+ &xswap_create_attr.attr,
+ NULL,
+};
+
+static const struct attribute_group xswap_attr_group = {
+ .attrs = xswap_attrs,
+};
+
+static struct kobject *xswap_kobj;
+
+static void xswap_sysfs_init(void)
+{
+ xswap_kobj = kobject_create_and_add("xswap", mm_kobj);
+ if (!xswap_kobj) {
+ pr_err("xswap: failed to create sysfs kobject\n");
+ return;
+ }
+ if (sysfs_create_group(xswap_kobj, &xswap_attr_group))
+ pr_err("xswap: failed to create sysfs group\n");
+}
+#else
+static inline void xswap_sysfs_init(void)
+{
+}
+#endif /* CONFIG_SYSFS */
+#else /* !CONFIG_XSWAP */
+static inline void xswap_sysfs_init(void)
+{
+}
+#endif /* CONFIG_XSWAP */
static void swap_range_alloc(struct swap_info_struct *si,
unsigned int nr_entries);
@@ -3312,7 +3380,7 @@ static void *swap_start(struct seq_file *swap, loff_t *pos)
return SEQ_START_TOKEN;
for (type = 0; (si = swap_type_to_info(type)); type++) {
- if (!(si->swap_file))
+ if (!(si->swap_file) && !(si->flags & SWP_XSWAP))
continue;
if (!--l)
return si;
@@ -3333,7 +3401,7 @@ static void *swap_next(struct seq_file *swap, void *v, loff_t *pos)
++(*pos);
for (; (si = swap_type_to_info(type)); type++) {
- if (!(si->swap_file))
+ if (!(si->swap_file) && !(si->flags & SWP_XSWAP))
continue;
return si;
}
@@ -3375,7 +3443,14 @@ static int swap_show(struct seq_file *swap, void *v)
inuse = K(swap_usage_in_pages(si));
file = si->swap_file;
- len = seq_file_path(swap, file, " \t\n\\");
+ if (file)
+ len = seq_file_path(swap, file, " \t\n\\");
+ else {
+ char name[16];
+
+ len = scnprintf(name, sizeof(name), "xswap%d", si->type);
+ seq_puts(swap, name);
+ }
seq_printf(swap, "%*s%s\t%lu\t%s%lu\t%s%d\n",
len < 40 ? 40 - len : 1, " ",
swap_type_str(si),
@@ -3897,6 +3972,86 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
return err;
}
+#ifdef CONFIG_XSWAP
+#ifdef CONFIG_SYSFS
+/* Create a file-less xswap device. si->max = full RAM; @percent sets the
+ * runtime nr_clusters ceiling.
+ */
+static int xswap_create(int percent)
+{
+ struct swap_info_struct *si;
+ unsigned long ram, maxpages, init_clusters;
+ int error;
+
+ if (percent < 1 || percent > 100)
+ return -EINVAL;
+
+ si = alloc_swap_info();
+ if (IS_ERR(si))
+ return PTR_ERR(si);
+
+ INIT_WORK(&si->discard_work, swap_discard_work);
+ INIT_WORK(&si->reclaim_work, swap_reclaim_work);
+
+ ram = totalram_pages();
+ maxpages = min_t(unsigned long, ram, swapfile_maximum_size);
+ if ((unsigned int)maxpages == 0)
+ maxpages = UINT_MAX;
+ if (maxpages < 2)
+ maxpages = 2;
+
+ si->bdev = NULL;
+ si->flags |= SWP_XSWAP | SWP_SOLIDSTATE;
+ si->max = maxpages;
+ si->pages = maxpages - 1;
+ /* no backing file: mirror the xswap branch of setup_swap_extents() */
+ si->ops = &swap_bdev_ops;
+
+ error = setup_swap_clusters_info(si, NULL, maxpages);
+ if (error)
+ goto bad_swap;
+
+ /* VM_SPARSE covers full RAM; runtime nr_clusters starts at percent. */
+ init_clusters = div_u64((u64)maxpages * percent, 100 * SWAPFILE_CLUSTER);
+ init_clusters = max_t(unsigned long, init_clusters, XSWAP_GROW_CLUSTERS);
+ if (init_clusters > si->nr_clusters)
+ init_clusters = si->nr_clusters;
+ si->nr_clusters = init_clusters;
+ si->pages = min_t(unsigned long, init_clusters * SWAPFILE_CLUSTER, si->max) - 1;
+
+ error = zswap_swapon(si->type, si->max);
+ if (error)
+ goto bad_swap;
+
+ mutex_lock(&swapon_mutex);
+ si->prio = DEF_SWAP_PRIO;
+ si->list.prio = -si->prio;
+ si->avail_list.prio = -si->prio;
+ /* si->swap_file stays NULL: this is a file-less device */
+ enable_swap_info(si);
+ mutex_unlock(&swapon_mutex);
+
+ pr_info("xswap: adding extendable swap type %d (%d%% of %lu pages = %u pages, max %lu)\n",
+ si->type, percent, ram, si->pages, maxpages);
+ atomic_inc(&proc_poll_event);
+ wake_up_interruptible(&proc_poll_wait);
+
+ return si->type;
+
+bad_swap:
+ kfree(si->global_cluster);
+ si->global_cluster = NULL;
+ destroy_swap_extents(si, NULL); /* safe: xswap never sets SWP_ACTIVATED */
+ free_swap_cluster_info(si);
+ si->cluster_info = NULL;
+ spin_lock(&swap_lock);
+ si->flags = 0;
+ spin_unlock(&swap_lock);
+ return error;
+}
+#endif /* CONFIG_SYSFS */
+#endif /* CONFIG_XSWAP */
+
SYSCALL_DEFINE2(swapon, const char __user *, specialfile, int, swap_flags)
{
struct swap_info_struct *si;
@@ -4240,6 +4395,8 @@ static int __init swapfile_init(void)
swap_migration_ad_supported = true;
#endif /* CONFIG_MIGRATION */
+ xswap_sysfs_init();
+
return 0;
}
subsys_initcall(swapfile_init);
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (5 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 06/16] mm, swap: add sysfs create interface for xswap Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-09-02 14:15 ` Nhat Pham
2026-08-27 9:44 ` [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Baoquan He
` (12 subsequent siblings)
19 siblings, 1 reply; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
When cluster_alloc_swap_entry() fails to find a free cluster and
the xswap device still has room to grow, expand the mapped range
by XSWAP_GROW_CLUSTERS clusters.
Since xswap is always SWP_SOLIDSTATE, no locks need to be dropped
before calling xswap_map_clusters(), global_cluster_lock is never
held on this path.
The grow sequence:
1. Check nr_clusters_mapped < nr_clusters and free list empty
2. Call xswap_map_clusters() to allocate and map more physical pages
3. Add newly mapped clusters to si->free_clusters under si->lock
4. Retry allocation from the fresh free clusters
This makes the xswap cluster space grow transparently as swap usage
increases, without any userspace intervention.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 45 +++++++++++++++++++++++++++++++++++++++++++++
1 file changed, 45 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 1127e3d6596b..c6a01d74298a 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -1273,6 +1273,51 @@ static unsigned long cluster_alloc_swap_entry(struct swap_info_struct *si,
if (found)
goto done;
}
+
+#ifdef CONFIG_XSWAP
+ /*
+ * For xswap: if no free cluster was found and more clusters
+ * can be mapped, grow the cluster_info array and retry.
+ */
+ if (!found && (si->flags & SWP_XSWAP) &&
+ READ_ONCE(si->nr_clusters_mapped) < READ_ONCE(si->nr_clusters) &&
+ list_empty(&si->free_clusters)) {
+ unsigned long nr_new = min(READ_ONCE(si->nr_clusters) -
+ READ_ONCE(si->nr_clusters_mapped),
+ XSWAP_GROW_CLUSTERS);
+ unsigned long start = READ_ONCE(si->nr_clusters_mapped);
+ unsigned long i;
+
+ if (!xswap_map_clusters(si, start, nr_new)) {
+ unsigned long added = 0;
+
+ for (i = start; i < start + nr_new; i++) {
+ struct swap_cluster_info *ci = &si->cluster_info[i];
+
+ /*
+ * A concurrent grower may have already added
+ * these clusters to the free list. Only add
+ * clusters that are still off-list (NONE).
+ * Lock ci->lock first: move_cluster() takes
+ * si->lock internally.
+ */
+ spin_lock(&ci->lock);
+ if (ci->flags == CLUSTER_FLAG_NONE) {
+ move_cluster(si, ci, &si->free_clusters,
+ CLUSTER_FLAG_FREE);
+ added++;
+ }
+ spin_unlock(&ci->lock);
+ }
+ WRITE_ONCE(si->nr_free_tail,
+ READ_ONCE(si->nr_free_tail) + added);
+
+ /* Retry allocation from the free list */
+ found = alloc_swap_scan_list(si, &si->free_clusters,
+ folio, false);
+ }
+ }
+#endif
done:
if (!(si->flags & SWP_SOLIDSTATE))
spin_unlock(&si->global_cluster_lock);
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
2026-08-27 9:44 ` [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
@ 2026-09-02 14:15 ` Nhat Pham
2026-09-03 8:24 ` Baoquan He
0 siblings, 1 reply; 36+ messages in thread
From: Nhat Pham @ 2026-09-02 14:15 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 5:45 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
>
> When cluster_alloc_swap_entry() fails to find a free cluster and
> the xswap device still has room to grow, expand the mapped range
> by XSWAP_GROW_CLUSTERS clusters.
>
> Since xswap is always SWP_SOLIDSTATE, no locks need to be dropped
> before calling xswap_map_clusters(), global_cluster_lock is never
> held on this path.
What about local_lock()? I believe we're still holding
percpu_swap_cluster's local lock as we invoke xswap_map_clusters()?
Would this lead to issues :/
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
2026-09-02 14:15 ` Nhat Pham
@ 2026-09-03 8:24 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-03 8:24 UTC (permalink / raw)
To: Nhat Pham
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, baohua, youngjun.park,
hannes, yosry, shikemeng, chengming.zhou, david, linux-kernel
On 09/02/26 at 10:15am, Nhat Pham wrote:
> On Thu, Aug 27, 2026 at 5:45 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
> >
> > When cluster_alloc_swap_entry() fails to find a free cluster and
> > the xswap device still has room to grow, expand the mapped range
> > by XSWAP_GROW_CLUSTERS clusters.
> >
> > Since xswap is always SWP_SOLIDSTATE, no locks need to be dropped
> > before calling xswap_map_clusters(), global_cluster_lock is never
> > held on this path.
>
> What about local_lock()? I believe we're still holding
> percpu_swap_cluster's local lock as we invoke xswap_map_clusters()?
> Would this lead to issues :/
Good question. Kashiko also reported this , have fixed it by moving
swap_alloc_slow()() out of the lock scope as swap_alloc_slow() does not
touch the per-cpu swap cluster cache.
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (6 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:44 ` [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters Baoquan He
` (11 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Add xswap_try_shrink(), the shrink logic that scans backwards from
the tail to find contiguous free clusters, then unmaps full pages
when >= XSWAP_GROW_CLUSTERS free clusters accumulate.
Wire the trigger in __free_cluster(): after a cluster is released to
the free list, call xswap_try_shrink() to attempt tail shrinking.
Also update the XSWAP_GROW_CLUSTERS comment to reflect both grow
and shrink semantics.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 48 ++++++++++++++++++++++++++++++++++++++++++++----
1 file changed, 44 insertions(+), 4 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index c6a01d74298a..6b18c1b21adc 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -53,11 +53,13 @@
#ifdef CONFIG_XSWAP
/*
- * xswap: dynamically grow the cluster_info array via a VM_SPARSE area.
+ * xswap: dynamically grow and shrink the cluster_info array via a
+ * VM_SPARSE area.
*
- * XSWAP_GROW_CLUSTERS is the number of clusters to map in one grow
- * operation. It is set to the number of cluster_info structs that
- * fit in a single page (at least 16), so that the vmalloc page table
+ * XSWAP_GROW_CLUSTERS is the number of clusters to map/unmap in one
+ * grow/shrink operation. It is set to the number of cluster_info
+ * structs that fit in a single page (at least 16), so that the vmalloc
+ * page table
* overhead is proportional to the number of clusters mapped.
*/
#define XSWAP_GROW_CLUSTERS \
@@ -69,6 +71,7 @@ static int xswap_map_clusters(struct swap_info_struct *si,
static void xswap_unmap_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr);
static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
+static void xswap_try_shrink(struct swap_info_struct *si);
#ifdef CONFIG_SYSFS
static int xswap_create(int percent);
@@ -697,6 +700,9 @@ static void __free_cluster(struct swap_info_struct *si, struct swap_cluster_info
swap_cluster_free_table(ci);
move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE);
ci->order = 0;
+#ifdef CONFIG_XSWAP
+ xswap_try_shrink(si);
+#endif
}
/*
@@ -3873,6 +3879,40 @@ static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data)
{
return 1;
}
+
+/*
+ * Try to shrink the cluster_info tail: unmap contiguous free clusters
+ * at the end of the mapped range.
+ */
+static void xswap_try_shrink(struct swap_info_struct *si)
+{
+ struct swap_cluster_info *ci;
+ unsigned long last, idx;
+
+ if (!(si->flags & SWP_XSWAP))
+ return;
+ if (READ_ONCE(si->nr_clusters_mapped) <= 1) /* keep cluster 0 */
+ return;
+
+ /* Find the last non-free cluster from the tail */
+ last = READ_ONCE(si->nr_clusters_mapped);
+ while (last > 1) {
+ idx = last - 1;
+ ci = &si->cluster_info[idx];
+ if (ci->count || ci->flags != CLUSTER_FLAG_FREE)
+ break;
+ last = idx;
+ }
+
+ if (last == si->nr_clusters_mapped)
+ return; /* nothing to shrink */
+
+ /* Only unmap if we can free at least one full page of clusters */
+ if (si->nr_clusters_mapped - last < XSWAP_GROW_CLUSTERS)
+ return;
+
+ xswap_unmap_clusters(si, last, si->nr_clusters_mapped - last);
+}
#endif /* CONFIG_XSWAP */
static int setup_swap_clusters_info(struct swap_info_struct *si,
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (7 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Baoquan He
@ 2026-08-27 9:44 ` Baoquan He
2026-08-27 9:45 ` [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection Baoquan He
` (10 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:44 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
vm_area_unmap_pages() only clears PTEs and frees intermediate page
table pages - it does not free the backing physical pages allocated
by xswap_map_clusters().
Fix this by walking the page table with apply_to_existing_page_range()
before the unmap to collect all struct pages in the range. After
vunmap_range() clears the PTEs, free the collected pages via
__free_page().
Use a simple xswap_page_data collector callback: for each present PTE,
collect pte_page() into a dynamically allocated array. The array is
freed after the pages are released.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 41 +++++++++++++++++++++++++++++++++++++++--
1 file changed, 39 insertions(+), 2 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 6b18c1b21adc..0fe4834eadde 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3847,6 +3847,23 @@ static int xswap_map_clusters(struct swap_info_struct *si,
return -ENOMEM;
}
+struct xswap_page_data {
+ struct page **pages;
+ int nr;
+ int max;
+};
+
+static int xswap_collect_page(pte_t *pte, unsigned long addr, void *data)
+{
+ struct xswap_page_data *xpd = data;
+
+ if (!pte_present(*pte))
+ return 0;
+ if (xpd->nr < xpd->max)
+ xpd->pages[xpd->nr++] = pte_page(*pte);
+ return 0;
+}
+
static void xswap_unmap_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr)
{
@@ -3856,6 +3873,10 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
/* Round to page boundaries for vm_area_unmap_pages(). */
unsigned long vm_start = PAGE_ALIGN(start_addr);
unsigned long vm_end = PAGE_ALIGN(end_addr);
+ unsigned long size;
+ unsigned long npages;
+ struct xswap_page_data xpd;
+ int i;
mutex_lock(&si->xswap_lock);
@@ -3865,9 +3886,25 @@ static void xswap_unmap_clusters(struct swap_info_struct *si,
return;
}
+ size = vm_end - vm_start;
+ npages = size >> PAGE_SHIFT;
+
+ xpd.pages = kmalloc_array(npages, sizeof(*xpd.pages), GFP_KERNEL);
+ if (xpd.pages) {
+ xpd.nr = 0;
+ xpd.max = npages;
+ apply_to_existing_page_range(&init_mm, vm_start, size,
+ xswap_collect_page, &xpd);
+ }
+
vm_area_unmap_pages(si->cluster_vm, vm_start, vm_end);
- /* vm_area_unmap_pages() clears PTEs but does not free pages. */
- /* TODO: free backing pages via page table walk or tracking bitmap */
+
+ /* Free the collected backing pages */
+ if (xpd.pages) {
+ for (i = 0; i < xpd.nr; i++)
+ __free_page(xpd.pages[i]);
+ kfree(xpd.pages);
+ }
/* Pairs with READ_ONCE() in shrink/grow paths. */
WRITE_ONCE(si->nr_clusters_mapped, start_idx);
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (8 preceding siblings ...)
2026-08-27 9:44 ` [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap Baoquan He
` (9 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Track contiguous free clusters at the tail of the mapped range in
si->nr_free_tail, maintained across alloc/free/grow paths. This
eliminates the backwards scan on every shrink check.
Three paths maintain the counter:
1. xswap_update_free_tail(): called on cluster free. If the freed
cluster is adjacent to the existing tail boundary, increment and
extend backwards to include already-free clusters now connected.
2. xswap_trim_free_tail(): called on cluster allocation. If the
allocated cluster lies within the tail free region, truncate the
count to end just before it.
3. Grow path: nr_free_tail += added - only the clusters actually
added to the free list extend the tail; a concurrent grower that
lost the race added none.
xswap_try_shrink() simplifies to a threshold check:
if nr_free_tail >= XSWAP_GROW_CLUSTERS -> unmap
Setup initializes nr_free_tail = nr_clusters_mapped - 1 (all but
cluster 0 are free at the tail).
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 128 +++++++++++++++++++++++++++++++++++++------
2 files changed, 112 insertions(+), 17 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index c2744fa8a009..fea0005b9f51 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -252,6 +252,7 @@ struct swap_info_struct {
struct vm_struct *cluster_vm; /* VM_SPARSE area for xswap dynamic cluster_info */
unsigned long nr_clusters; /* total cluster count for xswap */
unsigned long nr_clusters_mapped; /* currently mapped cluster count */
+ unsigned long nr_free_tail; /* contiguous free clusters at tail */
struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 0fe4834eadde..5235c6d07ab2 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -71,6 +71,9 @@ static int xswap_map_clusters(struct swap_info_struct *si,
static void xswap_unmap_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr);
static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data);
+static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx);
+static void xswap_update_free_tail(struct swap_info_struct *si,
+ unsigned long freed_idx);
static void xswap_try_shrink(struct swap_info_struct *si);
#ifdef CONFIG_SYSFS
@@ -701,6 +704,7 @@ static void __free_cluster(struct swap_info_struct *si, struct swap_cluster_info
move_cluster(si, ci, &si->free_clusters, CLUSTER_FLAG_FREE);
ci->order = 0;
#ifdef CONFIG_XSWAP
+ xswap_update_free_tail(si, ci - si->cluster_info);
xswap_try_shrink(si);
#endif
}
@@ -1049,6 +1053,9 @@ static bool __swap_cluster_alloc_entries(struct swap_info_struct *si,
if (cluster_is_empty(ci))
ci->order = order;
ci->count += nr_pages;
+#ifdef CONFIG_XSWAP
+ xswap_trim_free_tail(si, cluster_index(si, ci));
+#endif
swap_range_alloc(si, nr_pages);
return true;
@@ -3918,37 +3925,124 @@ static int xswap_check_mapped(pte_t *pte, unsigned long addr, void *data)
}
/*
- * Try to shrink the cluster_info tail: unmap contiguous free clusters
- * at the end of the mapped range.
+ * Maintain si->nr_free_tail, the number of contiguous free clusters at
+ * the tail of the mapped range. Called when a cluster at @freed_idx is
+ * freed. Provides O(1) shrink detection: if nr_free_tail is non-zero,
+ * the tail can be unmapped without scanning cluster_info[].
+ *
+ * Only increments when @freed_idx is the cluster immediately before the
+ * existing tail region. Then scans backwards for already-free clusters
+ * now connected to the tail, bounded by XSWAP_GROW_CLUSTERS at a time.
*/
-static void xswap_try_shrink(struct swap_info_struct *si)
+static void xswap_update_free_tail(struct swap_info_struct *si,
+ unsigned long freed_idx)
{
+ unsigned long nr_mapped, nr_tail, tid, i;
struct swap_cluster_info *ci;
- unsigned long last, idx;
if (!(si->flags & SWP_XSWAP))
return;
- if (READ_ONCE(si->nr_clusters_mapped) <= 1) /* keep cluster 0 */
+
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+
+ /* Protect against concurrent shrink that races past us */
+ if (nr_tail >= nr_mapped)
+ return;
+
+ tid = nr_mapped - nr_tail - 1;
+
+ /* Only the cluster immediately before the tail region counts */
+ if (freed_idx != tid)
return;
- /* Find the last non-free cluster from the tail */
- last = READ_ONCE(si->nr_clusters_mapped);
- while (last > 1) {
- idx = last - 1;
- ci = &si->cluster_info[idx];
- if (ci->count || ci->flags != CLUSTER_FLAG_FREE)
+ nr_tail++;
+ WRITE_ONCE(si->nr_free_tail, nr_tail);
+
+ /* Extend: include already-free clusters now connected to the tail */
+ for (i = 1; i < XSWAP_GROW_CLUSTERS; i++) {
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+ if (nr_tail >= nr_mapped - 1)
+ break; /* reached cluster 0 */
+ tid = nr_mapped - nr_tail - 1;
+ ci = &si->cluster_info[tid];
+
+ if (READ_ONCE(ci->count) ||
+ READ_ONCE(ci->flags) != CLUSTER_FLAG_FREE)
break;
- last = idx;
+ nr_tail++;
+ WRITE_ONCE(si->nr_free_tail, nr_tail);
}
+}
- if (last == si->nr_clusters_mapped)
- return; /* nothing to shrink */
+/*
+ * Trim si->nr_free_tail when a cluster in the tail region is allocated.
+ * @idx: index of the cluster being allocated.
+ */
+static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
+{
+ unsigned long nr_mapped, nr_tail, tail_start;
- /* Only unmap if we can free at least one full page of clusters */
- if (si->nr_clusters_mapped - last < XSWAP_GROW_CLUSTERS)
+ if (!(si->flags & SWP_XSWAP))
return;
- xswap_unmap_clusters(si, last, si->nr_clusters_mapped - last);
+ /*
+ * nr_clusters_mapped and nr_free_tail are read locklessly;
+ * concurrent updates may cause nr_free_tail to be trimmed
+ * slightly less than ideally, which is harmless.
+ */
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+ tail_start = nr_mapped - nr_tail;
+ if (idx >= tail_start)
+ WRITE_ONCE(si->nr_free_tail, nr_mapped - idx - 1);
+}
+
+/*
+ * Try to shrink the cluster_info tail. Uses si->nr_free_tail which
+ * is maintained incrementally during alloc/free — no scanning needed.
+ */
+static void xswap_try_shrink(struct swap_info_struct *si)
+{
+ unsigned long start_idx, nr_unmap, i;
+ struct swap_cluster_info *ci;
+
+ if (!(si->flags & SWP_XSWAP))
+ return;
+ if (si->nr_free_tail < XSWAP_GROW_CLUSTERS)
+ return;
+
+ nr_unmap = round_down(si->nr_free_tail, XSWAP_GROW_CLUSTERS);
+ start_idx = si->nr_clusters_mapped - nr_unmap;
+
+ /*
+ * Verify the tail clusters are still free before unmapping. Count the
+ * contiguous free run first, then detach it in a second pass once the
+ * whole run is confirmed long enough. Detaching while counting would
+ * leave a partial run off the free list if it proved too short to unmap.
+ */
+ spin_lock(&si->lock);
+ for (i = start_idx; i < si->nr_clusters_mapped; i++) {
+ ci = &si->cluster_info[i];
+ if (ci->flags != CLUSTER_FLAG_FREE)
+ break;
+ }
+ nr_unmap = i - start_idx;
+ if (nr_unmap < XSWAP_GROW_CLUSTERS) {
+ spin_unlock(&si->lock);
+ return;
+ }
+
+ for (i = start_idx; i < start_idx + nr_unmap; i++) {
+ ci = &si->cluster_info[i];
+ list_del_init(&ci->list);
+ ci->flags = CLUSTER_FLAG_NONE;
+ }
+ spin_unlock(&si->lock);
+
+ xswap_unmap_clusters(si, start_idx, nr_unmap);
+ si->nr_free_tail -= nr_unmap;
}
#endif /* CONFIG_XSWAP */
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (9 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit Baoquan He
` (8 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Split the xswap cluster limit into two fields:
- nr_clusters_max: immutable hard limit set at device creation
- nr_clusters: current growth ceiling, adjustable at runtime (<= nr_clusters_max)
The grow path already uses nr_clusters as the ceiling. Shrink now also
respects it: when nr_clusters drops below nr_clusters_mapped, shrinking
fires on free until the mapped count reaches the ceiling. When
nr_clusters == nr_clusters_max (default), shrink is effectively
disabled - all growth and no shrink.
At creation, nr_clusters starts at nr_clusters_max (full size).
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 3 ++-
mm/swapfile.c | 29 +++++++++++++++++++++--------
2 files changed, 23 insertions(+), 9 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index fea0005b9f51..c55af4a95ef7 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -250,7 +250,8 @@ struct swap_info_struct {
struct swap_cluster_info *cluster_info; /* cluster info. Only for SSD */
#ifdef CONFIG_XSWAP
struct vm_struct *cluster_vm; /* VM_SPARSE area for xswap dynamic cluster_info */
- unsigned long nr_clusters; /* total cluster count for xswap */
+ unsigned long nr_clusters_max;/* upper limit from swap header */
+ unsigned long nr_clusters; /* current growth ceiling (≤ nr_clusters_max) */
unsigned long nr_clusters_mapped; /* currently mapped cluster count */
unsigned long nr_free_tail; /* contiguous free clusters at tail */
struct mutex xswap_lock; /* serialize map/unmap operations */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 5235c6d07ab2..b2c3bb21f082 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -3235,6 +3235,7 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
free_vm_area(si->cluster_vm);
si->cluster_vm = NULL;
+ si->nr_clusters_max = 0;
si->nr_clusters = 0;
si->nr_clusters_mapped = 0;
return;
@@ -4005,16 +4006,27 @@ static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
*/
static void xswap_try_shrink(struct swap_info_struct *si)
{
- unsigned long start_idx, nr_unmap, i;
+ unsigned long nr_mapped, nr_ceiling, nr_tail, nr_unmap;
+ unsigned long start_idx, i;
struct swap_cluster_info *ci;
if (!(si->flags & SWP_XSWAP))
return;
- if (si->nr_free_tail < XSWAP_GROW_CLUSTERS)
+
+ nr_mapped = READ_ONCE(si->nr_clusters_mapped);
+ nr_ceiling = READ_ONCE(si->nr_clusters);
+ nr_tail = READ_ONCE(si->nr_free_tail);
+
+ if (nr_mapped <= nr_ceiling)
+ return;
+ if (nr_tail < XSWAP_GROW_CLUSTERS)
return;
- nr_unmap = round_down(si->nr_free_tail, XSWAP_GROW_CLUSTERS);
- start_idx = si->nr_clusters_mapped - nr_unmap;
+ nr_unmap = min(round_down(nr_tail, XSWAP_GROW_CLUSTERS),
+ nr_mapped - nr_ceiling);
+ if (nr_unmap < XSWAP_GROW_CLUSTERS)
+ return;
+ start_idx = nr_mapped - nr_unmap;
/*
* Verify the tail clusters are still free before unmapping. Count the
@@ -4023,7 +4035,7 @@ static void xswap_try_shrink(struct swap_info_struct *si)
* leave a partial run off the free list if it proved too short to unmap.
*/
spin_lock(&si->lock);
- for (i = start_idx; i < si->nr_clusters_mapped; i++) {
+ for (i = start_idx; i < nr_mapped; i++) {
ci = &si->cluster_info[i];
if (ci->flags != CLUSTER_FLAG_FREE)
break;
@@ -4042,7 +4054,7 @@ static void xswap_try_shrink(struct swap_info_struct *si)
spin_unlock(&si->lock);
xswap_unmap_clusters(si, start_idx, nr_unmap);
- si->nr_free_tail -= nr_unmap;
+ WRITE_ONCE(si->nr_free_tail, nr_tail - nr_unmap);
}
#endif /* CONFIG_XSWAP */
@@ -4066,6 +4078,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
cluster_info = vm->addr;
si->cluster_vm = vm;
+ si->nr_clusters_max = nr_clusters;
si->nr_clusters = nr_clusters;
si->cluster_info = cluster_info;
@@ -4103,6 +4116,8 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
}
}
+ /* All mapped clusters except cluster 0 are free at the tail */
+ si->nr_free_tail = si->nr_clusters_mapped - 1;
return 0;
err_unmap:
@@ -4595,7 +4610,6 @@ void __folio_throttle_swaprate(struct folio *folio, gfp_t gfp)
static int __init swapfile_init(void)
{
swapfile_maximum_size = arch_max_swapfile_size();
-
/*
* Once a cluster is freed, it's swap table content is read
* only, and all swap cache readers (swap_cache_*) verifies
@@ -4605,7 +4619,6 @@ static int __init swapfile_init(void)
swap_table_cachep = kmem_cache_create("swap_table",
sizeof(struct swap_table),
0, SLAB_PANIC | SLAB_TYPESAFE_BY_RCU, NULL);
-
#ifdef CONFIG_MIGRATION
if (swapfile_maximum_size >= (1UL << SWP_MIG_TOTAL_BITS))
swap_migration_ad_supported = true;
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (10 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 9:45 ` [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Baoquan He
` (7 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Add a per-device debugfs file for runtime adjustment of xswap cluster
limit:
/sys/kernel/debug/xswap/type<N>_cluster_limit
Reading shows the current ceiling (in clusters); writing sets it
(clamped to [0, nr_clusters_max]). Setting below nr_clusters_mapped
triggers an immediate shrink check via xswap_try_shrink().
The debugfs entry is created on device creation and removed on
destruction.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 92 ++++++++++++++++++++++++++++++++++++++++++--
2 files changed, 90 insertions(+), 3 deletions(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index c55af4a95ef7..5e50486103b9 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -254,6 +254,7 @@ struct swap_info_struct {
unsigned long nr_clusters; /* current growth ceiling (⤠nr_clusters_max) */
unsigned long nr_clusters_mapped; /* currently mapped cluster count */
unsigned long nr_free_tail; /* contiguous free clusters at tail */
+ struct dentry *debugfs_entry; /* debugfs: type<N>_max_clusters */
struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index b2c3bb21f082..6916a8b3f63b 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -50,6 +50,9 @@
#include "swap_table.h"
#include "internal.h"
#include "swap.h"
+#include <linux/debugfs.h>
+
+static DEFINE_SPINLOCK(swap_lock);
#ifdef CONFIG_XSWAP
/*
@@ -66,6 +69,8 @@
max_t(unsigned long, PAGE_SIZE / sizeof(struct swap_cluster_info), 16)
#define XSWAP_DEFAULT_CLUSTER_PERCENT 30
+static struct dentry *xswap_debugfs_root;
+
static int xswap_map_clusters(struct swap_info_struct *si,
unsigned long start_idx, unsigned long nr);
static void xswap_unmap_clusters(struct swap_info_struct *si,
@@ -76,6 +81,83 @@ static void xswap_update_free_tail(struct swap_info_struct *si,
unsigned long freed_idx);
static void xswap_try_shrink(struct swap_info_struct *si);
+/*
+ * debugfs read/write for per-device max cluster count.
+ */
+static ssize_t xswap_max_clusters_read(struct file *file, char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct swap_info_struct *si = file->private_data;
+ char tmp[32];
+ int len;
+
+ len = snprintf(tmp, sizeof(tmp), "%lu\n", READ_ONCE(si->nr_clusters));
+ return simple_read_from_buffer(buf, count, ppos, tmp, len);
+}
+
+static ssize_t xswap_max_clusters_write(struct file *file,
+ const char __user *buf,
+ size_t count, loff_t *ppos)
+{
+ struct swap_info_struct *si = file->private_data;
+ unsigned long val, new_pages;
+ int err;
+
+ err = kstrtoul_from_user(buf, count, 0, &val);
+ if (err)
+ return err;
+
+ if (val > si->nr_clusters_max)
+ val = si->nr_clusters_max;
+
+ spin_lock(&si->lock);
+ si->nr_clusters = val;
+ spin_unlock(&si->lock);
+
+ new_pages = min_t(unsigned long, val * SWAPFILE_CLUSTER, si->max);
+ if (new_pages)
+ new_pages--;
+ if (new_pages != si->pages) {
+ long delta = (long)new_pages - (long)si->pages;
+
+ spin_lock(&swap_lock);
+ si->pages = new_pages;
+ atomic_long_add(delta, &nr_swap_pages);
+ total_swap_pages += delta;
+ spin_unlock(&swap_lock);
+ }
+
+ /* Lowering the ceiling may free tail clusters. */
+ xswap_try_shrink(si);
+
+ return count;
+}
+
+static const struct file_operations xswap_debugfs_fops = {
+ .read = xswap_max_clusters_read,
+ .write = xswap_max_clusters_write,
+ .open = simple_open,
+ .llseek = default_llseek,
+};
+
+static void xswap_debugfs_add(struct swap_info_struct *si)
+{
+ char name[32];
+
+ if (!xswap_debugfs_root)
+ return;
+
+ snprintf(name, sizeof(name), "type%d_cluster_limit", si->type);
+ si->debugfs_entry = debugfs_create_file(name, 0644, xswap_debugfs_root,
+ si, &xswap_debugfs_fops);
+}
+
+static void xswap_debugfs_del(struct swap_info_struct *si)
+{
+ debugfs_remove(si->debugfs_entry);
+ si->debugfs_entry = NULL;
+}
+
#ifdef CONFIG_SYSFS
static int xswap_create(int percent);
/* /sys/kernel/mm/xswap/: create.
@@ -156,7 +238,6 @@ static void move_cluster(struct swap_info_struct *si,
*
* Also protects swap_active_head total_swap_pages, and the SWP_WRITEOK flag.
*/
-static DEFINE_SPINLOCK(swap_lock);
static unsigned int nr_swapfiles;
atomic_long_t nr_swap_pages;
atomic_t nr_real_swapfiles;
@@ -3230,6 +3311,7 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
#ifdef CONFIG_XSWAP
if (si->flags & SWP_XSWAP) {
+ xswap_debugfs_del(si);
/* Unmap all mapped clusters and free the VM_SPARSE area */
if (si->nr_clusters_mapped > 0)
xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
@@ -4118,6 +4200,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
/* All mapped clusters except cluster 0 are free at the tail */
si->nr_free_tail = si->nr_clusters_mapped - 1;
+ xswap_debugfs_add(si);
return 0;
err_unmap:
@@ -4245,8 +4328,8 @@ static int xswap_create(int percent)
/* VM_SPARSE covers full RAM; runtime nr_clusters starts at percent. */
init_clusters = div_u64((u64)maxpages * percent, 100 * SWAPFILE_CLUSTER);
init_clusters = max_t(unsigned long, init_clusters, XSWAP_GROW_CLUSTERS);
- if (init_clusters > si->nr_clusters)
- init_clusters = si->nr_clusters;
+ if (init_clusters > si->nr_clusters_max)
+ init_clusters = si->nr_clusters_max;
si->nr_clusters = init_clusters;
si->pages = min_t(unsigned long, init_clusters * SWAPFILE_CLUSTER, si->max) - 1;
@@ -4624,6 +4707,9 @@ static int __init swapfile_init(void)
swap_migration_ad_supported = true;
#endif /* CONFIG_MIGRATION */
+#ifdef CONFIG_XSWAP
+ xswap_debugfs_root = debugfs_create_dir("xswap", NULL);
+#endif
xswap_sysfs_init();
return 0;
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (11 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-09-02 14:50 ` Nhat Pham
2026-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
` (6 subsequent siblings)
19 siblings, 1 reply; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
xswap_try_shrink() was called directly from __free_cluster() while
holding ci->lock. The shrink path calls xswap_unmap_clusters()
which unmaps vmalloc pages backing cluster_info, and on return
swap_cache_del_folio() tries swap_cluster_unlock(ci) on the now-
unmapped address - crashing on a not-present page.
Replace direct calls with schedule_work() so shrink runs in an
independent workqueue context where no cluster locks are held.
Use cancel_work_sync() during swapoff to ensure no pending shrink
work races with the VM area teardown.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
include/linux/swap.h | 1 +
mm/swapfile.c | 12 +++++++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/include/linux/swap.h b/include/linux/swap.h
index 5e50486103b9..998f04c0b767 100644
--- a/include/linux/swap.h
+++ b/include/linux/swap.h
@@ -255,6 +255,7 @@ struct swap_info_struct {
unsigned long nr_clusters_mapped; /* currently mapped cluster count */
unsigned long nr_free_tail; /* contiguous free clusters at tail */
struct dentry *debugfs_entry; /* debugfs: type<N>_max_clusters */
+ struct work_struct xswap_shrink_work; /* deferred shrink trigger */
struct mutex xswap_lock; /* serialize map/unmap operations */
#endif
struct list_head free_clusters; /* free clusters list */
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 6916a8b3f63b..dc112c378489 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -786,7 +786,7 @@ static void __free_cluster(struct swap_info_struct *si, struct swap_cluster_info
ci->order = 0;
#ifdef CONFIG_XSWAP
xswap_update_free_tail(si, ci - si->cluster_info);
- xswap_try_shrink(si);
+ schedule_work(&si->xswap_shrink_work);
#endif
}
@@ -3312,6 +3312,7 @@ static void free_swap_cluster_info(struct swap_info_struct *si)
#ifdef CONFIG_XSWAP
if (si->flags & SWP_XSWAP) {
xswap_debugfs_del(si);
+ cancel_work_sync(&si->xswap_shrink_work);
/* Unmap all mapped clusters and free the VM_SPARSE area */
if (si->nr_clusters_mapped > 0)
xswap_unmap_clusters(si, 0, si->nr_clusters_mapped);
@@ -4082,6 +4083,13 @@ static void xswap_trim_free_tail(struct swap_info_struct *si, unsigned long idx)
WRITE_ONCE(si->nr_free_tail, nr_mapped - idx - 1);
}
+static void xswap_shrink_work_fn(struct work_struct *work)
+{
+ struct swap_info_struct *si = container_of(work,
+ struct swap_info_struct, xswap_shrink_work);
+ xswap_try_shrink(si);
+}
+
/*
* Try to shrink the cluster_info tail. Uses si->nr_free_tail which
* is maintained incrementally during alloc/free — no scanning needed.
@@ -4200,6 +4208,8 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
/* All mapped clusters except cluster 0 are free at the tail */
si->nr_free_tail = si->nr_clusters_mapped - 1;
+
+ INIT_WORK(&si->xswap_shrink_work, xswap_shrink_work_fn);
xswap_debugfs_add(si);
return 0;
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
2026-08-27 9:45 ` [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Baoquan He
@ 2026-09-02 14:50 ` Nhat Pham
2026-09-03 9:17 ` Baoquan He
0 siblings, 1 reply; 36+ messages in thread
From: Nhat Pham @ 2026-09-02 14:50 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 5:57 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
>
> xswap_try_shrink() was called directly from __free_cluster() while
> holding ci->lock. The shrink path calls xswap_unmap_clusters()
> which unmaps vmalloc pages backing cluster_info, and on return
> swap_cache_del_folio() tries swap_cluster_unlock(ci) on the now-
> unmapped address - crashing on a not-present page.
This reads like a bug. It would be a bisectability problem to not
squash this to an earlier patch, no?
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
2026-09-02 14:50 ` Nhat Pham
@ 2026-09-03 9:17 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-03 9:17 UTC (permalink / raw)
To: Nhat Pham
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, baohua, youngjun.park,
hannes, yosry, shikemeng, chengming.zhou, david, linux-kernel
On 09/02/26 at 10:50am, Nhat Pham wrote:
> On Thu, Aug 27, 2026 at 5:57 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
> >
> > xswap_try_shrink() was called directly from __free_cluster() while
> > holding ci->lock. The shrink path calls xswap_unmap_clusters()
> > which unmaps vmalloc pages backing cluster_info, and on return
> > swap_cache_del_folio() tries swap_cluster_unlock(ci) on the now-
> > unmapped address - crashing on a not-present page.
>
> This reads like a bug. It would be a bisectability problem to not
> squash this to an earlier patch, no?
I intentionally keep it as an independent patch. This is a new feature,
we don't need to worry about bisectability problem. Mainly I want to
the code change explained clearer, any people can get it from the git log.
If I squash all them into one commit, it's very hard to let people know
which paragraph describes what change, esp the commit is big.
If it's a logic unit with a reason, I would separate it out as an
independent patch. People review it and ack if no objection, then go to
the next one. It's better than a big patch with which we reviewed one
round, this place is changed, next round, the other place is changed.
Then next round, a new place mixed the 1st place is changed. We sometime
even don't know what is settled, what is not.
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (12 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-09-03 6:59 ` Youngjun Park
2026-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
` (5 subsequent siblings)
19 siblings, 1 reply; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
1. Extract __swapoff() from sys_swapoff(): the core teardown logic
now lives in __swapoff(), shared by sys_swapoff() and the new
xswap_destroy(). swap_file operations are guarded with NULL check
so __swapoff() works for file-less devices too. sys_swapoff()
retains file-matching; a NULL guard on p->swap_file ensures xswap
devices are never matched by the file path.
2. Add xswap_destroy(int type): tears down a file-less xswap device
by its swap type. Validates SWP_XSWAP | SWP_WRITEOK, removes
from lists, delegates to __swapoff().
3. Add /sys/kernel/mm/xswap/destroy: write a swap type to tear down
that xswap device. Requires CAP_SYS_ADMIN.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 197 ++++++++++++++++++++++++++++++++++----------------
1 file changed, 136 insertions(+), 61 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index dc112c378489..451b46b747e3 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -160,6 +160,7 @@ static void xswap_debugfs_del(struct swap_info_struct *si)
#ifdef CONFIG_SYSFS
static int xswap_create(int percent);
+static int xswap_destroy(int type);
/* /sys/kernel/mm/xswap/: create.
* Per-device runtime size is tuned via debugfs type<N>_cluster_limit.
*/
@@ -192,8 +193,33 @@ static ssize_t xswap_create_store(struct kobject *kobj,
static struct kobj_attribute xswap_create_attr = __ATTR(create, 0200, NULL,
xswap_create_store);
+static ssize_t xswap_destroy_store(struct kobject *kobj,
+ struct kobj_attribute *attr,
+ const char *buf, size_t count)
+{
+ unsigned long type;
+ int err;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ err = kstrtoul(buf, 0, &type);
+ if (err)
+ return err;
+
+ err = xswap_destroy(type);
+ if (err)
+ return err;
+
+ return count;
+}
+
+static struct kobj_attribute xswap_destroy_attr = __ATTR(destroy, 0200, NULL,
+ xswap_destroy_store);
+
static struct attribute *xswap_attrs[] = {
&xswap_create_attr.attr,
+ &xswap_destroy_attr.attr,
NULL,
};
@@ -3361,61 +3387,13 @@ static void flush_percpu_swap_cluster(struct swap_info_struct *si)
}
-SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
+/* Common swap teardown after list removal; shared by sys_swapoff() and
+ * xswap_destroy().
+ */
+static int __swapoff(struct swap_info_struct *p)
{
- struct swap_info_struct *p = NULL;
- struct file *swap_file, *victim;
- struct address_space *mapping;
- struct inode *inode;
- int err, found = 0;
-
- if (!capable(CAP_SYS_ADMIN))
- return -EPERM;
-
- BUG_ON(!current->mm);
-
- CLASS(filename, pathname)(specialfile);
- victim = file_open_name(pathname, O_RDWR|O_LARGEFILE, 0);
- if (IS_ERR(victim))
- return PTR_ERR(victim);
-
- mapping = victim->f_mapping;
- spin_lock(&swap_lock);
- plist_for_each_entry(p, &swap_active_head, list) {
- if (p->flags & SWP_WRITEOK) {
- if (p->swap_file->f_mapping == mapping) {
- found = 1;
- break;
- }
- }
- }
- if (!found) {
- err = -EINVAL;
- spin_unlock(&swap_lock);
- goto out_dput;
- }
-
- /* Refuse swapoff while the device is pinned for hibernation */
- if (p->flags & SWP_HIBERNATION) {
- err = -EBUSY;
- spin_unlock(&swap_lock);
- goto out_dput;
- }
-
- if (!security_vm_enough_memory_mm(current->mm, p->pages))
- vm_unacct_memory(p->pages);
- else {
- err = -ENOMEM;
- spin_unlock(&swap_lock);
- goto out_dput;
- }
- spin_lock(&p->lock);
- del_from_avail_list(p, true);
- plist_del(&p->list, &swap_active_head);
- atomic_long_sub(p->pages, &nr_swap_pages);
- total_swap_pages -= p->pages;
- spin_unlock(&p->lock);
- spin_unlock(&swap_lock);
+ struct file *swap_file = NULL;
+ int err;
wait_for_allocation(p);
@@ -3426,7 +3404,7 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
if (err) {
/* re-insert swap space back into swap_list */
reinsert_swap_info(p);
- goto out_dput;
+ return err;
}
/*
@@ -3469,12 +3447,14 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
p->max = 0;
p->cluster_info = NULL;
- inode = mapping->host;
+ if (swap_file) {
+ struct inode *inode = swap_file->f_mapping->host;
- inode_lock(inode);
- inode->i_flags &= ~S_SWAPFILE;
- inode_unlock(inode);
- filp_close(swap_file, NULL);
+ inode_lock(inode);
+ inode->i_flags &= ~S_SWAPFILE;
+ inode_unlock(inode);
+ filp_close(swap_file, NULL);
+ }
/*
* Clear the SWP_USED flag after all resources are freed so that swapon
@@ -3485,10 +3465,69 @@ SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
p->flags = 0;
spin_unlock(&swap_lock);
- err = 0;
atomic_inc(&proc_poll_event);
wake_up_interruptible(&proc_poll_wait);
+ return 0;
+}
+
+SYSCALL_DEFINE1(swapoff, const char __user *, specialfile)
+{
+ struct swap_info_struct *p = NULL;
+ struct file *victim;
+ struct address_space *mapping;
+ int err, found = 0;
+
+ if (!capable(CAP_SYS_ADMIN))
+ return -EPERM;
+
+ BUG_ON(!current->mm);
+
+ CLASS(filename, pathname)(specialfile);
+ victim = file_open_name(pathname, O_RDWR|O_LARGEFILE, 0);
+ if (IS_ERR(victim))
+ return PTR_ERR(victim);
+
+ mapping = victim->f_mapping;
+ spin_lock(&swap_lock);
+ plist_for_each_entry(p, &swap_active_head, list) {
+ if (p->flags & SWP_WRITEOK) {
+ if (p->swap_file && p->swap_file->f_mapping == mapping) {
+ found = 1;
+ break;
+ }
+ }
+ }
+ if (!found) {
+ err = -EINVAL;
+ spin_unlock(&swap_lock);
+ goto out_dput;
+ }
+
+ /* Refuse swapoff while the device is pinned for hibernation */
+ if (p->flags & SWP_HIBERNATION) {
+ err = -EBUSY;
+ spin_unlock(&swap_lock);
+ goto out_dput;
+ }
+
+ if (!security_vm_enough_memory_mm(current->mm, p->pages))
+ vm_unacct_memory(p->pages);
+ else {
+ err = -ENOMEM;
+ spin_unlock(&swap_lock);
+ goto out_dput;
+ }
+ spin_lock(&p->lock);
+ del_from_avail_list(p, true);
+ plist_del(&p->list, &swap_active_head);
+ atomic_long_sub(p->pages, &nr_swap_pages);
+ total_swap_pages -= p->pages;
+ spin_unlock(&p->lock);
+ spin_unlock(&swap_lock);
+
+ err = __swapoff(p);
+
out_dput:
filp_close(victim, NULL);
return err;
@@ -4373,6 +4412,42 @@ static int xswap_create(int percent)
spin_unlock(&swap_lock);
return error;
}
+
+/* Tear down a file-less xswap device by its swap type. */
+static int xswap_destroy(int type)
+{
+ struct swap_info_struct *p;
+
+ p = swap_type_to_info(type);
+ if (!p)
+ return -EINVAL;
+
+ spin_lock(&swap_lock);
+ if (!(p->flags & SWP_WRITEOK) || !(p->flags & SWP_XSWAP)) {
+ spin_unlock(&swap_lock);
+ return -EINVAL;
+ }
+ /* Refuse swapoff while the device is pinned for hibernation */
+ if (p->flags & SWP_HIBERNATION) {
+ spin_unlock(&swap_lock);
+ return -EBUSY;
+ }
+ if (!security_vm_enough_memory_mm(current->mm, p->pages))
+ vm_unacct_memory(p->pages);
+ else {
+ spin_unlock(&swap_lock);
+ return -ENOMEM;
+ }
+ spin_lock(&p->lock);
+ del_from_avail_list(p, true);
+ plist_del(&p->list, &swap_active_head);
+ atomic_long_sub(p->pages, &nr_swap_pages);
+ total_swap_pages -= p->pages;
+ spin_unlock(&p->lock);
+ spin_unlock(&swap_lock);
+
+ return __swapoff(p);
+}
#endif /* CONFIG_SYSFS */
#endif /* CONFIG_XSWAP */
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
2026-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
@ 2026-09-03 6:59 ` Youngjun Park
2026-09-04 5:33 ` Baoquan He
0 siblings, 1 reply; 36+ messages in thread
From: Youngjun Park @ 2026-09-03 6:59 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, nphamcs, baohua, hannes, yosry,
shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 05:45:04PM +0800, Baoquan He wrote:
> +/* Tear down a file-less xswap device by its swap type. */
> +static int xswap_destroy(int type)
> +{
> + struct swap_info_struct *p;
> +
> + p = swap_type_to_info(type);
> + if (!p)
> + return -EINVAL;
> +
> + spin_lock(&swap_lock);
> + if (!(p->flags & SWP_WRITEOK) || !(p->flags & SWP_XSWAP)) {
> + spin_unlock(&swap_lock);
> + return -EINVAL;
> + }
> + /* Refuse swapoff while the device is pinned for hibernation */
> + if (p->flags & SWP_HIBERNATION) {
> + spin_unlock(&swap_lock);
> + return -EBUSY;
> + }
non-blockfile swap device must not be chosen for Hibernation.
There is no space to hibernate.
Thanks
Youngjun
^ permalink raw reply [flat|nested] 36+ messages in thread* Re: [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
2026-09-03 6:59 ` Youngjun Park
@ 2026-09-04 5:33 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-04 5:33 UTC (permalink / raw)
To: Youngjun Park
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, nphamcs, baohua,
hannes, yosry, shikemeng, chengming.zhou, david, linux-kernel
On 09/03/26 at 03:59pm, Youngjun Park wrote:
> On Thu, Aug 27, 2026 at 05:45:04PM +0800, Baoquan He wrote:
> > +/* Tear down a file-less xswap device by its swap type. */
> > +static int xswap_destroy(int type)
> > +{
> > + struct swap_info_struct *p;
> > +
> > + p = swap_type_to_info(type);
> > + if (!p)
> > + return -EINVAL;
> > +
> > + spin_lock(&swap_lock);
> > + if (!(p->flags & SWP_WRITEOK) || !(p->flags & SWP_XSWAP)) {
> > + spin_unlock(&swap_lock);
> > + return -EINVAL;
> > + }
> > + /* Refuse swapoff while the device is pinned for hibernation */
> > + if (p->flags & SWP_HIBERNATION) {
> > + spin_unlock(&swap_lock);
> > + return -EBUSY;
> > + }
>
> non-blockfile swap device must not be chosen for Hibernation.
> There is no space to hibernate.
You areright.
xswap devices are file-less and zswap-backed with no disk space,
so they cannot host a hibernation image. Sashiko has reported this, v2
excludes SWP_XSWAP devices from hibernation device discovery (both
find_first_swap() and __find_hibernation_swap_type()), so they are never
chosen for hibernation.
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH 15/16] mm, swap: require zswap for xswap devices
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (13 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-09-03 6:52 ` Youngjun Park
2026-08-27 9:45 ` [PATCH 16/16] mm, swap: allow setting xswap device priority at creation Baoquan He
` (4 subsequent siblings)
19 siblings, 1 reply; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
xswap has no backing storage: swapped-out pages live only in zswap.
Without zswap, swapout always bounces back, so the device would
consume swap entry space without ever freeing memory. Refuse to
create a device when zswap is unavailable.
Runtime disabling of zswap after creation is safe: existing entries
stay loadable (zswap_load() gates on zswap_never_enabled(), not the
runtime zswap_enabled flag) and new swapouts merely bounce back to
memory without freeing it.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index 451b46b747e3..ae0c549cc9aa 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -4349,6 +4349,10 @@ static int xswap_create(int percent)
if (percent < 1 || percent > 100)
return -EINVAL;
+ /* xswap has no backing store, it relies on zswap. */
+ if (!zswap_is_enabled())
+ return -EOPNOTSUPP;
+
si = alloc_swap_info();
if (IS_ERR(si))
return PTR_ERR(si);
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* Re: [PATCH 15/16] mm, swap: require zswap for xswap devices
2026-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
@ 2026-09-03 6:52 ` Youngjun Park
2026-09-04 7:57 ` Baoquan He
0 siblings, 1 reply; 36+ messages in thread
From: Youngjun Park @ 2026-09-03 6:52 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, nphamcs, baohua, hannes, yosry,
shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 05:45:05PM +0800, Baoquan He wrote:
> xswap has no backing storage: swapped-out pages live only in zswap.
> Without zswap, swapout always bounces back, so the device would
> consume swap entry space without ever freeing memory. Refuse to
> create a device when zswap is unavailable.
>
> Runtime disabling of zswap after creation is safe: existing entries
> stay loadable (zswap_load() gates on zswap_never_enabled(), not the
> runtime zswap_enabled flag) and new swapouts merely bounce back to
> memory without freeing it.
Hello Boaquan.
If xswap is used alongside another swap device, wouldn't runtime
disabling of zswap cause a problem?
User assumes other remained swap device used right afte zswap disabled.
Once a device is allocated as xswap and zswap gets runtime disabled,
xswap keeps receiving new swap entry allocations, and every swapout
to it just bounces back without freeing memory . until xswap's
entries are exhausted. Meanwhile the other swap device sits unused.
If my point is right...
A few ways to handle this come to mind.
1. Once xswap has started accepting writes, refuse to runtime disable zswap.
2. If zswap is runtime disabled, stop handing out new entries from
xswap (drop it from the available list, or gate on an XSWAP flag).
This would also need nr_swap_pages, the visible swap count to be
reduced accordingly, and unused cluster memory reclaimed.
behave as if xswap had been swapoff'd.
3. Support falling back to another swap device once zswap becomes
unavailable.
Thanks!
Youngjun
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 15/16] mm, swap: require zswap for xswap devices
2026-09-03 6:52 ` Youngjun Park
@ 2026-09-04 7:57 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-04 7:57 UTC (permalink / raw)
To: Youngjun Park
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, nphamcs, baohua,
hannes, yosry, shikemeng, chengming.zhou, david, linux-kernel
On 09/03/26 at 03:52pm, Youngjun Park wrote:
> On Thu, Aug 27, 2026 at 05:45:05PM +0800, Baoquan He wrote:
> > xswap has no backing storage: swapped-out pages live only in zswap.
> > Without zswap, swapout always bounces back, so the device would
> > consume swap entry space without ever freeing memory. Refuse to
> > create a device when zswap is unavailable.
> >
> > Runtime disabling of zswap after creation is safe: existing entries
> > stay loadable (zswap_load() gates on zswap_never_enabled(), not the
> > runtime zswap_enabled flag) and new swapouts merely bounce back to
> > memory without freeing it.
>
> Hello Boaquan.
>
> If xswap is used alongside another swap device, wouldn't runtime
> disabling of zswap cause a problem?
> User assumes other remained swap device used right afte zswap disabled.
>
> Once a device is allocated as xswap and zswap gets runtime disabled,
> xswap keeps receiving new swap entry allocations, and every swapout
> to it just bounces back without freeing memory . until xswap's
> entries are exhausted. Meanwhile the other swap device sits unused.
>
> If my point is right...
> A few ways to handle this come to mind.
>
> 1. Once xswap has started accepting writes, refuse to runtime disable zswap.
> 2. If zswap is runtime disabled, stop handing out new entries from
> xswap (drop it from the available list, or gate on an XSWAP flag).
> This would also need nr_swap_pages, the visible swap count to be
> reduced accordingly, and unused cluster memory reclaimed.
> behave as if xswap had been swapoff'd.
> 3. Support falling back to another swap device once zswap becomes
> unavailable.
Agreed. 2) sounds better. Thanks for the detailed analysis.
^ permalink raw reply [flat|nested] 36+ messages in thread
* [PATCH 16/16] mm, swap: allow setting xswap device priority at creation
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (14 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
@ 2026-08-27 9:45 ` Baoquan He
2026-08-27 13:59 ` [syzbot ci] Re: xswap: extendable swap device backed by zswap syzbot ci
` (3 subsequent siblings)
19 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-27 9:45 UTC (permalink / raw)
To: linux-mm
Cc: akpm, chrisl, kasong, nphamcs, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel,
Baoquan He
Let the create interface (/sys/kernel/mm/xswap/create) accept an
optional priority:
echo "<percent> [<prio>]" > /sys/kernel/mm/xswap/create
A missing prio means DEF_SWAP_PRIO; and permitted values are
DEF_SWAP_PRIO or 0..SWAP_FLAG_PRIO_MASK, being consistent with the
swapon(2) ABI. Like a regular swap device, prio is set once at creation
and is not chnageable whne the device is active; use xswap_destroy
then create to cange it.
Signed-off-by: Baoquan He <hebaoquan@kylinos.cn>
---
mm/swapfile.c | 33 +++++++++++++++++++++------------
1 file changed, 21 insertions(+), 12 deletions(-)
diff --git a/mm/swapfile.c b/mm/swapfile.c
index ae0c549cc9aa..258b52bce438 100644
--- a/mm/swapfile.c
+++ b/mm/swapfile.c
@@ -52,6 +52,8 @@
#include "swap.h"
#include <linux/debugfs.h>
+#define DEF_SWAP_PRIO -1
+
static DEFINE_SPINLOCK(swap_lock);
#ifdef CONFIG_XSWAP
@@ -159,9 +161,11 @@ static void xswap_debugfs_del(struct swap_info_struct *si)
}
#ifdef CONFIG_SYSFS
-static int xswap_create(int percent);
+static int xswap_create(int percent, int prio);
static int xswap_destroy(int type);
/* /sys/kernel/mm/xswap/: create.
+ * Write "<percent> [<prio>]" to add a zswap-backed swap device; a missing
+ * priority means DEF_SWAP_PRIO.
* Per-device runtime size is tuned via debugfs type<N>_cluster_limit.
*/
@@ -169,21 +173,25 @@ static ssize_t xswap_create_store(struct kobject *kobj,
struct kobj_attribute *attr,
const char *buf, size_t count)
{
- unsigned long percent;
- int err;
+ long percent;
+ int prio = DEF_SWAP_PRIO;
+ int nr, err;
if (!capable(CAP_SYS_ADMIN))
return -EPERM;
- err = kstrtoul(buf, 0, &percent);
- if (err)
- return err;
+ /* "<percent> [<prio>]"; a missing prio means DEF_SWAP_PRIO.
+ * %li keeps kstrtoul(buf, 0)'s base-0 behavior for percent.
+ */
+ nr = sscanf(buf, "%li %d", &percent, &prio);
+ if (nr < 1)
+ return -EINVAL;
/* 0 means "use the default percent" */
if (percent == 0)
percent = XSWAP_DEFAULT_CLUSTER_PERCENT;
- err = xswap_create(percent);
+ err = xswap_create(percent, prio);
if (err < 0)
return err;
@@ -275,7 +283,6 @@ atomic_t nr_real_swapfiles;
EXPORT_SYMBOL_GPL(nr_swap_pages);
/* protected with swap_lock. reading in vm_swap_full() doesn't need lock */
long total_swap_pages;
-#define DEF_SWAP_PRIO -1
unsigned long swapfile_maximum_size;
#ifdef CONFIG_MIGRATION
bool swap_migration_ad_supported;
@@ -4340,7 +4347,7 @@ static int setup_swap_clusters_info(struct swap_info_struct *si,
/* Create a file-less xswap device. si->max = full RAM; @percent sets the
* runtime nr_clusters ceiling.
*/
-static int xswap_create(int percent)
+static int xswap_create(int percent, int prio)
{
struct swap_info_struct *si;
unsigned long ram, maxpages, init_clusters;
@@ -4348,6 +4355,8 @@ static int xswap_create(int percent)
if (percent < 1 || percent > 100)
return -EINVAL;
+ if (prio != DEF_SWAP_PRIO && (prio < 0 || prio > SWAP_FLAG_PRIO_MASK))
+ return -EINVAL;
/* xswap has no backing store, it relies on zswap. */
if (!zswap_is_enabled())
@@ -4391,15 +4400,15 @@ static int xswap_create(int percent)
goto bad_swap;
mutex_lock(&swapon_mutex);
- si->prio = DEF_SWAP_PRIO;
+ si->prio = prio;
si->list.prio = -si->prio;
si->avail_list.prio = -si->prio;
/* si->swap_file stays NULL: this is a file-less device */
enable_swap_info(si);
mutex_unlock(&swapon_mutex);
- pr_info("xswap: adding extendable swap type %d (%d%% of %lu pages = %u pages, max %lu)\n",
- si->type, percent, ram, si->pages, maxpages);
+ pr_info("xswap: adding extendable swap type %d (prio %d, %d%% of %lu pages = %u pages, max %lu)\n",
+ si->type, prio, percent, ram, si->pages, maxpages);
atomic_inc(&proc_poll_event);
wake_up_interruptible(&proc_poll_wait);
--
2.54.0
^ permalink raw reply related [flat|nested] 36+ messages in thread* [syzbot ci] Re: xswap: extendable swap device backed by zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (15 preceding siblings ...)
2026-08-27 9:45 ` [PATCH 16/16] mm, swap: allow setting xswap device priority at creation Baoquan He
@ 2026-08-27 13:59 ` syzbot ci
2026-08-31 8:35 ` Baoquan He
2026-08-31 17:54 ` [PATCH 00/16] " Kairui Song
` (2 subsequent siblings)
19 siblings, 1 reply; 36+ messages in thread
From: syzbot ci @ 2026-08-27 13:59 UTC (permalink / raw)
To: akpm, baohua, baoquan.he, chengming.zhou, chrisl, david, hannes,
hebaoquan, kasong, linux-kernel, linux-mm, nphamcs, shikemeng,
yosry, youngjun.park
Cc: syzbot, syzkaller-bugs
syzbot ci has tested the following series
[v1] xswap: extendable swap device backed by zswap
https://lore.kernel.org/all/20260827094509.1016740-1-hebaoquan@kylinos.cn
* [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
* [PATCH 02/16] mm: xswap support for zswap
* [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct
* [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct
* [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc
* [PATCH 06/16] mm, swap: add sysfs create interface for xswap
* [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
* [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free
* [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters
* [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection
* [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap
* [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit
* [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
* [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
* [PATCH 15/16] mm, swap: require zswap for xswap devices
* [PATCH 16/16] mm, swap: allow setting xswap device priority at creation
and found the following issue:
possible deadlock in console_flush_all
Full report is available here:
https://ci.syzbot.org/series/6e23a918-7d02-47ba-a61f-bcad82aebf14
***
possible deadlock in console_flush_all
tree: linux-next
URL: https://kernel.googlesource.com/pub/scm/linux/kernel/git/next/linux-next
base: 169393fff5d1ec2690934067eeb95544ff5ebdd7
arch: amd64
compiler: Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
config: https://ci.syzbot.org/builds/633d2731-0a5e-45d2-96bf-bd29305475b9/config
syz repro: https://ci.syzbot.org/findings/b9106c23-79f3-457d-bb14-c0cf63e4bed2/syz_repro
------------[ cut here ]------------
======================================================
WARNING: possible circular locking dependency detected
syzkaller #0 Not tainted
------------------------------------------------------
syz.1.20/5854 is trying to acquire lock:
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: rcu_try_lock_acquire include/linux/rcupdate.h:305 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: srcu_read_lock_nmisafe include/linux/srcu.h:428 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_srcu_read_lock kernel/printk/printk.c:291 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_flush_one_record kernel/printk/printk.c:3246 [inline]
ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_flush_all+0x123/0xaf0 kernel/printk/printk.c:3343
but task is already holding lock:
ffff88823c63a898 (&pool->lock){-.-.}-{2:2}, at: __queue_work+0x7fe/0x10a0 kernel/workqueue.c:2358
which lock already depends on the new lock.
the existing dependency chain (in reverse order) is:
-> #3 (&pool->lock){-.-.}-{2:2}:
__raw_spin_lock include/linux/spinlock_api_smp.h:158 [inline]
_raw_spin_lock+0x2e/0x40 kernel/locking/spinlock.c:158
__queue_work+0x72f/0x10a0 kernel/workqueue.c:2361
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
queue_work include/linux/workqueue.h:699 [inline]
rpm_suspend+0xeca/0x17b0 drivers/base/power/runtime.c:688
__pm_runtime_idle+0x12f/0x1a0 drivers/base/power/runtime.c:1129
pm_runtime_put include/linux/pm_runtime.h:551 [inline]
__device_attach+0x355/0x450 drivers/base/dd.c:1116
device_initial_probe+0xa1/0xd0 drivers/base/dd.c:1153
bus_probe_device+0x12a/0x220 drivers/base/bus.c:620
device_add+0x7d7/0xb80 drivers/base/core.c:3772
serial_base_port_add+0x18f/0x270 drivers/tty/serial/serial_base_bus.c:186
serial_core_port_device_add drivers/tty/serial/serial_core.c:3275 [inline]
serial_core_register_port+0x37f/0x2840 drivers/tty/serial/serial_core.c:3314
serial8250_register_8250_port+0x16b4/0x2090 drivers/tty/serial/8250/8250_core.c:828
serial_pnp_probe+0x56a/0x7f0 drivers/tty/serial/8250/8250_pnp.c:480
pnp_device_probe+0x30b/0x4c0 drivers/pnp/driver.c:111
call_driver_probe drivers/base/dd.c:-1 [inline]
really_probe+0x254/0xae0 drivers/base/dd.c:706
__driver_probe_device+0x1e8/0x360 drivers/base/dd.c:868
driver_probe_device+0x4f/0x240 drivers/base/dd.c:898
__driver_attach+0x339/0x600 drivers/base/dd.c:1292
bus_for_each_dev+0x23b/0x2c0 drivers/base/bus.c:383
bus_add_driver+0x345/0x670 drivers/base/bus.c:763
driver_register+0x23a/0x320 drivers/base/driver.c:174
serial8250_init+0x8f/0x160 drivers/tty/serial/8250/8250_platform.c:317
do_one_initcall+0x250/0x870 init/main.c:1347
do_initcall_level+0x10a/0x1a0 init/main.c:1409
do_initcalls+0x59/0xa0 init/main.c:1425
kernel_init_freeable+0x29d/0x3e0 init/main.c:1658
kernel_init+0x1d/0x1d0 init/main.c:1548
ret_from_fork+0x514/0xb70 arch/x86/kernel/process.c:158
ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
-> #2 (&dev->power.lock){-...}-{3:3}:
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:166
__pm_runtime_resume+0x10f/0x180 drivers/base/power/runtime.c:1196
pm_runtime_get include/linux/pm_runtime.h:494 [inline]
__uart_start+0x171/0x460 drivers/tty/serial/serial_core.c:149
uart_write+0x251/0x9f0 drivers/tty/serial/serial_core.c:629
process_output_block drivers/tty/n_tty.c:557 [inline]
n_tty_write+0xd49/0x11e0 drivers/tty/n_tty.c:2366
iterate_tty_write drivers/tty/tty_io.c:1006 [inline]
file_tty_write+0x509/0x9c0 drivers/tty/tty_io.c:1081
new_sync_write fs/read_write.c:595 [inline]
vfs_write+0x612/0xba0 fs/read_write.c:687
ksys_write+0x150/0x270 fs/read_write.c:739
do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
do_syscall_64+0x174/0x580 arch/x86/entry/syscall_64.c:94
entry_SYSCALL_64_after_hwframe+0x77/0x7f
-> #1 (&port_lock_key){-.-.}-{3:3}:
__raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
_raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:166
uart_port_lock_irqsave include/linux/serial_core.h:719 [inline]
serial8250_console_write+0x153/0x1ba0 drivers/tty/serial/8250/8250_port.c:3344
console_emit_next_record kernel/printk/printk.c:3183 [inline]
console_flush_one_record kernel/printk/printk.c:3269 [inline]
console_flush_all+0x6ea/0xaf0 kernel/printk/printk.c:3343
__console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
_printk+0xdd/0x130 kernel/printk/printk.c:2504
register_console+0xbc3/0xfc0 kernel/printk/printk.c:4208
univ8250_console_init+0x3a/0x70 drivers/tty/serial/8250/8250_core.c:515
console_init+0x10b/0x4a0 kernel/printk/printk.c:4407
start_kernel+0x238/0x3e0 init/main.c:1112
x86_64_start_reservations+0x24/0x30 arch/x86/kernel/head64.c:310
x86_64_start_kernel+0x137/0x1b0 arch/x86/kernel/head64.c:291
common_startup_64+0x13e/0x157
-> #0 (console_owner){..-.}-{0:0}:
check_prev_add kernel/locking/lockdep.c:3165 [inline]
check_prevs_add kernel/locking/lockdep.c:3284 [inline]
validate_chain kernel/locking/lockdep.c:3908 [inline]
__lock_acquire+0x15ff/0x2e50 kernel/locking/lockdep.c:5238
lock_acquire+0x115/0x350 kernel/locking/lockdep.c:5890
console_lock_spinning_enable kernel/printk/printk.c:1902 [inline]
console_emit_next_record kernel/printk/printk.c:3177 [inline]
console_flush_one_record kernel/printk/printk.c:3269 [inline]
console_flush_all+0x693/0xaf0 kernel/printk/printk.c:3343
__console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
_printk+0xdd/0x130 kernel/printk/printk.c:2504
__report_bug+0x349/0x570 lib/bug.c:248
report_bug+0x16a/0x220 lib/bug.c:286
handle_bug+0x9c/0x200 arch/x86/kernel/traps.c:436
exc_invalid_op+0x1a/0x50 arch/x86/kernel/traps.c:490
asm_exc_invalid_op+0x1a/0x20 arch/x86/include/asm/idtentry.h:593
__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
zap_nonpresent_ptes mm/memory.c:1900 [inline]
do_zap_pte_range mm/memory.c:1967 [inline]
zap_pte_range mm/memory.c:2065 [inline]
zap_pmd_range mm/memory.c:2151 [inline]
zap_pud_range mm/memory.c:2179 [inline]
zap_p4d_range mm/memory.c:2200 [inline]
__zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
unmap_vmas+0x390/0x550 mm/memory.c:2309
exit_mmap+0x293/0x9f0 mm/mmap.c:1315
__mmput+0x118/0x420 kernel/fork.c:1187
exit_mm+0x221/0x2d0 kernel/exit.c:615
do_exit+0x6cd/0x2360 kernel/exit.c:997
do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
get_signal+0x121b/0x12c0 kernel/signal.c:3046
arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
__exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
other info that might help us debug this:
Chain exists of:
console_owner --> &dev->power.lock --> &pool->lock
Possible unsafe locking scenario:
CPU0 CPU1
---- ----
lock(&pool->lock);
lock(&dev->power.lock);
lock(&pool->lock);
lock(console_owner);
*** DEADLOCK ***
8 locks held by syz.1.20/5854:
#0: ffff8881bb3703b8 (&mm->mmap_lock){++++}-{4:4}, at: mmap_read_lock include/linux/mmap_lock.h:600 [inline]
#0: ffff8881bb3703b8 (&mm->mmap_lock){++++}-{4:4}, at: exit_mmap+0x1a4/0x9f0 mm/mmap.c:1299
#1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:300 [inline]
#1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:840 [inline]
#1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: __pte_offset_map+0x29/0x240 mm/pgtable-generic.c:290
#2: ffff88816d267f18 (ptlock_ptr(ptdesc)#2){+.+.}-{3:3}, at: spin_lock include/linux/spinlock.h:342 [inline]
#2: ffff88816d267f18 (ptlock_ptr(ptdesc)#2){+.+.}-{3:3}, at: pte_offset_map_lock+0x13d/0x210 mm/pgtable-generic.c:404
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: spin_lock include/linux/spinlock.h:342 [inline]
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: __swap_cluster_lock mm/swap.h:155 [inline]
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: swap_cluster_lock mm/swap.h:170 [inline]
#3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: swap_put_entries_cluster+0x109/0x4b0 mm/swapfile.c:1864
#4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:300 [inline]
#4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:840 [inline]
#4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: __queue_work+0x1ea/0x10a0 kernel/workqueue.c:2321
#5: ffff88823c63a898 (&pool->lock){-.-.}-{2:2}, at: __queue_work+0x7fe/0x10a0 kernel/workqueue.c:2358
#6: ffffffff8eb4bfa0 (console_lock){+.+.}-{0:0}, at: _printk+0xdd/0x130 kernel/printk/printk.c:2504
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: rcu_try_lock_acquire include/linux/rcupdate.h:305 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: srcu_read_lock_nmisafe include/linux/srcu.h:428 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_srcu_read_lock kernel/printk/printk.c:291 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_flush_one_record kernel/printk/printk.c:3246 [inline]
#7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_flush_all+0x123/0xaf0 kernel/printk/printk.c:3343
stack backtrace:
CPU: 1 UID: 0 PID: 5854 Comm: syz.1.20 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
Call Trace:
<TASK>
dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
print_circular_bug+0x2e2/0x300 kernel/locking/lockdep.c:2043
check_noncircular+0x12f/0x150 kernel/locking/lockdep.c:2175
check_prev_add kernel/locking/lockdep.c:3165 [inline]
check_prevs_add kernel/locking/lockdep.c:3284 [inline]
validate_chain kernel/locking/lockdep.c:3908 [inline]
__lock_acquire+0x15ff/0x2e50 kernel/locking/lockdep.c:5238
lock_acquire+0x115/0x350 kernel/locking/lockdep.c:5890
console_lock_spinning_enable kernel/printk/printk.c:1902 [inline]
console_emit_next_record kernel/printk/printk.c:3177 [inline]
console_flush_one_record kernel/printk/printk.c:3269 [inline]
console_flush_all+0x693/0xaf0 kernel/printk/printk.c:3343
__console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
_printk+0xdd/0x130 kernel/printk/printk.c:2504
__report_bug+0x349/0x570 lib/bug.c:248
report_bug+0x16a/0x220 lib/bug.c:286
handle_bug+0x9c/0x200 arch/x86/kernel/traps.c:436
exc_invalid_op+0x1a/0x50 arch/x86/kernel/traps.c:490
asm_exc_invalid_op+0x1a/0x20 arch/x86/include/asm/idtentry.h:593
RIP: 0010:__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
Code: 3d 7e ec b2 0e 01 0f 85 35 02 00 00 e8 a1 28 39 00 e9 82 f3 ff ff e8 97 28 39 00 90 0f 0b 90 e9 21 fd ff ff e8 89 28 39 00 90 <0f> 0b 90 e9 80 fe ff ff e8 7b 28 39 00 eb 11 e8 74 28 39 00 4c 8b
RSP: 0018:ffffc90003cdf1d8 EFLAGS: 00010093
RAX: ffffffff818dc2d7 RBX: dffffc0000000000 RCX: ffff888113955a00
RDX: 0000000000000000 RSI: ffffffff8c4bca60 RDI: ffffffff8c4bca20
RBP: 0000000000000001 R08: ffffffff90572d37 R09: 1ffffffff20ae5a6
R10: dffffc0000000000 R11: fffffbfff20ae5a7 R12: ffff888160408400
R13: ffff88810fdd7088 R14: 0000000000000001 R15: ffff88810fdd7090
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
zap_nonpresent_ptes mm/memory.c:1900 [inline]
do_zap_pte_range mm/memory.c:1967 [inline]
zap_pte_range mm/memory.c:2065 [inline]
zap_pmd_range mm/memory.c:2151 [inline]
zap_pud_range mm/memory.c:2179 [inline]
zap_p4d_range mm/memory.c:2200 [inline]
__zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
unmap_vmas+0x390/0x550 mm/memory.c:2309
exit_mmap+0x293/0x9f0 mm/mmap.c:1315
__mmput+0x118/0x420 kernel/fork.c:1187
exit_mm+0x221/0x2d0 kernel/exit.c:615
do_exit+0x6cd/0x2360 kernel/exit.c:997
do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
get_signal+0x121b/0x12c0 kernel/signal.c:3046
arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
__exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f46ce79e0d9
Code: Unable to access opcode bytes at 0x7f46ce79e0af.
RSP: 002b:00007f46cf6fc028 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
RAX: 0000000000000000 RBX: 00007f46cea26090 RCX: 00007f46ce79e0d9
RDX: 0000000000000015 RSI: 0000000000600003 RDI: 0000200000000000
RBP: 00007f46ce835024 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f46cea26128 R14: 00007f46cea26090 R15: 00007ffefd86c578
</TASK>
!list_empty(&work->entry)
WARNING: kernel/workqueue.c:2385 at __queue_work+0xd18/0x10a0 kernel/workqueue.c:2385, CPU#1: syz.1.20/5854
Modules linked in:
CPU: 1 UID: 0 PID: 5854 Comm: syz.1.20 Not tainted syzkaller #0 PREEMPT(full)
Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
RIP: 0010:__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
Code: 3d 7e ec b2 0e 01 0f 85 35 02 00 00 e8 a1 28 39 00 e9 82 f3 ff ff e8 97 28 39 00 90 0f 0b 90 e9 21 fd ff ff e8 89 28 39 00 90 <0f> 0b 90 e9 80 fe ff ff e8 7b 28 39 00 eb 11 e8 74 28 39 00 4c 8b
RSP: 0018:ffffc90003cdf1d8 EFLAGS: 00010093
RAX: ffffffff818dc2d7 RBX: dffffc0000000000 RCX: ffff888113955a00
RDX: 0000000000000000 RSI: ffffffff8c4bca60 RDI: ffffffff8c4bca20
RBP: 0000000000000001 R08: ffffffff90572d37 R09: 1ffffffff20ae5a6
R10: dffffc0000000000 R11: fffffbfff20ae5a7 R12: ffff888160408400
R13: ffff88810fdd7088 R14: 0000000000000001 R15: ffff88810fdd7090
FS: 00007f46cf6fc6c0(0000) GS:ffff8882a8f5a000(0000) knlGS:0000000000000000
CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
CR2: 0000200000e6c000 CR3: 000000010a5a2000 CR4: 00000000000006f0
Call Trace:
<TASK>
queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
zap_nonpresent_ptes mm/memory.c:1900 [inline]
do_zap_pte_range mm/memory.c:1967 [inline]
zap_pte_range mm/memory.c:2065 [inline]
zap_pmd_range mm/memory.c:2151 [inline]
zap_pud_range mm/memory.c:2179 [inline]
zap_p4d_range mm/memory.c:2200 [inline]
__zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
unmap_vmas+0x390/0x550 mm/memory.c:2309
exit_mmap+0x293/0x9f0 mm/mmap.c:1315
__mmput+0x118/0x420 kernel/fork.c:1187
exit_mm+0x221/0x2d0 kernel/exit.c:615
do_exit+0x6cd/0x2360 kernel/exit.c:997
do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
get_signal+0x121b/0x12c0 kernel/signal.c:3046
arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
__exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
__exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
entry_SYSCALL_64_after_hwframe+0x77/0x7f
RIP: 0033:0x7f46ce79e0d9
Code: Unable to access opcode bytes at 0x7f46ce79e0af.
RSP: 002b:00007f46cf6fc028 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
RAX: 0000000000000000 RBX: 00007f46cea26090 RCX: 00007f46ce79e0d9
RDX: 0000000000000015 RSI: 0000000000600003 RDI: 0000200000000000
RBP: 00007f46ce835024 R08: 0000000000000000 R09: 0000000000000000
R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
R13: 00007f46cea26128 R14: 00007f46cea26090 R15: 00007ffefd86c578
</TASK>
***
If these findings have caused you to resend the series or submit a
separate fix, please add the following tag to your commit message:
Tested-by: syzbot@syzkaller.appspotmail.com
---
This report is generated by a bot. It may contain errors.
syzbot ci engineers can be reached at syzkaller@googlegroups.com.
To test a fix for this bug, please reply with `#syz test`
(on a separate line) and attach the patch to the email.
Notes:
- The patch will be applied on top of the tested series (as an
incremental fix).
- To test a new version of the whole series, please send it directly
to syzbot@lists.linux.dev.
- Arguments like custom git repos and branches are not supported.
^ permalink raw reply [flat|nested] 36+ messages in thread* Re: [syzbot ci] Re: xswap: extendable swap device backed by zswap
2026-08-27 13:59 ` [syzbot ci] Re: xswap: extendable swap device backed by zswap syzbot ci
@ 2026-08-31 8:35 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-08-31 8:35 UTC (permalink / raw)
To: syzbot ci
Cc: akpm, baohua, chengming.zhou, chrisl, david, hannes, hebaoquan,
kasong, linux-kernel, linux-mm, nphamcs, shikemeng, yosry,
youngjun.park, syzbot, syzkaller-bugs
On 08/27/26 at 06:59am, syzbot ci wrote:
> syzbot ci has tested the following series
>
> [v1] xswap: extendable swap device backed by zswap
> https://lore.kernel.org/all/20260827094509.1016740-1-hebaoquan@kylinos.cn
> * [PATCH 01/16] mm: zswap: return -ENOENT when the swap device is gone
> * [PATCH 02/16] mm: xswap support for zswap
> * [PATCH 03/16] mm, swap: add CONFIG_XSWAP and xswap fields to swap_info_struct
> * [PATCH 04/16] mm, swap: refactor free_swap_cluster_info to take swap_info_struct
> * [PATCH 05/16] mm, swap: add xswap cluster grow via VM_SPARSE vmalloc
> * [PATCH 06/16] mm, swap: add sysfs create interface for xswap
> * [PATCH 07/16] mm, swap: add xswap grow trigger on cluster allocation
> * [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free
> * [PATCH 09/16] mm, swap: free backing pages in xswap_unmap_clusters
> * [PATCH 10/16] mm, swap: add nr_free_tail for O(1) xswap shrink detection
> * [PATCH 11/16] mm, swap: add adjustable runtime ceiling (nr_clusters) for xswap
> * [PATCH 12/16] mm, swap: add debugfs knob for xswap per-device cluster limit
> * [PATCH 13/16] mm, swap: defer xswap shrink to workqueue to avoid lock recursion
> * [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy
> * [PATCH 15/16] mm, swap: require zswap for xswap devices
> * [PATCH 16/16] mm, swap: allow setting xswap device priority at creation
>
> and found the following issue:
> possible deadlock in console_flush_all
>
> Full report is available here:
> https://ci.syzbot.org/series/6e23a918-7d02-47ba-a61f-bcad82aebf14
>
> ***
>
> possible deadlock in console_flush_all
>
> tree: linux-next
> URL: https://kernel.googlesource.com/pub/scm/linux/kernel/git/next/linux-next
> base: 169393fff5d1ec2690934067eeb95544ff5ebdd7
> arch: amd64
> compiler: Debian clang version 22.1.8 (++20260613092233+e80beda6e255-1~exp1~20260613092250.77), Debian LLD 22.1.8
> config: https://ci.syzbot.org/builds/633d2731-0a5e-45d2-96bf-bd29305475b9/config
> syz repro: https://ci.syzbot.org/findings/b9106c23-79f3-457d-bb14-c0cf63e4bed2/syz_repro
>
> ------------[ cut here ]------------
> ======================================================
> WARNING: possible circular locking dependency detected
> syzkaller #0 Not tainted
> ------------------------------------------------------
> syz.1.20/5854 is trying to acquire lock:
> ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: rcu_try_lock_acquire include/linux/rcupdate.h:305 [inline]
> ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: srcu_read_lock_nmisafe include/linux/srcu.h:428 [inline]
> ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_srcu_read_lock kernel/printk/printk.c:291 [inline]
> ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_flush_one_record kernel/printk/printk.c:3246 [inline]
> ffffffff8eb4bf40 (console_owner){..-.}-{0:0}, at: console_flush_all+0x123/0xaf0 kernel/printk/printk.c:3343
>
> but task is already holding lock:
> ffff88823c63a898 (&pool->lock){-.-.}-{2:2}, at: __queue_work+0x7fe/0x10a0 kernel/workqueue.c:2358
>
> which lock already depends on the new lock.
>
>
> the existing dependency chain (in reverse order) is:
Thanks for testing the series.
Sashiko also reported this issue. The root cuase is the
!list_empty(&work->entry) WARN at __queue_work is triggered by
schedule_work(&si->xswap_shrink_work) being called for a non-xswap
swap device in __free_cluster(), where the work_struct is never
initialized (zeroed by kvzalloc). The "possible deadlock in
console_flush_all" report is a secondary lockdep artifact: the WARN
handler prints while holding the workqueue pool lock.
The fix is already included in v2. Now __free_cluster() schedules the
shrink work only for xswap devices while the device is active, guarded
by si->flags & SWP_XSWAP and SWP_WRITEOK, so the uninitialized work is
never queued for normal swap devices.
Thanks
Baoquan
>
> -> #3 (&pool->lock){-.-.}-{2:2}:
> __raw_spin_lock include/linux/spinlock_api_smp.h:158 [inline]
> _raw_spin_lock+0x2e/0x40 kernel/locking/spinlock.c:158
> __queue_work+0x72f/0x10a0 kernel/workqueue.c:2361
> queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
> queue_work include/linux/workqueue.h:699 [inline]
> rpm_suspend+0xeca/0x17b0 drivers/base/power/runtime.c:688
> __pm_runtime_idle+0x12f/0x1a0 drivers/base/power/runtime.c:1129
> pm_runtime_put include/linux/pm_runtime.h:551 [inline]
> __device_attach+0x355/0x450 drivers/base/dd.c:1116
> device_initial_probe+0xa1/0xd0 drivers/base/dd.c:1153
> bus_probe_device+0x12a/0x220 drivers/base/bus.c:620
> device_add+0x7d7/0xb80 drivers/base/core.c:3772
> serial_base_port_add+0x18f/0x270 drivers/tty/serial/serial_base_bus.c:186
> serial_core_port_device_add drivers/tty/serial/serial_core.c:3275 [inline]
> serial_core_register_port+0x37f/0x2840 drivers/tty/serial/serial_core.c:3314
> serial8250_register_8250_port+0x16b4/0x2090 drivers/tty/serial/8250/8250_core.c:828
> serial_pnp_probe+0x56a/0x7f0 drivers/tty/serial/8250/8250_pnp.c:480
> pnp_device_probe+0x30b/0x4c0 drivers/pnp/driver.c:111
> call_driver_probe drivers/base/dd.c:-1 [inline]
> really_probe+0x254/0xae0 drivers/base/dd.c:706
> __driver_probe_device+0x1e8/0x360 drivers/base/dd.c:868
> driver_probe_device+0x4f/0x240 drivers/base/dd.c:898
> __driver_attach+0x339/0x600 drivers/base/dd.c:1292
> bus_for_each_dev+0x23b/0x2c0 drivers/base/bus.c:383
> bus_add_driver+0x345/0x670 drivers/base/bus.c:763
> driver_register+0x23a/0x320 drivers/base/driver.c:174
> serial8250_init+0x8f/0x160 drivers/tty/serial/8250/8250_platform.c:317
> do_one_initcall+0x250/0x870 init/main.c:1347
> do_initcall_level+0x10a/0x1a0 init/main.c:1409
> do_initcalls+0x59/0xa0 init/main.c:1425
> kernel_init_freeable+0x29d/0x3e0 init/main.c:1658
> kernel_init+0x1d/0x1d0 init/main.c:1548
> ret_from_fork+0x514/0xb70 arch/x86/kernel/process.c:158
> ret_from_fork_asm+0x1a/0x30 arch/x86/entry/entry_64.S:245
>
> -> #2 (&dev->power.lock){-...}-{3:3}:
> __raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
> _raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:166
> __pm_runtime_resume+0x10f/0x180 drivers/base/power/runtime.c:1196
> pm_runtime_get include/linux/pm_runtime.h:494 [inline]
> __uart_start+0x171/0x460 drivers/tty/serial/serial_core.c:149
> uart_write+0x251/0x9f0 drivers/tty/serial/serial_core.c:629
> process_output_block drivers/tty/n_tty.c:557 [inline]
> n_tty_write+0xd49/0x11e0 drivers/tty/n_tty.c:2366
> iterate_tty_write drivers/tty/tty_io.c:1006 [inline]
> file_tty_write+0x509/0x9c0 drivers/tty/tty_io.c:1081
> new_sync_write fs/read_write.c:595 [inline]
> vfs_write+0x612/0xba0 fs/read_write.c:687
> ksys_write+0x150/0x270 fs/read_write.c:739
> do_syscall_x64 arch/x86/entry/syscall_64.c:63 [inline]
> do_syscall_64+0x174/0x580 arch/x86/entry/syscall_64.c:94
> entry_SYSCALL_64_after_hwframe+0x77/0x7f
>
> -> #1 (&port_lock_key){-.-.}-{3:3}:
> __raw_spin_lock_irqsave include/linux/spinlock_api_smp.h:132 [inline]
> _raw_spin_lock_irqsave+0x40/0x60 kernel/locking/spinlock.c:166
> uart_port_lock_irqsave include/linux/serial_core.h:719 [inline]
> serial8250_console_write+0x153/0x1ba0 drivers/tty/serial/8250/8250_port.c:3344
> console_emit_next_record kernel/printk/printk.c:3183 [inline]
> console_flush_one_record kernel/printk/printk.c:3269 [inline]
> console_flush_all+0x6ea/0xaf0 kernel/printk/printk.c:3343
> __console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
> console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
> vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
> _printk+0xdd/0x130 kernel/printk/printk.c:2504
> register_console+0xbc3/0xfc0 kernel/printk/printk.c:4208
> univ8250_console_init+0x3a/0x70 drivers/tty/serial/8250/8250_core.c:515
> console_init+0x10b/0x4a0 kernel/printk/printk.c:4407
> start_kernel+0x238/0x3e0 init/main.c:1112
> x86_64_start_reservations+0x24/0x30 arch/x86/kernel/head64.c:310
> x86_64_start_kernel+0x137/0x1b0 arch/x86/kernel/head64.c:291
> common_startup_64+0x13e/0x157
>
> -> #0 (console_owner){..-.}-{0:0}:
> check_prev_add kernel/locking/lockdep.c:3165 [inline]
> check_prevs_add kernel/locking/lockdep.c:3284 [inline]
> validate_chain kernel/locking/lockdep.c:3908 [inline]
> __lock_acquire+0x15ff/0x2e50 kernel/locking/lockdep.c:5238
> lock_acquire+0x115/0x350 kernel/locking/lockdep.c:5890
> console_lock_spinning_enable kernel/printk/printk.c:1902 [inline]
> console_emit_next_record kernel/printk/printk.c:3177 [inline]
> console_flush_one_record kernel/printk/printk.c:3269 [inline]
> console_flush_all+0x693/0xaf0 kernel/printk/printk.c:3343
> __console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
> console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
> vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
> _printk+0xdd/0x130 kernel/printk/printk.c:2504
> __report_bug+0x349/0x570 lib/bug.c:248
> report_bug+0x16a/0x220 lib/bug.c:286
> handle_bug+0x9c/0x200 arch/x86/kernel/traps.c:436
> exc_invalid_op+0x1a/0x50 arch/x86/kernel/traps.c:490
> asm_exc_invalid_op+0x1a/0x20 arch/x86/include/asm/idtentry.h:593
> __queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
> queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
> swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
> swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
> zap_nonpresent_ptes mm/memory.c:1900 [inline]
> do_zap_pte_range mm/memory.c:1967 [inline]
> zap_pte_range mm/memory.c:2065 [inline]
> zap_pmd_range mm/memory.c:2151 [inline]
> zap_pud_range mm/memory.c:2179 [inline]
> zap_p4d_range mm/memory.c:2200 [inline]
> __zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
> unmap_vmas+0x390/0x550 mm/memory.c:2309
> exit_mmap+0x293/0x9f0 mm/mmap.c:1315
> __mmput+0x118/0x420 kernel/fork.c:1187
> exit_mm+0x221/0x2d0 kernel/exit.c:615
> do_exit+0x6cd/0x2360 kernel/exit.c:997
> do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
> get_signal+0x121b/0x12c0 kernel/signal.c:3046
> arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
> __exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
> exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
> __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
> entry_SYSCALL_64_after_hwframe+0x77/0x7f
>
> other info that might help us debug this:
>
> Chain exists of:
> console_owner --> &dev->power.lock --> &pool->lock
>
> Possible unsafe locking scenario:
>
> CPU0 CPU1
> ---- ----
> lock(&pool->lock);
> lock(&dev->power.lock);
> lock(&pool->lock);
> lock(console_owner);
>
> *** DEADLOCK ***
>
> 8 locks held by syz.1.20/5854:
> #0: ffff8881bb3703b8 (&mm->mmap_lock){++++}-{4:4}, at: mmap_read_lock include/linux/mmap_lock.h:600 [inline]
> #0: ffff8881bb3703b8 (&mm->mmap_lock){++++}-{4:4}, at: exit_mmap+0x1a4/0x9f0 mm/mmap.c:1299
> #1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:300 [inline]
> #1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:840 [inline]
> #1: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: __pte_offset_map+0x29/0x240 mm/pgtable-generic.c:290
> #2: ffff88816d267f18 (ptlock_ptr(ptdesc)#2){+.+.}-{3:3}, at: spin_lock include/linux/spinlock.h:342 [inline]
> #2: ffff88816d267f18 (ptlock_ptr(ptdesc)#2){+.+.}-{3:3}, at: pte_offset_map_lock+0x13d/0x210 mm/pgtable-generic.c:404
> #3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: spin_lock include/linux/spinlock.h:342 [inline]
> #3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: __swap_cluster_lock mm/swap.h:155 [inline]
> #3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: swap_cluster_lock mm/swap.h:170 [inline]
> #3: ffff8881102a40f8 (&cluster_info[i].lock){+.+.}-{3:3}, at: swap_put_entries_cluster+0x109/0x4b0 mm/swapfile.c:1864
> #4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_lock_acquire include/linux/rcupdate.h:300 [inline]
> #4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: rcu_read_lock include/linux/rcupdate.h:840 [inline]
> #4: ffffffff8eb59c60 (rcu_read_lock){....}-{1:3}, at: __queue_work+0x1ea/0x10a0 kernel/workqueue.c:2321
> #5: ffff88823c63a898 (&pool->lock){-.-.}-{2:2}, at: __queue_work+0x7fe/0x10a0 kernel/workqueue.c:2358
> #6: ffffffff8eb4bfa0 (console_lock){+.+.}-{0:0}, at: _printk+0xdd/0x130 kernel/printk/printk.c:2504
> #7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: rcu_try_lock_acquire include/linux/rcupdate.h:305 [inline]
> #7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: srcu_read_lock_nmisafe include/linux/srcu.h:428 [inline]
> #7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_srcu_read_lock kernel/printk/printk.c:291 [inline]
> #7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_flush_one_record kernel/printk/printk.c:3246 [inline]
> #7: ffffffff8ea33838 (console_srcu){....}-{0:0}, at: console_flush_all+0x123/0xaf0 kernel/printk/printk.c:3343
>
> stack backtrace:
> CPU: 1 UID: 0 PID: 5854 Comm: syz.1.20 Not tainted syzkaller #0 PREEMPT(full)
> Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
> Call Trace:
> <TASK>
> dump_stack_lvl+0xe8/0x150 lib/dump_stack.c:120
> print_circular_bug+0x2e2/0x300 kernel/locking/lockdep.c:2043
> check_noncircular+0x12f/0x150 kernel/locking/lockdep.c:2175
> check_prev_add kernel/locking/lockdep.c:3165 [inline]
> check_prevs_add kernel/locking/lockdep.c:3284 [inline]
> validate_chain kernel/locking/lockdep.c:3908 [inline]
> __lock_acquire+0x15ff/0x2e50 kernel/locking/lockdep.c:5238
> lock_acquire+0x115/0x350 kernel/locking/lockdep.c:5890
> console_lock_spinning_enable kernel/printk/printk.c:1902 [inline]
> console_emit_next_record kernel/printk/printk.c:3177 [inline]
> console_flush_one_record kernel/printk/printk.c:3269 [inline]
> console_flush_all+0x693/0xaf0 kernel/printk/printk.c:3343
> __console_flush_and_unlock kernel/printk/printk.c:3373 [inline]
> console_unlock+0xd1/0x1c0 kernel/printk/printk.c:3413
> vprintk_emit+0x485/0x560 kernel/printk/printk.c:2479
> _printk+0xdd/0x130 kernel/printk/printk.c:2504
> __report_bug+0x349/0x570 lib/bug.c:248
> report_bug+0x16a/0x220 lib/bug.c:286
> handle_bug+0x9c/0x200 arch/x86/kernel/traps.c:436
> exc_invalid_op+0x1a/0x50 arch/x86/kernel/traps.c:490
> asm_exc_invalid_op+0x1a/0x20 arch/x86/include/asm/idtentry.h:593
> RIP: 0010:__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
> Code: 3d 7e ec b2 0e 01 0f 85 35 02 00 00 e8 a1 28 39 00 e9 82 f3 ff ff e8 97 28 39 00 90 0f 0b 90 e9 21 fd ff ff e8 89 28 39 00 90 <0f> 0b 90 e9 80 fe ff ff e8 7b 28 39 00 eb 11 e8 74 28 39 00 4c 8b
> RSP: 0018:ffffc90003cdf1d8 EFLAGS: 00010093
> RAX: ffffffff818dc2d7 RBX: dffffc0000000000 RCX: ffff888113955a00
> RDX: 0000000000000000 RSI: ffffffff8c4bca60 RDI: ffffffff8c4bca20
> RBP: 0000000000000001 R08: ffffffff90572d37 R09: 1ffffffff20ae5a6
> R10: dffffc0000000000 R11: fffffbfff20ae5a7 R12: ffff888160408400
> R13: ffff88810fdd7088 R14: 0000000000000001 R15: ffff88810fdd7090
> queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
> swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
> swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
> zap_nonpresent_ptes mm/memory.c:1900 [inline]
> do_zap_pte_range mm/memory.c:1967 [inline]
> zap_pte_range mm/memory.c:2065 [inline]
> zap_pmd_range mm/memory.c:2151 [inline]
> zap_pud_range mm/memory.c:2179 [inline]
> zap_p4d_range mm/memory.c:2200 [inline]
> __zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
> unmap_vmas+0x390/0x550 mm/memory.c:2309
> exit_mmap+0x293/0x9f0 mm/mmap.c:1315
> __mmput+0x118/0x420 kernel/fork.c:1187
> exit_mm+0x221/0x2d0 kernel/exit.c:615
> do_exit+0x6cd/0x2360 kernel/exit.c:997
> do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
> get_signal+0x121b/0x12c0 kernel/signal.c:3046
> arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
> __exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
> exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
> __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
> entry_SYSCALL_64_after_hwframe+0x77/0x7f
> RIP: 0033:0x7f46ce79e0d9
> Code: Unable to access opcode bytes at 0x7f46ce79e0af.
> RSP: 002b:00007f46cf6fc028 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
> RAX: 0000000000000000 RBX: 00007f46cea26090 RCX: 00007f46ce79e0d9
> RDX: 0000000000000015 RSI: 0000000000600003 RDI: 0000200000000000
> RBP: 00007f46ce835024 R08: 0000000000000000 R09: 0000000000000000
> R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
> R13: 00007f46cea26128 R14: 00007f46cea26090 R15: 00007ffefd86c578
> </TASK>
> !list_empty(&work->entry)
> WARNING: kernel/workqueue.c:2385 at __queue_work+0xd18/0x10a0 kernel/workqueue.c:2385, CPU#1: syz.1.20/5854
> Modules linked in:
> CPU: 1 UID: 0 PID: 5854 Comm: syz.1.20 Not tainted syzkaller #0 PREEMPT(full)
> Hardware name: QEMU Standard PC (Q35 + ICH9, 2009), BIOS 1.16.2-debian-1.16.2-1 04/01/2014
> RIP: 0010:__queue_work+0xd18/0x10a0 kernel/workqueue.c:2385
> Code: 3d 7e ec b2 0e 01 0f 85 35 02 00 00 e8 a1 28 39 00 e9 82 f3 ff ff e8 97 28 39 00 90 0f 0b 90 e9 21 fd ff ff e8 89 28 39 00 90 <0f> 0b 90 e9 80 fe ff ff e8 7b 28 39 00 eb 11 e8 74 28 39 00 4c 8b
> RSP: 0018:ffffc90003cdf1d8 EFLAGS: 00010093
> RAX: ffffffff818dc2d7 RBX: dffffc0000000000 RCX: ffff888113955a00
> RDX: 0000000000000000 RSI: ffffffff8c4bca60 RDI: ffffffff8c4bca20
> RBP: 0000000000000001 R08: ffffffff90572d37 R09: 1ffffffff20ae5a6
> R10: dffffc0000000000 R11: fffffbfff20ae5a7 R12: ffff888160408400
> R13: ffff88810fdd7088 R14: 0000000000000001 R15: ffff88810fdd7090
> FS: 00007f46cf6fc6c0(0000) GS:ffff8882a8f5a000(0000) knlGS:0000000000000000
> CS: 0010 DS: 0000 ES: 0000 CR0: 0000000080050033
> CR2: 0000200000e6c000 CR3: 000000010a5a2000 CR4: 00000000000006f0
> Call Trace:
> <TASK>
> queue_work_on+0x106/0x1d0 kernel/workqueue.c:2452
> swap_put_entries_cluster+0x3b1/0x4b0 mm/swapfile.c:1893
> swap_put_entries_direct+0x94/0x100 mm/swapfile.c:2403
> zap_nonpresent_ptes mm/memory.c:1900 [inline]
> do_zap_pte_range mm/memory.c:1967 [inline]
> zap_pte_range mm/memory.c:2065 [inline]
> zap_pmd_range mm/memory.c:2151 [inline]
> zap_pud_range mm/memory.c:2179 [inline]
> zap_p4d_range mm/memory.c:2200 [inline]
> __zap_vma_range+0x1da0/0x4f70 mm/memory.c:2240
> unmap_vmas+0x390/0x550 mm/memory.c:2309
> exit_mmap+0x293/0x9f0 mm/mmap.c:1315
> __mmput+0x118/0x420 kernel/fork.c:1187
> exit_mm+0x221/0x2d0 kernel/exit.c:615
> do_exit+0x6cd/0x2360 kernel/exit.c:997
> do_group_exit+0x22d/0x2f0 kernel/exit.c:1152
> get_signal+0x121b/0x12c0 kernel/signal.c:3046
> arch_do_signal_or_restart+0xbb/0x860 arch/x86/kernel/signal.c:337
> __exit_to_user_mode_loop kernel/entry/common.c:66 [inline]
> exit_to_user_mode_loop+0x104/0x730 kernel/entry/common.c:101
> __exit_to_user_mode_prepare include/linux/irq-entry-common.h:207 [inline]
> syscall_exit_to_user_mode_prepare include/linux/irq-entry-common.h:230 [inline]
> syscall_exit_to_user_mode include/linux/entry-common.h:318 [inline]
> do_syscall_64+0x353/0x580 arch/x86/entry/syscall_64.c:100
> entry_SYSCALL_64_after_hwframe+0x77/0x7f
> RIP: 0033:0x7f46ce79e0d9
> Code: Unable to access opcode bytes at 0x7f46ce79e0af.
> RSP: 002b:00007f46cf6fc028 EFLAGS: 00000246 ORIG_RAX: 000000000000001c
> RAX: 0000000000000000 RBX: 00007f46cea26090 RCX: 00007f46ce79e0d9
> RDX: 0000000000000015 RSI: 0000000000600003 RDI: 0000200000000000
> RBP: 00007f46ce835024 R08: 0000000000000000 R09: 0000000000000000
> R10: 0000000000000000 R11: 0000000000000246 R12: 0000000000000000
> R13: 00007f46cea26128 R14: 00007f46cea26090 R15: 00007ffefd86c578
> </TASK>
>
>
> ***
>
> If these findings have caused you to resend the series or submit a
> separate fix, please add the following tag to your commit message:
> Tested-by: syzbot@syzkaller.appspotmail.com
>
> ---
> This report is generated by a bot. It may contain errors.
> syzbot ci engineers can be reached at syzkaller@googlegroups.com.
>
> To test a fix for this bug, please reply with `#syz test`
> (on a separate line) and attach the patch to the email.
>
> Notes:
> - The patch will be applied on top of the tested series (as an
> incremental fix).
> - To test a new version of the whole series, please send it directly
> to syzbot@lists.linux.dev.
> - Arguments like custom git repos and branches are not supported.
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (16 preceding siblings ...)
2026-08-27 13:59 ` [syzbot ci] Re: xswap: extendable swap device backed by zswap syzbot ci
@ 2026-08-31 17:54 ` Kairui Song
2026-09-01 11:03 ` Baoquan He
2026-09-02 14:33 ` Nhat Pham
2026-09-03 7:35 ` Youngjun Park
19 siblings, 1 reply; 36+ messages in thread
From: Kairui Song @ 2026-08-31 17:54 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, nphamcs, baohua, youngjun.park,
hannes, yosry, shikemeng, chengming.zhou, baoquan.he, david,
linux-kernel
On Thu, Aug 27, 2026 at 05:44:50PM +0800, Baoquan He wrote:
> xswap is an extendable swap device with no backing storage. Swapped-out
> pages live only in zswap, so the device wastes no disk space and its
> size is independent of any physical device.
>
> xswap decouples PTE swap entries from physical backing storage. The
> cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> grown and shrunk on demand:
>
> - Grow: when cluster allocation runs out of free clusters and the device
> is below its ceiling, more physical pages are mapped into the VM_SPARSE
> area and their clusters are added to the free list.
>
> - Shrink: when contiguous free clusters accumulate at the tail of the
> mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> the backing pages freed. Shrink is deferred to a workqueue to avoid
> lock recursion.
>
Hi Baoquan,
I didn't check too many details on how the implementation in previous
RFC until now, After looking at it, using VM_SPARSE to setup the cluster
info area is a really smart idea, really good job!
I think many info are missing in the cover letter though so I wasn't
sure how this grow and shrink works from the description, after
checking the code, it looks much cleaner to me now, correct me
if I'm wrong:
Every xswap device will have a huge and fixed "hard limit"
(si->max and si->nr_clusters_max), and practically can be considered
large enough to hold any workload, and won't change once swapon
is done.
The actually data (si->cluster_info) of xswap device is completely
sparse and dynamic using VM_SPARSE, and so we don't need to change
any existing routine. It grow/alloc and shrink/free automatically by
the kernel, limited or driven by a "soft limit" (si->nr_clusters
and si->pages) which you can modify using the interface below.
Once concern is that the "hard limit" is now the total RAM size. Isn't
that actually a bit small? Will be better if that one is tunable too?
With a parameter, and before swap on, as the hard limit is hard to
adjust once swapon is done. Any thing limiting this?
And I think these details better be mentioned bit more too.
> A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> runtime via debugfs.
>
> Interface:
>
> /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> create a device; percent is a
> percent of RAM (0 for the default),
> prio is an optional swap priority
> (default DEF_SWAP_PRIO)
With what I have read so far, the mandatory percent limit here is kind of
strange, even with 0 as default. Why not make both args optional and just
let it grow without any limit by default? It looks more "fully dynamic"
that way.
> /sys/kernel/mm/xswap/destroy write a swap type to tear down
> a device
> /sys/kernel/debug/xswap/type<N>_cluster_limit
> read/write the per-device
> cluster ceiling
Having a lot of type<N>_cluster_limit in a seperate debug path
looks a bit odd to me too, and the _cluster_limit doesn't look
like a debug interface, we will be relying on debugfs for setting
the limit, also see below.
>
> Since xswap has no backing, swapped-out pages are stored compressed in
> zswap: physical writeout is skipped, and zswap writeback is disabled when
> every swapfile in the system is an xswap device. xswap requires zswap, so
> device creation is refused when zswap is unavailable.
>
> Naming:
> ======
> I'm going with "xswap" (the "x" for extendable/extension) rather than "vswap".
> Chris suggested this name, and this aligns with the "VFS-like swap layers"
> direction Chris Li described in the first swap abstraction LPC talk
> (co-hosted with Yosry) the swap ops and the xswap extension interfaces in
> this series are moving toward exactly that. I don't have a strong preference
> between xswap and vswap, so if reviewers object to the name, please comment.
>
> Note:
> =====
> This patchset only build the base. On top of this, the subsequent core code
> implementation of xswap writeback, rmap etc can be done more easily. E.g, we
> only need add one field in struct swap_cluster_info to let xs_table point to
> physical swap entry, or zswap entry etc. On top of this patchset, no need to
> stir core data structure too much or introduce extra data structure.
>
> --- a/mm/swap.h
> +++ b/mm/swap.h
> @@ -57,6 +57,9 @@ struct swap_cluster_info {
> u8 order;
> atomic_long_t __rcu *table; /* Swap table entries, see mm/swap_table.h */
> unsigned int *extend_table; /* For large swap count, protected by ci->lock */
> +#ifdef CONFIG_XSWAP
> + unsigned long *xs_table;
> +#endif
>
> Testing (taken on qemu kvm guest with 8G memory):
> =========
> 1. enable zswap and create/destroy xswap device
> ~# echo 0 > /sys/kernel/mm/xswap/create
> -bash: echo: write error: Operation not supported
> ~# echo 1 > /sys/module/zswap/parameters/enabled
> ~# echo 0 > /sys/kernel/mm/xswap/create
Recent proposal have mentioned mm/xswap, and mm/swap/tiers, while we
already have mm/swap and module/zswap. I think it's fine to use sysfs
to organize things but will it be good to have it in unified way to
put them all under mm/swap/? And will mkdir be prettier than
echo > create? For these part, just an idea, no strong opinion here.
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 2.3G 0B -1
> ~# echo 0 > /sys/kernel/mm/xswap/destroy
> ~# swapon
>
> 2. create xswap device and tune the zswap size
>
> ~# echo "50 10" > /sys/kernel/mm/xswap/create
> ~# echo 0 > /sys/kernel/mm/xswap/create
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 3.9G 0B 10
> xswap1 xswap 2.3G 0B -1
>
> ~# cat /sys/kernel/debug/xswap/type0_cluster_limit
> 1990
> ~# cat /sys/kernel/debug/xswap/type1_cluster_limit
Can we just use size instead? Calculating the cluster number
seems not neccessary, only making it harder to use, if this
is suppose to be a formal interface and not debug only.
> 1194
> ~# echo 2048 > /sys/kernel/debug/xswap/type0_cluster_limit
> ~# echo 2048 > /sys/kernel/debug/xswap/type1_cluster_limit
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 4G 0B 10
> xswap1 xswap 4G 0B -1
>
> 3. under heavy memory pressure tune swap size or destroy xswap device
>
> ~# stress-ng --vm 1 --vm-bytes 8G --vm-keep --timeout 120s &
>
> ~# echo 1024 > /sys/kernel/debug/xswap/type0_cluster_limit
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 2G 2.6G 10
> xswap1 xswap 4G 182M -1
> ~# echo 1024 > /sys/kernel/debug/xswap/type1_cluster_limit
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 2G 1.4G 10
> xswap1 xswap 2G 315.4M -1
>
> ~# echo 0 > /sys/kernel/mm/xswap/destroy
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap1 xswap 2G 1.1G -1
>
> I tried create/destroy and grow/shrink xswap device under heavy
> memory pressure, all passed.
Do you have some performance reading on this? I remember you had
some in your previous RFC, better to at least keep a link, I spend
quite some time to find the previous zswap test result from you.
I noticed this series is different from what you sent before as it
only contains the foundation so there could be no performance gain
currently but still, might worth mentioning what this could achieve.
Another thing is, maybe we can defer the implementation of shrink
for easier understand and review? The memory consumption is totally
acceptable even without shrink.
^ permalink raw reply [flat|nested] 36+ messages in thread* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-08-31 17:54 ` [PATCH 00/16] " Kairui Song
@ 2026-09-01 11:03 ` Baoquan He
2026-09-02 14:10 ` Nhat Pham
0 siblings, 1 reply; 36+ messages in thread
From: Baoquan He @ 2026-09-01 11:03 UTC (permalink / raw)
To: Kairui Song
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, nphamcs, baohua,
youngjun.park, hannes, yosry, shikemeng, chengming.zhou, david,
linux-kernel
On 09/01/26 at 01:54am, Kairui Song wrote:
> On Thu, Aug 27, 2026 at 05:44:50PM +0800, Baoquan He wrote:
> > xswap is an extendable swap device with no backing storage. Swapped-out
> > pages live only in zswap, so the device wastes no disk space and its
> > size is independent of any physical device.
> >
> > xswap decouples PTE swap entries from physical backing storage. The
> > cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> > grown and shrunk on demand:
> >
> > - Grow: when cluster allocation runs out of free clusters and the device
> > is below its ceiling, more physical pages are mapped into the VM_SPARSE
> > area and their clusters are added to the free list.
> >
> > - Shrink: when contiguous free clusters accumulate at the tail of the
> > mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> > the backing pages freed. Shrink is deferred to a workqueue to avoid
> > lock recursion.
> >
>
> Hi Baoquan,
>
> I didn't check too many details on how the implementation in previous
> RFC until now, After looking at it, using VM_SPARSE to setup the cluster
> info area is a really smart idea, really good job!
>
> I think many info are missing in the cover letter though so I wasn't
> sure how this grow and shrink works from the description, after
> checking the code, it looks much cleaner to me now, correct me
> if I'm wrong:
>
> Every xswap device will have a huge and fixed "hard limit"
> (si->max and si->nr_clusters_max), and practically can be considered
> large enough to hold any workload, and won't change once swapon
> is done.
>
> The actually data (si->cluster_info) of xswap device is completely
> sparse and dynamic using VM_SPARSE, and so we don't need to change
> any existing routine. It grow/alloc and shrink/free automatically by
> the kernel, limited or driven by a "soft limit" (si->nr_clusters
> and si->pages) which you can modify using the interface below.
Thanks a lot for careful checking, and you are quite right about the
mechanism and details.
>
> Once concern is that the "hard limit" is now the total RAM size. Isn't
> that actually a bit small? Will be better if that one is tunable too?
> With a parameter, and before swap on, as the hard limit is hard to
> adjust once swapon is done. Any thing limiting this?
Chris and I talked about this, we both think the total RAM size is a
good hard limit. Because xswap is similar with zswap/zram in essence by
compressing memory content to save memory. So the real limit is the
zswap pool, not the slot count. In fact it's never able to utilize the
total system RAM, right? Making it larger than system RAM is
meaningless.
Memory hotplug is a case in which system RAM can be enlarged during
system running, while that can be taken into account later as a enhanced
feature if it's really wanted.
>
> And I think these details better be mentioned bit more too.
Sure, I can put these thoughts into cover letter or patch log for
reference.
>
> > A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> > runtime via debugfs.
> >
> > Interface:
> >
> > /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> > create a device; percent is a
> > percent of RAM (0 for the default),
> > prio is an optional swap priority
> > (default DEF_SWAP_PRIO)
>
> With what I have read so far, the mandatory percent limit here is kind of
> strange, even with 0 as default. Why not make both args optional and just
> let it grow without any limit by default? It looks more "fully dynamic"
> that way.
I'd like to clarify why we default to a soft limit rather than "no limit".
The soft limit is the administrator's deliberate size choice, similar
to how zram requires an explicit size. On a multi-TB system the
cluster_info array is not free, so planning how much of it to allow is a
real decision. The current behavior is: grow up to the soft limit as usage
demands, then stay there. We do not shrink on idle, and shrink only happens
when the admin lowers the limit. So there is no grow/shrink oscillation in
normal operation.
A default of "no limit / fully dynamic" will instead let the device grow
without restriction under memory pressure. While allocating cluster_info
pages exactly when memory is scarce, relying on shrink to reclaim afterwards,
which is the oscillation we want to avoid. So we'll make both create arguments
optional, but the default will be a sensible ceiling rather than unbounded.
>
> > /sys/kernel/mm/xswap/destroy write a swap type to tear down
> > a device
> > /sys/kernel/debug/xswap/type<N>_cluster_limit
> > read/write the per-device
> > cluster ceiling
>
> Having a lot of type<N>_cluster_limit in a seperate debug path
> looks a bit odd to me too, and the _cluster_limit doesn't look
> like a debug interface, we will be relying on debugfs for setting
> the limit, also see below.
Agreed on both — it's a real interface, will move to sysfs, and will
use size (pages/MiB) instead of cluster count.
>
> >
> > Since xswap has no backing, swapped-out pages are stored compressed in
> > zswap: physical writeout is skipped, and zswap writeback is disabled when
> > every swapfile in the system is an xswap device. xswap requires zswap, so
> > device creation is refused when zswap is unavailable.
> >
> > Naming:
> > ======
> > I'm going with "xswap" (the "x" for extendable/extension) rather than "vswap".
> > Chris suggested this name, and this aligns with the "VFS-like swap layers"
> > direction Chris Li described in the first swap abstraction LPC talk
> > (co-hosted with Yosry) the swap ops and the xswap extension interfaces in
> > this series are moving toward exactly that. I don't have a strong preference
> > between xswap and vswap, so if reviewers object to the name, please comment.
> >
> > Note:
> > =====
> > This patchset only build the base. On top of this, the subsequent core code
> > implementation of xswap writeback, rmap etc can be done more easily. E.g, we
> > only need add one field in struct swap_cluster_info to let xs_table point to
> > physical swap entry, or zswap entry etc. On top of this patchset, no need to
> > stir core data structure too much or introduce extra data structure.
> >
> > --- a/mm/swap.h
> > +++ b/mm/swap.h
> > @@ -57,6 +57,9 @@ struct swap_cluster_info {
> > u8 order;
> > atomic_long_t __rcu *table; /* Swap table entries, see mm/swap_table.h */
> > unsigned int *extend_table; /* For large swap count, protected by ci->lock */
> > +#ifdef CONFIG_XSWAP
> > + unsigned long *xs_table;
> > +#endif
> >
> > Testing (taken on qemu kvm guest with 8G memory):
> > =========
> > 1. enable zswap and create/destroy xswap device
> > ~# echo 0 > /sys/kernel/mm/xswap/create
> > -bash: echo: write error: Operation not supported
> > ~# echo 1 > /sys/module/zswap/parameters/enabled
> > ~# echo 0 > /sys/kernel/mm/xswap/create
>
> Recent proposal have mentioned mm/xswap, and mm/swap/tiers, while we
> already have mm/swap and module/zswap. I think it's fine to use sysfs
> to organize things but will it be good to have it in unified way to
> put them all under mm/swap/? And will mkdir be prettier than
> echo > create? For these part, just an idea, no strong opinion here.
Creation passes percent+priority atomically; a plain mkdir can't. I'd
keep echo > create. On mm/swap/ unification, happy to align once the
direction settles, while keep this series as is for now.
>
> > ~# swapon
> > NAME TYPE SIZE USED PRIO
> > xswap0 xswap 2.3G 0B -1
> > ~# echo 0 > /sys/kernel/mm/xswap/destroy
> > ~# swapon
> >
> > 2. create xswap device and tune the zswap size
> >
> > ~# echo "50 10" > /sys/kernel/mm/xswap/create
> > ~# echo 0 > /sys/kernel/mm/xswap/create
> > ~# swapon
> > NAME TYPE SIZE USED PRIO
> > xswap0 xswap 3.9G 0B 10
> > xswap1 xswap 2.3G 0B -1
> >
> > ~# cat /sys/kernel/debug/xswap/type0_cluster_limit
> > 1990
> > ~# cat /sys/kernel/debug/xswap/type1_cluster_limit
>
> Can we just use size instead? Calculating the cluster number
> seems not neccessary, only making it harder to use, if this
> is suppose to be a formal interface and not debug only.
As said at above, I will change to size as suggested.
>
> > 1194
> > ~# echo 2048 > /sys/kernel/debug/xswap/type0_cluster_limit
> > ~# echo 2048 > /sys/kernel/debug/xswap/type1_cluster_limit
> > ~# swapon
> > NAME TYPE SIZE USED PRIO
> > xswap0 xswap 4G 0B 10
> > xswap1 xswap 4G 0B -1
> >
> > 3. under heavy memory pressure tune swap size or destroy xswap device
> >
> > ~# stress-ng --vm 1 --vm-bytes 8G --vm-keep --timeout 120s &
> >
> > ~# echo 1024 > /sys/kernel/debug/xswap/type0_cluster_limit
> > ~# swapon
> > NAME TYPE SIZE USED PRIO
> > xswap0 xswap 2G 2.6G 10
> > xswap1 xswap 4G 182M -1
> > ~# echo 1024 > /sys/kernel/debug/xswap/type1_cluster_limit
> > ~# swapon
> > NAME TYPE SIZE USED PRIO
> > xswap0 xswap 2G 1.4G 10
> > xswap1 xswap 2G 315.4M -1
> >
> > ~# echo 0 > /sys/kernel/mm/xswap/destroy
> > ~# swapon
> > NAME TYPE SIZE USED PRIO
> > xswap1 xswap 2G 1.1G -1
> >
> > I tried create/destroy and grow/shrink xswap device under heavy
> > memory pressure, all passed.
>
> Do you have some performance reading on this? I remember you had
> some in your previous RFC, better to at least keep a link, I spend
> quite some time to find the previous zswap test result from you.
> I noticed this series is different from what you sent before as it
> only contains the foundation so there could be no performance gain
> currently but still, might worth mentioning what this could achieve.
The initial version is about removing xarray in zswap, that has a obvious
performance improvement data. This series is the foundation
(grow/shrink/destroy). I will think about ways to get some testing data,
e.g xswap vs zswap (no writing back).
>
> Another thing is, maybe we can defer the implementation of shrink
> for easier understand and review? The memory consumption is totally
> acceptable even without shrink.
I understand the motivation, but I don't think it reduces the review burden
as much as it seems. When a device is removed (destroy/swapoff), its mapped
pages must be released regardless of shrink, otherwise they leak. And that
cleanup uses the same unmap-and-free code shrink does. So deferring shrink
removes only the runtime shrink triggers, not the unmap machinery, which
every device needs on removal. The nr_clusters ceiling and the limit knob
are also half shrink: without shrink, lowering the limit can stop growth
but never reclaim memory, leaving that interface only half functional.
Thanks again for your careful reviewing, thorough considering and great
suggestions.
Thanks
Baoquan
^ permalink raw reply [flat|nested] 36+ messages in thread* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-09-01 11:03 ` Baoquan He
@ 2026-09-02 14:10 ` Nhat Pham
2026-09-04 9:42 ` Baoquan He
0 siblings, 1 reply; 36+ messages in thread
From: Nhat Pham @ 2026-09-02 14:10 UTC (permalink / raw)
To: Baoquan He
Cc: Kairui Song, Baoquan He, linux-mm, akpm, chrisl, kasong, baohua,
youngjun.park, hannes, yosry, shikemeng, chengming.zhou, david,
linux-kernel
On Tue, Sep 1, 2026 at 7:03 AM Baoquan He <baoquan.he@linux.dev> wrote:
>
> On 09/01/26 at 01:54am, Kairui Song wrote:
> > On Thu, Aug 27, 2026 at 05:44:50PM +0800, Baoquan He wrote:
> > > xswap is an extendable swap device with no backing storage. Swapped-out
> > > pages live only in zswap, so the device wastes no disk space and its
> > > size is independent of any physical device.
> > >
> > > xswap decouples PTE swap entries from physical backing storage. The
> > > cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> > > grown and shrunk on demand:
> > >
> > > - Grow: when cluster allocation runs out of free clusters and the device
> > > is below its ceiling, more physical pages are mapped into the VM_SPARSE
> > > area and their clusters are added to the free list.
> > >
> > > - Shrink: when contiguous free clusters accumulate at the tail of the
> > > mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> > > the backing pages freed. Shrink is deferred to a workqueue to avoid
> > > lock recursion.
> > >
> >
> > Hi Baoquan,
> >
> > I didn't check too many details on how the implementation in previous
> > RFC until now, After looking at it, using VM_SPARSE to setup the cluster
> > info area is a really smart idea, really good job!
> >
> > I think many info are missing in the cover letter though so I wasn't
> > sure how this grow and shrink works from the description, after
> > checking the code, it looks much cleaner to me now, correct me
> > if I'm wrong:
> >
> > Every xswap device will have a huge and fixed "hard limit"
> > (si->max and si->nr_clusters_max), and practically can be considered
> > large enough to hold any workload, and won't change once swapon
> > is done.
> >
> > The actually data (si->cluster_info) of xswap device is completely
> > sparse and dynamic using VM_SPARSE, and so we don't need to change
> > any existing routine. It grow/alloc and shrink/free automatically by
> > the kernel, limited or driven by a "soft limit" (si->nr_clusters
> > and si->pages) which you can modify using the interface below.
>
> Thanks a lot for careful checking, and you are quite right about the
> mechanism and details.
>
> >
> > Once concern is that the "hard limit" is now the total RAM size. Isn't
> > that actually a bit small? Will be better if that one is tunable too?
> > With a parameter, and before swap on, as the hard limit is hard to
> > adjust once swapon is done. Any thing limiting this?
>
> Chris and I talked about this, we both think the total RAM size is a
> good hard limit. Because xswap is similar with zswap/zram in essence by
> compressing memory content to save memory. So the real limit is the
> zswap pool, not the slot count. In fact it's never able to utilize the
> total system RAM, right? Making it larger than system RAM is
> meaningless.
No. This is not quite right. The size of this device is the size of
the "swapped out" data, which is multiple times the post-compression
size (i.e zswap pool size). The multiple here depends on how well the
data is compressed.
This is not to consider the other swap backends:
1. zero-filled swap pages have effectively 0 memory footprint.
2. disk swap pages (I know this is not currently supported yet, but
it's a consideration for the overall design).
>
> Memory hotplug is a case in which system RAM can be enlarged during
> system running, while that can be taken into account later as a enhanced
> feature if it's really wanted.
A lot of these problems are self-inflicted. If we design a fully
dynamic swap device, then it's not in consideration.
>
> >
> > And I think these details better be mentioned bit more too.
>
> Sure, I can put these thoughts into cover letter or patch log for
> reference.
>
> >
> > > A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> > > runtime via debugfs.
> > >
> > > Interface:
> > >
> > > /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> > > create a device; percent is a
> > > percent of RAM (0 for the default),
> > > prio is an optional swap priority
> > > (default DEF_SWAP_PRIO)
> >
> > With what I have read so far, the mandatory percent limit here is kind of
> > strange, even with 0 as default. Why not make both args optional and just
> > let it grow without any limit by default? It looks more "fully dynamic"
> > that way.
>
> I'd like to clarify why we default to a soft limit rather than "no limit".
>
> The soft limit is the administrator's deliberate size choice, similar
> to how zram requires an explicit size. On a multi-TB system the
> cluster_info array is not free, so planning how much of it to allow is a
> real decision. The current behavior is: grow up to the soft limit as usage
> demands, then stay there. We do not shrink on idle, and shrink only happens
> when the admin lowers the limit. So there is no grow/shrink oscillation in
> normal operation.
>
> A default of "no limit / fully dynamic" will instead let the device grow
> without restriction under memory pressure. While allocating cluster_info
> pages exactly when memory is scarce, relying on shrink to reclaim afterwards,
> which is the oscillation we want to avoid. So we'll make both create arguments
> optional, but the default will be a sensible ceiling rather than unbounded.
>
> >
> > > /sys/kernel/mm/xswap/destroy write a swap type to tear down
> > > a device
> > > /sys/kernel/debug/xswap/type<N>_cluster_limit
> > > read/write the per-device
> > > cluster ceiling
> >
What's the point of having multiple xswap devices if it's going to be
dynamic, cluster-based anyway?
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-09-02 14:10 ` Nhat Pham
@ 2026-09-04 9:42 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-04 9:42 UTC (permalink / raw)
To: Nhat Pham
Cc: Kairui Song, Baoquan He, linux-mm, akpm, chrisl, kasong, baohua,
youngjun.park, hannes, yosry, shikemeng, chengming.zhou, david,
linux-kernel
On 09/02/26 at 10:10am, Nhat Pham wrote:
> On Tue, Sep 1, 2026 at 7:03 AM Baoquan He <baoquan.he@linux.dev> wrote:
> >
> > On 09/01/26 at 01:54am, Kairui Song wrote:
> > > On Thu, Aug 27, 2026 at 05:44:50PM +0800, Baoquan He wrote:
> > > > xswap is an extendable swap device with no backing storage. Swapped-out
> > > > pages live only in zswap, so the device wastes no disk space and its
> > > > size is independent of any physical device.
> > > >
> > > > xswap decouples PTE swap entries from physical backing storage. The
> > > > cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> > > > grown and shrunk on demand:
> > > >
> > > > - Grow: when cluster allocation runs out of free clusters and the device
> > > > is below its ceiling, more physical pages are mapped into the VM_SPARSE
> > > > area and their clusters are added to the free list.
> > > >
> > > > - Shrink: when contiguous free clusters accumulate at the tail of the
> > > > mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> > > > the backing pages freed. Shrink is deferred to a workqueue to avoid
> > > > lock recursion.
> > > >
> > >
> > > Hi Baoquan,
> > >
> > > I didn't check too many details on how the implementation in previous
> > > RFC until now, After looking at it, using VM_SPARSE to setup the cluster
> > > info area is a really smart idea, really good job!
> > >
> > > I think many info are missing in the cover letter though so I wasn't
> > > sure how this grow and shrink works from the description, after
> > > checking the code, it looks much cleaner to me now, correct me
> > > if I'm wrong:
> > >
> > > Every xswap device will have a huge and fixed "hard limit"
> > > (si->max and si->nr_clusters_max), and practically can be considered
> > > large enough to hold any workload, and won't change once swapon
> > > is done.
> > >
> > > The actually data (si->cluster_info) of xswap device is completely
> > > sparse and dynamic using VM_SPARSE, and so we don't need to change
> > > any existing routine. It grow/alloc and shrink/free automatically by
> > > the kernel, limited or driven by a "soft limit" (si->nr_clusters
> > > and si->pages) which you can modify using the interface below.
> >
> > Thanks a lot for careful checking, and you are quite right about the
> > mechanism and details.
> >
> > >
> > > Once concern is that the "hard limit" is now the total RAM size. Isn't
> > > that actually a bit small? Will be better if that one is tunable too?
> > > With a parameter, and before swap on, as the hard limit is hard to
> > > adjust once swapon is done. Any thing limiting this?
> >
> > Chris and I talked about this, we both think the total RAM size is a
> > good hard limit. Because xswap is similar with zswap/zram in essence by
> > compressing memory content to save memory. So the real limit is the
> > zswap pool, not the slot count. In fact it's never able to utilize the
> > total system RAM, right? Making it larger than system RAM is
> > meaningless.
>
> No. This is not quite right. The size of this device is the size of
> the "swapped out" data, which is multiple times the post-compression
> size (i.e zswap pool size). The multiple here depends on how well the
> data is compressed.
>
> This is not to consider the other swap backends:
>
> 1. zero-filled swap pages have effectively 0 memory footprint.
>
> 2. disk swap pages (I know this is not currently supported yet, but
> it's a consideration for the overall design).
You are right, system RAM may not be a good hard limit. Then 2x system
RAM?
>
> >
> > Memory hotplug is a case in which system RAM can be enlarged during
> > system running, while that can be taken into account later as a enhanced
> > feature if it's really wanted.
>
> A lot of these problems are self-inflicted. If we design a fully
> dynamic swap device, then it's not in consideration.
As initial version, I'd like to make it not fully dynamic. The current
mechanism can make the change of xswap being full dynamic very easy.
I am glad to see people can post patch to change it later with
justification. I don't think we need to make everything perfect and anyone
satisfied at the beginning.
>
> >
> > >
> > > And I think these details better be mentioned bit more too.
> >
> > Sure, I can put these thoughts into cover letter or patch log for
> > reference.
> >
> > >
> > > > A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> > > > runtime via debugfs.
> > > >
> > > > Interface:
> > > >
> > > > /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> > > > create a device; percent is a
> > > > percent of RAM (0 for the default),
> > > > prio is an optional swap priority
> > > > (default DEF_SWAP_PRIO)
> > >
> > > With what I have read so far, the mandatory percent limit here is kind of
> > > strange, even with 0 as default. Why not make both args optional and just
> > > let it grow without any limit by default? It looks more "fully dynamic"
> > > that way.
> >
> > I'd like to clarify why we default to a soft limit rather than "no limit".
> >
> > The soft limit is the administrator's deliberate size choice, similar
> > to how zram requires an explicit size. On a multi-TB system the
> > cluster_info array is not free, so planning how much of it to allow is a
> > real decision. The current behavior is: grow up to the soft limit as usage
> > demands, then stay there. We do not shrink on idle, and shrink only happens
> > when the admin lowers the limit. So there is no grow/shrink oscillation in
> > normal operation.
> >
> > A default of "no limit / fully dynamic" will instead let the device grow
> > without restriction under memory pressure. While allocating cluster_info
> > pages exactly when memory is scarce, relying on shrink to reclaim afterwards,
> > which is the oscillation we want to avoid. So we'll make both create arguments
> > optional, but the default will be a sensible ceiling rather than unbounded.
> >
> > >
> > > > /sys/kernel/mm/xswap/destroy write a swap type to tear down
> > > > a device
> > > > /sys/kernel/debug/xswap/type<N>_cluster_limit
> > > > read/write the per-device
> > > > cluster ceiling
> > >
>
> What's the point of having multiple xswap devices if it's going to be
> dynamic, cluster-based anyway?
^ permalink raw reply [flat|nested] 36+ messages in thread
* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (17 preceding siblings ...)
2026-08-31 17:54 ` [PATCH 00/16] " Kairui Song
@ 2026-09-02 14:33 ` Nhat Pham
2026-09-03 7:35 ` Youngjun Park
19 siblings, 0 replies; 36+ messages in thread
From: Nhat Pham @ 2026-09-02 14:33 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, baohua, youngjun.park, hannes,
yosry, shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 5:45 AM Baoquan He <hebaoquan@kylinos.cn> wrote:
Ah I think I used the wrong msg-id in my other response:
https://lore.kernel.org/all/CAKEwX=OW6M5n8u3L_oigD3Ffn_BcB2L7p=ROfmJCxMKQFH7tXg@mail.gmail.com/
My bad. I'll recap my comments here for your convenience.
>
> xswap is an extendable swap device with no backing storage. Swapped-out
> pages live only in zswap, so the device wastes no disk space and its
> size is independent of any physical device.
>
> xswap decouples PTE swap entries from physical backing storage. The
> cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> grown and shrunk on demand:
>
> - Grow: when cluster allocation runs out of free clusters and the device
> is below its ceiling, more physical pages are mapped into the VM_SPARSE
> area and their clusters are added to the free list.
>
> - Shrink: when contiguous free clusters accumulate at the tail of the
> mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> the backing pages freed. Shrink is deferred to a workqueue to avoid
> lock recursion.
>
> A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> runtime via debugfs.
>
> Interface:
>
> /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> create a device; percent is a
> percent of RAM (0 for the default),
> prio is an optional swap priority
> (default DEF_SWAP_PRIO)
> /sys/kernel/mm/xswap/destroy write a swap type to tear down
> a device
> /sys/kernel/debug/xswap/type<N>_cluster_limit
> read/write the per-device
> cluster ceiling
>
> Since xswap has no backing, swapped-out pages are stored compressed in
> zswap: physical writeout is skipped, and zswap writeback is disabled when
> every swapfile in the system is an xswap device. xswap requires zswap, so
> device creation is refused when zswap is unavailable.
>
> Naming:
> ======
> I'm going with "xswap" (the "x" for extendable/extension) rather than "vswap".
> Chris suggested this name, and this aligns with the "VFS-like swap layers"
> direction Chris Li described in the first swap abstraction LPC talk
> (co-hosted with Yosry) the swap ops and the xswap extension interfaces in
> this series are moving toward exactly that. I don't have a strong preference
> between xswap and vswap, so if reviewers object to the name, please comment.
>
> Note:
> =====
> This patchset only build the base. On top of this, the subsequent core code
> implementation of xswap writeback, rmap etc can be done more easily. E.g, we
> only need add one field in struct swap_cluster_info to let xs_table point to
> physical swap entry, or zswap entry etc. On top of this patchset, no need to
> stir core data structure too much or introduce extra data structure.
It's functionally equivalent. I think it looks cleaner to separate
vswap-only bits into a vswap-only struct (technically saving 1 word
for the cluster that doesnt need it too).
That aside, how would writeback, rmap be simpler? The complexity comes
from the operations around it, not the actual vtable/xs_table
operations itself.
>
> --- a/mm/swap.h
> +++ b/mm/swap.h
> @@ -57,6 +57,9 @@ struct swap_cluster_info {
> u8 order;
> atomic_long_t __rcu *table; /* Swap table entries, see mm/swap_table.h */
> unsigned int *extend_table; /* For large swap count, protected by ci->lock */
> +#ifdef CONFIG_XSWAP
> + unsigned long *xs_table;
> +#endif
>
> Testing (taken on qemu kvm guest with 8G memory):
> =========
> 1. enable zswap and create/destroy xswap device
> ~# echo 0 > /sys/kernel/mm/xswap/create
> -bash: echo: write error: Operation not supported
> ~# echo 1 > /sys/module/zswap/parameters/enabled
> ~# echo 0 > /sys/kernel/mm/xswap/create
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 2.3G 0B -1
> ~# echo 0 > /sys/kernel/mm/xswap/destroy
> ~# swapon
>
> 2. create xswap device and tune the zswap size
>
> ~# echo "50 10" > /sys/kernel/mm/xswap/create
> ~# echo 0 > /sys/kernel/mm/xswap/create
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 3.9G 0B 10
> xswap1 xswap 2.3G 0B -1
>
> ~# cat /sys/kernel/debug/xswap/type0_cluster_limit
> 1990
> ~# cat /sys/kernel/debug/xswap/type1_cluster_limit
> 1194
> ~# echo 2048 > /sys/kernel/debug/xswap/type0_cluster_limit
> ~# echo 2048 > /sys/kernel/debug/xswap/type1_cluster_limit
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 4G 0B 10
> xswap1 xswap 4G 0B -1
>
> 3. under heavy memory pressure tune swap size or destroy xswap device
>
> ~# stress-ng --vm 1 --vm-bytes 8G --vm-keep --timeout 120s &
>
> ~# echo 1024 > /sys/kernel/debug/xswap/type0_cluster_limit
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 2G 2.6G 10
> xswap1 xswap 4G 182M -1
> ~# echo 1024 > /sys/kernel/debug/xswap/type1_cluster_limit
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap0 xswap 2G 1.4G 10
> xswap1 xswap 2G 315.4M -1
>
> ~# echo 0 > /sys/kernel/mm/xswap/destroy
> ~# swapon
> NAME TYPE SIZE USED PRIO
> xswap1 xswap 2G 1.1G -1
>
> I tried create/destroy and grow/shrink xswap device under heavy
> memory pressure, all passed.
We have something that has performance benchmarks, including on a
real, production workload:
https://lore.kernel.org/all/20260825153238.2695446-1-nphamcs@gmail.com/
And somehow, we decide to implement a different data structures,
without concrete numbers to show that it is more efficient than xarray
(only intuitions)?
At the risk of sounding like a broken record - I have to ask: why
don't we land use case first, optimize later? Especially since, we
have data showing that performance is fairly close (better in some
aspects in fact), AND users who do not want to risk it still have the
old code and can do A/B testing before they decide to switch?
A lot of the complexity in this patch series comes from the fact that
you don't get a lot of what xarray gives you out of the box. Which is
fine for an optimization, but I don't see any A/B data to compare?
Doesn't that sound like pre-mature optimization?
^ permalink raw reply [flat|nested] 36+ messages in thread* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-08-27 9:44 [PATCH 00/16] xswap: extendable swap device backed by zswap Baoquan He
` (18 preceding siblings ...)
2026-09-02 14:33 ` Nhat Pham
@ 2026-09-03 7:35 ` Youngjun Park
2026-09-04 3:33 ` Baoquan He
19 siblings, 1 reply; 36+ messages in thread
From: Youngjun Park @ 2026-09-03 7:35 UTC (permalink / raw)
To: Baoquan He
Cc: linux-mm, akpm, chrisl, kasong, nphamcs, baohua, hannes, yosry,
shikemeng, chengming.zhou, baoquan.he, david, linux-kernel
On Thu, Aug 27, 2026 at 05:44:50PM +0800, Baoquan He wrote:
> xswap is an extendable swap device with no backing storage. Swapped-out
> pages live only in zswap, so the device wastes no disk space and its
> size is independent of any physical device.
>
> xswap decouples PTE swap entries from physical backing storage. The
> cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> grown and shrunk on demand:
>
> - Grow: when cluster allocation runs out of free clusters and the device
> is below its ceiling, more physical pages are mapped into the VM_SPARSE
> area and their clusters are added to the free list.
>
> - Shrink: when contiguous free clusters accumulate at the tail of the
> mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> the backing pages freed. Shrink is deferred to a workqueue to avoid
> lock recursion.
>
> A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> runtime via debugfs.
>
> Interface:
>
> /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> create a device; percent is a
> percent of RAM (0 for the default),
> prio is an optional swap priority
> (default DEF_SWAP_PRIO)
As discussed before, until there's a per-memcg tier concept, is there
a meaningful use case for having more than one xswap device? Would it
make sense to limit it to a single device for now, and add support
for multiple devices later once that structure exists?
Also, if xswap accepts an explicit prio, xswap devices would need to
stay grouped within the same tier. But a slow tier with a different
priority range could end up sandwiched in between, or an xswap device
could fall outside the priority range needed to belong to the same
tier.
Could prio just be fixed instead? Is there a reason it needs to be
assignable per device?
Thanks!
Youngjun
^ permalink raw reply [flat|nested] 36+ messages in thread* Re: [PATCH 00/16] xswap: extendable swap device backed by zswap
2026-09-03 7:35 ` Youngjun Park
@ 2026-09-04 3:33 ` Baoquan He
0 siblings, 0 replies; 36+ messages in thread
From: Baoquan He @ 2026-09-04 3:33 UTC (permalink / raw)
To: Youngjun Park
Cc: Baoquan He, linux-mm, akpm, chrisl, kasong, nphamcs, baohua,
hannes, yosry, shikemeng, chengming.zhou, david, linux-kernel
On 09/03/26 at 04:35pm, Youngjun Park wrote:
> On Thu, Aug 27, 2026 at 05:44:50PM +0800, Baoquan He wrote:
> > xswap is an extendable swap device with no backing storage. Swapped-out
> > pages live only in zswap, so the device wastes no disk space and its
> > size is independent of any physical device.
> >
> > xswap decouples PTE swap entries from physical backing storage. The
> > cluster_info array is backed by a sparse vmalloc (VM_SPARSE) area that is
> > grown and shrunk on demand:
> >
> > - Grow: when cluster allocation runs out of free clusters and the device
> > is below its ceiling, more physical pages are mapped into the VM_SPARSE
> > area and their clusters are added to the free list.
> >
> > - Shrink: when contiguous free clusters accumulate at the tail of the
> > mapped range (tracked in O(1) via nr_free_tail), they are unmapped and
> > the backing pages freed. Shrink is deferred to a workqueue to avoid
> > lock recursion.
> >
> > A per-device ceiling (nr_clusters) bounds growth and is adjustable at
> > runtime via debugfs.
> >
> > Interface:
> >
> > /sys/kernel/mm/xswap/create write "<percent> [<prio>]" to
> > create a device; percent is a
> > percent of RAM (0 for the default),
> > prio is an optional swap priority
> > (default DEF_SWAP_PRIO)
>
> As discussed before, until there's a per-memcg tier concept, is there
> a meaningful use case for having more than one xswap device? Would it
> make sense to limit it to a single device for now, and add support
> for multiple devices later once that structure exists?
Hi Youngjun,
Thanks for careful checking and your question.
Here, I am also confused why both you and Nhat oppose per-device xswap.
Actually, supporting per-device xswap was part of Chris's original plan
and was designed for the swap tier. From my perspective, it's also a
very natural thing because the current swap code natively supports
per-device, and swap tier needs to use it anyway, so I don't bother
to add code to restrict it to single device. More importantly, for
per-device xswap, you can use one if you want, or multiple if you want
too, it won't prevent anyone from using a single xswap instance.
>
> Also, if xswap accepts an explicit prio, xswap devices would need to
> stay grouped within the same tier. But a slow tier with a different
> priority range could end up sandwiched in between, or an xswap device
> could fall outside the priority range needed to belong to the same
> tier.
>
> Could prio just be fixed instead? Is there a reason it needs to be
> assignable per device?
A good question. I haven't thought of a good way for xswap prio. Fixed
prio, or a high prio region for xswap? Zram is ram swap device too, it
shares the same prio setting semantics as a normal one. Can we keep it
as is, to leave it to xswap user?
Thanks
Baoquan
^ permalink raw reply [flat|nested] 36+ messages in thread