From 92b5ce3ca5b7c641e2678f92419aa7833951bc9d Mon Sep 17 00:00:00 2001 From: Alexei Starovoitov Date: Mon, 2 Nov 2026 06:42:30 +0000 Subject: [PATCH 1/2] bpf: Don't move a MAP_FIXED address in arena_get_unmapped_area() The user range of an arena must not cross a 4 GiB boundary. Thus arena_get_unmapped_area() asks the mm for 2 * len bytes and rounds the result up if necessary. This is correct only if the mm finds a free range. With MAP_FIXED the mm does no search and returns addr. For MAP_FIXED_NOREPLACE, do_mmap() also sets MAP_FIXED. With MAP_FIXED the arena is not at addr after round_up(). Thus mmap() removes mappings outside the range from addr to addr + len. It keeps a part of those in this range. Also, round_up() can wrap to address 0, and the test for address 0 is before it. The arch code gets twice the length of the mapping. Thus it can refuse a valid MAP_FIXED range. On s390 the arch code also grows the page table for the range. With the double length it can omit this step for a valid range. The test for a crossing is not exact. Because ret is a long, ret >> 32 extends the sign, but the other side is unsigned. The sides are never equal for an address with bit 63 set, and round_up() always runs. Such user addresses exist on s390. arena_map_alloc() does the same test with a u64. With MAP_FIXED, pass len to the mm. Return -EINVAL if the range crosses a boundary, as for address 0. Make ret unsigned, so that only these ranges get the error. On x86-64, if round_up() gives address 0 today, mmap() now fails with -EINVAL or with -ENOMEM from the mm. Today mmap() can also fail for the address after round_up(), for example with -ENOMEM, -EEXIST, -EPERM or -EBUSY. Without MAP_FIXED the mm must still find a free range of 2 * len bytes inside the address space of the task. The range after round_up() is inside it, and round_up() cannot wrap. An address with bit 63 set now moves only if its range crosses a boundary. For an arena, libbpf uses MAP_FIXED only with map_extra, and arena_map_alloc() refuses a map_extra range that crosses a boundary. A subsequent mmap() must use the range of the first mmap(). For mmap() of the map fd, this range comes from arena_get_unmapped_area(), thus it crosses no boundary. arena_map_alloc() still accepts a map_extra above TASK_SIZE, and mmap() of such an arena fails. Fixes: 317460317a02 ("bpf: Introduce bpf_arena.") Signed-off-by: Alexei Starovoitov Link: https://patch.msgid.link/20261102064231.2153879-2-alexei.starovoitov@gmail.com --- kernel/bpf/arena.c | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c index 0b1ec3306ea4..ec2bda1a6ebf 100644 --- a/kernel/bpf/arena.c +++ b/kernel/bpf/arena.c @@ -7,6 +7,7 @@ #include #include "linux/filter.h" #include +#include #include #include #include @@ -630,7 +631,7 @@ static unsigned long arena_get_unmapped_area(struct file *filp, unsigned long ad { struct bpf_map *map = filp->private_data; struct bpf_arena *arena = container_of(map, struct bpf_arena, map); - long ret; + unsigned long ret; if (pgoff) return -EINVAL; @@ -647,7 +648,13 @@ static unsigned long arena_get_unmapped_area(struct file *filp, unsigned long ad return -EINVAL; } - ret = mm_get_unmapped_area(filp, addr, len * 2, 0, flags); + /* + * Without MAP_FIXED the mm finds a free range. Ask for twice the + * length, so that there is room for round_up(). With MAP_FIXED the mm + * does no search, and the caller wants this address. Thus the range + * cannot move. + */ + ret = mm_get_unmapped_area(filp, addr, flags & MAP_FIXED ? len : len * 2, 0, flags); if (IS_ERR_VALUE(ret)) return ret; if (!ret) @@ -655,6 +662,8 @@ static unsigned long arena_get_unmapped_area(struct file *filp, unsigned long ad return -EINVAL; if ((ret >> 32) == ((ret + len - 1) >> 32)) return ret; + if (flags & MAP_FIXED) + return -EINVAL; return round_up(ret, SZ_4G); } From c08b50dd174a1dadb67f215239664c9096a5c4bd Mon Sep 17 00:00:00 2001 From: Alexei Starovoitov Date: Mon, 2 Nov 2026 06:42:31 +0000 Subject: [PATCH 2/2] selftests/bpf: Run arena_mmap serially and test MAP_FIXED addresses Each arena reserves 4 GiB of vmalloc space. The KASAN shadow of arenas from parallel tests can exhaust the memory of a CI VM. Thus commit a14f9ba080c1 ("selftests/bpf: Run arena tests serially") made arena_* and libarena* serial. Do the same for arena_mmap, which the commit below added later. Add four subtests for "bpf: Don't move a MAP_FIXED address in arena_get_unmapped_area()". Test that mmap() with MAP_FIXED or MAP_FIXED_NOREPLACE fails with EINVAL if the range crosses a 4 GiB boundary. Test that mmap() with MAP_FIXED also fails for a range that wraps and for a map_extra range that ends above TASK_SIZE. Today arena_map_alloc() accepts such a map_extra. map_extra_last_pages also passes if the kernel refuses the map, because no task can map this range. The arch code gets a range before the arena code tests it for a crossing. For the last two ranges LoongArch returns -EINVAL, and powerpc with the hash MMU returns -ENOMEM. There the two subtests do not reach that test. On the other architectures with arenas, the arena code returns -EINVAL or __get_unmapped_area() returns -ENOMEM. Thus the two subtests accept both errnos. All these errors occur before mmap() changes a mapping. Two other changes of that patch have no subtest. With MAP_FIXED the arch code gets len and not 2 * len. A 64-bit task on x86, arm64 or riscv does not see this. LoongArch, s390 and powerpc with the hash MMU compare the end of the range with different limits. In arena_mmap the unsigned ret changes only the errno of map_extra_last_pages, which accepts both. Fixes: 14d3e5bf9ffe ("selftests/bpf: Test the length and the address of an arena mmap()") Signed-off-by: Alexei Starovoitov Link: https://patch.msgid.link/20261102064231.2153879-3-alexei.starovoitov@gmail.com --- .../selftests/bpf/prog_tests/arena_mmap.c | 36 +++++++++++++------ 1 file changed, 26 insertions(+), 10 deletions(-) diff --git a/tools/testing/selftests/bpf/prog_tests/arena_mmap.c b/tools/testing/selftests/bpf/prog_tests/arena_mmap.c index 2babc9ecf182..f55d950720f1 100644 --- a/tools/testing/selftests/bpf/prog_tests/arena_mmap.c +++ b/tools/testing/selftests/bpf/prog_tests/arena_mmap.c @@ -5,9 +5,12 @@ #define ARENA_PAGES 8 -static void test_refused(int pages, int flags, int err) +/* err == 0: the range ends above TASK_SIZE. EINVAL or ENOMEM, this depends on the arch */ +static void test_refused(__u64 map_extra, __u64 addr, int pages, int flags, int err) { - LIBBPF_OPTS(bpf_map_create_opts, opts, .map_flags = BPF_F_MMAPABLE); + LIBBPF_OPTS(bpf_map_create_opts, opts, + .map_flags = BPF_F_MMAPABLE, + .map_extra = map_extra); void *area; int fd; @@ -16,22 +19,35 @@ static void test_refused(int pages, int flags, int err) test__skip(); return; } + /* No task can map this range, so the kernel can also refuse the map. */ + if (fd < 0 && map_extra) + return; if (!ASSERT_GE(fd, 0, "bpf_map_create")) return; - area = mmap(NULL, pages * getpagesize(), PROT_READ | PROT_WRITE, - MAP_SHARED | flags, fd, 0); - if (ASSERT_EQ(area, MAP_FAILED, "mmap")) - ASSERT_EQ(errno, err, "errno"); - else + area = mmap((void *)(unsigned long)addr, pages * getpagesize(), + PROT_READ | PROT_WRITE, MAP_SHARED | flags, fd, 0); + if (!ASSERT_EQ(area, MAP_FAILED, "mmap")) munmap(area, pages * getpagesize()); + else if (err || errno != ENOMEM) + ASSERT_EQ(errno, err ?: EINVAL, "errno"); close(fd); } -void test_arena_mmap(void) +void serial_test_arena_mmap(void) { + __u64 page = getpagesize(), last = -(ARENA_PAGES * page); + if (test__start_subtest("long")) - test_refused(ARENA_PAGES + 1, 0, E2BIG); + test_refused(0, 0, ARENA_PAGES + 1, 0, E2BIG); if (test__start_subtest("address_0")) - test_refused(ARENA_PAGES, MAP_FIXED, EINVAL); + test_refused(0, 0, ARENA_PAGES, MAP_FIXED, EINVAL); + if (test__start_subtest("crosses_4g")) + test_refused(0, (1ULL << 32) - page, ARENA_PAGES, MAP_FIXED, EINVAL); + if (test__start_subtest("crosses_4g_noreplace")) + test_refused(0, (1ULL << 32) - page, ARENA_PAGES, MAP_FIXED_NOREPLACE, EINVAL); + if (test__start_subtest("wraps")) + test_refused(0, -page, ARENA_PAGES, MAP_FIXED, 0); + if (test__start_subtest("map_extra_last_pages")) + test_refused(last, last, ARENA_PAGES, MAP_FIXED, 0); }