[PATCH bpf-next v6 3/3] bpf, arena: check range_tree_set return in arena_free_pages and arena_free_worker

From: chenyuan_fl

Date: Tue Sep 22 2026 - 03:29:49 EST


From: Yuan Chen <chenyuan@xxxxxxxxxx>

range_tree_set() can fail with -ENOMEM, but the arena callers ignored its
return value. In arena_free_worker() it ran after PTE clearing, so a
failed update left the range marked allocated while its pages were
already unmapped and freed.

Check the return value at all three call sites: arena_alloc_pages() warns
if restoring the unpopulated tail of a partial allocation fails,
arena_free_pages() aborts the free, and arena_free_worker() moves
range_tree_set() before PTE clearing and, on failure, drops the span and
leaks the range, reclaimed later by arena_map_free().

Signed-off-by: Yuan Chen <chenyuan@xxxxxxxxxx>
---
kernel/bpf/arena.c | 38 +++++++++++++++++++++++++++++++-------
1 file changed, 31 insertions(+), 7 deletions(-)

diff --git a/kernel/bpf/arena.c b/kernel/bpf/arena.c
index 7b6847200b43..c9f81d08582b 100644
--- a/kernel/bpf/arena.c
+++ b/kernel/bpf/arena.c
@@ -766,7 +766,9 @@ static long arena_alloc_pages(struct bpf_arena *arena, long uaddr, long page_cnt
bpf_map_memcg_exit(old_memcg, new_memcg);
return clear_lo32(arena->user_vm_start) + uaddr32;
out:
- range_tree_set(&arena->rt, pgoff + mapped, page_cnt - mapped);
+ if (range_tree_set(&arena->rt, pgoff + mapped, page_cnt - mapped))
+ pr_warn_ratelimited("bpf_arena: leak range %ld+%ld on failed alloc\n",
+ pgoff + mapped, page_cnt - mapped);
raw_res_spin_unlock_irqrestore(&arena->spinlock, flags);
if (mapped) {
flush_vmap_cache(kern_vm_start + uaddr32, mapped << PAGE_SHIFT);
@@ -881,7 +883,18 @@ static void arena_free_pages(struct bpf_arena *arena, long uaddr, long page_cnt,
if (ret)
goto defer;

- range_tree_set(&arena->rt, pgoff, page_cnt);
+ ret = range_tree_set(&arena->rt, pgoff, page_cnt);
+ if (ret) {
+ /*
+ * range_tree_set() is failure-atomic: on -ENOMEM the range
+ * stays allocated and its pages mapped. Abort the free
+ * instead of unmapping pages the tree does not track; the
+ * program can free the range again later.
+ */
+ raw_res_spin_unlock_irqrestore(&arena->spinlock, flags);
+ bpf_map_memcg_exit(old_memcg, new_memcg);
+ return;
+ }

init_llist_head(&free_pages);
cdata.arena = arena;
@@ -977,7 +990,7 @@ static void arena_free_worker(struct work_struct *work)
struct llist_node *list, *pos, *t;
struct arena_free_span *s;
u64 arena_vm_start, user_vm_start;
- struct llist_head free_pages;
+ struct llist_head free_pages, cleared;
struct clear_range_data cdata;
struct page *page;
unsigned long full_uaddr;
@@ -992,28 +1005,39 @@ static void arena_free_worker(struct work_struct *work)
bpf_map_memcg_enter(&arena->map, &old_memcg, &new_memcg);

init_llist_head(&free_pages);
+ init_llist_head(&cleared);
cdata.arena = arena;
cdata.free_pages = &free_pages;
arena_vm_start = bpf_arena_get_kern_vm_start(arena);
user_vm_start = bpf_arena_get_user_vm_start(arena);

list = llist_del_all(&arena->free_spans);
- llist_for_each(pos, list) {
+ llist_for_each_safe(pos, t, list) {
s = llist_entry(pos, struct arena_free_span, node);
page_cnt = s->page_cnt;
kaddr = arena_vm_start + s->uaddr;
pgoff = compute_pgoff(arena, s->uaddr);

+ /*
+ * Mark the range free before clearing PTEs so a failed
+ * update keeps the tree and the PTEs consistent. On failure
+ * leak the range, reclaimed later by arena_map_free(); only
+ * spans on @cleared reach the flush/zap/release loop below.
+ */
+ if (range_tree_set(&arena->rt, pgoff, page_cnt)) {
+ kfree_nolock(s);
+ continue;
+ }
+
/* clear ptes and collect pages in free_pages llist */
apply_to_existing_page_range(&init_mm, kaddr, page_cnt << PAGE_SHIFT,
apply_range_clear_cb, &cdata);
-
- range_tree_set(&arena->rt, pgoff, page_cnt);
+ llist_add(&s->node, &cleared);
}
raw_res_spin_unlock_irqrestore(&arena->spinlock, flags);

/* Iterate the list again without holding spinlock to do the tlb flush and zap_pages */
- llist_for_each_safe(pos, t, list) {
+ llist_for_each_safe(pos, t, cleared.first) {
s = llist_entry(pos, struct arena_free_span, node);
page_cnt = s->page_cnt;
full_uaddr = clear_lo32(user_vm_start) + s->uaddr;
--
2.54.0