* [PATCH bpf-next v8 0/3] bpf: arena: handle memory.max on fault-in with reclaim
@ 2026-09-18 8:40 Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock Jiayuan Chen
` (2 more replies)
0 siblings, 3 replies; 6+ messages in thread
From: Jiayuan Chen @ 2026-09-18 8:40 UTC (permalink / raw)
To: bpf
Cc: Jiayuan Chen, Alexei Starovoitov, Daniel Borkmann,
Andrii Nakryiko, Eduard Zingerman, Kumar Kartikeya Dwivedi,
Martin KaFai Lau, Song Liu, Yonghong Song, Jiri Olsa,
Emil Tsalapatis, Ihor Solodrai, Shuah Khan, linux-kernel,
linux-kselftest
Since commit e66fe1bc6d25 ("bpf: arena: Reintroduce memcg accounting"),
arena pages are charged to the memcg of the process that created the arena.
That exposes a problem in the arena user page fault path: the fault-in
allocation runs under arena->spinlock, so it can only use the non-blocking
allocator, which never reclaims. Once memory.current is at memory.max the
allocation simply fails, even when the memcg is full of page cache that
could be dropped right away. Reaching memory.max is completely normal for a
healthy application - e.g. reading a large file fills memory.current with
page cache - and the process then gets SIGSEGV on a perfectly valid arena
address.
Preallocate the page outside the lock (patch 1), the way do_anonymous_page()
does, so the allocation can sleep and reclaim. It never invokes the OOM
killer: the page is charged to the map's memcg, which need not be the
faulting task's, so an OOM there could kill unrelated tasks in the map's
cgroup. On a genuine failure the fault returns VM_FAULT_SIGBUS. patch 2&3
adds a selftest that fills a memcg with reclaimable page cache and faults an
arena in under memory.max: without the fix the child gets SIGSEGV on a valid
address, with it the fault-in reclaims and succeeds.
v7 -> v8:
- Merge patch 1 into patch 2. (Alexei Starovoitov)
v4 -> v7:
- Simplify the implementation: do not aim for OOM anymore. An arena is
shared between processes and can be shared across cgroups, so the OOM
killer is the wrong tool here - it would act on the map's memcg, which
need not be the faulting task's. Only try to reclaim now, via
__GFP_RETRY_MAYFAIL. Everything else is kept as before.
- selftest: changed accordingly - fill the memcg with reclaimable page
cache and check that the arena fault-in succeeds by reclaiming it,
instead of relying on an OOM kill.
v3 -> v4:
- rebase bpf-next and fix conflict
- add Reviewed-by tag from Emil Tsalapatis
v2 -> v3:
- selftest: check the memcg OOM via memory.events "oom_kill" instead of
the exit signal; it only aims to pass on the fixed kernel, since the
unfixed SIGSEGV is racy.
v1 -> v2:
- Rebase on the separate deadlock fix (found by the Sashiko AI review),
now applied to bpf-next.
- Honor the map's NUMA node on fault-in.
- Return VM_FAULT_SIGBUS for the non-recoverable faults (lock, range-tree
and page-table failures); a scratch-page hole stays VM_FAULT_SIGSEGV
only under BPF_F_SEGV_ON_FAULT. (Kumar Kartikeya Dwivedi)
- Add read_cgroup_file() to cgroup_helpers instead of open-coding the
/mnt/... path in the test. (Emil Tsalapatis)
- Dump the cgroup memory stats on test failure to ease debugging.
v7:
https://lore.kernel.org/bpf/20260917015618.7488-1-jiayuan.chen@linux.dev/#r
v4:
https://lore.kernel.org/bpf/20260821050250.35112-1-jiayuan.chen@linux.dev/T/#t
v2:
https://lore.kernel.org/bpf/20260805091720.139924-1-jiayuan.chen@linux.dev/
v1:
https://lore.kernel.org/bpf/20260727062521.376231-1-jiayuan.chen@linux.dev/
Jiayuan Chen (3):
bpf: arena: allocate the fault-in page outside the lock
selftests/bpf: Add read_cgroup_file() to cgroup_helpers
selftests/bpf: Add a test for arena fault-in under memory.max
kernel/bpf/arena.c | 90 ++++++--
tools/testing/selftests/bpf/cgroup_helpers.c | 67 ++++++
tools/testing/selftests/bpf/cgroup_helpers.h | 4 +
.../selftests/bpf/prog_tests/arena_memcg.c | 196 ++++++++++++++++++
.../testing/selftests/bpf/progs/arena_memcg.c | 23 ++
5 files changed, 360 insertions(+), 20 deletions(-)
create mode 100644 tools/testing/selftests/bpf/prog_tests/arena_memcg.c
create mode 100644 tools/testing/selftests/bpf/progs/arena_memcg.c
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock
2026-09-18 8:40 [PATCH bpf-next v8 0/3] bpf: arena: handle memory.max on fault-in with reclaim Jiayuan Chen
@ 2026-09-18 8:40 ` Jiayuan Chen
2026-09-18 9:51 ` bot+bpf-ci
2026-09-18 17:59 ` Alexei Starovoitov
2026-09-18 8:40 ` [PATCH bpf-next v8 2/3] selftests/bpf: Add read_cgroup_file() to cgroup_helpers Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 3/3] selftests/bpf: Add a test for arena fault-in under memory.max Jiayuan Chen
2 siblings, 2 replies; 6+ messages in thread
From: Jiayuan Chen @ 2026-09-18 8:40 UTC (permalink / raw)
To: bpf
Cc: Jiayuan Chen, Emil Tsalapatis, Alexei Starovoitov,
Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu,
Yonghong Song, Jiri Olsa, Ihor Solodrai, Shuah Khan,
linux-kernel, linux-kselftest
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 uses
__GFP_RETRY_MAYFAIL so it never invokes the OOM killer: the page is
charged to the map's memcg, which need not be the faulting task's, so
an OOM there could kill unrelated tasks in the map's cgroup while a
foreign faulter could never be its victim. Like the other bpf map
allocators it places the page on the map's numa_node rather than
following the task's mempolicy; arena memory is shared, so the map's
node is the right placement policy.
- 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>
---
To sashiko:
- The non-blocking fallback under the lock still zeroes:
alloc_pages_nolock() forces __GFP_ZERO internally and only accepts
__GFP_ACCOUNT.
- __GFP_ZERO matches the existing __bpf_alloc_page(), and no
arena-capable arch has D-cache aliasing, so there is no dcache concern.
---
kernel/bpf/arena.c | 90 +++++++++++++++++++++++++++++++++++-----------
1 file changed, 70 insertions(+), 20 deletions(-)
diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
index 7b6847200b431..2d0efc0232afd 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,108 @@ 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 so the allocation can sleep and
+ * reclaim, which the non-blocking allocator under
+ * arena->spinlock cannot. __GFP_RETRY_MAYFAIL: reclaim, but
+ * never OOM-kill in the map's memcg, which need not be the
+ * faulting task's. On failure, fallthrough: 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 = alloc_pages_node(map->numa_node,
+ GFP_KERNEL | __GFP_ZERO |
+ __GFP_ACCOUNT | __GFP_NOWARN |
+ __GFP_RETRY_MAYFAIL, 0);
+ 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
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH bpf-next v8 2/3] selftests/bpf: Add read_cgroup_file() to cgroup_helpers
2026-09-18 8:40 [PATCH bpf-next v8 0/3] bpf: arena: handle memory.max on fault-in with reclaim Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock Jiayuan Chen
@ 2026-09-18 8:40 ` Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 3/3] selftests/bpf: Add a test for arena fault-in under memory.max Jiayuan Chen
2 siblings, 0 replies; 6+ messages in thread
From: Jiayuan Chen @ 2026-09-18 8:40 UTC (permalink / raw)
To: bpf
Cc: Jiayuan Chen, Emil Tsalapatis, Alexei Starovoitov,
Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu,
Yonghong Song, Jiri Olsa, Ihor Solodrai, Shuah Khan,
linux-kernel, linux-kselftest
cgroup_helpers has write_cgroup_file()/write_cgroup_file_parent() but no
read counterpart. Add read_cgroup_file() and read_cgroup_file_parent() so
a forked child can read a cgroup file (e.g. memory.current) from the work
dir owned by the parent that set the environment up, without hand-building
the /mnt/... path.
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
tools/testing/selftests/bpf/cgroup_helpers.c | 67 ++++++++++++++++++++
tools/testing/selftests/bpf/cgroup_helpers.h | 4 ++
2 files changed, 71 insertions(+)
diff --git a/tools/testing/selftests/bpf/cgroup_helpers.c b/tools/testing/selftests/bpf/cgroup_helpers.c
index 45cd0b479fe35..4183ff6150c28 100644
--- a/tools/testing/selftests/bpf/cgroup_helpers.c
+++ b/tools/testing/selftests/bpf/cgroup_helpers.c
@@ -188,6 +188,73 @@ int write_cgroup_file_parent(const char *relative_path, const char *file,
return __write_cgroup_file(cgroup_path, file, buf);
}
+static int __read_cgroup_file(const char *cgroup_path, const char *file,
+ char *buf, size_t len)
+{
+ char file_path[PATH_MAX + 1];
+ ssize_t got;
+ int fd;
+
+ snprintf(file_path, sizeof(file_path), "%s/%s", cgroup_path, file);
+ fd = open(file_path, O_RDONLY);
+ if (fd < 0) {
+ log_err("Opening %s", file_path);
+ return 1;
+ }
+
+ got = read(fd, buf, len - 1);
+ if (got < 0) {
+ log_err("Reading %s", file_path);
+ close(fd);
+ return 1;
+ }
+ buf[got] = '\0';
+ close(fd);
+ return 0;
+}
+
+/**
+ * read_cgroup_file() - Read from a cgroup file
+ * @relative_path: The cgroup path, relative to the workdir
+ * @file: The name of the file in cgroupfs to read from
+ * @buf: Buffer to read into, NUL-terminated on success
+ * @len: Size of @buf
+ *
+ * Read from a file in the given cgroup's directory.
+ *
+ * If successful, 0 is returned.
+ */
+int read_cgroup_file(const char *relative_path, const char *file,
+ char *buf, size_t len)
+{
+ char cgroup_path[PATH_MAX - 24];
+
+ format_cgroup_path(cgroup_path, relative_path);
+ return __read_cgroup_file(cgroup_path, file, buf, len);
+}
+
+/**
+ * read_cgroup_file_parent() - Read from a cgroup file in the parent process
+ * workdir
+ * @relative_path: The cgroup path, relative to the parent process workdir
+ * @file: The name of the file in cgroupfs to read from
+ * @buf: Buffer to read into, NUL-terminated on success
+ * @len: Size of @buf
+ *
+ * Read from a file in the given cgroup's directory under the parent process
+ * workdir.
+ *
+ * If successful, 0 is returned.
+ */
+int read_cgroup_file_parent(const char *relative_path, const char *file,
+ char *buf, size_t len)
+{
+ char cgroup_path[PATH_MAX - 24];
+
+ format_parent_cgroup_path(cgroup_path, relative_path);
+ return __read_cgroup_file(cgroup_path, file, buf, len);
+}
+
/**
* setup_cgroup_environment() - Setup the cgroup environment
*
diff --git a/tools/testing/selftests/bpf/cgroup_helpers.h b/tools/testing/selftests/bpf/cgroup_helpers.h
index 3857304be8741..d42d2e13044e5 100644
--- a/tools/testing/selftests/bpf/cgroup_helpers.h
+++ b/tools/testing/selftests/bpf/cgroup_helpers.h
@@ -15,6 +15,10 @@ int write_cgroup_file(const char *relative_path, const char *file,
const char *buf);
int write_cgroup_file_parent(const char *relative_path, const char *file,
const char *buf);
+int read_cgroup_file(const char *relative_path, const char *file,
+ char *buf, size_t len);
+int read_cgroup_file_parent(const char *relative_path, const char *file,
+ char *buf, size_t len);
int cgroup_setup_and_join(const char *relative_path);
int get_root_cgroup(void);
int create_and_get_cgroup(const char *relative_path);
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* [PATCH bpf-next v8 3/3] selftests/bpf: Add a test for arena fault-in under memory.max
2026-09-18 8:40 [PATCH bpf-next v8 0/3] bpf: arena: handle memory.max on fault-in with reclaim Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 2/3] selftests/bpf: Add read_cgroup_file() to cgroup_helpers Jiayuan Chen
@ 2026-09-18 8:40 ` Jiayuan Chen
2 siblings, 0 replies; 6+ messages in thread
From: Jiayuan Chen @ 2026-09-18 8:40 UTC (permalink / raw)
To: bpf
Cc: Jiayuan Chen, Emil Tsalapatis, Alexei Starovoitov,
Daniel Borkmann, Andrii Nakryiko, Eduard Zingerman,
Kumar Kartikeya Dwivedi, Martin KaFai Lau, Song Liu,
Yonghong Song, Jiri Olsa, Ihor Solodrai, Shuah Khan,
linux-kernel, linux-kselftest
A child joins a memcg, fills it with 128M of clean page cache by reading
a sparse temp file (the way the cgroup selftests do), caps memory.max 8M
above its usage and then faults 64M of arena in, which only fits by
reclaiming that cache.
With the fix the arena fault-in reclaims, every fault succeeds and the
child exits 0. Without it the allocation cannot reclaim, fails once the
headroom is used up, and the child dies with SIGSEGV on a valid arena
address, so the test fails.
# test_progs -v -t arena_memcg
serial_test_arena_memcg:PASS:child faulted the arena in
#8 arena_memcg:OK
# without the fix
child killed by signal 11
serial_test_arena_memcg:FAIL:child faulted the arena in
The page cache must be reclaimable, so the temp file has to live on a
disk-backed filesystem, not tmpfs - the same assumption the cgroup
selftests make.
Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
---
To sashiko:
- The non-arm64 1<<44 map_extra is copied from the existing arena tests.
- waitpid() without an EINTR retry is copied from the existing tests.
- fork() then work without exec follows the existing tests (the
SIGEV_THREAD watchdog is test_progs-wide).
- Not skipping on EOPNOTSUPP (unsupported arena JIT) follows most of the
existing arena tests; only arena_direct_value/arena_spin_lock skip.
- cgroup.memory=nobpf would charge the arena pages to the root memcg, so
the limit would not bind and the test would pass either way; CI does
not configure it.
- The test SKIPs when the working directory cannot host the reserve: it
detects tmpfs (shmem pages are not reclaimable without swap) and a
filesystem without O_TMPFILE support. The cgroup selftests make the
same assumption about the working directory.
---
.../selftests/bpf/prog_tests/arena_memcg.c | 196 ++++++++++++++++++
.../testing/selftests/bpf/progs/arena_memcg.c | 23 ++
2 files changed, 219 insertions(+)
create mode 100644 tools/testing/selftests/bpf/prog_tests/arena_memcg.c
create mode 100644 tools/testing/selftests/bpf/progs/arena_memcg.c
diff --git a/tools/testing/selftests/bpf/prog_tests/arena_memcg.c b/tools/testing/selftests/bpf/prog_tests/arena_memcg.c
new file mode 100644
index 0000000000000..c76a7eb2f01d9
--- /dev/null
+++ b/tools/testing/selftests/bpf/prog_tests/arena_memcg.c
@@ -0,0 +1,196 @@
+// SPDX-License-Identifier: GPL-2.0
+#define _GNU_SOURCE
+#include <test_progs.h>
+#include <fcntl.h>
+#include <linux/magic.h>
+#include <sys/vfs.h>
+#include <sys/wait.h>
+#include <unistd.h>
+#include <sys/user.h>
+#ifndef PAGE_SIZE /* on some archs it comes in sys/user.h */
+#define PAGE_SIZE getpagesize()
+#endif
+
+#include "cgroup_helpers.h"
+#include "arena_memcg.skel.h"
+
+#define CG_PATH "/arena_memcg"
+
+/* Reclaimable page cache the child builds up before it gets capped. */
+#define RECLAIMABLE (128 * 1024 * 1024)
+/* Headroom left under memory.max, far less than the arena we fault in. */
+#define HEADROOM (8 * 1024 * 1024)
+/* Arena to fault in; it only fits by reclaiming the page cache. */
+#define ARENA_FAULT (64 * 1024 * 1024)
+/* Child exit code for "this environment cannot host the test". */
+#define CHILD_UNSUPPORTED 9
+
+static void dump_memcg(void)
+{
+ char buf[512];
+
+ if (!read_cgroup_file(CG_PATH, "memory.max", buf, sizeof(buf)))
+ fprintf(stderr, "memory.max: %s", buf);
+ if (!read_cgroup_file(CG_PATH, "memory.peak", buf, sizeof(buf)))
+ fprintf(stderr, "memory.peak: %s", buf);
+ if (!read_cgroup_file(CG_PATH, "memory.events", buf, sizeof(buf)))
+ fprintf(stderr, "memory.events:\n%s", buf);
+}
+
+/*
+ * Fill the page cache with @size bytes of clean, reclaimable pages by
+ * reading a sparse temp file, the way the cgroup selftests do. Returns the
+ * fd, which must stay open: closing it drops the cache. Returns -EOPNOTSUPP
+ * if the working directory cannot back such a file, -1 on error.
+ */
+static int alloc_pagecache(size_t size)
+{
+ struct statfs stfs;
+ char buf[4096];
+ size_t off;
+ int fd;
+
+ fd = open(".", O_TMPFILE | O_RDWR | O_EXCL, 0600);
+ if (fd < 0)
+ return errno == EOPNOTSUPP ? -EOPNOTSUPP : -1;
+ /* tmpfs hands out shmem pages, which are not reclaimable without swap */
+ if (fstatfs(fd, &stfs) || stfs.f_type == TMPFS_MAGIC) {
+ close(fd);
+ return -EOPNOTSUPP;
+ }
+ if (ftruncate(fd, size))
+ goto err;
+ for (off = 0; off < size; off += sizeof(buf))
+ if (read(fd, buf, sizeof(buf)) < 0)
+ goto err;
+ return fd;
+err:
+ close(fd);
+ return -1;
+}
+
+void serial_test_arena_memcg(void)
+{
+ int cgroup_fd = -1, status, err;
+ const long ps = PAGE_SIZE;
+ char buf[64];
+ pid_t pid;
+
+ err = setup_cgroup_environment();
+ if (!ASSERT_OK(err, "setup_cgroup_environment"))
+ goto out;
+
+ cgroup_fd = create_and_get_cgroup(CG_PATH);
+ if (!ASSERT_OK_FD(cgroup_fd, "create_and_get_cgroup"))
+ goto out;
+
+ /* No memory controller -> nothing to test. */
+ if (read_cgroup_file(CG_PATH, "memory.current", buf, sizeof(buf))) {
+ fprintf(stderr, "%s:SKIP:no memory controller or other env error\n",
+ __func__);
+ test__skip();
+ goto out;
+ }
+
+ pid = fork();
+ if (!ASSERT_GE(pid, 0, "fork"))
+ goto out;
+ if (pid == 0) {
+ struct arena_memcg *cskel;
+ __u32 i, npages;
+ char *base;
+ size_t sz;
+ long cur;
+ int fd;
+
+ /*
+ * Everything runs in the child: the arena vma is VM_DONTCOPY so
+ * it does not survive fork(), and only the child should be under
+ * the limit. The work dir belongs to the parent, so use the
+ * _parent() helpers; errors come back as an exit code, ASSERT_*
+ * does not reach the parent from here.
+ */
+
+ /* Step 1: join the memcg, so what follows is charged to it. */
+ snprintf(buf, sizeof(buf), "%d", getpid());
+ if (write_cgroup_file_parent(CG_PATH, "cgroup.procs", buf))
+ _exit(2);
+
+ /*
+ * Step 2: load the arena. A map is charged to whoever creates
+ * it, hence joining first.
+ */
+ cskel = arena_memcg__open_and_load();
+ if (!cskel)
+ _exit(3);
+ base = bpf_map__initial_value(cskel->maps.arena, &sz);
+ if (!base)
+ _exit(4);
+ npages = ARENA_FAULT / ps;
+ if (npages > bpf_map__max_entries(cskel->maps.arena))
+ _exit(5);
+
+ /* Step 3: make RECLAIMABLE bytes of clean page cache. */
+ fd = alloc_pagecache(RECLAIMABLE);
+ if (fd == -EOPNOTSUPP)
+ _exit(CHILD_UNSUPPORTED);
+ if (fd < 0)
+ _exit(6);
+
+ /*
+ * Step 4: set memory.max to what we use now plus HEADROOM. The
+ * page cache is already inside the limit, so only HEADROOM is
+ * left.
+ */
+ if (read_cgroup_file_parent(CG_PATH, "memory.current", buf, sizeof(buf)))
+ _exit(7);
+ cur = strtol(buf, NULL, 10);
+ snprintf(buf, sizeof(buf), "%ld", cur + HEADROOM);
+ if (write_cgroup_file_parent(CG_PATH, "memory.max", buf))
+ _exit(8);
+
+ /*
+ * Step 5: fault ARENA_FAULT of arena in, much more than
+ * HEADROOM. Once it hits memory.max every further page has to
+ * come from reclaiming the page cache. With the fix the
+ * fault-in reclaims and all of it succeeds; without it the
+ * allocation cannot reclaim and we die on a valid address.
+ */
+ for (i = 0; i < npages; i++)
+ base[(size_t)i * ps] = 1;
+ _exit(0); /* fd deliberately kept open until here */
+ }
+
+ if (!ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid"))
+ goto out;
+
+ /* The working directory cannot hold a reclaimable page cache. */
+ if (WIFEXITED(status) && WEXITSTATUS(status) == CHILD_UNSUPPORTED) {
+ fprintf(stderr, "%s:SKIP:no disk-backed O_TMPFILE in cwd\n", __func__);
+ test__skip();
+ goto out;
+ }
+
+ /* A non-zero exit means the child failed to set up; the code says where. */
+ if (WIFEXITED(status) && WEXITSTATUS(status)) {
+ ASSERT_OK(WEXITSTATUS(status), "child setup");
+ goto out;
+ }
+
+ /*
+ * With the fix the arena fault-in reclaims the page cache and every
+ * fault succeeds, so the child exits 0. Without it the allocation
+ * cannot reclaim, fails once the headroom is used up, and the child
+ * dies with SIGSEGV on a valid arena address.
+ */
+ if (!ASSERT_TRUE(WIFEXITED(status) && !WEXITSTATUS(status),
+ "child faulted the arena in")) {
+ if (WIFSIGNALED(status))
+ fprintf(stderr, "child killed by signal %d\n", WTERMSIG(status));
+ dump_memcg();
+ }
+out:
+ if (cgroup_fd >= 0)
+ close(cgroup_fd);
+ cleanup_cgroup_environment();
+}
diff --git a/tools/testing/selftests/bpf/progs/arena_memcg.c b/tools/testing/selftests/bpf/progs/arena_memcg.c
new file mode 100644
index 0000000000000..aff73757e7941
--- /dev/null
+++ b/tools/testing/selftests/bpf/progs/arena_memcg.c
@@ -0,0 +1,23 @@
+// SPDX-License-Identifier: GPL-2.0
+
+#include <vmlinux.h>
+#include <bpf/bpf_helpers.h>
+#include "bpf_arena_common.h"
+
+struct {
+ __uint(type, BPF_MAP_TYPE_ARENA);
+ __uint(map_flags, BPF_F_MMAPABLE);
+ /*
+ * Number of pages. Must cover ARENA_FAULT on the smallest page size
+ * (64M/4K = 16384) yet stay under the 4G arena limit on 64K pages
+ * (50000*64K = 3.2G).
+ */
+ __uint(max_entries, 50000);
+#ifdef __TARGET_ARCH_arm64
+ __ulong(map_extra, 0x1ull << 32); /* start of mmap() region */
+#else
+ __ulong(map_extra, 0x1ull << 44); /* start of mmap() region */
+#endif
+} arena SEC(".maps");
+
+char _license[] SEC("license") = "GPL";
--
2.43.0
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock
2026-09-18 8:40 ` [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock Jiayuan Chen
@ 2026-09-18 9:51 ` bot+bpf-ci
2026-09-18 17:59 ` Alexei Starovoitov
1 sibling, 0 replies; 6+ messages in thread
From: bot+bpf-ci @ 2026-09-18 9:51 UTC (permalink / raw)
To: jiayuan.chen, bpf
Cc: jiayuan.chen, emil, ast, daniel, andrii, eddyz87, memxor,
martin.lau, song, yonghong.song, jolsa, ihor.solodrai, shuah,
linux-kernel, linux-kselftest, ast, andrii, daniel, martin.lau,
eddyz87, yonghong.song, mason, ihor.solodrai
[-- Attachment #1: Type: text/plain, Size: 4706 bytes --]
> 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 uses
> __GFP_RETRY_MAYFAIL so it never invokes the OOM killer: the page is
> charged to the map's memcg, which need not be the faulting task's, so
> an OOM there could kill unrelated tasks in the map's cgroup while a
> foreign faulter could never be its victim. Like the other bpf map
> allocators it places the page on the map's numa_node rather than
> following the task's mempolicy; arena memory is shared, so the map's
> node is the right placement policy.
This isn't a bug, but since arena_map_alloc() rejects BPF_F_NUMA_NODE,
map->numa_node is always NUMA_NO_NODE here. Is the numa_node rationale in
the changelog saying more than 'unchanged, still the local node'?
> - 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.
Small changelog nit: the lock-failure path already returned VM_FAULT_SIGBUS
before this patch -- would it be clearer to list only the range-tree/
page-table/allocation failures as the ones changing from VM_FAULT_SIGSEGV?
> - Tidy up the error labels.
>
> Reviewed-by: Emil Tsalapatis <emil@etsalapatis.com>
> Signed-off-by: Jiayuan Chen <jiayuan.chen@linux.dev>
> diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
> index 7b6847200b431..2d0efc0232afd 100644
> --- a/kernel/bpf/arena.c
> +++ b/kernel/bpf/arena.c
[ ... ]
> @@ -489,59 +490,108 @@ 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 so the allocation can sleep and
> + * reclaim, which the non-blocking allocator under
> + * arena->spinlock cannot. __GFP_RETRY_MAYFAIL: reclaim, but
> + * never OOM-kill in the map's memcg, which need not be the
> + * faulting task's. On failure, fallthrough: 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 = alloc_pages_node(map->numa_node,
> + GFP_KERNEL | __GFP_ZERO |
> + __GFP_ACCOUNT | __GFP_NOWARN |
> + __GFP_RETRY_MAYFAIL, 0);
> + 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;
> + }
[ ... ]
---
AI reviewed your patch. Please fix the bug or email reply why it's not a bug.
See: https://github.com/kernel-patches/vmtest/blob/master/ci/claude/README.md
CI run summary: https://github.com/kernel-patches/bpf/actions/runs/35327041280
^ permalink raw reply [flat|nested] 6+ messages in thread
* Re: [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock
2026-09-18 8:40 ` [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock Jiayuan Chen
2026-09-18 9:51 ` bot+bpf-ci
@ 2026-09-18 17:59 ` Alexei Starovoitov
1 sibling, 0 replies; 6+ messages in thread
From: Alexei Starovoitov @ 2026-09-18 17:59 UTC (permalink / raw)
To: Jiayuan Chen, bpf
Cc: Emil Tsalapatis, Daniel Borkmann, Andrii Nakryiko,
Eduard Zingerman, Kumar Kartikeya Dwivedi, Martin KaFai Lau,
Song Liu, Yonghong Song, Jiri Olsa, Ihor Solodrai, Shuah Khan,
linux-kernel, linux-kselftest
On Fri, Sep 18, 2026 at 04:40 PM Jiayuan Chen <jiayuan.chen@linux.dev> wrote:
> @@ -489,59 +490,108 @@ 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 so the allocation can sleep and
> + * reclaim, which the non-blocking allocator under
> + * arena->spinlock cannot. __GFP_RETRY_MAYFAIL: reclaim, but
> + * never OOM-kill in the map's memcg, which need not be the
> + * faulting task's. On failure, fallthrough: 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, an
d the fault path
> + * would just retry it forever.
> + */
I asked for one line comment. This block and the one in !new_page
branch below repeat the commit log. Trim both to a sentence or two.
pw-bot: cr
^ permalink raw reply [flat|nested] 6+ messages in thread
end of thread, other threads:[~2026-09-18 17:59 UTC | newest]
Thread overview: 6+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2026-09-18 8:40 [PATCH bpf-next v8 0/3] bpf: arena: handle memory.max on fault-in with reclaim Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 1/3] bpf: arena: allocate the fault-in page outside the lock Jiayuan Chen
2026-09-18 9:51 ` bot+bpf-ci
2026-09-18 17:59 ` Alexei Starovoitov
2026-09-18 8:40 ` [PATCH bpf-next v8 2/3] selftests/bpf: Add read_cgroup_file() to cgroup_helpers Jiayuan Chen
2026-09-18 8:40 ` [PATCH bpf-next v8 3/3] selftests/bpf: Add a test for arena fault-in under memory.max Jiayuan Chen
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®