From: Jijie Shao <shaojijie@huawei.com>
To: Mina Almasry <almasrymina@google.com>
Cc: <shaojijie@huawei.com>, <davem@davemloft.net>,
<edumazet@google.com>, <kuba@kernel.org>, <pabeni@redhat.com>,
<andrew+netdev@lunn.ch>, <horms@kernel.org>, <hawk@kernel.org>,
<ilias.apalodimas@linaro.org>, <toke@redhat.com>,
<shenjian15@huawei.com>, <liuyonglong@huawei.com>,
<chenhao418@huawei.com>, <yangshuaisong@h-partners.com>,
<netdev@vger.kernel.org>, <linux-kernel@vger.kernel.org>
Subject: Re: [PATCH net] net: page_pool: fix DMA index not cleared on xa_cmpxchg race
Date: Sat, 25 Jul 2026 10:48:03 +0800 [thread overview]
Message-ID: <f708da1d-3ce1-469d-b63a-ae2d6f331a24@huawei.com> (raw)
In-Reply-To: <CAHS8izPTQ5rOXs5rfVc3tnWcaeQ8wJ1qJqSXEfboe0OTGNkLkA@mail.gmail.com>
on 2026/7/25 2:33, Mina Almasry wrote:
> On Fri, Jul 24, 2026 at 2:22 AM Jijie Shao <shaojijie@huawei.com> wrote:
>> page_pool_release_dma_index() uses xa_cmpxchg() to atomically remove a
>> page from pool->dma_mapped. When the cmpxchg loses the race (the entry
>> was already removed by a concurrent release path, e.g. page_pool_scrub()
>> during page_pool_destroy()), the function returns -1 *without* clearing
>> the DMA index bits stored in pp_magic. Its caller then skips dma_unmap
>> based on that -1, which is correct, but the stale DMA index bits left
>> in pp_magic are wrong: the xarray entry they index no longer exists.
>> A page recycled back into the pool then carries a dangling DMA index,
>> and kernels that validate pp_magic in the return path surface this as
>> a WARN.
>>
>> Move netmem_set_dma_index(netmem, 0) to execute unconditionally before
>> return, so both the winner and the loser of the xa_cmpxchg race clear
>> the DMA index bits. The return value still distinguishes the two
>> cases (-1 loser / 0 winner), preserving the contract that exactly one
>> side performs dma_unmap.
>>
>> The race window opens when page_pool_destroy() runs concurrently with
>> late page returns from SKBs held in per-CPU defer lists, TCP receive
>> queues or GRO hashes -- e.g. a driver reconfiguring channels/ring
>> depth while traffic is flowing.
>>
>> Observed on arm64 (7.2.0-rc1) with the hns3 driver:
>>
>> WARNING: net/core/netmem_priv.h:18 at page_pool_clear_pp_info+0x20/0x38,
>> CPU#83: iperf/3874878
>>
>> Return path (CPU_B, process context, triggering the WARN). The page
>> was held in a per-CPU SKB defer list and is being released through
>> tcp_recvmsg():
>>
>> page_pool_clear_pp_info+0x20/0x38
>> page_pool_put_unrefed_netmem+0x11c/0x2e8
>> napi_pp_put_page+0xf0/0x120
>> skb_release_data+0x170/0x228
>> skb_attempt_defer_free+0x7c/0x1f0
>> tcp_recvmsg_locked+0x710/0x9a0
>> tcp_recvmsg+0x74/0x1c8
>> inet_recvmsg+0x2c/0xf0
>> __sys_recvfrom+0xdc/0x198
>> __arm64_sys_recvfrom+0x2c/0x48
>> invoke_syscall+0x5c/0x120
>> el0_svc_common.constprop.0+0xc8/0xf0
>> do_el0_svc+0x24/0x38
>> el0_svc+0x34/0x1e0
>> el0t_64_sync_handler+0xa0/0xe8
>> el0t_64_sync+0x1ac/0x1b0
>>
>> Scrub path (CPU_A, racing with the return path, derived from code):
>>
>> page_pool_destroy
>> -> page_pool_scrub
>> -> xa_for_each(dma_mapped)
>> -> __page_pool_release_netmem_dma
>> -> page_pool_release_dma_index <- race window
>>
>> Debug approach and evidence:
>> Reproduced with a stability test that repeatedly reconfigures the hns3
>> channel count and ring depth (ethtool -L/-G) while running iperf3 with
>> multiple parallel streams, then closes the iperf3 sockets to release
>> the SKBs held in TCP receive queues and per-CPU defer lists.
>>
>> On the unmodified kernel the WARN reproduces after hours to tens of
>> hours. To make the race window observable, a debugfs knob injects a
>> controlled udelay() right after the winning xa_cmpxchg() in
>> page_pool_release_dma_index(), widening the gap between the xarray
>> entry removal and the pp_magic cleanup. With the delay injected the
>> WARN reproduces 4-5 times within about half an hour.
>>
>> Instrumentation added to page_pool_release_dma_index() logs the per-
>> page pp_magic, DMA index, refcount and dma_addr at release time. A
>> representative log line from a WARN-triggering page:
>>
>> PP_DMA_IDX: magic=0xdead0000000cbec0 idx=6525 refcnt=1 dma_addr=0x0
>>
>> magic=0xdead0000000cbec0 -> PP_SIGNATURE | (6525 << shift)
>> idx=6525 -> DMA index bits still set in pp_magic
>> dma_addr=0x0 -> scrub already did dma_unmap + cleared addr
>> refcnt=1 -> inflight page, unrefed release path
>>
>> The combination "dma_addr == 0 but DMA index != 0" is the signature of
>> the race: CPU_A (scrub) won the xa_cmpxchg, performed dma_unmap and
>> cleared dma_addr; CPU_B (return) lost the cmpxchg, returned -1 and
>> skipped netmem_set_dma_index(0), leaving the DMA index bits stale --
>> exactly the gap this fix closes.
>>
>> With this fix applied the WARN no longer triggers under the same
>> workload (verified overnight), both with and without the debugfs
>> delay knob enabled.
>>
>> Fixes: 95920c2ed02b ("page_pool: Fix PP_MAGIC_MASK to avoid crashing on some 32-bit arches")
>> Fixes: ee62ce7a1d90 ("page_pool: Track DMA-mapped pages and unmap them when destroying the pool")
>> Assisted-by: OhMyOpenCode:GLM-5.2
>> Signed-off-by: Jijie Shao <shaojijie@huawei.com>
> If at all possible, when generating fixes via LLMs, spend time
> reviewing the patch yourself before sending to make sure the patch
> makes sense. In this case, I was taken back that this 10+, 3- fix came
> with such a huge huge commit message.
Thanks for the review, Mina. Fair point on the commit message length — this
was my first time digging into page_pool internals and the message ended up
documenting the whole investigation path. I first saw the call trace on
6.17-rc4 and spent a long time chasing the hns3 driver before realizing the
issue was in page_pool itself. The fix was LLM-suggested and I did run it,
but only under iperf — which, as you note, is not enough to surface the
UAF (the freed page isn't reused fast enough for the stale write to land on
a different subsystem's memory, so "no crash / no WARN" lulled me into
thinking the fix was safe). I should have reasoned about the refcount
contract from the code rather than relying on workload symptoms. I'll trim
the commit message heavily in v2.
>
> I did not review my self but my LLM - which does not tire in reading
> the long commit message - came up with this review. It looks like it
> may be correct. I think the fact that we do not touch the netmem at
> all if we lose the race is intentional, because the netmem may not be
> allocated:
>
> ```
> While this fixes the WARN_ON_ONCE, it introduces a critical
> Use-After-Free (UAF) and memory corruption bug in the scrub path.
>
> page_pool_scrub() iterates over pool->dma_mapped without holding a
> reference to the page. If the scrub path races with the normal unref
> path and wins xa_cmpxchg(), the unref path will proceed to free the
> page.
>
> If the scrub path is preempted immediately after xa_cmpxchg(), the
> page will be freed before the scrub path can continue. When the scrub
> path resumes, it will execute your new netmem_set_dma_index(netmem,
> 0), read the dma_addr,
> and clear it—all on a freed page that may now belong to another
> subsystem. This will corrupt memory and unmap garbage DMA addresses.
You're right. I verified in the source:
- page_pool_scrub() walks pool->dma_mapped via xa_for_each() with no
get_page on the entries (net/core/page_pool.c:1178).
- page_pool_return_netmem() calls put_page() after
__page_pool_release_netmem_dma() returns (net/core/page_pool.c:778),
so the unref path can free the page once refcount hits 0.
So "loser doesn't touch netmem" is indeed the intended contract, and v1
breaking it on the scrub-loser path is a real UAF.
One clarification on the scope of the LLM review's UAF claim: the scrub
**winner** UAF it describes is pre-existing, not introduced by v1. In
__page_pool_release_netmem_dma() (unchanged by v1), the winner path does:
if (page_pool_release_dma_index(pool, netmem)) // cmpxchg
return;
dma = page_pool_get_dma_addr_netmem(netmem); // READ netmem->dma_addr
dma_unmap_page_attrs(..., dma, ...);
page_pool_set_dma_addr_netmem(netmem, 0); // WRITE netmem->dma_addr
These three netmem dereferences happen after the cmpxchg wins, with no page
ref held. If the unref loser races to put_page() in between, the scrub winner
reads/writes a freed page. v1 doesn't change the winner path; it only adds a
new netmem_set_dma_index() write on the **loser** path, which is the new UAF
v1 introduces.
>
> To safely fix this:
>
> 1. __page_pool_release_netmem_dma() must cache dma_addr to a local
> variable before calling xa_cmpxchg().
> 2. The scrub path (winner) must not touch netmem after a successful
> xa_cmpxchg().
> 3. The unref path (loser), which safely holds the page reference,
> should handle clearing the DMA index and dma_addr to satisfy the WARN,
> regardless of the race outcome.
For v2 I'll follow your direction:
1. Cache dma_addr to a local variable before xa_cmpxchg() in
__page_pool_release_netmem_dma().
2. Scrub path (winner or loser) does not touch netmem fields after
cmpxchg; it only does dma_unmap on the cached address.
3. Unref path, which holds the page ref, clears dma_addr and DMA index
bits regardless of the cmpxchg outcome.
This fixes both the v1-introduced loser UAF and the pre-existing winner UAF.
Will send v2 soon.
Thanks,
Jijie Shao
prev parent reply other threads:[~2026-07-25 2:48 UTC|newest]
Thread overview: 3+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-07-24 9:21 Jijie Shao
2026-07-24 18:33 ` Mina Almasry
2026-07-25 2:48 ` Jijie Shao [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=f708da1d-3ce1-469d-b63a-ae2d6f331a24@huawei.com \
--to=shaojijie@huawei.com \
--cc=almasrymina@google.com \
--cc=andrew+netdev@lunn.ch \
--cc=chenhao418@huawei.com \
--cc=davem@davemloft.net \
--cc=edumazet@google.com \
--cc=hawk@kernel.org \
--cc=horms@kernel.org \
--cc=ilias.apalodimas@linaro.org \
--cc=kuba@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=liuyonglong@huawei.com \
--cc=netdev@vger.kernel.org \
--cc=pabeni@redhat.com \
--cc=shenjian15@huawei.com \
--cc=toke@redhat.com \
--cc=yangshuaisong@h-partners.com \
/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®