* [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-08-27 9:44 ` [PATCH 02/16] mm: xswap support for zswap Baoquan He
` (15 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (14 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (13 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (12 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (11 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (10 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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-08-27 9:44 ` [PATCH 08/16] mm, swap: add xswap_try_shrink and shrink trigger on cluster free Baoquan He
` (9 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (8 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (7 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (6 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (5 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
` (4 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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-08-27 9:45 ` [PATCH 14/16] mm, swap: refactor swapoff + add xswap_destroy Baoquan He
` (3 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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-08-27 9:45 ` [PATCH 15/16] mm, swap: require zswap for xswap devices Baoquan He
` (2 subsequent siblings)
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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-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] Re: xswap: extendable swap device backed by zswap syzbot ci
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
16 siblings, 0 replies; 18+ 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 [flat|nested] 18+ 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
16 siblings, 0 replies; 18+ 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] 18+ messages in thread