Re: [PATCH bpf-next 3/3] selftests/bpf: Add a test for arena fault-in under memory.max
"Emil Tsalapatis" <[email protected]> Mon, 27 Jul 2026 20:54:56 -0400
| Newsgroups | dev.linux.lists.linux-rt-devel,org.kernel.vger.bpf,org.kernel.vger.linux-kernel,org.kernel.vger.linux-kselftest |
|---|---|
| Message-ID | <[email protected]> |
On Mon Jul 27, 2026 at 2:24 AM EDT, Jiayuan Chen wrote: > A child joins a memcg capped at 64M and faults an arena in until it runs > out of the budget. Without the kernel fix the child dies with SIGSEGV on > a valid arena address; with it, the child is killed by the memcg OOM > killer. > > With the fix: > > serial_test_arena_memcg:PASS:child killed by signal > serial_test_arena_memcg:PASS:not killed by SIGSEGV > #5 arena_memcg:OK > > # dmesg > arena_vm_fault+0x655/0xa90 > Memory cgroup out of memory: Killed process 512, file-rss:67920kB > > Without the fix: > > serial_test_arena_memcg:PASS:child killed by signal > serial_test_arena_memcg:FAIL:not killed by SIGSEGV: actual 11 > #5 arena_memcg:FAIL > > # dmesg > test_progs[508]: segfault at 100004025000 ... > > Signed-off-by: Jiayuan Chen <[email protected]> > --- > .../selftests/bpf/prog_tests/arena_memcg.c | 158 ++++++++++++++++++ > .../testing/selftests/bpf/progs/arena_memcg.c | 24 +++ > 2 files changed, 182 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 000000000000..9665946fa29e > --- /dev/null > +++ b/tools/testing/selftests/bpf/prog_tests/arena_memcg.c > @@ -0,0 +1,158 @@ > +// SPDX-License-Identifier: GPL-2.0 > + > +#include <test_progs.h> > +#include <fcntl.h> > +#include <signal.h> > +#include <sys/mman.h> > +#include <sys/wait.h> > +#include <unistd.h> > +#include <sys/user.h> > +#ifndef PAGE_SIZE /* on some archs it comes in sys/user.h */ > +#include <unistd.h> > +#define PAGE_SIZE getpagesize() > +#endif > + > +#include "cgroup_helpers.h" > +#include "arena_memcg.skel.h" > + > +#define CG_PATH "/arena_memcg" > + > +/* Budget the arena gets on top of whatever is already charged after loa= d. */ > +#define ARENA_BUDGET (64 * 1024 * 1024) > + > +/* > + * cgroup_helpers builds paths from getpid(), but the work dir belongs t= o the > + * process that set the environment up. The child references it through = that > + * pid, so build the path explicitly. > + */ > +static void cg_file_path(char *buf, size_t sz, pid_t owner, const char *= file) > +{ > + snprintf(buf, sz, "/mnt/cgroup-test-work-dir%d%s/%s", owner, CG_PATH, f= ile); These are copied over from cgroup_helpers.c, but it's not obvious they originate from there. Maybe let's move them to cgroup_helpers.h where we can use them everywhere for consistency. The main issue I see is that this only triggers consistently with PREEMPT_R= T, correct? I tried with the default vmtest config we have but it does not trigger at all. More importantly, it doesn't trigger reliably with PREEMPT_RT, either. Does it for you? Can we cycle forks/frees multiple times to try and trigger this more reliably? > +} > + > +static long cg_read_ulong(pid_t owner, const char *file) > +{ > + char path[PATH_MAX], buf[64]; > + long val =3D -1; > + FILE *f; > + > + cg_file_path(path, sizeof(path), owner, file); > + f =3D fopen(path, "r"); > + if (!f) > + return -1; > + if (fgets(buf, sizeof(buf), f)) > + val =3D strtol(buf, NULL, 10); > + fclose(f); > + return val; > +} > + > +static int cg_write(pid_t owner, const char *file, const char *val) > +{ > + char path[PATH_MAX]; > + int fd, len, ret =3D -1; > + > + cg_file_path(path, sizeof(path), owner, file); > + fd =3D open(path, O_WRONLY); > + if (fd < 0) > + return -1; > + len =3D strlen(val); > + if (write(fd, val, len) =3D=3D len) > + ret =3D 0; > + close(fd); > + return ret; > +} > + > +void serial_test_arena_memcg(void) > +{ > + int cgroup_fd =3D -1, status; > + const long ps =3D PAGE_SIZE; > + pid_t owner, pid; > + > + if (setup_cgroup_environment()) > + return; > + owner =3D getpid(); > + > + cgroup_fd =3D 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 (cg_read_ulong(owner, "memory.current") < 0) { > + test__skip(); > + goto out; > + } > + > + pid =3D fork(); > + if (!ASSERT_GE(pid, 0, "fork")) > + goto out; > + if (pid =3D=3D 0) { > + struct arena_memcg *cskel; > + __u32 i, npages; > + char buf[32]; > + char *base; > + size_t sz; > + long cur; > + > + /* > + * Do everything from the child: the arena vma is VM_DONTCOPY so > + * it would not survive fork(), only the child should be under the > + * limit so that a memcg OOM cannot pick test_progs, and a map is > + * charged to the memcg of the task that creates it - so join > + * before load. Errors are reported to the parent through the exit > + * code, since ASSERT_* in a forked child does not reach it. > + */ > + snprintf(buf, sizeof(buf), "%d", getpid()); > + if (cg_write(owner, "cgroup.procs", buf)) > + _exit(2); > + > + cskel =3D arena_memcg__open_and_load(); > + if (!cskel) > + _exit(3); > + > + base =3D bpf_map__initial_value(cskel->maps.arena, &sz); > + if (!base) > + _exit(4); > + npages =3D bpf_map__max_entries(cskel->maps.arena); > + > + /* > + * Cap only now, after load: everything but the fault-in is > + * charged, so the arena gets a fixed budget regardless of what > + * the load itself cost, and the load can never hit the limit. > + */ > + cur =3D cg_read_ulong(owner, "memory.current"); > + if (cur < 0) > + _exit(5); > + snprintf(buf, sizeof(buf), "%ld", cur + ARENA_BUDGET); > + if (cg_write(owner, "memory.max", buf)) > + _exit(6); > + > + for (i =3D 0; i < npages; i++) > + base[(size_t)i * ps] =3D 1; > + _exit(0); > + } > + > + if (!ASSERT_EQ(waitpid(pid, &status, 0), pid, "waitpid")) > + goto out; > + > + /* A non-zero exit means the child failed to set up; the code says wher= e. */ > + if (WIFEXITED(status) && WEXITSTATUS(status)) { > + ASSERT_OK(WEXITSTATUS(status), "child setup"); > + goto out; > + } > + > + /* > + * Faulting a valid arena address until memory.max is hit must not look > + * like an invalid access. Without the fix the fault path allocated wit= h > + * the non-blocking allocator, turned its -ENOMEM into VM_FAULT_SIGSEGV= , > + * and the child died with SIGSEGV on a valid address; now it is handle= d > + * by the memcg OOM path and the child is killed by SIGKILL instead. A > + * clean exit means the child failed to set up (see the exit codes). > + */ > + if (!ASSERT_TRUE(WIFSIGNALED(status), "child killed by signal")) > + goto out; > + ASSERT_NEQ(WTERMSIG(status), SIGSEGV, "not killed by SIGSEGV"); > +out: > + if (cgroup_fd >=3D 0) > + close(cgroup_fd); > + cleanup_cgroup_environment(); > +} > diff --git a/tools/testing/selftests/bpf/progs/arena_memcg.c b/tools/test= ing/selftests/bpf/progs/arena_memcg.c > new file mode 100644 > index 000000000000..adecd9e8463e > --- /dev/null > +++ b/tools/testing/selftests/bpf/progs/arena_memcg.c > @@ -0,0 +1,24 @@ > +// 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); > + __uint(max_entries, 100000); /* number of pages */ > +#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"); > + > +SEC("syscall") > +int noop(void *ctx) > +{ > + return 0; > +} > + > +char _license[] SEC("license") =3D "GPL";