Linux-mm Archive on lore.kernel.org
 help / color / mirror / Atom feed
From: sashiko-bot@kernel.org
To: "Kanchana P. Sridhar" <kanchanapsridhar2026@gmail.com>,
	hannes@cmpxchg.org, yosry@kernel.org, nphamcs@gmail.com,
	chengming.zhou@linux.dev, akpm@linux-foundation.org,
	linux-mm@kvack.org, herbert@gondor.apana.org.au,
	davem@davemloft.net, linux-crypto@vger.kernel.org
Cc: rientjes@google.com, willy@infradead.org, senozhatsky@chromium.org
Subject: Re: [PATCH v2 7/8] mm: zswap: Batched zswap_compress() for storing large folios in batches.
Date: Thu, 29 Oct 2026 03:06:05 +0000	[thread overview]
Message-ID: <sashiko-outbox-188659@kernel.org> (raw)
In-Reply-To: <20261028030503.17923-8-kanchanapsridhar2026@gmail.com>

Thank you for your contribution! Sashiko AI review found 6 potential issue(s) to consider:
- [Critical] mm: zswap: unclamped nr_batch_pages in zswap_compress() causes OOB and crash
- [High] mm: zswap: zsmalloc handle leak on partial batch failure in zswap_compress()
- [High] mm: zswap: invalid and unreset dst scatterlist lengths in zswap_compress()
- [High] mm: zswap: zswap_cpu_comp_prepare() returns 0 on buffer or sg_table alloc fail
- [High] mm: zswap: zswap_compress() reads -EINPROGRESS sg->length for async acomp
- [Medium] mm: zswap: zswap_compress() stores PAGE_SIZE outputs when writeback disabled
--

commit 61c906de6e16dcc53db7f835142f9c05558dca5f
Author: Kanchana P. Sridhar <kanchanapsridhar2026@gmail.com>

mm: zswap: Batched zswap_compress() for storing large folios in batches.

This commit introduces a batched zswap_compress() implementation using
per-CPU scatterlists and refactors zsmalloc allocation into
zswap_update_entries().

> diff --git a/mm/zswap.c b/mm/zswap.c
> index c8890e8a9b836..4476fb4aeb5fd 100644
> --- a/mm/zswap.c
> +++ b/mm/zswap.c

[ ... ]

> @@ -944,6 +956,21 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
>  			goto req_fail;
>  	}
>  
> +	acomp_ctx->comp.sg_table = kmalloc(sizeof(*acomp_ctx->comp.sg_table), GFP_KERNEL);
> +	if (!acomp_ctx->comp.sg_table)
> +		goto req_fail;
> +
> +	if (sg_alloc_table(acomp_ctx->comp.sg_table, pool->compr_batch_size,
> +			   GFP_KERNEL))
> +		goto req_fail;
> +
> +	/*
> +	 * Statically map the per-CPU destination buffers to the per-CPU
> +	 * SG lists; crucial for performance.
> +	 */
> +	for_each_sg(acomp_ctx->comp.sg_table->sgl, sg, pool->compr_batch_size, i)
> +		sg_set_buf(sg, acomp_ctx->comp.buffers[i], PAGE_SIZE);
> +
>  	/* Skip the per-CPU request when decompression can use the stack. */
>  	if (acomp_is_async(acomp_ctx->acomp)) {
>  		ret = zswap_acomp_req_init(&acomp_ctx->decomp, acomp_ctx->acomp);
> @@ -960,92 +987,161 @@ static int zswap_cpu_comp_prepare(unsigned int cpu, struct hlist_node *node)
>  	return ret;
>  }

[Severity: High]
Does zswap_cpu_comp_prepare() return 0 on failure if allocating
acomp_ctx->comp.buffers, acomp_ctx->comp.sg_table, or sg_alloc_table()
fails?

Earlier in zswap_cpu_comp_prepare(), ret is overwritten by
zswap_acomp_req_init():

	ret = zswap_acomp_req_init(&acomp_ctx->comp.areq, acomp_ctx->acomp);
	if (ret)
		goto req_fail;

When that call succeeds, ret is 0. If any of the subsequent allocations
for buffers or sg_table fail and jump to req_fail, acomp_ctx_free() tears
down and zeroes acomp_ctx, and zswap_cpu_comp_prepare() returns ret (0) to
the CPU hotplug core, leading to a NULL pointer dereference when zswap
later uses the per-CPU context.

> -static bool zswap_compress(struct folio *folio, long index,
> -			   struct zswap_entry *entry, struct zswap_pool *pool,
> -			   bool wb_enabled)
> +static bool zswap_update_entries(struct zswap_store_context *zc,
> +				 u8 batch_start)
>  {
> -	struct zswap_comp_ctx *comp_ctx;
> -	struct scatterlist input, output;
> -	int comp_ret = 0, alloc_ret = 0;
> -	unsigned int dlen = PAGE_SIZE;
> +	gfp_t gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
> +	u8 i, batch_iter, comp_batch_iter;
>  	unsigned long handle;
> -	gfp_t gfp;
> +	unsigned int dlen;
>  	u8 *dst;
> -	bool mapped = false;
> -
> -	comp_ctx = &raw_cpu_ptr(pool->acomp_ctx)->comp;
> -	mutex_lock(&comp_ctx->areq.mutex);
> -
> -	dst = comp_ctx->buffers[0];
> -	sg_init_table(&input, 1);
> -	sg_set_folio(&input, folio, PAGE_SIZE, index * PAGE_SIZE);
> -
> -	sg_init_one(&output, dst, PAGE_SIZE);
> -	acomp_request_set_params(comp_ctx->areq.req, &input, &output,
> -				 PAGE_SIZE, dlen);
>  
>  	/*
> -	 * it maybe looks a little bit silly that we send an asynchronous request,
> -	 * then wait for its completion synchronously. This makes the process look
> -	 * synchronous in fact.
> -	 * Theoretically, acomp supports users send multiple acomp requests in one
> -	 * acomp instance, then get those requests done simultaneously. but in this
> -	 * case, zswap actually does store and load page by page, there is no
> -	 * existing method to send the second page before the first page is done
> -	 * in one thread doing zswap.
> -	 * but in different threads running on different cpu, we have different
> -	 * acomp instance, and compression and decompression use separate
> -	 * requests, so multiple threads can do (de)compression in parallel.
> +	 * For each output SG list in @acomp_ctx->req->sg_table->sgl,
> +	 * the @sg->length should be set to either the page's compressed
> +	 * length (success), or it's negative value compression error.
>  	 */
> -	comp_ret = crypto_wait_req(crypto_acomp_compress(comp_ctx->areq.req),
> -				   &comp_ctx->areq.wait);
> -	dlen = comp_ctx->areq.req->dlen;
> -	if (!comp_ret) {
> -		if (dlen == PAGE_SIZE)
> -			comp_ret = -ENOSPC;
> -		else if (WARN_ON_ONCE(!dlen || dlen > PAGE_SIZE))
> -			comp_ret = -EINVAL;
> +	for (comp_batch_iter = 0; comp_batch_iter < zc->nr_batch_pages;
> +	     ++comp_batch_iter) {
> +		batch_iter = batch_start + comp_batch_iter;
> +		dlen = zc->entries[batch_iter]->length;
> +		handle = zs_malloc(zc->zs_pool, dlen, gfp, zc->nid);
> +
> +		if (unlikely(IS_ERR_VALUE(handle))) {
> +			if (PTR_ERR((void *)handle) == -ENOSPC)
> +				zswap_reject_compress_poor++;
> +			else
> +				zswap_reject_alloc_fail++;
> +
> +			for (i = 0; i < comp_batch_iter; ++i)
> +				zs_free(zc->zs_pool, zc->entries[batch_start + i]->handle);
> +
> +			return false;
> +		}
> +
> +		zc->entries[batch_iter]->handle = handle;
> +
> +		if (likely(dlen < PAGE_SIZE)) {
> +			dst = zc->comp_ctx->buffers[comp_batch_iter];
> +			zs_obj_write(zc->zs_pool, handle, dst, dlen);
> +		} else {
> +			dst = kmap_local_folio(zc->folio,
> +					(zc->folio_start + batch_iter) * PAGE_SIZE);
> +			zs_obj_write(zc->zs_pool, handle, dst, dlen);
> +			kunmap_local(dst);
> +		}
>  	}
>  
> +	return true;
> +}
> +
> +/*
> + * zswap_compress() batching implementation for sequential and batching
> + * compressors.
> + *
> + * Description:
> + * ============
> + * Compress multiple @nr_pages in @folio starting from the @folio_start index
> + * in batches of @nr_batch_pages; the latter representing the lesser of the
> + * @nr_pages to be batch compressed and the compressor's batch size.
> + *
> + * @nr_pages can be in (1, ZSWAP_MAX_BATCH_SIZE] even if the compressor does not
> + * support batching.
> + *
> + * If @nr_batch_pages is 1, each page is processed sequentially.
> + *
> + * If @nr_batch_pages is > 1, the compressor may use batching to optimize
> + * compression.
> + */
> +static bool zswap_compress(struct zswap_store_context *zc)
> +{
> +	u8 batch_start, comp_batch_iter, batch_iter;
> +	struct scatterlist *sg;
> +	bool ret;
> +	int err;
> +
>  	/*
> -	 * If a page cannot be compressed into a size smaller than PAGE_SIZE,
> -	 * save the content as is without a compression, to keep the LRU order
> -	 * of writebacks.  If writeback is disabled, reject the page since it
> -	 * only adds metadata overhead.  swap_writeout() will put the page back
> -	 * to the active LRU list in the case.
> +	 * Compress the @nr_pages in @folio starting at index @folio_start
> +	 * in batches of @nr_batch_pages.
>  	 */
> -	if (comp_ret) {
> -		if (!wb_enabled)
> -			goto unlock;
> -		comp_ret = 0;
> -		dlen = PAGE_SIZE;
> -		dst = kmap_local_folio(folio, index * PAGE_SIZE);
> -		mapped = true;
> -	}
> +	for (batch_start = 0; batch_start < zc->nr_pages;
> +	     batch_start += zc->nr_batch_pages) {
> +		/*
> +		 * Send @nr_batch_pages to crypto_acomp for compression:
> +		 *
> +		 * These pages are in @folio's range of indices in the interval
> +		 *     [@folio_start + @batch_start,
> +		 *      @folio_start + @batch_start + @nr_batch_pages).
> +		 *
> +		 * @slen indicates the total source length bytes for
> +		 * @nr_batch_pages.
> +		 *
> +		 * The source folio pages in the batch are directly submitted to
> +		 * crypto_acomp via acomp_request_set_src_folio().
> +		 *
> +		 * The pool's compressor batch size is at least @nr_batch_pages,
> +		 * hence the acomp_ctx has at least @nr_batch_pages dst @buffers.
> +		 */
> +		acomp_request_set_src_folio(zc->comp_ctx->areq.req, zc->folio,
> +				(zc->folio_start + batch_start) * PAGE_SIZE,
> +				zc->slen);
> +
> +		zc->comp_ctx->sg_table->sgl->length = zc->slen;
> +
> +		/*
> +		 * The per-CPU @zc->comp_ctx->sg_table scatterlists are statically
> +		 * mapped to the per-CPU dst @buffers at pool creation time.
> +		 */
> +		acomp_request_set_dst_sg(zc->comp_ctx->areq.req,
> +					 zc->comp_ctx->sg_table->sgl, zc->slen);

[Severity: High]
How are the destination scatterlist entry lengths in
zc->comp_ctx->sg_table->sgl maintained across batches when
zc->nr_batch_pages > 1?

In zswap_cpu_comp_prepare(), each acomp_ctx->comp.buffers[i] is allocated
with size PAGE_SIZE and mapped to sgl[i] via sg_set_buf(..., PAGE_SIZE).

Here in zswap_compress(), setting zc->comp_ctx->sg_table->sgl->length =
zc->slen sets sgl[0].length to zc->nr_batch_pages * PAGE_SIZE, which
exceeds the single PAGE_SIZE allocation backing buffers[0] when
zc->nr_batch_pages > 1.

At the same time, sgl[1 .. zc->nr_batch_pages - 1].length are never reset
to PAGE_SIZE before calling crypto_acomp_compress(). After the first batch
completes (or after a batch fails and jumps to compress_error), those SG
entries retain their previous compressed lengths or negative error codes on
subsequent requests.

> +
> +		err = crypto_wait_req(crypto_acomp_compress(zc->comp_ctx->areq.req),
> +				      &zc->comp_ctx->areq.wait);
>  
> -	gfp = GFP_NOWAIT | __GFP_NORETRY | __GFP_HIGHMEM | __GFP_MOVABLE;
> -	handle = zs_malloc(pool->zs_pool, dlen, gfp, folio_nid(folio));
> -	if (IS_ERR_VALUE(handle)) {
> -		alloc_ret = PTR_ERR((void *)handle);
> -		goto unlock;
> +		/*
> +		 * If a page cannot be compressed into a size smaller than
> +		 * PAGE_SIZE, save the content as is without a compression, to
> +		 * keep the LRU order of writebacks.  If writeback is disabled,
> +		 * reject the page since it only adds metadata overhead.
> +		 * swap_writeout() will put the page back to the active LRU list
> +		 * in the case.
> +		 *
> +		 * It is assumed that any compressor that sets the output length
> +		 * to 0 or a value >= PAGE_SIZE will also return a negative
> +		 * error status in @err; i.e, will not return a successful
> +		 * compression status in @err in this case.
> +		 */
> +		if (unlikely(err && !zc->wb_enabled))
> +			goto compress_error;
> +
> +		for_each_sg(zc->comp_ctx->sg_table->sgl, sg,
> +			    zc->nr_batch_pages, comp_batch_iter) {
> +			batch_iter = batch_start + comp_batch_iter;
> +			if (unlikely(sg->length <= 0 || sg->length >= PAGE_SIZE))
> +				sg->length = PAGE_SIZE;
> +
> +			zc->entries[batch_iter]->length = sg->length;
> +		}

[Severity: High]
What happens in zswap_compress() when using an asynchronous non-segmented
acomp driver?

In crypto/acompress.c:crypto_acomp_compress(), when req->unit_size ==
req->slen and !crypto_acomp_req_seg(tfm), the fast path runs:

	if (likely((req->unit_size == req->slen) && acomp_request_issg(req) &&
		    !crypto_acomp_req_seg(tfm) && !acomp_req_on_stack(req))) {
		int err = tfm->compress(req);

		req->dst->length = unlikely(err) ? err : req->dlen;
		return err;
	}

When tfm->compress(req) is asynchronous and returns -EINPROGRESS or -EBUSY,
req->dst->length (sg->length) is set to -EINPROGRESS before
crypto_wait_req() waits for completion, and nothing updates sg->length to
req->dlen when the request finishes.

Wouldn't zswap_compress() then see sg->length == (unsigned int)-EINPROGRESS
after crypto_wait_req() returns 0, clamp sg->length to PAGE_SIZE, and store
every page uncompressed instead of using req->dlen?

[Severity: Medium]
Also, is the assumption that compressors never return 0 when the output
length is PAGE_SIZE true for software compressors?

Software compressors such as lz4 and zstd return 0 with dlen == PAGE_SIZE
when a page compresses to exactly PAGE_SIZE bytes. Previously,
zswap_compress() checked:

	if (!comp_ret) {
		if (dlen == PAGE_SIZE)
			comp_ret = -ENOSPC;
		else if (WARN_ON_ONCE(!dlen || dlen > PAGE_SIZE))
			comp_ret = -EINVAL;
	}

Without that check, when err == 0 and sg->length == PAGE_SIZE with
!zc->wb_enabled, won't zswap_compress() and zswap_update_entries() allocate
a PAGE_SIZE object in zsmalloc and store the uncompressed page even though
writeback is disabled?

> +
> +		ret = zswap_update_entries(zc, batch_start);
> +		if (unlikely(!ret))
> +			return false;
>  	}

[ ... ]

> @@ -1681,11 +1785,10 @@ static bool zswap_store_pages(struct zswap_store_context *zc,
>  	return true;
>  
>  store_pages_failed:
> -	for (i = store_fail_idx; i < zc->nr_pages; ++i) {
> -		if (!IS_ERR_VALUE(zc->entries[i]->handle))
> -			zs_free(pool->zs_pool, zc->entries[i]->handle);
> -	}
> +	for (i = store_fail_idx; i < zc->nr_pages; ++i)
> +		zs_free(pool->zs_pool, zc->entries[i]->handle);
>  
> +compress_failed:
>  	zswap_entries_cache_free_batch(&zc->entries[store_fail_idx],
>  				       zc->nr_pages - store_fail_idx);
>  

[Severity: High]
Does this leak zsmalloc handles in zc->entries[0 .. batch_start - 1] when a
later sub-batch (batch_start > 0) fails in zswap_compress()?

For example, with a non-batching compressor (zc->nr_batch_pages == 1) and a
large folio (zc->nr_pages == 8), zswap_compress() iterates batch_start from
0 to zc->nr_pages - 1, calling zswap_update_entries(zc, batch_start) on
each iteration.

If sub-batch 0 succeeds (allocating zc->entries[0]->handle via zs_malloc())
and a later sub-batch at batch_start > 0 fails either in
crypto_acomp_compress() (with !zc->wb_enabled) or in zs_malloc() inside
zswap_update_entries(), zswap_update_entries() only frees handles in the
current sub-batch [batch_start, batch_start + comp_batch_iter):

	for (i = 0; i < comp_batch_iter; ++i)
		zs_free(zc->zs_pool, zc->entries[batch_start + i]->handle);

zswap_store_pages() then jumps to compress_failed, which skips the
zs_free() loop at store_pages_failed and frees zc->entries[], leaking all
zsmalloc handles allocated in [0, batch_start).

> @@ -1757,6 +1860,10 @@ bool zswap_store(struct folio *folio)
>  	store_batch_size = (pool->compr_batch_size > 1) ?
>  			    pool->compr_batch_size : ZSWAP_MAX_BATCH_SIZE;
>  
> +	zc.nr_batch_pages = pool->compr_batch_size;
> +	zc.slen = zc.nr_batch_pages * PAGE_SIZE;
> +	zc.zs_pool = pool->zs_pool;
> +
>  	for (start = 0; start < nr_pages; start += store_batch_size) {
>  		end = min(start + store_batch_size, nr_pages);
>  

[Severity: Critical]
What happens in zswap_store() -> zswap_store_pages() -> zswap_compress()
when pool->compr_batch_size > 1 and the folio (or trailing batch end -
start) has fewer pages than pool->compr_batch_size, such as an order-0
folio where nr_pages == 1?

In zswap_store(), zc.nr_batch_pages is set to pool->compr_batch_size and
zc.slen is set to zc.nr_batch_pages * PAGE_SIZE, while zswap_store_pages()
sets zc->nr_pages = end - start and only allocates zc->nr_pages entries in
zc->entries[0 .. zc->nr_pages - 1].

Because zc->nr_batch_pages and zc->slen are never clamped to zc->nr_pages,
when zc->nr_pages < zc->nr_batch_pages:

1. acomp_request_set_src_folio() in zswap_compress() maps zc->slen bytes,
   reading past the end of zc->folio.

2. Both the for_each_sg() loop in zswap_compress() and the loop in
   zswap_update_entries() iterate comp_batch_iter up to zc->nr_batch_pages,
   dereferencing zc->entries[batch_iter] for batch_iter >= zc->nr_pages. On
   the first batch, zc->entries[zc->nr_pages] is NULL (causing a NULL
   pointer dereference); on a trailing partial batch after a full batch, it
   contains stale zswap_entry pointers from the previous batch (corrupting
   live entries and leaking the newly allocated zsmalloc handles).

-- 
Sashiko AI review · https://sashiko.dev/#/patchset/20261028030503.17923-1-kanchanapsridhar2026@gmail.com?part=7


  reply	other threads:[~2026-10-29  3:06 UTC|newest]

Thread overview: 20+ messages / expand[flat|nested]  mbox.gz  Atom feed  top
2026-10-28  3:04 [PATCH v2 0/8] Batched zswap_store() framework with compression batching Kanchana P. Sridhar
2026-10-28  3:04 ` [PATCH v2 1/8] crypto: acomp - Define a unit_size in struct acomp_req to enable batching. mm: zswap: Set the unit size for zswap to PAGE_SIZE Kanchana P. Sridhar
2026-10-28  3:04 ` [PATCH v2 2/8] crypto: acomp - Add bit to indicate segmentation support Kanchana P. Sridhar
2026-10-28  3:04 ` [PATCH v2 3/8] crypto: acomp - Add trivial segmentation wrapper Kanchana P. Sridhar
2026-10-29  3:05   ` sashiko-bot
2026-10-29  5:09     ` Kanchana P. Sridhar
2026-10-29  5:38       ` Herbert Xu
2026-10-29  6:03         ` Kanchana P. Sridhar
2026-10-29  8:22           ` Herbert Xu
2026-10-28  3:04 ` [PATCH v2 4/8] crypto: acomp - Add API to get a compression algorithm's batch-size Kanchana P. Sridhar
2026-10-29  3:06   ` sashiko-bot
2026-10-29  5:49     ` Kanchana P. Sridhar
2026-10-28  3:05 ` [PATCH v2 5/8] mm: zswap: Store the folio's node id in the zswap_entry Kanchana P. Sridhar
2026-10-29  3:06   ` sashiko-bot
2026-10-28  3:05 ` [PATCH v2 6/8] mm: zswap: Store large folios in batches Kanchana P. Sridhar
2026-10-29  3:06   ` sashiko-bot
2026-10-28  3:05 ` [PATCH v2 7/8] mm: zswap: Batched zswap_compress() for storing " Kanchana P. Sridhar
2026-10-29  3:06   ` sashiko-bot [this message]
2026-10-28  3:05 ` [PATCH v2 8/8] mm: zswap: Refactor and optimize zswap_store_pages() batch operations Kanchana P. Sridhar
2026-10-29  3:06   ` sashiko-bot

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

  Avoid top-posting and favor interleaved quoting:
  https://en.wikipedia.org/wiki/Posting_style#Interleaved_style

* Reply using the --to, --cc, and --in-reply-to
  switches of git-send-email(1):

  git send-email \
    --in-reply-to=sashiko-outbox-188659@kernel.org \
    --to=sashiko-bot@kernel.org \
    --cc=akpm@linux-foundation.org \
    --cc=chengming.zhou@linux.dev \
    --cc=davem@davemloft.net \
    --cc=hannes@cmpxchg.org \
    --cc=herbert@gondor.apana.org.au \
    --cc=kanchanapsridhar2026@gmail.com \
    --cc=linux-crypto@vger.kernel.org \
    --cc=linux-mm@kvack.org \
    --cc=nphamcs@gmail.com \
    --cc=rientjes@google.com \
    --cc=sashiko-reviews@lists.linux.dev \
    --cc=senozhatsky@chromium.org \
    --cc=willy@infradead.org \
    --cc=yosry@kernel.org \
    /path/to/YOUR_REPLY

  https://kernel.org/pub/software/scm/git/docs/git-send-email.html

* If your mail client supports setting the In-Reply-To header
  via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line before the message body.
This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox