From: "Vlastimil Babka (SUSE)" <vbabka@kernel.org>
To: Harry Yoo <harry@kernel.org>,
Nickolai Zeldovich <nickolai@csail.mit.edu>
Cc: Andrew Morton <akpm@linux-foundation.org>,
Hao Li <hao.li@linux.dev>, Christoph Lameter <cl@gentwo.org>,
David Rientjes <rientjes@google.com>,
Roman Gushchin <roman.gushchin@linux.dev>,
linux-mm@kvack.org, linux-kernel@vger.kernel.org,
Christoph Hellwig <hch@lst.de>,
Vitaly Wool <vitaly.wool@konsulko.se>,
Vitaly Vul <vitaly.vul@partner.samsung.com>
Subject: Re: [PATCH v1] mempool: fix min_nr exceeding the elements array after concurrent resizes
Date: Tue, 20 Oct 2026 10:42:53 +0200 [thread overview]
Message-ID: <de86277d-ed7e-4208-a3c2-6d26a8278d0c@kernel.org> (raw)
In-Reply-To: <atJBK6hWYh2AqXwK@thinkstation>
On 10/16/26 17:46, Harry Yoo wrote:
> [+Cc Christoph Hellwig and Vitaly Wool ]
>
> On Fri, Oct 16, 2026 at 09:25:10AM -0400, Nickolai Zeldovich wrote:
>> mempool_resize() shrinks a pool by dropping pool->lock around every
>> free_fn() call and, once the pool holds no more than new_min_nr
>> elements, stores new_min_nr into pool->min_nr. new_min_nr was only
>> compared against pool->min_nr before the lock was first dropped, so that
>> store assumes nothing changed meanwhile. The kernel-doc allows
>> mempool_alloc()/mempool_free() to run while a resize is in progress and
>> the grow path also copes with concurrent resizes ("Raced, other resize
>> will do our work"), but the shrink path does not: while its lock is
>> dropped another resize may shrink the pool below new_min_nr and a third
>> one may grow it again to a size between the two, replacing
>> pool->elements with an array of exactly that size. Storing the stale
>> new_min_nr afterwards leaves pool->min_nr larger than the elements
>> array.
>>
>> Interleaving on a pool with min_nr = curr_nr = 10:
>>
>> CPU A: mempool_resize(pool, 8)
>> takes the lock, 8 <= 10 so it shrinks: remove_element(), drops the
>> lock, calls free_fn() and is preempted there
>> CPU B: mempool_resize(pool, 3)
>> completes: curr_nr = 3, min_nr = 3, elements still has 10 slots
>> CPU B: mempool_resize(pool, 5)
>> completes: elements = kmalloc_objs(5), min_nr = 5, curr_nr = 5
>> CPU A: retakes the lock, 8 < curr_nr (5) is false, stores min_nr = 8
>> while elements has 5 slots
>>
>> Every later mempool_free() sees curr_nr < min_nr and add_element()
>> stores the returned element at elements[5], [6] and [7], past the end
>> of the 5-slot kmalloc allocation: a heap out-of-bounds write, i.e. slab
>> corruption, a KASAN report, or a crash in whichever unrelated object
>> sits next to the array. The invariant that breaks is that pool->min_nr
>> never exceeds the size of pool->elements; every other path that changes
>> min_nr or elements keeps it.
Instead of reading all this, it was simpler for me to check what the code does.
>> An in-tree caller can hit this: fs/smb/client resizes the global
>> cifs_req_poolp from cifs_demultiplex_thread() (grow, when a TCP session
>> is created) and from clean_demultiplex_info() (shrink, when one goes
>> away). The two are coordinated only through the atomic
>> tcpSesAllocCount, so mounts and unmounts racing on one client run the
>> resizes above concurrently; pool->free is kmem_cache_free() there and
>> the window is a preemption inside it. The other callers
>> (block/blk-zoned.c, drivers/s390/scsi/zfcp_erp.c) serialise their
>> resizes.
This is useful to know, but maybe could be also less dense.
> The bug looks valid in theory, not sure if how easily it can be
> reproduced in practice as resizing should be quite rare.
> (A bug is a bug and should be fixed, but no need to hurry in this case)
Ack.
>> The shrink path has stored new_min_nr unconditionally since
>> mempool_resize() was introduced, while the grow path has always
>> re-checked pool->min_nr after reacquiring the lock.
>
> So it's Fixes: 1da177e4c3f4 ("Linux-2.6.12-rc2") ?
>
> https://git.kernel.org/pub/scm/linux/kernel/git/tglx/history.git/commit/?id=6892188b54d484098b79b0c9813c20e543124e2d
>
> ...from a historical commit before git era, 1da177e4c3f4.
>
>> Re-check new_min_nr against pool->min_nr under the lock before storing
>> it. If a concurrent resize has shrunk the pool below new_min_nr in the
>> meantime, keep that resize's min_nr (and whatever array it installed),
>> as the grow path already does; otherwise the store is unchanged. The
>> elements the loop has already freed stay freed, which is right either
>> way because the pool's limit is now below new_min_nr.
Again too verbose IMHO.
>> Found by formal verification of mm/mempool.c against the kernel binary.
>> Reproduced with a userspace build of the affected functions in which the
>> free_fn() callback drives the interleaving above: before the fix min_nr
>> ends at 8 with a 5-slot array and the next mempool_free() writes past it
>> (AddressSanitizer heap-buffer-overflow); after the fix min_nr stays at 5
>> and the element is handed to free_fn(). Not reproduced on a running
>> kernel, where the window is a single preemption inside free_fn().
Alright.
>> Assisted-by: LLM
>> Signed-off-by: Nickolai Zeldovich <nickolai@csail.mit.edu>
>> ---
>>
>> mm/mempool.c | 4 +++-
>> 1 file changed, 3 insertions(+), 1 deletion(-)
>>
>> diff --git a/mm/mempool.c b/mm/mempool.c
>> index cb74e718b2c6..95533e2975ce 100644
>> --- a/mm/mempool.c
>> +++ b/mm/mempool.c
>> @@ -378,7 +378,9 @@ int mempool_resize(struct mempool *pool, int new_min_nr)
>> pool->free(element, pool->pool_data);
>> spin_lock_irqsave(&pool->lock, flags);
>> }
>> - pool->min_nr = new_min_nr;
>> + /* Raced, the array may now be smaller than new_min_nr */
>> + if (new_min_nr <= pool->min_nr)
There could be just '<' ?
The outer if uses <= but the == case only leads to an early goto out_unlock.
>> + pool->min_nr = new_min_nr;
>> goto out_unlock;
>> }
>> spin_unlock_irqrestore(&pool->lock, flags);
>>
>> base-commit: 90201351a862182bdabfc1ad1c97b1e1b6ed5931
>> --
>> 2.53.0
>>
>
prev parent reply other threads:[~2026-10-20 8:42 UTC|newest]
Thread overview: 11+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-10-16 13:25 Nickolai Zeldovich
2026-10-16 15:46 ` Harry Yoo
2026-10-16 16:33 ` Nickolai Zeldovich
2026-10-18 15:01 ` Vitaly Wool
2026-10-18 15:15 ` Harry Yoo
2026-10-18 16:03 ` Vitaly Wool
2026-10-18 16:15 ` Harry Yoo
2026-10-19 6:45 ` Vitaly Wool
2026-10-19 13:27 ` Harry Yoo
2026-10-20 8:27 ` Vlastimil Babka (SUSE)
2026-10-20 8:42 ` Vlastimil Babka (SUSE) [this message]
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=de86277d-ed7e-4208-a3c2-6d26a8278d0c@kernel.org \
--to=vbabka@kernel.org \
--cc=akpm@linux-foundation.org \
--cc=cl@gentwo.org \
--cc=hao.li@linux.dev \
--cc=harry@kernel.org \
--cc=hch@lst.de \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-mm@kvack.org \
--cc=nickolai@csail.mit.edu \
--cc=rientjes@google.com \
--cc=roman.gushchin@linux.dev \
--cc=vitaly.vul@partner.samsung.com \
--cc=vitaly.wool@konsulko.se \
/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
all inboxes | Powered by JetHome®