From: Jiayuan Chen <jiayuan.chen@linux.dev>
To: bpf@vger.kernel.org
Cc: Jiayuan Chen <jiayuan.chen@linux.dev>,
Emil Tsalapatis <emil@etsalapatis.com>,
Alexei Starovoitov <ast@kernel.org>,
Daniel Borkmann <daniel@iogearbox.net>,
Andrii Nakryiko <andrii@kernel.org>,
Eduard Zingerman <eddyz87@gmail.com>,
Kumar Kartikeya Dwivedi <memxor@gmail.com>,
Martin KaFai Lau <martin.lau@linux.dev>,
Song Liu <song@kernel.org>,
Yonghong Song <yonghong.song@linux.dev>,
Jiri Olsa <jolsa@kernel.org>,
Ihor Solodrai <ihor.solodrai@linux.dev>,
John Fastabend <john.fastabend@gmail.com>,
Shuah Khan <shuah@kernel.org>,
linux-kernel@vger.kernel.org, linux-kselftest@vger.kernel.org
Subject: [PATCH bpf-next v7 2/4] bpf: arena: allocate the fault-in page outside the lock
Date: Thu, 17 Sep 2026 09:55:07 +0800 [thread overview]
Message-ID: <20260917015618.7488-3-jiayuan.chen@linux.dev> (raw)
In-Reply-To: <20260917015618.7488-1-jiayuan.chen@linux.dev>
arena_vm_fault() allocated the page while holding arena->spinlock, so it
could only use the non-blocking allocator, which never reclaims. Once the
memcg is at memory.max that allocation just fails, the fault turns into
VM_FAULT_SIGSEGV, and the process gets a SIGSEGV on a perfectly valid
arena address. Hitting memory.max is routine (e.g. page cache from
reading a big file), so this kills innocent processes over memory that
reclaim could have freed.
Rework the fault handler:
- Preallocate the page before taking the lock, like do_anonymous_page()
does, so it can sleep and reclaim, instead of turning a routine
memory.max into a fake segfault. The allocation never invokes the OOM
killer, see bpf_map_alloc_page_sleepable().
- On allocation failure fall through to the locked recheck rather than
failing right away: a page a concurrent allocator installed meanwhile
is used, otherwise the non-blocking fallback fails and we return
VM_FAULT_SIGBUS. Not VM_FAULT_OOM: nothing ran the OOM killer, and the
fault path would just retry it forever.
- A lockless probe skips that preallocation when a page is already mapped
(e.g. allocated by the bpf program), so the common case wastes no
allocation. The rare race where such a page is freed before we take the
lock falls back to the non-blocking allocator under the lock.
- Return VM_FAULT_SIGBUS for the other non-recoverable errors (lock
failure, range-tree and page-table failures) instead of
VM_FAULT_SIGSEGV; only BPF_F_SEGV_ON_FAULT, and a scratch-page hole
under that flag, is a real user addressing error and keeps
VM_FAULT_SIGSEGV.
- Tidy up the error labels.
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
kernel/bpf/arena.c | 86 +++++++++++++++++++++++++++++++++++-----------
1 file changed, 66 insertions(+), 20 deletions(-)
diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
index 7b6847200b431..e3c8b6086bbee 100644
--- a/kernel/bpf/arena.c
+++ b/kernel/bpf/arena.c
@@ -481,7 +481,8 @@ static vm_fault_t arena_vm_fault(struct vm_fault *vmf)
struct bpf_map *map = vmf->vma->vm_file->private_data;
struct bpf_arena *arena = container_of(map, struct bpf_arena, map);
struct mem_cgroup *new_memcg, *old_memcg;
- struct page *page;
+ struct page *page, *new_page = NULL;
+ vm_fault_t fault_ret;
long kbase, kaddr;
unsigned long flags;
int ret;
@@ -489,59 +490,104 @@ static vm_fault_t arena_vm_fault(struct vm_fault *vmf)
kbase = bpf_arena_get_kern_vm_start(arena);
kaddr = kbase + (u32)(vmf->address);
- if (raw_res_spin_lock_irqsave(&arena->spinlock, flags))
+ page = vmalloc_to_page((void *)kaddr);
+ if (!page && !(arena->map.map_flags & BPF_F_SEGV_ON_FAULT)) {
+ /*
+ * Preallocate outside the lock with a sleepable allocator so it
+ * can reclaim, which the non-blocking allocator under
+ * arena->spinlock cannot; it never invokes the OOM killer, see
+ * bpf_map_alloc_page_sleepable(). On failure fall through
+ * anyway: the locked recheck below picks up a page a concurrent
+ * allocator may have installed meanwhile, and otherwise the
+ * non-blocking fallback fails and we return VM_FAULT_SIGBUS.
+ * Not VM_FAULT_OOM: nothing ran the OOM killer, and the fault
+ * path would just retry it forever.
+ */
+ bpf_map_memcg_enter(&arena->map, &old_memcg, &new_memcg);
+ new_page = bpf_map_alloc_page_sleepable(map);
+ bpf_map_memcg_exit(old_memcg, new_memcg);
+ }
+
+ if (raw_res_spin_lock_irqsave(&arena->spinlock, flags)) {
/*
* A failed lock means a possible deadlock was detected. Don't
* return VM_FAULT_RETRY: this handler never took mmap_lock, but
* the fault path would re-take it on retry and deadlock. Fail.
*/
+ if (new_page)
+ free_pages_nolock(new_page, 0);
return VM_FAULT_SIGBUS;
+ }
page = vmalloc_to_page((void *)kaddr);
if (page) {
- if (page == arena->scratch_page)
+ if (page == arena->scratch_page) {
/* BPF triggered scratch here; don't lazy-alloc over it */
- goto out_sigsegv;
+ fault_ret = (arena->map.map_flags & BPF_F_SEGV_ON_FAULT) ?
+ VM_FAULT_SIGSEGV : VM_FAULT_SIGBUS;
+ goto out_err_locked;
+ }
/* already have a page vmap-ed */
goto out;
}
+ if (arena->map.map_flags & BPF_F_SEGV_ON_FAULT) {
+ /* User space requested to segfault when page is not allocated by bpf prog */
+ fault_ret = VM_FAULT_SIGSEGV;
+ goto out_err_locked;
+ }
+
bpf_map_memcg_enter(&arena->map, &old_memcg, &new_memcg);
- if (arena->map.map_flags & BPF_F_SEGV_ON_FAULT)
- /* User space requested to segfault when page is not allocated by bpf prog */
- goto out_sigsegv_memcg;
+ if (!new_page) {
+ /*
+ * No page in hand: either the lockless probe saw a page (a bpf
+ * program page or the scratch page) so we skipped preallocation
+ * but a concurrent bpf_arena_free_pages() cleared it before we
+ * took the lock, or the sleepable preallocation failed. We need
+ * one now and cannot sleep here, so try the non-blocking
+ * allocator and give up if it fails.
+ */
+ ret = bpf_map_alloc_pages(map, map->numa_node, 1, &new_page);
+ if (ret) {
+ fault_ret = VM_FAULT_SIGBUS;
+ goto out_err_locked_memcg;
+ }
+ }
ret = range_tree_clear(&arena->rt, vmf->pgoff, 1);
- if (ret)
- goto out_sigsegv_memcg;
-
- struct apply_range_data data = { .arena = arena, .pages = &page, .i = 0 };
- /* Account into memcg of the process that created bpf_arena */
- ret = bpf_map_alloc_pages(map, NUMA_NO_NODE, 1, &page);
if (ret) {
- range_tree_set(&arena->rt, vmf->pgoff, 1);
- goto out_sigsegv_memcg;
+ fault_ret = VM_FAULT_SIGBUS;
+ goto out_err_locked_memcg;
}
+ struct apply_range_data data = { .arena = arena, .pages = &new_page, .i = 0 };
ret = apply_to_page_range(&init_mm, kaddr, PAGE_SIZE, apply_range_set_cb, &data);
if (ret) {
range_tree_set(&arena->rt, vmf->pgoff, 1);
- free_pages_nolock(page, 0);
- goto out_sigsegv_memcg;
+ fault_ret = VM_FAULT_SIGBUS;
+ goto out_err_locked_memcg;
}
flush_vmap_cache(kaddr, PAGE_SIZE);
bpf_map_memcg_exit(old_memcg, new_memcg);
+ /* new_page was consumed */
+ page = new_page;
+ new_page = NULL;
out:
page_ref_add(page, 1);
raw_res_spin_unlock_irqrestore(&arena->spinlock, flags);
+ if (new_page)
+ free_pages_nolock(new_page, 0);
vmf->page = page;
return 0;
-out_sigsegv_memcg:
+
+out_err_locked_memcg:
bpf_map_memcg_exit(old_memcg, new_memcg);
-out_sigsegv:
+out_err_locked:
raw_res_spin_unlock_irqrestore(&arena->spinlock, flags);
- return VM_FAULT_SIGSEGV;
+ if (new_page)
+ free_pages_nolock(new_page, 0);
+ return fault_ret;
}
static const struct vm_operations_struct arena_vm_ops = {
--
2.43.0
next prev parent reply other threads:[~2026-09-17 1:58 UTC|newest]
Thread overview: 6+ messages / expand[flat|nested] mbox.gz Atom feed top
2026-09-17 1:55 [PATCH bpf-next v7 0/4] bpf: arena: handle memory.max on fault-in with reclaim/OOM Jiayuan Chen
2026-09-17 1:55 ` [PATCH bpf-next v7 1/4] bpf: Add a sleepable page allocator for map memory Jiayuan Chen
2026-09-17 2:17 ` Alexei Starovoitov
2026-09-17 1:55 ` Jiayuan Chen [this message]
2026-09-17 1:55 ` [PATCH bpf-next v7 3/4] selftests/bpf: Add read_cgroup_file() to cgroup_helpers Jiayuan Chen
2026-09-17 1:55 ` [PATCH bpf-next v7 4/4] selftests/bpf: Add a test for arena fault-in under memory.max Jiayuan Chen
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=20260917015618.7488-3-jiayuan.chen@linux.dev \
--to=jiayuan.chen@linux.dev \
--cc=andrii@kernel.org \
--cc=ast@kernel.org \
--cc=bpf@vger.kernel.org \
--cc=daniel@iogearbox.net \
--cc=eddyz87@gmail.com \
--cc=emil@etsalapatis.com \
--cc=ihor.solodrai@linux.dev \
--cc=john.fastabend@gmail.com \
--cc=jolsa@kernel.org \
--cc=linux-kernel@vger.kernel.org \
--cc=linux-kselftest@vger.kernel.org \
--cc=martin.lau@linux.dev \
--cc=memxor@gmail.com \
--cc=shuah@kernel.org \
--cc=song@kernel.org \
--cc=yonghong.song@linux.dev \
/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 an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.